之前我們完成了使用Unity創建塔防游戲這個小項目,在這篇文章里,我們對項目中學習到的知識進行一次總結。
Part1的地址:http://www.cnblogs.com/lcxBlog/p/6075984.html
Part2的地址:http://www.cnblogs.com/lcxBlog/p/6185330.html
首先,在我們開展這個項目之前,必須具備Unity的基礎知識,例如如何添加游戲資源和組件,理解預設體(prefabs)以及一些C#的編程基礎。可以點擊Chris LaPollo的Unity教程來學習這些基礎知識。
不論是做2D游戲還是3D游戲,搭建好游戲場景是第一步,由于在starter工程中已經包含了背景和UI設置好的場景,所以我們只需要在這基礎之上進行即可。
為Game視圖設置合適的顯示比例,可以保證場景中的Lable(標簽)能夠正確對齊。
prefab
快速創建prefab的方法:將游戲對象從Hierarchy視圖拖拽到Project視圖。
將Project視圖中的prefab拖拽到場景視圖中,就能以此prefab創建出一個游戲對象來,重復多次就能創建多個這樣的對象了。
為腳本中的prefab對象賦值,將prefab從Project視圖拖拽到Inspector視圖。
假如我們為prefab添加了一個游戲組件(例如,腳本、剛體、碰撞體等),那么場景中所有以此prefab創建的對象都會擁有這個游戲組件。
快速復制prefab:傳統的Ctrl + C,Ctrl + V不可行,Unity提供了快捷鍵 Ctrl + D,即Duplicate命令。選中prefab后,按下Ctrl + D即可。同理,也可以用于其他類型資源的復制。
項目中遇到的BUG:小怪獸的所有形態都疊在一起,原因:當一個prefab下有多個子sprite時,若未指定顯示哪個子sprite,則當游戲對象被創建出來后,所有的sprite都會被顯示出來。解決辦法:在創建游戲對象的時候,指定要顯示的sprite。
腳本中數據初始化
通常我們在Start() 中進行數據初始化,但考慮腳本中方法執行順序的問題,有些操作必須放在Start()之前的方法(例如,Awake()、OnEnable() )中做。注意:這些方法名稱的大小寫必須正確,否則不會被調用。執行順序:Awake() ——》OnEnable() ——》Start() 。
項目中運用的地方:腳本MonsterData屬于Monster對象,在OnEnable()中初始化小怪獸的數據,因為OnEnable()會在Unity創建小怪獸的prefab時,立即被調用;Start()需要等到小怪獸對象作為場景的一部分時才會被調用;所以在小怪獸作為場景的一部分之前,我們需要設置好有關的數據;最終得到結論,在OnEnable()中初始化小怪獸的數據。
項目中游戲信息的共享
使用一個其他對象都能訪問的共享對象來存儲數據:GameManager,選擇Create Empty來創建這樣的一個游戲對象。對應的類:GameManagerBehavior,這個類里面管理的信息包括:金幣、波數(第X波敵人)、游戲是否結束、玩家的生命值。
以一個public的bool 變量 gameOver來表示游戲是否結束,其他信息則都有各自對應的屬性,這些屬性的getter方法都很簡單,只是返回字段的值而已,Setter方法除了設置字段的值,還做了不少其他的操作,例如設置Label的顯示,播放相關的動畫等。
C#中的屬性
對應一個私有字段,它是對外使用的,在項目中用于信息的共享。
在類的內部進行取值操作的時候,如果沒有特殊要求,盡量使用字段,直接取值一步到位。
賦值的選擇:對屬性賦值,還是對字段賦值? 取決于我們的目的,是一次單純的賦值,還是要調用Setter方法做更多的操作。 項目中出現的BUG:對字段進行賦值,召喚小怪獸后,小怪獸所有的形態都疊在一起了;因為Setter方法中指定了小怪獸的當前形態。
這個項目中,我們用到的屬性的getter方法都很簡單,只是返回字段的值而已;setter方法中做的操作可以看作一個小函數。同樣是扣除玩家100金幣,gameManager.Gold -= 100; 和 gameManager.DeductPlayerGold(100); 都能做到,但很明顯前者顯得更簡潔,我們不必為函數起名而煩惱了。
項目中用到的特性
1、System.Serializable
在C#中主要用于將一個對象序列化,在Unity中主要作用是使一個數據類型出現在Inspector中。這個數據類型必須是C#基本的數據類型(這里不只是C#,其他Unity能夠識別的編程語言也可以,如JS),或者是Unity3D對象,另外再加上以這些可識別的對象構建的自定義數據類型(如類、結構體等)。注意:我們必須將訪問權限設置為public。
這樣做的好處——用于調節游戲的平衡性:我們可以在游戲運行時隨時更改數據,并且在游戲中立即生效,停止運行后各屬性又能恢復到最初的狀態。這是Unity3D提供的一種運行時調試方式。
[System.Serializable] public class MonsterLevel { public int cost; //召喚小怪獸所消耗的金幣 public GameObject visualization; //小怪獸在某個特定等級的外觀 public GameObject bullet; public float fireRate; }
Inspector中,我們可以查看MonsterLevel這個類的所有public成員,修改它們的數值。
2、HideInspector
與上面的System.Serializable作用相反,可以確保某個數據類型不會出現在Inspector中,這些數據類型往往不希望在Inspector中被修改,但仍然可以在其他腳本中訪問它們。
在下面的代碼中,HideInspector只對waypoints起作用,但被private修飾的currentWaypoint和lastWaypointSwitchTime也不會出現在Inspector中。
[HideInInspector]
public GameObject[] waypoints; //所有的路標
private int currentWaypoint = 0; //敵人當前所在的路標
private float lastWaypointSwitchTime; //敵人經過路標時的時間
public float speed = 1.0f; //敵人的移動速度
出場率較高的方法
1、實例化游戲對象的方法 Static Instantiate()
它的返回值是Object類型,所以它可以克隆任何物體,包括腳本。
Instantiate(original : Object) : Object,等同于復制命令(duplicate,即Ctrl + D),只是對原物體進行復制,不指定position和rotation。
Instantiate(original : Object, position : Vector3, rotation : Quaternion) : Object,等同于復制命令(duplicate),對原物體進行復制,還指定了position和rotation。
這個方法有多個重載,在項目中,我們要選擇合適的重載來完成功能。
2、獲取游戲對象組件的方法 GetComponet(type: Type) : Componet
如果這個游戲對象包含一個類型為type的組件,則返回該組件;如果沒有則為空。我們通過這個方法訪問內建的組件或者腳本組件。調用方式舉例:
//保持金幣數和顯示的同步
goldLable.GetComponet<Text>().text = "GOLD" + gold;
//播放游戲結束的動畫
gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
獲取子物體組件的方法 GetComponetInChildren(type: Type) : Componet
返回這個游戲物體或者它的所有子物體上(深度優先)的類型為type的組件,只返回活動組件(Only active components are returned)。調用方式舉例:
monsterData = gameObject.GetComponentInChildren<MonsterData>();
3、查找游戲對象組件的方法 static Function Find(name: string) : GameObject
Find()方法執行過程是較耗時,所以盡量不要在每一幀中使用它,例如不要在Update()中調用它。
為游戲對象添加標簽:為敵人對象添加標簽Enemy。在Project視圖中,選中名為Enemy的prefab。在Inspector面板的頂部,點擊Tag右邊的下拉框,從彈出的對話框中選擇Add Tag。
點擊下圖中的 + ,新建一個標簽,命名為Enemy。選中Enemy prefab,將它的標簽屬性設置為Enemy。
通過對游戲對象添加Tags(標簽)來區別于其他游戲對象,在腳本中可以通過標簽名快速查找游戲對象。調用的方法:static Function FindGameObjectWithTag(name: string) : GameObject
在項目中是如何運用的:為了便于判斷場景中是否還有敵人存在 GameObject.FindGameObjectWithTag("Enemy") == null
項目中的難點1:
創建塔防游戲里的敵人
1、單波敵人的信息
在大部分塔防游戲中,每一波敵人的數量、外觀、能力都不完全相同,在一波敵人都是一個一個出現的(植物大戰僵尸,一大群僵尸一起出現)。于是我們需要配置每一波敵人的信息有:敵人的外觀、數量、每隔多少秒出現下一個敵人。這些數據可以寫在一個序列化的類Wave里面,這樣我們可以在Inspector面板中更改它的數據。然后,再議Wave[] waves 這個數組來存儲每一波敵人的信息。我們在Inspector面板中設置好waves的長度,為數組的每一個元素都賦值。
2、把一波敵人創建出來
對應的腳本為 SpawnEnemy.cs。
要點:1、游戲未結束,且滿足創建敵人的條件,就要不停地創建敵人,敵人是一個一個被創建出來的,所以在創建一個敵人后,必須隔spawnInterval秒才能創建下一個敵人。
2、這波敵人中,已被創建出來的敵人有多少個enemiesSpawned ;創建上一個敵人的時間 lastSpawnTime,在Start() 中將它設置為 Time.time。
3、同一時刻,場景中只能有一波敵人 4、給玩家留一些時間來準備(放置新的防御塔,升級防御塔),于是在第一波敵人出現之前 或者 第N波敵人全部被消滅時,不要馬上創建第N+1波敵人。于是我們設置 timeBetweenWaves = 5; 5秒鐘后,才會開始出現下一波敵人。
5、當某一波敵人被全部消滅時,為創建下一波敵人做準備,再給予玩家一些金幣獎勵 6、若所有敵人都被消滅,就要播放游戲勝利的動畫
實現:1、判斷是否還有下一波敵人,若沒有的話,游戲結束,玩家勝利; int currentWave = gameManager.Wave; if (currentWave < waves.Length)
2、創建單個敵人。 計算出距離創建上一個敵人過去了多少時間,timeInterval = Time.time - lastSpawnTime
前提:enemiesSpawned < 這波敵人的總數量 。只要滿足以下兩個條件之一,就可以創建。
條件1:已創建的敵人數量 為0,因為要留給玩家一些準備時間,所以還須滿足 timeInterval > timeBetweenWaves ,創建第1個敵人的時候不必考慮spawnInterval的問題 。
條件2:timeInterval > spawnInterval,這個條件表示已經在場景中創建了X個敵人,且到了可以創建下一個敵人的時間。
創建出某個敵人后,enemiesSpawned++
3、表示玩家消滅了一波敵人: enemiesSpawned 等于 這波敵人的總數量 并且 場景中沒有一個敵人對象。
為創建下一波敵人做準備:gameManager.Wave++ enemiesSpawned = 0 lastSpawnTime = Time.time
項目中的難點2:
讓敵人沿著你設定的路線移動
1、為敵人定義移動的路線
按照背景圖中的路徑,建立6個Waypoint路標,游戲中敵人是沿著直線移動的,我們將路標設置在起點、終點、4個拐點上。
如下圖所示,起點路標是在游戲場景之外,敵人的初始位置是在起點路標上,終點路標在我們的餅干上。
2、讓敵人沿著路線移動
這里我們要先設置好敵人的移動速度。
要點:1、敵人是沿著直線移動的,是一種緩動效果。 2、只要敵人沒有被消滅,它們就會一直朝著餅干移動
3、敵人的初始位置在路標0,游戲開始不久后,敵人處在路標0和路標1之間;當敵人經過了路標1后,它的處于路標1和路標2之間。于是,我們得到結論:敵人所處的位置必然在 [路標X , 路標X+ 1] 這個區間里,我們需要記錄敵人已經通過的路標——路標X,以及敵人經過此路標的時間(游戲開始時敵人在路標0,所以敵人經過路標0的時間為當前時間)。
4、當敵人移動后,需判斷它是否抵達了終點路標。A、未抵達,則敵人已通過的路標變為路標X+1,敵人經過進過路標X+1的時間為當前時間,旋轉敵人讓敵人朝著餅干前進;B、抵達了終點路標,銷毀敵人對象,減少玩家的血量。
實現:1、實現緩動效果的方法:Vector3.Lerp(startPosition, endPosition, currentTimeOnPath / totalTimeForPath),計算出某個時刻敵人所處的位置。 startPosition 路標X所在的位置,endPostion 路標X+1所在的位置;totalTimeForPath表示敵人從路標X走到路標X+1所需的時間;由于敵人在路標X的時間lastTime是已知的,所以我們可以計算出currentTimeOnPath = 當前時間 - lastTime ; currentTimeOnPath / totalTimeForPath 就可以表示敵人走完路程的百分比。 最后,Lerp返回值類型為Vector3,即為敵人當前所處的位置。
2、敵人移動的代碼放在Update()中。
3、若敵人當前位置與終點路標的位置相同,則敵人抵達了最終路標。此時需要扣減玩家的血量,我們只需要gameManager.Health -= 1; 即可
4、當敵人抵達一個新的路標(非終點路標)時,旋轉敵人,讓敵人看起來有方向感。將敵人對象圍繞Z旋轉,讓敵人沿著路線前進。此處是本項目中一個不易理解的地方。
A、敵人前進的方向發生了改變,所以我們要先計算出敵人新的前進方向。Vector3 newDirection = (newEndposition - newStartPosition); 我們要讓敵人沿著newDirection所指的方向前進。
B、敵人要旋轉的角度就是新的前進方向和舊的前進方向之間的夾角,我們要計算出這個角度。float rotationAngle = Mathf.Atan2(newDirection.y ,newDirection. x ) * 180 / Mathf.PI; Mathf.Atan2的返回結果是弧度,需要將它 *180 / Math.PI 轉化為弧度。
C、在2D的塔防游戲中,敵人頭頂上的血條都始終保持水平,所以敵人頭頂上的血條沒有必要旋轉,我們只旋轉敵人的子對象——Sprite。 GameObject sprite = (GameObject)gameObject.transform.FindChild("Sprite").gameObject; sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle , Vector3.forward);
游戲中的生命值
1、敵人頭頂上的血條
思路:A、用兩張圖片來顯示,一張是暗的,表示背景圖;另一張是綠色較小的細長圖片,表示前景圖。通過縮放前景圖的長度,來匹配敵人當前血量。
B、設置好兩張圖片的屬性
C、為前景圖添加一個腳本,用來調整它的縮放長度
如何為敵人添加血條:
A、將Enemy prefab 拖拽到場景中,現在Hierarchy視圖中出現了一個名為Enemy的對象。
B、將Image HealthBarBackground 拖拽到Enemy對象上,作為Enemy的子對象。
C、將Image HealthBar 的Pivot設置為Left,因為血條的縮減是從右到左的;將HealthBar的X scale設置為125,把它拉長,令它的長度不小于HealthBarBackground
D、為HealthBar添加一個C#腳本,命名為HealthBar.cs
E、Enemy對象的初始位置是在場景之外的,于是需要將它的坐標設置為(20, 0, 0)
F、點擊Inspector面板頂部的Apply按鈕,保存對prefab的更改。刪除Hierarchy視圖中的Enemy對象。
反向思考:刪掉敵人頭頂上的血條?例如:將Enemy2的血條刪掉。(實質問題:刪掉prefab下的某個、某些Sprite)
選中與為敵人添加血條的過程相似:將Enemy prefab拖拽到場景中,然后依次刪除Enemy對象下的兩個Sprite,最后Inspector面板頂部的Apply按鈕,保存對prefab的更改。刪除Hierarchy視圖中的Enemy對象。
不啟用敵人頭頂上的血條?
取消上圖的勾勾,只是不啟用HealthBarBackground 這個Sprite而已,當我們想要用到它的時候,勾上這個勾勾即可。不啟用的效果如下圖所示:
在腳本中縮放血條的長度
要點: A、敵人剛出現的時候,都是滿血的,我們需要記錄敵人的最大生命值、當前生命值、血條圖片縮放的長度——X Scale。
B、在Start()方法中,設置血條圖片縮放的長度
C、敵人在移動過程中遭到攻擊,血量會減少,我們需要在Update()方法中縮放血條的長度
實現: A、用2個public類型的變量來記錄敵人的最生命值 maxHealth 和 敵人當前的生命值 currentHealth。用一個private類型的變量 originalScale 來記錄血條圖片縮放的長度——X Scale。
B、在Start() 中寫: originalScale = gameObject.transform.localScale.x;
C、用一個臨時變量tmpScale獲取localScale的值,然后為tmpScale.X賦值,最后將tmpScale賦給localScale 。
void Update ()
{
Vector3 tmpScale = gameObject.transform.localScale;
tmpScale.x = currentHealth / maxHealth * originalScale;
gameObject.transform.localScale = tmpScale;
}
以上代碼不能簡寫成: gameObject.transform.localScale.x = currentHealth / maxHealth * originalScale;
因為編譯器會報錯,提示:” 不能修改 UnityEngine.Transform.localScale 的返回值,因為它不是變量“。
2、玩家的生命值
在GameManagerBehavior.cs 中管理玩家的血量。
要點:A、以一個Text healthLabel來顯示玩家的血量;為了讓游戲更有趣些,GameObject[] healthIndicator 數組用來表示5只正在啃餅干的小蟲子,當玩家血量減1的時候,就隱藏一只小蟲子。
B、玩家血量減到0的時候,需要結束游戲,播放游戲失敗的動畫。
C、以一個屬性Helath來管理玩家的血量,處理血量變化的代碼都放在Setter方法中。
D、需要削減玩家血量的時候,只要寫出如下簡潔的代碼即可:
GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
gameManager.Health -= 1;
碰撞體組件——Collider 2D
我們需要根據,物體的形狀和游戲需求來選擇合適形狀的碰撞體,這個組件在項目中發揮了兩個作用:
1、檢測在某個點的鼠標點擊
在鼠標點擊召喚點的時候,就可以在上面放置防御塔(就是我們的小怪獸啦)或者對防御塔進行升級。 為召喚點Openspot添加一個Box Collider 2D,看矩形的碰撞體最適合。
響應鼠標點擊的方法:OnMouseUp(),在鼠標點擊了一個游戲對象的碰撞體時,Unity會自動調用這個方法。這個方法的大小寫不可寫錯,否則不會被調用。
2、用于觸發事件
令小怪獸能夠檢測到在它射程內的敵人,在添加碰撞體的時候,我們需要做一些適當的設置。
A、為Monster prefab添加一個Circle Collider 2D組件,一個2D圓形碰撞體組件。
為什用Circle,而不是上面的Box?使用Circle可以很好地展示小怪獸的攻擊范圍(以它為圓心的一個圓形區域),它的半徑就是小怪獸的射程。
啟用Is Trigger這個屬性,目的是令此碰撞體用于觸發事件,并且不會發生任何物理交互。如果不啟用這個屬性的話,就是會發生碰撞。我們希望觸發的事件——當敵人進入小怪獸的射程中時,小怪獸立即對它開火。
因為小怪獸被放置在召喚點的上方,所以必須防止小怪獸的Circle Collider 2D響應鼠標點擊——應該由召喚點來響應;否則,會造成召喚小怪獸后,無法對其進行升級。在Inspector面板中,將Layer屬性設置為Ignore Raycast,然后在彈出的對話框中選擇Yes,change children。這樣,小怪獸的Circle Collider 2D就不會響應鼠標點擊了。
B、為Enemy prefab添加一個Rigid Body 2D組件(剛體)和一個Circle Collider 2D組件。
當兩個碰撞體發生碰撞的時候,至少要有一個附帶剛體組件,才會觸發碰撞事件。而我們希望觸發的碰撞事件為:Enemy的碰撞體和Monster的碰撞體互相碰撞時所觸發的碰撞事件。
勾選剛體的Is Kinematic屬性,這是為了令敵人對象不受Unity中的物理引擎影響。
將的Circle Collider 2D組件半徑設置為1。
C、響應碰撞事件的方法
void OnTriggerEnter2D(Collider2D other) 當碰撞體other進入觸發器時OnTriggerEnter2D被調用 當敵人進入小怪獸的射程內時會被調用
void OnTriggerExit2D(Collider2D other) 當碰撞體other離開觸發器時OnTriggerExit2D被調用 當敵人移動到小怪獸的射程外時會被調用
項目中的難點3:
讓小怪獸們追蹤射程內的敵人
為Enemy prefab添加一個腳本組件——EnemyDestructionDelegate.cs,這個腳本包含了一個委托 void EnemyDelegate(GameObject enemy);
為Monster prefab添加一個腳本組件,命名為ShootEnemies.cs。
思路:1、以一個List集合——enemiesInRange 來存儲某個小怪獸攻擊范圍內所有的敵人。這個List初始是空的。每個小怪獸對象都有一個這樣的List,一個敵人可能會在多個小怪獸的射程內。
2、當敵人進入射程內時,將此敵人添加到這個List中;當敵人移動到射程外 或者 敵人被消滅 時,將此敵人從這個List中移除。
3、由于我們無法得知Unity什么時候會調用OnTriggerEnter2D和OnTriggerExit2D這兩個方法,于是我們需要靈活地添加、移除敵人對象。而委托可以讓一個游戲對象靈活地通知另一個游戲對象做出改變。
實現:1、這個List里面儲存的類型是GameObject類型,為什么不是Enemy類型?因為游戲中的敵人不止Enemy這一種,還有Enemy2等等。
2、寫一個方法:當敵人被消滅的時候移除enemiesInRange 中的某個對象 void OnEnemyDestroy(GameObject enemy);
3、當敵人進入射程時,我們需要將OnEnemyDestroy添加到委托EnemyDestructionDelegate的方法列表中;當敵人移動到射程外時,我們需要將OnEnemyDestroy從委托EnemyDestructionDelegate的方法列表中移除。
以下是三個方法的實現:
void OnEnemyDestroy(GameObject enemy) { enemiesInRange.Remove(enemy); } void OnTriggerEnter2D(Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")){ enemiesInRange.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D(Collider2D other) { if (other.gameObject.tag.Equals("Enemy")){ enemiesInRange.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } }
項目中的難點4:
小怪獸的子彈
要點:A、子彈也是由prefab初始化出來的游戲對象,帶有一個腳本BulletBehavior.cs來處理子彈的行為。
B、子彈的坐標設置:由于本項目是個2D游戲,所以我們可以事先設置好子彈的Z坐標。而子彈產生的位置是不確定的,于是我們只能在子彈產生的時候設置它的X、Y坐標。
C、子彈的飛行速度、子彈的攻擊力 這兩個數據可以配置的。子彈的初始位置、子彈的目標(小怪獸要攻擊的敵人)、目標所處的位置,這3個數據在子彈對象產生的時候才能確定下來。
D、與敵人移動的方式一樣,子彈的飛行也是一種緩動效果,只不過比敵人移動得更快而已。
E、子彈擊中敵人后,如果敵人被消滅,需要給予玩家金幣獎勵。
F、每一種子彈對應以一個等級的小怪獸,小怪獸的等級越高,子彈的攻擊力越強。
實現:1、子彈產生的時間startTime = Time.time ,用于實現子彈的緩動效果;在Start()中計算出子彈與目標間的距離;獲取GameManagerBehavior的實例,用于給予玩家金幣獎勵。
2、子彈產生后就會朝著目標飛過去,與處理敵人移動的邏輯相同,都是放在Update()中。
3、計算子彈的當前位置: A、子彈飛行了多長時間 timeInterval = Time.time - startTime;
B、還是使用Lerp來計算,gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);
4、當子彈的位置與目標的位置相同時,子彈擊中了敵人。若敵人已不存在(它已被其他小怪獸消滅了);若敵人存在,則按子彈的攻擊力削減敵人的生命值。最后子彈消失(銷毀子彈這個游戲對象)
5、子彈擊中敵人后,若敵人的生命值被削減至0或0以下,則該敵人被消滅,玩家獲得一些金幣獎勵。
6、子彈和小怪獸的對應關系需要在MonsterData.cs里進行配置。在Inspector面板中,展開Monster Data腳本組件中的Levels數組,設置好每一項數據。
項目中的難點5:
小怪獸的攻擊對象
每個小怪獸都有一個射程內的敵人List,但我們的小怪獸每次只能攻擊一個敵人(你可以在此之上拓展,做出有AOE能力的小怪獸),所以必須確定對哪個敵人開火。其實答案很簡單,對距離餅干最近的敵人開火。這部分的邏輯寫在ShootEnemies.cs這個腳本里。
1、找出距離餅干最近的敵人 如何找出這樣的敵人是關鍵點!
思路:A、MoveEnemy.cs這個腳本要提供一個方法:計算敵人與餅干之間的距離
B、計算List中每一個敵人與餅干的距離,游戲的每一幀中都需要找出距離餅干最近的敵人。
C、通過尋找最小數的算法找到距離餅干最近的敵人,
實現:A、計算出敵人尚未走完的路程distance有多長。任何時候敵人都處于[ 路標X,路標X+1 ] 這個區間內。我們先計算出敵人當前位置與路標X+1之間的距離;然后通過循環累加路標X+1與路標X+2的距離,一直累加到路標X+N與終點路標的距離。將這些距離都累加起來,就可以得出敵人與餅干之間的距離了。
B、在Update()中遍歷enemiesInRange,計算出每一個敵人與餅干之間的距離distanceToGoal。
C、臨時變量 minimalEnemyDistance = float.MaxValue; 確保不會有比它更大的距離。 若 distanceToGoal < minimalEnemyDistance , 目標被暫定為這個敵人,minimalEnemyDistance = distanceToGoaL 。當循環結束的時候,我們就找出了距離餅干最近的敵人。
D、假如List是空的,那么這個循環不會執行,小怪獸就沒有開火的目標了。
2、攻擊這個敵人
只要這個敵人仍然存在場景中,我們就要攻擊它。
思路:A、因為每個等級的小怪獸都有自己的發射率(如3秒發射一次,2秒發射一次),所以小怪獸必須是間歇性地發射子彈。這一點與創建敵人的方式是相同的,需要記錄上一次發射子彈的時間。
B、寫一個void Shoot(target)方法,處理射擊的邏輯
C、旋轉小怪獸的角度,讓它能夠對著敵人開火(如果你做出了能夠AOE的防御塔,可以不必旋轉它)。
實現:A、計算 當前時間 與 上一次射擊時間 的差值,若大于 小怪獸的當前等級的發射率,則小怪獸可以繼續發射子彈,上一次射擊時間更新為當前的時間。
B、 分為以下3個步驟: 1、獲取小怪獸當前等級的子彈的prefab,子彈的初始坐標startPosition與小怪獸的坐標相同,目標的坐標targetPostion就是target的坐標了。但startPosition.z和targetPostion.z必須設置為bulletPrefab的Z坐標。
2、實例化一個子彈對象,設置好它的位置、初始位置、目標所在位置。
3、播放一個射擊的動畫和一個射擊的音效。
C、旋轉角度的問題:與旋轉敵人角度的處理方式是相同的。
文章列表