尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity游戏开发中的模板方法模式:优化代码结构与提升可维护性

Unity游戏开发中的模板方法模式:优化代码结构与提升可维护性 1. 项目概述为什么要在Unity里搞懂模板方法模式如果你在Unity里写过一些稍微复杂点的系统比如战斗流程、UI管理器或者资源加载模块大概率遇到过这种场景一套流程的骨架是固定的但其中几个关键步骤的具体实现会因为角色职业、UI类型或者资源格式的不同而千差万别。新手最常见的做法是什么复制粘贴一大段代码然后修修改改。结果就是改一个Bug要动好几个地方加一个新功能又得再复制一遍代码很快就变成了一团乱麻。模板方法模式就是专门来治这个“病”的。它不是什么高深莫测的黑科技而是一种极其务实的设计思路。简单说它把一套固定的算法骨架比如“打开界面-加载数据-刷新显示-播放动画”定义在一个父类或抽象类里然后把其中可以变化的具体步骤抽象成虚方法或抽象方法留给子类去按需实现。这样算法的整体结构被稳定地封装起来避免了重复而变化的细节则被隔离到各个子类中易于扩展和维护。在Unity开发中这个模式的应用场景多到数不过来。比如所有敌人都要经历“出生-寻敌-攻击-死亡”这个流程但骷髅兵和法师的攻击方式能一样吗再比如你做一个任务系统“接取-执行-提交-领奖”这个流程是固定的但“执行”这一步可能是打怪、收集物品也可能是对话每种的实现逻辑都不同。用模板方法你只需要在父类Task里把流程框死然后为“打怪任务”、“收集任务”分别创建子类重写那个“执行”方法就行了。代码清晰职责分明新人接手也能一眼看懂整个系统的运转逻辑。所以今天我们不空谈理论就结合C#和Unity的特性把这个模式掰开了、揉碎了讲清楚。从为什么需要它到怎么手把手写出来再到Unity里有哪些“坑”和高级玩法最后分享几个我实际项目里用这个模式优化了代码结构的真实案例。目标是让你看完之后不仅能看懂更能立刻在项目里用起来实实在在地提升代码质量。2. 核心思路拆解模板方法模式的“道”与“术”理解一个设计模式最关键的不是背下它的UML图而是搞明白它解决什么痛点以及它设计思路背后的权衡。模板方法模式的核心可以概括为“好莱坞原则”别打电话给我们我们会打给你Don‘t call us, we’ll call you。意思是父类掌控着程序流程的主动权子类只需要被动地提供某些步骤的具体实现。父类是导演子类是演员。2.1 模式的结构与角色一个标准的模板方法模式通常包含两类角色抽象类AbstractClass这是模式的“大脑”和“骨架”。它定义了一个模板方法Template Method这是一个具体方法里面用固定的顺序调用了一系列其他方法。这个方法通常被声明为final在C#中是sealed以防止子类重写整个算法结构确保流程的稳定性。在Unity的C#中我们常用sealed关键字或者简单地通过不标记为virtual来实现。它定义了一系列抽象操作Primitive Operations这些是模板方法中所调用的步骤。它们被声明为abstract抽象方法或virtual虚方法。抽象方法强制子类必须实现虚方法则允许子类选择性地覆盖提供默认实现时。具体类ConcreteClass这是模式的“手”和“脚”。它继承自抽象类。它负责实现或重写父类中定义的抽象操作为算法的某些步骤提供具体的实现。一个具体类只需要关心它需要变化的那部分无需理会整体流程。2.2 C#与Unity中的实现特性在C#里实现这个模式有几个语言特性我们用得最顺手abstract关键字用于声明抽象类和抽象方法。抽象类不能实例化抽象方法没有方法体强制子类实现。virtual和override关键字virtual声明一个方法可以被子类重写override用于子类中明确表示要重写父类的虚方法。这给了我们灵活性子类可以选择性地改变某些步骤。sealed关键字可以用于方法防止该方法在派生类中被进一步重写。我们通常用它来“锁死”模板方法本身。在Unity中我们还要特别注意MonoBehaviour的生命周期。Unity的Start(),Update(),OnEnable()等本身就是一种“模板方法”——引擎定义了它们何时被调用你只需要在里面填写具体逻辑。当我们自己的模板方法需要与这些生命周期协同工作时设计要格外小心避免循环调用或执行顺序混乱。2.3 何时该用何时不该用使用模板方法模式的典型场景多个类有相同的主要流程但部分步骤的实现不同。这是最直接的信号。你需要控制子类的扩展点只允许它们修改算法的特定部分从而保护核心流程不被破坏。你想将公共行为提取到父类避免代码重复这是面向对象的基本原则。不适合使用模板方法模式的情况如果算法的每一步都可能剧烈变化那么模板方法里会充满大量的抽象方法导致子类实现负担很重这时可能策略模式Strategy Pattern更合适。如果子类需要频繁地改变算法的整体结构而不仅仅是某些步骤那这个模式就成了枷锁。在Unity中如果某个流程严重依赖于MonoBehaviour的生命周期事件并且这些事件本身已经构成了清晰的流程强行套用模板方法可能会让代码更晦涩。实操心得判断该不该用的一个简单方法是画流程图。如果多个对象的流程图里大部分框步骤都一样只有少数几个框的内容不同那模板方法就是天作之合。如果每个对象的流程图都长得完全不一样那就得另寻他法了。3. 从零实现一个完整的Unity案例——游戏关卡流程控制器光说不练假把式。我们用一个Unity游戏里非常常见的需求——关卡流程控制来完整实现一遍模板方法模式。假设我们的游戏关卡都有固定的流程显示开始UI - 加载场景资源 - 初始化关卡 - 开始游戏 - 判断胜利/失败 - 显示结束UI。3.1 第一步定义抽象类算法骨架我们创建一个抽象类LevelControllerBase。注意它不继承MonoBehaviour因为我们希望它是个纯逻辑类便于管理和测试。实际的MonoBehaviour组件会持有它的实例。// LevelControllerBase.cs using UnityEngine; public abstract class LevelControllerBase { // 这就是我们的“模板方法”。它定义了通关的固定流程。 // 这里没有用sealed因为在这个例子中我们不禁止子类重写整个流程虽然通常不建议。 // 但为了清晰我们可以把它标记为 virtual 并提供一个默认实现子类除非有充分理由否则不应重写它。 public void PlayLevel() { Debug.Log($[{GetType().Name}] 关卡流程开始); OnShowStartUI(); OnLoadResources(); OnInitializeLevel(); OnGameStart(); // 假设游戏循环在外部如Update中驱动这里我们模拟一个胜利条件检查 bool isWin CheckWinCondition(); if(isWin) { OnLevelWin(); } else { OnLevelLose(); } OnShowEndUI(); Debug.Log($[{GetType().Name}] 关卡流程结束); } // 以下是需要子类实现的“抽象操作”Primitive Operations // 我们将它们定义为 protected abstract强制子类实现且对外部隐藏。 /// summary /// 显示关卡开始UI如关卡名称、目标提示 /// /summary protected abstract void OnShowStartUI(); /// summary /// 加载关卡特定资源如场景、怪物Prefab、音频 /// /summary protected abstract void OnLoadResources(); /// summary /// 初始化关卡数据如生成敌人、设置出生点 /// /summary protected abstract void OnInitializeLevel(); /// summary /// 游戏正式开始如启动计时器、解锁玩家控制 /// /summary protected virtual void OnGameStart() { // 提供一个默认的空实现。有些关卡可能不需要特殊的“开始”动作。 Debug.Log(游戏开始); } /// summary /// 检查胜利条件如击败所有敌人、到达终点 /// /summary protected abstract bool CheckWinCondition(); /// summary /// 关卡胜利时的处理 /// /summary protected abstract void OnLevelWin(); /// summary /// 关卡失败时的处理 /// /summary protected abstract void OnLevelLose(); /// summary /// 显示关卡结束UI如胜利/失败面板、结算信息 /// /summary protected abstract void OnShowEndUI(); }关键点解析PlayLevel()方法就是模板方法它像一个导演脚本严格规定了各个步骤的执行顺序。我们将所有可变的步骤都定义成了protected abstract方法。protected保证了这些细节只对子类可见对外部调用者隐藏。abstract强制每个子类都必须给出自己的实现。OnGameStart()被设计成了virtual方法并提供了默认实现。这意味着有些关卡可能不需要特殊的开始处理直接沿用父类的空操作即可这增加了灵活性。3.2 第二步创建具体子类实现变化部分现在我们创建两个具体的关卡控制器一个简单的“击杀所有敌人”关卡和一个需要“限时抵达终点”的关卡。// KillAllEnemiesLevelController.cs using UnityEngine; public class KillAllEnemiesLevelController : LevelControllerBase { private int _totalEnemies 5; private int _defeatedEnemies 0; private GameObject _winUIPrefab; private GameObject _loseUIPrefab; protected override void OnShowStartUI() { // 假设我们有一个UI管理器这里简化为Debug.Log Debug.Log( 关卡目标击杀所有敌人5个); // 实际项目中这里可能会调用 UIManager.Instance.ShowLevelStartPanel(击杀所有敌人); } protected override void OnLoadResources() { Debug.Log(加载敌人Prefab、战斗音效...); // Resources.Load 或 Addressables.LoadAssetAsync _winUIPrefab Resources.LoadGameObject(UI/WinPanel); _loseUIPrefab Resources.LoadGameObject(UI/LosePanel); } protected override void OnInitializeLevel() { Debug.Log(在地图指定位置生成5个敌人...); _defeatedEnemies 0; // 重置击败数 // 这里会有生成敌人的逻辑并为每个敌人注册死亡事件 // enemy.OnDeath () { _defeatedEnemies; }; } protected override bool CheckWinCondition() { // 简单的检查击败数是否等于总数 // 实际游戏中这里可能会在Update中被频繁调用或由事件触发 Debug.Log($检查胜利条件已击败{_defeatedEnemies}/{_totalEnemies}个敌人); return _defeatedEnemies _totalEnemies; } protected override void OnLevelWin() { Debug.Log(太棒了你击败了所有敌人); Object.Instantiate(_winUIPrefab); // 实例化胜利UI // 播放胜利音效、发放奖励等 } protected override void OnLevelLose() { Debug.Log(任务失败...); Object.Instantiate(_loseUIPrefab); // 实例化失败UI // 播放失败音效 } protected override void OnShowEndUI() { // 在这个例子中胜利/失败UI已经在 OnLevelWin/Lose 中显示了。 // 所以这个方法可能只是隐藏一些通用的HUD或者什么都不做。 Debug.Log(隐藏关卡内HUD...); } // 提供一个公共方法供敌人死亡时调用 public void OnEnemyDefeated() { _defeatedEnemies; Debug.Log($敌人被击败当前进度{_defeatedEnemies}/{_totalEnemies}); } }// ReachGoalInTimeLevelController.cs using UnityEngine; public class ReachGoalInTimeLevelController : LevelControllerBase { private float _timeLimit 60f; // 60秒限制 private float _timeElapsed 0f; private bool _playerReachedGoal false; private Transform _player; private Transform _goalPoint; protected override void OnShowStartUI() { Debug.Log($ 关卡目标在{_timeLimit}秒内抵达终点 ); } protected override void OnLoadResources() { Debug.Log(加载终点旗帜Prefab、计时器UI...); _goalPoint new GameObject(GoalPoint).transform; _goalPoint.position new Vector3(10, 0, 10); // 假设终点位置 } protected override void OnInitializeLevel() { Debug.Log(设置玩家出生点初始化计时器...); _player GameObject.FindGameObjectWithTag(Player).transform; _timeElapsed 0f; _playerReachedGoal false; } protected override void OnGameStart() { // 这个关卡需要特殊的“开始”处理启动计时器 base.OnGameStart(); // 可以选择性调用父类的默认实现 Debug.Log(计时开始); // 这里可以开始一个协程或者在其他地方更新_timeElapsed } protected override bool CheckWinCondition() { // 两个条件1. 玩家到达终点2. 用时未超时。 // 这里简化处理假设有一个方法在玩家到达终点时会调用MarkGoalReached bool isWin _playerReachedGoal _timeElapsed _timeLimit; Debug.Log($检查胜利条件到达终点{_playerReachedGoal}用时{_timeElapsed}/{_timeLimit} 结果{isWin}); return isWin; } protected override void OnLevelWin() { Debug.Log(恭喜你在规定时间内到达了终点); } protected override void OnLevelLose() { if (_timeElapsed _timeLimit) { Debug.Log(时间到了任务失败。); } else { Debug.Log(任务失败。); // 其他失败原因 } } protected override void OnShowEndUI() { Debug.Log(显示结算界面展示用时和评级...); } // 供其他系统调用的方法 public void MarkGoalReached() _playerReachedGoal true; public void UpdateTime(float deltaTime) _timeElapsed deltaTime; }3.3 第三步在Unity中组装与调用最后我们需要一个MonoBehaviour组件来驱动这个流程。通常这会是一个GameManager或LevelManager。// LevelManager.cs using UnityEngine; public class LevelManager : MonoBehaviour { public enum LevelType { KillAll, ReachGoal } [SerializeField] private LevelType _currentLevelType; private LevelControllerBase _currentLevelController; void Start() { // 根据关卡类型创建具体的控制器 switch (_currentLevelType) { case LevelType.KillAll: _currentLevelController new KillAllEnemiesLevelController(); break; case LevelType.ReachGoal: _currentLevelController new ReachGoalInTimeLevelController(); break; default: Debug.LogError(未知的关卡类型); return; } // 执行模板方法启动关卡流程 _currentLevelController.PlayLevel(); } void Update() { // 如果是限时关卡需要在这里更新时间 if (_currentLevelController is ReachGoalInTimeLevelController timeLevel) { timeLevel.UpdateTime(Time.deltaTime); // 可以每帧或定期检查 CheckWinCondition这里简化为在PlayLevel中一次性检查 } } // 假设这个方法会被敌人死亡事件调用 public void NotifyEnemyDefeated() { if (_currentLevelController is KillAllEnemiesLevelController killLevel) { killLevel.OnEnemyDefeated(); // 这里可以实时检查胜利条件或者等待一个显式的检查点 } } // 假设这个方法会被玩家触发 public void NotifyGoalReached() { if (_currentLevelController is ReachGoalInTimeLevelController reachLevel) { reachLevel.MarkGoalReached(); } } }这样做的优势一目了然流程统一且清晰所有关卡的启动、运行、结束都遵循PlayLevel()定义的步骤不会出现某个关卡忘了显示结束UI的情况。代码复用性高公共的流程控制逻辑都在LevelControllerBase里子类只关心自己特有的逻辑。易于扩展要加一个新类型的关卡比如“解谜关卡”只需要新建一个类继承LevelControllerBase实现那几个抽象方法即可。LevelManager的Start()方法里加一个case就行其他代码几乎不用动。便于维护如果想修改所有关卡的通用流程比如在OnLoadResources前后加一个加载动画只需要改父类LevelControllerBase一处。注意事项在这个例子中为了简化胜利条件检查是放在PlayLevel()里一次性完成的。在真实游戏中CheckWinCondition()可能需要在Update中持续检查或者由特定事件如敌人死亡、玩家到达某地触发。这时模板方法PlayLevel()可能只负责初始化而将循环检查的逻辑交给MonoBehaviour的生命周期或事件系统。这并不违背模板方法模式只是“算法骨架”的定义变得更灵活可能是一个“初始化模板方法”加上一个“每帧更新模板方法”。关键在于变化的步骤如何检查胜利仍然被抽象出来由子类实现。4. 深入剖析Unity中的高级应用与避坑指南掌握了基础实现后我们来看看在Unity工程实践中模板方法模式有哪些更高级的用法和需要注意的“坑”。4.1 与Unity生命周期和协程的结合Unity是帧驱动的很多操作如加载资源、播放序列动画是异步的。我们的模板方法不能阻塞主线程。这时我们可以将模板方法改造为返回IEnumerator的协程。public abstract class AsyncLevelControllerBase : MonoBehaviour { // 模板方法变成了一个协程 public IEnumerator PlayLevelAsync() { Debug.Log($[{GetType().Name}] 异步关卡流程开始); yield return OnShowStartUIAsync(); yield return OnLoadResourcesAsync(); yield return OnInitializeLevelAsync(); yield return OnGameStartAsync(); // 等待胜利条件达成。这里需要子类提供一个“等待条件”的协程。 yield return WaitForWinCondition(); bool isWin CheckWinCondition(); // 最终检查 if(isWin) { yield return OnLevelWinAsync(); } else { yield return OnLevelLoseAsync(); } yield return OnShowEndUIAsync(); Debug.Log($[{GetType().Name}] 异步关卡流程结束); } protected abstract IEnumerator OnShowStartUIAsync(); protected abstract IEnumerator OnLoadResourcesAsync(); protected abstract IEnumerator OnInitializeLevelAsync(); protected virtual IEnumerator OnGameStartAsync() { yield break; // 默认空实现 } protected abstract IEnumerator WaitForWinCondition(); // 新的抽象方法等待条件满足 protected abstract bool CheckWinCondition(); protected abstract IEnumerator OnLevelWinAsync(); protected abstract IEnumerator OnLevelLoseAsync(); protected abstract IEnumerator OnShowEndUIAsync(); }这样设计的好处是每个步骤都可以包含yield return new WaitForSeconds()、yield return StartCoroutine(...)或者yield return AsyncOperation等异步操作完美契合Unity的资源加载、动画播放等需求。4.2 “钩子”方法Hook的妙用除了强制子类实现的抽象方法我们还可以提供一些带有默认实现的虚方法Hook钩子。子类可以视情况决定是否覆盖它们从而在算法的特定点插入自定义行为。public abstract class LevelControllerBaseWithHooks : LevelControllerBase { // 在模板方法的关键节点前后添加钩子 public new void PlayLevel() // 注意这里用new隐藏了父类的PlayLevel不推荐大量使用这里仅为演示 { OnLevelStart(); base.PlayLevel(); // 调用父类的原流程 OnLevelEnd(); } // 钩子方法默认空实现 protected virtual void OnLevelStart() { } protected virtual void OnLevelEnd() { } // 也可以在父类的PlayLevel内部插入钩子 // protected override void OnGameStart() // { // BeforeGameStartHook(); // 钩子 // base.OnGameStart(); // AfterGameStartHook(); // 钩子 // } // protected virtual void BeforeGameStartHook() { } // protected virtual void AfterGameStartHook() { } }钩子的应用场景比如你想在所有关卡开始前播放一段通用的开场动画在所有关卡结束后统一上传游戏数据。你可以在基类OnLevelStart和OnLevelEnd里写好这些通用逻辑。如果某个特殊关卡比如Boss关需要在开场动画前先播一段特殊剧情它就可以重写OnLevelStart先播自己的剧情然后调用base.OnLevelStart()播放通用动画。4.3 常见陷阱与避坑指南过度设计Over-engineering这是新手最容易犯的错。如果一个流程只有一两个简单的变体直接用if-else或者简单的继承可能更清晰。不要为了用模式而用模式。判断标准是当增加一个新的变体时你发现需要复制大量代码并修改多处这时候就该考虑模板方法了。模板方法过于庞大如果模板方法PlayLevel()里的步骤太多比如超过10步会导致父类难以理解和维护。此时应考虑将一些相关的步骤组合成新的方法或者审视是否应该将这个大流程拆分成几个更小的、独立的模板方法。子类破坏流程Liskov替换原则子类在重写虚方法时不应该改变该步骤在整体流程中的约定。例如OnLoadResources方法的本意是加载资源子类重写时不应该在里面偷偷地开始游戏逻辑否则会破坏父类设定的流程导致难以预料的行为。良好的命名和注释可以帮助约束这一点。与Unity组件生命周期混淆如果你的抽象类本身是MonoBehaviour并且模板方法比如一个Initialize()在Awake或Start中调用要小心子类也可能有Awake或Start。Unity会调用所有脚本的Awake和Start这可能导致父类的模板方法被子类的生命周期方法中的代码干扰。建议明确分工。要么让父类完全控制流程子类不实现Start要么使用“初始化调用”模式由父类在适当时机如Start内显式调用子类的初始化方法。对“算法骨架”的僵化理解模板方法定义的骨架不一定是线性的、一次性的。在游戏循环中骨架可能是一个“状态机”。比如一个EnemyAI的父类其模板方法UpdateAI()可能定义了“巡逻 - 发现玩家 - 追击 - 攻击 - 返回巡逻”的状态转换骨架而每个状态的具体行为如何巡逻、如何攻击则由子类实现。这依然是模板方法模式的灵活应用。实操心得在团队项目中应用模板方法模式文档和命名至关重要。一定要在抽象类和方法上写清XML注释说明每个步骤的职责、输入输出、以及子类重写时需要注意什么。比如“此方法仅用于加载资源切勿在此处初始化游戏逻辑初始化请放在OnInitializeLevel中。” 这能极大减少队友的困惑和误用。5. 实战扩展模板方法在Unity各系统中的应用案例模板方法模式在Unity中几乎无处不在下面我分享几个在真实项目中优化过的案例看看它如何解决实际问题。5.1 案例一可复用的UI弹窗系统问题游戏里有各种弹窗提示框、确认框、物品详情框它们都有类似的生命周期打开动画 - 获取数据并刷新 - 接收玩家输入 - 关闭动画。但每个弹窗的内容、动画和输入处理都不同。模板方法解决方案public abstract class UIPopupBase : MonoBehaviour { public void OpenPopup(object data null) { gameObject.SetActive(true); StartCoroutine(OpenPopupRoutine(data)); } private IEnumerator OpenPopupRoutine(object data) { // 1. 播放打开动画可选 yield return StartCoroutine(OnPlayOpenAnimation()); // 2. 用传入的数据刷新界面 OnRefreshUI(data); // 3. 等待玩家交互如点击确认、取消、关闭按钮 yield return StartCoroutine(WaitForUserInteraction()); // 4. 处理交互结果 OnHandleInteractionResult(); // 5. 播放关闭动画可选 yield return StartCoroutine(OnPlayCloseAnimation()); // 6. 关闭自己或通知管理器回收 OnClosePopup(); } protected virtual IEnumerator OnPlayOpenAnimation() { yield break; } protected abstract void OnRefreshUI(object data); protected abstract IEnumerator WaitForUserInteraction(); protected abstract void OnHandleInteractionResult(); protected virtual IEnumerator OnPlayCloseAnimation() { yield break; } protected virtual void OnClosePopup() { gameObject.SetActive(false); } }具体子类确认框public class ConfirmationPopup : UIPopupBase { [SerializeField] private Text _messageText; [SerializeField] private Button _confirmBtn; [SerializeField] private Button _cancelBtn; private bool _userConfirmed false; protected override void OnRefreshUI(object data) { if (data is string message) { _messageText.text message; } } protected override IEnumerator WaitForUserInteraction() { var waitEvent new System.Threading.Tasks.TaskCompletionSourcebool(); _confirmBtn.onClick.AddListener(() { _userConfirmed true; waitEvent.TrySetResult(true); }); _cancelBtn.onClick.AddListener(() { _userConfirmed false; waitEvent.TrySetResult(true); }); // 等待两个按钮中的任意一个被点击 yield return new WaitUntil(() waitEvent.Task.IsCompleted); // 移除监听防止重复触发 _confirmBtn.onClick.RemoveAllListeners(); _cancelBtn.onClick.RemoveAllListeners(); } protected override void OnHandleInteractionResult() { if (_userConfirmed) { Debug.Log(用户点击了确认); // 执行确认后的逻辑... } else { Debug.Log(用户点击了取消); // 执行取消后的逻辑... } } }效果所有弹窗的行为一致管理方便。新增一个弹窗类型只需关注内容填充和交互处理打开关闭动画等通用逻辑无需重复编写。5.2 案例二差异化的人物状态机问题游戏中有英雄、怪物等多种角色它们都有“闲置-移动-攻击-受伤-死亡”等状态但每个状态下的具体行为动画、音效、逻辑差异很大。模板方法解决方案简化版状态机public abstract class CharacterStateMachine : MonoBehaviour { protected enum State { Idle, Move, Attack, Hurt, Die } protected State _currentState; void Update() { // 模板方法每帧根据当前状态执行对应的行为 switch (_currentState) { case State.Idle: OnIdleUpdate(); break; case State.Move: OnMoveUpdate(); break; case State.Attack: OnAttackUpdate(); break; // ... 其他状态 } } // 状态进入、更新、退出 的钩子 protected virtual void OnEnterIdle() {} protected abstract void OnIdleUpdate(); protected virtual void OnExitIdle() {} protected virtual void OnEnterMove() {} protected abstract void OnMoveUpdate(); protected virtual void OnExitMove() {} // ... 其他状态类似 // 状态切换方法封装了“退出旧状态 - 切换状态 - 进入新状态”的固定流程 public void ChangeState(State newState) { // 退出旧状态 switch (_currentState) { case State.Idle: OnExitIdle(); break; case State.Move: OnExitMove(); break; // ... } // 切换状态 _currentState newState; // 进入新状态 switch (_currentState) { case State.Idle: OnEnterIdle(); break; case State.Move: OnEnterMove(); break; // ... } } }具体子类英雄public class HeroStateMachine : CharacterStateMachine { protected override void OnIdleUpdate() { // 英雄的闲置逻辑播放呼吸动画检测输入 if(Input.GetKeyDown(KeyCode.Space)) { ChangeState(State.Attack); } } protected override void OnMoveUpdate() { // 英雄的移动逻辑根据输入移动播放跑步动画 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); transform.Translate(new Vector3(h, 0, v) * Time.deltaTime * 5f); } // ... 实现其他抽象方法 }效果状态机的框架被固化在父类中确保了状态切换的逻辑正确不会忘记调用退出和进入方法。不同角色只需实现自己特有的状态行为即可。5.3 案例三资源加载策略模板问题游戏不同阶段启动、关卡内、切换场景需要不同的资源加载策略同步加载、异步加载、使用Addressables但加载的流程“计算依赖-加载-实例化-回调”是相似的。模板方法解决方案public abstract class ResourceLoaderBase { public GameObject LoadPrefab(string path, Transform parent null) { // 1. 前置处理如记录加载开始时间 OnBeforeLoad(path); // 2. 执行具体的加载逻辑由子类实现 Object loadedAsset PerformLoad(path); // 3. 实例化如果有需要 GameObject instance null; if (loadedAsset is GameObject prefab) { instance Object.Instantiate(prefab, parent); } // 4. 后置处理如记录加载结束时间、触发事件 OnAfterLoad(path, loadedAsset, instance); return instance; } protected virtual void OnBeforeLoad(string path) { Debug.Log($开始加载资源: {path}); } protected abstract Object PerformLoad(string path); protected virtual void OnAfterLoad(string path, Object asset, GameObject instance) { Debug.Log($资源加载完成: {path}); if (instance ! null) { Debug.Log($实例化成功实例ID: {instance.GetInstanceID()}); } } }具体子类Resources同步加载器public class ResourcesLoader : ResourceLoaderBase { protected override Object PerformLoad(string path) { // 移除了可能的Resources/前缀实际项目会更严谨 return Resources.Load(path); } }具体子类Addressables异步加载器public class AddressablesLoader : ResourceLoaderBase { protected override Object PerformLoad(string path) { // 注意这里简化了实际Addressables是异步的需要更复杂的处理如协程、async/await // 仅为展示模式应用 var handle Addressables.LoadAssetAsyncGameObject(path); // 通常我们会等待handle完成这里简化返回 return handle.WaitForCompletion(); // 注意WaitForCompletion可能阻塞主线程 } protected override void OnBeforeLoad(string path) { base.OnBeforeLoad(path); Debug.Log(使用Addressables系统加载...); } }效果加载策略同步/异步/远程可以轻松切换而加载的通用逻辑日志、实例化、回调只需写一次。当我们需要从Resources迁移到Addressables时只需替换使用的Loader子类业务逻辑代码几乎不用改。通过这些案例可以看到模板方法模式的价值在于它提炼了流程中的不变部分隔离了变化部分。它让我们的代码在面对相似但略有不同的需求时能够保持结构清晰、易于扩展而不是陷入复制粘贴和散弹式修改的泥潭。在Unity这种组件化、事件驱动的环境中合理运用模板方法模式能显著提升框架代码的健壮性和可读性。
返回列表