本次代码实现的重点是订阅和发布模式和动画的制作
UML图参考之前的博客,运用了订阅和发布模式
发布订阅模式和观察者模式有类似之处,但在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。其余和观察者模式相同,都是通过发布者将消息传递给订阅者
代码实现中运用到的发布和订阅模式需要定义一个GameEventManager类实现对游戏事件的消息传递,这里实现了玩家逃脱分数增加事件和游戏结束事件的订阅和发布,代码实现如下
public class GameEventManager : MonoBehaviour{ //玩家逃脱加分 public delegate void AddScore(); public static event AddScore addScore; // 游戏失败事件 public delegate void GameOverEvent(); public static event GameOverEvent GameOver; //玩家逃脱 public void PlayerEscape(){ if (addScore != null){ addScore(); } } public void gameOver(){ if(GameOver != null){ GameOver(); } } }在场记类中通过OnEnable和OnDisable方法实现两个事件的开始和结束的发布
void OnEnable(){ GameEventManager.addScore += AddScore; GameEventManager.GameOver += Gameover; } void OnDisable(){ GameEventManager.addScore -= AddScore; GameEventManager.GameOver -= Gameover; } void AddScore(){ recorder.AddScore(); } void Gameover(){ game_over = true; action_manager.DestroyAllAction(); }玩家的预设体我下载了一个预设,并用资源中所含的动画制作玩家后续的跑动和死亡动画
玩家需要挂载的组件为一个刚体、一个碰撞器还有一个动画,玩家的tag设置为Player,方便后续碰撞的检测
玩家动画控制器制作如下
玩家的动画控制器包含一个Trigger属性death和一个Bool属性run,run属性检测是否有键盘输入控制玩家移动,如果有则触发跑动动画,death在玩家和巡逻兵碰撞时触发,播放玩家死亡的动画,动画均采用下载资源中包含的动画,动画控制器制作如下
PlayerColiider代码,用于检测玩家是否和巡逻兵碰撞,如果碰撞发生则触发游戏结束的一系列动作
public class PlayerCollide : MonoBehaviour{ void OnCollisionEnter(Collision other){ //玩家和巡逻兵碰撞触发死亡动画和游戏结束订阅 if (other.gameObject.tag == "Player"){ other.gameObject.GetComponent<Animator>().SetTrigger("death"); this.GetComponent<Animator>().SetTrigger("attack");//玩家死亡和巡逻兵攻击动画触发 Singleton<GameEventManager>.Instance.gameOver(); } } }玩家的移动在UserGUI中检测键盘输入,并以此控制玩家的上下左右移动
void Update(){ //获取方向键的偏移量 float translationX = Input.GetAxis("Horizontal"); float translationZ = Input.GetAxis("Vertical"); //移动玩家 action.PlayerMove(translationX, translationZ); }UserAction接口的PlayerMove接口实现玩家的具体位置改变,注意要包含玩家的移动和旋转,旋转用于玩家方向改变时玩家正面的朝向改变,使游戏画面更加真实
public void PlayerMove(float pos_X, float pos_Z){ if(!game_over && player != null){ if (pos_X != 0 || pos_Z != 0){ player.GetComponent<Animator>().SetBool("run", true); } else{ player.GetComponent<Animator>().SetBool("run", false); } //移动和旋转 player.transform.Translate(0, 0, pos_Z * 5f * Time.deltaTime); player.transform.Rotate(0, pos_X*1.5f, 0); player.transform.Translate(pos_X * 5f * Time.deltaTime, 0, 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); } } }巡逻兵的外部需要添加一个碰撞器,用于检测玩家和自身的碰撞,同样需要添加刚体并在外部挂载检测玩家的PropCollider脚本和巡逻兵自身数据的脚本
Prop.cs包含巡逻兵的属性
public class Prop : MonoBehaviour{ public int block;//巡逻兵所在区域 public bool is_follow = false;//是否追踪 public int player_pos = -1;//玩家所在区域 public GameObject player;//追踪玩家 public Vector3 start_position;//初始位置 //防止碰撞后旋转 void Update () { if (this.gameObject.transform.localEulerAngles.x != 0 || gameObject.transform.localEulerAngles.z != 0){ gameObject.transform.localEulerAngles = new Vector3(0, gameObject.transform.localEulerAngles.y, 0); } if (gameObject.transform.position.y != 0){ gameObject.transform.position = new Vector3(gameObject.transform.position.x, 0, gameObject.transform.position.z); } } }巡逻兵的身体上还需要重新添加一个包围一个有一定范围的碰撞器和对应脚本PropCollider用于检测玩家进入了巡逻区域,更改巡逻兵行为从巡逻变为追击,并检测玩家是否逃脱
public class PropCollide : MonoBehaviour { //玩家和巡逻兵装载的BoxCollide碰撞 void OnTriggerEnter(Collider collider){ if (collider.gameObject.tag == "Player"){ this.gameObject.transform.parent.GetComponent<Prop>().is_follow = true; this.gameObject.transform.parent.GetComponent<Prop>().player = collider.gameObject; } } //玩家逃脱后属性改变,切换回巡逻动作 void OnTriggerExit(Collider collider){ if (collider.gameObject.tag == "Player"){ this.gameObject.transform.parent.GetComponent<Prop>().is_follow = false; this.gameObject.transform.parent.GetComponent<Prop>().player = null; //玩家逃脱 Singleton<GameEventManager>.Instance.PlayerEscape(); } } }巡逻兵的动画同样包含一个Trigger属性attack和一个Bool属性run,run属性控制巡逻兵默认的行走动画,attack属性在玩家和巡逻兵碰撞时触发,巡逻兵攻击玩家,动画控制器实现如下
巡逻兵的产生通过工厂模式,工厂模式设定巡逻兵的初始位置,初始状态等属性
public class PropFactory : MonoBehaviour{ private List<GameObject> used = new List<GameObject>();//巡逻兵列表 int[] X_pos = {-6,4,12}; int[] Z_pos = {-4,6,-12}; public List<GameObject> GetProps(){ for(int i = 0;i < 3; i ++){ for(int j = 0;j < 3;j ++){ GameObject prop = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/zombie")); prop.AddComponent<Prop>(); prop.GetComponent<Prop>().block = i*3+j+1; prop.GetComponent<Prop>().is_follow = false; prop.transform.position = new Vector3(X_pos[i],0,Z_pos[j]); prop.SetActive(true); prop.GetComponent<Animator>().SetBool("run", true); used.Add(prop); } } return used; } public void PropInit(List<GameObject> props){ for(int i = 0;i < 3;i ++){ for(int j = 0;j < 3;j ++){ props[i*3+j].transform.position = new Vector3(X_pos[i], 0, Z_pos[j]); } } } }巡逻兵包含跟随动作和巡逻动作
巡逻兵跟随动作
public class PropFollowAction : SSAction{ private float speed = 2f;//巡逻兵速度 private GameObject player;//追踪目标 private PropFollowAction() { } public static PropFollowAction getAction(GameObject player){ PropFollowAction action = CreateInstance<PropFollowAction>(); action.player = player; return action; } public override void Update(){ transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime); this.transform.LookAt(player.transform.position); //玩家逃脱 if (!this.gameObject.GetComponent<Prop>().is_follow || this.gameObject.GetComponent<Prop>().player_pos != this.gameObject.GetComponent<Prop>().block){ this.destroy = true; this.callback.SSActionEvent(this,1,this.gameObject); } } public override void Start(){ } }巡逻兵按矩形移动,采用一个随机数定义巡逻兵的移动边长
public class PropMoveAction : SSAction{ private enum Dirction { EAST, NORTH, WEST, SOUTH }; private float pos_x, pos_z; private float move_length;//移动的距离 private float move_speed = 1.5f;//巡逻速度 private bool move_sign = true;//是否到达目的地 private Dirction dirction = Dirction.EAST; //移动的方向 private PropMoveAction(){} public static PropMoveAction getAction(Vector3 pos){ PropMoveAction action = CreateInstance<PropMoveAction>(); action.pos_x = pos.x; action.pos_z = pos.z; //设定移动矩形的边长 action.move_length = Random.Range(4, 7); return action; } public override void Start(){} public override void Update(){ 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 > 1){ 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; } //触发跟随后侦查动作删除 if (this.gameObject.GetComponent<Prop>().is_follow && this.gameObject.GetComponent<Prop>().player_pos == this.gameObject.GetComponent<Prop>().block){ this.destroy = true; this.callback.SSActionEvent(this,0,this.gameObject); } } }定义相机跟随玩家
public class CameraFollow : MonoBehaviour{ public GameObject player;//相机跟随玩家 public float speed = 5f;//相机跟随的速度 Vector3 offset;//相机和玩家的相对位置 void Start(){ offset = transform.position - player.transform.position; } //相机位置刷新 void FixedUpdate(){ Vector3 target = player.transform.position + offset; transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); } }搭建的场景块内挂载碰撞检测代码,检测玩家进入当前块
public class BlockCollide : MonoBehaviour{ public int block = 0; FirstSceneController controller; private void Start(){ controller = SSDirector.getInstance().currentScenceController as FirstSceneController; } void OnTriggerEnter(Collider collider){ //玩家进入该区域标记玩家当前区域 if (collider.gameObject.tag == "Player"){ controller.player_pos = block; } } }场景搭建如下
游戏运行效果如下
详细代码见github