CC Package and Physics

This commit is contained in:
2026-06-02 08:56:26 -07:00
parent a5af81c8a8
commit 2ee30c01fd
37 changed files with 1295 additions and 142 deletions
@@ -0,0 +1,58 @@
using NUnit.Framework;
using ProjectM.Simulation;
using Unity.Mathematics;
namespace ProjectM.Tests
{
/// <summary>
/// Pure-helper unit tests for <see cref="CharacterControlMath.DesiredMovement"/> (M5b: Unity Character
/// Controller). Replaces the M5 PlayerMoveSystemTests: the character now moves via collide-and-slide
/// inside the predicted physics loop, so the unit-testable seam is the input→world-velocity mapping that
/// <see cref="PlayerControlSystem"/> feeds the CC processor. The actual sweep/collision/replication is
/// covered by the Play Mode runtime check (a full PhysicsWorld is not built in a bare EditMode world).
/// Netcode-free and version-independent.
/// </summary>
public class CharacterControlMathTests
{
[Test]
public void DesiredMovement_Maps_Cardinal_To_Planar_Velocity()
{
const float speed = 5f;
var v = CharacterControlMath.DesiredMovement(new float2(1f, 0f), speed);
Assert.AreEqual(speed, v.x, 1e-4f, "Move=(1,0) -> +X scaled by speed.");
Assert.AreEqual(0f, v.y, 1e-4f, "Movement is planar; Y must be zero.");
Assert.AreEqual(0f, v.z, 1e-4f, "Move=(1,0) maps to +X only; Z must be zero.");
}
[Test]
public void DesiredMovement_Clamps_Diagonal_To_Unit_Speed()
{
const float speed = 6f;
var v = CharacterControlMath.DesiredMovement(new float2(1f, 1f), speed);
float planarSpeed = math.length(new float2(v.x, v.z));
Assert.AreEqual(speed, planarSpeed, 1e-3f, "Diagonal input clamped to unit length -> speed == MoveSpeed.");
Assert.AreEqual(0f, v.y, 1e-4f, "Linear Y must stay zero.");
}
[Test]
public void DesiredMovement_Sub_Unit_Input_Is_Proportional()
{
const float speed = 10f;
var v = CharacterControlMath.DesiredMovement(new float2(0.5f, 0f), speed);
Assert.AreEqual(0.5f * speed, v.x, 1e-3f, "Sub-unit analog input scales speed proportionally (not renormalised).");
}
[Test]
public void DesiredMovement_Is_Deterministic_And_Maps_Z()
{
var a = CharacterControlMath.DesiredMovement(new float2(0f, 1f), 3f);
var b = CharacterControlMath.DesiredMovement(new float2(0f, 1f), 3f);
Assert.AreEqual(a.z, b.z, 0f, "Deterministic: identical args -> identical result.");
Assert.AreEqual(3f, a.z, 1e-4f, "Move=(0,1) maps to +Z scaled by speed.");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 051c0bdf57f9cca41963e3c2ed60b2bf
@@ -1,90 +0,0 @@
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="PlayerMoveSystem"/> (the M1 predicted move
/// system). Boots a bare ECS world, registers the system in the SimulationSystemGroup, creates a
/// synthetic player (PlayerInput + EffectiveCharacterStats + LocalTransform + enabled Simulate),
/// injects a fixed delta-time, ticks N times, and asserts the position advanced by exactly
/// MoveSpeed * dt * N. As of M3 move speed is read from EffectiveCharacterStats (the data-driven
/// effective stat) rather than the removed PlayerMoveStats. Version-independent and netcode-free.
/// </summary>
public class PlayerMoveSystemTests
{
[Test]
public void PlayerMove_Advances_By_MoveSpeed_Times_Dt_Each_Tick()
{
using var world = new World("PlayerMoveTestWorld");
var simulationGroup = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
var moveSystem = world.GetOrCreateSystem<PlayerMoveSystem>();
simulationGroup.AddSystemToUpdateList(moveSystem);
simulationGroup.SortSystems();
var em = world.EntityManager;
var entity = em.CreateEntity(
typeof(PlayerInput), typeof(EffectiveCharacterStats), typeof(LocalTransform), typeof(Simulate));
const float moveSpeed = 5f;
const float dt = 0.1f;
const int ticks = 10;
em.SetComponentData(entity, LocalTransform.FromPosition(float3.zero));
em.SetComponentData(entity, new EffectiveCharacterStats { MoveSpeed = moveSpeed, TurnRateRadiansPerSec = 0f, MaxHealth = 0f });
em.SetComponentData(entity, new PlayerInput { Move = new float2(1f, 0f), Aim = float2.zero });
for (int i = 0; i < ticks; i++)
{
// Fixed delta so the predicted move 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;
Assert.AreEqual(moveSpeed * dt * ticks, position.x, 1e-3f,
"X should advance by MoveSpeed * dt each tick for Move=(1,0).");
Assert.AreEqual(0f, position.y, 1e-3f, "Movement is planar; Y should stay 0.");
Assert.AreEqual(0f, position.z, 1e-3f, "Move=(1,0) maps to +X only; Z should stay 0.");
}
[Test]
public void PlayerMove_Is_Idempotent_Across_Equal_Tick_Batches()
{
// Determinism/idempotence: the same inputs and dt must yield the same result regardless
// of how the ticks are grouped (mirrors the prediction loop re-simulating a tick).
float3 RunTicks(int ticks)
{
using var world = new World("PlayerMoveDetWorld");
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<PlayerMoveSystem>());
group.SortSystems();
var em = world.EntityManager;
var e = em.CreateEntity(
typeof(PlayerInput), typeof(EffectiveCharacterStats), typeof(LocalTransform), typeof(Simulate));
em.SetComponentData(e, LocalTransform.FromPosition(float3.zero));
em.SetComponentData(e, new EffectiveCharacterStats { MoveSpeed = 3f, TurnRateRadiansPerSec = 0f, MaxHealth = 0f });
em.SetComponentData(e, new PlayerInput { Move = new float2(0f, 1f), Aim = float2.zero });
for (int i = 0; i < ticks; i++)
{
world.SetTime(new TimeData(0.05f * (i + 1), 0.05f));
group.Update();
}
return em.GetComponentData<LocalTransform>(e).Position;
}
var a = RunTicks(20);
var b = RunTicks(20);
Assert.AreEqual(a.z, b.z, 1e-4f, "Two identical runs must produce identical positions.");
Assert.AreEqual(3f * 0.05f * 20f, a.z, 1e-3f, "Move=(0,1) maps to +Z by MoveSpeed*dt*N.");
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: aed328b85e7264aa9aedca0a628c6169
@@ -8,6 +8,7 @@
"Unity.Transforms",
"Unity.Collections",
"Unity.Mathematics",
"Unity.Physics",
"Unity.NetCode",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"