3D Game Programming & Design:物理系統與碰撞
編程實踐
作業要求
- 提交要求:
- 遊戲設計要求:
創建一個地圖和若干巡邏兵(使用動畫);
每個巡邏兵走一個3~5個邊的凸多邊型,位置數據是相對地址。即每次確定下一個目標位置,用自己當前位置爲原點計算;
巡邏兵碰撞到障礙物,則會自動選下一個點爲目標;
巡邏兵在設定範圍內感知到玩家,會自動追擊玩家;
失去玩家目標後,繼續巡邏;
計分:玩家每次甩掉一個巡邏兵計一分,與巡邏兵碰撞遊戲結束; - 程序設計要求:
- 必須使用訂閱與發佈模式傳消息
subject:OnLostGoal
Publisher: ?
Subscriber: ? - 工廠模式生產巡邏兵
- 必須使用訂閱與發佈模式傳消息
- 友善提示1:生成 3~5個邊的凸多邊型
隨機生成矩形
在矩形每個邊上隨機找點,可得到 3 - 4 的凸多邊型
5 ? - 友善提示2:參考以前博客,給出自己新玩法
模式分析:訂閱與發佈模式
在“發佈者-訂閱者”模式中:
稱爲發佈者的消息發送者不會將消息編程爲直接發送給稱爲訂閱者的特定接收者。這意味着發佈者和訂閱者不知道彼此的存在。存在第三個組件,稱爲代理或消息代理或事件總線,它由發佈者和訂閱者都知道,它過濾所有傳入的消息並相應地分發它們。
換句話說,pub-sub是用於在不同系統組件之間傳遞消息的模式,而這些組件不知道關於彼此身份的任何信息。經紀人如何過濾所有消息?實際上,有幾個消息過濾過程。最常用的方法有:基於主題和基於內容的。
發佈/訂閱模式的有點非常明顯:
- 時間上的解耦:發佈者/訂閱者在大多情況下是異步方式(使用消息隊列)。
- 對象之間的解耦:在Publisher / Subscriber模式中,組件是鬆散耦合的,而不是Observer模式。
代碼分析
本次作業參考了前輩的博客,設計的UML圖如下:
-
訂閱與發佈模式的利用
利用訂閱和發佈模式傳信息,實現不同預製之間信息的傳遞,也即實現遊戲中GameStatus(遊戲狀態)和CanvasStatus(分數和遊戲界面顯示) 的交流,信息包括玩家得分和遊戲結束信息。using System.Collections; using System.Collections.Generic; using UnityEngine; using Com.Patrols; public class GameStatus: MonoBehaviour { public delegate void GameScoreAction(); public static event GameScoreAction myGameScoreAction; public delegate void GameOverAction(); public static event GameOverAction myGameOverAction; private SceneController scene; private int canMove = 1; void Start () { scene = SceneController.getInstance(); scene.setGameStatus(this); } void Update () { } //hero逃離巡邏兵,得分 public void heroEscapeAndScore() { myGameScoreAction(); canMove = 1; } //巡邏兵捕獲hero,遊戲結束 public void patrolHitHeroAndGameover() { myGameOverAction(); canMove = 0; } }
由於此次作業的得分/遊戲結束均只需改變得分數or顯示"Game Over!",所以我在掛載在UI.text上的腳本CanvasStatus訂閱兩種信息gameOver和gameScore即可,這樣就可以在觸發玩家得分、遊戲結束時,調用GetComponent< Text >自己改變文字內容。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; //---------------------------------- // 此腳本加在text上 //---------------------------------- public class CanvasStatus : MonoBehaviour { private int score = 0; private int textType; void Start () { distinguishText(); } void Update () { } void distinguishText() { if (gameObject.name.Contains ("Score")) textType = 0; else { Debug.Log (gameObject.name); textType = 1; } } void OnEnable() { GameStatus.myGameScoreAction += gameScore; GameStatus.myGameOverAction += gameOver; } void OnDisable() { GameStatus.myGameScoreAction -= gameScore; GameStatus.myGameOverAction -= gameOver; } void gameScore() { if (textType == 0 && this.gameObject.name.Contains("Score")) { score++; this.gameObject.GetComponent<Text>().text = "Score: " + score; } } void gameOver() { if (textType == 1) { this.gameObject.GetComponent<Text> ().text = "Game Over!"; } } }
這種模式可以有效的實現功能的分類,降低代碼耦合。
-
PatrolFactory主要用來實現巡邏兵和玩家的生產
using System.Collections; using System.Collections.Generic; using UnityEngine; using Com.Patrols; namespace Com.Patrols { public class PatrolFactory : System.Object { private static PatrolFactory instance; private GameObject PatrolItem; private GameObject HeroItem; private Vector3[] PatrolPosSet = new Vector3[] { new Vector3(-6, 0, 16), new Vector3(-1, 0, 19), new Vector3(6, 0, 16), new Vector3(-5, 0, 7), new Vector3(0, 0, 7), new Vector3(6, 0, 7)}; public static PatrolFactory getInstance() { if (instance == null) instance = new PatrolFactory(); return instance; } public void initPatrol(GameObject _PatrolItem) { PatrolItem = _PatrolItem; } public void initHero(GameObject _Hero){ HeroItem = _Hero; } public GameObject getPatrol() { GameObject newPatrol = Camera.Instantiate(PatrolItem); return newPatrol; } public GameObject getHero(){ GameObject newHero = Camera.Instantiate (HeroItem); return newHero; } public Vector3[] getPosSet() { return PatrolPosSet; } } }
-
將上述腳本中的初始化載入GameModel中,在這個腳本中具體實現巡邏兵和玩家直接動作的交互。
這裏動作實現的難點在於如何實現巡邏兵的循環走動,這裏主要涉及兩個函數:
addRandomMovement:當巡邏兵爲非追捕狀態時,添加隨機方向動作public void addRandomMovement(GameObject sourceObj, bool isActive) { if (ifStop == 1) return; 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); }
addDirectMovement:當巡邏兵爲追捕狀態時,添加指向玩家的移動動作,它的實現就是用巡邏兵和玩家的位置相減得到移動方向。
public void addDirectMovement(GameObject sourceObj) { if (ifStop == 1) return; 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); }
這裏要注意的是巡邏兵不能走出自己的區域,我們通過一個bool函數來做判斷。
//判定巡邏兵走出了自己的區域 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; }
-
玩家掛載腳本HeroStatus
該腳本上有個standOnArea的整形變量,時刻根據玩家位置改變該變量的值。//檢測所在區域 void modifyStandOnArea() { float posX = this.gameObject.transform.position.x; float posZ = this.gameObject.transform.position.z; if (posZ >= FenchLocation.FenchHori) { if (posX < FenchLocation.FenchVertLeft) standOnArea = 0; else if (posX > FenchLocation.FenchVertRight) standOnArea = 2; else standOnArea = 1; } else { if (posX < FenchLocation.FenchVertLeft) standOnArea = 3; else if (posX > FenchLocation.FenchVertRight) standOnArea = 5; else standOnArea = 4; } }
巡邏兵在Update()方法裏時刻檢測該值來判斷玩家是否進入自己區域。若是,添加跟蹤動作addDirectMovement(在上面GameModel.cs 裏實現),而原動作也自動銷燬。
javascript void Update () { modifyStandOnArea(); }
- 巡邏兵掛載腳本PatrolBehaviour
這裏有個isCatching的變量,代表巡邏兵是否處於追捕狀態。若處於追捕狀態,而發現玩家已不在自己的區域,說明在剛一瞬間,玩家逃離了自己區域,此時會添加隨機動作,即繼續巡邏。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Com.Patrols; //---------------------------------- // 此腳本加在巡邏兵上 //---------------------------------- public class PatrolStatus : 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")) { Debug.Log ("Pump wall"); isCatching = false; addAction.addRandomMovement(this.gameObject, false); } //撞擊hero,遊戲結束 if (e.gameObject.name.Contains("hero")) { gameStatusOp.patrolHitHeroAndGameover(); isCatching = false; addAction.addStop (); } } }
- 巡邏兵掛載腳本PatrolBehaviour
-
還有其他一些聯繫各個腳本動作的部分代碼,以及界面和用戶交互代碼就不贅述了,見Github
遊戲界面效果