Unity 脚本入门教程
原文:Introduction to Unity Scripting
作者:Georgi Ivanov
譯者:kmyhy
Unity 的許多功能都要通過它的富腳本語言 C# 來體現。你可以用它來處理用戶輸入,操作場景中的對象,碰撞檢測,自動生成新的 GameObject 和在場景中發射定向光以處理游戲邏輯。聽起來很可怕,但 Unity 提供了有良好文檔的 API,使得這些任務的完成輕而易舉——哪怕你是一個新手!
在本教程中,你將創建一個 Top down shooter 游戲,用 Unity 腳本處理敵人的生成、玩家控制、開火以及其它游戲中的重要方面。
注意:本教程假設你擁有一定的 C# 或類似編程語言經驗,理解 Unity 的界面和工作方式。如果你忘記了這些內容,請閱讀我們的 Unity 入門教程。
本教程針對 Unity 5.3 以上。你可以從這里下載 Unity 的最新版。
Unity 同時支持 UnityScript 和 Boo,但大部分程序員都喜歡使用 C#。C# 被數百萬開發者用于開發 app、web 和游戲開發,有海量的資料和教程能夠幫你學習它。
開始
下載開始項目 BlockBuster,解壓縮,用 Unity 打開文件夾。
打開后是這個樣子:
看一下場景視圖。有一個小的競技場,這是游戲的主戰場,以及一個相機和一盞燈。如果你的布局和上圖不同,請點擊右上角的下拉框并選擇 2 by 3。
主角都沒有叫什么游戲?你的第一個任務就是創建一個代表玩家的 GameObject。
創建玩家角色
在結構視圖中,點解 Create 按鈕,選擇 3D Object\Sphere。將球體放到 (X:0, Y:0.5, Z:0) 然后命名為 Player。
Unity 用全組件式系統來構建 GameObject。也就是說所有的 GameObject 都是由組件構成,這些組件會給游戲對象賦予行為和特性。這是幾個 Unity 的內置組件:
- Transforme: 每個 GameObject 都有這個組件。它保存了 GameObject 的位置、角度和比例。
- Box Collider:一種立方形的碰撞體,用于檢測碰撞。
- Mesh Filter:用于顯示 3D 模型的網格數據。
Player 游戲對象需要和場景中的其它對象發生碰撞反應。要實現這一點,請在結構視圖中選擇 Player,然后點擊檢視器窗口中的 Add Component 按鈕。在彈出菜單中選擇 Physics > Rigidbody,這就為 Player 添加了一個剛體組件,這樣它就能夠使用 Unity 的物理引擎了。
修改這個剛性體的屬性為:Drag 設為 1,Angular Drag 為 0,勾選 Freeze Position 旁邊的 Y。
這將保證玩家角色不會被上下移動,同時在轉動時不添加阻尼系數。
編寫玩家移動的腳本
玩家角色創建好之后,準備創建接收鍵盤輸入以及移動玩家的腳本。
在項目窗口中,點擊 Create Button\Folder。命名文件夾為 Scripts,然后在下面創建一個 Player 子文件夾。
在 Player 文件夾中,點擊 Create 按鈕,選擇 C# Script。新腳本命名為 PlayerMovement。這個樣子:
注意:創建這些文件夾有利于將文件安裝各自的職能進行組織,避免混亂。你將為 Player 創建多個腳本,因此單獨用一個文件夾會更好。
雙擊 PlayerMovement.cs 腳本。這會用你喜歡的代碼編輯器打開這個腳本。Unity 內置了 MonoDevelop,它支持所有平臺,在安裝器運行時,Windows 用戶可以安裝 Visual Studio 來取代它。
本教程假設你使用 MonoDevelop,但 Visual Studo 用戶也不會有任何問題。
當代碼編輯器打開,你會看到:
這是 Unity 在新腳本中生成的默認的類。它繼承了 MonoBehaviour 基類,這樣腳本才能夠在游戲中運行,同時還有一些特殊的方法對特定事件作出響應。如果你是一個 iOS 開發者,這個類就好比 UIViewCotnroller。Unity 會在運行腳本時以特定順序調用多個方法。最常見的幾個方法包括:
- Start(): 這個方法在腳本第一次 update 時調用。
- Update(): 當游戲正在運行,同時腳本是可用的,這個方法會在每幀刷新時調用。
- OnDestroy(): 在這個腳本所附著的 GameObject 被銷毀之前調用。
- OnCollisionEnter(): 當這個腳本所附著的碰撞體或剛體和其它碰撞體或剛體發生接觸時調用。
完整的事件列表,請參考 Unity 的 MonoBehaviours 文檔。
在 Start() 方法前,添加兩行代碼:
public float acceleration; public float maxSpeed;腳本看起來是這個樣子:
這是公共變量聲明,這意味著這兩個變量能夠在檢視器中看到并修改,而無需在腳本和編輯器中來回切換。
acceleration 表示玩家的速度隨著時間遞增。maxSpeed 則表示速度的上限。
在它們后面聲明這幾個變量:
private Rigidbody rigidBody; private KeyCode[] inputKeys; private Vector3[] directionsForKeys;私有變量無法用檢視器來設置,它的初始化由開發者在某個時機負責。
rigidBody 用于保存一個對剛體組件的引用,即附著在 Player GameObject 上的剛體組件。
inputKeys 是一個鍵盤碼的數組,用于檢查輸入。
directionsForKeys 用于保存一個 Vector3 變量數組,這些變量表示方向數據。
將 Start() 方法修改為:
void Start () {inputKeys = new KeyCode[] { KeyCode.W, KeyCode.A, KeyCode.S, KeyCode.D };directionsForKeys = new Vector3[] { Vector3.forward, Vector3.left, Vector3.back, Vector3.right };rigidBody = GetComponent<Rigidbody>(); }這段代碼將按鍵對應到方向,比如 W 是向前。最后一行獲得了一個對所附著的剛體組件的引用,將它保存到 rigidBody 變量以便使用。
要真正移動玩家的角色,還需要處理鍵盤輸入。
將 Update() 修改為 FixedUpdate() 并加入以下代碼:
// 1 void FixedUpdate () {for (int i = 0; i < inputKeys.Length; i++){var key = inputKeys[i];// 2if(Input.GetKey(key)) {// 3Vector3 movement = directionsForKeys[i] * acceleration * Time.deltaTime;}} }有幾個地方需要注意一下:
如果你是編程新手,你可能奇怪為什么要乘以 Time.detalTime。游戲是在幀率(幀/秒)下運行的,幀率是取決于硬件和它運行壓力,這樣在性能好的機器上幀率快,而在性能差的機器上幀率慢,從而導致不可預知的結果。通常的辦法是,當需要按每幀執行一個動作時,都乘上 Time.deltaTime。
在 FixedUpdate() 方法后添加:
void movePlayer(Vector3 movement) {if(rigidBody.velocity.magnitude * acceleration > maxSpeed) {rigidBody.AddForce(movement * -1);} else {rigidBody.AddForce(movement);} }這個方法向剛體施加一個力,驅使它移動。乳溝當前速度超過 maxSpeed,這個力會轉成反方向,讓玩家減速,將速度有效地限制在最大速度下。
在 FixedUpdate() 方法中,在 if 語句右括號結束之前,添加:
movePlayer(movement);棒極了!保存腳本,回到 Unity 編輯器。在項目窗口,將 PlayerMovement 腳本拖到結構視圖的 Player 上。
將腳本添加到一個 GameObject 會導致創建一個組件實例,也就是說所有的代碼會被所附著的 GameObject 所執行。
用檢視器將 Acceleration 設置為 625,Max Speed 設置為 4375:
運行場景,用 WASD 鍵移動玩家角色:
太好了,我們只用了幾行代碼!:]
但是,有一個很明顯的問題——玩家會飛快地跑到視線以外,讓我們很難去和敵人戰斗啊。
編寫相機腳本
在腳本編輯器中,新建腳本名為 CameraRig,然后將它添加到 Main Camera。還需要介紹詳細步驟嗎?你可以參考下面的答案。
參考步驟
選擇 Scripts 文件夾,點擊項目瀏覽器中的 Create 按鈕,選擇 C# 腳本。取名為 CameraRig。將新腳本拖到 Main Camera 對象:
然后,在新的 CameraRig 類的 Start() 方法之上中添加如下變量:
public float moveSpeed; public GameObject target;private Transform rigTransform;你可能想到了,moveSpeed 是相機跟隨目標——任何場景內部游戲對象——進行移動的速度。
在 Start() 方法中,添加:
rigTransform = this.transform.parent;這句引用了父對象 Camera 在場景樹中的 transform 組件。每個在場景中的對象都會有一個 Transform 組件,它描述了對象的位置、角度和比例。
在同一腳本中,添加方法:
void FixedUpdate () {if(target == null){return;}rigTransform.position = Vector3.Lerp(rigTransform.position, target.transform.position, Time.deltaTime * moveSpeed); }CameraRig 的移動代碼比 PlayerMovement 要簡單。這是因為你不需要剛體,在 rigTransform 和 target 的位置之間做插值運算即可。
Vector3.Lerp() 以兩個點和一個0-1 之間的小數做參數,這個小數表示兩個端點之間的一個位置。左端點為 0,右端點為 1。0.5 則返回兩點之間的終點。
以漸慢方式讓 rigTransform 靠近 target 的位置。也就是說——相機會跟隨玩家角色。
回到 Unity。在結構視圖選中 Main Camera。在檢視器中,設置 Move Speed 為 8 ,Target 為 Player:
運行游戲,在場景中四處移動;相機將平滑跟隨 target。
創建敵人
沒有對手的射擊游戲玩起來固然輕松,但也太無聊了 :] 通過頂部 GameObject\3D Object\Cube 菜單創建一個方塊作為敵人。將方塊命名為 Enemy 并添加一個 Rigidbody 組件。
在檢視器中,首先將方塊的 Transform 設為 (0, 0.5,4)。在 Rigidbody 組件的 Constraints欄,勾上 Freeze Position 旁邊的 Y。
太好了——現在讓敵人以一種嚇人的方式移動吧。在 Scripts 目錄下新建腳本 Enemy。這個步驟你應該很熟悉了,如果忘記了,請參考前面的描述過的步驟。
然后,在類中聲明變量:
public float moveSpeed; public int health; public int damage; public Transform targetTransform;這些變量的作用并不難猜。moveSpeed 先前在相機中也用到過,這里是同樣的作用。health 和 damage 用于決定敵人什么時候死,以及它們對玩家造成的傷害。targetTransform 引用了玩家的 transform。
對于 Player 來說,你需要一個類描述玩家的所有屬性,這一切恰好是敵人想摧毀的。
在項目瀏覽器中,選中 Player 文件夾并新建腳本 Player,這個腳本用于對碰撞進行處理,并保存玩家的生命值。雙擊腳本,打開它。
添加一個公共變量用于保存玩家的生命值:
public int health = 3;這里為 health 設置了一個默認值,但你還可以在檢視器中修改這個值。
要處理碰撞,添加如下方法:
void collidedWithEnemy(Enemy enemy) {// Enemy attack codeif(health <= 0) {// Todo } }void OnCollisionEnter (Collision col) {Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();collidedWithEnemy(enemy); }OnCollisionEnter() 方法會在兩個帶有碰撞體的剛體發生碰撞時觸發。Collision 參數包含了交點和碰撞速度等信息。在這里,你只對 Collision 中的 Enemy 組件感興趣,因此調用 collidedWithEnemy() 并執行攻擊邏輯——這個在后面添加。
回到 Enemy.cs,添加下列方法:
void FixedUpdate () {if(targetTransform != null) {this.transform.position = Vector3.MoveTowards(this.transform.position, targetTransform.transform.position, Time.deltaTime * moveSpeed);} }public void TakeDamage(int damage) {health -= damage;if(health <= 0) {Destroy(this.gameObject);} }public void Attack(Player player) {player.health -= this.damage;Destroy(this.gameObject); }FixedUpdate() 方法你應該很熟悉了,略有不同的地方是,你用 MoveToward() 替代了 Lerp() 方法。這是因為敵人始終以同樣的速度進行移動,當它到達目標后不需要減速。當敵人被子彈擊中,TakeDamage() 方法被調用;當敵人的生命值變為 0,它將被銷毀。Attack() 是類似的——它將傷害施加到 Player,然后敵人自動銷毀。
回到 Player.cs,在 collidedWithEnemy() 方法中,將注釋“Enemy attack code”替換成:
enemy.Attack(this);在這個過程中,玩家被減血,敵人自毀。
返回 Unity。將 Enemy 腳本綁定到 Enemy 對象,在檢視器中,修改 Enemy 的屬性:
現在你應該自己嘗試著修改這些值。自己動手,然后和下面的 Gif 動畫進行比較:
參考答案
在這個游戲中,當敵人和玩家發生碰撞,就會構成一次攻擊。用 Unity 的物理引擎來檢測碰撞不過是小菜一碟。
最終,將 Player 腳本綁定到結構視圖中的 Player 上。
運行游戲,注意查看控制臺:
當敵人碰上玩家,它會進行攻擊并扣減玩家的生命值為 2。但控制臺會拋出一個 NullReferenceException 錯誤,指向了 Player 腳本的這一行:
哎呀——玩家不僅僅和敵人發生了碰撞,也和游戲中的其它對象發生了碰撞,比如競技場。因為這個對象沒有 Enemy 腳本,因此 GetComponent() 返回了 null。
打開 Player.cs。在 OnCollisionEnter() 方法中,用一個 if 語句將 collidedWithEnemy() 方法包裹起來:
Open Player.cs. In OnCollisionEnter(), wrap collidedWithEnemy() in an if statements: if(enemy) {collidedWithEnemy(enemy); }不會為空了!
使用預制件
只能逃跑、躲避敵人的游戲完全是一邊倒的游戲。是該武裝我們的玩家進行戰斗的時候了!
點擊結構視圖中的 Create 按鈕,然后選擇 3D Object/Capsule。命名為 Projectile,然后設置它的 transform 為:
Position: (0, 0, 0) Rotation: (90, 0, 0) Scale: (0.075, 0.246, 0.075)當玩家開火時,會發射一顆 Projectile 對象。要實現這個,你需要創建一個預制件。和場景中你曾經創建的對象不同,預制件是根據游戲邏輯按需創建的。
在 Assets 下新建一個文件夾 Prefabs。將 Projectile 對象拖進這個文件夾。現在,你就擁有了一個預制件!
你的預制件需要寫點腳本。在 Scripts 目錄下新建腳本 Projectile,聲明如下變量:
public float speed; public int damage;Vector3 shootDirection;和教程里面其它會動的對象一樣,這里也用到了速度和傷害變量,因為這也是戰斗邏輯的一部分。shootDiretion 向量決定了子彈射向的方向。
要使用這個向量,需要定義如下方法:
// 1 void FixedUpdate () {this.transform.Translate(shootDirection * speed, Space.World); }// 2 public void FireProjectile(Ray shootRay) {this.shootDirection = shootRay.direction;this.transform.position = shootRay.origin; }// 3 void OnCollisionEnter (Collision col) {Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();if(enemy) {enemy.TakeDamage(damage);}Destroy(this.gameObject); }上述代碼解釋如下:
在場景結構視圖中,將 Projectile 腳本綁定到 Projectile GameObject。將 Speed 設為 0.2,Damage 設為 1,然后點擊檢視器頂部附近的 Apply 按鈕。這將修改應用到所有的預制件實例。
從場景結構視圖中刪除 Projectile 對象——你不再需要它了。
發射子彈
你已經有一個會飛會造成傷害的預制件了,接下來可以開始射擊了。
在 Player 文件夾下,新建腳本 PlayerShooting,并將它綁定到場景的 Player 中。在腳本中聲明變量如下:
public Projectile projectilePrefab; public LayerMask mask;第一個變量保存對之前創建的子彈預制件的引用。當玩家每次射擊時,你都要創建一個該預制件的實例。掩碼(mask 變量)用于過濾游戲對象。
等等,發射射線?這是什么魔法?
不,這里沒有什么黑魔法——有時候你需要知道在某個方向上是否有碰撞發生。為了解決這個問題,Unity 從某個點開始向指定方向發射一條看不見的射線。很可能和這條射線相交的游戲對象會有很多,掩碼(mask 變量)允許你過濾掉一些無關的對象。
Raycasts 超級好用,可以用于各種目的。通常會用它來判斷其它角色在否中彈,也可以用它來判斷在鼠標指針下面是否有某個幾何體。要了解更多 Raycast 的用法,請閱讀 Unity 官網上的 Unity 在線教學視頻。
下圖顯示了一條從立方體到達圓錐體的射線。因為這條射線有一個 iconsphere 的掩碼,它會忽略這個 GameObject 然后告訴你它擊中了圓錐體:
讓我們來發射自己的射線。
在 PlayerShooting.cs 中添加如下方法:
void shoot(RaycastHit hit){// 1var projectile = Instantiate(projectilePrefab).GetComponent<Projectile>();// 2var pointAboveFloor = hit.point + new Vector3(0, this.transform.position.y, 0);// 3var direction = pointAboveFloor - transform.position;// 4var shootRay = new Ray(this.transform.position, direction);Debug.DrawRay(shootRay.origin, shootRay.direction * 100.1f, Color.green, 2);// 5Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>());// 6projectile.FireProjectile(shootRay); }以上代碼解釋如下:
注意:在發射射線的時候調用 Debug.DrawRay() 非常有用,它會讓你看見射線,以及它擊中些什么。
射擊邏輯寫完,添加下列方法,讓玩家可以真的開槍:
// 1 void raycastOnMouseClick () {RaycastHit hit;Ray rayToFloor = Camera.main.ScreenPointToRay(Input.mousePosition);Debug.DrawRay(rayToFloor.origin, rayToFloor.direction * 100.1f, Color.red, 2);if(Physics.Raycast(rayToFloor, out hit, 100.0f, mask, QueryTriggerInteraction.Collide)) {shoot(hit);} }// 2 void Update () {bool mouseButtonDown = Input.GetMouseButtonDown(0);if(mouseButtonDown) {raycastOnMouseClick(); } }上述方法依序解釋如下:
回到 Unity,在檢視器中設置以下屬性:
- Projectile Prefab: 引用 Prefabs folder 文件夾中的 Projectile
- Mask: Floor
注意:Unity 有一個事先定義的圖層(layer)列表,這些圖層在創建掩碼時會用到。點擊 GameObject 的 Layer 下拉框,點擊 Add Layer 就可以新建圖層:
要為 GameObject 指定圖層,請在 Layer 下拉框中進行選擇:
關于圖層的更多內容,請參考 Unity 的 Layers 文檔。
運行項目,開火吧!子彈飛向所指的方向,但還是有點不對,不是嗎?
如果子彈的方向和飛行的方向保持一致就更好了。要解決這個問題,打開 Projectile.cs,添加如下方法:
void rotateInShootDirection() {Vector3 newRotation = Vector3.RotateTowards(transform.forward, shootDirection, 0.01f, 0.0f);transform.rotation = Quaternion.LookRotation(newRotation); }注意 RotateTowards 方法和 MoveTowards 方法很像,但它把向量當成是方向而不是位置。同時,你也不需要隨時都去修改方向,因此只需要一個接近于 0 的 step 就夠了。在 Unity 中,Transform 的 rotation 是用四元數表示的,這不屬于本書范疇。在本教程中,你只需要知道它在進行和 3D 旋轉相關的計算中比起向量來說更有優勢即可。
如果對四元數和及其優點感興趣,可以閱讀這篇:How I learned to Stop Worrying and Love Quaternions。
在 FireProjectile() 方法最后,調用 rotateInShootDirection()。 FireProjectile() 最終變成這個樣子:
public void FireProjectile(Ray shootRay) {this.shootDirection = shootRay.direction;this.transform.position = shootRay.origin;rotateInShootDirection(); }再次運行游戲,向各個方向開火,這次子彈會直直指向射擊的方向:
在不需要的時候可以刪除 Debug.DrawRay 語句。
制造更多的壞蛋
只有一個敵人一點挑戰性都沒有嘛!但現在,自從你學過了預制件之后,你想要多少就可以制造出多少!:]
為了讓玩家不可預知,你可以讓每個 Enemy 的生命、速度和位置隨機。
創建一個空的游戲對象——用 GameObject\Create Empty。命名它為 EnemyProducer 并添加一個 Box Collider 組件給它。在檢視器中,修改它的屬性:
- Is Trigger: true
- Center: (0, 0.5, 0)
- Size: (29, 1, 29)
你綁定的這個碰撞體在競技場中定義了一個特殊的 3D 空間。要看見它,在結構視圖中選中 Enemy Producer 游戲對象,然后在場景視圖中你會看到:
綠色的外框代表了這個碰撞體。
接下來你將編寫腳本,用這個空間中的隨機位置 X 和 Z 生成一個 Enemy 預制件的實例。
新建腳本 EnemyProducer,并綁定到 EnemyProducer 游戲對象上。在類文件中,添加成員:
public bool shouldSpawn; public Enemy[] enemyPrefabs; public float[] moveSpeedRange; public int[] healthRange;private Bounds spawnArea; private GameObject player;第一個變量用于開啟/禁止自動孵化。這個腳本會從 enemyPrefabs 中挑選一個隨機的敵人預制件來實例化。后兩個數組用于指定速度和生命值的最大、最小值。spawnArea 是你在場景視圖中看見的那個綠色方框。最后,你需要引用玩家角色,將它作為壞蛋們的目標。
在這個腳本中,定義方法:
public void SpawnEnemies(bool shouldSpawn) {if(shouldSpawn) {player = GameObject.FindGameObjectWithTag("Player");}this.shouldSpawn = shouldSpawn; }void Start () {spawnArea = this.GetComponent<BoxCollider>().bounds;SpawnEnemies(shouldSpawn);InvokeRepeating("spawnEnemy", 0.5f, 1.0f); }SpawnEnemies() 方法獲取 Tag 標記為 Player 的游戲對象,并判斷是否應當孵化出一個敵人。
Start() 方法初始化孵化區并在游戲啟動后以 0.5 秒調用一個方法。這個方法每秒都會重復執行。除了充當 setter 方法,SpawnEnemies() 方法還會獲取一個 Tag 為 Player 的游戲對象的引用。
但是,Player 游戲對象還沒有設置 Tag 屬性——你現在來做這個。從結構視圖中選擇 Player 對象,然后在檢視器中,從 Tag 下拉菜單中選擇 Player:
現在,需要編寫孵化一個敵人的代碼了。
打開 Enemy 腳本,添加方法:
public void Initialize(Transform target, float moveSpeed, int health) {this.targetTransform = target;this.moveSpeed = moveSpeed;this.health = health; }這個方法可以看成是一個 setter 方法,用于創建對象。接下來,這個代碼會用于孵化出敵人。打開 EnemyProducer.cs,添加下列方法:
Vector3 randomSpawnPosition() {float x = Random.Range(spawnArea.min.x, spawnArea.max.x);float z = Random.Range(spawnArea.min.z, spawnArea.max.z);float y = 0.5f;return new Vector3(x, y, z); }void spawnEnemy() {if(shouldSpawn == false || player == null) {return;}int index = Random.Range(0, enemyPrefabs.Length);var newEnemy = Instantiate(enemyPrefabs[index], randomSpawnPosition(), Quaternion.identity) as Enemy;newEnemy.Initialize(player.transform, Random.Range(moveSpeedRange[0], moveSpeedRange[1]), Random.Range(healthRange[0], healthRange[1])); }spawnEnemy() 方法會選擇一個隨機的敵人預制件,用隨機的位置實例化預制件并初始化 Enemy 腳本中的公共變量。
EnemyProducer.cs 準備得差不多了!
回到 Unity。從結構視圖拖一個 Ememy 對象到 Prefabs 文件夾,這將創建一個 Enemy 預制件。從場景中刪除 enemy 對象——你用不到它了。然后將 Enemy Producer 腳本的公共變量修改為:
Enemy Prefabs:
- Size: 1
- Element 0: Reference the enemy prefab
Move Speed Range:
- Size: 2
- Element 0: 3
- Element 1: 8
Health Range:
- Size: 2
- Element 0: 2
- Element 1: 6
運行游戲查看效果——源源不斷的壞蛋們出來了!
好的,這些方塊沒一點也不嚇人。是時候來加點料了。
在場景中創建一個 3D Cylinder 和 Capsule。分別命名為 Enemy2 和 Enemy 3 。如同你在第一個敵人中所做的,添加剛體組件和 Enemy 腳本給它們。選中 Enemy2 在檢視器中修改它的屬性:
Rigidbody:
- Use Gravity: False
- Freeze Position: Y
- Freeze Rotation: X, Y, Z
Enemy Component:
- Move Speed: 5
- Health: 2
- Damage: 1
- Target Transform: None
然后在 Enemy3 上重復同樣步驟,但 Scale 設置為 0.7:
然后,將它們轉成預制件,這和之前在原來的 Enemy 上的做法是一樣的,然后在 Enemy Producer 中引用它們。在檢視器中查看是這個樣子:
Enemy Prefabs:
- Size: 3
- Element 0: Enemy
- Element 1: Enemy2
- Element 2: Enemy3
運行游戲,你會看到孵化出不同的預制件來了。
很快,你就會發現自己是無敵的!雖然這很爽,但你還是要讓戰斗均衡一點。
實現游戲控制器
你現在可以開槍和移動,敵人也出現了,接下來應該實現基本的游戲控制器。它會在玩家“死亡”之后重新開始游戲。但首先,你必須創建一種機制,通知所有感興趣的對象玩家的生命值已經為 0。
打開 Player 腳本,在類聲明之前添加:
using System;在類中,添加一個新的公有時間:
public event Action<Player> onPlayerDeath;事件是 C# 的語言特性,允許你將對象的變化廣播給其它監聽者。關于事件的用法,請閱讀 Unity 的 event 在線教學視頻。
將 collidedWithEnemy() 實現為如下代碼:
void collidedWithEnemy(Enemy enemy) {enemy.Attack(this);if(health <= 0) {if(onPlayerDeath != null) {onPlayerDeath(this);}} }事件提供了一種簡單的在對象之間實現信號狀態改變的方法。一個游戲控制器應該對上面聲明的事件很敏感。在 Scripts 文件夾中,新建腳本 GameController。雙擊文件打開它,添加如下變量:
public EnemyProducer enemyProducer; public GameObject playerPrefab;在這個腳本中我們需要控制敵人的生成,因為玩家死亡后還繼續制造敵人是沒有必要的。另外,重啟游戲意味著需要重新創建玩家…也就是玩家也需要轉成預制件。
添加下列方法:
void Start () {var player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();player.onPlayerDeath += onPlayerDeath; }void onPlayerDeath(Player player) {enemyProducer.SpawnEnemies(false);Destroy(player.gameObject);Invoke("restartGame", 3); }在 Start() 方法中,獲取了一個對 Player 腳本的引用,并且訂閱了之前新建的事件。當玩家生命值歸 0,onPlayerDeath() 方法會被調用,于是停止孵化敵人,將玩家從場景中移除,然后延遲 3 秒調用 restartGame() 方法。
最后來實現重啟游戲的代碼:
void restartGame() {var enemies = GameObject.FindGameObjectsWithTag("Enemy");foreach (var enemy in enemies){Destroy(enemy);}var playerObject = Instantiate(playerPrefab, new Vector3(0, 0.5f, 0), Quaternion.identity) as GameObject;var cameraRig = Camera.main.GetComponent<CameraRig>();cameraRig.target = playerObject;enemyProducer.SpawnEnemies(true);playerObject.GetComponent<Player>().onPlayerDeath += onPlayerDeath; }這里你需要進行一些清理工作:銷毀場景中的敵人,創建新的 Player 對象。將相機的 target 設置為新實例,恢復敵人的孵化,讓 Game Controller 訂閱玩家死亡事件。
回到 Unity,打開 Prefabs 文件夾,將所有 Enemy 預制件的 tag 設為 Enemy。然后,將 Player 游戲對象拖進 Prefabs 文件夾。新建空游戲對象,命名為 GameController,將剛剛創建的腳本綁定上去。在檢視器中,將所有的引用都連接上。
這個工作你已經很熟悉了。請自行完成引用的連接并檢查是否和下面的答案一致:
Game Controller:
- Enemy Producer: 從結構視圖中引用 Enemy Producer
- Player Prefab: 從 Prefabs 文件夾引用 Player Prefab
運行游戲,看游戲控制器是否運行正常。
好了,你用腳本完成了你的第一個 Unity 游戲!恭喜你 :]
結束
完成后的項目從這里下載。
現在,你應該掌握了如何構建一個簡單的動作游戲。游戲制作不是簡單工作;要完成整個游戲,大量的工作和腳本絕對只是其中一部分。為了添加更多的亮點,你必須在游戲中添加動畫和 UI。因此,我強烈建議你閱讀我們的這些教程:
- Unity UI 入門
- Unity 動畫入門
如果你喜歡學習設計、編碼和美化你的 Unity 游戲,請閱讀 Unity 游戲教程。
這本書會教你構建 Unity 游戲所需要的一切只是,無論你是初學者還是有經驗的開發者。在這本書中,你將編寫出 4 個優秀的游戲:
- 一個 3D twin-stick 射擊游戲
- 一個經典的 2D 平臺游戲
- 一個 3D 塔防游戲(支持 VR)
- 一個第一人稱射擊游戲
希望你喜歡本教程,并激發你開始制作你一直想制作的游戲。有問題和建議,請在下面留言。
總結
以上是生活随笔為你收集整理的Unity 脚本入门教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AStar算法通用实现+可视化(Matl
- 下一篇: java itext 页边距_iText