Unity MyFramework 用法说明(十四):使用 ComponentOwner 和 GameComponent 拆分对象能力

角色、界面和管理器的功能不断增加后,所有逻辑都堆在一个类中,会产生大量字段、生命周期函数和条件判断。

MyFramework 提供了一套纯 C# 组件系统,通过 ComponentOwnerGameComponent,把一个复杂对象拆分为多个相对独立的能力组件。

项目地址:

github.com/ZHOURUIH/My...

一、为什么还需要一套组件系统

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# 对象体系中。

通过这种方式,角色、界面、场景和管理器都可以由多个小组件组合而成,避免一个类承担越来越多互不相关的职责。

相关推荐
_zhourui_h_1 天前
Unity MyFramework 用法说明(十三):使用 StateMachine 管理互斥与共存的角色状态
unity3d
SmalBox1 天前
【节点】[UnpackWaterData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
Unity MyFramework 用法说明(十二):使用 DoubleBuffer 在多线程之间传递数据
unity3d
SmalBox2 天前
【节点】[PackWaterVertexData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
Unity MyFramework 用法说明(十一):使用 SafeList 在遍历过程中安全增删
unity3d
SmalBox3 天前
【节点】[EvaluateTipThickness节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_3 天前
Unity MyFramework 用法说明(十):使用 ClassObject 和 ClassScope 管理可复用对象
unity3d