3D Computer Game Programming-Note 7

1、智能巡逻兵

  • 游戏设计要求:
    • 创建一个地图和若干巡逻兵(使用动画);
    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
    • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
    • 失去玩家目标后,继续巡逻;
    • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
  • 程序设计要求:
    • 必须使用订阅与发布模式传消息
    • 使用工厂模式生产巡逻兵

【结构设计】

  使用订阅与发布模式传消息,即有一个事件发布者来发布不同类型的事件消息,有多个订阅者可以关注该发布者,当事件发布者触发事件通知时,订阅者会第一时间接收到消息,进而触发其他相关事件。

  1. 设计程序的 UML 图:

Unity-模型与动画 - 图1

【编程实现】

  注:本博客重点讲述动画状态机和订阅/发布模式,代码细节参见🔗github

  • GameEventManager.cs: 使用事件——代理机制,delegate 关键字定义了关于得分和游戏结果事件的代理类型,同时申明两个静态变量 ScoreChangeGameoverChange,当它们分别被调用时,代理事件会被触发,订阅者从代理事件那里得到消息
  1. public class GameEventManager : MonoBehaviour {
  2. public delegate void ScoreEvent();
  3. public static event ScoreEvent ScoreChange;
  4. public delegate void GameoverEvent();
  5. public static event GameoverEvent GameoverChange;
  6. public void PlayerEscape() {
  7. if (ScoreChange != null)
  8. {
  9. ScoreChange();
  10. }
  11. }
  12. public void PlayerGameover() {
  13. if (GameoverChange != null)
  14. {
  15. GameoverChange();
  16. }
  17. }
  18. }
  • FirstController.cs: FirstController 为订阅者,加赋值表示订阅,减赋值表示取消订阅,这样,在订阅后 FirstController 可以随时关注到游戏事件的动态变化,将信息的传递和控制进一步分离出来
  1. public class FirstController : MonoBehaviour, IUserAction, ISceneController {
  2. /* ... */
  3. void OnEnable() {
  4. GameEventManager.ScoreChange += AddScore;
  5. GameEventManager.GameoverChange += Gameover;
  6. }
  7. void OnDisable() {
  8. GameEventManager.ScoreChange -= AddScore;
  9. GameEventManager.GameoverChange -= Gameover;
  10. }
  11. void AddScore() {
  12. recorder.AddScore();
  13. }
  14. public void Gameover() {
  15. game_over = true;
  16. patrol_factory.StopPatrol();
  17. action_manager.DestroyAllAction();
  18. }
  19. }
  • PatrolCollider.cs: 通过实例化事件发布类的一个实例发布消息,所有订阅了该事件的订阅者都会接收到这个消息
  1. public class PatrolCollider : MonoBehaviour {
  2. /* ... */
  3. void OnCollisionEnter(Collision obj) {
  4. if (obj.gameObject.tag == "Player") {
  5. obj.gameObject.GetComponent<Animator>().SetTrigger("death");
  6. this.GetComponent<Animator>().SetTrigger("shoot");
  7. Singleton<GameEventManager>.Instance.PlayerGameover();
  8. }
  9. }
  10. }
  • GoPatrolAction.cs: 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址,即每次确定下一个目标位置,用自己当前位置为原点计算;当巡逻兵碰撞到障碍物(墙壁时),自动选择下一个点为目标
  1. public class GoPatrolAction : SSAction {
  2. private enum Dirction { EAST, NORTH, WEST, SOUTH };
  3. /* ... */
  4. void Gopatrol() {
  5. if (move_sign) {
  6. switch (dirction) {
  7. case Dirction.EAST:
  8. pos_x -= move_length;
  9. break;
  10. case Dirction.NORTH:
  11. pos_z += move_length;
  12. break;
  13. case Dirction.WEST:
  14. pos_x += move_length;
  15. break;
  16. case Dirction.SOUTH:
  17. pos_z -= move_length;
  18. break;
  19. }
  20. move_sign = false;
  21. }
  22. this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
  23. float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));
  24. if (distance > 0.9) {
  25. transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime);
  26. }
  27. else {
  28. dirction = dirction + 1;
  29. if (dirction > Dirction.SOUTH)
  30. {
  31. dirction = Dirction.EAST;
  32. }
  33. move_sign = true;
  34. }
  35. }
  36. }
  • PatrolZoneCollider: 巡逻兵在设定范围内感知到玩家时,将玩家设置为追击目标和碰撞检测对象,自动追击玩家
  1. public class PatrolZoneCollider : MonoBehaviour
  2. {
  3. /* ... */
  4. void OnTriggerEnter(Collider collider)
  5. {
  6. if (collider.gameObject.tag == "Player")
  7. {
  8. this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
  9. this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
  10. }
  11. }
  12. }
  • PatrolFollowAction.cs: 实现巡逻兵追击玩家的动作。当失去玩家目标后,继续巡逻
  1. public class PatrolFollowAction : SSAction {
  2. private float speed = 2f;
  3. private GameObject player;
  4. private PatrolData data;
  5. public override void Update() {
  6. /* ... */
  7. //若不存在玩家目标或玩家目标不在巡逻兵感知范围内
  8. if (!data.follow_player || data.wall_sign != data.sign)
  9. {
  10. this.destroy = true;
  11. this.callback.SSActionEvent(this, 1, this.gameobject);
  12. }
  13. }
  14. /* ... */
  15. }

  使用素材包 Toony Tiny People,内有多种人物预制动画可选,根据游戏需要选用 idle、run 和 shoot。
  为 PatrolPlayer 制作动画状态机:

Unity-模型与动画 - 图2
Unity-模型与动画 - 图3
  对于巡逻兵而言,当感知到玩家时条件 run 为真,巡逻兵从闲散的巡逻状态过渡到奔跑状态,感知消失则回到 Idle 状态;当与玩家碰撞时 shoot 事件被触发,巡逻兵向玩家开枪,游戏结束。

Unity-模型与动画 - 图4
Unity-模型与动画 - 图5
  对于玩家而言,当接收到键盘方向键输入时条件 run 为真,玩家从闲散状态过渡到奔跑运动状态,无输入时则回到 Idle 状态;当与巡逻兵碰撞时 death 事件被触发,玩家倒地不能移动,游戏结束。

【游戏效果】

Unity-模型与动画 - 图6
Unity-模型与动画 - 图7
Unity-模型与动画 - 图8

【动态展示】

  🔗视频链接