原文地址:如何在 Unity 中製作一個道具系統
如果音速小子中沒有金色戒指和電動鞋,超級馬里奧中沒有了蘑菇,或者吃豆人中沒有強力豆會是什麼樣子呢?遊戲就不會那麼有趣了!
道具系統是一個關鍵的遊戲組件,因爲它們增加了額外的複雜性和策略層,來保持移動的動作。
在本教程中你將學會:
- 設計、構建一個可重用的道具系統。
- 在遊戲中使用基於消息的通信。
- 在一個上下“躲避”遊戲中加入你自己的道具系統!
注:本教程假設你熟悉 Unity 並具有 C# 中級技能。如果你需要回顧這些知識,您可以查看我們的其他 Unity 教程。
本教程需要 Unity 2017.1或更高版,所以如果你還沒有升級 Unity,請先升級。
開始
這個遊戲是一個自上而下的戰場躲避遊戲,就像是一個不能射擊、也沒有贏得商業成功的幾何戰爭遊戲。你戴着頭盔的主角必須躲開敵人才能到達出口,撞上敵人會減少生命。當所有的生命耗盡,遊戲就結束了。
將開始項目下載並解壓縮到你指定的位置。用 Unity 打開項目,查看項目文件夾:
- Audio:遊戲的音效文件。
- Materials:遊戲的材質。
- Prefabs:遊戲的預製件,包括遊戲場景、玩家、敵人、粒子和道具。
- Scenes:主遊戲場景。
- Scripts:遊戲的 C# 腳本,包含詳盡的註釋。如果想在開始前熟悉一下它們,請隨意查看這些腳本。
- Textures:遊戲和啓動屏的圖片。
打開 main 遊戲場景,按 Play 按鈕。
你會發現遊戲還沒有任何道具。因此,它很難完成,也有點無聊。你的任務是增加一個道具系統並讓遊戲生動一點。當玩家收集到一個道具,屏幕上會顯示一句來自知名電影中的臺詞。看你能不能猜到是哪部電影,本教程最後會有答案!
道具的生命週期
每個道具都會有一個生命週期,它有幾種不同的狀態:
- 第一階段是創建,它發生在遊戲期間或在設計時,你手動將道具遊戲對象放到場景中時。
- 接下來是吸引模式,道具要麼會動,要麼做一些吸引你注意的事情。
- 收集階段是指拿取道具的行爲,它會觸發聲音、粒子系統或其他特殊效果。
- 收集會導致相關功能的生效,於是道具會“做它該做的事情”。道具效果可以是任何你能想象到的東西,從一個不起眼的加血到給玩家一些超能力。生效階段還會設置過期檢查。如果經過一定的時間之後,玩家受傷或在一定數量的使用之後,或者在其他遊戲條件發生之後,道具就會過期。
- 過期檢查後會來到過期階段。過期階段將銷燬道具,標誌其生命週期結束。
上面的生命週期包含了你想在每個遊戲中重複使用的元素,以及只屬於當前遊戲的元素。例如,檢測玩家是否收集到一個道具,這是你在每個遊戲中都想要的一個功能,但是讓玩家隱形的效果可能只想用在當前遊戲中。在設計腳本邏輯時,這一點很重要。
創建一個簡單的星星道具
你可能很熟悉“低級”的道具,比如金幣、星星或戒指,會提供簡單的加分或加血之類的獎勵。現在,你將在場景中創建一個星星道具,它將立即爲玩家提供一定的生命值,讓你的英雄能活得更好。
只靠星星還是不能很好地進行躲避,但你可以在後面繼續增加其他的道具,這絕對會給你的英雄更大的競爭力。
創建一個精靈,命名爲PowerUpStar,並將其放置在玩家上方(X:6,Y:-1.3)。爲了保持場景的整潔,把精靈放到空文件夾 Powerups 下面:
現在設置精靈的外觀。設置 Transform Scale 爲 (X:0.7, Y:0.7),在Sprite Renderer 組件中,將 Sprite 欄設置爲 star,將 Color 設置爲淡褐色 (R:211, G:221, B:200)。
添加一個方形 2D 碰撞體組件,勾選 Is Trigger 選項框,設置 Size 爲 (X:0.2, Y:0.2):
你已經創建了你的第一個道具。試玩一下游戲,看看效果。道具顯示出來了,但當你撿起它後什麼也不會發生。要解決這個問題,需要寫點腳本。
將遊戲邏輯分離到不同類
作爲一名一絲不苟的開發人員,你希望將自己的時間利用到極致,並重用以前項目中的元素。如果要在道具系統中這樣做,你需要設計它的類層次結構。類層次結構可以將道具邏輯分離成可重用的引擎部分和特定於遊戲的部分。如果你對類層次結構和繼承的概念還不清楚,那麼你可以看看這個視頻的解釋。
上圖顯示中的 PowerUp 是父類。它包含了獨立於遊戲的邏輯,所以可以在其他項目中重用。教程項目已經包含父該類。父類負責管理道具的生命週期,管理道具的各種狀態,並處理碰撞、收集、生效、消息和過期。
父類實現了一個簡單的狀態機,它跟蹤了道具的生命週期。你必須實現它的子類並針對每個新道具在檢查器中設置一些值就行了!
道具的編寫步驟
注:要編寫一個道具的腳本,你必須創建一個 PowerUp 子類,並遵循以下步驟。本教程會多次提到這個步驟,請讓它保持隨時可查狀態!
- 實現 PowerUpPayload 執行想要的效果。
- 此步可選,實現 PowerUpHasExpired 移除上一步的效果。
- 當道具過期時,調用 PowerUpHasExpired。如果道具需要立即過期,請在檢查器中勾選 ExpiresImmediately 選擇框,這樣就不會調用 PowerUpHasExpired。
具體到星星道具上看,它只是簡單的增加一點生命值。你只需要寫很少的代碼就能實現。
創建你的第一個道具腳本
爲 PowerUpStar 遊戲對象新建一個腳本,命名爲 PowerUpStar,然後用編輯器打開。
加入以下代碼,替換原來的 Unity 代碼,保留頭部的 using 語句。
class PowerUpStar : PowerUp
{
public int healthBonus = 20;
protected override void PowerUpPayload() // 步驟 1
{
base.PowerUpPayload();
// 道具的效果是加血
playerBrain.SetHealthAdjustment(healthBonus);
}
}
代碼非常簡短,但這就是你需要在行星道具中實現的全部邏輯!這個腳本實現了道具編寫步驟中的每一步:
- PowerUpPayload 方法中調用了 playerBrain.SetHealthAdjustment 來增加生命點。PowerUp 父類中已經聲明瞭一個 playerBarin 引用。因爲你繼承了父類,你需要調用 base.PowerUpPayload 方法以確保在你的代碼執行之前先執行所有核心邏輯。
- 你不需要實現 PowerUpHasExpired 方法,因爲生命點的增加是永久性的。
- 這個道具是立即過期的,因此,你不需要做任何事情,只需要在檢查器中勾上 ExpiresImmediately 即可。保存代碼,回到 Unity 開始設置檢查器。
創建場景中第一個道具
保存並返回 Unity 後,StarPowerUp 遊戲對象的檢查器是這個樣子:
設置下列屬性:
- Power Up Name: Star
- Explanation: Recovered some health…
- Power Up Quote: (I will become more powerful than you can possibly imagine)
- Expires Immediately: 勾選
- Special Effect: 從項目文件夾中將 Prefabs/Power Ups/ParticlesCollected 預製件拖入
- Sound Effect: 從項目文件夾中將 Audio/power_up_collect_01 音頻剪輯拖入
- Health Bonus: 40
完成後的道具看起來是這個樣子:
設置好 PowerUpStar 遊戲對象後,將它拖到 Prefabs/Power Ups 文件夾,創建一個預製件。
使用新預製件來添加幾顆星星到場景右邊,位置隨意。
運行場景,操作角色到第一個道具。伴隨着你的收集,會發出某些聲音和閃耀的粒子特效。酷!
基於消息的通信
下一個你將創建的道具需要用到一些背景信息。要獲得這些信息,你需要一種方式讓遊戲對象之間進行通信。
例如,當你收集一個道具時,UI必須知道要顯示什麼信息.當玩家的生命值發生變化時,生命欄需要知道如何更新生命值。有很多方法可以做到這一點,Unity 手冊就羅列了其中幾種機制。
每一種通信方法都有其優缺點,而且也不可能有萬能的方法。你在這個遊戲中看到的是一種基於通信的消息機制,在“Unity 手冊”中也大致描述過。
您可以將遊戲對象分爲消息廣播者、消息監聽者,或者兩者的混合:
圖的左邊是消息廣播者。你可以認爲當某些有趣的事情發生時,這些對象會“大聲喊出來”。例如,玩家會廣播“我受傷了”的消息。圖的右邊是消息監聽者。正如其名所示的,它們會監聽消息。監聽者不一定要監聽所有消息,它們只需要監聽自己想響應的那些消息。
消息廣播者也可以同時是消息監聽者。例如,道具會廣播消息,但也會監聽玩家的消息。舉個例子,一個道具在玩家受傷的同時過期。
你可以想象有許多廣播者和監聽者,左邊和右邊會有許多交叉的線條相連。爲了簡單起見,Unity 提供了一個 EventSystem 組件,位於二者之間,就像這樣:
Unity 使用可擴展的 EventSystem 組件來處理輸入。該組件還能管理衆多的發送/接收事件邏輯。
嘿!這理論也太多了點,但無論怎麼說,這樣的消息傳遞系統將允許道具輕易地監聽並減少了對象之間的硬連線。這將使添加新的道具變得非常簡單,特別是在開發的後期,因爲大多數消息都已經被廣播了。
建立消息通信的步驟
這是你真正開始動手之前的最後一點理論。要廣播消息,你需要注意以下步驟:
- 廣播者將他們想要廣播的消息定義爲 C# 接口。
- 然後,廣播者將消息發送給存儲在偵聽者列表中的偵聽者。
如果你需要複習一下,請看這個視頻: C# 接口。
簡而言之,接口定義了方法簽名。實現接口的任何類都承諾通過這些方法來提供功能。
來看一個例子,你會更好理解一些。看下 IPlayerEvents.cs 文件中的代碼:
public interface IPlayerEvents : IEventSystemHandler
{
void OnPlayerHurt(int newHealth);
void OnPlayerReachedExit(GameObject exit);
}
這個 C# 接口定義了 OnPlayerHurt 和 OnPlayerReachedExit提 方法。這些是玩家可以發送的信息。現在查看 PlayerBrain.cs 文件中的SendPlayerHurtMessages 方法。代碼後面的說明中引用的數字和代碼片段中的數字一一對應:
private void SendPlayerHurtMessages()
{
// Send message to any listeners
foreach (GameObject go in EventSystemListeners.main.listeners) // 1
{
ExecuteEvents.Execute<IPlayerEvents> // 2
(go, , // 3
(x, y) => x.OnPlayerHurt(playerHitPoints) // 4
);
}
}
上面的方法負責 OnPlayerHurt 消息的發送。foreach循環遍歷列表EventSystemListeners.main.listers中的每個監聽者,並對每個監聽者調用 ExecuteEvents.Execute,在這個方法發送消息。
上面的代碼可以分爲幾個步驟:
- EventSystemListener.main.listeners 是單例對象 EventSystemListeners 中一個存放全局可見的 GameObjects 列表。任何想要偵聽消息的遊戲對象都必須在此列表中。你可以將GameObjects添加到此列表,通過在檢查器中爲 GameObject 提供一個 Listener 標籤,或者調用EventSystemListeners.main.AddListener 方法。
- ExecuteEvents.Execute 是 Unity 提供的將消息發送到 GameObject 的方法。尖括號中的類型是包含要發送的消息的接口名稱。
- 依照 Unity 手冊的語法示例,GameObject 表明將消息發送給誰,NULL 用於表示額外的事件信息。
- 一個 lamba 表達式。這是一個超出本教程範圍的高級C語言主題。基本上,lambda 表達式允許將代碼作爲參數傳遞到方法中。在這種情況下,代碼包含要發送的消息(OnPlayerHurt)及其所需的參數(PraveHeToPoT)。
該項目已經爲廣播所有必要消息進行了設置。如果你想要擴展項目並在以後添加自己的道具,你可能會發現一些東西很有用。按照慣例,所有接口名稱都以字母I開頭:
- IPlayerEvents: 當玩家受傷或到達出口處時的消息。
- IPowerUpEvents: 當一個道具被收集或過期時的消息。
- IMainGameEvents: 當玩家獲勝或失敗時的消息。
如果你查看上述接口,它們都會有詳盡的註釋。在本教程中理解它們並不重要,因此如果你願意,可以繼續往下看。
一個加速道具
現在你已經學習了消息通信,你需要把它付諸實踐,來監聽一條信息吧!
你將會創建一個道具,給玩家提升額外的速度,直到他們撞到一些東西。當玩家在遊戲中“進行監聽”時,道具會檢測什麼時候玩家碰到某樣東西。更具體地說,道具會監聽玩家發送的“我受傷了”的消息。
要監聽一個消息,你可以遵循以下步驟:
- 實現適當的 C# 接口來定義屬於監聽者的 GameObject 想要監聽的內容。
- 確保監聽者 GameObjects 本身添加到 EventSystemListeners.main.listers 數組。
創建一個新的精靈,命名爲 PowerUpSpeed,並將其放置在遊戲場景左上角的某個位置。將其 Scale 設置爲(X:0.6,Y:0.6)。這個遊戲對象是一個監聽者,所以在檢查器中給它加上一個 Listener 標記。
添加一個方形 2D 碰撞體,將 Size 調整到(X:0.2,Y:0.2)。在 Sprite Renderer 中,將 Sprite 設爲 fast,顏色設置爲之前小星星的顏色。確保您也啓用了 Is Trigger。完成之後,該遊戲對象應該如下所示:
爲這個遊戲對象添加一個腳本,名爲 PowerUpSpeed,添加代碼如下:
class PowerUpSpeed : PowerUp
{
[Range(1.0f, 4.0f)]
public float speedMultiplier = 2.0f;
protected override void PowerUpPayload() // Checklist item 1
{
base.PowerUpPayload();
playerBrain.SetSpeedBoostOn(speedMultiplier);
}
protected override void PowerUpHasExpired() // Checklist item 2
{
playerBrain.SetSpeedBoostOff();
base.PowerUpHasExpired();
}
}
注意代碼中的 Cheklist item 編號。對應每個編號說明如下:
- PowerUpPayload。這會調用基類方法,以確保調用父類代碼,然後在提升玩家速度。注意,父類中定義了 playerBrain,它包含對獲得道具的玩家的引用。
- PowerUpHasExpired。你必須減去之前增加的速度,然後調用基類的方法。
- 最後一個 Checklist item 是當道具過期時調用 PowerUpHasExpired。你將在監聽玩家消息時進行處理。
將類聲明修改爲實現玩家消息接口:
class PowerUpSpeed : PowerUp, IPlayerEvents
注:如果你使用的是 Visual Studio,你可以在輸入 IPlayerEvents 後將鼠標放到上面,然後選擇 Implement interface explicitly 選項。這將爲你創建方法存根。
添加或修改這幾個方法,確保它們如下所示並仍然位於 PowerUpSpeed 類定義之內:
void IPlayerEvents.OnPlayerHurt(int newHealth)
{
// You only want to react once collected
if (powerUpState != PowerUpState.IsCollected)
{
return;
}
// You expire when player hurt
PowerUpHasExpired(); // Checklist item 3
}
/// <summary>
/// You have to implement the whole IPlayerEvents interface, but you don't care about reacting to this message
/// </summary>
void IPlayerEvents.OnPlayerReachedExit(GameObject exit)
{
}
方法 IPlayerEvents.OnPlayerHurt 每當玩家受到傷害時都會被調用。這是“監聽廣播消息”的一部分。在這個方法中,你首先檢查以確保道具只有在被收集之後纔會起作用。然後,代碼調用父類中的 PowerUpHasExpired,它將處理過期邏輯。
保存方法,回到 Unity,進入檢查器進行一些設置。
在場景中創建加速道具
SpeedPowerUp 遊戲對象的檢查器看起來是這個樣子:
在檢查器進行如下設置:
- Power Up Name: Speed
- Explanation: Super fast movement until enemy contact
- Power Up Quote: (Make the Kessel run in less than 12 parsecs)
- Expires Immediately: 反選
- Special Effect: ParticlesCollected (和 Star 道具相同)
- Sound Effect power_up_collect_01 (和 Star 道具相同)
- Speed Multiplier: 2
這樣,你的道具就會變成這樣:
一旦你對加速道具設置好之後,就把它拖到項目樹文件夾 Prefabs/Power Ups 中來創建預製件。你不會在演示項目中這樣用,但是爲了完整起見,這樣做是個好主意。
運行遊戲場景,移動角色收集加速道具,你將獲得一些加速,直到碰上下一個敵人。
推擋道具
下一個道具允許玩家通過按下 P 鍵來推開物體,但是有使用次數的限制。到目前爲止,你應該已經熟悉創建道具步驟了,因此,爲了簡單起見,你將只查看代碼中感興趣的部分,然後拖入預置文件。這種道具不需要監聽消息。
在項目結構視圖中,找到並查看 PowerUpPush 預製件。打開它的 PowerUpPush 腳本,查看代碼。
你會看到其中有我們熟悉的方法。推擋道具的關鍵動作全部在 Update 方法中,如下所示:
private void Update ()
{
if (powerUpState == PowerUpState.IsCollected && //1
numberOfUsesRemaining > 0) //2
{
if (Input.GetKeyDown ("p")) //3
{
PushSpecialEffects (); //4
PushPhysics (); //5
numberOfUsesRemaining--; //6
if (numberOfUsesRemaining <= 0)
{
PowerUpHasExpired (); //7
}
}
}
}
上述代碼做了以下工作:
- 這個腳本只在道具被收集時執行。
- 這個腳本只在還有剩餘的使用次數時執行。
- 這個腳本只在玩家按下 P 鍵時執行。
- 這個腳本會在玩家四周播放漂亮的粒子特效。
- 這個腳本將玩家身邊的敵人推開。
- 這個道具可以多次使用。
- 如果這個道具使用次數用完,則道具就過期。
這個推擋道具很有趣,你可以根據情況在場景中放入 2 個或更多。再次運行遊戲場景,用新道具把附近搞得一團糟吧。
附加作業:傷害免疫道具
本節是可選內容,但很有趣。利用你學到的知識,創造一個道具,使玩家在一定的時間內無懈可擊。
關鍵點和建議:
- Sprite: 看下項目文件夾 Textures/shiled 下面的精靈圖。
- Power Up Name: Invulnerable
- Power Up Explanation: You are Invulnerable for a time
- Power Up Quote: (Great kid, don’t get cocky)
- 編碼: 編碼步驟和星星道具、加速道具一樣。你需要用一個定時器來控制道具的過期時間。PlayerBrain 中的 SetInvulnrability 方法可以切換免疫屬性開或者關。
- Effects: 項目中已經包含了一個漂亮的當玩家處於傷害免疫時圍繞在玩家周圍的脈衝效果。請看 Prefabs/Power Ups/ParticlesInvuln 中的預製件。當玩家傷害免疫時,你可以實例化該預製件,作爲玩家的子節點。
需要完整的答案,或者檢查自己答案是否與我們答案一致,請看下面:
創建新精靈,命名爲 PowerUpInvuln,將它放到 (X:-0.76, Y:1.29)。設置 Scale 爲 X:0.7, Y:0.7。這個遊戲對象不需要監聽任何消息,只會在設定的時間後過期,因此不需要在檢查器中設置其 tag。
添加一個方形 2D 碰撞體,設置 Size 爲 X = 0.2, Y = 0.2。在 Sprite Renderer,設置 Sprite 爲 shield ,顏色和其它道具一致。確保勾上 Is Trigger。做完後,這個遊戲對象會是這個樣子:
爲該遊戲對象添加一個腳本,名爲 PowerUpInvuln 然後加入以下代碼:
class PowerUpInvuln : PowerUp
{
public float invulnDurationSeconds = 5f;
public GameObject invulnParticles;
private GameObject invulnParticlesInstance;
protected override void PowerUpPayload () // 步驟 1
{
base.PowerUpPayload ();
playerBrain.SetInvulnerability (true);
if (invulnParticles != )
{
invulnParticlesInstance = Instantiate (invulnParticles, playerBrain.gameObject.transform.position, playerBrain.gameObject.transform.rotation, transform);
}
}
protected override void PowerUpHasExpired () // 步驟 2
{
if (powerUpState == PowerUpState.IsExpiring)
{
return;
}
playerBrain.SetInvulnerability (false);
if (invulnParticlesInstance != )
{
Destroy (invulnParticlesInstance);
}
base.PowerUpHasExpired ();
}
private void Update () // 步驟 3
{
if (powerUpState == PowerUpState.IsCollected)
{
invulnDurationSeconds -= Time.deltaTime;
if (invulnDurationSeconds < 0)
{
PowerUpHasExpired ();
}
}
}
}
再次注意代碼中的步驟編號。對應於步驟的編號,每段腳本分別進行了如下工作:
- PowerUpPayload: 調用基類方法確保父類代碼被執行,然後設置玩家的 invulnerability 爲 on。同時添加脈衝粒子效果。
- PowerUpHasExpired: 移除前面添加的傷害免疫屬性,調用基類方法。
- 最後一個步驟是在道具過期時調用 PowerUpHasExpired 方法。這樣,你需要在 Update() 方法中計算時間的流逝。
保存腳本,回到 Unity 運行遊戲。你現在將能夠無視傷害安全地打擊一切來犯之敵——直到道具過期!
希望你在看答案之前先自己嘗試一下!
接下來做什麼
你可以下載完整的項目。
如果你想把這個項目繼續做下去,你可以:
- 添加更多道具。也許是一個能夠發射激光殺死敵人的道具?
- 創建一個工廠類,在運行時在戰場中隨機生成道具。
- 如果你想進一步學習 Unity,請去我們的商店中看一下這本 Unity 遊戲教程。
還不知道是哪部電影的臺詞嗎?答案在這裏:
哇喔!你可能是網絡上唯一一個點這個按鈕的人了!答案就是《星際戰爭》系列。
希望喜歡本教程!有任何問題或建議,請在下面留言。