智能巡逻兵
1. 提交要求
- 游戏设计要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求
- 必须使用订阅与发布模式传消息
subject:OnLostGoal
Publisher: ?
Subscriber: ?- 工厂模式生产巡逻兵
- 友善提示
- 生成 3~5个边的凸多边型
随机生成矩形
在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
5 ?- 参考以前博客,给出自己新玩法
2. 实现
- 玩法介绍
在以上游戏设计要求的基础上,游戏运行时,按WASD或者方向键可以控制player的移动,player手持武器,可在patrol靠近时按鼠标左键、鼠标右键击杀patrol,或者空格键跳跃。player每次甩掉或击杀patrol可计1分。
- 知识点概要
-
状态机控制器
(1)Animator组件用于将动画分配给场景中的游戏对象,使用Animator组件需要引用Animator Controller
(2)Animator Controller
用于管理游戏对象的运动,包括状态、变迁、参数、条件四部分的设计。我理解的Animator Controller是将几个状态用带箭头的线连接在一起,带箭头的线表示变迁,即从一个状态转换为另一个状态。控制变迁的条件是使用转移控制变量(Float, Int, Bool, Trigger),变量就是参数。条件成立,变迁发生。任意时刻有且仅有一个活跃的状态或变迁。
-
发布与订阅模式
(1)行为型模式行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。行为型模式包含11中,观察者(发布与订阅)模式就是其中的一种。
(2)发布与订阅模式
发布者(事件源)/Publisher:事件或消息的拥有者
主题(渠道)/Subject:消息发布媒体
接收器(地址)/Handle:处理消息的方法
订阅者(接收者)/Subscriber:对主题感兴趣的人特点:
- 发布者与订阅者没有直接的耦合
- 发布者与订阅者没有直接关联,可以 多对多 通讯
- 在MVC设计中,是实现模型与视图分离的重要手段
例如:数据DataSource对象,就是Subject。任何使用该数据源的显示控件,如Grid都会及时更新。
-
-
项目结构
AnimatorController
: 动画控制器的文件夹,存放人物的动作。
AxeyWorks
、NecturusArmySkeletons
、SKYBOX_GreenLight
: 从资源商店下载的资源包,分别是制作plane
、patrol(巡逻兵)、player(玩家)
和skybox
的材料。
Resources
: 存放预制。
Scripts
: 脚本文件夹。预制有三个:Plane、Patrol、Player。
脚本:
-
人物动作
首先下载的资源包中,patrol
的原型Skeleton_footman
有以下动作:
patrol
的动作控制器Patrol:
3个参数
run
: Bool类型,当run为true时从Idle状态转为Walk状态,run为false相反。
shoot
: Trigger类型,触发Skeleton_1H_swing_right。
shock
: Trigger类型,触发Skeleton_anger。player
的原型Skeleton_footman
有以下动作:
player
的动作控制器skeleton:
5个参数
run
: bool类型,run 为true变迁为Run状态。
death
: bool类型,death为true时变迁为Death状态。
attack1
: Trigger类型,触发2H_Skeleton_swing_mid_left。
attack2
: Trigger类型,触发2H_Skeleton_special_attack_B。
jump
: Trigger类型,触发Skeleton_Jump_running。 -
碰撞检测
patrol:
player:
-
代码实现
以下列出了本次游戏特有的类,而动作设计中SSDirector.cs
、SSActionManager.cs
、ISSActionInterface.cs
,门面模式的ISenceInterface.cs
、SSDirector.cs
还有工厂模式的Singleton.cs
对比往次作业没有改动,这里不予列出。
CameraFlow.cs
该游戏使用第三人称视角,通过挂载在camera上来实现。using System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraFlow : MonoBehaviour { public GameObject follow; //跟随的物体 void Start() { } void FixedUpdate() { Vector3 nextpos = follow.transform.forward * -1 * 4 + follow.transform.up * 3 + follow.transform.position; this.transform.position = nextpos; this.transform.LookAt(new Vector3(follow.transform.position.x, follow.transform.position.y+2, follow.transform.position.z)); } }
AreaDetection.cs
挂载场景的九个方块中,用于检测player是否进入该方块。using System.Collections; using System.Collections.Generic; using UnityEngine; public class AreaDetection : MonoBehaviour { public int sign = 0; FirstSceneController sceneController; private void Start() { sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController; } void OnTriggerEnter(Collider collider) { if (collider.gameObject.tag == "Player") { sceneController.wall_sign = sign; } } }
PatrolData.cs
patrol的数据。游戏开始时每个方块有一个patrol。using System.Collections; using System.Collections.Generic; using UnityEngine; public class PatrolData : MonoBehaviour { public int sign; //标志巡逻兵在哪一块区域 public bool follow_player = false; //是否跟随玩家 public int wall_sign = -1; //当前玩家所在区域标志 public GameObject player; //玩家游戏对象 public Vector3 start_position; //当前巡逻兵初始位置 }
PatrolFactory.cs
负责生产player和patrolusing System.Collections; using System.Collections.Generic; using UnityEngine; public class PatorlFactory : MonoBehaviour { //工厂模式就负责生产,其他的不负责,所以可以看到这个类只和角色有关系和其他所有代码都没有关系 private GameObject player = null; //玩家 private GameObject patrol = null; //巡逻兵 private List<GameObject> patrolList = new List<GameObject>(); //正在被使用的巡逻兵 private Vector3[] vec = new Vector3[9]; //保存每个巡逻兵的初始位置 public GameObject LoadPlayer() { player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject; return player; } public List<GameObject> LoadPatrol() { 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/Patrol1")); patrol.transform.position = vec[i]; patrol.GetComponent<PatrolData>().sign = i + 1; patrol.GetComponent<PatrolData>().start_position = vec[i]; patrolList.Add(patrol); } return patrolList; } //游戏结束的时候会暂停所有动作 public void StopPatrol() { for (int i = 0; i < patrolList.Count; i++) { patrolList[i].gameObject.GetComponent<Animator>().SetBool("run", false); } } }
PlayerInDetection.cs
检测player是否进入patrol的追踪范围using System.Collections; using System.Collections.Generic; using UnityEngine; //玩家进入巡逻兵的追踪范围 public class PlayerInDetection : MonoBehaviour { //玩家进入巡逻兵的追踪范围 void OnTriggerEnter(Collider collider) { //加上判断Player的原因是,人物会与地板碰撞 if (collider.gameObject.tag == "Player") { this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true; this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject; //触发巡逻兵向前扑的动作 this.gameObject.transform.parent.GetComponent<Animator>().SetTrigger("shock"); } } //玩家离开巡逻兵的追踪范围 void OnTriggerExit(Collider collider) { if (collider.gameObject.tag == "Player") { this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = false; this.gameObject.transform.parent.GetComponent<PatrolData>().player = null; } } }
PlayerCollideDetection.cs
检测player是否与player碰撞using System.Collections; using System.Collections.Generic; using UnityEngine; //玩家与巡逻兵碰撞 public class PlayerCollideDetection : MonoBehaviour { //当玩家与巡逻兵碰撞 void OnCollisionEnter(Collision other) { //在这里皮了一下,因为一开始是想做攻击判定的,但是剑和人的模型是一起的 //没法单独给剑加一个碰撞盒,所以只好放弃 //于是想攻击的时候直接让巡逻兵的碰撞盒半径为0,然后播放死亡动画也可以达到目的 //然后发现会鬼畜,就注释掉了,现在只要在攻击的时候碰到巡逻兵,巡逻兵会直接消失 //一般情况下的,玩家会自己挂掉,并且播放死亡动画,巡逻兵也会砍一刀然后死掉。 if (other.gameObject.tag == "Player" && other.gameObject.GetComponent<Animator>().GetBool("attack1")) { //this.gameObject.GetComponent<CapsuleCollider>().radius = 0; this.gameObject.SetActive(false); return; } else if (other.gameObject.tag == "Player") { other.gameObject.GetComponent<Animator>().SetBool("death",true); this.GetComponent<Animator>().SetTrigger("shoot"); Singleton<GameEventManager>.Instance.PlayerGameover(); } } }
PatrolAction.cs
patrol的动作类,当没有player的时候,patrol在它自己的方块中以固定的速度移动,遇到阻碍物转换方向。using System.Collections; using System.Collections.Generic; using UnityEngine; 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; //移动的距离随机,在4-7之间 action.move_length = Random.Range(4, 7); return action; } public override void Update() { //因为碰撞会产生不可预料的结果,所以还是要保证模型在正确的位置 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(); //如果有跟随的玩家,并且玩家在自己所在的区域,就会调用ISSActionCallback接口中的函数,切换到追踪状态 //所以说动作的管理是靠ISSActionCallback实现的,同样在跟踪的时候的动作也会有切换到巡逻状态的方法。 if (data.follow_player && data.wall_sign == data.sign) { //当前动作摧毁掉 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; } } }
PatrolActionManager.cs
管理patrol的动作using System.Collections; using System.Collections.Generic; using UnityEngine; public class PatrolActionManager : SSActionManager, ISSActionCallback { //这个函数是游戏初始化的时候用到,最重要的实现还是在SSActionEvent函数中实现,也就是ISSActionCallback接口的函数 public void GoPatrol(GameObject patrol) { GoPatrolAction go_patrol = GoPatrolAction.GetSSAction(patrol.transform.position); this.RunAction(patrol, go_patrol, this); } //停止所有动作 public void DestroyAllAction() { DestroyAll(); } //动作管理最终要的部分,实现各个动作的切换,以及条件判断。 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直接生成实例,调用PlayerEscape就相当于发出通知,那么在OnEnable函数就会触发加分的函数 Singleton<GameEventManager>.Instance.PlayerEscape(); } } }
PatrolFollowAction.cs
当patrol检测在player靠近时,将以一定速度朝player移动。using System.Collections; using System.Collections.Generic; using UnityEngine; 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 (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(); //如果没有跟随的玩家,或者玩家不在自己所在的区域,就会调用ISSActionCallback接口中的函数,切换到巡逻状态 //所以说动作的管理是靠ISSActionCallback实现的,同样在巡逻的时候的动作也会有切换到追踪状态的方法。 if (!data.follow_player || data.wall_sign != data.sign) { //当前动作摧毁掉 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); } }
GameEventManager.cs
player躲避patrol追踪的时候触发计分机制,player与patrol碰撞时触发游戏结束机制。using System.Collections; using System.Collections.Generic; using UnityEngine; 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(); } } }
-
游戏运行
游戏效果图:
游戏演示视频:
patrol