系列定位:Unity 老版本兼容(.NET 3.5 / C# 4)+ 项目驱动。
本章目标:把“能跑”升级到“能长时间稳定跑”,让战斗 30~60 分钟连续运行仍可控。
学习目标
完成本章后,你应该能做到:
- 给核心对象定义清晰生命周期边界(创建、启用、停用、销毁)。
- 识别并修复常见泄漏源:事件未解绑、定时器未取消、池对象脏状态。
- 建立统一的资源注册表与销毁顺序。
- 输出可量化指标:对象存活数、池命中率、每分钟 GC 次数。
背景问题
战斗系统在前 13 章已经可用,但一旦长时间运行会出现:
- 内存缓慢上涨,最终卡顿或崩溃。
- 场景切换后“幽灵回调”继续触发。
- 对象池复用后出现旧状态污染。
- 回放/重开后行为异常,无法复现。
根因不是某一行代码,而是缺少统一生命周期治理模型。
生命周期分层模型
建议把对象分成 3 层:
- Session 级:一场战斗内存活(如 BattleWorld、SystemManager)。
- Entity 级:随角色/子弹生成销毁(Actor、Projectile)。
- Frame 级:每帧临时对象(命中列表、排序缓存、事件缓冲)。
治理原则:
- Session 级只能在 Match Begin/End 边界创建与清理。
- Entity 级优先池化,禁止频繁
new/destroy。 - Frame 级必须复用容器,不允许泄漏到下一帧。
核心接口:可治理生命周期
public interface ILifecycle
{
void OnCreate();
void OnEnable();
void OnDisable();
void OnDestroy();
}
public sealed class LifecycleHost
{
private readonly List<ILifecycle> _items = new List<ILifecycle>(64);
public void Register(ILifecycle item)
{
if (item == null)
{
return;
}
_items.Add(item);
item.OnCreate();
item.OnEnable();
}
public void DisableAll()
{
for (var i = _items.Count - 1; i >= 0; i--)
{
_items[i].OnDisable();
}
}
public void DestroyAll()
{
for (var i = _items.Count - 1; i >= 0; i--)
{
_items[i].OnDestroy();
}
_items.Clear();
}
}
倒序销毁是关键:避免上层已销毁而下层还在回调。
项目驱动:战斗模块治理落地
1. 事件订阅自动解绑
public sealed class EventSubscriptionGroup : ILifecycle
{
private readonly EventBus _eventBus;
private readonly List<string> _keys = new List<string>(16);
private readonly List<Action<object>> _handlers = new List<Action<object>>(16);
public EventSubscriptionGroup(EventBus eventBus)
{
_eventBus = eventBus;
}
public void Bind(string key, Action<object> handler)
{
_eventBus.Subscribe(key, handler);
_keys.Add(key);
_handlers.Add(handler);
}
public void OnCreate() { }
public void OnEnable() { }
public void OnDisable()
{
for (var i = 0; i < _keys.Count; i++)
{
_eventBus.Unsubscribe(_keys[i], _handlers[i]);
}
}
public void OnDestroy()
{
_keys.Clear();
_handlers.Clear();
}
}
2. 调度器任务令牌统一回收
public sealed class SchedulerTokenGroup : ILifecycle
{
private readonly Scheduler _scheduler;
private readonly List<int> _tokens = new List<int>(32);
public SchedulerTokenGroup(Scheduler scheduler)
{
_scheduler = scheduler;
}
public void Add(int token)
{
if (token > 0)
{
_tokens.Add(token);
}
}
public void OnCreate() { }
public void OnEnable() { }
public void OnDisable()
{
for (var i = 0; i < _tokens.Count; i++)
{
_scheduler.Cancel(_tokens[i]);
}
_tokens.Clear();
}
public void OnDestroy() { }
}
3. 池对象脏状态防线
public interface IPoolResettable
{
void ResetForPool();
}
public sealed class BulletRuntime : IPoolResettable
{
public int OwnerId;
public int TargetId;
public float Speed;
public bool Active;
public void ResetForPool()
{
OwnerId = 0;
TargetId = 0;
Speed = 0f;
Active = false;
}
}
public sealed class SafePool<T> where T : class, IPoolResettable
{
private readonly ObjectPool<T> _inner;
public SafePool(Func<T> factory, int initialCapacity)
{
_inner = new ObjectPool<T>(factory, null, delegate(T x) { x.ResetForPool(); }, initialCapacity);
}
public T Get()
{
return _inner.Get();
}
public void Release(T value)
{
_inner.Release(value);
}
}
资源注册表(场景级总控)
public sealed class BattleResourceRegistry : ILifecycle
{
public readonly LifecycleHost LifecycleHost;
public readonly EventSubscriptionGroup Subscriptions;
public readonly SchedulerTokenGroup SchedulerTokens;
public BattleResourceRegistry(EventBus eventBus, Scheduler scheduler)
{
LifecycleHost = new LifecycleHost();
Subscriptions = new EventSubscriptionGroup(eventBus);
SchedulerTokens = new SchedulerTokenGroup(scheduler);
}
public void OnCreate()
{
LifecycleHost.Register(Subscriptions);
LifecycleHost.Register(SchedulerTokens);
}
public void OnEnable() { }
public void OnDisable()
{
LifecycleHost.DisableAll();
}
public void OnDestroy()
{
LifecycleHost.DestroyAll();
}
}
这样场景结束时只做一次:
_registry.OnDisable();
_registry.OnDestroy();
运行时监控指标
最低限度建议每 5 秒输出一次:
alive_actor_countpool_inactive_bulletpending_scheduler_tasksgc_collections_gen0event_subscription_count
示例:
FoundationLog.Info(
"Health",
"actors=" + actorCount +
" bulletPoolInactive=" + bulletPool.CountInactive +
" schedulerPending=" + schedulerPending +
" gc0=" + GC.CollectionCount(0)
);
回归验收
- 30 分钟连续战斗后,内存曲线稳定在可控区间。
- 场景切换 20 次,无幽灵事件回调。
- 回放模式与实时模式切换 10 次,无状态污染。
- 对象池命中率达到预期(例如 > 90%)。
常见坑
坑 1:在 OnDestroy 才解绑事件
对象停用后仍会收事件,导致无效逻辑执行。应该在 OnDisable 即解绑。
坑 2:池对象只重置一半字段
最容易引发“偶现 Bug”。必须建立统一 ResetForPool() 协议。
坑 3:清理顺序错误
先销毁 EventBus,再解绑订阅会触发异常。总是先解绑,再销毁总线。
本月作业
实现“战斗健康检查面板”:
- 实时展示上述 5 个运行指标。
- 指标超过阈值自动告警并落日志。
- 一键导出最近 60 秒监控快照与回放文件路径。
下一章进入 C# 路线收官阶段:工程化测试与回归基线,把前面所有基础设施纳入可自动验证流程。