Unity俯視角射擊游戲腳本實戰(zhàn)
譯文簡介
Unity的強(qiáng)大功能主要得益于其豐富的腳本語言。你可以使用腳本來處理用戶輸入、移動場景中的物體、檢測碰撞、使用預(yù)制對象以及沿場景四周投射定向光線來增強(qiáng)你的游戲邏輯等。這聽起來有點令人生畏,但由于Unity官方提供了良好的API文檔支持,所以完成上述任務(wù)變得輕而易舉——即使對于Unity開發(fā)新手亦然!
在本教程中,你將創(chuàng)建一個基于俯視角的Unity射擊游戲。游戲中,你將使用Unity #腳本來生成敵人、控制玩家、發(fā)射炮彈以及實現(xiàn)游戲其他重要方面的控制。
【提示】本文假設(shè)你有一個C#或類似的編程語言開發(fā)經(jīng)驗。另外,本文示例游戲使用Unity 5.3+開發(fā)而成。
準(zhǔn)備
首先,請下載本文示例啟動項目(http://www.raywenderlich.com/wp-content/uploads/2016/03/BlockBuster.zip)并解壓縮。為了在Unity中打開啟動器項目,你可以從【Start Up Wizard】下單擊【Open】命令,然后導(dǎo)航到項目文件夾。或者,您可以直接從路徑【BlockBuster/Assets/Scenes】下打開文件Main.unity。
下圖給出您的示例工程中場景的初始樣子。
首先,請觀察一下場景視圖周圍的情況。有一個小的場地,這將作為本示例游戲的戰(zhàn)場;還有一部相機(jī),當(dāng)玩家在戰(zhàn)場上走動時相機(jī)會跟隨他們。如果您的布局與截圖有所不同,你可以選擇右上角的下拉菜單,把其中的選項改為「2 by 3」。
沒有英雄存在,那算是什么游戲呢?因此,你的第一個任務(wù)是創(chuàng)建一個游戲?qū)ο螅员硎緫?zhàn)場中的英雄。
創(chuàng)建玩家對象
在【Hierarchy】中,點擊【Create】按鈕,然后從「3D」部分選擇「Sphere」。將球體拖動到坐標(biāo)位置(0,0.5,0),并將其命名為Player,如圖所示。
從現(xiàn)在起,你將引用這一個球體作為玩家(Player)對象。
Unity使用組件系統(tǒng)來構(gòu)建它的游戲?qū)ο螅贿@意味著,在一個場景中的所有對象都可以通過組件的任何組合來創(chuàng)建。這些組合包括:用來描述一個對象位置的變換(Transform);網(wǎng)格過濾器(Mesh Filter),其中包含圖形幾何體或者任何個數(shù)的腳本(Scripts)。
玩家(Player)對象需要響應(yīng)與場景中的其他對象的碰撞。
要做到這一點,請從【Hierarchy】中選擇「Player」。然后,從【Inspector】選項卡下點擊【Add Component】按鈕。在【Physics】類別中,選擇【Rigidbody】組件。這將使Player對象為Unity的物理引擎所控制。
現(xiàn)在,請更改Rigidody的值,如下所示:
1.Drag:1
2.Angular Drag:0
3.Constraints: Freeze Position:Y
編寫玩家運(yùn)動腳本
現(xiàn)在你有了一個Player對象。接下來,我們來編寫腳本以便接收鍵盤輸入,進(jìn)而移動玩家。
在項目瀏覽器(Project Browser)中點擊【Create】按鈕,然后選擇「Folder」。命名新文件夾為「Scripts」并在名為「Player」的文件夾下創(chuàng)建一個子文件夾。接下來,在「Player」文件夾下,點擊【Create】按鈕,并選擇【C# Script】。命名新的腳本為PlayerMovement。順序大致如下圖所示:
【提示】Player對象將包含多個腳本,各自負(fù)責(zé)其行為的不同部分。在一個單獨的文件夾下保存所有相關(guān)的腳本,使項目中文件更容易管理,并減少混亂。
現(xiàn)在,請雙擊PlayerMovement.cs腳本。在Mac上,這將打開隨同Unity一起打包的MonoDevelop開發(fā)環(huán)境;在Windows上,它應(yīng)該打開Visual Studio。本教程假設(shè)你使用MonoDevelop。
在類中聲明下面兩個公共變量:
public float acceleration;
public float maxSpeed;
其中,acceleration用于描述玩家的速度如何隨時間增加,而maxSpeed代表“速度極限”。制作一個public類型的變量將會使之顯示于【Inspector】之中,這樣你就可以通過Unity界面來設(shè)置它的值,并根據(jù)需要調(diào)整它。
緊接著上面的聲明,再聲明以下變量:
private Rigidbody rigidBody;
private KeyCode[] inputKeys;
private Vector3[] directionsForKeys;
注意,私有變量無法通過【Inspector】進(jìn)行設(shè)置。因此,需要由程序員在適當(dāng)?shù)臅r候以完全手動方式對它們進(jìn)行初始化。
接下來,把Start()函數(shù)修改成如下所示的代碼:
- 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>();
- }
上述代碼中的inputKeys數(shù)組包含了您將用于移動玩家的鍵碼。directionsForKeys包含相應(yīng)于每個鍵的方向;例如,按下W用于向前移動對象。至于最后一行代碼,你還記得前面添加的剛體嗎?這是可以得到對該組件的引用的一種方式。
要移動玩家,你就必須處理來自于鍵盤的輸入。
現(xiàn)在,請重命名函數(shù)Update()為FixedUpdate(),并給它添加以下代碼:
- // 1
- void FixedUpdate () {
- for (int i = 0; i < inputKeys.Length; i++){
- var key = inputKeys[i];
- // 2
- if(Input.GetKey(key)) {
- // 3
- Vector3 movement = directionsForKeys[i] * acceleration * Time.deltaTime;
- }
- }
- }
這里發(fā)生了幾件重要的事情:
1.FixedUpdate()函數(shù)是幀速率獨立的,在操作剛體時應(yīng)該調(diào)用此函數(shù)。
2.這個循環(huán)檢查是否有任何輸入鍵被按下。
3.在這里,你得到按鍵的方向,并把它乘以加速度和完成最后一幀的修復(fù)所耗費(fèi)的秒數(shù)。這將產(chǎn)生您創(chuàng)建一個矢量,正是使用它來移動Player對象。
注意,當(dāng)您創(chuàng)建一個新的Unity腳本時,你實際上是創(chuàng)建一個新的MonoBehaviour對象。如果你熟悉iOS編程世界,那么你會知道它是一個UIViewController的等價物;也就是說,你可以使用這個對象來響應(yīng)Unity內(nèi)的事件,從而訪問你自己的數(shù)據(jù)對象。
MonoBehaviours有很多不同的方法,它們分別對各種事件作出響應(yīng)。舉例來說,當(dāng)MonoBehaviour實例化時如果你想初始化一些變量,那么你就可以實現(xiàn)方法Awake()。在MonoBehaviour被禁用時為了運(yùn)行代碼,你可以實現(xiàn)方法OnDisable()。
【提示】如果你想研究這些事件的完整列表,請訪問Unity官方文檔,地址是 http://docs.unity3d.com/ScriptReference/MonoBehaviour.html。
如果你是游戲編程新手,你可能會問自己,為什么必須乘以Time.deltaTime?一般的規(guī)律是,當(dāng)你每隔固定的時間幀數(shù)執(zhí)行一個動作時,你需要乘以Time.deltaTime。在本例情況下,你想要沿按鍵方向加速移動玩家,加速數(shù)值為固定的更新時間。
接下來,在方法FixedUpdate()下面添加以下方法:
- void movePlayer(Vector3 movement) {
- if(rigidBody.velocity.magnitude * acceleration > maxSpeed) {
- rigidBody.AddForce(movement * -1);
- } else {
- rigidBody.AddForce(movement);
- }
- }
上述方法用于對剛體施加力作用,使其移動。如果當(dāng)前速度超過maxSpeed,力會變成相反的方向......這有點像一個速度極限。
現(xiàn)在,請在方法FixedUpdate()中,在if語句的結(jié)束括號之前,添加以下行:
movePlayer(movement);
很好!回到Unity中。然后,在項目瀏覽器中,將PlayerMovement腳本拖動到【Hierarchy】中的Player對象上。然后,使用【Inspector】來把「Acceleration」的值設(shè)置為625并把最大速度(Max Speed)修改為4375:
現(xiàn)在,請運(yùn)行一下游戲場景,并試著使用鍵盤上的WASD鍵移動玩家對象,觀察效果:
到目前,我們僅僅實現(xiàn)了幾行代碼,這已經(jīng)算是一個相當(dāng)不錯的結(jié)果了!
然而,現(xiàn)在有一個明顯的問題:玩家可以移出人們的視線之外,這在打壞人時是個麻煩事。
編寫攝相機(jī)腳本
在「Scripts」文件夾中,創(chuàng)建一個名為CameraRig的新的腳本,并將其附加到主攝像機(jī)(Main Camera)上。
【提示】在選擇【Scripts】文件夾情況下,點擊工程瀏覽器中的【Create】按鈕,然后選擇【C# Script】。命名新的腳本為「CameraRig」。最后,把此腳本拖動到「Main Camera」對象上即可。
現(xiàn)在,在新創(chuàng)建的CameraRig類中添加下列變量:
public float moveSpeed;
public GameObject target;
private Transform rigTransform;
正如你可能已經(jīng)猜到的,moveSpeed代表了相機(jī)跟蹤目標(biāo)的速度——這可能是場景里面的任何游戲?qū)ο蟆?/p>
接下來,在Start()函數(shù)中添加以下代碼行:
rigTransform= this.transform.parent;
此代碼獲取場景層次樹中的到父Camera對象的引用。場景中的每個對象具有一個變換(Transform),其中描述了一個對象的位置旋轉(zhuǎn)和縮放等信息。
然后,在與上面同一個腳本文件中添加下面的方法:
- void FixedUpdate () {
- if(target == null){
- return;
- }
- rigTransform.position = Vector3.Lerp(rigTransform.position, target.transform.position,
- Time.deltaTime * moveSpeed);
- }
這部分CameraRig移動代碼要比在PlayerMovement中的簡單一些。這是因為你不需要一個剛體;只需要在rigTransform的位置和目標(biāo)之間進(jìn)行插值就足夠了。
Vector3.Lerp()函數(shù)使用了空間中的兩個點,還有一個界于[0,1]范圍內(nèi)的浮點數(shù)(它描述了沿兩個端點的中間的某一點)作參數(shù)。左端點為0,右側(cè)端點是1。于是,把0.5傳遞給Lerp()函數(shù)將正好返回位于兩個端點中間的一個點。
這會將rigTransform移到距目標(biāo)位置更近一些,而且略有緩動效果。總之,相機(jī)跟隨玩家運(yùn)動。
現(xiàn)在,返回到Unity。確保層次樹(Hierarchy)中的主攝像機(jī)(Main Camera)仍處于選中狀態(tài)。在【Inspector】中,把Move Speed(移動速度)設(shè)置為8,并把Target(目標(biāo))設(shè)置為Player:
再次運(yùn)行游戲工程,沿場景四處移動;你會注意到,無論玩家走到哪里,相機(jī)都能夠平滑地跟隨目標(biāo)變換。
創(chuàng)建敵人對象
一款沒有敵人的射擊游戲很容易被擊敗,當(dāng)然也很無聊。所以,現(xiàn)在我們來通過單擊頂部菜單中的【GameObject\3D Object\Cube】創(chuàng)建一個用于表示敵人的立方體對象。然后,把此立方體重命名為「Enemy」,并添加一個Rigidbody(剛體)組件。
在【Inspector】中,首先設(shè)置立方體的變換為(0,0.5,4)。并在剛體組件的「Constraints」部分的「Freeze Position」類別下勾選「Y」選擇對應(yīng)的復(fù)選框。
很好,現(xiàn)在使你的敵人氣勢洶洶地走動吧。然后,在【Scripts】文件夾下創(chuàng)建一個命名為「Enemy」的腳本。現(xiàn)在,你應(yīng)該對這種操作很熟練了;恕不再贅述。
接下來,在類內(nèi)部添加下列公共變量:
public float moveSpeed;
public int health;
public int damage;
public Transform targetTransform;
你也許可以很容易地確定出這些變量所代表的含義。你可以使用如前面一樣的moveSpeed變量技巧來操縱攝像機(jī),而且它們的效果相同。Health和damage這兩個變量分別用于確定何時敵人死了以及他們死多少會傷害玩家。最后,變量targetTransform用于引用玩家對象對應(yīng)的變換。
說到玩家對象,你需要創(chuàng)建一個類來描述敵人想破壞的所有玩家的健康值。
在項目瀏覽器中,選擇「Player」文件夾,并創(chuàng)建一個名為「Player」的新腳本。這個腳本會響應(yīng)于碰撞,并跟蹤玩家的健康值。現(xiàn)在,我們通過雙擊此腳本來編輯它。
添加下列公共變量來保存玩家的健康值:
public int health = 3;
這樣便提供了玩家健康值的默認(rèn)值,但它也可以通過【Inspector】進(jìn)行修改。
為了處理沖突,添加以下方法:
- void collidedWithEnemy(Enemy enemy) {
- // Enemy attack code
- if(health <= 0) {
- // Todo
- }
- }
- void OnCollisionEnter (Collision col) {
- Enemy enemy = col.collider.gameObject.GetComponent<Enemy>();
- collidedWithEnemy(enemy);
- }
當(dāng)兩個剛體發(fā)生碰撞時,OnCollisionEnter()即被觸發(fā)。其中,Collision參數(shù)中包含了諸如接觸點和沖擊速度相關(guān)的信息。在本示例情況下,我們只對碰撞物體中的Enemy組件感興趣,所以可以調(diào)用collidedWithEnemy()并執(zhí)行攻擊邏輯——接下來就會實現(xiàn)這種邏輯。
切換回文件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);
- }
你已經(jīng)熟悉了FixedUpdate()函數(shù),略有不同的是現(xiàn)在使用的是MoveTowards()而不是Lerp()函數(shù)。這是因為敵人應(yīng)該一直以相同的速度移動而不會在接近目標(biāo)時出現(xiàn)快速移動。當(dāng)敵人被彈丸擊中時,TakeDamage()即被調(diào)用;當(dāng)敵人到達(dá)值為0的健康值時他會自我毀滅。Attack()函數(shù)的實現(xiàn)邏輯是與之很類似的——對玩家進(jìn)行傷害,然后敵人破壞自身。
切換回Player.cs。然后,在函數(shù)collidedWithEnemy()中,使用下面代碼替換注釋// Enemy attack code:
enemy.Attack(this);
游戲中,玩家將受到傷害,而敵人在該過程中將自我毀滅。
切換回Unity。把Enemy腳本附加到 Enemy對象上;并在【Inspector】中,針對Enemy對象設(shè)置以下值:
1.Move Speed:5
2.Health:2
3.Damage:1
4.Target Transform:Player
現(xiàn)在,你應(yīng)該能夠自己做這一切了。結(jié)束后,你可以與文后完整的工程源碼進(jìn)行比較。
在游戲中,敵人與玩家碰撞,從而實現(xiàn)一種有效的敵對攻擊。使用Unity的物理碰撞檢測幾乎是一個很簡單的任務(wù)。
最后,在層次結(jié)構(gòu)中把Player腳本附加到Player對象。
運(yùn)行游戲工程,并留意在控制臺上輸出的結(jié)果:
當(dāng)敵人接觸到玩家時,它能夠成功地進(jìn)行攻擊,并把玩家的健康值變量降低到2。但是,現(xiàn)在在控制臺中拋出一個NullReferenceException異常,錯誤指向Player腳本:
哈哈,現(xiàn)在玩家不僅可以與敵人碰撞,也可能與游戲世界中的其他部分,如戰(zhàn)場,發(fā)生碰撞!這些游戲?qū)ο蟛]有Enemy腳本,因此GetComponent()函數(shù)將返回null。
接下來,打開文件Player.cs。然后,在OnCollisionEnter()函數(shù)中,把collidedWithEnemy()函數(shù)調(diào)用使用一個if語句包括起來,如下所示:
- if(enemy) {
- collidedWithEnemy(enemy);
- }
此時,異常消失!
使用預(yù)制
只是簡單地在戰(zhàn)場上跑來跑去,而且避開敵人;這只能算是一個一邊倒的游戲。現(xiàn)在,我們來武裝一下玩家,使之能夠作戰(zhàn)。
單擊層次結(jié)構(gòu)中的【Create】按鈕,并選擇【3D Object/Capsule】。命名它為Projectile,并給它指定下列變換值:
1. Position:(0, 0, 0)
2. Rotation:(90, 0, 0)
3. Scale:(0.075, 0.246, 0.075)
每當(dāng)玩家射擊時,他就會發(fā)射Projectile(炮彈)的一個實例。要做到這一點,你需要創(chuàng)建一個預(yù)制(Prefab)。不像場景中你已經(jīng)擁有的其他對象,預(yù)制對象是根據(jù)游戲邏輯需要而創(chuàng)建的。
現(xiàn)在,在文件夾「Assets」下創(chuàng)建一個新的文件夾,名為Prefabs。現(xiàn)在,把Projectile對象拖動到這個文件夾上。就是這樣:你創(chuàng)建了一個預(yù)制!
您的預(yù)制還需要一點腳本。現(xiàn)在,在【Scripts】文件夾內(nèi)創(chuàng)建一個名為「Projectile」的新腳本,并添加下面的類變量:
public float speed;
public int damage;
Vector3 shootDirection;
就像目前為止在本教程中任何可移動的物體一樣,這個對象也會有速度和傷害對應(yīng)的變量,因為它是戰(zhàn)斗邏輯的一部分。其中,shootDirection矢量決定了炮彈將向哪兒發(fā)射。
在類中實現(xiàn)下面的方法即可使這個矢量發(fā)揮作用:
- // 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);
- }
在上面的代碼中發(fā)生了下面的事情:
1.炮彈在游戲中的運(yùn)動方式與其他對象不同。它不具有一個目標(biāo),或者一直對它施加一些力;相反,它在其整個生命周期中的按照預(yù)定方向進(jìn)行運(yùn)動。
2.在這里,我們設(shè)置了預(yù)制對象的起始位置和方向。Ray參數(shù)看上去似乎很神秘吧,但你很快就會知道它是如何計算出來的。
3.如果一個炮彈與敵人發(fā)生碰撞,它會調(diào)用TakeDamage(),并進(jìn)行自我毀滅。
在場景層次中,把Projectile腳本附加到Projectile游戲?qū)ο笊稀TO(shè)置它的速度為0.2,并把損壞值設(shè)置為1,然后點擊【Inspector】頂部的【Apply】按鈕。這將針對這個預(yù)制的所有實例保存剛才所做的更改。
現(xiàn)在,請從場景層次樹中刪除Projectile對象,因為我們不再需要它了。
發(fā)射炮彈
現(xiàn)在,你既然已經(jīng)擁有了可以移動并施加傷害能力的預(yù)制對象,那么,接下來你就可以開始考慮實現(xiàn)發(fā)射炮彈相關(guān)的編程了。
在Player文件夾下,創(chuàng)建一個名為PlayerShooting的新腳本,并將其附加到場景中的Player游戲?qū)ο蟆H缓螅赑layer類中,聲明以下變量:
public Projectile projectilePrefab;
public LayerMask mask;
第一個變量將包含對前面創(chuàng)建的Projectile預(yù)制對象的引用。每當(dāng)玩家發(fā)射炮彈時,您將從這個預(yù)制創(chuàng)建一個新的實例。mask變量是用來篩選游戲?qū)ο螅℅ameObject)的。
現(xiàn)在,我們要介紹一下光線投射的問題。何謂光線投射(casting Ray)?這是什么魔法?
其實,并不存在什么黑魔法。但是,有時候在你的游戲中,你的確需要知道是否在一個特定方向上存在碰撞。要做到這一點,Unity在您指定的方向上能夠從某一個點投出一條看不見的射線。你可能會遇到很多與射線相交的游戲?qū)ο螅灰虼耍褂煤Y選器可以過濾掉任何不需要參與碰撞的對象。
光線投射是非常有用的,并且可以用于各種用途。它們常用于測試是否另一名玩家已經(jīng)被炮彈擊中;而且,你也可以使用它們來測試是否在鼠標(biāo)指針下方存在任何的幾何形狀。要更多地了解關(guān)于光線投射的內(nèi)容,請參考一下Unity官方網(wǎng)站提供的在線培訓(xùn)視頻(https://unity3d.com/learn/tutorials/modules/beginner/physics/raycasting)。
下圖顯示了從一個立方體到一個錐體的光線投射情況。由于射線上有一個圖標(biāo)掩碼,因此它忽略掉游戲?qū)ο蠖到y(tǒng)給出的提示是擊中了錐體:
接下來,我們需要創(chuàng)建自己的射線了。
把如下代碼添加到文件PlayerShooting.cs:
- void shoot(RaycastHit hit){
- // 1
- var projectile = Instantiate(projectilePrefab).GetComponent<Projectile>();
- // 2
- var pointAboveFloor = hit.point + new Vector3(0, this.transform.position.y, 0);
- // 3
- var direction = pointAboveFloor - transform.position;
- // 4
- var shootRay = new Ray(this.transform.position, direction);
- Debug.DrawRay(shootRay.origin, shootRay.direction * 100.1f, Color.green, 2);
- // 5
- Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>());
- // 6
- projectile.FireProjectile(shootRay);
- }
概括來看,上面的代碼主要實現(xiàn)如下功能:
1. 實例化一個炮彈預(yù)制并獲得它的Projectile組件,從而可以把它初始化。
2. 這個坐標(biāo)點總是使用像(X,0.5,Z)這樣的格式。其中,X和Z坐標(biāo)位于地面上,正好對應(yīng)于射線投射擊中的鼠標(biāo)點擊位置的坐標(biāo)。這里的計算是很重要的,因為炮彈必須平行于地面;否則,你會向下射擊,而只有外行的玩家才會出現(xiàn)向地面射擊的情況。
3. 計算從游戲物體Player指向pointAboveFloor的方向。
4. 創(chuàng)建一條新的射線,并通過其原點和方向來共同描述炮彈軌跡。
5. 這行代碼告訴Unity的物理引擎忽略玩家與炮彈之間的碰撞。否則,在炮彈飛出去前將調(diào)用Projectile腳本中的OnCollisionEnter()方法。
6. 最后,設(shè)置炮彈的運(yùn)動軌跡。
【注意】當(dāng)光線投射不可見時,你可以使用Debug.DrawRay()方法來輔助調(diào)試程序,因為它可以幫助您更直觀地觀察光線的外觀和它所擊中的對象。
好,現(xiàn)在既然發(fā)射炮彈的邏輯已經(jīng)實現(xiàn),請繼續(xù)添加下面的方法來讓玩家真正扣動扳機(jī):
- // 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();
- }
- }
讓我們按上面編號進(jìn)行逐個解釋:
1.這個方法把射線從攝相機(jī)射向鼠標(biāo)點擊的位置,然后檢查是否射線相交于符合給定LayerMask掩碼值的游戲?qū)ο蟆?/strong>
2.在每次更新中,腳本都會檢查一下鼠標(biāo)左鍵按下情況。如果發(fā)現(xiàn)存在按下的情況,就調(diào)用raycastOnMouseClick()方法。
現(xiàn)在,請返回到Unity中,并在【Inspector】中設(shè)置下列變量:
Projectile Prefab:引用文件夾prefab下的Projectile;
Mask:Floor
【注意】Unity使用數(shù)量有限的預(yù)定義掩碼——也稱為層。
你可以通過點擊一個游戲物體的【Layer】下拉菜單然后選擇【Add Layer】(添加圖層)來定義你自己的掩碼:
您也可以通過從【Layer】下拉菜單中選擇一個層來給游戲?qū)ο蠓峙溲诖a:
有關(guān)Unity3d引擎中層的更多的信息,請參考官方文檔,地址是http://docs.unity3d.com/Manual/Layers.html。
現(xiàn)在,請運(yùn)行示例項目并隨意發(fā)射炮彈!你會注意到:炮彈按照希望的方向發(fā)射,但看起來還缺少點什么,不是嗎?
如果炮彈是沿著其發(fā)射的方向行進(jìn)的,那將酷多了。為了解決這個問題,打開Projectile.cs腳本并添加下面的方法:
- void rotateInShootDirection() {
- Vector3 newRotation = Vector3.RotateTowards(transform.forward, shootDirection, 0.01f, 0.0f);
- transform.rotation = Quaternion.LookRotation(newRotation);
【注意】RotateTowards非常類似于MoveTowards,但它把矢量作為方向,而不是位置。此外,你并不需要一直改變旋轉(zhuǎn);因此,使用一個接近零的步長值就足夠了。在Unity中實現(xiàn)旋轉(zhuǎn)變換是使用四元組實現(xiàn)的,這已超出了本教程的討論范圍。在本教程中,你只需要知道在涉及三維旋轉(zhuǎn)計算時使用四元組的優(yōu)勢超過矢量即可。當(dāng)然,如果你有興趣更多地了解關(guān)于四元組以及它們有何用處,請參考這篇優(yōu)秀的文章,地址是http://developerblog.myo.com/quaternions/。
接下來,在FireProjectile()方法的結(jié)束處,添加對rotateInShootDirection()方法的調(diào)用。 現(xiàn)在,F(xiàn)ireProjectile()方法看起來應(yīng)該像下面這樣:
- public void FireProjectile(Ray shootRay) {
- this.shootDirection = shootRay.direction;
- this.transform.position = shootRay.origin;
- rotateInShootDirection();
- }
再次運(yùn)行游戲,并沿幾個不同的方向發(fā)射炮彈。此時,炮彈將指向它們發(fā)射的方向。現(xiàn)在,你可以清除代碼中的Debug.DrawRay調(diào)用了,因為你不再需要它們了。
生成更多敵人對象
只有一個敵人的游戲并不具有挑戰(zhàn)性。但現(xiàn)在,你已經(jīng)知道了預(yù)制的用法。于是,你可以生成任意數(shù)目的對手了!
為了讓玩家不斷猜想,你可以隨機(jī)地控制每個敵人的健康值、速度和位置等。
現(xiàn)在,使用命令【GameObject】-【Create Empty】創(chuàng)建一個空的游戲?qū)ο蟆C鼮椤窫nemyProducer」,并添加一個Box碰撞器組件。最后,在【Inspector】設(shè)置其值如下:
1. Position:(0, 0, 0)
2. Box Collider:
3. Is Trigger:true
4. Center:(0, 0.5, 0)
5. Size:(29, 1, 29)
上面你附加的這個碰撞器實際上在戰(zhàn)場中定義了一個特定的3D空間。為了看到這個對象,請從層次結(jié)構(gòu)樹下選擇【Enemy Producer】游戲物體;于是,在場景視圖中你會看到這個對象,如下圖所示。
圖中用綠線框出的部分代表了一個碰撞器
現(xiàn)在,你要編寫一個腳本實現(xiàn)沿X軸和Z軸方向選取空間中的一個隨機(jī)位置并實例化一個敵人預(yù)制。
創(chuàng)建一個名為EnemyProducer的新腳本,并將其附加到游戲?qū)ο驟nemyProducer。然后,在新設(shè)置的類內(nèi)部,添加以下實例成員:
public bool shouldSpawn;
public Enemy[] enemyPrefabs;
public float[] moveSpeedRange;
public int[] healthRange;
private Bounds spawnArea;
private GameObject player;
第一個變量控制啟用還是禁用敵人對象的生成。該腳本將從enemyPrefabs中選擇一個隨機(jī)的敵人預(yù)制并創(chuàng)建其實例。接下來的兩個數(shù)組將分別指定速度和健康值的最小值和最大值。生成敵人的地方是你在場景視圖中看到的綠色框。最后,你需要一個到玩家Player的引用,并把它作為目標(biāo)參數(shù)傳遞給敵人對象。
在腳本中,接著定義以下方法:
- 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()方法獲取到標(biāo)簽為Player的游戲?qū)ο蟮囊茫⒋_定是否應(yīng)該生成一個敵人。
Start()方法初始化敵人生成的位置并在游戲開始0.5秒之后調(diào)用一個方法。每一秒它都會被反復(fù)調(diào)用。除了作為一個setter方法外,SpawnEnemies()方法還得到一個到標(biāo)簽為「Player」的游戲?qū)ο蟮囊谩?/p>
注意,到現(xiàn)在為止,玩家游戲?qū)ο笊形礃?biāo)記。現(xiàn)在,就要做這件事情。請從【Hierarchy】中選擇Player對象,然后在【Inspector】選項卡中從「Tag」下拉菜單中選擇Player,如下圖所示。
現(xiàn)在,你需要編寫實際的生成單個敵人的代碼。
打開Enemy腳本,并添加下面的方法:
- public void Initialize(Transform target, float moveSpeed, int health) {
- this.targetTransform = target;
- this.moveSpeed = moveSpeed;
- this.health = health;
- }
這個方法充當(dāng)用于創(chuàng)建對象的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()方法所做的就是選擇一個隨機(jī)的敵人預(yù)制,在隨機(jī)位置實例化并初始化腳本Enemy中的公共變量。
現(xiàn)在,腳本EnemyProducer.cs快要準(zhǔn)備好了!
返回到Unity中。通過把Enemy對象從【Hierarchy】拖動到【Prefabs】文件夾創(chuàng)建一個Enemy預(yù)制。然后,從場景中移除Enemy對象——你不需要它了。接下來,設(shè)置Enemy Producer腳本中的公共變量:
1. Should Spawn:True
2. Enemy Prefabs:
Size:1
Element 0:引用敵人預(yù)制
3. Move Speed Range:
Size:2
Element 0:3
Element 1:8
4. Health Range:
Size:2
Element 0:2
Element 1:6
現(xiàn)在,運(yùn)行游戲并注意觀察。你會注意到場景中無休止地出現(xiàn)成群的敵人!
好吧,這些立方體看起來還不算非常可怕。現(xiàn)在,我們再來添加一些細(xì)節(jié)修飾。
在場景中創(chuàng)建一個三維圓柱(Cylinder)和一個膠囊(Capsule)。分別命名為「Enemy2」和「Enemy3」。就像前面你針對第一個敵人所做的那樣,向這兩個對象分別都添加一個剛體組件和一個Enemy腳本。然后,選擇Enemy2,并在【Inspector】中像下面這樣更改它的配置:
1. Scale:(0, 0.5, 0)
2. Rigidbody:
Use Gravity:False
Freeze Position:Y
Freeze Rotation:X, Y, Z
3. Enemy Component:
Move Speed: 5
Health: 2
Damage: 1
Target Transform: None
現(xiàn)在,針對Enemy3也進(jìn)行與上面同樣的設(shè)置,但是把它的Scale設(shè)置成0.7,如下圖所示。
接下來,把他們轉(zhuǎn)換成預(yù)制,就像你操作最開始的那個敵人那樣,并在「Enemy Producer」中引用它們。在【Inspector】中的值應(yīng)該像下面這樣:
Enemy Prefabs:
Size: 3
Element 0: Enemy
Element 1: Enemy2
Element 2: Enemy3
再次運(yùn)行游戲;現(xiàn)在,你會觀察到在場景中生成不同的預(yù)制。
其實,在你意識到你是不可戰(zhàn)勝的之前,不會花費(fèi)太長的時間!
開發(fā)游戲控制器
現(xiàn)在,您已經(jīng)能夠射擊、移動,而且能夠把敵人放在指定位置。在本節(jié)中,你將實現(xiàn)一個基本的游戲控制器。一旦玩家“死”了,它將重新啟動游戲。但首先,你必須建立一種機(jī)制以通知所有有關(guān)各方——玩家已達(dá)到0健康值。
現(xiàn)在,打開Player腳本,并在類聲明上方添加如下內(nèi)容:
using System;
然后,在類中添加以下新的公共事件:
public event Action<Player> onPlayerDeath;
【提示】事件是C#語言中的重要功能之一,讓你向所有監(jiān)聽者廣播對象中的變化。要了解如何使用事件,你可以參考一下官方的事件培訓(xùn)視頻(https://unity3d.com/learn/tutorials/topics/scripting/events)。
接下來,編輯collidedWithEnemy()方法,使之最終看起來具有像下面這樣的代碼:
- void collidedWithEnemy(Enemy enemy) {
- enemy.Attack(this);
- if(health <= 0) {
- if(onPlayerDeath != null) {
- onPlayerDeath(this);
- }
- }
事件為對象之間的狀態(tài)變化通知提供了一種整潔的實現(xiàn)方案。游戲控制器對上述聲明的事件是很感興趣的。在Scripts文件夾中,創(chuàng)建一個名為GameController的新腳本。然后,雙擊該文件進(jìn)行編輯,并給它添加下列變量:
public EnemyProducer enemyProducer;
public GameObject playerPrefab;
腳本在生成敵人時需要進(jìn)行一定的控制,因為一旦玩家喪生再生成敵人是沒有任何意義的。此外,重新啟動游戲意味著你將不得不重新創(chuàng)建玩家,這意味著……是的,你要通過把玩家變成預(yù)制來更靈活地實現(xiàn)這一目的。
于是,請?zhí)砑酉铝蟹椒ǎ?/p>
- 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腳本的引用,并訂閱你先前創(chuàng)建的事件。一旦玩家的健康值達(dá)到0, onPlayerDeath()方法即被調(diào)用,從而停止敵人的生成,從場景中移除Player對象和并在3秒鐘后調(diào)用restartGame()方法。
最后,重新啟動游戲的動作實現(xiàn)如下:
- 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;
在這里,我們做了一些清理工作:摧毀場景中的所有敵人,并創(chuàng)建一個新的Player對象。然后,重新指定攝像機(jī)的目標(biāo)為玩家對象,恢復(fù)敵人生成支持,并為游戲控制器訂閱玩家死亡的事件。
現(xiàn)在返回到Unity,打開Prefebs文件夾,更改所有敵人預(yù)制為標(biāo)簽Enemy。接下來,通過拖動Player游戲?qū)ο蟮絇refebs文件夾使玩家變成預(yù)制。再創(chuàng)建一個空的游戲?qū)ο螅瑢⑵涿麨镚ameController,并將您剛剛創(chuàng)建的腳本附加到其上。綁定【Inspector】中所有對應(yīng)的需要的引用。
現(xiàn)在,你應(yīng)該很熟悉這種模式了。建議你試著自己實現(xiàn)引用,再次運(yùn)行游戲。請觀察游戲控制器是如何實現(xiàn)游戲控制的。
故事至此結(jié)束;你已經(jīng)成功地使用腳本實現(xiàn)了你的第一個Unity游戲!祝賀你!
小結(jié)
本文示例工程完整的下載地址是http://www.raywenderlich.com/wp-content/uploads/2016/03/BlockBusterFinal.zip。
現(xiàn)在,你應(yīng)該對編寫一個簡單的動作游戲所需要的內(nèi)容有了一個很好的理解。實際上,制作游戲決不是一個簡單的任務(wù);它肯定需要大量的工作,而腳本只是把一個項目實現(xiàn)為一款真正的游戲所必需的要素之一。為了進(jìn)一步添加游戲修飾效果,還需要將動畫和漂亮的UI及粒子效果等添加到您的游戲中。當(dāng)然,要實現(xiàn)一款真正意義上的商業(yè)游戲,您還要克服更多的困難。