diff --git a/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs b/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs new file mode 100644 index 0000000..f223938 --- /dev/null +++ b/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs @@ -0,0 +1,342 @@ +using System.Reflection; +using Arch.Core; +using GFramework.Core.Abstractions.ecs; +using GFramework.Core.Abstractions.rule; +using GFramework.Core.architecture; +using GFramework.Core.ecs; +using GFramework.Core.ecs.components; +using GFramework.Core.ecs.systems; +using GFramework.Core.ioc; +using GFramework.Core.logging; +using NUnit.Framework; + +namespace GFramework.Core.Tests.ecs; + +[TestFixture] +public class EcsAdvancedTests +{ + [SetUp] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + + _container = new MicrosoftDiContainer(); + var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", + BindingFlags.NonPublic | BindingFlags.Instance); + loggerField?.SetValue(_container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(EcsAdvancedTests))); + + _context = new ArchitectureContext(_container); + } + + [TearDown] + public void TearDown() + { + _ecsWorld?.Dispose(); + _ecsWorld = null; + _container?.Clear(); + _context = null; + } + + private MicrosoftDiContainer? _container; + private ArchitectureContext? _context; + private EcsWorld? _ecsWorld; + + private void InitializeEcsWithSystems(params Type[] systemTypes) + { + _ecsWorld = new EcsWorld(); + _container!.Register(_ecsWorld); + _container.Register(_ecsWorld as IEcsWorld); + + var systems = new List(); + foreach (var systemType in systemTypes) + { + var system = (IEcsSystem)Activator.CreateInstance(systemType)!; + ((IContextAware)system).SetContext(_context!); + system.Init(); + systems.Add(system); + _container.RegisterPlurality(system); + } + + _container.Register(systems as IReadOnlyList); + } + + private EcsSystemRunner CreateRunner() + { + var runner = new EcsSystemRunner(); + ((IContextAware)runner).SetContext(_context!); + runner.Init(); + return runner; + } + + [Test] + public void EcsWorld_Dispose_Should_Be_Idempotent() + { + _ecsWorld = new EcsWorld(); + _ecsWorld.CreateEntity(typeof(Position)); + + Assert.DoesNotThrow(() => + { + _ecsWorld.Dispose(); + _ecsWorld.Dispose(); + }); + + _ecsWorld = null; + } + + [Test] + public void EcsWorld_CreateEntity_WithNoComponents_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(); + + Assert.That(_ecsWorld.EntityCount, Is.EqualTo(1)); + Assert.That(_ecsWorld.IsAlive(entity), Is.True); + } + + [Test] + public void EcsWorld_CreateEntity_WithMultipleComponents_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity)); + + var world = _ecsWorld.InternalWorld; + Assert.That(world.Has(entity), Is.True); + Assert.That(world.Has(entity), Is.True); + } + + [Test] + public void EcsWorld_IsAlive_AfterDestroy_Should_ReturnFalse() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(typeof(Position)); + + Assert.That(_ecsWorld.IsAlive(entity), Is.True); + + _ecsWorld.DestroyEntity(entity); + + Assert.That(_ecsWorld.IsAlive(entity), Is.False); + } + + [Test] + public void EcsSystemRunner_Update_WithoutStart_Should_NotUpdate() + { + InitializeEcsWithSystems(typeof(MovementSystem)); + + var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity)); + var world = _ecsWorld.InternalWorld; + world.Set(entity, new Position(0, 0)); + world.Set(entity, new Velocity(10, 5)); + + var runner = CreateRunner(); + + runner.Update(1.0f); + + ref var pos = ref world.Get(entity); + Assert.That(pos.X, Is.EqualTo(0), "Position should not change without Start()"); + Assert.That(pos.Y, Is.EqualTo(0), "Position should not change without Start()"); + } + + [Test] + public void EcsSystemRunner_StartStop_Should_ControlUpdates() + { + InitializeEcsWithSystems(typeof(MovementSystem)); + + var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity)); + var world = _ecsWorld.InternalWorld; + world.Set(entity, new Position(0, 0)); + world.Set(entity, new Velocity(10, 5)); + + var runner = CreateRunner(); + + runner.Start(); + runner.Update(1.0f); + runner.Stop(); + runner.Update(1.0f); + + ref var pos = ref world.Get(entity); + Assert.That(pos.X, Is.EqualTo(10).Within(0.001f), "Only first update should apply"); + Assert.That(pos.Y, Is.EqualTo(5).Within(0.001f), "Only first update should apply"); + } + + [Test] + public void EcsSystemRunner_WithNoSystems_Should_NotThrow() + { + _ecsWorld = new EcsWorld(); + _container!.Register(_ecsWorld); + _container.Register(new List() as IReadOnlyList); + + var runner = CreateRunner(); + + Assert.DoesNotThrow(() => + { + runner.Start(); + runner.Update(1.0f); + runner.Stop(); + }); + } + + [Test] + public void EcsSystemRunner_OnDestroy_Should_ClearSystems() + { + InitializeEcsWithSystems(typeof(MovementSystem)); + + var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity)); + var world = _ecsWorld.InternalWorld; + world.Set(entity, new Position(0, 0)); + world.Set(entity, new Velocity(10, 5)); + + var runner = CreateRunner(); + runner.Start(); + + // 销毁前先更新一次,记录初始位置 + runner.Update(1.0f); + ref var posBeforeDestroy = ref world.Get(entity); + var xBefore = posBeforeDestroy.X; + var yBefore = posBeforeDestroy.Y; + + runner.Destroy(); + + // 销毁后再更新,位置应该保持不变 + runner.Update(1.0f); + + ref var posAfterDestroy = ref world.Get(entity); + Assert.That(posAfterDestroy.X, Is.EqualTo(xBefore), "Position should not change after Destroy()"); + Assert.That(posAfterDestroy.Y, Is.EqualTo(yBefore), "Position should not change after Destroy()"); + } + + [Test] + public void MultipleSystems_Should_ExecuteInPriorityOrder() + { + var executionOrder = new List(); + + _ecsWorld = new EcsWorld(); + _container!.Register(_ecsWorld); + + var systemA = new OrderTrackingSystem("A", 10, executionOrder); + var systemB = new OrderTrackingSystem("B", -10, executionOrder); + var systemC = new OrderTrackingSystem("C", 0, executionOrder); + + foreach (var system in new[] { systemA, systemB, systemC }) + { + ((IContextAware)system).SetContext(_context!); + system.Init(); + _container.RegisterPlurality(system); + } + + _container.Register(new List { systemA, systemB, systemC } as IReadOnlyList); + + var runner = CreateRunner(); + runner.Start(); + runner.Update(1.0f); + + Assert.That(executionOrder, Is.EqualTo(["B", "C", "A"]), + "Systems should execute in priority order (B=-10, C=0, A=10)"); + } + + [Test] + public void ChainedSystems_Should_PassDataBetweenSystems() + { + _ecsWorld = new EcsWorld(); + _container!.Register(_ecsWorld); + + var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity)); + var world = _ecsWorld.InternalWorld; + world.Set(entity, new Position(0, 0)); + world.Set(entity, new Velocity(10, 0)); + + var movementSystem = new MovementSystem(); + ((IContextAware)movementSystem).SetContext(_context!); + movementSystem.Init(); + _container.RegisterPlurality(movementSystem); + + _container.Register(new List { movementSystem } as IReadOnlyList); + + var runner = CreateRunner(); + runner.Start(); + runner.Update(1.0f); + runner.Update(1.0f); + + ref var pos = ref world.Get(entity); + Assert.That(pos.X, Is.EqualTo(20).Within(0.001f), "Position should accumulate over multiple updates"); + } + + [Test] + public void InitializeEcs_CalledTwice_Should_BeIdempotent() + { + _context!.InitializeEcs(); + var ecsWorld1 = _context.GetEcsWorld(); + + Assert.DoesNotThrow(() => _context.InitializeEcs()); + + var ecsWorld2 = _context.GetEcsWorld(); + Assert.That(ecsWorld2, Is.SameAs(ecsWorld1), "Should return same world instance"); + } + + [Test] + public void GetEcsWorld_Should_ReturnIEcsWorld() + { + _context!.InitializeEcs(); + var ecsWorld = _context.GetEcsWorld(); + + Assert.That(ecsWorld, Is.InstanceOf()); + Assert.That(ecsWorld, Is.InstanceOf()); + } + + [Test] + public void Component_AddAfterCreation_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(Array.Empty()); + var world = _ecsWorld.InternalWorld; + + world.Add(entity, new Position(5, 10)); + + Assert.That(world.Has(entity), Is.True); + ref var pos = ref world.Get(entity); + Assert.That(pos.X, Is.EqualTo(5)); + Assert.That(pos.Y, Is.EqualTo(10)); + } + + [Test] + public void Component_Remove_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity)); + var world = _ecsWorld.InternalWorld; + + world.Remove(entity); + + Assert.That(world.Has(entity), Is.True); + Assert.That(world.Has(entity), Is.False); + } + + [Test] + public void Component_Replace_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(typeof(Position)); + var world = _ecsWorld.InternalWorld; + + world.Set(entity, new Position(1, 1)); + world.Set(entity, new Position(100, 200)); + + ref var pos = ref world.Get(entity); + Assert.That(pos.X, Is.EqualTo(100)); + Assert.That(pos.Y, Is.EqualTo(200)); + } +} + +internal class OrderTrackingSystem(string name, int priority, List executionOrder) : EcsSystemBase +{ + public override int Priority { get; } = priority; + + protected override void OnEcsInit() + { + } + + public override void Update(float deltaTime) + { + executionOrder.Add(name); + } +} \ No newline at end of file