游戏要求
游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
游戏操作方法
使用方向键或 WASD 键操控玩家,躲避巡逻兵追捕。玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞则游戏结束,在倒计时结束后仍未死亡则游戏获胜。
实现思路
订阅与发布模式
添加类 GameEventManager,并在其中定义游戏事件的处理逻辑。这是订阅及发布事件的入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class GameEventManager : MonoBehaviour { public delegate void EscapeEvent(GameObject patrol); public static event EscapeEvent OnGoalLost; public delegate void FollowEvent(GameObject patrol); public static event FollowEvent OnFollowing; public delegate void GameOverEvent(); public static event GameOverEvent GameOver; public delegate void WinEvent(); public static event WinEvent Win;
public void PlayerEscape(GameObject patrol) { if (OnGoalLost != null) { OnGoalLost(patrol); } }
public void FollowPlayer(GameObject patrol) { if (OnFollowing != null) { OnFollowing(patrol); } }
public void OnPlayerCatched() { if (GameOver != null) { GameOver(); } }
public void TimeIsUP() { if (Win != null) { Win(); } } }
|
在 FirstSceneController 中订阅事件,将不同的事件交给场记进行响应和处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void OnEnable() { GameEventManager.OnGoalLost += OnGoalLost; GameEventManager.OnFollowing += OnFollowing; GameEventManager.GameOver += GameOver; GameEventManager.Win += Win; }
void OnDisable() { GameEventManager.OnGoalLost -= OnGoalLost; GameEventManager.OnFollowing -= OnFollowing; GameEventManager.GameOver -= GameOver; GameEventManager.Win -= Win; }
|
对应的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public void OnGoalLost(GameObject patrol) { patrolActionManager.Patrol(patrol); scoreRecorder.Record(); }
public void OnFollowing(GameObject patrol) { patrolActionManager.Follow(player, patrol); }
public void GameOver() { gameState = GameState.LOSE; StopAllCoroutines(); patrolFactory.PausePatrol(); player.GetComponent<Animator>().SetTrigger("death"); patrolActionManager.DestroyAllActions(); }
public void Win() { gameState = GameState.WIN; StopAllCoroutines(); patrolFactory.PausePatrol(); player.GetComponent<Animator>().SetBool("pause", true); }
|
游戏地图
利用 Plane, Cube 和下载的栅栏资源,设计一个具有九个正方形区域的地图。如下图:
在地图的每个区域中,放置一个空对象,并在对象上添加一个 Box Collider。通过空对象的 EnterRegion 脚本代码,检测玩家或巡逻兵进出区域的事件,并进行相应的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class EnterRegion : MonoBehaviour { public int region; FirstSceneController sceneController;
void OnTriggerEnter(Collider collider) { sceneController = Director.GetInstance().CurrentSceneController as FirstSceneController; if (collider.gameObject.tag == "Player") { sceneController.playerRegion = region; } }
private void OnTriggerExit(Collider collider) { if (collider.gameObject.tag == "Patrol") { collider.gameObject.GetComponent<PatrolData>().isCollided = true; } } }
|
巡逻兵
在 Asset Store 中搜索 soldier,找到了一个骨骼、动画和贴图都不错的士兵模型包。其中有三种士兵可以选择。
巡逻兵预制的结构如下,由头部和身体两大部分组成:
在 Patrol 上添加一个 Capsule Collider,用于检测巡逻兵与障碍物、玩家的碰撞。在 Bip001 上添加一个 Capsule Collider,用于感知玩家。自定义 Collider 的形状,使巡逻兵具有一定视线范围,只能发现前方区域的玩家。如下图:
巡逻兵数据
1 2 3 4 5 6 7 8 9
| public class PatrolData : MonoBehaviour { public bool isPlayerInRange; public bool isFollowing; public bool isCollided; public int patrolRegion; public int playerRegion; public GameObject player; }
|
巡逻兵动画控制
利用 shoot 和 pause 两个变量来控制巡逻兵的动画播放。
使用工厂模式生产巡逻兵
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class PatrolFactory : MonoBehaviour { public GameObject patrol = null; private List<PatrolData> used = new List<PatrolData>();
public List<GameObject> GetPatrols() { List<GameObject> patrols = new List<GameObject>(); float[] pos_x = { -4.5f, 1.5f, 7.5f }; float[] pos_z = { 7.5f, 1.5f, -4.5f }; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol")); patrol.transform.position = new Vector3(pos_x[j], 0, pos_z[i]); patrol.GetComponent<PatrolData>().patrolRegion = i * 3 + j + 1; patrol.GetComponent<PatrolData>().playerRegion = 4; patrol.GetComponent<PatrolData>().isPlayerInRange = false; patrol.GetComponent<PatrolData>().isFollowing = false; patrol.GetComponent<PatrolData>().isCollided = false; patrol.GetComponent<Animator>().SetBool("pause", true); used.Add(patrol.GetComponent<PatrolData>()); patrols.Add(patrol); } } return patrols; }
public void PausePatrol() { for (int i = 0; i < used.Count; i++) { used[i].gameObject.GetComponent<Animator>().SetBool("pause", true); } }
public void StartPatrol() { for (int i = 0; i < used.Count; i++) { used[i].gameObject.GetComponent<Animator>().SetBool("pause", false); } } }
|
检测巡逻兵的碰撞
在 Patrol 上添加 PatrolCollide,检测巡逻兵的碰撞事件。若巡逻兵碰撞玩家,则游戏结束;若巡逻兵碰撞其他障碍物,则标记碰撞状态,以便在巡逻兵动作中做相应的处理。此处也用到了订阅与发布模式,在游戏结束时发布玩家被捕事件,使订阅了事件的场记能做出相应的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class PatrolCollide : MonoBehaviour { void OnCollisionEnter(Collision collision) { if (collision.gameObject.tag == "Player") { this.GetComponent<Animator>().SetTrigger("shoot"); Singleton<GameEventManager>.Instance.OnPlayerCatched(); } else { this.GetComponent<PatrolData>().isCollided = true; } } }
|
感知玩家
在 Bip001 上添加 PlayerInRange,利用 Bip001 上的 Capsule Collider 感知玩家进入追击范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class PlayerInRange : MonoBehaviour { void OnTriggerEnter(Collider collider) { if (collider.gameObject.tag == "Player") { this.gameObject.transform.parent.GetComponent<PatrolData>().isPlayerInRange = true; this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject; } } void OnTriggerExit(Collider collider) { if (collider.gameObject.tag == "Player") { this.gameObject.transform.parent.GetComponent<PatrolData>().isPlayerInRange = false; this.gameObject.transform.parent.GetComponent<PatrolData>().player = null; } } }
|
巡逻兵动作
巡逻兵巡逻动作如下。巡逻兵每次寻找新位置时,会随机选择其附近的一个位置,然后转向并移动到新的位置。若移动过程中碰到障碍物,则向后转,重新选择新的位置。其中采用了订阅与发布模式。当玩家进入追捕范围,巡逻兵开始追捕时,触发 FollowPlayer 事件,通知订阅者采取相应的操作(这里对应的操作是巡逻兵停止巡逻)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| public class PatrolAction : Action { private float pos_x, pos_z; private bool turn = true; private PatrolData data;
public static PatrolAction GetAction(Vector3 location) { PatrolAction action = CreateInstance<PatrolAction>(); action.pos_x = location.x; action.pos_z = location.z; return action; }
public override void Start() { data = this.gameObject.GetComponent<PatrolData>(); }
public override void Update() { if (Director.GetInstance().CurrentSceneController.getGameState().Equals(GameState.RUNNING)) { Patrol(); if (!data.isFollowing && data.isPlayerInRange && data.patrolRegion == data.playerRegion && !data.isCollided) { this.destroy = true; this.enable = false; this.callback.ActionEvent(this); this.gameObject.GetComponent<PatrolData>().isFollowing = true; Singleton<GameEventManager>.Instance.FollowPlayer(this.gameObject); } } }
void Patrol() { if (turn) { pos_x = this.transform.position.x + Random.Range(-5f, 5f); pos_z = this.transform.position.z + Random.Range(-5f, 5f); this.transform.LookAt(new Vector3(pos_x, 0, pos_z)); this.gameObject.GetComponent<PatrolData>().isCollided = false; turn = false; } float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));
if (this.gameObject.GetComponent<PatrolData>().isCollided) { this.transform.Rotate(Vector3.up, 180); GameObject temp = new GameObject(); temp.transform.position = this.transform.position; temp.transform.rotation = this.transform.rotation; temp.transform.Translate(0, 0, Random.Range(0.5f, 3f)); pos_x = temp.transform.position.x; pos_z = temp.transform.position.z; this.transform.LookAt(new Vector3(pos_x, 0, pos_z)); this.gameObject.GetComponent<PatrolData>().isCollided = false; Destroy(temp); } else if (distance <= 0.1) { turn = true; } else { this.transform.Translate(0, 0, Time.deltaTime); } } }
|
巡逻兵追捕玩家的动作如下。其中采用了订阅与发布模式。当玩家离开追捕范围,巡逻兵放弃追捕时,触发 PlayerEscape 事件,通知订阅者采取相应的操作(这里对应的操作是巡逻兵开始巡逻,游戏增加一分)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class PatrolFollowAction : Action { private float speed = 1.5f; private GameObject player; private PatrolData data;
public static PatrolFollowAction GetAction(GameObject player) { PatrolFollowAction action = CreateInstance<PatrolFollowAction>(); action.player = player; return action; }
public override void Start() { data = this.gameObject.GetComponent<PatrolData>(); }
public override void Update() { if (Director.GetInstance().CurrentSceneController.getGameState().Equals(GameState.RUNNING)) { transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime); this.transform.LookAt(player.transform.position); if (data.isFollowing && (!(data.isPlayerInRange && data.patrolRegion == data.playerRegion) || data.isCollided)) { this.destroy = true; this.enable = false; this.callback.ActionEvent(this); this.gameObject.GetComponent<PatrolData>().isFollowing = false; Singleton<GameEventManager>.Instance.PlayerEscape(this.gameObject); } } } }
|
通过 PatrolActionManager 统一管理巡逻兵的两个动作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class PatrolActionManager : ActionManager, ActionCallback { public PatrolAction patrol; public PatrolFollowAction follow;
public void Patrol(GameObject ptrl) { this.patrol = PatrolAction.GetAction(ptrl.transform.position); this.RunAction(ptrl, patrol, this); }
public void Follow(GameObject player, GameObject patrol) { this.follow = PatrolFollowAction.GetAction(player); this.RunAction(patrol, follow, this); }
public void DestroyAllActions() { DestroyAll(); }
public void ActionEvent(Action source, ActionEventType events = ActionEventType.Completed, int intParam = 0, string strParam = null, object objectParam = null){ } }
|
玩家
玩家预制的结构如下,由头部、身体和背包三大部分组成。在顶层添加 Capsule Collider,检测玩家的碰撞。
玩家动画控制
利用 run、pause 和 death 变量来控制玩家动画播放。
移动玩家
在 UserGUI 类中的 Update 方法获取键盘方向输入,并调用 UserAction 中的 MovePlayer 方法实现玩家的移动。
1 2 3 4 5 6 7 8 9 10 11 12
| private void Update() { action = Director.GetInstance().CurrentSceneController as UserAction; controller = Director.GetInstance().CurrentSceneController as SceneController; if (controller.getGameState().Equals(GameState.RUNNING)) { float translationX = Input.GetAxis("Horizontal"); float translationZ = Input.GetAxis("Vertical"); action.MovePlayer(translationX, translationZ); } }
|
在 FirstSceneController 中实现 MovePlayer 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public void MovePlayer(float translationX, float translationZ) { if (translationX != 0 || translationZ != 0) { player.GetComponent<Animator>().SetBool("run", true); } else { player.GetComponent<Animator>().SetBool("run", false); } translationX *= Time.deltaTime; translationZ *= Time.deltaTime; player.transform.LookAt(new Vector3(player.transform.position.x + translationX, player.transform.position.y, player.transform.position.z + translationZ)); if (translationX == 0) player.transform.Translate(0, 0, Mathf.Abs(translationZ) * 2); else if (translationZ == 0) player.transform.Translate(0, 0, Mathf.Abs(translationX) * 2); else player.transform.Translate(0, 0, Mathf.Abs(translationZ) + Mathf.Abs(translationX)); }
|
游戏倒计时
在导演中控制游戏倒计时,初始时间为 60 秒。当倒计时结束时,发布 TimeIsUp 事件,使订阅了事件的场记能够做出响应(游戏胜利)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Director : System.Object { public int leaveSeconds = 60; public IEnumerator CountDown() { while (leaveSeconds > 0) { yield return new WaitForSeconds(1f); leaveSeconds--; if (leaveSeconds == 0) { Singleton<GameEventManager>.Instance.TimeIsUP(); } } } }
|
镜头跟随
在 CameraFollowAction 中控制镜头跟随玩家移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class CameraFollowAction : MonoBehaviour { public GameObject player; public float smothing = 5f; Vector3 offset;
void Start() { offset = new Vector3(0, 5, -5); }
void FixedUpdate() { Vector3 target = player.transform.position + offset; transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime); } }
|
游戏效果预览
游戏视频
http://v.youku.com/v_show/id_XMzYwNTg1OTA1Mg==.html?spm=a2h3j.8428770.3416059.1
项目地址
GitHub.
参考博客
[1] Unity3d 学习之路 - 简单巡逻兵.
[2] [Unity3D 课堂作业] 巡逻兵 GetAwayFromPatrols.
返回 Unity 3D Learning