Validate a physics change
Physics changes are easier to reason about when the test states the input geometry, requested motion, and allowed result. Start with a small deterministic test of the math or shape query. Add a world-level check when block storage, entity lifecycle, or scheduled updates are part of the behavior.
Test geometry without a running world
Section titled “Test geometry without a running world”SystemValidation.Phys.cs uses fixed shapes through ICollisionGetter so collision math can be exercised without constructing a scene for every case. For example, a box moving down toward a floor should be clipped at the floor’s top face:
const double epsilon = 1e-9;VoxelShape floor = Shapes.Box(-5, 0, -5, 5, 1, 5);var movingBox = new AABB(-0.3, 1.5, -0.3, 0.3, 3.3, 0.3);
double allowedY = floor.Collide(Axis.Y, movingBox, -2.0);Assert(Math.Abs(allowedY - (-0.5)) < epsilon, $"The box stops at the floor face, got {allowedY}");The requested displacement is -2, but the box starts only 0.5 above the floor. Compare floating-point results with an appropriate tolerance, and include the observed value in the failure message.
For multi-shape movement, use a fixed collision getter and assert each axis separately. That makes a sliding result clear: the blocked axis is shortened while the unblocked axis continues.
Add a level-integrated check when needed
Section titled “Add a level-integrated check when needed”When the behavior depends on the real WorldManager, use the existing validation world and set up only the cells the case needs. The physics suite takes a PlayerController and obtains its world. A small probe entity can then exercise the actual level collision path:
var origin = new Vector3I(24, 64, 40);world.SetBlock(origin, BlockType.Stone);
var probe = new ProbeEntity();probe.SetPos(24.5, 65.3, 40.5);Assert(world.AddFreshEntity(probe), "The level accepts the probe entity");
probe.DeltaMovement = new Vec3d(0.0, -0.5, 0.0);probe.Move(MoverType.Self, probe.DeltaMovement);
Assert(probe.OnGround && probe.VerticalCollisionBelow, "The probe lands on the floor");This uses a probe derived from GDCraft.Entity.Entity, as in TestEntityMovementBlockFactorsAndFallingBlocks. Clean up the entity and restore changed blocks before leaving the case so nearby cases do not inherit its state.
For specialized movement, assert an observable outcome across a small number of world ticks. The dolphin check initializes a controlled swimming mob, points its movement control into a water volume, and verifies both forward displacement and a downward-facing travel pitch:
dolphin.MoveControl.SetWantedPosition(new Vector3(9.0f, 2.0f, 40.5f), 1.0f);for (int tick = 0; tick < 30; tick++) dolphin._PhysicsProcess(WorldManager.GameTickSeconds);
Assert(dolphin.GlobalPosition.X > startX + 0.5f, "The dolphin swims toward its requested position");Assert(dolphin.XRotDegrees < 0.0f, "The dolphin pitches toward a target above or ahead");Use the actual collision world for this kind of integration case: swimming depends on fluid occupancy, movement controls, and the mob’s custom travel path working together. Then move the same mob onto a dry floor to verify moisture drain and stranded behavior independently.
Keep the assertion about one rule
Section titled “Keep the assertion about one rule”Use one assertion per behavior where practical. Separate shape intersection, axis clipping, landing flags, velocity response, and block side effects. When a test needs game ticks, advance only the number needed and assert the intermediate state as well as the final one.
Register the case with the Phys suite in SystemValidation.cs. Run just that suite during iteration:
./scripts/validate.ps1 -Filter PhysThe validation script builds the project before launching its headless runner. See build and validation for filters and failure output.