Unity3D RPG Core | 20 接口实现观察者模式的订阅和广播
这节内容继续完善上节 GameManager 的功能:当角色人物死亡时,敌人播放胜利的动画。实现上述功能,我们采取观察者模式。即角色人物死亡时我们通知订阅的观察者,使其做出响应。
1. 添加胜利动画
我们首先为史莱姆添加胜利动画。如图 1 所示,我们为胜利动画再添加一个层。添加层的原因是,胜利动画是 Any State 可以转移的,在其他层的基础上添加,可能会出现层覆盖导致动画不生效的情况。

如果在 Base Layer 基础上添加胜利动画,需要确保动画先“回到” Base Layer。代码逻辑上会有点奇怪。
同时新增 Winning 变量,用于指示胜利动画的转移条件。
2. 实现观察者接口
如代码清单 1 所示,我们实现游戏结束的观察者接口。其中只有一个接口函数,它响应游戏结束时的事件。
- public interface IEndGameObserver
- {
- void OnEndGameEvent();
- }
如代码清单 2 所示,我们让之前的敌人控制器再继承观察者接口(第 1 行)。实现的接口在第 54 至 57 行,胜利响应的结果就是将状态变更为胜利状态(第 8 行添加)。在状态转移函数中我们实现胜利状态的逻辑(第 44 至 50 行),它将胜利变量设置为真,并取消其余状态变量。设置的变量参与每帧动画变量设置操作(第 37 行)。
- public enum EnemyStates
- {
- GUARD,
- PATROL,
- PURSUIT,
- DEATH,
- RETURN,
- WIN,
- }
- public class EnemyController : MonoBehaviour, IEndGameObserver
- {
- struct AnimatorState
- {
- // Base Layer
- public bool isWalking;
- public bool isSensing;
- // Attack Layer
- public bool isPursuing;
- public bool isFollowing;
- // Victory Layer
- public bool isWinning;
- }
- AnimatorState m_animatorState;
- void Awake()
- {
- m_animatorState.isWinning = false;
- }
- void SwitchAnimation()
- {
- m_animator.SetBool("Walking", m_animatorState.isWalking);
- m_animator.SetBool("Sensing", m_animatorState.isSensing);
- m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
- m_animator.SetBool("Following", m_animatorState.isFollowing);
- m_animator.SetBool("Dead", m_characterData.CurrentHealth == 0);
- m_animator.SetBool("Winning", m_animatorState.isWinning);
- }
- void SwitchState()
- {
- switch (m_enemyStates)
- {
- case EnemyStates.WIN:
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = false;
- m_animatorState.isPursuing = false;
- m_animatorState.isFollowing = false;
- m_animatorState.isWinning = true;
- break;
- }
- }
- void IEndGameObserver.OnEndGameEvent()
- {
- m_enemyStates = EnemyStates.WIN;
- }
- }
3. 实现被观察者
为了方便我们直接让 GameManager 充当被观察者。如代码清单 3 所示,其中提供了添加观察者函数(AddEndGameObserver)、移除观察者函数(RemoveEndGameObserver)以及通知观察者函数(NotifyEndGame)。在 Update() 函数中,进行角色人物的血量跟踪,如果人物血量为 0,则通知观察者对游戏结束做出响应。
- public class GameManager : Singleton<GameManager>
- {
- public CharacterData m_playerData;
- List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();
- public void RegisterPlayerData(CharacterData playerData)
- {
- m_playerData = playerData;
- Debug.Log("RegisterPlayerData Done.");
- }
- public void AddEndGameObserver(IEndGameObserver observer)
- {
- endGameObservers.Add(observer);
- }
- public void RemoveEndGameObserver(IEndGameObserver observer)
- {
- endGameObservers.Remove(observer);
- }
- public void NotifyEndGame()
- {
- foreach (IEndGameObserver observer in endGameObservers)
- {
- observer.OnEndGameEvent();
- }
- }
- // Update is called once per frame
- void Update()
- {
- if (m_playerData.CurrentHealth == 0)
- NotifyEndGame();
- }
- }
最后要考虑的是添加和移除观察者。如代码清单 4 所示,我们将其放置在 OnEnable() 和 OnDisable() 周期。但是实际运行下来添加观察者放在 OnEnable() 周期中,此时 GameManager 的 Awake() 周期都还没被执行到,实例会为空。因此我们先将其放在 Start() 周期中。
- public class EnemyController : MonoBehaviour, IEndGameObserver
- {
- // Start is called before the first frame update
- void Start()
- {
- //FIXME:
- GameManager.GetInstance().AddEndGameObserver(this);
- }
- void OnEnable()
- {
- //GameManager.GetInstance().AddEndGameObserver(this);
- }
- void OnDisable()
- {
- GameManager.GetInstance().RemoveEndGameObserver(this);
- }
- }
单例基于 Unity 上下的周期,当时看着就觉得有定拿捏不稳(因为我对 Unity 的加载逻辑不了解)。
现在也不明白为什么 GameManager 脚本会比 EnemyController 脚本后加载。网上倒是找到可以指定脚本加载顺序的方法,但是这种方式也感觉怪怪的。
实验下来,Unity 各个脚本的初始化加载是同步的:因为我用 AutoResetEvent 想等 GameManager Awake() 执行完毕再返回实例,直接就卡住了。
这块留作问题。
为了验证效果,我们将角色人物的血量设置低一点。如图 2 所示,当角色人物死亡时,史莱姆播放胜利动画。
