自制Unity小游戲TankHero-2D(1)制作主角坦克


自制Unity小游戲TankHero-2D(1)制作主角坦克

我在做這樣一個坦克游戲,是仿照(http://game.kid.qq.com/a/20140221/028931.htm)這個游戲制作的。僅為學習Unity之用。圖片大部分是自己畫的,少數是從網上搜來的。您可以到我的github頁面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源碼。

clip_image002

本篇主要記錄制作主角坦克(TankHero)的一些重點。

2D游戲布局

clip_image004

如上圖所示,紅色矩形圍起來的是主角坦克,白色的一圈是圍牆,坦克和圍牆在同一平面上。地面背景放到離攝像機最遠的后方。這樣,在2D攝像機下看起來是這樣的:

clip_image006

坦克本身由底座(Base)和炮塔(Head)兩部分組成。當然,在2D世界,其實就是兩個扁平的貼圖。在2D攝像機下是這樣的:

clip_image007

(PS:上圖中的綠色矩形框是Box Collider 2D,忽略即可)

為了保證炮塔始終顯示在底座上方,我們要讓炮塔稍微靠近一點攝像機。如下圖所示,炮塔和底座兩張貼圖是分隔開的。

clip_image008

坦克的運動

坦克的運動包括:上下左右平移;底座旋轉;炮塔旋轉。其中平移時會同樣地移動底座和炮塔,所以用最上層的TankHero負責。底座和炮塔的旋轉我們要求兩者互不干涉,所以TankHead和TankBase放在同一層,並且分別負責各自的旋轉。

clip_image009

移動

坦克的移動十分容易。玩家在縱橫方向的按鍵情況就是坦克的移動方向,速度由程序員指定,再乘上時間就好了。

 1     void Update () {
 2         var h = Input.GetAxis ("Horizontal");
 3         var v = Input.GetAxis ("Vertical");
 4 
 5         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
 6         {
 7             Move (h, v);
 8         }
 9     }
10 
11     void Move(float h, float v) 
12     {
13         var moveVector = new Vector3 (h, v, 0);
14         moveVector.Normalize ();
15         this.transform.position += moveVector * speed * Time.deltaTime;
16     }

 

底座旋轉

底座應該朝向移動的方向,即上文的 moveVector 。這里用 Quaternion.Slerp 使底座平滑地轉向 moveVector 。

 1     void Update () {
 2         var h = Input.GetAxis ("Horizontal");
 3         var v = Input.GetAxis ("Vertical");
 4         
 5         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
 6         {
 7             this.targetAngle = Mathf.Atan2(v, h) * Mathf.Rad2Deg;
 8             //Debug.Log("target angle: " + targetAngle);
 9         }
10         
11         this.transform.rotation = Quaternion.Slerp (
12             this.transform.rotation,
13             Quaternion.Euler (0, 0, targetAngle),
14             rotationSpeed * Time.deltaTime);
15     }

 

炮塔旋轉

炮塔要指向鼠標(即目標)所在的位置,所以從炮塔到鼠標的向量就是炮塔的方向。

注意炮塔不是圍繞自身的中心旋轉的,這個旋轉點需要根據坦克的形狀來指定。所以這里要用 transform.RotateAround 來進行旋轉。

 1     void Update () {
 2         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
 3         RaycastHit hit;        
 4         if(Physics.Raycast(ray, out hit))
 5         {
 6             var p = hit.point;
 7             var y = p.y - this.transform.position.y;
 8             var x = p.x - this.transform.position.x;
 9             if (Mathf.Abs(y) > Quaternion.kEpsilon || Mathf.Abs(x) > Quaternion.kEpsilon)
10             {
11                 this.targetAngle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
12                 var angle = this.targetAngle - this.transform.rotation.eulerAngles.z; 
13 
14                 this.transform.RotateAround (this.rotationCenter.position, new Vector3 (0, 0, 1), angle);
15             }
16         }
17     }

 

車輪滾動

其實這不算是運動了,不過放在這一節也還算緊湊。

坦克移動的時候,我希望車輪下下圖所示這樣,顯得很生動:

clip_image010

我的思路是用4張圖片表現車輪滾動的效果,讓TankBase負責循環顯示這4張圖片。

clip_image012

當然,腳本可以處理任意多張圖片的循環播放。其關鍵就是依次將各個BaseSprite的 renderer.enabled 字段設為 true 。

 1     public float interval = 10;
 2     public List<GameObject> wheels;
 3     private int current = 0;
 4     private float passedInterval = 0;
 5 
 6     // Use this for initialization
 7     void Start () {
 8         if (wheels != null && wheels.Count > 0)
 9         { wheels[0].renderer.enabled = true; }
10 
11         for (int i = 1; i < wheels.Count; i++) 
12         {
13             wheels[i].renderer.enabled = false;
14         }
15     }
16     
17     // Update is called once per frame
18     void Update () {
19         if (wheels == null || wheels.Count < 2) { return; }
20 
21         var h = Input.GetAxis ("Horizontal");
22         var v = Input.GetAxis ("Vertical");
23         
24         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
25         {
26             passedInterval += Time.deltaTime * 100;
27             //Debug.Log (passedInterval);
28             if (passedInterval >= interval)
29             {
30                 var tmp = current;
31 
32                 if (current == wheels.Count - 1) { current = 0; }
33                 else { current++; }
34 
35                 wheels[current].renderer.enabled = true;
36                 wheels[tmp].renderer.enabled = false;
37                 passedInterval = 0;
38             }
39         }
40     }
車輪滾動

 

坦克開炮

這個游戲中,TankHero能夠發射多種炮彈,所以需要有多種武器,每種武器發射一種炮彈。因此炮塔充當了武器管理員的角色,而不是武器本身。一種武器決定了它發射的炮彈的速度、威力等信息。這段話是武器系統的關鍵。

clip_image014

發射炮彈這種事,典型的方法是用 Instantiate 。這就需要在場景中持有一個現成的炮彈。如下圖所示:

clip_image016

這個炮彈要永遠存在,還不能被攝像機看到,所以我們把它放到之前說的地面背景的更后面。

你注意到圖中的炮彈中心有個比較小的綠色的圈,這個圈是Circle Collider 2D,是用來產生碰撞的。我刻意把這個Collider調到這么小,是為了避免在坦克剛剛發射出炮彈時,炮彈與自身產生碰撞(即自己開炮瞬間打了自己)。

clip_image017

同時,在上圖中我用黃色圈圈出了那個BulletPosition的gameobject,這是專門用來指定炮彈產生點的,也是為了避免炮彈剛剛發射出來就把自己給打了。

注意,帶2D的Collider似乎有這樣的問題:無論在Z方向上是否在同一Z平面,都能引發碰撞事件。所以,那個永生的炮彈,雖然藏到地面背景后方去了,卻仍舊可能與游戲中的其它物體發生碰撞(然后就會爆炸消失被Destroy掉,之后就無法再用Instantiate來創建炮彈了)。為了避免它的 Destroy ,我們需要將它和其它炮彈區別開來,所以就必須給炮彈對象添加一個 undying 字段,讓 undying 為true的炮彈在觸發了碰撞事件時也不爆炸消失。

攝像機隨主角移動

我希望地圖能夠大一點,所以一屏肯定放不下。所以需要攝像機隨主角坦克的移動而移動。這個很容易,不斷跟隨主角坦克就行了。

 1     public float catchingSpeed = 1;
 2     private Transform tankHero;
 3 
 4     void Awake()
 5     {
 6         this.tankHero = GameObject.FindGameObjectWithTag (Tags.hero).transform;
 7     }
 8     void Update () {
 9         var targetPosition = new Vector3 (this.tankHero.position.x, this.tankHero.position.y, this.transform.position.z);
10         this.transform.position = Vector3.Lerp (this.transform.position, targetPosition, Time.deltaTime * this.catchingSpeed);
11     }

注意這里將 catchingSpeed 調低一些,會產生攝像機延遲跟隨主角坦克的現象。我很喜歡這種跟隨的感覺,柔和不生硬,而且還解決了后文遇到的一個問題。

自定義鼠標樣式

我希望鼠標在游戲中顯示為下圖所示的樣子,很帶感。

clip_image018

方法有兩種。

Default cursor

一是在File – Build Settings – Player Settings打開的Inspector面板中設置Default Cursor。

clip_image020

這個方法有點問題,首先在build之后的exe中你可能發現鼠標徹底消失了,既沒有原始圖標也沒有自定義圖標,其次在你修改了自定義圖標之后,可能會顯示成一個很奇怪的圖標,最后,這樣自定義的圖標,其清晰度大打折扣,其size也是固定的。

所以我推薦另一種方法,即用腳本實現。

腳本實現

典型的實現方式是這樣的,在主攝像機上添加一個TargetCusor.cs的腳本(腳本名無所謂),編寫代碼如下:

 1     //3D貼圖是Material,2D貼圖是Texture
 2     public Texture CurosrTexture;
 3     void OnGUI() { //    渲染GUI和處理GUI時調用。
 4         if (CurosrTexture != null) {
 5             // 計算圖片左上角的坐標
 6             float left = Input.mousePosition.x - CurosrTexture.width / 2;
 7             float top = Screen.height - Input.mousePosition.y - CurosrTexture.height / 2;
 8             
 9             GUI.DrawTexture(new Rect(left, top, CurosrTexture.width, CurosrTexture.height), CurosrTexture);
10         }
11     }

在Inspector面板中指定你的圖標即可。

clip_image022

圍牆

限制坦克和炮彈的活動范圍是必須的。這里我暫且簡單地制作一個正方形圍牆。

clip_image023

這個圍牆由四個quad組成。綠色的線條是Box Collider 2D組件。圍牆的功能就是把撞上它的東西(坦克、炮彈等)彈回去。這里不得不用一個 Dictionary<Collider2D, Vector3> 字典記錄撞到圍牆的物體在碰撞瞬間的位置,因為之后要將物體彈回這個位置。

 1 public class PushBackToField : MonoBehaviour {
 2     Dictionary<Collider2D, Vector3> initialPositionDict;// = new Dictionary<Collider, Vector3>();
 3 
 4     void Awake()
 5     {
 6         initialPositionDict = new Dictionary<Collider2D, Vector3> ();
 7     }
 8 
 9     void OnTriggerEnter2D(Collider2D other)
10     {
11         if (initialPositionDict.ContainsKey(other))
12         {
13             initialPositionDict[other] = other.transform.position;
14         }
15         else
16         {
17             initialPositionDict.Add(other, other.transform.position);
18         }
19     }
20     
21     void OnTriggerStay2D(Collider2D other)
22     {
23         Push (other);
24     }
25     
26     void OnTriggerExit2D(Collider2D other)
27     {
28         if (initialPositionDict.ContainsKey(other))
29         {
30             initialPositionDict.Remove(other);
31         }
32     }
33     
34     void Push(Collider2D other)
35     {
36         Vector3 initialPosition = Vector3.zero;
37         if (initialPositionDict.ContainsKey(other))
38         {
39             initialPosition = initialPositionDict[other];
40         }
41         else
42         {
43             Debug.LogError(string.Format("{0} should have been added to the dict.", other.gameObject.name));
44         }
45         
46         if ((initialPosition - other.transform.position).magnitude > 0.001f)
47         {
48             //Debug.Log("lerp push");
49             other.transform.position = Vector3.Lerp(other.transform.position, Vector3.zero, Time.deltaTime);
50         }
51         else
52         {
53             //Debug.Log("sudden push");
54             other.transform.position = initialPosition;
55         }
56     }
57 }
圍牆

這個腳本對上下左右四個圍牆都適用,以后有了別的形狀的圍牆,也仍然適用。這也是它的優點之一。

說到這個圍牆的反彈,就涉及攝像機跟隨的一個問題。實際上,圍牆反彈時,如果玩家持續撞擊圍牆,會使玩家坦克產生快速的震動。此時攝像機也就跟着快速震動,這很影響體驗。上文里將跟隨速度設置得比較低時,這種震動就不會影響到攝像機。這是因為,攝像機反應慢,震動速度快,不等攝像機需要向左跟隨,就又要向右跟隨了,所以攝像機基本上就在原地不動了。

將上文的 catchingSpeed 調大一些,再持續去撞牆,你就會明白了。

光源

忘了說,要添加一個線光源,不然場景會很暗淡。下圖就是沒有添加光源的樣子。

clip_image024

加上光源就成了這樣:

clip_image025

顯示文字

想顯示上圖所示的文字?用Unity最近推出的uGUI還是很舒服的(也可能是因為我沒有學過nGUI等等的UI系統吧)。

clip_image026

點擊Text后,會增加3個對象,Canvas,Text和EventSystem。

clip_image028

給Text對象添加一個DrawMouseInfo.cs的組件(名字無所謂)。

 1 public class DrawMouseInfo : MonoBehaviour {
 2     Text guiText;
 3 
 4     void Awake()
 5     {
 6         guiText = this.GetComponent<Text> ();
 7     }
 8 
 9     void Update () {
10         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
11         RaycastHit hit;        
12         if(Physics.Raycast(ray, out hit))
13         {
14             guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, hit.point, hit.transform.gameObject.name);
15         }
16         else
17         {
18             guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, "null", "null");
19         }
20     }
21 }

uGUI對象是可以在Scene視圖里拖動的,只不過你要先找到它。

clip_image030

它的位置很奇葩,如上圖所示,整個地圖在它的Canvas腳下都很渺小。

快速自制貼圖資源

本項目中的坦克、子彈、光標、背景圖都是本人制作的,制作工具你猜猜?是PPT

clip_image032

坦克底座是SmartArt圖形里的。

clip_image034

輪子只是設置了一下漸變填充

clip_image036

炮塔的圓形,把底座的圓形縮小一點就是。炮塔的炮管,是“形狀”里的箭頭,刪掉凸起的尖的部分,調整一下錨點長短就OK。

clip_image037

背景用的是“紋理填充”,看到第二行第一個了沒?

clip_image038

准星,用的是SmartArt里的“分離射線”。把四個箭頭留下,其它內容刪除。再把箭頭的尾部頂點刪除,左右交換位置,上下交換位置,上個色就成了。

clip_image040

clip_image041

還可以吧?

總結

您可以到我的github頁面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源碼。

請多多指教~


免責聲明!

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



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