326 lines
13 KiB
C#
326 lines
13 KiB
C#
using NUnit.Framework;
|
|
using SepCore.Components;
|
|
using SepCore.Simulation;
|
|
using UnityEngine;
|
|
|
|
namespace SepCore.Tests.Editor
|
|
{
|
|
public class SimulationWorldTickTests
|
|
{
|
|
private GameObject _worldObject;
|
|
private SimulationWorld _world;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_worldObject = new GameObject("SimulationWorldTests");
|
|
_world = _worldObject.AddComponent<SimulationWorld>();
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (_worldObject != null)
|
|
{
|
|
Object.DestroyImmediate(_worldObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void MovementComponent_OnUpdate_DoesNotMoveTransformDirectly()
|
|
{
|
|
GameObject moverObject = new GameObject("Mover");
|
|
try
|
|
{
|
|
MovementComponent movement = moverObject.AddComponent<MovementComponent>();
|
|
moverObject.transform.position = new Vector3(1f, 0f, 2f);
|
|
|
|
movement.OnInit(5f, moverObject.transform, null, false, 0.45f, 2);
|
|
movement.SetMove(true);
|
|
movement.SetDirection(Vector3.right);
|
|
|
|
movement.OnUpdate(1f, 1f);
|
|
|
|
Assert.That(moverObject.transform.position.x, Is.EqualTo(1f).Within(0.0001f));
|
|
Assert.That(moverObject.transform.position.y, Is.EqualTo(0f).Within(0.0001f));
|
|
Assert.That(moverObject.transform.position.z, Is.EqualTo(2f).Within(0.0001f));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(moverObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Tick_AdvancesEnemyAndUpdatesNearestTargetQuery()
|
|
{
|
|
_world.UpsertEnemy(new EnemySimData
|
|
{
|
|
EntityId = 1001,
|
|
Position = new Vector3(0f, 0f, 0f),
|
|
Forward = Vector3.forward,
|
|
Rotation = Quaternion.identity,
|
|
Speed = 3f,
|
|
AttackRange = 0.5f,
|
|
AvoidEnemyOverlap = true,
|
|
EnemyBodyRadius = 0.45f,
|
|
SeparationIterations = 2
|
|
});
|
|
|
|
_world.Tick(new SimulationTickContext(1f, new Vector3(10f, 0f, 0f)));
|
|
|
|
Assert.That(_world.EnemyCount, Is.EqualTo(1));
|
|
Assert.That(_world.TryGetEnemySnapshot(1001, out EnemySimData enemy), Is.True);
|
|
Assert.That(enemy.State, Is.EqualTo(1));
|
|
Assert.That(enemy.Position.x, Is.EqualTo(3f).Within(0.001f));
|
|
Assert.That(_world.TryGetNearestEnemyEntityId(new Vector3(3.2f, 0f, 0f), 1f, out int enemyEntityId),
|
|
Is.True);
|
|
Assert.That(enemyEntityId, Is.EqualTo(1001));
|
|
}
|
|
|
|
[Test]
|
|
public void Tick_AccumulatesTimeAcrossTicksBeforeRunningFixedStep()
|
|
{
|
|
_world.UpsertEnemy(new EnemySimData
|
|
{
|
|
EntityId = 1003,
|
|
Position = Vector3.zero,
|
|
Forward = Vector3.forward,
|
|
Rotation = Quaternion.identity,
|
|
Speed = 6f,
|
|
AttackRange = 1f,
|
|
AvoidEnemyOverlap = false,
|
|
EnemyBodyRadius = 0.45f,
|
|
SeparationIterations = 1
|
|
});
|
|
|
|
_world.Tick(new SimulationTickContext(0.01f, new Vector3(10f, 0f, 0f)));
|
|
|
|
Assert.That(_world.TryGetEnemySnapshot(1003, out EnemySimData enemy), Is.True);
|
|
Assert.That(enemy.Position.x, Is.EqualTo(0f).Within(0.0001f));
|
|
|
|
_world.Tick(new SimulationTickContext(0.01f, new Vector3(10f, 0f, 0f)));
|
|
|
|
Assert.That(_world.TryGetEnemySnapshot(1003, out enemy), Is.True);
|
|
Assert.That(enemy.Position.x, Is.EqualTo(0.1f).Within(0.0001f));
|
|
}
|
|
|
|
[Test]
|
|
public void SyncEnemyMovementInput_DisablesEnemyMovement()
|
|
{
|
|
_world.UpsertEnemy(new EnemySimData
|
|
{
|
|
EntityId = 1002,
|
|
Position = Vector3.zero,
|
|
Forward = Vector3.forward,
|
|
Rotation = Quaternion.identity,
|
|
Speed = 4f,
|
|
AttackRange = 0.5f,
|
|
AvoidEnemyOverlap = true,
|
|
EnemyBodyRadius = 0.45f,
|
|
SeparationIterations = 2
|
|
});
|
|
|
|
_world.SyncEnemyMovementInput(1002, false, Vector3.left, 4f, true, 0.45f, 2);
|
|
_world.Tick(new SimulationTickContext(1f, new Vector3(10f, 0f, 0f)));
|
|
|
|
Assert.That(_world.TryGetEnemySnapshot(1002, out EnemySimData enemy), Is.True);
|
|
Assert.That(enemy.State, Is.EqualTo(0));
|
|
Assert.That(enemy.Position.x, Is.EqualTo(0f).Within(0.0001f));
|
|
Assert.That(enemy.Position.y, Is.EqualTo(0f).Within(0.0001f));
|
|
Assert.That(enemy.Position.z, Is.EqualTo(0f).Within(0.0001f));
|
|
}
|
|
|
|
[Test]
|
|
public void Tick_ExpiresProjectileThroughSimulationLifetime()
|
|
{
|
|
_world.UpsertProjectile(new ProjectileSimData
|
|
{
|
|
EntityId = 2001,
|
|
OwnerEntityId = 1001,
|
|
Position = Vector3.zero,
|
|
Forward = Vector3.right,
|
|
Velocity = Vector3.right * 4f,
|
|
LifeTime = 0.25f,
|
|
Age = 0f,
|
|
Active = true
|
|
});
|
|
|
|
_world.Tick(new SimulationTickContext(0.5f, Vector3.zero));
|
|
|
|
Assert.That(_world.ProjectileCount, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void Tick_WithIdenticalInput_ProducesEquivalentSnapshots()
|
|
{
|
|
const int tickCount = 30;
|
|
GameObject firstWorldObject = new GameObject("SimulationWorldDeterminismA");
|
|
GameObject secondWorldObject = new GameObject("SimulationWorldDeterminismB");
|
|
try
|
|
{
|
|
SimulationWorld firstWorld = firstWorldObject.AddComponent<SimulationWorld>();
|
|
SimulationWorld secondWorld = secondWorldObject.AddComponent<SimulationWorld>();
|
|
SeedDeterminismScene(firstWorld);
|
|
SeedDeterminismScene(secondWorld);
|
|
|
|
SimulationTickContext tickContext =
|
|
new SimulationTickContext(1f / 30f, new Vector3(3.5f, 0f, -2.5f));
|
|
for (int tick = 0; tick < tickCount; tick++)
|
|
{
|
|
firstWorld.Tick(in tickContext);
|
|
secondWorld.Tick(in tickContext);
|
|
}
|
|
|
|
Assert.That(firstWorld.EnemyCount, Is.EqualTo(DeterminismEnemyIds.Length));
|
|
Assert.That(secondWorld.EnemyCount, Is.EqualTo(firstWorld.EnemyCount));
|
|
for (int i = 0; i < DeterminismEnemyIds.Length; i++)
|
|
{
|
|
Assert.That(firstWorld.TryGetEnemySnapshot(DeterminismEnemyIds[i], out EnemySimData first),
|
|
Is.True);
|
|
Assert.That(secondWorld.TryGetEnemySnapshot(DeterminismEnemyIds[i], out EnemySimData second),
|
|
Is.True);
|
|
Assert.That(second.Position.x, Is.EqualTo(first.Position.x).Within(0.01f));
|
|
Assert.That(second.Position.z, Is.EqualTo(first.Position.z).Within(0.01f));
|
|
Assert.That(second.State, Is.EqualTo(first.State));
|
|
}
|
|
|
|
Assert.That(secondWorld.ProjectileCount, Is.EqualTo(firstWorld.ProjectileCount));
|
|
for (int i = 0; i < DeterminismProjectileIds.Length; i++)
|
|
{
|
|
Assert.That(
|
|
firstWorld.TryGetProjectileSnapshot(DeterminismProjectileIds[i], out ProjectileSimData first),
|
|
Is.True);
|
|
Assert.That(
|
|
secondWorld.TryGetProjectileSnapshot(DeterminismProjectileIds[i], out ProjectileSimData second),
|
|
Is.True);
|
|
Assert.That(second.Position.x, Is.EqualTo(first.Position.x).Within(0.01f));
|
|
Assert.That(second.Position.z, Is.EqualTo(first.Position.z).Within(0.01f));
|
|
Assert.That(second.Age, Is.EqualTo(first.Age).Within(0.001f));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(firstWorldObject);
|
|
Object.DestroyImmediate(secondWorldObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ClearSimulationState_ClearsPublicStateContainers()
|
|
{
|
|
_world.UpsertEnemy(new EnemySimData { EntityId = 1, Position = Vector3.zero, Forward = Vector3.forward });
|
|
_world.UpsertProjectile(new ProjectileSimData { EntityId = 2, Active = true });
|
|
|
|
_world.ClearSimulationState();
|
|
|
|
Assert.That(_world.EnemyCount, Is.EqualTo(0));
|
|
Assert.That(_world.ProjectileCount, Is.EqualTo(0));
|
|
Assert.That(_world.PickupCount, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void Pickup_WhenOutsideAbsorbRange_DoesNotMove()
|
|
{
|
|
_world.UpsertPickup(new PickupSimData
|
|
{
|
|
EntityId = 7001,
|
|
Position = new Vector3(10f, 0f, 10f),
|
|
PickupRadius = 0.35f,
|
|
State = 0
|
|
});
|
|
|
|
_world.SyncPlayerAbsorbSettings(3f, 5f, 0.5f);
|
|
_world.Tick(new SimulationTickContext(0.1f, Vector3.zero));
|
|
|
|
Assert.That(_world.TryGetPickupSnapshot(7001, out PickupSimData snapshot), Is.True);
|
|
Assert.That(snapshot.Position.x, Is.EqualTo(10f).Within(0.001f));
|
|
Assert.That(snapshot.Position.z, Is.EqualTo(10f).Within(0.001f));
|
|
Assert.That(snapshot.State, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void Pickup_WhenInsideAbsorbRange_MovesTowardsPlayerAndTransitionsState()
|
|
{
|
|
_world.UpsertPickup(new PickupSimData
|
|
{
|
|
EntityId = 7002,
|
|
Position = new Vector3(2f, 0f, 0f),
|
|
PickupRadius = 0.2f,
|
|
State = 0
|
|
});
|
|
|
|
_world.SyncPlayerAbsorbSettings(5f, 4f, 0.5f);
|
|
_world.Tick(new SimulationTickContext(0.25f, Vector3.zero));
|
|
|
|
Assert.That(_world.TryGetPickupSnapshot(7002, out PickupSimData snapshot), Is.True);
|
|
// Moved from 2.0 towards 0 by 4.0 * 0.25 = 1.0 -> remaining distance is 1.0
|
|
Assert.That(snapshot.Position.x, Is.EqualTo(1.0f).Within(0.01f));
|
|
Assert.That(snapshot.Position.z, Is.EqualTo(0f).Within(0.01f));
|
|
Assert.That(snapshot.State, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void Pickup_TriggerFullMapMagnet_ActivatesDistantPickups()
|
|
{
|
|
_world.UpsertPickup(new PickupSimData
|
|
{
|
|
EntityId = 7003,
|
|
Position = new Vector3(50f, 0f, 0f),
|
|
PickupRadius = 0.2f,
|
|
State = 0
|
|
});
|
|
|
|
_world.SyncPlayerAbsorbSettings(5f, 10f, 0.5f);
|
|
_world.TriggerFullMapMagnet();
|
|
|
|
Assert.That(_world.TryGetPickupSnapshot(7003, out PickupSimData snapshotBefore), Is.True);
|
|
Assert.That(snapshotBefore.State, Is.EqualTo(1));
|
|
|
|
_world.Tick(new SimulationTickContext(0.5f, Vector3.zero));
|
|
|
|
Assert.That(_world.TryGetPickupSnapshot(7003, out PickupSimData snapshotAfter), Is.True);
|
|
// Moved from 50 towards 0 by 10 * 0.5 = 5 -> remaining distance is 45
|
|
Assert.That(snapshotAfter.Position.x, Is.EqualTo(45f).Within(0.01f));
|
|
}
|
|
|
|
private static readonly int[] DeterminismEnemyIds = { 5001, 5002, 5003, 5004, 5005, 5006 };
|
|
private static readonly int[] DeterminismProjectileIds = { 6001, 6002, 6003 };
|
|
|
|
private static void SeedDeterminismScene(SimulationWorld world)
|
|
{
|
|
for (int i = 0; i < DeterminismEnemyIds.Length; i++)
|
|
{
|
|
float angle = i * 0.9f;
|
|
world.UpsertEnemy(new EnemySimData
|
|
{
|
|
EntityId = DeterminismEnemyIds[i],
|
|
Position = new Vector3(Mathf.Cos(angle) * 1.5f, 0f, Mathf.Sin(angle) * 1.5f),
|
|
Forward = Vector3.forward,
|
|
Rotation = Quaternion.identity,
|
|
Speed = 2f + i * 0.25f,
|
|
AttackRange = 0.5f,
|
|
AvoidEnemyOverlap = true,
|
|
EnemyBodyRadius = 0.4f + i * 0.05f,
|
|
SeparationIterations = 2
|
|
});
|
|
}
|
|
|
|
for (int i = 0; i < DeterminismProjectileIds.Length; i++)
|
|
{
|
|
world.UpsertProjectile(new ProjectileSimData
|
|
{
|
|
EntityId = DeterminismProjectileIds[i],
|
|
OwnerEntityId = DeterminismEnemyIds[i],
|
|
Position = new Vector3(0f, 1f, 0f),
|
|
Forward = Vector3.right,
|
|
Velocity = Vector3.right * (3f + i),
|
|
LifeTime = 5f,
|
|
Age = 0f,
|
|
Active = true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|