自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Unity俯視角射擊游戲腳本實(shí)戰(zhàn)

譯文
開(kāi)發(fā) 游戲開(kāi)發(fā)
Unity的強(qiáng)大功能主要得益于其豐富的腳本語(yǔ)言。你可以使用腳本來(lái)處理用戶(hù)輸入、移動(dòng)場(chǎng)景中的物體、檢測(cè)碰撞、使用預(yù)制對(duì)象以及沿場(chǎng)景四周投射定向光線(xiàn)來(lái)增強(qiáng)你的游戲邏輯等。

簡(jiǎn)介

Unity的強(qiáng)大功能主要得益于其豐富的腳本語(yǔ)言。你可以使用腳本來(lái)處理用戶(hù)輸入、移動(dòng)場(chǎng)景中的物體、檢測(cè)碰撞、使用預(yù)制對(duì)象以及沿場(chǎng)景四周投射定向光線(xiàn)來(lái)增強(qiáng)你的游戲邏輯等。這聽(tīng)起來(lái)有點(diǎn)令人生畏,但由于Unity官方提供了良好的API文檔支持,所以完成上述任務(wù)變得輕而易舉——即使對(duì)于Unity開(kāi)發(fā)新手亦然!

在本教程中,你將創(chuàng)建一個(gè)基于俯視角的Unity射擊游戲。游戲中,你將使用Unity #腳本來(lái)生成敵人、控制玩家、發(fā)射炮彈以及實(shí)現(xiàn)游戲其他重要方面的控制。

【提示】本文假設(shè)你有一個(gè)C?;蝾?lèi)似的編程語(yǔ)言開(kāi)發(fā)經(jīng)驗(yàn)。另外,本文示例游戲使用Unity 5.3+開(kāi)發(fā)而成。

準(zhǔn)備

首先,請(qǐng)下載本文示例啟動(dòng)項(xiàng)目(http://www.raywenderlich.com/wp-content/uploads/2016/03/BlockBuster.zip)并解壓縮。為了在Unity中打開(kāi)啟動(dòng)器項(xiàng)目,你可以從【Start Up Wizard】下單擊【Open】命令,然后導(dǎo)航到項(xiàng)目文件夾?;蛘?,您可以直接從路徑【BlockBuster/Assets/Scenes】下打開(kāi)文件Main.unity。

下圖給出您的示例工程中場(chǎng)景的初始樣子。

 

首先,請(qǐng)觀察一下場(chǎng)景視圖周?chē)那闆r。有一個(gè)小的場(chǎng)地,這將作為本示例游戲的戰(zhàn)場(chǎng);還有一部相機(jī),當(dāng)玩家在戰(zhàn)場(chǎng)上走動(dòng)時(shí)相機(jī)會(huì)跟隨他們。如果您的布局與截圖有所不同,你可以選擇右上角的下拉菜單,把其中的選項(xiàng)改為「2 by 3」。

 

沒(méi)有英雄存在,那算是什么游戲呢?因此,你的第一個(gè)任務(wù)是創(chuàng)建一個(gè)游戲?qū)ο?,以表示?zhàn)場(chǎng)中的英雄。

創(chuàng)建玩家對(duì)象

在【Hierarchy】中,點(diǎn)擊【Create】按鈕,然后從「3D」部分選擇「Sphere」。將球體拖動(dòng)到坐標(biāo)位置(0,0.5,0),并將其命名為Player,如圖所示。

從現(xiàn)在起,你將引用這一個(gè)球體作為玩家(Player)對(duì)象。

Unity使用組件系統(tǒng)來(lái)構(gòu)建它的游戲?qū)ο?;這意味著,在一個(gè)場(chǎng)景中的所有對(duì)象都可以通過(guò)組件的任何組合來(lái)創(chuàng)建。這些組合包括:用來(lái)描述一個(gè)對(duì)象位置的變換(Transform);網(wǎng)格過(guò)濾器(Mesh Filter),其中包含圖形幾何體或者任何個(gè)數(shù)的腳本(Scripts)。

玩家(Player)對(duì)象需要響應(yīng)與場(chǎng)景中的其他對(duì)象的碰撞。

要做到這一點(diǎn),請(qǐng)從【Hierarchy】中選擇「Player」。然后,從【Inspector】選項(xiàng)卡下點(diǎn)擊【Add Component】按鈕。在【Physics】類(lèi)別中,選擇【Rigidbody】組件。這將使Player對(duì)象為Unity的物理引擎所控制。

現(xiàn)在,請(qǐng)更改Rigidody的值,如下所示:

1.Drag:1

2.Angular Drag:0

3.Constraints: Freeze Position:Y

 

編寫(xiě)玩家運(yùn)動(dòng)腳本

現(xiàn)在你有了一個(gè)Player對(duì)象。接下來(lái),我們來(lái)編寫(xiě)腳本以便接收鍵盤(pán)輸入,進(jìn)而移動(dòng)玩家。

在項(xiàng)目瀏覽器(Project Browser)中點(diǎn)擊【Create】按鈕,然后選擇「Folder」。命名新文件夾為「Scripts」并在名為「Player」的文件夾下創(chuàng)建一個(gè)子文件夾。接下來(lái),在「Player」文件夾下,點(diǎn)擊【Create】按鈕,并選擇【C# Script】。命名新的腳本為PlayerMovement。順序大致如下圖所示:

 

【提示】Player對(duì)象將包含多個(gè)腳本,各自負(fù)責(zé)其行為的不同部分。在一個(gè)單獨(dú)的文件夾下保存所有相關(guān)的腳本,使項(xiàng)目中文件更容易管理,并減少混亂。

現(xiàn)在,請(qǐng)雙擊PlayerMovement.cs腳本。在Mac上,這將打開(kāi)隨同Unity一起打包的MonoDevelop開(kāi)發(fā)環(huán)境;在Windows上,它應(yīng)該打開(kāi)Visual Studio。本教程假設(shè)你使用MonoDevelop。

在類(lèi)中聲明下面兩個(gè)公共變量:

public float acceleration;

public float maxSpeed;

其中,acceleration用于描述玩家的速度如何隨時(shí)間增加,而maxSpeed代表“速度極限”。制作一個(gè)public類(lèi)型的變量將會(huì)使之顯示于【Inspector】之中,這樣你就可以通過(guò)Unity界面來(lái)設(shè)置它的值,并根據(jù)需要調(diào)整它。

緊接著上面的聲明,再聲明以下變量:

private Rigidbody rigidBody;

private KeyCode[] inputKeys;

private Vector3[] directionsForKeys;

注意,私有變量無(wú)法通過(guò)【Inspector】進(jìn)行設(shè)置。因此,需要由程序員在適當(dāng)?shù)臅r(shí)候以完全手動(dòng)方式對(duì)它們進(jìn)行初始化。

接下來(lái),把Start()函數(shù)修改成如下所示的代碼:

  1. void Start () { 
  2.   inputKeys = new KeyCode[] { KeyCode.W, KeyCode.A, KeyCode.S, KeyCode.D }; 
  3.   directionsForKeys = new Vector3[] { Vector3.forward, Vector3.left, Vector3.back, Vector3.right }; 
  4.   rigidBody = GetComponent<Rigidbody>(); 

上述代碼中的inputKeys數(shù)組包含了您將用于移動(dòng)玩家的鍵碼。directionsForKeys包含相應(yīng)于每個(gè)鍵的方向;例如,按下W用于向前移動(dòng)對(duì)象。至于最后一行代碼,你還記得前面添加的剛體嗎?這是可以得到對(duì)該組件的引用的一種方式。

要移動(dòng)玩家,你就必須處理來(lái)自于鍵盤(pán)的輸入。

現(xiàn)在,請(qǐng)重命名函數(shù)Update()為FixedUpdate(),并給它添加以下代碼:

  1. // 1 
  2. void FixedUpdate () { 
  3.   for (int i = 0; i < inputKeys.Length; i++){ 
  4.     var key = inputKeys[i]; 
  5.   
  6.     // 2 
  7.     if(Input.GetKey(key)) { 
  8.       // 3 
  9.       Vector3 movement = directionsForKeys[i] * acceleration * Time.deltaTime; 
  10.     } 
  11.   } 

這里發(fā)生了幾件重要的事情:

1.FixedUpdate()函數(shù)是幀速率獨(dú)立的,在操作剛體時(shí)應(yīng)該調(diào)用此函數(shù)。

2.這個(gè)循環(huán)檢查是否有任何輸入鍵被按下。

3.在這里,你得到按鍵的方向,并把它乘以加速度和完成最后一幀的修復(fù)所耗費(fèi)的秒數(shù)。這將產(chǎn)生您創(chuàng)建一個(gè)矢量,正是使用它來(lái)移動(dòng)Player對(duì)象。

注意,當(dāng)您創(chuàng)建一個(gè)新的Unity腳本時(shí),你實(shí)際上是創(chuàng)建一個(gè)新的MonoBehaviour對(duì)象。如果你熟悉iOS編程世界,那么你會(huì)知道它是一個(gè)UIViewController的等價(jià)物;也就是說(shuō),你可以使用這個(gè)對(duì)象來(lái)響應(yīng)Unity內(nèi)的事件,從而訪(fǎng)問(wèn)你自己的數(shù)據(jù)對(duì)象。

MonoBehaviours有很多不同的方法,它們分別對(duì)各種事件作出響應(yīng)。舉例來(lái)說(shuō),當(dāng)MonoBehaviour實(shí)例化時(shí)如果你想初始化一些變量,那么你就可以實(shí)現(xiàn)方法Awake()。在MonoBehaviour被禁用時(shí)為了運(yùn)行代碼,你可以實(shí)現(xiàn)方法OnDisable()。

【提示】如果你想研究這些事件的完整列表,請(qǐng)?jiān)L問(wèn)Unity官方文檔,地址是 http://docs.unity3d.com/ScriptReference/MonoBehaviour.html。

如果你是游戲編程新手,你可能會(huì)問(wèn)自己,為什么必須乘以Time.deltaTime?一般的規(guī)律是,當(dāng)你每隔固定的時(shí)間幀數(shù)執(zhí)行一個(gè)動(dòng)作時(shí),你需要乘以Time.deltaTime。在本例情況下,你想要沿按鍵方向加速移動(dòng)玩家,加速數(shù)值為固定的更新時(shí)間。

接下來(lái),在方法FixedUpdate()下面添加以下方法:

  1. void movePlayer(Vector3 movement) { 
  2.   if(rigidBody.velocity.magnitude * acceleration > maxSpeed) { 
  3.     rigidBody.AddForce(movement * -1); 
  4.   } else { 
  5.     rigidBody.AddForce(movement); 
  6.   } 

上述方法用于對(duì)剛體施加力作用,使其移動(dòng)。如果當(dāng)前速度超過(guò)maxSpeed,力會(huì)變成相反的方向......這有點(diǎn)像一個(gè)速度極限。

現(xiàn)在,請(qǐng)?jiān)诜椒‵ixedUpdate()中,在if語(yǔ)句的結(jié)束括號(hào)之前,添加以下行:

movePlayer(movement);

很好!回到Unity中。然后,在項(xiàng)目瀏覽器中,將PlayerMovement腳本拖動(dòng)到【Hierarchy】中的Player對(duì)象上。然后,使用【Inspector】來(lái)把「Acceleration」的值設(shè)置為625并把最大速度(Max Speed)修改為4375:

 

現(xiàn)在,請(qǐng)運(yùn)行一下游戲場(chǎng)景,并試著使用鍵盤(pán)上的WASD鍵移動(dòng)玩家對(duì)象,觀察效果:

 

到目前,我們僅僅實(shí)現(xiàn)了幾行代碼,這已經(jīng)算是一個(gè)相當(dāng)不錯(cuò)的結(jié)果了!

然而,現(xiàn)在有一個(gè)明顯的問(wèn)題:玩家可以移出人們的視線(xiàn)之外,這在打壞人時(shí)是個(gè)麻煩事。

編寫(xiě)攝相機(jī)腳本

在「Scripts」文件夾中,創(chuàng)建一個(gè)名為CameraRig的新的腳本,并將其附加到主攝像機(jī)(Main Camera)上。

【提示】在選擇【Scripts】文件夾情況下,點(diǎn)擊工程瀏覽器中的【Create】按鈕,然后選擇【C# Script】。命名新的腳本為「CameraRig」。最后,把此腳本拖動(dòng)到「Main Camera」對(duì)象上即可。

現(xiàn)在,在新創(chuàng)建的CameraRig類(lèi)中添加下列變量:

public float moveSpeed;

public GameObject target;

private Transform rigTransform;

正如你可能已經(jīng)猜到的,moveSpeed代表了相機(jī)跟蹤目標(biāo)的速度——這可能是場(chǎng)景里面的任何游戲?qū)ο蟆?/p>

接下來(lái),在Start()函數(shù)中添加以下代碼行:

rigTransform= this.transform.parent;

此代碼獲取場(chǎng)景層次樹(shù)中的到父Camera對(duì)象的引用。場(chǎng)景中的每個(gè)對(duì)象具有一個(gè)變換(Transform),其中描述了一個(gè)對(duì)象的位置旋轉(zhuǎn)和縮放等信息。

 

然后,在與上面同一個(gè)腳本文件中添加下面的方法:

  1. void FixedUpdate () { 
  2.   if(target == null){ 
  3.     return; 
  4.   } 
  5.   
  6.   rigTransform.position = Vector3.Lerp(rigTransform.position, target.transform.position, 
  7.     Time.deltaTime * moveSpeed); 

這部分CameraRig移動(dòng)代碼要比在PlayerMovement中的簡(jiǎn)單一些。這是因?yàn)槟悴恍枰粋€(gè)剛體;只需要在rigTransform的位置和目標(biāo)之間進(jìn)行插值就足夠了。

Vector3.Lerp()函數(shù)使用了空間中的兩個(gè)點(diǎn),還有一個(gè)界于[0,1]范圍內(nèi)的浮點(diǎn)數(shù)(它描述了沿兩個(gè)端點(diǎn)的中間的某一點(diǎn))作參數(shù)。左端點(diǎn)為0,右側(cè)端點(diǎn)是1。于是,把0.5傳遞給Lerp()函數(shù)將正好返回位于兩個(gè)端點(diǎn)中間的一個(gè)點(diǎn)。

這會(huì)將rigTransform移到距目標(biāo)位置更近一些,而且略有緩動(dòng)效果??傊?,相機(jī)跟隨玩家運(yùn)動(dòng)。

現(xiàn)在,返回到Unity。確保層次樹(shù)(Hierarchy)中的主攝像機(jī)(Main Camera)仍處于選中狀態(tài)。在【Inspector】中,把Move Speed(移動(dòng)速度)設(shè)置為8,并把Target(目標(biāo))設(shè)置為Player:

再次運(yùn)行游戲工程,沿場(chǎng)景四處移動(dòng);你會(huì)注意到,無(wú)論玩家走到哪里,相機(jī)都能夠平滑地跟隨目標(biāo)變換。

創(chuàng)建敵人對(duì)象

一款沒(méi)有敵人的射擊游戲很容易被擊敗,當(dāng)然也很無(wú)聊。所以,現(xiàn)在我們來(lái)通過(guò)單擊頂部菜單中的【GameObject\3D Object\Cube】創(chuàng)建一個(gè)用于表示敵人的立方體對(duì)象。然后,把此立方體重命名為「Enemy」,并添加一個(gè)Rigidbody(剛體)組件。

在【Inspector】中,首先設(shè)置立方體的變換為(0,0.5,4)。并在剛體組件的「Constraints」部分的「Freeze Position」類(lèi)別下勾選「Y」選擇對(duì)應(yīng)的復(fù)選框。

很好,現(xiàn)在使你的敵人氣勢(shì)洶洶地走動(dòng)吧。然后,在【Scripts】文件夾下創(chuàng)建一個(gè)命名為「Enemy」的腳本?,F(xiàn)在,你應(yīng)該對(duì)這種操作很熟練了;恕不再贅述。

接下來(lái),在類(lèi)內(nèi)部添加下列公共變量:

public float moveSpeed;

public int health;

public int damage;

public Transform targetTransform;

你也許可以很容易地確定出這些變量所代表的含義。你可以使用如前面一樣的moveSpeed變量技巧來(lái)操縱攝像機(jī),而且它們的效果相同。Health和damage這兩個(gè)變量分別用于確定何時(shí)敵人死了以及他們死多少會(huì)傷害玩家。最后,變量targetTransform用于引用玩家對(duì)象對(duì)應(yīng)的變換。

說(shuō)到玩家對(duì)象,你需要?jiǎng)?chuàng)建一個(gè)類(lèi)來(lái)描述敵人想破壞的所有玩家的健康值。

在項(xiàng)目瀏覽器中,選擇「Player」文件夾,并創(chuàng)建一個(gè)名為「Player」的新腳本。這個(gè)腳本會(huì)響應(yīng)于碰撞,并跟蹤玩家的健康值?,F(xiàn)在,我們通過(guò)雙擊此腳本來(lái)編輯它。

添加下列公共變量來(lái)保存玩家的健康值:

public int health = 3;

這樣便提供了玩家健康值的默認(rèn)值,但它也可以通過(guò)【Inspector】進(jìn)行修改。

為了處理沖突,添加以下方法:

  1. void collidedWithEnemy(Enemy enemy) { 
  2.   // Enemy attack code 
  3.   if(health <= 0) { 
  4.     // Todo 
  5.   } 
  6. void OnCollisionEnter (Collision col) { 
  7.     Enemy enemy = col.collider.gameObject.GetComponent<Enemy>(); 
  8.     collidedWithEnemy(enemy); 

當(dāng)兩個(gè)剛體發(fā)生碰撞時(shí),OnCollisionEnter()即被觸發(fā)。其中,Collision參數(shù)中包含了諸如接觸點(diǎn)和沖擊速度相關(guān)的信息。在本示例情況下,我們只對(duì)碰撞物體中的Enemy組件感興趣,所以可以調(diào)用collidedWithEnemy()并執(zhí)行攻擊邏輯——接下來(lái)就會(huì)實(shí)現(xiàn)這種邏輯。

切換回文件Enemy.cs,并添加以下方法:

  1. void FixedUpdate () { 
  2.   if(targetTransform != null) { 
  3.     this.transform.position = Vector3.MoveTowards(this.transform.position, targetTransform.transform.position, Time.deltaTime * moveSpeed); 
  4.   } 
  5. }  
  6. public void TakeDamage(int damage) { 
  7.   health -damage
  8.   if(health <= 0) { 
  9.     Destroy(this.gameObject); 
  10.   } 
  11.   
  12. public void Attack(Player player) { 
  13.   player.health -this.damage; 
  14.   Destroy(this.gameObject); 

你已經(jīng)熟悉了FixedUpdate()函數(shù),略有不同的是現(xiàn)在使用的是MoveTowards()而不是Lerp()函數(shù)。這是因?yàn)閿橙藨?yīng)該一直以相同的速度移動(dòng)而不會(huì)在接近目標(biāo)時(shí)出現(xiàn)快速移動(dòng)。當(dāng)敵人被彈丸擊中時(shí),TakeDamage()即被調(diào)用;當(dāng)敵人到達(dá)值為0的健康值時(shí)他會(huì)自我毀滅。Attack()函數(shù)的實(shí)現(xiàn)邏輯是與之很類(lèi)似的——對(duì)玩家進(jìn)行傷害,然后敵人破壞自身。

切換回Player.cs。然后,在函數(shù)collidedWithEnemy()中,使用下面代碼替換注釋// Enemy attack code:

enemy.Attack(this);

游戲中,玩家將受到傷害,而敵人在該過(guò)程中將自我毀滅。

切換回Unity。把Enemy腳本附加到 Enemy對(duì)象上;并在【Inspector】中,針對(duì)Enemy對(duì)象設(shè)置以下值:

1.Move Speed:5

2.Health:2

3.Damage:1

4.Target Transform:Player

現(xiàn)在,你應(yīng)該能夠自己做這一切了。結(jié)束后,你可以與文后完整的工程源碼進(jìn)行比較。

在游戲中,敵人與玩家碰撞,從而實(shí)現(xiàn)一種有效的敵對(duì)攻擊。使用Unity的物理碰撞檢測(cè)幾乎是一個(gè)很簡(jiǎn)單的任務(wù)。

最后,在層次結(jié)構(gòu)中把Player腳本附加到Player對(duì)象。

運(yùn)行游戲工程,并留意在控制臺(tái)上輸出的結(jié)果:

 

當(dāng)敵人接觸到玩家時(shí),它能夠成功地進(jìn)行攻擊,并把玩家的健康值變量降低到2。但是,現(xiàn)在在控制臺(tái)中拋出一個(gè)NullReferenceException異常,錯(cuò)誤指向Player腳本:

哈哈,現(xiàn)在玩家不僅可以與敵人碰撞,也可能與游戲世界中的其他部分,如戰(zhàn)場(chǎng),發(fā)生碰撞!這些游戲?qū)ο蟛](méi)有Enemy腳本,因此GetComponent()函數(shù)將返回null。

接下來(lái),打開(kāi)文件Player.cs。然后,在OnCollisionEnter()函數(shù)中,把collidedWithEnemy()函數(shù)調(diào)用使用一個(gè)if語(yǔ)句包括起來(lái),如下所示:

  1. if(enemy) { 
  2.   collidedWithEnemy(enemy); 

此時(shí),異常消失!

使用預(yù)制

只是簡(jiǎn)單地在戰(zhàn)場(chǎng)上跑來(lái)跑去,而且避開(kāi)敵人;這只能算是一個(gè)一邊倒的游戲?,F(xiàn)在,我們來(lái)武裝一下玩家,使之能夠作戰(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)玩家射擊時(shí),他就會(huì)發(fā)射Projectile(炮彈)的一個(gè)實(shí)例。要做到這一點(diǎn),你需要?jiǎng)?chuàng)建一個(gè)預(yù)制(Prefab)。不像場(chǎng)景中你已經(jīng)擁有的其他對(duì)象,預(yù)制對(duì)象是根據(jù)游戲邏輯需要而創(chuàng)建的。

現(xiàn)在,在文件夾「Assets」下創(chuàng)建一個(gè)新的文件夾,名為Prefabs。現(xiàn)在,把Projectile對(duì)象拖動(dòng)到這個(gè)文件夾上。就是這樣:你創(chuàng)建了一個(gè)預(yù)制!

您的預(yù)制還需要一點(diǎn)腳本?,F(xiàn)在,在【Scripts】文件夾內(nèi)創(chuàng)建一個(gè)名為「Projectile」的新腳本,并添加下面的類(lèi)變量:

public float speed;

public int damage;

Vector3 shootDirection;

就像目前為止在本教程中任何可移動(dòng)的物體一樣,這個(gè)對(duì)象也會(huì)有速度和傷害對(duì)應(yīng)的變量,因?yàn)樗菓?zhàn)斗邏輯的一部分。其中,shootDirection矢量決定了炮彈將向哪兒發(fā)射。

在類(lèi)中實(shí)現(xiàn)下面的方法即可使這個(gè)矢量發(fā)揮作用:

  1. // 1 
  2. void FixedUpdate () { 
  3.   this.transform.Translate(shootDirection * speed, Space.World); 
  4. // 2 
  5. public void FireProjectile(Ray shootRay) { 
  6.   this.shootDirection = shootRay.direction; 
  7.   this.transform.position = shootRay.origin; 
  8. // 3 
  9. void OnCollisionEnter (Collision col) { 
  10.   Enemy enemy = col.collider.gameObject.GetComponent<Enemy>(); 
  11.   if(enemy) { 
  12.     enemy.TakeDamage(damage); 
  13.   } 
  14.   Destroy(this.gameObject); 

在上面的代碼中發(fā)生了下面的事情:

1.炮彈在游戲中的運(yùn)動(dòng)方式與其他對(duì)象不同。它不具有一個(gè)目標(biāo),或者一直對(duì)它施加一些力;相反,它在其整個(gè)生命周期中的按照預(yù)定方向進(jìn)行運(yùn)動(dòng)。

2.在這里,我們?cè)O(shè)置了預(yù)制對(duì)象的起始位置和方向。Ray參數(shù)看上去似乎很神秘吧,但你很快就會(huì)知道它是如何計(jì)算出來(lái)的。

3.如果一個(gè)炮彈與敵人發(fā)生碰撞,它會(huì)調(diào)用TakeDamage(),并進(jìn)行自我毀滅。

在場(chǎng)景層次中,把Projectile腳本附加到Projectile游戲?qū)ο笊稀TO(shè)置它的速度為0.2,并把損壞值設(shè)置為1,然后點(diǎn)擊【Inspector】頂部的【Apply】按鈕。這將針對(duì)這個(gè)預(yù)制的所有實(shí)例保存剛才所做的更改。

現(xiàn)在,請(qǐng)從場(chǎng)景層次樹(shù)中刪除Projectile對(duì)象,因?yàn)槲覀儾辉傩枰恕?nbsp;

發(fā)射炮彈

現(xiàn)在,你既然已經(jīng)擁有了可以移動(dòng)并施加傷害能力的預(yù)制對(duì)象,那么,接下來(lái)你就可以開(kāi)始考慮實(shí)現(xiàn)發(fā)射炮彈相關(guān)的編程了。

在Player文件夾下,創(chuàng)建一個(gè)名為PlayerShooting的新腳本,并將其附加到場(chǎng)景中的Player游戲?qū)ο?。然后,在Player類(lèi)中,聲明以下變量:

public Projectile projectilePrefab;

public LayerMask mask;

第一個(gè)變量將包含對(duì)前面創(chuàng)建的Projectile預(yù)制對(duì)象的引用。每當(dāng)玩家發(fā)射炮彈時(shí),您將從這個(gè)預(yù)制創(chuàng)建一個(gè)新的實(shí)例。mask變量是用來(lái)篩選游戲?qū)ο螅℅ameObject)的。

現(xiàn)在,我們要介紹一下光線(xiàn)投射的問(wèn)題。何謂光線(xiàn)投射(casting Ray)?這是什么魔法?

其實(shí),并不存在什么黑魔法。但是,有時(shí)候在你的游戲中,你的確需要知道是否在一個(gè)特定方向上存在碰撞。要做到這一點(diǎn),Unity在您指定的方向上能夠從某一個(gè)點(diǎn)投出一條看不見(jiàn)的射線(xiàn)。你可能會(huì)遇到很多與射線(xiàn)相交的游戲?qū)ο?;因此,使用篩選器可以過(guò)濾掉任何不需要參與碰撞的對(duì)象。

光線(xiàn)投射是非常有用的,并且可以用于各種用途。它們常用于測(cè)試是否另一名玩家已經(jīng)被炮彈擊中;而且,你也可以使用它們來(lái)測(cè)試是否在鼠標(biāo)指針下方存在任何的幾何形狀。要更多地了解關(guān)于光線(xiàn)投射的內(nèi)容,請(qǐng)參考一下Unity官方網(wǎng)站提供的在線(xiàn)培訓(xùn)視頻(https://unity3d.com/learn/tutorials/modules/beginner/physics/raycasting)。

下圖顯示了從一個(gè)立方體到一個(gè)錐體的光線(xiàn)投射情況。由于射線(xiàn)上有一個(gè)圖標(biāo)掩碼,因此它忽略掉游戲?qū)ο蠖到y(tǒng)給出的提示是擊中了錐體:

接下來(lái),我們需要?jiǎng)?chuàng)建自己的射線(xiàn)了。

把如下代碼添加到文件PlayerShooting.cs:

  1.  void shoot(RaycastHit hit){ 
  2.  
  3.   // 1 
  4.  
  5.   var projectile = Instantiate(projectilePrefab).GetComponent<Projectile>(); 
  6.  
  7.   // 2 
  8.  
  9.   var pointAboveFloor = hit.point + new Vector3(0, this.transform.position.y, 0); 
  10.  
  11.   // 3 
  12.  
  13.   var direction = pointAboveFloor - transform.position; 
  14.  
  15.  // 4 
  16.  
  17.   var shootRay = new Ray(this.transform.position, direction); 
  18.  
  19.   Debug.DrawRay(shootRay.origin, shootRay.direction * 100.1f, Color.green, 2);  
  20.  
  21.   // 5 
  22.  
  23.   Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>());  
  24.  
  25.   // 6 
  26.  
  27.   projectile.FireProjectile(shootRay); 
  28.  

概括來(lái)看,上面的代碼主要實(shí)現(xiàn)如下功能:

1.    實(shí)例化一個(gè)炮彈預(yù)制并獲得它的Projectile組件,從而可以把它初始化。

2.    這個(gè)坐標(biāo)點(diǎn)總是使用像(X,0.5,Z)這樣的格式。其中,X和Z坐標(biāo)位于地面上,正好對(duì)應(yīng)于射線(xiàn)投射擊中的鼠標(biāo)點(diǎn)擊位置的坐標(biāo)。這里的計(jì)算是很重要的,因?yàn)榕趶棻仨毱叫杏诘孛?;否則,你會(huì)向下射擊,而只有外行的玩家才會(huì)出現(xiàn)向地面射擊的情況。

3.    計(jì)算從游戲物體Player指向pointAboveFloor的方向。

4.    創(chuàng)建一條新的射線(xiàn),并通過(guò)其原點(diǎn)和方向來(lái)共同描述炮彈軌跡。

5.    這行代碼告訴Unity的物理引擎忽略玩家與炮彈之間的碰撞。否則,在炮彈飛出去前將調(diào)用Projectile腳本中的OnCollisionEnter()方法。

6.    最后,設(shè)置炮彈的運(yùn)動(dòng)軌跡。

【注意】當(dāng)光線(xiàn)投射不可見(jiàn)時(shí),你可以使用Debug.DrawRay()方法來(lái)輔助調(diào)試程序,因?yàn)樗梢詭椭庇^地觀察光線(xiàn)的外觀和它所擊中的對(duì)象。

好,現(xiàn)在既然發(fā)射炮彈的邏輯已經(jīng)實(shí)現(xiàn),請(qǐng)繼續(xù)添加下面的方法來(lái)讓玩家真正扣動(dòng)扳機(jī):

  1.  // 1 
  2.  
  3. void raycastOnMouseClick () { 
  4.  
  5.   RaycastHit hit; 
  6.  
  7.   Ray rayToFloor = Camera.main.ScreenPointToRay(Input.mousePosition); 
  8.  
  9.   Debug.DrawRay(rayToFloor.origin, rayToFloor.direction * 100.1f, Color.red, 2); 
  10.  
  11.    if(Physics.Raycast(rayToFloor, out hit, 100.0f, mask, QueryTriggerInteraction.Collide)) { 
  12.  
  13.     shoot(hit); 
  14.  
  15.   } 
  16.  
  17.  
  18. // 2 
  19.  
  20. void Update () { 
  21.  
  22.   bool mouseButtonDown = Input.GetMouseButtonDown(0); 
  23.  
  24.   if(mouseButtonDown) { 
  25.  
  26.     raycastOnMouseClick();  
  27.  
  28.   } 
  29.  

讓我們按上面編號(hào)進(jìn)行逐個(gè)解釋?zhuān)?/p>

1.這個(gè)方法把射線(xiàn)從攝相機(jī)射向鼠標(biāo)點(diǎn)擊的位置,然后檢查是否射線(xiàn)相交于符合給定LayerMask掩碼值的游戲?qū)ο蟆?/strong>

2.在每次更新中,腳本都會(huì)檢查一下鼠標(biāo)左鍵按下情況。如果發(fā)現(xiàn)存在按下的情況,就調(diào)用raycastOnMouseClick()方法。

現(xiàn)在,請(qǐng)返回到Unity中,并在【Inspector】中設(shè)置下列變量:

  Projectile Prefab:引用文件夾prefab下的Projectile;

  Mask:Floor

【注意】Unity使用數(shù)量有限的預(yù)定義掩碼——也稱(chēng)為層。

你可以通過(guò)點(diǎn)擊一個(gè)游戲物體的【Layer】下拉菜單然后選擇【Add Layer】(添加圖層)來(lái)定義你自己的掩碼:

您也可以通過(guò)從【Layer】下拉菜單中選擇一個(gè)層來(lái)給游戲?qū)ο蠓峙溲诖a:

有關(guān)Unity3d引擎中層的更多的信息,請(qǐng)參考官方文檔,地址是http://docs.unity3d.com/Manual/Layers.html。

現(xiàn)在,請(qǐng)運(yùn)行示例項(xiàng)目并隨意發(fā)射炮彈!你會(huì)注意到:炮彈按照希望的方向發(fā)射,但看起來(lái)還缺少點(diǎn)什么,不是嗎?

如果炮彈是沿著其發(fā)射的方向行進(jìn)的,那將酷多了。為了解決這個(gè)問(wèn)題,打開(kāi)Projectile.cs腳本并添加下面的方法:

  1. void rotateInShootDirection() { 
  2.  
  3.  Vector3 newRotation = Vector3.RotateTowards(transform.forward, shootDirection, 0.01f, 0.0f); 
  4.  
  5.  transform.rotation = Quaternion.LookRotation(newRotation); 

【注意】RotateTowards非常類(lèi)似于MoveTowards,但它把矢量作為方向,而不是位置。此外,你并不需要一直改變旋轉(zhuǎn);因此,使用一個(gè)接近零的步長(zhǎng)值就足夠了。在Unity中實(shí)現(xiàn)旋轉(zhuǎn)變換是使用四元組實(shí)現(xiàn)的,這已超出了本教程的討論范圍。在本教程中,你只需要知道在涉及三維旋轉(zhuǎn)計(jì)算時(shí)使用四元組的優(yōu)勢(shì)超過(guò)矢量即可。當(dāng)然,如果你有興趣更多地了解關(guān)于四元組以及它們有何用處,請(qǐng)參考這篇優(yōu)秀的文章,地址是http://developerblog.myo.com/quaternions/。

接下來(lái),在FireProjectile()方法的結(jié)束處,添加對(duì)rotateInShootDirection()方法的調(diào)用。 現(xiàn)在,F(xiàn)ireProjectile()方法看起來(lái)應(yīng)該像下面這樣:

  1.  public void FireProjectile(Ray shootRay) { 
  2.  
  3.   this.shootDirection = shootRay.direction; 
  4.  
  5.   this.transform.position = shootRay.origin; 
  6.  
  7.   rotateInShootDirection(); 
  8.  

再次運(yùn)行游戲,并沿幾個(gè)不同的方向發(fā)射炮彈。此時(shí),炮彈將指向它們發(fā)射的方向?,F(xiàn)在,你可以清除代碼中的Debug.DrawRay調(diào)用了,因?yàn)槟悴辉傩枰鼈兞恕?/p>

生成更多敵人對(duì)象

只有一個(gè)敵人的游戲并不具有挑戰(zhàn)性。但現(xiàn)在,你已經(jīng)知道了預(yù)制的用法。于是,你可以生成任意數(shù)目的對(duì)手了!

為了讓玩家不斷猜想,你可以隨機(jī)地控制每個(gè)敵人的健康值、速度和位置等。

現(xiàn)在,使用命令【GameObject】-【Create Empty】創(chuàng)建一個(gè)空的游戲?qū)ο蟆C鼮椤窫nemyProducer」,并添加一個(gè)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) 

上面你附加的這個(gè)碰撞器實(shí)際上在戰(zhàn)場(chǎng)中定義了一個(gè)特定的3D空間。為了看到這個(gè)對(duì)象,請(qǐng)從層次結(jié)構(gòu)樹(shù)下選擇【Enemy Producer】游戲物體;于是,在場(chǎng)景視圖中你會(huì)看到這個(gè)對(duì)象,如下圖所示。

圖中用綠線(xiàn)框出的部分代表了一個(gè)碰撞器

現(xiàn)在,你要編寫(xiě)一個(gè)腳本實(shí)現(xiàn)沿X軸和Z軸方向選取空間中的一個(gè)隨機(jī)位置并實(shí)例化一個(gè)敵人預(yù)制。

創(chuàng)建一個(gè)名為EnemyProducer的新腳本,并將其附加到游戲?qū)ο驟nemyProducer。然后,在新設(shè)置的類(lèi)內(nèi)部,添加以下實(shí)例成員:

public bool shouldSpawn;

public Enemy[] enemyPrefabs;

public float[] moveSpeedRange;

public int[] healthRange;

private Bounds spawnArea;

private GameObject player;

第一個(gè)變量控制啟用還是禁用敵人對(duì)象的生成。該腳本將從enemyPrefabs中選擇一個(gè)隨機(jī)的敵人預(yù)制并創(chuàng)建其實(shí)例。接下來(lái)的兩個(gè)數(shù)組將分別指定速度和健康值的最小值和最大值。生成敵人的地方是你在場(chǎng)景視圖中看到的綠色框。最后,你需要一個(gè)到玩家Player的引用,并把它作為目標(biāo)參數(shù)傳遞給敵人對(duì)象。

在腳本中,接著定義以下方法:

  1.  public void SpawnEnemies(bool shouldSpawn) { 
  2.  
  3.   if(shouldSpawn) { 
  4.  
  5.     player = GameObject.FindGameObjectWithTag("Player"); 
  6.  
  7.   } 
  8.  
  9.   this.shouldSpawn = shouldSpawn; 
  10.  
  11.  
  12. void Start () { 
  13.  
  14.   spawnArea = this.GetComponent<BoxCollider>().bounds; 
  15.  
  16.   SpawnEnemies(shouldSpawn); 
  17.  
  18.   InvokeRepeating("spawnEnemy", 0.5f, 1.0f); 
  19.  

SpawnEnemies()方法獲取到標(biāo)簽為Player的游戲?qū)ο蟮囊茫⒋_定是否應(yīng)該生成一個(gè)敵人。

Start()方法初始化敵人生成的位置并在游戲開(kāi)始0.5秒之后調(diào)用一個(gè)方法。每一秒它都會(huì)被反復(fù)調(diào)用。除了作為一個(gè)setter方法外,SpawnEnemies()方法還得到一個(gè)到標(biāo)簽為「Player」的游戲?qū)ο蟮囊谩?/p>

注意,到現(xiàn)在為止,玩家游戲?qū)ο笊形礃?biāo)記?,F(xiàn)在,就要做這件事情。請(qǐng)從【Hierarchy】中選擇Player對(duì)象,然后在【Inspector】選項(xiàng)卡中從「Tag」下拉菜單中選擇Player,如下圖所示。

現(xiàn)在,你需要編寫(xiě)實(shí)際的生成單個(gè)敵人的代碼。

打開(kāi)Enemy腳本,并添加下面的方法:

  1.  public void Initialize(Transform target, float moveSpeed, int health) { 
  2.  
  3.   this.targetTransform = target; 
  4.  
  5.   this.moveSpeed = moveSpeed; 
  6.  
  7.   this.health = health; 
  8.  

這個(gè)方法充當(dāng)用于創(chuàng)建對(duì)象的setter方法。下一步:要編寫(xiě)生成成群的敵人的代碼。打開(kāi)EnemyProducer.cs文件,并添加以下方法:

  1.  Vector3 randomSpawnPosition() { 
  2.  
  3.   float x = Random.Range(spawnArea.min.x, spawnArea.max.x); 
  4.  
  5.   float z = Random.Range(spawnArea.min.z, spawnArea.max.z); 
  6.  
  7.   float y = 0.5f; 
  8.  
  9.   return new Vector3(x, y, z); 
  10.  
  11. }  
  12.  
  13. void spawnEnemy() { 
  14.  
  15.   if(shouldSpawn == false || player == null) { 
  16.  
  17.     return; 
  18.  
  19.   }  
  20.  
  21.   int index = Random.Range(0, enemyPrefabs.Length); 
  22.  
  23.   var newEnemy = Instantiate(enemyPrefabs[index], randomSpawnPosition(), Quaternion.identity) as Enemy; 
  24.  
  25.   newEnemy.Initialize(player.transform, 
  26.  
  27.       Random.Range(moveSpeedRange[0], moveSpeedRange[1]), 
  28.  
  29.       Random.Range(healthRange[0], healthRange[1])); 
  30.  

這個(gè)spawnEnemy()方法所做的就是選擇一個(gè)隨機(jī)的敵人預(yù)制,在隨機(jī)位置實(shí)例化并初始化腳本Enemy中的公共變量。

現(xiàn)在,腳本EnemyProducer.cs快要準(zhǔn)備好了!

返回到Unity中。通過(guò)把Enemy對(duì)象從【Hierarchy】拖動(dòng)到【Prefabs】文件夾創(chuàng)建一個(gè)Enemy預(yù)制。然后,從場(chǎng)景中移除Enemy對(duì)象——你不需要它了。接下來(lái),設(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)行游戲并注意觀察。你會(huì)注意到場(chǎng)景中無(wú)休止地出現(xiàn)成群的敵人!

好吧,這些立方體看起來(lái)還不算非??膳隆,F(xiàn)在,我們?cè)賮?lái)添加一些細(xì)節(jié)修飾。

在場(chǎng)景中創(chuàng)建一個(gè)三維圓柱(Cylinder)和一個(gè)膠囊(Capsule)。分別命名為「Enemy2」和「Enemy3」。就像前面你針對(duì)第一個(gè)敵人所做的那樣,向這兩個(gè)對(duì)象分別都添加一個(gè)剛體組件和一個(gè)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)在,針對(duì)Enemy3也進(jìn)行與上面同樣的設(shè)置,但是把它的Scale設(shè)置成0.7,如下圖所示。

接下來(lái),把他們轉(zhuǎn)換成預(yù)制,就像你操作最開(kāi)始的那個(gè)敵人那樣,并在「Enemy Producer」中引用它們。在【Inspector】中的值應(yīng)該像下面這樣:

  Enemy Prefabs:

  Size: 3

  Element 0: Enemy

  Element 1: Enemy2

  Element 2: Enemy3 

再次運(yùn)行游戲;現(xiàn)在,你會(huì)觀察到在場(chǎng)景中生成不同的預(yù)制。

其實(shí),在你意識(shí)到你是不可戰(zhàn)勝的之前,不會(huì)花費(fèi)太長(zhǎng)的時(shí)間!

開(kāi)發(fā)游戲控制器

現(xiàn)在,您已經(jīng)能夠射擊、移動(dòng),而且能夠把敵人放在指定位置。在本節(jié)中,你將實(shí)現(xiàn)一個(gè)基本的游戲控制器。一旦玩家“死”了,它將重新啟動(dòng)游戲。但首先,你必須建立一種機(jī)制以通知所有有關(guān)各方——玩家已達(dá)到0健康值。

現(xiàn)在,打開(kāi)Player腳本,并在類(lèi)聲明上方添加如下內(nèi)容:

using System;

然后,在類(lèi)中添加以下新的公共事件:

public event Action<Player> onPlayerDeath;

【提示】事件是C#語(yǔ)言中的重要功能之一,讓你向所有監(jiān)聽(tīng)者廣播對(duì)象中的變化。要了解如何使用事件,你可以參考一下官方的事件培訓(xùn)視頻(https://unity3d.com/learn/tutorials/topics/scripting/events)。

接下來(lái),編輯collidedWithEnemy()方法,使之最終看起來(lái)具有像下面這樣的代碼:

 

  1. void collidedWithEnemy(Enemy enemy) { 
  2.  
  3.  enemy.Attack(this); 
  4.  
  5.  if(health <= 0) { 
  6.  
  7.    if(onPlayerDeath != null) { 
  8.  
  9.      onPlayerDeath(this); 
  10.  
  11.    } 
  12.  
  13.  } 

事件為對(duì)象之間的狀態(tài)變化通知提供了一種整潔的實(shí)現(xiàn)方案。游戲控制器對(duì)上述聲明的事件是很感興趣的。在Scripts文件夾中,創(chuàng)建一個(gè)名為GameController的新腳本。然后,雙擊該文件進(jìn)行編輯,并給它添加下列變量:

public EnemyProducer enemyProducer;

public GameObject playerPrefab;

腳本在生成敵人時(shí)需要進(jìn)行一定的控制,因?yàn)橐坏┩婕覇噬偕蓴橙耸菦](méi)有任何意義的。此外,重新啟動(dòng)游戲意味著你將不得不重新創(chuàng)建玩家,這意味著……是的,你要通過(guò)把玩家變成預(yù)制來(lái)更靈活地實(shí)現(xiàn)這一目的。

于是,請(qǐng)?zhí)砑酉铝蟹椒ǎ?/p>

  1.  void Start () { 
  2.  
  3.   var player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>(); 
  4.  
  5.   player.onPlayerDeath += onPlayerDeath; 
  6.  
  7. }  
  8.  
  9. void onPlayerDeath(Player player) { 
  10.  
  11.   enemyProducer.SpawnEnemies(false); 
  12.  
  13.   Destroy(player.gameObject);  
  14.  
  15.   Invoke("restartGame", 3); 
  16.  

在Start()方法中,該腳本先獲取到Player腳本的引用,并訂閱你先前創(chuàng)建的事件。一旦玩家的健康值達(dá)到0, onPlayerDeath()方法即被調(diào)用,從而停止敵人的生成,從場(chǎng)景中移除Player對(duì)象和并在3秒鐘后調(diào)用restartGame()方法。

最后,重新啟動(dòng)游戲的動(dòng)作實(shí)現(xiàn)如下:

  1. void restartGame() { 
  2.  
  3.  var enemies = GameObject.FindGameObjectsWithTag("Enemy"); 
  4.  
  5.  foreach (var enemy in enemies) 
  6.  
  7.  { 
  8.  
  9.    Destroy(enemy); 
  10.  
  11.  }  
  12.  
  13.  var playerObject = Instantiate(playerPrefab, new Vector3(0, 0.5f, 0), Quaternion.identity) as GameObject; 
  14.  
  15.  var cameraRig = Camera.main.GetComponent<CameraRig>(); 
  16.  
  17.  cameraRig.target = playerObject
  18.  
  19.  enemyProducer.SpawnEnemies(true); 
  20.  
  21.  playerObject.GetComponent<Player>().onPlayerDeath += onPlayerDeath; 

在這里,我們做了一些清理工作:摧毀場(chǎng)景中的所有敵人,并創(chuàng)建一個(gè)新的Player對(duì)象。然后,重新指定攝像機(jī)的目標(biāo)為玩家對(duì)象,恢復(fù)敵人生成支持,并為游戲控制器訂閱玩家死亡的事件。

現(xiàn)在返回到Unity,打開(kāi)Prefebs文件夾,更改所有敵人預(yù)制為標(biāo)簽Enemy。接下來(lái),通過(guò)拖動(dòng)Player游戲?qū)ο蟮絇refebs文件夾使玩家變成預(yù)制。再創(chuàng)建一個(gè)空的游戲?qū)ο?,將其命名為GameController,并將您剛剛創(chuàng)建的腳本附加到其上。綁定【Inspector】中所有對(duì)應(yīng)的需要的引用。

現(xiàn)在,你應(yīng)該很熟悉這種模式了。建議你試著自己實(shí)現(xiàn)引用,再次運(yùn)行游戲。請(qǐng)觀察游戲控制器是如何實(shí)現(xiàn)游戲控制的。

故事至此結(jié)束;你已經(jīng)成功地使用腳本實(shí)現(xiàn)了你的第一個(gè)Unity游戲!祝賀你!

小結(jié)

本文示例工程完整的下載地址是http://www.raywenderlich.com/wp-content/uploads/2016/03/BlockBusterFinal.zip

現(xiàn)在,你應(yīng)該對(duì)編寫(xiě)一個(gè)簡(jiǎn)單的動(dòng)作游戲所需要的內(nèi)容有了一個(gè)很好的理解。實(shí)際上,制作游戲決不是一個(gè)簡(jiǎn)單的任務(wù);它肯定需要大量的工作,而腳本只是把一個(gè)項(xiàng)目實(shí)現(xiàn)為一款真正的游戲所必需的要素之一。為了進(jìn)一步添加游戲修飾效果,還需要將動(dòng)畫(huà)和漂亮的UI及粒子效果等添加到您的游戲中。當(dāng)然,要實(shí)現(xiàn)一款真正意義上的商業(yè)游戲,您還要克服更多的困難。

 

 

責(zé)任編輯:陳琳 來(lái)源: 51cto
相關(guān)推薦

2011-04-15 12:48:28

雙搖桿射擊游戲游戲iOS

2011-05-04 10:22:27

2012-04-01 10:02:00

HTML5

2012-03-16 09:35:52

HTML 5

2013-04-25 00:06:06

unity3D手機(jī)游戲引擎

2012-12-24 08:51:23

iOSUnity3D

2023-07-26 07:59:28

2012-04-24 22:10:37

2015-08-27 16:35:10

Unity游戲引擎Linux

2012-03-11 15:20:36

Android

2012-05-15 13:57:41

HTML5

2024-03-22 09:45:34

大型語(yǔ)言模型Unity引擎游戲開(kāi)發(fā)

2012-12-24 08:46:50

iOSUnity3D

2015-04-22 20:33:06

寶德云游戲蠻牛unity

2018-02-06 10:46:53

2012-03-06 10:56:32

HTML 5

2013-04-25 09:56:24

unity3D手機(jī)游戲引擎

2012-12-24 08:40:12

2012-12-24 08:50:21

iOSUnity3D
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)