unity实现简单巡逻兵
游戏要求
创建一个地图和若干巡逻兵(使用动画);
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
巡逻兵在设定范围内感知到玩家,会自动追击玩家;
失去玩家目标后,继续巡逻;
计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
游戏实现
本次作业代码主要基于该博客,在其基础上,修改了游戏玩法。
主要代码部分师兄的博客已经讲得十分清晰,这里只挑选比较重要和自己修改的部分进行讲解。
巡逻兵
由于游戏要求巡逻兵在设定范围内感知到玩家,会自动追击玩家;并且玩家与巡逻兵碰撞游戏结束。因此要给予巡逻兵两个component,一个是Capsule Collider,用于是否与玩家发生碰撞,另一个是Box Collider,检测一定范围内是否有玩家。通过调整Size的大小来控制检测的范围。
- PatrolData
巡逻兵的基础数据。
public class PatrolData : MonoBehaviour
{
public bool follow_player = false; //是否跟随玩家
public GameObject player; //玩家游戏对象
public Vector3 start_position; //当前巡逻兵初始位置
public bool alive = true; //巡逻兵状态
}
- PropFactory
工厂模式生产巡逻兵,用数组记录每个巡逻兵的位置,当游戏结束时,巡逻兵跑的动作被停止。
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);
}
}
}
- 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);
}
}
- GoPatrolAction
巡逻兵巡逻,巡逻兵的巡逻轨迹为矩形,当发现附近存在玩家时,会调用回调函数,使得巡逻兵改为追踪状态,追踪玩家。关键代码:
if (data.follow_player)
{
this.destroy = true;
this.callback.SSActionEvent(this,0,this.gameobject);
}
- 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);
}
}
玩家的移动、旋转、射击
通过获取输入来移动和旋转玩家,相关代码如下:
- UserGUI
void Update()
{
//获取方向键的偏移量
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
//移动玩家,获取鼠标点击
action.MovePlayer(translationX, translationZ, Input.GetMouseButton(0));
action.RotatePlayer();
}
- FirstSceneController
当获取到鼠标点击,即flag为true时,将会触发玩家的射击动画,并从玩家身体往前发射一条射线,若该射线触碰到巡逻兵,将会将其击倒,播放巡逻兵死亡动画。
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);
}
}
}
由于射击需要依赖鼠标来进行瞄准,如果只是利用键盘来旋转玩家的话,怕是怎么也打不着巡逻兵了,因此获取鼠标的位置,将玩家的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();
}
}
}
场景控制器作为订阅者,订阅了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();
}
当玩家与巡逻兵碰撞时,将会发布游戏结束的消息
public class PlayerCollide : MonoBehaviour
{
void OnCollisionEnter(Collision other)
{
//当玩家与侦察兵相撞
if (other.gameObject.tag == "Player" && this.GetComponent<PatrolData>().alive == true)
{
other.gameObject.GetComponent<Animator>().SetTrigger("death");
this.GetComponent<Animator>().SetTrigger("shoot");
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
}
游戏结束条件的判断
当玩家被抓住或杀掉所有巡逻兵
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;
}
}