作业要求
创建一个地图和若干巡逻兵(使用动画);
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
巡逻兵在设定范围内感知到玩家,会自动追击玩家;
失去玩家目标后,继续巡逻;
计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
游戏设计要求
必须使用订阅与发布模式传消息
工厂模式生产巡逻兵
部署工作
-
建立预制体
建立游戏地图预制,玩家预制和巡逻兵预制。其中,巡逻兵需要添加两个Collider,一个是Capsule Collider,添加在预制体父节点上,用于检测巡逻兵与玩家的碰撞。另一个是Box Collider,添加在预制体的子节点上,用于检测玩家进入巡逻兵巡逻的范围,可以通过调整Size的大小来控制检测的范围
重要代码分析
-
巡逻兵创建-PatrolData
public class PatrolData : MonoBehaviour { public int sign; //标志巡逻兵在哪一块区域 public bool follow_player = false; //是否跟随玩家 public int wall_sign = -1; //当前玩家所在区域标志 public GameObject player; //玩家游戏对象 public Vector3 start_position; //当前巡逻兵初始位置 }
-
巡逻兵创建-PropFactory
利用工厂模式生产巡逻兵,用数组记录每个巡逻兵的位置,创建的9个巡逻兵的位置有规律,所以可以用循环初始化位置坐标。当游戏结束时候,巡逻兵跑的动作被停止,将巡逻兵的动画设置为初始状态。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PropFactory : MonoBehaviour { private GameObject patrol = null; //巡逻兵 private List<GameObject> used = new List<GameObject>(); //正在被使用的巡逻兵 private Vector3[] vec = new Vector3[9]; //保存每个巡逻兵的初始位置 public FirstSceneController sceneControler; //场景控制器 public List<GameObject> GetPatrols() { int[] pos_x = { -6, 4, 13 }; int[] pos_z = { -4, 6, -13 }; int index = 0; for(int i=0;i < 3;i++) { for(int j=0;j < 3;j++) { vec[index] = new Vector3(pos_x[i], 0, pos_z[j]); index++; } } for(int i=0; i < 9; i++) { patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol")); patrol.transform.position = vec[i]; patrol.GetComponent<PatrolData>().start_position = vec[i]; used.Add(patrol); } return used; } public void StopPatrol() { for (int i = 0; i < used.Count; i++) { used[i].gameObject.GetComponent<Animator>().SetBool("run", false); } } }
-
巡逻兵巡逻-GoPatrolAction
巡逻兵巡逻的动作,逻兵的巡逻轨迹为矩形,根据四个方向来选择要去到的目的地,当当前位置与目的地相差0.9f的时候,换一个方向继续巡逻;当发现附近存在玩家时,会调用回调函数,使得巡逻兵改为追踪状态,追踪玩家。
public class GoPatrolAction : SSAction { private enum Dirction { EAST, NORTH, WEST, SOUTH }; private float pos_x, pos_z; //移动前的初始x和z方向坐标 private float move_length; //移动的长度 private float move_speed = 1.2f; //移动速度 private bool move_sign = true; //是否到达目的地 private Dirction dirction = Dirction.EAST; //移动的方向 private PatrolData data; //侦察兵的数据 private GoPatrolAction() {} public static GoPatrolAction GetSSAction(Vector3 location) { GoPatrolAction action = CreateInstance<GoPatrolAction>(); action.pos_x = location.x; action.pos_z = location.z; //设定移动矩形的边长 action.move_length = Random.Range(4, 7); return action; } public override void Update() { if (data.alive == false){ this.destroy = true; } //防止碰撞发生后的旋转 if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0) { transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0); } if (transform.position.y != 0) { transform.position = new Vector3(transform.position.x, 0, transform.position.z); } Gopatrol(); if (data.follow_player) { this.destroy = true; this.callback.SSActionEvent(this,0,this.gameobject); } } public override void Start() { this.gameobject.GetComponent<Animator>().SetBool("run", true); data = this.gameobject.GetComponent<PatrolData>(); } void Gopatrol() { if (move_sign) { switch (dirction) { case Dirction.EAST: pos_x -= move_length; break; case Dirction.NORTH: pos_z += move_length; break; case Dirction.WEST: pos_x += move_length; break; case Dirction.SOUTH: pos_z -= move_length; break; } move_sign = false; } this.transform.LookAt(new Vector3(pos_x, 0, pos_z)); float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z)); if (distance > 0.9) { transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime); } else { dirction = dirction + 1; if(dirction > Dirction.SOUTH) { dirction = Dirction.EAST; } move_sign = true; } } }
-
巡逻兵追踪-PatrolFollowAction
巡逻兵将目标位置定位为玩家位置,将会向着玩家位置移动,直到玩家逃离追踪范围。
public class PatrolFollowAction : SSAction { private float speed = 2f; //跟随玩家的速度 private GameObject player; //玩家 private PatrolData data; //侦查兵数据 private PatrolFollowAction() {} public static PatrolFollowAction GetSSAction(GameObject player) { PatrolFollowAction action = CreateInstance<PatrolFollowAction>(); action.player = player; return action; } public override void Update() { if (data.alive == false){ this.destroy = true; } if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0) { transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0); } if (transform.position.y != 0) { transform.position = new Vector3(transform.position.x, 0, transform.position.z); } Follow(); if (!data.follow_player) { this.destroy = true; this.callback.SSActionEvent(this,1,this.gameobject); } } public override void Start() { data = this.gameobject.GetComponent<PatrolData>(); } void Follow() { transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime); this.transform.LookAt(player.transform.position); } }
-
动作管理类-SSActionManager
此类管理巡逻兵是巡逻状态还是追踪状态,巡逻动作结束条件是需要追捕玩家,使用此类来实现追捕动作。而当玩家离开追捕范围后,需要重新巡逻,也需要使用此类实现从初始的位置和方向继续巡逻。在游戏结束后,此类也负责摧毁所有动作,停止巡逻兵的移动。
public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null) { if(intParam == 0) { //侦查兵跟随玩家 PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player); this.RunAction(objectParam, follow, this); } else { //侦察兵按照初始位置开始继续巡逻 GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position); this.RunAction(objectParam, move, this); //玩家逃脱 //Singleton<GameEventManager>.Instance.PlayerEscape(); } } public void DestroyAll() { foreach (KeyValuePair<int, SSAction> kv in actions) { SSAction ac = kv.Value; ac.destroy = true; } }
-
GUI部分-UserGUI
UserGUI绘制游戏提示语以及可以得到用户的输入,并调用场景控制器移动玩家的函数
public class UserGUI : MonoBehaviour { private IUserAction action; private GUIStyle score_style = new GUIStyle(); private GUIStyle text_style = new GUIStyle(); private GUIStyle over_style = new GUIStyle(); public int show_time = 8; //展示提示的时间长度 void Start () { action = SSDirector.GetInstance().CurrentScenceController as IUserAction; text_style.normal.textColor = new Color(0, 0, 0, 1); text_style.fontSize = 16; score_style.normal.textColor = new Color(1,0.92f,0.016f,1); score_style.fontSize = 16; over_style.fontSize = 25; //展示提示 StartCoroutine(ShowTip()); } void Update() { //获取方向键的偏移量 float translationX = Input.GetAxis("Horizontal"); float translationZ = Input.GetAxis("Vertical"); //移动玩家 action.MovePlayer(translationX, translationZ, Input.GetMouseButton(0)); action.RotatePlayer(); } private void OnGUI() { GUI.Label(new Rect(10, 5, 200, 50), "分数:", text_style); GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), score_style); if(action.GetGameover() /*&& action.GetCrystalNumber() != 0*/) { GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "游戏结束", over_style); if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始")) { action.Restart(); return; } } if(action.GetPatrols()==0){ GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "恭喜获胜", over_style); if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始")) { action.Restart(); return; } } if(GUI.Button (new Rect (Screen.width-100, Screen.height-100, 65, 65), "Exit")){ Application.Quit(); } if(show_time > 0) { GUI.Label(new Rect(Screen.width / 2-80 ,10, 100, 100), "按WSAD或方向键移动", text_style); GUI.Label(new Rect(Screen.width / 2 - 87, 30, 100, 100), "成功击杀巡逻兵加1分", text_style); GUI.Label(new Rect(Screen.width / 2 - 90, 50, 100, 100), "击杀所有巡逻兵则获胜", text_style); } } public IEnumerator ShowTip() { while (show_time >= 0) { yield return new WaitForSeconds(1); show_time--; } } }
-
玩家移动-FirstSceneController
获得偏移量进行上下的移动,左右的旋转,播放对应的动画。当获取到鼠标点击,即flag为true时,将会触发玩家的射击动画,并从玩家身体往前发射一条射线,若该射线触碰到巡逻兵,将会将其击倒,播放巡逻兵死亡动画。出键盘控制移动外,还需要使用鼠标进行瞄准对象,获取鼠标的位置,将玩家的z轴方向指向鼠标,即可实现鼠标转动玩家了。
//玩家移动 public void MovePlayer(float translationX, float translationZ, bool flag) { if(!game_over) { if (flag){ player.GetComponent<Animator>().SetTrigger("shoot"); Ray ray = new Ray(player.transform.position, player.transform.forward); RaycastHit hit; if (Physics.Raycast (ray, out hit)) { //print("hit:"+hit.collider.gameObject.name); if (hit.collider.gameObject.tag == "Patrol"){ hit.collider.gameObject.GetComponent<Animator>().SetTrigger("death"); if(hit.collider.gameObject.GetComponent<PatrolData>().alive == true){ AddScore(); patrol--; } hit.collider.gameObject.GetComponent<PatrolData>().alive = false; } } } if (translationX != 0 || translationZ != 0) { player.GetComponent<Animator>().SetBool("run", true); } else { player.GetComponent<Animator>().SetBool("run", false); } //移动和旋转 player.transform.Translate(0, 0, translationZ * player_speed * Time.deltaTime); player.transform.Rotate(0, translationX * rotate_speed * Time.deltaTime, 0); //防止碰撞带来的移动 if (player.transform.localEulerAngles.x != 0 || player.transform.localEulerAngles.z != 0) { player.transform.localEulerAngles = new Vector3(0, player.transform.localEulerAngles.y, 0); } if (player.transform.position.y != 0) { player.transform.position = new Vector3(player.transform.position.x, 0, player.transform.position.z); } } } //鼠标控制玩家移动 public void RotatePlayer(){ if(!game_over) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; if(Physics.Raycast(ray, out hitInfo)){ Vector3 target = hitInfo.point; target.y = player.transform.position.y; player.transform.LookAt(target); } } }
-
订阅与发布-GameEventManager
此类负责发布事件,当订阅者订阅该类的事件并且某个事件发生调用了该类来发布此事件,则订阅者将会收到事件并执行操作。
public class GameEventManager : MonoBehaviour { //分数变化 public delegate void ScoreEvent(); public static event ScoreEvent ScoreChange; //游戏结束变化 public delegate void GameoverEvent(); public static event GameoverEvent GameoverChange; //玩家逃脱 public void PlayerEscape() { if (ScoreChange != null) { ScoreChange(); } } //玩家被捕 public void PlayerGameover() { if (GameoverChange != null) { GameoverChange(); } } }
-
订阅与发布-FirstSceneController
此类也作为订阅者,订阅了GameEventManager中的事件,只要相应事件发生,就会导致场景控制器调用注册的方法。
void OnEnable() { GameEventManager.ScoreChange += AddScore; GameEventManager.GameoverChange += Gameover; } void OnDisable() { GameEventManager.ScoreChange -= AddScore; GameEventManager.GameoverChange -= Gameover; } void AddScore() { recorder.AddScore(); } void Gameover() { game_over = true; patrol_factory.StopPatrol(); action_manager.DestroyAllAction(); }
视频演示及项目链接
bilibili视频:https://www.bilibili.com/video/BV1Wb4y1B7jD/
项目链接:https://gitee.com/zipzhou/hw6-3d