Attention:
由于此次作业代码量与平时作业相比稍多_(:зゝ∠)_为了避免篇幅过长,所以下文仅介绍实现难点,而不考虑po出所有的代码。但是可以访问我的github查看完整项目代码和资源哦(可以两边参照着看 or 下载下来跑哦~~):
https://github.com/MarkMoHR/GetAwayFromPatrols
游戏规则:
游戏效果:
游戏UML类图:
游戏难点实现:
1、巡逻兵(Patrol)的行走模式,如何实现循环走动?
@答:简单来说就是“动作结束后,在动作结束回调函数里再添加动作”
@详解:从“巡逻兵交互”部分看到有个IAddAction接口,提供两个方法:addRandomMovement(当巡逻兵为非追捕状态时,添加随机 移动动作)、addDirectMovement(当巡逻兵为追捕状态时,添加指向玩家的移动动作)。
addRandomMovement的实现就是添加随机方向动作、addDirectMovement的实现就是用巡逻兵和玩家的位置相减得到移动方
向。注意的是巡逻兵不能走出自己的区域,代码里也有根据位置进行判断。
巡逻兵的行走利用了面向对象的动作管理(上面UML图右下红框部分)。当巡逻兵初始化时候,加随机动作。由于动作结束时候会回调接口函数SSActionEvent(在GameModel里实现),此时再根据传回参数(传参下面会说)继续调用添加动作的方法addRandomMovement或addDirectMovement。
==> GameModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class GameModel : SSActionManager, ISSActionCallback {
public GameObject PatrolItem, HeroItem, sceneModelItem, canvasItem;
private SceneController scene;
private GameObject myHero, sceneModel, canvasAndText;
private List<GameObject> PatrolSet;
private List<int> PatrolLastDir;
private const float PERSON_SPEED_NORMAL = 0.05f;
private const float PERSON_SPEED_CATCHING = 0.06f;
void Awake() {
PatrolFactory.getInstance().initItem(PatrolItem);
}
protected new void Start () {
scene = SceneController.getInstance();
scene.setGameModel(this);
genHero();
genPatrols();
sceneModel = Instantiate(sceneModelItem);
canvasAndText = Instantiate(canvasItem);
}
protected new void Update() {
base.Update();
}
//生产英雄
void genHero() {
myHero = Instantiate(HeroItem);
}
//生产巡逻兵
void genPatrols() {
PatrolSet = new List<GameObject>(6);
PatrolLastDir = new List<int>(6);
Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
for (int i = 0; i < 6; i++) {
GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
newPatrol.transform.position = posSet[i];
newPatrol.name = "Patrol" + i;
PatrolLastDir.Add(-2);
PatrolSet.Add(newPatrol);
addRandomMovement(newPatrol, true);
}
}
//hero移动
public void heroMove(int dir) {
myHero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
switch (dir) {
case Diretion.UP:
myHero.transform.position += new Vector3(0, 0, 0.1f);
break;
case Diretion.DOWN:
myHero.transform.position += new Vector3(0, 0, -0.1f);
break;
case Diretion.LEFT:
myHero.transform.position += new Vector3(-0.1f, 0, 0);
break;
case Diretion.RIGHT:
myHero.transform.position += new Vector3(0.1f, 0, 0);
break;
}
}
//动作结束后
public void SSActionEvent(SSAction source, SSActionEventType eventType = SSActionEventType.Completed,
SSActionTargetType intParam = SSActionTargetType.Normal, string strParam = null, object objParam = null) {
if (intParam == SSActionTargetType.Normal)
addRandomMovement(source.gameObject, true);
else
addDirectMovement(source.gameObject);
}
//isActive说明是否主动变向(动作结束)
public void addRandomMovement(GameObject sourceObj, bool isActive) {
int index = getIndexOfObj(sourceObj);
int randomDir = getRandomDirection(index, isActive);
PatrolLastDir[index] = randomDir;
sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
Vector3 target = sourceObj.transform.position;
switch (randomDir) {
case Diretion.UP:
target += new Vector3(0, 0, 1);
break;
case Diretion.DOWN:
target += new Vector3(0, 0, -1);
break;
case Diretion.LEFT:
target += new Vector3(-1, 0, 0);
break;
case Diretion.RIGHT:
target += new Vector3(1, 0, 0);
break;
}
addSingleMoving(sourceObj, target, PERSON_SPEED_NORMAL, false);
}
int getIndexOfObj(GameObject sourceObj) {
string name = sourceObj.name;
char cindex = name[name.Length - 1];
int result = cindex - '0';
return result;
}
int getRandomDirection(int index, bool isActive) {
int randomDir = Random.Range(-1, 3);
if (!isActive) { //当碰撞时,不走同方向
while (PatrolLastDir[index] == randomDir || PatrolOutOfArea(index, randomDir)) {
randomDir = Random.Range(-1, 3);
}
}
else { //当非碰撞时,不走反方向
while (PatrolLastDir[index] == 0 && randomDir == 2
|| PatrolLastDir[index] == 2 && randomDir == 0
|| PatrolLastDir[index] == 1 && randomDir == -1
|| PatrolLastDir[index] == -1 && randomDir == 1
|| PatrolOutOfArea(index, randomDir)) {
randomDir = Random.Range(-1, 3);
}
}
//Debug.Log(isActive + " isActive " + "PatrolLastDir " + PatrolLastDir[index] + " -- randomDir " + randomDir);
return randomDir;
}
//判定巡逻兵走出了自己的区域
bool PatrolOutOfArea(int index, int randomDir) {
Vector3 patrolPos = PatrolSet[index].transform.position;
float posX = patrolPos.x;
float posZ = patrolPos.z;
switch (index) {
case 0:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 1:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 2:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHori)
return true;
break;
case 3:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
case 4:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
case 5:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHori)
return true;
break;
}
return false;
}
//追捕hero
public void addDirectMovement(GameObject sourceObj) {
int index = getIndexOfObj(sourceObj);
PatrolLastDir[index] = -2;
sourceObj.transform.LookAt(sourceObj.transform);
Vector3 oriTarget = myHero.transform.position - sourceObj.transform.position;
Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
target += sourceObj.transform.position;
//Debug.Log("addDirectMovement: " + target);
addSingleMoving(sourceObj, target, PERSON_SPEED_CATCHING, true);
}
void addSingleMoving(GameObject sourceObj, Vector3 target, float speed, bool isCatching) {
this.runAction(sourceObj, CCMoveToAction.CreateSSAction(target, speed, isCatching), this);
}
void addCombinedMoving(GameObject sourceObj, Vector3[] target, float[] speed, bool isCatching) {
List<SSAction> acList = new List<SSAction>();
for (int i = 0; i < target.Length; i++) {
acList.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
}
CCSequeneActions MoveSeq = CCSequeneActions.CreateSSAction(acList);
this.runAction(sourceObj, MoveSeq, this);
}
//获取hero所在区域
public int getHeroStandOnArea() {
return myHero.GetComponent<HeroStatus>().standOnArea;
}
}
2、巡逻兵添加动作后,移动过程中遇到障碍物怎么办?
(我在编码的时候也遇到添加动作,但是遇到障碍物过不去,即巡逻兵不能到达指定目标位置,动作一直不能结束,不断产生抖动现象。怎么解决这个问题?)
@答:利用巡逻兵Collision碰撞检测方法,检测到碰撞,发送信号,销毁现在的动作,再添加其他方向的动作。
@详解:每个巡逻兵上面都挂载一个脚本PatrolBehaviour.cs,在碰撞检测函数里(注意这里用了OnCollisionStay函数,而不是Enter,原因是确保处于碰撞中必定能变换方向,而Enter只会触发进入碰撞那一瞬间),调用IAddAction接口函数添加随机动作。
void OnCollisionStay(Collision e) {
//撞击围栏,选择下一个点移动
if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
|| e.gameObject.tag.Contains("FenceAround")) {
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
//撞击hero,游戏结束
……
}
3、上面的代码只是在碰撞时候添加动作,那原来还有的动作呢(一直不能结束)?
@答:添加动作时候需要销毁该对象现有的动作,保证巡逻兵所有时刻都只有一个动作
@详解:我把SSActionManager改写了一下,主要是runAction方法里添加了一部分代码,用于在某对象添加动作时,需先销毁属于他的现在的所有动作。
==> SSActionManager.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
//并发顺序
public class SSActionManager : MonoBehaviour {
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 Start () {
}
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(kv.Key);
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 gameObj, SSAction action, ISSActionCallback manager) {
//先把该对象现有的动作销毁(与原来不同部分)
for (int i = 0; i < waitingAdd.Count; i++) {
if (waitingAdd[i].gameObject.Equals(gameObj)) {
SSAction ac = waitingAdd[i];
waitingAdd.RemoveAt(i);
i--;
DestroyObject(ac);
}
}
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.gameObject.Equals(gameObj)) {
ac.destroy = true;
}
}
action.gameObject = gameObj;
action.transform = gameObj.transform;
action.callBack = manager;
waitingAdd.Add(action);
action.Start();
}
}
4、巡逻兵如何 感知玩家,并进行追踪?如何 感知到玩家逃离,并继续巡逻?
@答:玩家hero有个所在区域的变量信息,每个巡逻兵时刻检测该信息,来判断是否在自己区域,从而有对应行动。
@详解:在玩家hero上添加一个脚本HeroStatus.cs,有个standOnArea的整形变量,时刻根据玩家位置改变该变量的值。巡逻兵在Update()方法里时刻检测该值来判断玩家是否进入自己区域。若是,添加跟踪动作addDirectMovement(在上面GameModel.cs 里实现),而原动作也自动销毁。
巡逻兵上挂载的脚本PatrolBehaviour.cs里有个isCatching的变量,代表巡逻兵是否处于追捕状态。若处于追捕状态,而发现玩家已不在自己的区域,说明在刚一瞬间,玩家逃离了自己区域,此时会添加随机动作,即继续巡逻。
==> PatrolBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
//----------------------------------
// 此脚本加在巡逻兵上
//----------------------------------
public class PatrolBehaviour : MonoBehaviour {
private IAddAction addAction;
private IGameStatusOp gameStatusOp;
public int ownIndex;
public bool isCatching; //是否感知到hero
private float CATCH_RADIUS = 3.0f;
void Start () {
addAction = SceneController.getInstance() as IAddAction;
gameStatusOp = SceneController.getInstance() as IGameStatusOp;
ownIndex = getOwnIndex();
isCatching = false;
}
void Update () {
checkNearByHero();
}
int getOwnIndex() {
string name = this.gameObject.name;
char cindex = name[name.Length - 1];
int result = cindex - '0';
return result;
}
//检测进入自己区域的hero
void checkNearByHero () {
if (gameStatusOp.getHeroStandOnArea() == ownIndex) { //只有当走进自己的区域
if (!isCatching) {
isCatching = true;
addAction.addDirectMovement(this.gameObject);
}
}
else {
if (isCatching) { //刚才为捕捉状态,但此时hero已经走出所属区域
gameStatusOp.heroEscapeAndScore();
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
}
}
void OnCollisionStay(Collision e) {
//撞击围栏,选择下一个点移动
if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
|| e.gameObject.tag.Contains("FenceAround")) {
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
//撞击hero,游戏结束
if (e.gameObject.name.Contains("Hero")) {
gameStatusOp.patrolHitHeroAndGameover();
Debug.Log("Game Over!");
}
}
}
5、如何利用 订阅和发布模式传信息(玩家得分、游戏结束信息)?
@答:通过GameEventManager发布上述两信息。GameStatusText订阅信息。
@详解:此模式的理解为:当玩家得分或游戏结束时,都会GameEventManager的得分/游戏结束方法,但是具体怎么实现不用管。因为有可能后期需求更改,得分/游戏结束后需要实现更多效果。而此时我们只需要让这些效果订阅GameEventManager发布的得分/游戏结束信息,即可产生作用。实现功能的分类,降低代码耦合。
而由于此次作业的得分/游戏结束均只需改变得分数or显示"Game Over!",所以我在挂载在UI.text上的脚本GameStatusText订阅两种信息即可,这样就可以在触发玩家得分、游戏结束时,自己改变文字内容。
==> GameEventManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Patrols;
public class GameEventManager : MonoBehaviour {
public delegate void GameScoreAction();
public static event GameScoreAction myGameScoreAction;
public delegate void GameOverAction();
public static event GameOverAction myGameOverAction;
private SceneController scene;
void Start () {
scene = SceneController.getInstance();
scene.setGameEventManager(this);
}
void Update () {
}
//hero逃离巡逻兵,得分
public void heroEscapeAndScore() {
if (myGameScoreAction != null)
myGameScoreAction();
}
//巡逻兵捕获hero,游戏结束
public void patrolHitHeroAndGameover() {
if (myGameOverAction != null)
myGameOverAction();
}
}
==> GameStatusText.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//----------------------------------
// 此脚本加在text上
//----------------------------------
public class GameStatusText : MonoBehaviour {
private int score = 0;
private int textType; //0为score,1为gameover
void Start () {
distinguishText();
}
void Update () {
}
void distinguishText() {
if (gameObject.name.Contains("Score"))
textType = 0;
else
textType = 1;
}
void OnEnable() {
GameEventManager.myGameScoreAction += gameScore;
GameEventManager.myGameOverAction += gameOver;
}
void OnDisable() {
GameEventManager.myGameScoreAction -= gameScore;
GameEventManager.myGameOverAction -= gameOver;
}
void gameScore() {
if (textType == 0) {
score++;
this.gameObject.GetComponent<Text>().text = "Score: " + score;
}
}
void gameOver() {
if (textType == 1)
this.gameObject.GetComponent<Text>().text = "Game Over!";
}
}
好啦,主要是实现起来有一点点麻烦的东西大致就是上面那些了。琐碎的那些代码没放出来哈。有需要的可以到我的github上看哦:
https://github.com/MarkMoHR/GetAwayFromPatrols