自制Unity小游戲TankHero-2D(4)關卡+小地圖圖標+碰撞條件分析


自制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     }
GetLevel

在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)上得到工程源碼。

請多多指教~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM