Homework07
项目地址
使用说明:创建一个空GameObject将GameModel.cs
挂载到新建游戏对象即可
结果演示
智能巡逻兵
1. 游戏设计要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
2. 程序设计要求
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
实现过程
制作预制
制作主角Hero、巡逻兵Patrol、地图SceneMode的预制如下图
主要代码实现如下
-
GameModel.cs
游戏逻辑主体,实现包括生产英雄、巡逻兵、移动、追捕等。在师兄博客的基础上做了改进,实现了hero碰撞栅栏和边界的检测,只能从栅栏之间的空隙穿过(原实现hero可任意穿过栅栏并在走出边界后会掉落)
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 Holes myHoles; // 巡逻兵正常状态的速度 private const float PERSON_SPEED_NORMAL = 0.01f; // 巡逻兵追捕状态的速度 private const float PERSON_SPEED_CATCHING = 0.01f; void Awake() { PatrolFactory.getInstance().initItem(PatrolItem); myHoles = new Holes(); myHoles.leftUpHole = new Hole(-4.0f, -1.0f, 16.0f, 17.0f); myHoles.leftMidHole = new Hole(-6.0f, -5.0f, 10.0f, 14.0f); myHoles.leftDownHole = new Hole(-4.0f, -1.0f, 6.5f, 7.5f); myHoles.MidHole = new Hole(0.0f, 1.5f, 10.0f, 14.0f); myHoles.rightUpHole = new Hole(2.0f, 4.0f, 17.0f, 18.0f); myHoles.rightMidHole = new Hole(4.5f, 5.5f, 10.0f, 14.0f); myHoles.rightDownHole = new Hole(2.0f, 4.0f, 8.0f, 9.0f); } 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)); int index = getHeroStandOnArea(); Vector3 herolPos = myHero.transform.position; float posX = herolPos.x; float posZ = herolPos.z; switch (dir) { case Diretion.UP: heroMoveUp(index, posX, posZ); // myHero.transform.position += new Vector3(0, 0, 0.1f); break; case Diretion.DOWN: heroMoveDown(index, posX, posZ); // myHero.transform.position += new Vector3(0, 0, -0.1f); break; case Diretion.LEFT: heroMoveLeft(index, posX, posZ); // myHero.transform.position += new Vector3(-0.1f, 0, 0); break; case Diretion.RIGHT: heroMoveRight(index, posX, posZ); // myHero.transform.position += new Vector3(0.1f, 0, 0); break; } } private void heroMoveUp(int index, float posX, float posZ) { switch (index) { case 0: case 1: case 2: if (posZ + 1 < FenchLocation.FenchTop) myHero.transform.position += new Vector3(0, 0, 0.1f); break; case 3: if (posZ + 1 < FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, 0.1f); } else if(myHoles.leftMidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, 1.0f); } break; case 4: if (posZ + 1 < FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, 0.1f); } else if(myHoles.MidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, 1.0f); } break; case 5: if (posZ + 1 < FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, 0.1f); } else if(myHoles.rightMidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, 1.0f); } break; } } private void heroMoveDown(int index, float posX, float posZ) { switch (index) { case 0: if (posZ - 1 > FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, -0.1f); } else if(myHoles.leftMidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, -1.0f); } break; case 1: if (posZ - 1 > FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, -0.1f); } else if(myHoles.MidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, -1.0f); } break; case 2: if (posZ - 1 > FenchLocation.FenchHori) { myHero.transform.position += new Vector3(0, 0, -0.1f); } else if(myHoles.rightMidHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(0, 0, -1.0f); } break; case 3: case 4: case 5: if (posZ - 1 > FenchLocation.FenchBottom) myHero.transform.position += new Vector3(0, 0, -0.1f); break; } } private void heroMoveLeft(int index, float posX, float posZ) { switch (index) { case 0: case 3: if (posX - 1 > FenchLocation.FenchLeft) myHero.transform.position += new Vector3(-0.1f, 0, 0); break; case 1: if (posX - 1 > FenchLocation.FenchVertLeft) { myHero.transform.position += new Vector3(-0.1f, 0, 0); } else if(myHoles.leftUpHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(-1.0f, 0, 0); } break; case 4: if (posX - 1 > FenchLocation.FenchVertLeft) { myHero.transform.position += new Vector3(-0.1f, 0, 0); } else if(myHoles.leftDownHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(-1.0f, 0, 0); } break; case 2: if (posX - 1 > FenchLocation.FenchVertRight) { myHero.transform.position += new Vector3(-0.1f, 0, 0); } else if(myHoles.rightUpHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(-1.0f, 0, 0); } break; case 5: if (posX - 1 > FenchLocation.FenchVertRight) { myHero.transform.position += new Vector3(-0.1f, 0, 0); } else if(myHoles.rightDownHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(-1.0f, 0, 0); } break; } } private void heroMoveRight(int index, float posX, float posZ) { switch (index) { case 0: if (posX + 1 < FenchLocation.FenchVertLeft) { myHero.transform.position += new Vector3(0.1f, 0, 0); } else if(myHoles.leftUpHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(1.0f, 0, 0); } break; case 3: if (posX + 1 < FenchLocation.FenchVertLeft) { myHero.transform.position += new Vector3(0.1f, 0, 0); } else if(myHoles.leftDownHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(1.0f, 0, 0); } break; case 1: if (posX + 1 < FenchLocation.FenchVertRight) { myHero.transform.position += new Vector3(0.1f, 0, 0); } else if(myHoles.rightUpHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(1.0f, 0, 0); } break; case 4: if (posX + 1 < FenchLocation.FenchVertRight) { myHero.transform.position += new Vector3(0.1f, 0, 0); } else if(myHoles.rightDownHole.isInHole(posX, posZ)) { myHero.transform.position += new Vector3(1.0f, 0, 0); } break; case 2: case 5: if (posX + 1 < FenchLocation.FenchRight) 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) { // -1左 0上 1右 2下 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); } } 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; 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; } }
-
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); Object.Destroy(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--; Object.Destroy(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(); } }
-
PatrolBehaviour.cs
挂载到每个巡逻兵,实现巡逻兵对hero的感知和追踪,以及正常和追捕状态的转换。若处于追捕状态,而发现玩家已不在自己的区域,说明在刚一瞬间,玩家逃离了自己区域,此时会添加随机动作,即继续巡逻。
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; 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 () { // hero在自己区域 if (gameStatusOp.getHeroStandOnArea() == ownIndex) { if (!isCatching) { isCatching = true; addAction.addDirectMovement(this.gameObject); } } else { if (isCatching) { 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!"); } } }
-
GameEventManager.cs
采用订阅和发布模式,传递得分、游戏结束等消息,当玩家得分或游戏结束时,都会GameEventManager的得分/游戏结束方法
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逃离巡逻兵追捕,得分+1 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; public class GameStatusText : MonoBehaviour { private int score = 0; // 0为score,1为gameover private int textType; 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!"; } }