Unity3D RPG Core | 20 接口实现观察者模式的订阅和广播

这节内容继续完善上节 GameManager 的功能:当角色人物死亡时,敌人播放胜利的动画。实现上述功能,我们采取观察者模式。即角色人物死亡时我们通知订阅的观察者,使其做出响应。

1. 添加胜利动画

我们首先为史莱姆添加胜利动画。如图 1 所示,我们为胜利动画再添加一个层。添加层的原因是,胜利动画是 Any State 可以转移的,在其他层的基础上添加,可能会出现层覆盖导致动画不生效的情况。

图1 添加胜利动画

如果在 Base Layer 基础上添加胜利动画,需要确保动画先“回到” Base Layer。代码逻辑上会有点奇怪。

同时新增 Winning 变量,用于指示胜利动画的转移条件。

2. 实现观察者接口

如代码清单 1 所示,我们实现游戏结束的观察者接口。其中只有一个接口函数,它响应游戏结束时的事件。

代码清单 1 观察者接口
  1. public interface IEndGameObserver
  2. {
  3.     void OnEndGameEvent();
  4. }

如代码清单 2 所示,我们让之前的敌人控制器再继承观察者接口(第 1 行)。实现的接口在第 54 至 57 行,胜利响应的结果就是将状态变更为胜利状态(第 8 行添加)。在状态转移函数中我们实现胜利状态的逻辑(第 44 至 50 行),它将胜利变量设置为真,并取消其余状态变量。设置的变量参与每帧动画变量设置操作(第 37 行)。

代码清单 2 实现观察者接口
  1. public enum EnemyStates
  2. {
  3.     GUARD,
  4.     PATROL,
  5.     PURSUIT,
  6.     DEATH,
  7.     RETURN,
  8.     WIN,
  9. }
  10. public class EnemyController : MonoBehaviour, IEndGameObserver
  11. {
  12.     struct AnimatorState
  13.     {
  14.         // Base Layer
  15.         public bool isWalking;
  16.         public bool isSensing;
  17.         // Attack Layer
  18.         public bool isPursuing;
  19.         public bool isFollowing;
  20.         // Victory Layer
  21.         public bool isWinning;
  22.     }
  23.     AnimatorState m_animatorState;
  24.  
  25.     void Awake()
  26.     {
  27.         m_animatorState.isWinning = false;
  28.     }
  29.  
  30.     void SwitchAnimation()
  31.     {
  32.         m_animator.SetBool("Walking", m_animatorState.isWalking);
  33.         m_animator.SetBool("Sensing", m_animatorState.isSensing);
  34.         m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
  35.         m_animator.SetBool("Following", m_animatorState.isFollowing);
  36.         m_animator.SetBool("Dead", m_characterData.CurrentHealth == 0);
  37.         m_animator.SetBool("Winning", m_animatorState.isWinning);
  38.     }
  39.  
  40.     void SwitchState()
  41.     {
  42.         switch (m_enemyStates)
  43.         {
  44.             case EnemyStates.WIN:
  45.                 m_animatorState.isWalking = false;
  46.                 m_animatorState.isSensing = false;
  47.                 m_animatorState.isPursuing = false;
  48.                 m_animatorState.isFollowing = false;
  49.                 m_animatorState.isWinning = true;
  50.                 break;
  51.         }
  52.     }
  53.  
  54.     void IEndGameObserver.OnEndGameEvent()
  55.     {
  56.         m_enemyStates = EnemyStates.WIN;
  57.     }
  58. }

3. 实现被观察者

为了方便我们直接让 GameManager 充当被观察者。如代码清单 3 所示,其中提供了添加观察者函数(AddEndGameObserver)、移除观察者函数(RemoveEndGameObserver)以及通知观察者函数(NotifyEndGame)。在 Update() 函数中,进行角色人物的血量跟踪,如果人物血量为 0,则通知观察者对游戏结束做出响应。

代码清单 3 被观察者
  1. public class GameManager : Singleton<GameManager>
  2. {
  3.     public CharacterData m_playerData;
  4.  
  5.     List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();
  6.  
  7.     public void RegisterPlayerData(CharacterData playerData)
  8.     {
  9.         m_playerData = playerData;
  10.         Debug.Log("RegisterPlayerData Done.");
  11.     }
  12.  
  13.     public void AddEndGameObserver(IEndGameObserver observer)
  14.     {
  15.         endGameObservers.Add(observer);
  16.     }
  17.  
  18.     public void RemoveEndGameObserver(IEndGameObserver observer)
  19.     {
  20.         endGameObservers.Remove(observer);
  21.     }
  22.  
  23.     public void NotifyEndGame()
  24.     {
  25.         foreach (IEndGameObserver observer in endGameObservers)
  26.         {
  27.             observer.OnEndGameEvent();
  28.         }
  29.     }
  30.  
  31.     // Update is called once per frame
  32.     void Update()
  33.     {
  34.         if (m_playerData.CurrentHealth == 0)
  35.             NotifyEndGame();
  36.     }
  37. }

最后要考虑的是添加和移除观察者。如代码清单 4 所示,我们将其放置在 OnEnable() 和 OnDisable() 周期。但是实际运行下来添加观察者放在 OnEnable() 周期中,此时 GameManager 的 Awake() 周期都还没被执行到,实例会为空。因此我们先将其放在 Start() 周期中。

代码清单 4 添加和移除观察者
  1. public class EnemyController : MonoBehaviour, IEndGameObserver
  2. {
  3.     // Start is called before the first frame update
  4.     void Start()
  5.     {
  6.         //FIXME:
  7.         GameManager.GetInstance().AddEndGameObserver(this);
  8.     }
  9.  
  10.     void OnEnable()
  11.     {
  12.         //GameManager.GetInstance().AddEndGameObserver(this);
  13.     }
  14.     void OnDisable()
  15.     {
  16.         GameManager.GetInstance().RemoveEndGameObserver(this);
  17.     }
  18. }

单例基于 Unity 上下的周期,当时看着就觉得有定拿捏不稳(因为我对 Unity 的加载逻辑不了解)。

现在也不明白为什么 GameManager 脚本会比 EnemyController 脚本后加载。网上倒是找到可以指定脚本加载顺序的方法,但是这种方式也感觉怪怪的。

实验下来,Unity 各个脚本的初始化加载是同步的:因为我用 AutoResetEvent 想等 GameManager Awake() 执行完毕再返回实例,直接就卡住了。

这块留作问题。

为了验证效果,我们将角色人物的血量设置低一点。如图 2 所示,当角色人物死亡时,史莱姆播放胜利动画。

图2 最终效果