本文最后更新于 2026 年 1 月 22 日。
摘要
本文是《Unity 电子书|ScriptableObject 模块化架构》系列文章第四篇,Delegate Object,委托对象模式。把ScriptableObject 当做委托,存储逻辑方法,给其他模块调用,实现策略模式和可插拔行为。
核心概念:使用 SO 资产存储方法
该模式的核心非常直观:
一个 MonoBehaviour 组件持有一个对 ScriptableObject 资产的引用。
当 MonoBehaviour 需要执行某个特定任务时,它不自己实现具体逻辑,而是调用其引用的 ScriptableObject 内存储的方法。
当 ScriptableObject 被用来封装和委托行为逻辑时,我们称之为委托对象(Delegate Object)模式。它允许 MonoBehaviour 将特定任务的执行委托给一个外部的、可替换的 ScriptableObject 资产。
在 Unity 中,ScriptableObjects 不仅仅是数据容器,它们也可以包含方法,这意味着它们可以同时持有数据(用什么)和逻辑(做什么)。这种将算法封装到独立对象中的思想,源自“四人帮”(Gang of Four)经典设计模式中的策略模式(Strategy Pattern)。
关键限制
在使用此模式时,需要注意 ScriptableObject 的两个关键限制:
生命周期方法不会自动调用:ScriptableObject 上的方法(如 Start()、Update())不会像 MonoBehaviour 那样被 Unity 引擎自动调用。你必须在 MonoBehaviour 中手动调用它们。
不能直接引用场景对象:作为项目资产,ScriptableObject 不能直接引用场景中的 GameObject。如果其逻辑需要操作场景对象(例如,移动一个敌人),你需要将该场景对象作为参数传递给 ScriptableObject 的方法。通常,MonoBehaviour 会将自身(this)或其他依赖项传递过去。
重要使用场景:可插拔行为
委托对象模式最强大的应用是创建可插拔、可互换的行为。
通过将 ScriptableObject 定义为抽象类(abstract class),我们可以为一系列兼容的行为创建一个模板。然后,可以创建多个具体的 ScriptableObject 子类,每个子类实现一种不同的算法。
这些具体的行为资产在编辑器中是可互换的,设计师只需通过拖放就能改变一个对象的行为,而无需修改任何代码。
实例:敌人 AI 系统
假设我们要为游戏中的敌人单位定义不同的行为,例如巡逻(Patrol)、待机(Idle)或逃跑(Flee)。
定义抽象的 AI 行为基类
我们首先创建一个抽象的 ScriptableObject 作为所有 AI 行为的模板。
// EnemyAI.cs
using UnityEngine;
publicabstractclassEnemyAI : ScriptableObject
{
// 抽象方法,需要子类实现
// 参数 enemyUnit 提供了对场景中游戏对象的访问
publicabstractvoidMoveUnit(MonoBehaviour enemyUnit);
}
创建具体的 AI 行为
接着,我们创建具体的行为类,继承自 EnemyAI 并实现 MoveUnit 方法。
// PatrolAI.cs
[CreateAssetMenu(fileName = "PatrolAI", menuName = "Enemy AI/Patrol")]
publicclassPatrolAI : EnemyAI
{
publicoverridevoidMoveUnit(MonoBehaviour enemyUnit)
{
// 实现巡逻逻辑...
// 例如:enemyUnit.transform.Translate(Vector3.forward * Time.deltaTime);
Debug.Log("Patrolling...");
}
}
// FleeAI.cs
[CreateAssetMenu(fileName = "FleeAI", menuName = "Enemy AI/Flee")]
publicclassFleeAI : EnemyAI
{
publicoverridevoidMoveUnit(MonoBehaviour enemyUnit)
{
// 实现逃跑逻辑...
Debug.Log("Fleeing!");
}
}
在 MonoBehaviour 中使用
最后,EnemyUnit 只需要持有一个 EnemyAI 的引用,并在 Update 中调用其方法。
// EnemyUnit.cs
using UnityEngine;
publicclassEnemyUnit : MonoBehaviour
{
// 在 Inspector 中拖放 PatrolAI.asset 或 FleeAI.asset
public EnemyAI enemyAI;
voidUpdate()
{
if (enemyAI != null)
{
// 将自身作为参数传递,让 AI 逻辑可以操作这个 GameObject
enemyAI.MoveUnit(this);
}
}
}
通过这种方式,设计师可以在编辑器中轻松切换敌人的行为。此外,代码还可以在运行时更改 enemyAI 引用,以响应游戏事件(例如,当敌人生命值低时,将其 AI 行为切换到 FleeAI)。
使用枚举(Enum)也能在编辑器中切换行为,但新增行为时必须修改代码,这违背了“对修改关闭”的原则。
而委托对象模式只需创建新的行为资产,无需改动现有代码,真正实现了对扩展开放、对修改关闭,更具灵活性和可维护性。
实例:音频委托
委托对象中的行为不一定很复杂。例如,我们可以用它来为音效增加随机变化,避免单调重复。
定义抽象的音频委托
// AudioDelegateSO.cs
using UnityEngine;
publicabstractclassAudioDelegateSO : ScriptableObject
{
publicabstractvoidPlay(AudioSource source);
}
创建具体的音频委托
这个具体的实现会从一个列表中随机选择音频片段,并随机化音量和音高。
// SimpleAudioDelegateSO.cs
[CreateAssetMenu(fileName = "AudioDelegate", menuName = "Audio/Simple Audio Delegate")]
publicclassSimpleAudioDelegateSO : AudioDelegateSO
{
public AudioClip[] Clips;
[Range(0, 1)] publicfloat minVolume = 0.8f;
[Range(0, 1)] publicfloat maxVolume = 1.0f;
[Range(0, 2)] publicfloat minPitch = 0.9f;
[Range(0, 2)] publicfloat maxPitch = 1.1f;
publicoverridevoidPlay(AudioSource source)
{
if (Clips.Length == 0 || source == null) return;
source.clip = Clips[Random.Range(0, Clips.Length)];
source.volume = Random.Range(minVolume, maxVolume);
source.pitch = Random.Range(minPitch, maxPitch);
source.Play();
}
}
任何需要播放声音的 MonoBehaviour 都可以引用一个 SimpleAudioDelegateSO 资产,并调用其 Play 方法,从而轻松实现音效的多样化。
委托对象模式把多个音频从具体的 MonoBehaviour 组件上,移动到 SO 资产中,以便随时复用。
优缺点分析
优点
保持代码库可扩展:遵循 SOLID 中的“开闭原则”,可以在不修改现有 MonoBehaviour 的情况下添加新行为。
分离逻辑与 MonoBehaviour:将行为逻辑从 MonoBehaviour 中移出,使其更轻量、更专注于“驱动”行为,而不是实现行为。
对设计师友好:行为可以通过在编辑器中拖放来配置和切换,无需编写代码。
可在运行时切换行为:可以根据游戏状态动态地改变对象的行为,实现更复杂的逻辑。
更强的可伸缩性:随着团队成员或游戏设计的变化,项目更容易扩展和维护。
行为是项目资产:行为作为项目级资产存在,不依赖于特定场景,可在整个项目中复用。
缺点
需要更多前期设置:需要规划和编写更多的模板代码(例如抽象类和具体的实现类)。
方法必须手动调用:不像 MonoBehaviour 的生命周期方法那样会自动执行,增加了驱动代码的编写工作。
需要传递场景引用:如果逻辑需要操作场景对象,必须作为参数传递,可能使方法签名变得复杂。
对于简单项目可能过度设计:如果行为非常简单且固定不变,使用此模式会增加不必要的复杂性。
需要团队协调:程序员需要定义好基类和接口,并与设计师就如何配置和使用这些行为资产进行清晰的沟通。
适用场景
委托对象模式特别适用于以下场景:
具有多种行为类型或状态的敌人 AI 系统。
需要播放带有随机变化的音频系统。
任何需要可插拔、可替换算法的系统(如不同的寻路算法、武器效果、物品技能等)。
希望让设计师无需编码就能配置或调整对象行为的工作流。
需要在运行时根据游戏内条件动态切换行为的系统。
锐评
纯程序员角度
该模式是 SOLID 原则的优秀实践,实现了逻辑与驱动的分离,极大提升了代码的可维护性和扩展性。缺点是增加了抽象层和前期设计成本,需要为简单的行为编写更多模板代码,且必须手动调用逻辑,不像 MonoBehaviour 生命周期那样自动化。
纯策划角度
优势巨大。它将行为配置变成了拖拽资产,无需编程就能快速试验和迭代新玩法,极大提升了工作效率。缺点是强依赖程序员预先搭建好框架,且需要理解 SO 资产的组织方式。一旦框架有变,所有相关资产可能都需要调整。
独立开发者角度
一把双刃剑。对于小型、快速验证的项目,它可能过度设计,增加不必要的前期时间投入。但若项目有扩展计划,这套框架能提供长期收益,避免后期重构的痛苦。关键在于预判项目规模,避免为做“牛刀”而耽误“杀鸡”。
写在最后
ScriptableObject 模块化架构可以归纳为一种思维:一切皆可为资产。这是一种极端的方式,参考前置文章,我们把数据容器,变量,枚举都设计为了一个资产,而且是项目级别的资产。这样的思维方式是一个抽象的思维。和一切皆为状态机,一切都是技能 Buff 的思维类似。他们的核心目的都是把具体项目问题中的共同点,抽象为一类事物。抓住核心逻辑,以不同的方式实现。
SO 架构可能使用起来会比较麻烦,因为需要在编辑器中配置,这和编写代码是两种不同的方式,开发者可能需要不断切换工作方式,对于某些开发者,属于打断心流,有的开发者会觉得是平衡一下两种工作方式,降低心智负担,毕竟只选择单一方式,开发需要对单一方面有更深入的了解。