119 lines
5.3 KiB
C#
119 lines
5.3 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Simulation;
|
|
using Unity.Core;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Plain-Entities determinism test for <see cref="ProjectileMoveSystem"/> (the M2 predicted
|
|
/// projectile integrator). Boots a bare ECS world, registers the system in the
|
|
/// SimulationSystemGroup, creates a synthetic projectile (LocalTransform + Projectile + enabled
|
|
/// Simulate), injects a fixed delta-time, ticks N times, and asserts the position advanced by
|
|
/// exactly Speed * dt * N along the planar (XZ) Direction and that DistanceTravelled accumulated
|
|
/// the same total. Version-independent and netcode-free, mirroring PlayerMoveSystemTests.
|
|
/// The system's <c>[UpdateInGroup(PredictedSimulationSystemGroup)]</c> attribute is inert here
|
|
/// because the system is added to the SimulationSystemGroup manually.
|
|
/// </summary>
|
|
public class ProjectileMoveSystemTests
|
|
{
|
|
[Test]
|
|
public void Projectile_Advances_By_Speed_Times_Dt_Each_Tick()
|
|
{
|
|
using var world = new World("ProjectileMoveTestWorld");
|
|
var simulationGroup = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
var moveSystem = world.GetOrCreateSystem<ProjectileMoveSystem>();
|
|
simulationGroup.AddSystemToUpdateList(moveSystem);
|
|
simulationGroup.SortSystems();
|
|
|
|
var em = world.EntityManager;
|
|
var entity = em.CreateEntity(
|
|
typeof(LocalTransform), typeof(Projectile), typeof(Simulate));
|
|
|
|
const float speed = 10f;
|
|
const float dt = 0.1f;
|
|
const int ticks = 5;
|
|
|
|
em.SetComponentData(entity, LocalTransform.FromPosition(float3.zero));
|
|
em.SetComponentData(entity, new Projectile
|
|
{
|
|
Direction = new float2(1f, 0f),
|
|
SpawnId = 0u,
|
|
Speed = speed,
|
|
Damage = 20f,
|
|
Range = 100f,
|
|
DistanceTravelled = 0f,
|
|
});
|
|
|
|
for (int i = 0; i < ticks; i++)
|
|
{
|
|
// Fixed delta so the predicted integration is fully deterministic (no wall-clock).
|
|
world.SetTime(new TimeData(elapsedTime: dt * (i + 1), deltaTime: dt));
|
|
simulationGroup.Update();
|
|
}
|
|
|
|
var position = em.GetComponentData<LocalTransform>(entity).Position;
|
|
var distance = em.GetComponentData<Projectile>(entity).DistanceTravelled;
|
|
|
|
Assert.AreEqual(speed * dt * ticks, position.x, 1e-3f,
|
|
"X should advance by Speed * dt each tick for Direction=(1,0).");
|
|
Assert.AreEqual(0f, position.y, 1e-3f, "Travel is planar; Y should stay 0.");
|
|
Assert.AreEqual(0f, position.z, 1e-3f, "Direction=(1,0) maps to +X only; Z should stay 0.");
|
|
Assert.AreEqual(speed * dt * ticks, distance, 1e-3f,
|
|
"DistanceTravelled should accumulate Speed * dt each tick.");
|
|
}
|
|
|
|
[Test]
|
|
public void Projectile_Integration_Is_Idempotent_Across_Equal_Tick_Batches()
|
|
{
|
|
// Determinism/idempotence: the same Direction, Speed and dt must yield the same result
|
|
// regardless of how the ticks are grouped (mirrors the prediction loop re-simulating a
|
|
// tick on rollback). Returns (finalPosition, distanceTravelled).
|
|
(float3 Position, float Distance) RunTicks(int ticks)
|
|
{
|
|
using var world = new World("ProjectileMoveDetWorld");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<ProjectileMoveSystem>());
|
|
group.SortSystems();
|
|
|
|
var em = world.EntityManager;
|
|
var e = em.CreateEntity(
|
|
typeof(LocalTransform), typeof(Projectile), typeof(Simulate));
|
|
em.SetComponentData(e, LocalTransform.FromPosition(float3.zero));
|
|
em.SetComponentData(e, new Projectile
|
|
{
|
|
Direction = new float2(0f, 1f),
|
|
SpawnId = 0u,
|
|
Speed = 7f,
|
|
Damage = 20f,
|
|
Range = 100f,
|
|
DistanceTravelled = 0f,
|
|
});
|
|
|
|
for (int i = 0; i < ticks; i++)
|
|
{
|
|
world.SetTime(new TimeData(0.05f * (i + 1), 0.05f));
|
|
group.Update();
|
|
}
|
|
|
|
var proj = em.GetComponentData<Projectile>(e);
|
|
return (em.GetComponentData<LocalTransform>(e).Position, proj.DistanceTravelled);
|
|
}
|
|
|
|
var a = RunTicks(20);
|
|
var b = RunTicks(20);
|
|
|
|
Assert.AreEqual(a.Position.z, b.Position.z, 1e-4f,
|
|
"Two identical runs must produce identical positions.");
|
|
Assert.AreEqual(a.Distance, b.Distance, 1e-4f,
|
|
"Two identical runs must produce identical DistanceTravelled.");
|
|
Assert.AreEqual(7f * 0.05f * 20f, a.Position.z, 1e-3f,
|
|
"Direction=(0,1) maps to +Z by Speed*dt*N.");
|
|
Assert.AreEqual(7f * 0.05f * 20f, a.Distance, 1e-3f,
|
|
"DistanceTravelled equals Speed*dt*N.");
|
|
}
|
|
}
|
|
}
|