// ---------------------------------------------------------------------------- // The MIT License // Simple Entity Component System framework https://github.com/Leopotam/ecs // Copyright (c) 2017-2020 Leopotam // ---------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Reflection; namespace Leopotam.Ecs { /// /// Base interface for all systems. /// public interface IEcsSystem { } /// /// Interface for PreInit systems. PreInit() will be called before Init(). /// public interface IEcsPreInitSystem : IEcsSystem { void PreInit (); } /// /// Interface for Init systems. Init() will be called before Run(). /// public interface IEcsInitSystem : IEcsSystem { void Init (); } /// /// Interface for AfterDestroy systems. AfterDestroy() will be called after Destroy(). /// public interface IEcsAfterDestroySystem : IEcsSystem { void AfterDestroy (); } /// /// Interface for Destroy systems. Destroy() will be called last in system lifetime cycle. /// public interface IEcsDestroySystem : IEcsSystem { void Destroy (); } /// /// Interface for Run systems. /// public interface IEcsRunSystem : IEcsSystem { void Run (); } #if DEBUG /// /// Debug interface for systems events processing. /// public interface IEcsSystemsDebugListener { void OnSystemsDestroyed (); } #endif /// /// Logical group of systems. /// #if ENABLE_IL2CPP [Unity.IL2CPP.CompilerServices.Il2CppSetOption (Unity.IL2CPP.CompilerServices.Option.NullChecks, false)] [Unity.IL2CPP.CompilerServices.Il2CppSetOption (Unity.IL2CPP.CompilerServices.Option.ArrayBoundsChecks, false)] #endif public sealed class EcsSystems : IEcsInitSystem, IEcsDestroySystem, IEcsRunSystem { public readonly string Name; public readonly EcsWorld World; readonly EcsGrowList _allSystems = new EcsGrowList (64); readonly EcsGrowList _runSystems = new EcsGrowList (64); readonly Dictionary _namedRunSystems = new Dictionary (64); readonly Dictionary _injections = new Dictionary (32); bool _injected; #if DEBUG bool _inited; bool _destroyed; readonly List _debugListeners = new List (4); /// /// Adds external event listener. /// /// Event listener. public void AddDebugListener (IEcsSystemsDebugListener listener) { if (listener == null) { throw new Exception ("listener is null"); } _debugListeners.Add (listener); } /// /// Removes external event listener. /// /// Event listener. public void RemoveDebugListener (IEcsSystemsDebugListener listener) { if (listener == null) { throw new Exception ("listener is null"); } _debugListeners.Remove (listener); } #endif /// /// Creates new instance of EcsSystems group. /// /// EcsWorld instance. /// Custom name for this group. public EcsSystems (EcsWorld world, string name = null) { World = world; Name = name; } /// /// Adds new system to processing. /// /// System instance. /// Optional name of system. public EcsSystems Add (IEcsSystem system, string namedRunSystem = null) { #if DEBUG if (system == null) { throw new Exception ("System is null."); } if (_inited) { throw new Exception ("Cant add system after initialization."); } if (_destroyed) { throw new Exception ("Cant touch after destroy."); } if (!string.IsNullOrEmpty (namedRunSystem) && !(system is IEcsRunSystem)) { throw new Exception ("Cant name non-IEcsRunSystem."); } #endif _allSystems.Add (system); if (system is IEcsRunSystem) { if (namedRunSystem != null) { _namedRunSystems[namedRunSystem.GetHashCode ()] = _runSystems.Count; } _runSystems.Add (new EcsSystemsRunItem () { Active = true, System = (IEcsRunSystem) system }); } return this; } public int GetNamedRunSystem (string name) { return _namedRunSystems.TryGetValue (name.GetHashCode (), out var idx) ? idx : -1; } /// /// Sets IEcsRunSystem active state. /// /// Index of system. /// New state of system. public void SetRunSystemState (int idx, bool state) { #if DEBUG if (idx < 0 || idx >= _runSystems.Count) { throw new Exception ("Invalid index"); } #endif _runSystems.Items[idx].Active = state; } /// /// Gets IEcsRunSystem active state. /// /// Index of system. public bool GetRunSystemState (int idx) { #if DEBUG if (idx < 0 || idx >= _runSystems.Count) { throw new Exception ("Invalid index"); } #endif return _runSystems.Items[idx].Active; } /// /// Get all systems. Important: Don't change collection! /// public EcsGrowList GetAllSystems () { return _allSystems; } /// /// Gets all run systems. Important: Don't change collection! /// public EcsGrowList GetRunSystems () { return _runSystems; } /// /// Injects instance of object type to all compatible fields of added systems. /// /// Instance. public EcsSystems Inject (T obj) { #if DEBUG if (_inited) { throw new Exception ("Cant inject after initialization."); } #endif _injections[typeof (T)] = obj; return this; } /// /// Processes injections immediately. /// Can be used to DI before Init() call. /// public EcsSystems ProcessInjects () { #if DEBUG if (_inited) { throw new Exception ("Cant inject after initialization."); } if (_destroyed) { throw new Exception ("Cant touch after destroy."); } #endif if (!_injected) { _injected = true; for (int i = 0, iMax = _allSystems.Count; i < iMax; i++) { var nestedSystems = _allSystems.Items[i] as EcsSystems; if (nestedSystems != null) { foreach (var pair in _injections) { nestedSystems._injections[pair.Key] = pair.Value; } nestedSystems.ProcessInjects (); } else { InjectDataToSystem (_allSystems.Items[i], World, _injections); } } } return this; } /// /// Registers component type as one-frame for auto-removing at end of Run() call. /// public EcsSystems OneFrame () where T : class { Add (new RemoveOneFrame ()); return this; } /// /// Closes registration for new systems, initialize all registered. /// public void Init () { #if DEBUG if (_inited) { throw new Exception ("Already inited."); } if (_destroyed) { throw new Exception ("Cant touch after destroy."); } #endif ProcessInjects (); // IEcsPreInitSystem processing. for (int i = 0, iMax = _allSystems.Count; i < iMax; i++) { var system = _allSystems.Items[i]; if (system is IEcsPreInitSystem) { ((IEcsPreInitSystem) system).PreInit (); #if DEBUG World.CheckForLeakedEntities ($"{system.GetType ().Name}.PreInit()"); #endif } } // IEcsInitSystem processing. for (int i = 0, iMax = _allSystems.Count; i < iMax; i++) { var system = _allSystems.Items[i]; if (system is IEcsInitSystem) { ((IEcsInitSystem) system).Init (); #if DEBUG World.CheckForLeakedEntities ($"{system.GetType ().Name}.Init()"); #endif } } #if DEBUG _inited = true; #endif } /// /// Processes all IEcsRunSystem systems. /// public void Run () { #if DEBUG if (!_inited) { throw new Exception ($"[{Name ?? "NONAME"}] EcsSystems should be initialized before."); } if (_destroyed) { throw new Exception ("Cant touch after destroy."); } #endif for (int i = 0, iMax = _runSystems.Count; i < iMax; i++) { var runItem = _runSystems.Items[i]; if (runItem.Active) { runItem.System.Run (); } #if DEBUG if (World.CheckForLeakedEntities (null)) { throw new Exception ($"Empty entity detected, possible memory leak in {_runSystems.Items[i].GetType ().Name}.Run ()"); } #endif } } /// /// Destroys registered data. /// public void Destroy () { #if DEBUG if (_destroyed) { throw new Exception ("Already destroyed."); } _destroyed = true; #endif // IEcsDestroySystem processing. for (var i = _allSystems.Count - 1; i >= 0; i--) { var system = _allSystems.Items[i]; if (system is IEcsDestroySystem) { ((IEcsDestroySystem) system).Destroy (); #if DEBUG World.CheckForLeakedEntities ($"{system.GetType ().Name}.Destroy ()"); #endif } } // IEcsAfterDestroySystem processing. for (var i = _allSystems.Count - 1; i >= 0; i--) { var system = _allSystems.Items[i]; if (system is IEcsAfterDestroySystem) { ((IEcsAfterDestroySystem) system).AfterDestroy (); #if DEBUG World.CheckForLeakedEntities ($"{system.GetType ().Name}.AfterDestroy ()"); #endif } } #if DEBUG for (int i = 0, iMax = _debugListeners.Count; i < iMax; i++) { _debugListeners[i].OnSystemsDestroyed (); } #endif } /// /// Injects custom data to fields of ISystem instance. /// /// ISystem instance. /// EcsWorld instance. /// Additional instances for injection. public static void InjectDataToSystem (IEcsSystem system, EcsWorld world, Dictionary injections) { var systemType = system.GetType (); var worldType = world.GetType (); var filterType = typeof (EcsFilter); var ignoreType = typeof (EcsIgnoreInjectAttribute); foreach (var f in systemType.GetFields (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { // skip statics or fields with [EcsIgnoreInject] attribute. if (f.IsStatic || Attribute.IsDefined (f, ignoreType)) { continue; } // EcsWorld if (f.FieldType.IsAssignableFrom (worldType)) { f.SetValue (system, world); continue; } // EcsFilter #if DEBUG if (f.FieldType == filterType) { throw new Exception ($"Cant use EcsFilter type at \"{system}\" system for dependency injection, use generic version instead"); } #endif if (f.FieldType.IsSubclassOf (filterType)) { f.SetValue (system, world.GetFilter (f.FieldType)); continue; } // Other injections. foreach (var pair in injections) { if (f.FieldType.IsAssignableFrom (pair.Key)) { f.SetValue (system, pair.Value); break; } } } } } /// /// System for removing OneFrame component. /// /// OneFrame component type. sealed class RemoveOneFrame : IEcsRunSystem where T : class { readonly EcsFilter _oneFrames = null; void IEcsRunSystem.Run () { foreach (var idx in _oneFrames) { _oneFrames.Entities[idx].Unset (); } } } /// /// IEcsRunSystem instance with active state. /// public sealed class EcsSystemsRunItem { public bool Active; public IEcsRunSystem System; } }