add physics tests that verify the status quo

This commit is contained in:
Marcel Märtens 2022-05-19 23:51:29 +02:00
parent 35f9c5cbdf
commit 997b330f19
6 changed files with 451 additions and 0 deletions

1
Cargo.lock generated
View File

@ -6511,6 +6511,7 @@ dependencies = [
"veloren-common-base",
"veloren-common-ecs",
"veloren-common-net",
"veloren-common-state",
]
[[package]]

View File

@ -36,6 +36,15 @@ impl MaterialStatManifest {
pub fn armor_stats(&self, key: &str) -> Option<armor::Stats> {
self.armor_stats.get(key).copied()
}
#[doc(hidden)]
/// needed for tests to load it without actual assets
pub fn with_empty() -> Self {
Self {
tool_stats: hashbrown::HashMap::default(),
armor_stats: hashbrown::HashMap::default(),
}
}
}
// This could be a Compound that also loads the keys, but the RecipeBook

View File

@ -31,3 +31,7 @@ specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "s
# Tweak running code
# inline_tweak = { version = "1.0.8", features = ["release_tweak"] }
[dev-dependencies]
# Setup a State
common-state = { package = "veloren-common-state", path = "../state" }

View File

@ -0,0 +1,296 @@
use crate::utils;
use approx::assert_relative_eq;
use common::{comp::Controller, resources::Time};
use specs::WorldExt;
use std::error::Error;
use utils::{DT, DT_F64, EPSILON};
use vek::{approx, Vec2, Vec3};
use veloren_common_systems::add_local_systems;
#[test]
fn simple_run() {
let mut state = utils::setup();
utils::create_player(&mut state);
state.tick(
DT,
|dispatcher_builder| {
add_local_systems(dispatcher_builder);
},
false,
);
}
#[test]
fn dont_fall_outside_world() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
{
let mut storage = state.ecs_mut().write_storage::<common::comp::Pos>();
storage
.insert(p1, common::comp::Pos(Vec3::new(1000.0, 1000.0, 265.0)))
.unwrap();
}
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 1000.0);
assert_relative_eq!(pos.0.y, 1000.0);
assert_relative_eq!(pos.0.z, 265.0);
assert_eq!(vel.0, vek::Vec3::zero());
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 1000.0);
assert_relative_eq!(pos.0.y, 1000.0);
assert_relative_eq!(pos.0.z, 265.0);
assert_eq!(vel.0, vek::Vec3::zero());
Ok(())
}
#[test]
fn fall_simple() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.0);
assert_relative_eq!(pos.0.y, 16.0);
assert_relative_eq!(pos.0.z, 265.0);
assert_eq!(vel.0, vek::Vec3::zero());
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.0);
assert_relative_eq!(pos.0.y, 16.0);
assert_relative_eq!(pos.0.z, 264.9975, epsilon = EPSILON);
assert_relative_eq!(vel.0.z, -0.25, epsilon = EPSILON);
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.z, 264.9925, epsilon = EPSILON);
assert_relative_eq!(vel.0.z, -0.49969065, epsilon = EPSILON);
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.z, 264.985, epsilon = EPSILON);
assert_relative_eq!(vel.0.z, -0.7493813, epsilon = EPSILON);
utils::tick(&mut state, DT * 7);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(state.ecs_mut().read_resource::<Time>().0, DT_F64 * 10.0);
assert_relative_eq!(pos.0.z, 264.8102, epsilon = EPSILON);
assert_relative_eq!(vel.0.z, -2.4969761, epsilon = EPSILON);
Ok(())
}
#[test]
/// will fall in 20 x DT and 2 x 10*DT steps. compare the end result and make
/// log the "error" between both caluclation
fn fall_dt_speed_diff() -> Result<(), Box<dyn Error>> {
let mut sstate = utils::setup();
let mut fstate = utils::setup();
let sp1 = utils::create_player(&mut sstate);
let fp1 = utils::create_player(&mut fstate);
for _ in 0..10 {
utils::tick(&mut sstate, DT);
}
utils::tick(&mut fstate, DT * 10);
let (spos, svel, _) = utils::get_transform(&sstate, sp1)?;
let (fpos, fvel, _) = utils::get_transform(&fstate, fp1)?;
assert_relative_eq!(spos.0.x, 16.0);
assert_relative_eq!(spos.0.y, 16.0);
assert_relative_eq!(spos.0.z, 264.86267, epsilon = EPSILON);
assert_relative_eq!(svel.0.z, -2.496151, epsilon = EPSILON);
assert_relative_eq!(fpos.0.x, 16.0);
assert_relative_eq!(fpos.0.y, 16.0);
assert_relative_eq!(fpos.0.z, 264.75, epsilon = EPSILON);
assert_relative_eq!(fvel.0.z, -2.5, epsilon = EPSILON);
assert_relative_eq!((spos.0.z - fpos.0.z).abs(), 0.1126709, epsilon = EPSILON);
assert_relative_eq!((svel.0.z - fvel.0.z).abs(), 0.0038490295, epsilon = EPSILON);
for _ in 0..10 {
utils::tick(&mut sstate, DT);
}
utils::tick(&mut fstate, DT * 10);
let (spos, svel, _) = utils::get_transform(&sstate, sp1)?;
let (fpos, fvel, _) = utils::get_transform(&fstate, fp1)?;
assert_relative_eq!(spos.0.x, 16.0);
assert_relative_eq!(spos.0.y, 16.0);
assert_relative_eq!(spos.0.z, 264.47607, epsilon = EPSILON);
assert_relative_eq!(svel.0.z, -4.9847627, epsilon = EPSILON);
assert_relative_eq!(fpos.0.x, 16.0);
assert_relative_eq!(fpos.0.y, 16.0);
assert_relative_eq!(fpos.0.z, 264.25073, epsilon = EPSILON);
assert_relative_eq!(fvel.0.z, -4.9930925, epsilon = EPSILON);
// Diff after 200ms
assert_relative_eq!((spos.0.z - fpos.0.z).abs(), 0.2253418, epsilon = EPSILON);
assert_relative_eq!((svel.0.z - fvel.0.z).abs(), 0.008329868, epsilon = EPSILON);
Ok(())
}
#[test]
fn walk_simple() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
for _ in 0..100 {
utils::tick(&mut state, DT);
}
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.z, 257.0); // make sure it landed on ground
assert_eq!(vel.0, vek::Vec3::zero());
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(1.0, 0.0);
utils::set_control(&mut state, p1, actions)?;
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.01, epsilon = EPSILON);
assert_relative_eq!(pos.0.y, 16.0);
assert_relative_eq!(pos.0.z, 257.0);
assert_relative_eq!(vel.0.x, 0.90703666, epsilon = EPSILON);
assert_relative_eq!(vel.0.y, 0.0);
assert_relative_eq!(vel.0.z, 0.0);
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.029068, epsilon = EPSILON);
assert_relative_eq!(vel.0.x, 1.7296565, epsilon = EPSILON);
utils::tick(&mut state, DT);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.05636, epsilon = EPSILON);
assert_relative_eq!(vel.0.x, 2.4756372, epsilon = EPSILON);
for _ in 0..8 {
utils::tick(&mut state, DT);
}
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.492111, epsilon = EPSILON);
assert_relative_eq!(vel.0.x, 6.411994, epsilon = EPSILON);
Ok(())
}
#[test]
fn walk_max() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
for x in 2..30 {
utils::generate_chunk(&mut state, Vec2::new(x, 0));
}
let p1 = utils::create_player(&mut state);
for _ in 0..100 {
utils::tick(&mut state, DT);
}
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(1.0, 0.0);
utils::set_control(&mut state, p1, actions)?;
for _ in 0..500 {
utils::tick(&mut state, DT);
}
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 68.40794, epsilon = EPSILON);
assert_relative_eq!(vel.0.x, 9.695188, epsilon = EPSILON);
for _ in 0..100 {
utils::tick(&mut state, DT);
}
let (_, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(vel.0.x, 9.695188, epsilon = EPSILON);
Ok(())
}
#[test]
/// will run in 20 x DT and 2 x 10*DT steps. compare the end result and make
/// log the "error" between both caluclation
fn walk_dt_speed_diff() -> Result<(), Box<dyn Error>> {
let mut sstate = utils::setup();
let mut fstate = utils::setup();
let sp1 = utils::create_player(&mut sstate);
let fp1 = utils::create_player(&mut fstate);
for _ in 0..100 {
utils::tick(&mut sstate, DT);
utils::tick(&mut fstate, DT);
}
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(1.0, 0.0);
utils::set_control(&mut sstate, sp1, actions.clone())?;
utils::set_control(&mut fstate, fp1, actions)?;
for _ in 0..10 {
utils::tick(&mut sstate, DT);
}
utils::tick(&mut fstate, DT * 10);
let (spos, svel, _) = utils::get_transform(&sstate, sp1)?;
let (fpos, fvel, _) = utils::get_transform(&fstate, fp1)?;
assert_relative_eq!(spos.0.x, 16.421423, epsilon = EPSILON);
assert_relative_eq!(spos.0.y, 16.0);
assert_relative_eq!(spos.0.z, 257.0);
assert_relative_eq!(svel.0.x, 6.071788, epsilon = EPSILON);
assert_relative_eq!(fpos.0.x, 16.993896, epsilon = EPSILON);
assert_relative_eq!(fpos.0.y, 16.0);
assert_relative_eq!(fpos.0.z, 257.0);
assert_relative_eq!(fvel.0.x, 3.7484815, epsilon = EPSILON);
assert_relative_eq!((spos.0.x - fpos.0.x).abs(), 0.5724735, epsilon = EPSILON);
assert_relative_eq!((svel.0.x - fvel.0.x).abs(), 2.3233063, epsilon = EPSILON);
for _ in 0..10 {
utils::tick(&mut sstate, DT);
}
utils::tick(&mut fstate, DT * 10);
let (spos, svel, _) = utils::get_transform(&sstate, sp1)?;
let (fpos, fvel, _) = utils::get_transform(&fstate, fp1)?;
assert_relative_eq!(spos.0.x, 17.248621, epsilon = EPSILON);
assert_relative_eq!(svel.0.x, 8.344364, epsilon = EPSILON);
assert_relative_eq!(fpos.0.x, 18.357212, epsilon = EPSILON);
assert_relative_eq!(fvel.0.x, 5.1417327, epsilon = EPSILON);
// Diff after 200ms
assert_relative_eq!((spos.0.x - fpos.0.x).abs(), 1.1085911, epsilon = EPSILON);
assert_relative_eq!((svel.0.x - fvel.0.x).abs(), 3.2026315, epsilon = EPSILON);
Ok(())
}
#[test]
fn cant_run_during_fall() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(1.0, 0.0);
utils::set_control(&mut state, p1, actions)?;
utils::tick(&mut state, DT * 2);
let (pos, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(pos.0.x, 16.0);
assert_relative_eq!(pos.0.y, 16.0);
assert_relative_eq!(vel.0.x, 0.0);
assert_relative_eq!(vel.0.y, 0.0);
utils::tick(&mut state, DT * 2);
let (_, vel, _) = utils::get_transform(&state, p1)?;
assert_relative_eq!(state.ecs_mut().read_resource::<Time>().0, DT_F64 * 4.0);
assert_relative_eq!(pos.0.x, 16.0);
assert_relative_eq!(pos.0.y, 16.0);
assert_relative_eq!(vel.0.x, 0.04999693, epsilon = EPSILON);
assert_relative_eq!(vel.0.y, 0.0, epsilon = EPSILON);
Ok(())
}

View File

@ -0,0 +1,2 @@
mod basic;
mod utils;

View File

@ -0,0 +1,139 @@
use common::{
comp::{
inventory::item::MaterialStatManifest,
skills::{GeneralSkill, Skill},
Auras, Buffs, CharacterState, Collider, Combo, Controller, Energy, Health, Ori, Pos, Stats,
Vel,
},
resources::{DeltaTime, GameMode, Time},
skillset_builder::SkillSetBuilder,
terrain::{Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainGrid},
};
use common_ecs::{dispatch, System};
use common_net::sync::WorldSyncExt;
use common_state::State;
use rand::{prelude::*, rngs::SmallRng};
use specs::{Builder, Entity, WorldExt};
use std::{error::Error, sync::Arc, time::Duration};
use vek::{Rgb, Vec2, Vec3};
use veloren_common_systems::{character_behavior, phys};
pub const EPSILON: f32 = 0.00002;
const DT_MILLIS: u64 = 10;
const MILLIS_PER_SEC: f64 = 1_000.0;
pub const DT: Duration = Duration::from_millis(DT_MILLIS);
pub const DT_F64: f64 = DT_MILLIS as f64 / MILLIS_PER_SEC;
pub fn setup() -> State {
let mut state = State::new(GameMode::Server);
state.ecs_mut().insert(MaterialStatManifest::with_empty());
state.ecs_mut().read_resource::<Time>();
state.ecs_mut().read_resource::<DeltaTime>();
state.ecs_mut().insert(TerrainGrid::new());
for x in 0..2 {
for y in 0..2 {
generate_chunk(&mut state, Vec2::new(x, y));
}
}
state
}
pub fn tick(state: &mut State, dt: Duration) {
state.tick(
dt,
|dispatch_builder| {
dispatch::<character_behavior::Sys>(dispatch_builder, &[]);
dispatch::<phys::Sys>(dispatch_builder, &[&character_behavior::Sys::sys_name()]);
},
false,
);
}
pub fn set_control(
state: &mut State,
entity: Entity,
control: Controller,
) -> Result<(), specs::error::Error> {
let mut storage = state.ecs_mut().write_storage::<Controller>();
storage.insert(entity, control).map(|_| ())
}
pub fn get_transform(state: &State, entity: Entity) -> Result<(Pos, Vel, Ori), Box<dyn Error>> {
let storage = state.ecs().read_storage::<Pos>();
let pos = *storage
.get(entity)
.ok_or("Storage does not contain Entity Pos")?;
let storage = state.ecs().read_storage::<Vel>();
let vel = *storage
.get(entity)
.ok_or("Storage does not contain Entity Vel")?;
let storage = state.ecs().read_storage::<Ori>();
let ori = *storage
.get(entity)
.ok_or("Storage does not contain Entity Ori")?;
Ok((pos, vel, ori))
}
pub fn create_player(state: &mut State) -> Entity {
let body = common::comp::Body::Humanoid(common::comp::humanoid::Body::random_with(
&mut thread_rng(),
&common::comp::humanoid::Species::Human,
));
let (p0, p1, radius) = body.sausage();
let collider = Collider::CapsulePrism {
p0,
p1,
radius,
z_min: 0.0,
z_max: body.height(),
};
let skill_set = SkillSetBuilder::default().build();
state
.ecs_mut()
.create_entity_synced()
.with(Pos(Vec3::new(16.0, 16.0, 265.0)))
.with(Vel::default())
.with(Ori::default())
.with(body.mass())
.with(body.density())
.with(collider)
.with(body)
.with(Controller::default())
.with(CharacterState::default())
.with(Buffs::default())
.with(Combo::default())
.with(Auras::default())
.with(Energy::new(
body,
skill_set
.skill_level(Skill::General(GeneralSkill::EnergyIncrease))
.unwrap_or(0),
))
.with(Health::new(body, body.base_health()))
.with(skill_set)
.with(Stats::empty())
.build()
}
pub fn generate_chunk(state: &mut State, chunk_pos: Vec2<i32>) {
let (x, y) = chunk_pos.map(|e| e.to_le_bytes()).into_tuple();
let mut rng = SmallRng::from_seed([
x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3], x[0], x[1], x[2], x[3], y[0], y[1], y[2],
y[3], x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3], x[0], x[1], x[2], x[3], y[0], y[1],
y[2], y[3],
]);
let height = rng.gen::<i32>() % 8;
state.ecs().write_resource::<TerrainGrid>().insert(
chunk_pos,
Arc::new(TerrainChunk::new(
256 + if rng.gen::<u8>() < 64 { height } else { 0 },
Block::new(BlockKind::Grass, Rgb::new(11, 102, 35)),
Block::air(SpriteKind::Empty),
TerrainChunkMeta::void(),
)),
);
}