角色、界面和管理器的功能不断增加后,所有逻辑都堆在一个类中,会产生大量字段、生命周期函数和条件判断。
MyFramework 提供了一套纯 C# 组件系统,通过 ComponentOwner 和 GameComponent,把一个复杂对象拆分为多个相对独立的能力组件。
项目地址:
一、为什么还需要一套组件系统
Unity 已经提供了 MonoBehaviour 组件,但并不是所有业务对象都适合挂在 GameObject 上。
例如一个玩家角色可能包含:
bash
模型管理
动作播放
状态机
技能数据
背包数据
装备数据
输入控制
全部放进角色类后,很容易变成:
bash
public class CharacterPlayer
{
protected Animator mAnimator;
protected List<Skill> mSkillList;
protected List<Item> mPackItems;
protected List<Equip> mEquipList;
protected CharacterState mState;
protected bool mCanControl;
protected bool mModelLoaded;
public void update(float elapsedTime)
{
updateAnimation(elapsedTime);
updateSkill(elapsedTime);
updateState(elapsedTime);
updateController(elapsedTime);
}
}
随着功能增加,这个类会不断扩大。
使用组件后,可以拆分为:
bash
CharacterPlayer
├── COMCharacterAvatar
├── COMCharacterAnimation
├── COMCharacterStateMachine
├── COMCharacterSkill
├── COMCharacterPack
├── COMCharacterEquip
└── COMCharacterController
角色只负责组合这些能力,不再直接实现所有细节。
二、ComponentOwner 和 GameComponent
框架中的核心关系是:
bash
ComponentOwner
└── 拥有多个 GameComponent
ComponentOwner 负责:
bash
创建和保存组件
控制组件启用状态
按照顺序更新组件
销毁并回收组件
根据类型查找组件
GameComponent 负责实现一个具体能力:
bash
public abstract class GameComponent : ClassObject
{
protected ComponentOwner mComponentOwner;
protected bool mIgnoreTimeScale;
protected bool mDefaultActive;
protected bool mActive;
public virtual void init(ComponentOwner owner)
{
mComponentOwner = owner;
}
public virtual void update(float elapsedTime) { }
public virtual void fixedUpdate(float elapsedTime) { }
public virtual void lateUpdate(float elapsedTime) { }
}
它不是 Unity 的 Component,也不会被挂到 GameObject 上。
它是一个由框架创建、更新和回收的纯 C# 对象。
三、创建一个角色生命值组件
先创建一个简单的生命值组件:
bash
public class COMCharacterHealth : GameComponent
{
protected int mHP;
protected int mMaxHP;
public void setHealth(int hp, int maxHP)
{
mHP = hp;
mMaxHP = maxHP;
}
public void setHP(int hp)
{
mHP = hp;
}
public int getHP()
{
return mHP;
}
public int getMaxHP()
{
return mMaxHP;
}
public bool isDead()
{
return mHP <= 0;
}
public override void resetProperty()
{
base.resetProperty();
mHP = 0;
mMaxHP = 0;
}
}
组件继承自 ClassObject,会由对象池复用,所以必须在 resetProperty() 中清理全部字段。
四、在角色中添加组件
Character 本身继承自 ComponentOwner,可以重写 initComponents():
bash
public class CharacterPlayer : Character
{
protected COMCharacterHealth mCOMHealth;
protected COMCharacterPack mCOMPack;
protected COMCharacterEquip mCOMEquip;
protected override void initComponents()
{
base.initComponents();
addInitComponent(out mCOMHealth, true);
addInitComponent(out mCOMPack, true);
addInitComponent(out mCOMEquip, true);
}
public override void resetProperty()
{
base.resetProperty();
mCOMHealth = null;
mCOMPack = null;
mCOMEquip = null;
}
public COMCharacterHealth getCOMHealth()
{
return mCOMHealth;
}
}
参数中的 true 表示组件创建后默认启用:
bash
addInitComponent(out mCOMHealth, true);
组件初始化顺序就是添加顺序:
bash
基类组件
↓
生命值组件
↓
背包组件
↓
装备组件
Character.init() 会自动调用 initComponents(),不需要业务代码再次手动初始化每个组件。
五、组件的自动更新
只要拥有者每帧调用:
bash
base.update(elapsedTime);
ComponentOwner 就会按照添加顺序更新全部启用组件:
bash
Owner.update
↓
组件1.update
↓
组件2.update
↓
组件3.update
例如创建一个自动恢复生命值的组件:
bash
public class COMCharacterHealthRecover : GameComponent
{
protected float mRecoverTimer;
public override void update(float elapsedTime)
{
mRecoverTimer += elapsedTime;
if (mRecoverTimer < 1.0f)
{
return;
}
mRecoverTimer = 0.0f;
CharacterPlayer character =
getOwner() as CharacterPlayer;
COMCharacterHealth health =
character?.getCOMHealth();
if (health == null ||
health.isDead() ||
health.getHP() >= health.getMaxHP())
{
return;
}
health.setHP(health.getHP() + 1);
}
public override void resetProperty()
{
base.resetProperty();
mRecoverTimer = 0.0f;
}
}
角色类不需要再增加:
bash
updateHealthRecover(elapsedTime);
只要把组件加入角色,它就会自动获得更新。
六、动态添加和获取组件
并不是所有组件都需要初始化时创建。
例如技能组件可以在角色第一次获得技能时再创建:
bash
COMCharacterSkill skill =
character.getOrAddComponent<COMCharacterSkill>();
getOrAddComponent<T>() 的规则是:
bash
组件已经存在
返回已有组件
组件不存在
创建、初始化并返回新组件
只查询、不自动创建:
bash
COMCharacterSkill skill =
character.getComponent<COMCharacterSkill>();
只获取当前启用的组件:
bash
COMCharacterSkill skill =
character.getActiveComponent<COMCharacterSkill>();
因此三种接口用途不同:
bash
getComponent
只查询
getActiveComponent
只查询当前启用的组件
getOrAddComponent
没有时自动创建
同一个拥有者不能添加两个完全相同类型的组件。
七、启用和停用组件
暂时不需要某个能力时,可以停用组件:
bash
character.activeComponent<COMCharacterHealthRecover>(false);
重新启用:
bash
character.activeComponent<COMCharacterHealthRecover>();
停用后的组件仍然存在,只是不再执行:
bash
update
fixedUpdate
lateUpdate
它保存的数据不会被销毁。
例如进入安全区后停止自动战斗:
bash
character.activeComponent<COMCharacterAutoBattle>(false);
离开安全区后重新启用:
bash
character.activeComponent<COMCharacterAutoBattle>();
这比反复创建和销毁组件更适合短时间关闭某项能力。
八、只暂停组件更新
有时不希望改变组件自身的启用状态,只想暂时跳过更新,可以使用:
bash
character.addDisableComponent<COMCharacterController>();
恢复更新:
bash
character.removeDisableComponent<COMCharacterController>();
它和 activeComponent(false) 的区别是:
bash
activeComponent(false)
真正修改组件的激活状态
addDisableComponent
只让拥有者暂时跳过该类型组件的更新
例如播放一段不可操作的剧情时,可以暂停控制器更新,但不需要销毁或重新初始化控制器。
九、让子类取消基类组件
基类可能默认创建了一些组件,但某个子类不需要。
例如 Character 默认会创建:
bash
COMCharacterAvatar
COMCharacterAnimation
COMCharacterStateMachine
某种特殊角色不需要这些能力,可以在调用基类初始化前声明:
bash
public override void init()
{
addDontAutoCreate<COMCharacterStateMachine>();
addDontAutoCreate<COMCharacterAnimation>();
addDontAutoCreate<COMCharacterAvatar>();
base.init();
}
当基类执行:
bash
addInitComponent(out mStateMachine, true);
框架会检测该类型是否被标记为不自动创建。
这种方式允许基类统一声明默认能力,同时允许子类按需裁剪。
十、组件的销毁与回收
拥有者销毁时,会自动销毁它拥有的全部组件:
bash
ComponentOwner.destroy
↓
逐个调用组件 destroy
↓
调用 resetProperty
↓
归还 ClassPool
组件内部如果注册了事件或持有外部对象,应在 destroy() 中清理:
bash
public override void destroy()
{
mEventSystem?.unlistenEvent(this);
base.destroy();
}
普通字段则在 resetProperty() 中重置:
bash
public override void resetProperty()
{
base.resetProperty();
mTarget = null;
mTimer = 0.0f;
}
角色中缓存的组件引用也需要重置为 null,避免对象池复用后保留旧引用。
十一、什么时候适合使用 GameComponent
适合拆成组件的功能通常具备以下特点:
bash
属于某个对象,而不是全局系统
拥有相对独立的数据和生命周期
可能只在部分对象上存在
可以独立启用或停用
需要跟随拥有者自动更新和销毁
典型例子包括:
bash
角色状态机
角色动画
角色背包
角色技能
摄像机跟随
对象拖拽
自动旋转
透明度渐变
界面交互
场景音效
如果功能是整个游戏唯一的全局管理器,更适合使用 FrameSystem。
如果只是一个短暂的业务操作,更适合封装成 Command。
十二、总结
MyFramework 的组件系统由两部分组成:
bash
ComponentOwner
负责组合、更新和销毁组件
GameComponent
负责实现一项具体能力
它与 Unity 的 MonoBehaviour 组件思想相似,但完全运行在框架的纯 C# 对象体系中。
通过这种方式,角色、界面、场景和管理器都可以由多个小组件组合而成,避免一个类承担越来越多互不相关的职责。