3D游戏编程课程的第七次作业:
课程主页
智能巡逻兵
项目地址
演示视频
设计要求
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
设计实现
巡逻兵
巡逻兵数据
1 2 3 4 5 6
| public class PatrolData : MonoBehaviour { public int manageFloor; public int plyerFloor; public Vector3 initPosition; }
|
巡逻兵工厂
巡逻兵工厂负责产生一定数量、预定位置的巡逻兵,并记录每个巡逻兵相应的管辖范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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]); patrolPrefab = Object.Instantiate(Resources.Load<GameObject>("Prefabs/patrol2"), 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); } }
}
|
巡逻兵动作
巡逻兵动作一共有两种:普通巡逻和追踪敌人,动作的切换通过回调函数实现。
巡逻动作
每个巡逻兵都按照简单的矩形路线不断重复运动,每当到达指定的顶点就换方向。在每次巡逻的同时,判断玩家所在区域是不是自己管辖的区域。如果是,则停止巡逻,开始追击玩家(销毁当前动作,执行回调函数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| public class PatrolMoveAction : SSAction { private float posX, posZ; private float rectLength; private enum Dirction { EAST, NORTH, WEST, SOUTH }; private float speed = 2f; private bool reach = true; private Dirction dirction = Dirction.EAST; private PatrolData data;
public static PatrolMoveAction GetSSAction(Vector3 location) { PatrolMoveAction action = CreateInstance<PatrolMoveAction>(); action.posX = location.x; action.posZ = location.z;
action.rectLength = Random.Range(5, 8); return action; }
public override void Start() { data = this.gameObject.GetComponent<PatrolData>(); }
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.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); } }
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; } } }
|
追踪玩家动作
追踪动作比较简单,创建该动作时会传入玩家对象,之后都朝着玩家的位置移动即可。在动作执行的同时判断玩家是否离开管辖区域,如果离开了,就重新开始巡逻(销毁当前动作,执行回调函数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class PatrolFollowAction : SSAction { private GameObject player; private float speed = 3f; private PatrolData data;
public static PatrolFollowAction GetSSAction(GameObject player) { PatrolFollowAction action = CreateInstance<PatrolFollowAction>(); action.player = player;
return action; }
public override void Start() { data = this.gameObject.GetComponent<PatrolData>(); }
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.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); } } }
|
巡逻兵动作管理器
动作管理器主要负责的是在游戏的开始让巡逻兵开始巡逻,并通过回调函数处理不同的情况:根据回调的参数不同,进行巡逻动作和追踪动作的切换。同时,在由追踪切换为巡逻时,需要发布玩家逃脱的消息,进行后续的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class PatrolActionManager : SSActionManager, ISSActionCallback { private PatrolMoveAction move; private SceneController sceneController;
protected new void Start() { sceneController = SSDirector.GetInstance().CurrentSceneController as SceneController; }
public void PatrolMove(GameObject patrol) { move = PatrolMoveAction.GetSSAction(patrol.transform.position); this.RunAction(patrol, move, this); }
#region ISSActionCallback implementation 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(); } } #endregion }
|
玩家
玩家移动
玩家移动的实现通过获取键盘输入并执行相应的函数完成。这里实现的移动可以通过方向键来控制,上下表示前进和后退,左右表示转向,类似于汽车游戏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void Update() { float transitionX = Input.GetAxis("Horizontal"); float transitionZ = Input.GetAxis("Vertical"); action.MovePlayer(transitionX, transitionZ); timeCounter = action.GetTime(); }
public void MovePlayer(float x, float z) { if (!gameOver) { player.transform.Translate(0, 0, z * playerSpeed * Time.deltaTime); player.transform.Rotate(0, x * 135f * 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.5f) { player.transform.position = new Vector3(player.transform.position.x, 0.5f, player.transform.position.z); } } }
|
碰撞事件
玩家与地图碰撞
为不同的房间添加以下脚本,每个房间的sign
值不同,通过玩家与不同房间地面的碰撞更新玩家的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; } } }
|
玩家与金币碰撞
为金币添加以下脚本,与玩家碰撞后设为不可见,并发布金币减少的消息。
1 2 3 4 5 6 7 8 9 10 11 12
| public class CoinCollide : MonoBehaviour { void OnTriggerEnter(Collider collider) { if (collider.gameObject.name == "player") { this.gameObject.SetActive(false); Singleton<GameEventManager>.Instance.RecudeCoinNum(); } } }
|
玩家与巡逻兵碰撞
为巡逻兵添加以下脚本,当巡逻兵与玩家相撞后,设置两者的动作,并发布玩家被捕的消息。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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(); } } }
|
订阅与发布模式实现
发布者
发布者类共定义了三种消息类型:分数改变、游戏结束和硬币被玩家拾取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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(); } } }
|
订阅者
订阅者为场景控制器,实现的相关函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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(); }
void ReduceCoinNumber() { coinNumberGet += 1; }
|
附加功能
金币
金币是玩家获得游戏胜利的条件,需要拾起地上所有的金币才能结束游戏。
金币工厂
金币也通过工厂模式生成,按照预定位置生成固定数量的金币。玩家每次与金币发生碰撞就把金币设为不可见即可,同时在重新开始游戏时全部设置成可见,这样金币就得到了复用,不需要重新生成新的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class CoinFactory : MonoBehaviour { private GameObject coinPrefab = null; private Vector3[] position = new Vector3[6]; private List<GameObject> coins = new List<GameObject>();
public List<GameObject> GetCoins() { int[] pos_x = {-15, 3, 13, -7, 9}; int[] pos_z = {-15, -3, -15, 15, 10};
for (int i = 0; i < 5; i++) { position[i] = new Vector3(pos_x[i], 1, pos_z[i]); coinPrefab = Object.Instantiate(Resources.Load<GameObject>("Prefabs/Coin"), position[i], Quaternion.identity); coinPrefab.name = "coin" + i; coins.Add(coinPrefab); } return coins; }
public void Reset() { for (int i = 0; i < coins.Count; i++) { coins[i].SetActive(true); } } }
|
倒计时
为了增加游戏的趣味性,增加了倒计时功能,由TimeManager
类实现。当倒计时为0后,游戏结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class TimeManager : MonoBehaviour { private float gameTime = 90f; private float timer = 0; private string timeCounter;
void Start() {
}
public void Reset() { gameTime = 90f; }
public string GetTimeText() { return timeCounter; }
void Update() { int M = (int)(gameTime / 60); float S = gameTime % 60;
timer += Time.deltaTime; if (timer >= 1f) { timer = 0; gameTime--; timeCounter = M.ToString() + ":" + string.Format("{0:00}", S); }
if (gameTime == 0) { Singleton<GameEventManager>.Instance.TimeOut(); } } }
|
相机跟随
相机跟随的实现方式就是让相机在上空与玩家始终保持固定距离,产生相机跟随着玩家移动的效果,增强游戏的真实感。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class CameraFollow : MonoBehaviour { public GameObject player; public float smothing = 5f; Vector3 offset;
void Start() { offset = new Vector3(0, 25, -20); }
void FixedUpdate() { Vector3 target = player.transform.position + offset; transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime); } }
|
实现结果
游戏画面如下: