智能巡逻兵
作业要求
- 提交要求:
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 必须使用订阅与发布模式传消息
游戏设计
操作方法
玩家控制的角色为一只僵尸,通过按键W、S、A、D控制角色的移动并,躲避巡逻兵的巡逻并夺取金币!
游戏机制
玩家在90秒内收集到5个金币则完成任务,若期间被巡逻兵触碰或超时,则任务失败。地图上一共有六个巡逻兵,每个巡逻兵在一个特点房间内巡逻,若玩家进入房间,巡逻兵则会跟踪玩家,直到玩家离开特定范围。
游戏实现
预设
游戏中的游戏对象都必须通过预设生成。
player
玩家控制的角色预设为playr,其拥有的组件有:Animator、Rigidbody和BoxCollider。
patrol
巡逻兵的预设有三种,只有模型上的区别,都拥有组件Animator、Rigidbody和BoxCollider。
maze
maze是游戏的场景,其包含若干个Floor和Wall预制,其拥有的组件有BoxCollider和Rigidbody。
Coin
金币是一个扁平圆柱体加上特制的Material组成的预设,其包含的组件有:Animator和BoxCollider。
动画
玩家角色动画
玩家控制的角色一开始就保持奔跑的动画,如果碰到巡逻兵则转为死亡动画,如果游戏重新开始则回到奔跑动画,对应的Animator Controller设计如下:
巡逻兵动画
巡逻兵和玩家角色类似,对应的Animator Controller设计如下:
代码
巡逻兵
PatrolData.cs
该脚本声明了每个巡逻兵的初始位置、管理的房间,同时实时记录玩家所在的房间以判断是否追踪玩家。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class PatrolData : MonoBehaviour
{
public int manageFloor; // 管理的房间
public int plyerFloor; // 玩家当前所在房间
public Vector3 initPosition; // 初始位置
}
PatrolFactory.cs
利用工厂模式生产巡逻兵:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class PatrolFactory : MonoBehaviour
{
private GameObject patrolPrefab = null;
private Vector3[] position = new Vector3[6]; //记录巡逻兵的位置
private List<GameObject> patrols = new List<GameObject>();
public List<GameObject> GetPatrols()
{
int[] pos_x = { -10, 3, 18, -11, 3, 15 };
int[] pos_z = { -18, -15, -15, 12, 10, 13 };
for(int i = 0; i < 6; i++)
{
position[i] = new Vector3(pos_x[i], 0, pos_z[i]);
if(i <2){
patrolPrefab = Object.Instantiate(Resources.Load<GameObject>("Prefabs/patrol1"), position[i], Quaternion.identity);
}
else if(i < 4){
patrolPrefab = Object.Instantiate(Resources.Load<GameObject>("Prefabs/patrol2"), position[i], Quaternion.identity);
}
else{
patrolPrefab = Object.Instantiate(Resources.Load<GameObject>("Prefabs/patrol3"), position[i], Quaternion.identity);
}
patrolPrefab.name = "patrol" + i;
patrolPrefab.AddComponent<PatrolData>();
patrolPrefab.GetComponent<PatrolData>().manageFloor = i + 1;
patrolPrefab.GetComponent<PatrolData>().initPosition = position[i];
patrols.Add(patrolPrefab);
}
return patrols;
}
public void Reset()
{
for(int i = 0; i < patrols.Count; i++)
{
patrols[i].transform.position = position[i];
patrols[i].GetComponent<Animator>().SetBool("shoot", false);
}
}
}
PatrolMoveAction.cs
每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算。该脚本则是实现了巡逻兵在不追踪玩家时的移动动作,通过生成一个5到8的随机数确定往某一方向移动的距离:
public void Move()
{
if (reach)
{
// 如果已到达,就换方向
switch (dirction)
{
case Dirction.EAST:
posX -= rectLength;
break;
case Dirction.NORTH:
posZ += rectLength;
break;
case Dirction.WEST:
posX += rectLength;
break;
case Dirction.SOUTH:
posZ -= rectLength;
break;
}
reach = false;
}
//面朝目的地
this.transform.LookAt(new Vector3(posX, 0.5f, posZ));
//计算距离
float distance = Vector3.Distance(transform.position, new Vector3(posX, 0.5f, posZ));
if (distance > 1)
{
transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(posX, 0.5f, posZ), speed * Time.deltaTime);
}
else
{
dirction = dirction + 1;
if (dirction > Dirction.SOUTH)
{
dirction = Dirction.EAST;
}
reach = true;
}
}
同时巡逻兵碰撞到障碍物,则会自动选下一个点为目标:
//防止碰撞发生后的旋转
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
{
transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
}
if (transform.position.y != 0.5f)
{
transform.position = new Vector3(transform.position.x, 0.5f, transform.position.z);
}
// 移动
Move();
巡逻兵在设定范围内感知到玩家,会自动追击玩家
// 如果所在房间相同,摧毁当前动作并回调
if (data.manageFloor == data.plyerFloor)
{
this.destory = true;
this.callback.SSActionEvent(this, SSActionEventType.Compeleted, 0 ,"follow player", this.gameObject);
}
PatrolFollowAction.cs
首先要实现巡逻兵追踪玩家角色的动作。
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
{
transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
}
if (transform.position.y != 0.5f)
{
transform.position = new Vector3(transform.position.x, 0.5f, transform.position.z);
}
transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
//追踪时面朝玩家
this.transform.LookAt(player.transform.position);
失去玩家目标后,继续巡逻:
//丢失目标,停止追踪
//如果侦察兵没有跟随对象,或者需要跟随的玩家不在侦查兵的区域内
if (data.manageFloor != data.plyerFloor)
{
this.destory = true;
this.callback.SSActionEvent(this, SSActionEventType.Compeleted, 1, "stop follow", this.gameObject);
}
PatrolActionManager.cs
巡逻动作结束条件是需要追捕玩家,所以调用了回调函数,用回调函数来进行追捕动作。而当玩家离开追捕范围后,需要重新巡逻,也需要调用回调函数,从初始的位置和方向继续巡逻。
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Compeleted,
int intParam = 0,
string strParam = null,
GameObject objectParam = null)
{
//回调函数,动作执行完后调用
if (intParam == 0)
{
//开始跟随玩家
PatrolFollowAction follow = PatrolFollowAction.GetSSAction(sceneController.player);
this.RunAction(objectParam, follow, this);
}
else
{
//丢失目标,继续巡逻
PatrolMoveAction move = PatrolMoveAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().initPosition);
this.RunAction(objectParam, move, this);
//玩家逃脱
Singleton<GameEventManager>.Instance.PlayerEscape();
}
}
PatrolCollide.cs
该脚本用于判断巡逻兵是否与玩家角色发生了碰撞,若是则游戏结束且任务失败,通知GameEventManager发送玩家被捕的事件信息。
using UnityEngine;
using System.Collections;
public class PatrolCollide : MonoBehaviour
{
void OnCollisionStay(Collision other)
{
//当侦察兵与玩家相撞
if (other.gameObject.name == "player")
{
other.gameObject.GetComponent<Animator>().SetBool("death", true);
this.GetComponent<Animator>().SetBool("shoot", true);
Singleton<GameEventManager>.Instance.PlayerArrested();
}
}
}
玩家角色
角色移动
在UserGUI.cs脚本中读取玩家输入的移动指令,再调用IUserAction.cs中的MovePlayer()函数移动角色。
float transitionX = Input.GetAxis("Horizontal");
float transitionZ = Input.GetAxis("Vertical");
//移动玩家
action.MovePlayer(transitionX, transitionZ);
public void MovePlayer(float x, float z)
{
if (!gameOver)
{
//移动
player.transform.Translate(new Vector3(x*13.5f , 0, z*13.5f)*Time.deltaTime,Space.World);
//转向
player.transform.LookAt(new Vector3(x*180,0,z*180));
//防止碰撞带来的移动
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.5f)
{
player.transform.position = new Vector3(player.transform.position.x, 0.5f, player.transform.position.z);
}
}
}
地图
AreaCollide.cs
因为游戏需要实时记录玩家所在的区域,所以当玩家进入该区域的时候,会设置场景控制器的区域标识为自己的标识,这样其他的巡逻兵就知道玩家现在在哪个区域了。
using UnityEngine;
using System.Collections;
public class AreaCollide : MonoBehaviour
{
public int sign = 0;
SceneController sceneController;
private void Start()
{
sceneController = SSDirector.GetInstance().CurrentSceneController as SceneController;
}
void OnTriggerEnter(Collider collider)
{
//标记玩家进入自己的区域
if (collider.gameObject.name == "player")
{
Debug.Log("player enter floor " + sign);
sceneController.floorNumber = sign;
}
}
}
CoinCollider.cs
因为玩家角色需要收集金币,所以需要判断角色和金币的碰撞:
using UnityEngine;
using System.Collections;
public class CoinCollide : MonoBehaviour
{
void OnTriggerEnter(Collider collider)
{
//玩家吃到金币事件触发
if (collider.gameObject.name == "player")
{
this.gameObject.SetActive(false);
Singleton<GameEventManager>.Instance.RecudeCoinNum();
}
}
}
订阅与发布模式部分
GameEventManager.cs(发布者)
设计一个类,用于发布事件如:玩家获得金币、玩家成功逃脱、玩家任务失败等。
using UnityEngine;
using System.Collections;
public class GameEventManager : MonoBehaviour
{
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
public delegate void GameOverEvent();
public static event GameOverEvent GameOver;
public delegate void CoinEvent();
public static event CoinEvent CoinNumberChange;
//玩家逃脱
public void PlayerEscape()
{
if (ScoreChange != null)
{
ScoreChange();
}
}
//玩家被捕
public void PlayerArrested()
{
if (GameOver != null)
{
GameOver();
}
}
public void RecudeCoinNum()
{
if(CoinNumberChange != null)
{
CoinNumberChange();
}
}
public void TimeOut()
{
if (GameOver != null)
{
GameOver();
}
}
}
订阅者
在场景控制器中,一旦接收到特定的事件信息,则调用对应的函数对场景进行改变如分数增加、游戏结束:
void OnEnable()
{
//注册事件
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameOver += GameOver;
GameEventManager.CoinNumberChange += ReduceCoinNumber;
}
void OnDisable()
{
//取消注册事件
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameOver -= GameOver;
GameEventManager.CoinNumberChange -= ReduceCoinNumber;
}
void AddScore()
{
scoreRecorder.AddScore();
}
void GameOver()
{
gameOver = true;
actionManager.DestroyAll();
}
游戏效果
项目代码在我的仓库