在ECS(Entity-Component-System)架构中,组件是纯粹的数据容器,系统负责处理数据。然而实际开发中,当组件发生变更(添加、移除、更新)时,往往需要触发额外的业务逻辑------例如更新UI、启动异步任务或同步网络状态。传统的轮询方式不仅低效,还会导致代码耦合。本文介绍一个专为ECS设计的组件生命周期管理器,它通过事件驱动和清晰的层次划分,让组件事件处理变得简单、灵活且高性能。
- 设计目标
解耦:组件生产者(事件源)与消费者(处理器)仅通过接口交互,双方互不感知。
高性能:优先使用泛型事件,配合缓存和表达式树编译,最大程度减少反射和装箱开销。
灵活执行:支持同步、线程池、UI线程三种执行模式,适应不同场景需求。
线程安全:在多线程环境下安全地进行注册、注销和事件分发。
易扩展:可通过适配器接入任意实现了标准接口的事件源。
- 核心组件
整个框架围绕以下几个核心组件构建:
2.1 事件源接口与适配器
框架定义了两层事件源:基础的非泛型接口提供组件添加、移除、更新的非泛型事件,以及按类型获取组件的方法;泛型接口在其基础上增加了泛型订阅方法,允许直接获得强类型组件,避免装箱。为了统一处理这两种事件源,适配器模式被引入------适配器封装了非泛型事件源,并提供获取组件和检测泛型源的能力。默认适配器实现通过表达式树编译缓存了泛型获取组件的方法,确保非泛型调用也能高效工作。
2.2 处理器接口
用户通过实现泛型接口来定义自己的业务逻辑,接口包含三个默认方法:组件添加时调用、组件移除时调用、组件更新时调用(携带新旧值)。由于提供了默认实现,用户只需重写需要关注的方法即可。
2.3 执行策略
不同的业务场景可能需要不同的执行方式:UI更新必须在UI线程执行,耗时计算可以扔到线程池,而实时性要求高的逻辑应立即同步执行。框架通过执行模式枚举和执行策略接口来抽象这一需求。默认执行策略根据模式在当前线程、线程池或捕获的UI同步上下文上执行任务,并自动捕获异常,避免单个处理器的崩溃影响整个系统。
2.4 核心管理器
生命周期管理器是整个框架的心脏,它负责协调上述所有组件:
处理器管理:维护每个组件类型对应的处理器列表(支持多线程安全更新),并提供注册和注销方法。
事件订阅:订阅事件源的泛型或非泛型事件,优先使用泛型以获得最佳性能;若事件源不支持泛型,则通过适配器回退到非泛型事件。
组件缓存:为每个组件类型维护线程安全的缓存,存储实体当前关联的组件值,以便在非泛型事件中也能快速提供新旧值。
执行调度:根据注册时指定的执行模式,通过执行策略异步或同步地调用处理器。
资源管理:实现IDisposable接口,取消所有订阅、清空缓存,并等待正在执行的处理器完成,确保资源正确释放。
- 工作流程简述
初始化:创建事件源适配器和生命周期管理器,可传入自定义执行策略或使用默认策略。
注册处理器:调用管理器的注册方法,将实现了处理器接口的实例与特定组件类型绑定,并指定执行模式。
事件触发:当外部事件源触发组件添加/移除/更新时,管理器接收到通知:
如果是泛型事件,直接获得强类型组件,更新缓存并调用对应处理器。
如果是非泛型事件,通过适配器获取组件对象,更新缓存,然后通过动态生成的委托调用处理器,并传入转换后的强类型参数。
执行调度:每个处理器调用会被包装在执行策略中,按照指定的模式执行,确保不会阻塞事件源线程(如果需要异步)。
资源释放:调用Dispose取消所有订阅,清空缓存,释放资源。
- 使用示例
以下是一个完整的示例,演示如何利用该框架监听组件生命周期事件,代码行数严格控制在50行以内。
csharp
// 1. 定义实体与组件
public readonly record struct Entity(int Id);
public record struct Position(float X, float Y);
// 2. 实现处理器
public class LoggingHandler : IComponentLifecycleHandler<Position>
{
public void OnAdded(Entity e, Position p) =>
Console.WriteLine($"[Added] Entity {e.Id} at ({p.X}, {p.Y})");
public void OnChanged(Entity e, Position newP, Position oldP) =>
Console.WriteLine($"[Changed] Entity {e.Id}: ({oldP.X},{oldP.Y}) -> ({newP.X},{newP.Y})");
}
// 3. 模拟事件源(非泛型,用于演示适配器)
class MockEventSource : IComponentEventSource
{
public event Action<Entity, Type>? ComponentAdded, ComponentRemoved;
public event Action<Entity, Type, object?, object>? ComponentUpdated;
public T GetComponent<T>(Entity entity) where T : struct => throw new NotImplementedException();
public bool IsEntityValid(Entity entity) => true;
public void RaiseAdd(Entity e, Position p) => ComponentAdded?.Invoke(e, typeof(Position));
public void RaiseUpdate(Entity e, Position oldP, Position newP) =>
ComponentUpdated?.Invoke(e, typeof(Position), oldP, newP);
}
// 4. 使用示例
public static void Main()
{
// 创建事件源和适配器
var source = new MockEventSource();
var adapter = new ComponentEventSourceAdapter(source);
// 创建管理器,使用同步执行策略
using var manager = new ComponentLifecycleManager(adapter);
// 注册处理器
manager.RegisterHandler<Position>(new LoggingHandler(), ExecutionMode.Sync);
// 模拟组件添加事件
var entity = new Entity(42);
source.RaiseAdd(entity, new Position(10, 20));
// 模拟组件更新事件
source.RaiseUpdate(entity, new Position(10, 20), new Position(15, 25));
// 输出:
// [Added] Entity 42 at (10, 20)
// [Changed] Entity 42: (10,20) -> (15,25)
}
说明:
第1-2行定义了实体类型和组件(值类型)。
第4-12行实现了一个简单的日志处理器,关注添加和更新事件。
第14-22行模拟了一个最小化的非泛型事件源,用于手动触发事件。
第24-38行展示了完整流程:创建适配器、管理器、注册处理器、触发事件,最终输出预期结果。
该示例完整展示了框架的核心用法,所有代码均可直接运行,无需依赖任何外部库。
- 总结
组件生命周期管理器为ECS架构提供了一种解耦、高效且灵活的事件处理方案。通过清晰的接口分层、泛型优先的设计、可插拔的执行策略以及完善的资源管理,开发者可以轻松地监听组件变化并执行相应的业务逻辑,同时保持代码的简洁和可维护性。无论是游戏开发、实时仿真还是数据驱动应用,该框架都能成为你工具箱中得力的助手。
csharp
namespace ComponentLifecycle
{
// ========== 接口定义(保持不变)==========
public enum ExecutionMode { Sync, ThreadPool, UI }
public interface IComponentEventSource
{
event Action<Entity, Type> ComponentAdded;
event Action<Entity, Type> ComponentRemoved;
event Action<Entity, Type, object?, object> ComponentUpdated;
T GetComponent<T>(Entity entity) where T : struct;
bool IsEntityValid(Entity entity);
}
public interface IGenericComponentEventSource : IComponentEventSource
{
void SubscribeAdded<T>(Action<Entity, T> handler) where T : struct;
void UnsubscribeAdded<T>(Action<Entity, T> handler) where T : struct;
void SubscribeRemoved<T>(Action<Entity, T> handler) where T : struct;
void UnsubscribeRemoved<T>(Action<Entity, T> handler) where T : struct;
void SubscribeUpdated<T>(Action<Entity, T, T> handler) where T : struct;
void UnsubscribeUpdated<T>(Action<Entity, T, T> handler) where T : struct;
}
public interface IComponentLifecycleHandler<T> where T : struct
{
void OnAdded(Entity entity, T component) { }
void OnRemoved(Entity entity, T component) { }
void OnChanged(Entity entity, T newComponent, T oldComponent) { }
}
public interface IComponentEventSourceAdapter
{
event Action<Entity, Type> ComponentAdded;
event Action<Entity, Type> ComponentRemoved;
event Action<Entity, Type, object?, object> ComponentUpdated;
object? GetComponent(Entity entity, Type componentType);
IGenericComponentEventSource? AsGeneric();
}
public interface IExecutionStrategy
{
void Execute(Action action, ExecutionMode mode);
}
// ========== 默认实现 ==========
public class ComponentEventSourceAdapter : IComponentEventSourceAdapter
{
readonly IComponentEventSource _source;
readonly IGenericComponentEventSource? _genericSource;
static readonly ConcurrentDictionary<Type, Func<IComponentEventSource, Entity, object>> _getterCache = new();
public ComponentEventSourceAdapter(IComponentEventSource source) => (_source, _genericSource) = (source ?? throw new ArgumentNullException(nameof(source)), source as IGenericComponentEventSource);
public event Action<Entity, Type> ComponentAdded { add => _source.ComponentAdded += value; remove => _source.ComponentAdded -= value; }
public event Action<Entity, Type> ComponentRemoved { add => _source.ComponentRemoved += value; remove => _source.ComponentRemoved -= value; }
public event Action<Entity, Type, object?, object> ComponentUpdated { add => _source.ComponentUpdated += value; remove => _source.ComponentUpdated -= value; }
public object? GetComponent(Entity entity, Type componentType)
{
var getter = _getterCache.GetOrAdd(componentType, t =>
{
var method = typeof(IComponentEventSource).GetMethod(nameof(IComponentEventSource.GetComponent))!.MakeGenericMethod(t);
var s = Expression.Parameter(typeof(IComponentEventSource), "s");
var e = Expression.Parameter(typeof(Entity), "e");
return Expression.Lambda<Func<IComponentEventSource, Entity, object>>(Expression.Convert(Expression.Call(s, method, e), typeof(object)), s, e).Compile();
});
return getter(_source, entity);
}
public IGenericComponentEventSource? AsGeneric() => _genericSource;
}
public class DefaultExecutionStrategy : IExecutionStrategy
{
readonly SynchronizationContext _uiContext;
public DefaultExecutionStrategy(SynchronizationContext uiContext) => _uiContext = uiContext ?? throw new ArgumentNullException(nameof(uiContext));
public void Execute(Action action, ExecutionMode mode)
{
void Safe() { try { action(); } catch { } }
switch (mode)
{
case ExecutionMode.Sync: Safe(); break;
case ExecutionMode.ThreadPool: ThreadPool.QueueUserWorkItem(_ => Safe()); break;
case ExecutionMode.UI: _uiContext.Post(_ => Safe(), null); break;
}
}
}
// ========== 核心管理器(简化版)==========
public class ComponentLifecycleManager : IDisposable
{
readonly IComponentEventSourceAdapter _adapter;
readonly bool _disposeHandlersOnShutdown;
readonly IExecutionStrategy _executor;
readonly Dictionary<Type, ImmutableList<HandlerEntry>> _handlers = new();
readonly ReaderWriterLockSlim _lock = new();
readonly ConcurrentDictionary<Type, MethodDelegates> _handlerMethodDelegates = new();
readonly Dictionary<Type, SubscriptionInfo> _genericSubscriptionInfos = new();
readonly HashSet<Type> _genericSubscribedTypes = new();
readonly ConcurrentDictionary<Type, IComponentTypeCache> _componentCaches = new();
int _executingCount;
bool _subscribed;
bool _disposed;
interface IComponentTypeCache { object? Get(Entity entity); void Set(Entity entity, object component); bool Remove(Entity entity); }
class ComponentTypeCache<T> : IComponentTypeCache, IDisposable where T : struct
{
readonly Dictionary<Entity, T> _cache = new();
readonly ReaderWriterLockSlim _lock = new();
public object? Get(Entity entity) { _lock.EnterReadLock(); try { return _cache.TryGetValue(entity, out var v) ? v : null; } finally { _lock.ExitReadLock(); } }
public void Set(Entity entity, object component) { _lock.EnterWriteLock(); try { _cache[(Entity)entity] = (T)component; } finally { _lock.ExitWriteLock(); } }
public bool Remove(Entity entity) { _lock.EnterWriteLock(); try { return _cache.Remove(entity); } finally { _lock.ExitWriteLock(); } }
public void Dispose() => _lock.Dispose();
}
record HandlerEntry(object Handler, ExecutionMode Mode);
record SubscriptionInfo(Type ComponentType, Delegate Added, Delegate Removed, Delegate Updated);
record MethodDelegates(Action<object, Entity, object> Added, Action<object, Entity, object> Removed, Action<object, Entity, object, object> Changed);
static readonly MethodInfo OnAddedMethodDef = typeof(IComponentLifecycleHandler<>).GetMethod("OnAdded", BindingFlags.Public | BindingFlags.Instance)!;
static readonly MethodInfo OnRemovedMethodDef = typeof(IComponentLifecycleHandler<>).GetMethod("OnRemoved", BindingFlags.Public | BindingFlags.Instance)!;
static readonly MethodInfo OnChangedMethodDef = typeof(IComponentLifecycleHandler<>).GetMethod("OnChanged", BindingFlags.Public | BindingFlags.Instance)!;
public ComponentLifecycleManager(IComponentEventSourceAdapter adapter, bool disposeHandlersOnShutdown = false, IExecutionStrategy? executor = null)
{
_adapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
_disposeHandlersOnShutdown = disposeHandlersOnShutdown;
_executor = executor ?? new DefaultExecutionStrategy(SynchronizationContext.Current ?? new SynchronizationContext());
}
public ComponentLifecycleManager(IComponentEventSource eventSource, SynchronizationContext? uiContext = null, bool disposeHandlersOnShutdown = false)
: this(new ComponentEventSourceAdapter(eventSource), disposeHandlersOnShutdown, uiContext == null ? null : new DefaultExecutionStrategy(uiContext)) { }
public ComponentLifecycleManager(IGenericComponentEventSource eventSource, SynchronizationContext? uiContext = null, bool disposeHandlersOnShutdown = false)
: this(new ComponentEventSourceAdapter(eventSource), disposeHandlersOnShutdown, uiContext == null ? null : new DefaultExecutionStrategy(uiContext)) { }
public void RegisterHandler<T>(IComponentLifecycleHandler<T> handler, ExecutionMode mode = ExecutionMode.Sync) where T : struct
{
if (handler == null) throw new ArgumentNullException(nameof(handler));
CheckDisposed();
var type = typeof(T);
using (Lock.Write(_lock))
{
_handlers[type] = (_handlers.TryGetValue(type, out var list) ? list : ImmutableList<HandlerEntry>.Empty).Add(new(handler, mode));
if (_handlers[type].Count == 1 && _adapter.AsGeneric() is { } generic)
{
var (added, removed, updated) = (new Action<Entity, T>(OnAddedGeneric), new Action<Entity, T>(OnRemovedGeneric), new Action<Entity, T, T>(OnUpdatedGeneric));
generic.SubscribeAdded(added); generic.SubscribeRemoved(removed); generic.SubscribeUpdated(updated);
_genericSubscriptionInfos[type] = new(type, added, removed, updated);
_genericSubscribedTypes.Add(type);
}
if (!_subscribed)
{
_adapter.ComponentAdded += OnComponentAddedNonGeneric;
_adapter.ComponentRemoved += OnComponentRemovedNonGeneric;
_adapter.ComponentUpdated += OnComponentUpdatedNonGeneric;
_subscribed = true;
}
}
}
public void UnregisterHandler<T>(IComponentLifecycleHandler<T> handler) where T : struct
{
if (handler == null) throw new ArgumentNullException(nameof(handler));
CheckDisposed();
if (!_subscribed) return;
var type = typeof(T);
using (Lock.Write(_lock))
{
if (!_handlers.TryGetValue(type, out var list)) return;
var newList = list.RemoveAll(e => ReferenceEquals(e.Handler, handler));
if (newList.IsEmpty)
{
_handlers.Remove(type);
_genericSubscribedTypes.Remove(type);
if (_genericSubscriptionInfos.TryGetValue(type, out var info) && _adapter.AsGeneric() is { } generic)
{
if (info.Added is Action<Entity, T> a) generic.UnsubscribeAdded(a);
if (info.Removed is Action<Entity, T> r) generic.UnsubscribeRemoved(r);
if (info.Updated is Action<Entity, T, T> u) generic.UnsubscribeUpdated(u);
_genericSubscriptionInfos.Remove(type);
}
}
else _handlers[type] = newList;
}
}
void OnAddedGeneric<T>(Entity entity, T component) where T : struct { if (_disposed) return; GetCacheForType<T>().Set(entity, component); ForEachHandler(entity, component, (h, e, c) => h.OnAdded(e, c)); }
void OnRemovedGeneric<T>(Entity entity, T component) where T : struct { if (_disposed) return; GetCacheForType<T>().Remove(entity); ForEachHandler(entity, component, (h, e, c) => h.OnRemoved(e, c)); }
void OnUpdatedGeneric<T>(Entity entity, T newComponent, T oldComponent) where T : struct { if (_disposed) return; GetCacheForType<T>().Set(entity, newComponent); ForEachHandler(entity, newComponent, (h, e, _) => h.OnChanged(e, newComponent, oldComponent)); }
void ForEachHandler<T>(Entity entity, T component, Action<IComponentLifecycleHandler<T>, Entity, T> invoke) where T : struct
{
if (TryGetHandlers(typeof(T)) is not { } list) return;
foreach (var e in list)
if (e.Handler is IComponentLifecycleHandler<T> h)
{
Interlocked.Increment(ref _executingCount);
_executor.Execute(() => { try { invoke(h, entity, component); } finally { Interlocked.Decrement(ref _executingCount); } }, e.Mode);
}
}
void OnComponentAddedNonGeneric(Entity entity, Type type)
{
if (_disposed) return;
var (handlers, isGeneric) = GetHandlersAndGenericFlag(type);
if (isGeneric || handlers == null || handlers.IsEmpty) return;
if (_adapter.GetComponent(entity, type) is not { } comp) return;
UpdateCache(entity, type, comp);
InvokeHandlers(entity, type, comp, null, true);
}
void OnComponentRemovedNonGeneric(Entity entity, Type type)
{
if (_disposed) return;
var (handlers, isGeneric) = GetHandlersAndGenericFlag(type);
if (isGeneric || handlers == null || handlers.IsEmpty) return;
object? old = TryGetFromCache(entity, type);
if (old == null) return;
RemoveFromCache(entity, type);
InvokeHandlers(entity, type, old, null, false);
}
void OnComponentUpdatedNonGeneric(Entity entity, Type type, object? oldComponent, object? newComponent)
{
if (_disposed || newComponent == null) return;
var (handlers, isGeneric) = GetHandlersAndGenericFlag(type);
if (isGeneric || handlers == null || handlers.IsEmpty) return;
UpdateCache(entity, type, newComponent);
InvokeHandlers(entity, type, newComponent, oldComponent ?? TryGetFromCache(entity, type), true);
}
(ImmutableList<HandlerEntry>? handlers, bool isGeneric) GetHandlersAndGenericFlag(Type type)
{
using (Lock.Read(_lock))
return (_handlers.TryGetValue(type, out var l) ? l : null, _genericSubscribedTypes.Contains(type));
}
void InvokeHandlers(Entity entity, Type type, object component, object? oldComponent, bool isAddOrUpdate)
{
var list = TryGetHandlers(type);
if (list == null) return;
var del = GetCachedDelegates(type);
foreach (var e in list)
{
Interlocked.Increment(ref _executingCount);
_executor.Execute(() =>
{
try
{
if (isAddOrUpdate)
if (oldComponent == null) del.Added(e.Handler, entity, component);
else del.Changed(e.Handler, entity, component, oldComponent);
else del.Removed(e.Handler, entity, component);
}
finally { Interlocked.Decrement(ref _executingCount); }
}, e.Mode);
}
}
ComponentTypeCache<T> GetCacheForType<T>() where T : struct => (ComponentTypeCache<T>)_componentCaches.GetOrAdd(typeof(T), _ => new ComponentTypeCache<T>());
IComponentTypeCache CreateCache(Type type) => (IComponentTypeCache)Activator.CreateInstance(typeof(ComponentTypeCache<>).MakeGenericType(type))!;
void UpdateCache(Entity entity, Type type, object component) => _componentCaches.GetOrAdd(type, CreateCache).Set(entity, component);
object? TryGetFromCache(Entity entity, Type type) => _componentCaches.TryGetValue(type, out var c) ? c.Get(entity) : null;
void RemoveFromCache(Entity entity, Type type) { if (_componentCaches.TryGetValue(type, out var c)) c.Remove(entity); }
ImmutableList<HandlerEntry>? TryGetHandlers(Type type)
{
if (_disposed) return null;
using (Lock.Read(_lock))
return _handlers.TryGetValue(type, out var l) ? l : null;
}
MethodDelegates GetCachedDelegates(Type type) => _handlerMethodDelegates.GetOrAdd(type, t =>
{
var closed = typeof(IComponentLifecycleHandler<>).MakeGenericType(t);
var onAdded = closed.GetMethod("OnAdded", BindingFlags.Public | BindingFlags.Instance)!;
var onRemoved = closed.GetMethod("OnRemoved", BindingFlags.Public | BindingFlags.Instance)!;
var onChanged = closed.GetMethod("OnChanged", BindingFlags.Public | BindingFlags.Instance)!;
static TDelegate Create<TDelegate>(MethodInfo method) where TDelegate : Delegate
{
var p = method.GetParameters();
var h = Expression.Parameter(typeof(object), "h");
var e = Expression.Parameter(typeof(Entity), "e");
var args = new List<Expression> { e };
var rest = new List<ParameterExpression>();
for (int i = 1; i < p.Length; i++)
{
var param = Expression.Parameter(typeof(object), $"a{i}");
rest.Add(param);
args.Add(Expression.Convert(param, p[i].ParameterType));
}
return Expression.Lambda<TDelegate>(Expression.Call(Expression.Convert(h, method.DeclaringType!), method, args), new[] { h, e }.Concat(rest)).Compile();
}
return new(Create<Action<object, Entity, object>>(onAdded), Create<Action<object, Entity, object>>(onRemoved), Create<Action<object, Entity, object, object>>(onChanged));
});
void CheckDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(ComponentLifecycleManager)); }
void UnsubscribeGenericEvents(IGenericComponentEventSource generic)
{
foreach (var kv in _genericSubscriptionInfos)
try
{
typeof(IGenericComponentEventSource).GetMethod("UnsubscribeAdded")!.MakeGenericMethod(kv.Key).Invoke(generic, new[] { kv.Value.Added });
typeof(IGenericComponentEventSource).GetMethod("UnsubscribeRemoved")!.MakeGenericMethod(kv.Key).Invoke(generic, new[] { kv.Value.Removed });
typeof(IGenericComponentEventSource).GetMethod("UnsubscribeUpdated")!.MakeGenericMethod(kv.Key).Invoke(generic, new[] { kv.Value.Updated });
}
catch { }
_genericSubscriptionInfos.Clear(); _genericSubscribedTypes.Clear();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
using (Lock.Write(_lock))
{
if (_subscribed)
{
_adapter.ComponentAdded -= OnComponentAddedNonGeneric;
_adapter.ComponentRemoved -= OnComponentRemovedNonGeneric;
_adapter.ComponentUpdated -= OnComponentUpdatedNonGeneric;
_subscribed = false;
}
if (_adapter.AsGeneric() is { } generic) UnsubscribeGenericEvents(generic);
}
while (Interlocked.CompareExchange(ref _executingCount, 0, 0) > 0) Thread.Sleep(1);
var disposables = new List<IDisposable>();
using (Lock.Write(_lock))
{
if (_disposeHandlersOnShutdown)
foreach (var list in _handlers.Values)
foreach (var e in list)
if (e.Handler is IDisposable d) disposables.Add(d);
foreach (var c in _componentCaches.Values) if (c is IDisposable d) disposables.Add(d);
_handlers.Clear(); _componentCaches.Clear(); _handlerMethodDelegates.Clear();
}
foreach (var d in disposables) try { d.Dispose(); } catch { }
_lock.Dispose();
}
}
}