一、智能巡逻兵的基本要求
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
- 友善提示1:生成 3~5个边的凸多边型
- 随机生成矩形
- 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
二、简单巡逻兵的实现
1.首先根据要求,本次实验要求使用订阅与发布模式传递消息,同时,我们还需要用到之前的动作管理模式和工厂模式,以下是本次实验的UML图
2.游戏实现部分
根据要求本次游戏需要大致有三个要素:巡逻兵,玩家,以及地图。
- 巡逻兵的预制:采用了在Asset Store中下载的人物模型,首先为他添加物理学属性:,和一个碰撞检测器:(由于人物模型是胶囊状的,此处采用了Capsule Collider)。因为巡逻兵在玩家接近的时候能够检测到玩家并且自动开始追捕行动,所以我们还需要为他再添加一个碰撞检测器,我将它放到了patrol预制的子对象上,并为他挂载了追捕脚本(即当玩家进入碰撞范围,Patrol开始追捕)。同时,在Patrol的父对象上挂载控制巡逻兵自动随机行动的脚本和记录当前巡逻兵相关状态的脚本:。不仅如此,我们还需要为预制的Animation进行修改。此外,将 预制的标签更改为Patrol。
- 玩家的预制:,同样采用了在Asset Store中下载的人物模型,同样为他添加碰撞检测器和物理学属性:,将标签更改为Player,对Animation进行更改。
- 地图的预制:事先布局好一个地图,该地图可以随意生成,看自己喜好即可。按照九宫格模式,我生成了如下地图:。需要注意的是,我在每个格子中 都设置了一个trigger,用于判断玩家处于哪个格子中(防止巡逻兵 跨越格子去追杀玩家)。
在完成了预制的制作后,就可以开始着手于类的设计。
- 完成巡逻兵有关类的设计:
- 首先实一个记录巡逻兵相关状态的类:PatrolData
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PatrolData : MonoBehaviour {
public int sign; //标志巡逻兵在哪一块区域
public bool follow_player = false; //是否跟随玩家
public int wallSign = -1; //当前玩家所在区域标志
public GameObject player; //玩家游戏对象
public Vector3 start_position; //当前巡逻兵初始位置
}
该类记录了有关巡逻兵的相关信息,例如巡逻兵的初始位置,玩家是否在巡逻兵的监测范围内,以及巡逻兵当前的状态(随机行动还是追捕玩家)等等。
2.实现当玩家进入巡逻兵检测范围后巡逻兵相关行动的类:PatrolCollide
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PatrolCollide : MonoBehaviour {
void OnTriggerEnter(Collider collider) {
if (collider.gameObject.tag == "Player")
{
//玩家进入侦察兵追捕范围
this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
}
}
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;
}
}
}
3.实现巡逻兵追捕玩家的类:PatrolFollowAction
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();
//如果侦察兵没有跟随对象,或者需要跟随的玩家不在侦查兵的区域内
if (!data.follow_player || data.wallSign != 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);
}
}
4.完成当玩家被巡逻兵抓到时的类:PlayerCollide
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCollide : MonoBehaviour {
void OnCollisionEnter(Collision other) {
//当巡逻兵捉到玩家
if (other.gameObject.tag == "Player") {
other.gameObject.GetComponent<Animator>().SetTrigger("death");
this.GetComponent<Animator>().SetTrigger("shoot");
Singleton<GameEventManager>.Instance.PlayerGameover();
}
}
}
5.实现接口类:IUserInterFace:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources();
}
public interface IUserAction {
//根据输入移动玩家
void MovePlayer(float translationX, float translationZ);
//加分
int GetScore();
//游戏结束
bool GetGameover();
//重新开始
void Restart();
}
public interface ISSActionCallback {
void SSActionEvent(SSAction source,int intParam = 0,GameObject objectParam = null);
}
public interface IGameStatusOp {
void PlayerWin();
void PlayerGameover();
}
6.根据接口类完成ScoreRecord类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
public FirstSceneController sceneController;
public int score = 0; //分数
void Start() {
sceneController = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
sceneController.recorder = this;
}
public int GetScore() {
return score;
}
public void AddScore() {
score++;
}
}
7.实现动作管理器类:SSActionManage
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSActionManager : MonoBehaviour, ISSActionCallback {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
protected void Update() {
foreach (SSAction ac in waitingAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable) {
ac.Update();
}
}
foreach (int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
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.PlayerWin();
}
}
public void DestroyAll() {
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
ac.destroy = true;
}
}
}
8.实现导演类:SSDirector:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
private static SSDirector _instance; //导演类的实例
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance() {
if (_instance == null) {
_instance = new SSDirector();
}
return _instance;
}
}
9.实现用户界面UserGUI,在其中完成交互界面的实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
private GUIStyle score = new GUIStyle();
private GUIStyle text = new GUIStyle();
private GUIStyle over = new GUIStyle();
void Start () {
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
text.normal.textColor = new Color(0, 0, 0, 5);
text.fontSize = 16;
score.normal.textColor = new Color(1,0.92f,0.016f,1);
score.fontSize = 16;
over.fontSize = 25;
}
void Update() {
//获取按键信息
float translationX = Input.GetAxis("Horizontal");
float translationZ = Input.GetAxis("Vertical");
//移动玩家
action.MovePlayer(translationX, translationZ);
}
private void OnGUI() {
Debug.Log (action.GetScore ());
GUI.Label(new Rect(10, 5, 200, 50), "分数:", text);
GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), score);
if(action.GetGameover() && action.GetScore()!=20) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "游戏结束", over);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始")) {
action.Restart();
return;
}
}
else if(action.GetGameover() && action.GetScore() == 20) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "恭喜胜利!", over);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "重新开始")) {
action.Restart();
return;
}
}
if(GUI.RepeatButton (new Rect (Screen.width / 2-80 ,10, 100, 20), "Help")) {
GUI.TextArea(new Rect(350 ,30, 100, 100),"按WSAD或方向键移动", text);
GUI.TextArea(new Rect(250 , 50, 80, 80), "巡逻兵只会在格子所在的单元格移动,成功躲避巡逻兵追捕加1分", text);
GUI.TextArea(new Rect(350, 70, 100, 100), "分数达到20分即为胜利", text);
}
}
}
10.实现单例模式的类:Singleton
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour {
protected static T instance;
public static T Instance {
get {
if (instance == null) {
instance = (T)FindObjectOfType(typeof(T));
if (instance == null) {
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
11.实现控制巡逻兵的生成及其位置的类:PropFactory
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>().sign = i + 1;
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);
}
}
}
12.实现场记FirstSceneController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
public PropFactory patrolFactory; //巡逻者工厂
public ScoreRecorder recorder; //记录员
public PatrolActionManager actionManager; //动作管理器
public int wallSign = -1; //记录玩家当前所在的位置
public GameObject player; //玩家
public Camera mainCamera; //主相机
public float playerSpeed = 5; //玩家移动速度
public float rotateSpeed = 135f; //玩家旋转速度
private List<GameObject> patrols; //巡逻者列表
private bool gameOver = false; //游戏结束
void Update() {
for (int i = 0; i < patrols.Count; i++) {
patrols[i].gameObject.GetComponent<PatrolData>().wallSign = wallSign;
}
//分数达到20分
if(recorder.GetScore() == 20) {
Gameover();
}
}
void Start() {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
patrolFactory = Singleton<PropFactory>.Instance;
actionManager = gameObject.AddComponent<PatrolActionManager>() as PatrolActionManager;
LoadResources();
mainCamera.GetComponent<CameraFlow>().follow = player;
recorder = Singleton<ScoreRecorder>.Instance;
}
public void LoadResources() {
Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;
patrols = patrolFactory.GetPatrols();
//所有侦察兵移动
for (int i = 0; i < patrols.Count; i++) {
actionManager.GoPatrol(patrols[i]);
}
}
//玩家移动
public void MovePlayer(float translationX, float translationZ) {
if(!gameOver) {
if (translationX != 0 || translationZ != 0) {
player.GetComponent<Animator>().SetBool("run", true);
}
else {
player.GetComponent<Animator>().SetBool("run", false);
}
//移动和旋转
player.transform.Translate(0, 0, translationZ * playerSpeed * Time.deltaTime);
player.transform.Rotate(0, translationX * rotateSpeed * 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 int GetScore() {
return recorder.GetScore();
}
public bool GetGameover() {
return gameOver;
}
public void Restart() {
SceneManager.LoadScene("a");
}
void OnEnable() {
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable() {
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
}
void AddScore() {
recorder.AddScore();
}
void Gameover() {
gameOver = true;
patrolFactory.StopPatrol();
actionManager.DestroyAllAction();
}
}
13.因为人物是以第三人称在迷宫中运动的,所以我们有两个选择,一是直接展示整个迷宫,让玩家能够完全看到所有的场景;二是只展示一部分迷宫,让镜头随着人物的移动而移动,这样一来,玩家并不能看到迷宫的全貌,而且对人物的控制能够更加直观,所以我们选择了第二种,为此需要实现一个类:CameraFollow
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraFlow : MonoBehaviour
{
public GameObject follow; //跟随的物体
public float smothing = 5f; //相机跟随的速度
Vector3 offset; //相机与物体相对偏移位置
void Start() {
offset = transform.position - follow.transform.position;
}
void FixedUpdate() {
Vector3 target = follow.transform.position + offset;
transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
}
}
14.在完成了上述工作后,我们只需要最后实现一个判断玩家位置以确定哪一个巡逻兵执行追捕 玩家的动作的类:AreaCollide即可
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AreaCollide : 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.wallSign = sign;
}
}
}
15.最后,我们 来实现订阅预发布模式:GameEventManager
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 PlayerWin() {
if (ScoreChange != null) {
ScoreChange();
}
}
//玩家被捕
public void PlayerGameover() {
if (GameoverChange != null) {
GameoverChange();
}
}
}