You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

373 lines
14 KiB

// ----------------------------------------------------------------------------
// The MIT License
// Simple Entity Component System framework https://github.com/Leopotam/ecs
// Copyright (c) 2017-2020 Leopotam <leopotam@gmail.com>
// ----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Leopotam.Ecs {
/// <summary>
/// Base interface for all systems.
/// </summary>
public interface IEcsSystem { }
/// <summary>
/// Interface for PreInit systems. PreInit() will be called before Init().
/// </summary>
public interface IEcsPreInitSystem : IEcsSystem {
void PreInit ();
}
/// <summary>
/// Interface for Init systems. Init() will be called before Run().
/// </summary>
public interface IEcsInitSystem : IEcsSystem {
void Init ();
}
/// <summary>
/// Interface for AfterDestroy systems. AfterDestroy() will be called after Destroy().
/// </summary>
public interface IEcsAfterDestroySystem : IEcsSystem {
void AfterDestroy ();
}
/// <summary>
/// Interface for Destroy systems. Destroy() will be called last in system lifetime cycle.
/// </summary>
public interface IEcsDestroySystem : IEcsSystem {
void Destroy ();
}
/// <summary>
/// Interface for Run systems.
/// </summary>
public interface IEcsRunSystem : IEcsSystem {
void Run ();
}
#if DEBUG
/// <summary>
/// Debug interface for systems events processing.
/// </summary>
public interface IEcsSystemsDebugListener {
void OnSystemsDestroyed ();
}
#endif
/// <summary>
/// Logical group of systems.
/// </summary>
#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<IEcsSystem> _allSystems = new EcsGrowList<IEcsSystem> (64);
readonly EcsGrowList<EcsSystemsRunItem> _runSystems = new EcsGrowList<EcsSystemsRunItem> (64);
readonly Dictionary<int, int> _namedRunSystems = new Dictionary<int, int> (64);
readonly Dictionary<Type, object> _injections = new Dictionary<Type, object> (32);
bool _injected;
#if DEBUG
bool _inited;
bool _destroyed;
readonly List<IEcsSystemsDebugListener> _debugListeners = new List<IEcsSystemsDebugListener> (4);
/// <summary>
/// Adds external event listener.
/// </summary>
/// <param name="listener">Event listener.</param>
public void AddDebugListener (IEcsSystemsDebugListener listener) {
if (listener == null) { throw new Exception ("listener is null"); }
_debugListeners.Add (listener);
}
/// <summary>
/// Removes external event listener.
/// </summary>
/// <param name="listener">Event listener.</param>
public void RemoveDebugListener (IEcsSystemsDebugListener listener) {
if (listener == null) { throw new Exception ("listener is null"); }
_debugListeners.Remove (listener);
}
#endif
/// <summary>
/// Creates new instance of EcsSystems group.
/// </summary>
/// <param name="world">EcsWorld instance.</param>
/// <param name="name">Custom name for this group.</param>
public EcsSystems (EcsWorld world, string name = null) {
World = world;
Name = name;
}
/// <summary>
/// Adds new system to processing.
/// </summary>
/// <param name="system">System instance.</param>
/// <param name="namedRunSystem">Optional name of system.</param>
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;
}
/// <summary>
/// Sets IEcsRunSystem active state.
/// </summary>
/// <param name="idx">Index of system.</param>
/// <param name="state">New state of system.</param>
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;
}
/// <summary>
/// Gets IEcsRunSystem active state.
/// </summary>
/// <param name="idx">Index of system.</param>
public bool GetRunSystemState (int idx) {
#if DEBUG
if (idx < 0 || idx >= _runSystems.Count) { throw new Exception ("Invalid index"); }
#endif
return _runSystems.Items[idx].Active;
}
/// <summary>
/// Get all systems. Important: Don't change collection!
/// </summary>
public EcsGrowList<IEcsSystem> GetAllSystems () {
return _allSystems;
}
/// <summary>
/// Gets all run systems. Important: Don't change collection!
/// </summary>
public EcsGrowList<EcsSystemsRunItem> GetRunSystems () {
return _runSystems;
}
/// <summary>
/// Injects instance of object type to all compatible fields of added systems.
/// </summary>
/// <param name="obj">Instance.</param>
public EcsSystems Inject<T> (T obj) {
#if DEBUG
if (_inited) { throw new Exception ("Cant inject after initialization."); }
#endif
_injections[typeof (T)] = obj;
return this;
}
/// <summary>
/// Processes injections immediately.
/// Can be used to DI before Init() call.
/// </summary>
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;
}
/// <summary>
/// Registers component type as one-frame for auto-removing at end of Run() call.
/// </summary>
public EcsSystems OneFrame<T> () where T : class {
Add (new RemoveOneFrame<T> ());
return this;
}
/// <summary>
/// Closes registration for new systems, initialize all registered.
/// </summary>
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
}
/// <summary>
/// Processes all IEcsRunSystem systems.
/// </summary>
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
}
}
/// <summary>
/// Destroys registered data.
/// </summary>
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
}
/// <summary>
/// Injects custom data to fields of ISystem instance.
/// </summary>
/// <param name="system">ISystem instance.</param>
/// <param name="world">EcsWorld instance.</param>
/// <param name="injections">Additional instances for injection.</param>
public static void InjectDataToSystem (IEcsSystem system, EcsWorld world, Dictionary<Type, object> 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;
}
}
}
}
}
/// <summary>
/// System for removing OneFrame component.
/// </summary>
/// <typeparam name="T">OneFrame component type.</typeparam>
sealed class RemoveOneFrame<T> : IEcsRunSystem where T : class {
readonly EcsFilter<T> _oneFrames = null;
void IEcsRunSystem.Run () {
foreach (var idx in _oneFrames) {
_oneFrames.Entities[idx].Unset<T> ();
}
}
}
/// <summary>
/// IEcsRunSystem instance with active state.
/// </summary>
public sealed class EcsSystemsRunItem {
public bool Active;
public IEcsRunSystem System;
}
}