Files
vampire-like/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs
T

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
});
}
}
}
}