自制Unity小游戲TankHero-2D(4)關卡+小地圖圖標+碰撞條件分析
我在做這樣一個坦克游戲,是仿照(http://game.kid.qq.com/a/20140221/028931.htm)這個游戲制作的。僅為學習Unity之用。圖片大部分是自己畫的,少數是從網上搜來的。您可以到我的github頁面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源碼。
本篇主要記錄關卡解析器、小地圖圖標和對碰撞的原理的探索,需要耐心分析。
關卡解析器
在一個關卡里,敵方坦克應該是一波一波地出現,每波敵人出現多少個,每個敵人是什么類型的坦克、出現在什么位置都應該是可配置的。這需要一個關卡解析器,把如下的文字解析為一個數據結構 Level 。
1 level 2 { 3 tank{0 0} | 4 tank{0 1} | 5 tank{0 2} | 6 tank{0 0} tank{0 1} tank{0 2} | 7 tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} | 8 tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} | 9 tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} | 10 tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} tank{0 0} tank{0 1} tank{0 2} 11 }
這段文字的意思是,第一波敵人是類型編號為0,出生位置編號為0的1個坦克;第二波敵人是類型編號為0,出生位置編號為1的1個坦克;。。。
這種東西我喜歡用編譯原理解決,因為我在(https://github.com/bitzhuwei/CGCompiler.git)有一個自己寫的自動生成詞法、語法分析器的工具。關於這個工具的介紹可參考我博客里關於編譯原理的文章(在這里搜索"編譯器")。
先總結一下關卡的文法
1 <Level> ::= "level" "{" <StepList> "}"; 2 <StepList> ::= <Step> <StepList> | null; 3 <Step> ::= "step" "{" <TankList> "}"; 4 <TankList> ::= <Tank> <TankList> | null; 5 <Tank> ::= "tank" "{" <TankPrefab> <BornPoint> "}"; 6 <TankPrefab> ::= number; 7 <BornPoint> ::= number;
然后用工具生成詞法語法解析器代碼。
剩下的就是自己寫一下從語法樹到數據結構的轉換。代碼如下。

1 public static Level GetValue(this SyntaxTree<EnumTokenTypeLevelCompiler, 2 EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 3 { 4 if (syntaxTree == null) { return null; } 5 6 var result = new Level(); 7 _GetLevel(result, syntaxTree); 8 return result; 9 } 10 11 private static void _GetLevel(Level result, SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 12 { 13 if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Level___tail_levelLeave()) 14 { 15 GetTankList(result, syntaxTree.Children[2]); 16 } 17 } 18 19 private static void GetTankList(Level level, SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 20 { 21 if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_tankLeave() 22 || syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_or_Leave()) 23 { 24 var egg = GetTank(syntaxTree.Children[0]); 25 level.Add(egg); 26 GetTankList(level, syntaxTree.Children[1]); 27 } 28 else if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankList___tail_rightBrace_Leave()) 29 { 30 //nothing to do 31 } 32 33 } 34 35 private static TankEgg GetTank(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 36 { 37 if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Tank___tail_tankLeave()) 38 { 39 var tankPrefab = GetTankPrefab(syntaxTree.Children[2]); 40 var bornPoint = GetBornPoint(syntaxTree.Children[3]); 41 var result = new TankEgg(tankPrefab, bornPoint); 42 return result; 43 } 44 else if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_Tank___tail_or_Leave()) 45 { 46 var result = new TankEgg(-1, -1); 47 return result; 48 } 49 50 return null; 51 } 52 53 private static int GetBornPoint(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 54 { 55 if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_BornPoint___numberLeave()) 56 { 57 var result = int.Parse(syntaxTree.Children[0].NodeValue.NodeName); 58 return result; 59 } 60 61 return 0; 62 } 63 64 private static int GetTankPrefab(SyntaxTree<EnumTokenTypeLevelCompiler, EnumVTypeLevelCompiler, TreeNodeValueLevelCompiler> syntaxTree) 65 { 66 if (syntaxTree.CandidateFunc == LL1SyntaxParserLevelCompiler.GetFuncParsecase_TankPrefab___numberLeave()) 67 { 68 var result = int.Parse(syntaxTree.Children[0].NodeValue.NodeName); 69 return result; 70 } 71 72 return 0; 73 }
在VS2013里你可以獲得Tip,便於coding。
解析器寫好了,調用方式如下。
語法分析器的類型太長,只好用上圖表示一下。
這樣就有敵方坦克一波一波來襲的感覺了。
小地圖圖標
小地圖上顯示的坦克很不清晰,如果能顯示出一個鮮艷的三角形就好了,尖頭指向開炮的方向。如下圖所示,綠色的為玩家坦克,紅色的為敵方坦克。
首先給坦克的prefab增加一個顯示箭頭的子對象。
給子對象smallMapTankHero增加Sprite Renderer組件,在組件的Sprite屬性里賦予下面的圖片,並把此對象的Layer屬性設置為自定義的"smallCamera"(Layer的名字無所謂)。
Hierarchy里的smallCamera對象是用來顯示小地圖的。設置其Culling Mask屬性如下圖所示,勾選smallCamera。這樣,箭頭就會顯示在smallCamera里。
相應的,在主攝像機Main Camera里,設置Culling Mask屬性如下圖所示,取消勾選smallCamera。這樣,箭頭就不會顯示在smallCamera里。
有點平行世界異次元的感覺。
小地圖里仍舊會顯示原有的坦克貼圖,為了擋住坦克貼圖,我們把小地圖圖標向上(靠近攝像機的方向)移動一點點。
當然,也可以通過把坦克對象的Layer設置為一個自定義的Layer(比如自定義為realworld),然后在smallCamera的Culling Mask屬性中取消勾選realworld即可。不過這還需要把各種對象都放到自定義的Layer里去,太麻煩了。
碰撞
Unity中的碰撞,有Collision(物理碰撞)和Trigger(觸發器碰撞)兩種。
這里我用精心設計的試驗分析出了碰撞的產生條件。
觸發碰撞的基本條件
下面,假設Hierarchy中有兩個對象A和B,A和B都有Collider組件和Rigidbody組件。那么:
如果去掉其中任何一個的Collider,那么不會發生碰撞事件(只會穿透)。
如果去掉B的rigidbody,那么移動B去撞A時,不會發生碰撞事件(只會穿透)。
如果去掉B的rigidbody,那么移動A去撞B時,Collision和Trigger的碰撞都可以發生在A和B身上,但B不會受物理引擎影響而移動。
OnTriggerXXX的觸發原則
下面,再假設A是Cube,B是sphere;A有3個Box Collider,設置第2個Box Collider的Is Trigger為true;B有4個Shpere Collider,設置第2、3個Sphere Collider的Is Trigger為true。
然后,給A和B分別添加如下的腳本組件:
1 public class TriggerTest : MonoBehaviour 2 { 3 4 // Use this for initialization 5 void Start() 6 { 7 } 8 9 // Update is called once per frame 10 void Update() 11 { 12 } 13 14 void OnTriggerEnter(Collider other) 15 { 16 Debug.Log(string.Format("{0} trigger {1}'s({2})", this.name, other.name, other.GetInstanceID())); 17 } 18 19 void OnCollisionEnter(Collision collision) 20 { 21 Debug.Log(string.Format("{0} collision {1}'s({2})", this.name, collision.transform.name, collision.collider.GetInstanceID())); 22 } 23 }
這樣就可以記錄自己碰撞了對方的哪個Collider。
我們先讓A撞B,再讓B撞A,分別得到如下圖左右所示的結果。
分析發現,雖然引發的碰撞事件的先后順序有所不同,但碰撞事件是相同的,都是那16個Trigger事件和2個Collision事件。
據此我總結出Trigger事件的觸發原則,如下圖所示。
當A的第2個Collider設置為Is Trigger=true時,此Collider會與B的各個Collider都引發一次Trigger碰撞事件。B也同理。不過兩邊均為Is Trigger=true時,只會引發一次。數一下上圖,可以看到有8條線,每條線在AB兩方各引發一次Trigger碰撞事件,所以就是上文的16次。另外,每個Collider引發的次數也與上文的圖示吻合。
OnCollisionXXX的觸發條件
下面,我把A和B的所有Collider的Is Trigger都設為false,來研究Collision的觸發條件。
下圖展示了去掉B的Rigidbody后,分別用A撞B和用B撞A的情況。
可以看到,沒有Rigidbody的B撞A,什么都不會發生,B直接穿透了A。
再用含有2個Collider的Cube和含有2個Collider的Sphere試驗(此處略)就可以知道,A撞B則會使A的第1個Collider依次與B的各個Collider引發Collision碰撞事件。所以上圖左側會有1個A的Collider與4個B的Collider引發的Collision事件。
繼續看下圖,是同時具有Rigidbody的A和B相互碰撞的結果。
可以看到A和B都只有1次Collision事件。
根據上述試驗,我認為Unity3D引擎在處理Collision事件時,是去找AB雙方的Rigidbody,如果找到了就讓它執行物理引擎;如果都找到了,此次碰撞就告結束,不再引發此次A和B的Collision事件。
總結
我查了Unity一些資料,總結了一下Unity中關於碰撞的原則:
physics will not be applied to objects that do not have Rigidbodies attached.
Kinematic Rigidbody 自身不受外力,但仍舊可對其它物體產生力。
您可以到我的github頁面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源碼。
請多多指教~