天天看點

手遊開發初探

原帖位址

http://blog.csdn.net/kakashi8841

手遊開發攻防——一、遊戲引擎的選擇

現在手遊火的一塌糊塗,引擎也是層出不窮除了引領3D市場的Unity3D,獨霸2D市場的Cocos2D-X之外,還有虛幻、Sphinx等,甚至搜狐也開發了國産的Genesis-3D引擎。

其它的不多少,這裡主要就對比Unity3D和Cocos2D-X,幫助大家選擇。

如果你想開發一個2D遊戲,有着C++/C/LUA之一的基礎,那麼Cocos2D-X也許比較适合你。他本身就為2D遊戲設計,有着豐富的源碼、執行個體和教程文檔。你可以得到社群的大力支援。

如果你想開發一個3D或2D遊戲,有着面向對象程式設計語言基礎(比如Java、C#、AS等)。那麼Unity3D會很适合你。雖然他主攻3D,但是assetstore有着豐富的插件,使得你想快速建構一個2D遊戲也不是一件難事。

那麼我個人是怎麼選擇Unity3D和Cocos2D-X的?

我自然是選擇Unity3D:

1、雖然你現在可能是做2D遊戲,但如果你以後想做3D呢?用Unity3D的話從2D到3D的轉換成本很低。

2、豐富的插件支援。

3、成熟的工具以及強大的編輯器拓展,讓你可以很友善的定制适合自己項目需要的編輯器。

4、内置強大的性能分析工具。

5、幾乎支援所有主流平台:WEB、Win、Mac、IOS、Android、XBox、PS3、Wii

6、Unity5還有更完善的光照系統、UI架構以及支援釋出成Html5。可見該引擎的發展處于高速階段。

下一章會以一個免費的項目為例子來講解Unity3D的項目構成。

手遊開發攻防——二、基礎篇(更新完)

目錄(?)[-]

  1. 項目大緻了解
  2. 碰撞器基礎
  3. 控制角色
    1. AAnimator
    2. BCapsule Collider 
    3. CRigidbody 
    4. DDonePlayerMovement
  4. 碰撞器及其使用
  5. 怪物AI
    1. AAnimator
    2. B碰撞器
    3. CDoneEnemySight
    4. DDoneEnemyAI
    5. EDoneEnemyShooting
  6. 事件通知
  7. 渲染特效
    1. A霧效
    2. B遊戲中螢幕投射到地上的亮點
    3. C雷射牆的雷射

不好意思,最近公司成員擴招,然後技術教育訓練,項目事宜原因,是以這篇文章等到現在才出。

好了,不多說其它。

文章适合人群:對Unity基礎元件有一些了解的,想知道怎麼在項目中具體應用各種元件。

這篇文章以一個Asset Store上面的例子“Unity Projects Stealth”來講解Unity的一些知識。是以可能你要對Unity一些概念有個了解。另外,這個例子"Unity Projects Stealth.unitypackage"我已經分享到http://pan.baidu.com/s/1gd6A0L1了,大家也可以去下載下傳下來參照着文章看。

這篇項目會大概分成以下部分來講(這個目錄在後續文章編寫中可能會根據需要稍作修改):

1、項目大緻了解

2、碰撞器基礎

3、控制角色

4、碰撞器及其使用

5、怪物AI

6、事件通知

7、渲染特效

OK。

在此之前先說下本文運作項目時的環境:

系統 :Window 7 X64

Unity:4.3.3f1

1、項目大緻了解:

我們打開unity,建立項目,然後導入上面下載下傳的"Unity Projects Stealth.unitypackage"。等導入完畢後就可以看到“Project”視圖(如果你沒打開,可以從菜單欄“Window”-“Project”打開)裡面的結構如下:

手遊開發初探

裡面每個目錄大家都可以随便打開看看:

Animations  一些角色動畫
Animator  動畫控制器,實際裡面啥都沒有-_-
Audio 音頻檔案
Fonts 字型
Gizmos 在scene視圖用于調試的一些圖示
Materials 材質
Models 模型
Prefabs 預設
Scenes 場景檔案,實際裡面啥都沒有-_-
Scripts 腳本,實際裡面啥都沒有-_-
Shaders 着色器
Textures 一些貼圖紋理
Done 這裡面才包含了上面缺少的動畫控制器、場景檔案和腳本〜·

  大家平常做項目也可以參考上面的做法,對不同的資源放不同目錄進行歸類整理,而且大家一看到這些檔案夾就都知道裡面有什麼資源。可以減少很多溝通成本,也為自己查找資源帶來便利。

好了,如果沒什麼問題,點選播放按鈕,應該就能運作項目了。

手遊開發初探

使用鍵盤的WASD或上下左右箭頭就可以控制人物走動了。螢幕左下方也有文本給出其它操作的按鍵。比如"Z"可以打開開關等。

2、碰撞器基礎

一般情況下,你想讓物體直接有體積,能産生碰撞,那麼你需要給這個物體增加一個碰撞器“Collider”。在Unity中,你隻需要選擇一個對象,然後點選菜單欄的”Component“-“Physics”,裡面就有各種各樣的碰撞器,你根據自己模型選擇一個比較接近的就OK了。Unity裡面的碰撞器區分為兩種,一種為靜态的,另一種動态的。

靜态的意思是這個碰撞器在遊戲過程中不會發生位移、旋轉、縮放。

動态的意思則是說這個碰撞器可能會在遊戲過程中發生位移、旋轉、縮放。動态碰撞器有兩種:

一種是CharacterController。

另一種是普通的碰撞器+剛體元件。

第二種一定要增加剛體元件。不然可能會導緻碰撞失效、性能開銷增加。(比如Unity一個UI元件NGUI,它新版本的Panel會檢測目前對象是否帶了Rigibody,如果沒有則自動增加一個。就是為了防止開發者做一些界面動畫,忘記修改添加Rigibody,導緻UI按鈕點選失效。)

3、控制角色

控制一個角色在場景中運動,最簡單的做法是把一個對象拖到場景中,然後根據按鍵。設定這個對象transform的position值。這樣對象就可以在場景中運動起來。

擷取按鍵可以用Input.GetAxis方法。擷取X和Z軸的按鍵分别可以使用:Input.GetAxis("Horizontal") 和 Input.GetAxis("Vertical")方法。“Horizontal”和“Vertical”其實是在“Edit”-“Project Settings”-“Input”裡面配置的。

那麼我們這個例子是使用什麼方法來讓玩家角色運動的呢?

先在“Hierarchy”視圖(如果沒有這個視圖,可以在“Window”-“Hierarchy”打開它)中找到“char_ethan”對象,選中它。

手遊開發初探

我們先看看它的“Inspector”視圖(如果沒有可以在“Window”-“Inspector”打開它)。

手遊開發初探

注意這裡除了有基本的Transform元件外,還有“Animator”,“Capsule Collider”,“Rigidbody”元件。這三個元件其實用于做可移動物體其實是黃金組合(當然也有用“Character Controller”的)。

還有“Audio Source”、“Audio Listener”兩個分别是播放聲音和監聽聲音的元件。

然後"Done Player Health (Script)","Done Player Inventory (Script)","Done Player Movement (Script)"分别是對角色做控制的腳本。下面我們簡單說說每個元件的用途:

A、Animator

其實是Unity内置的一個動畫控制器,原理是狀态機。比如輕按兩下上面的“Animator”面闆裡面的“Controller”屬性

手遊開發初探

就會出現狀态機的編輯視窗

手遊開發初探

B、Capsule Collider 

膠囊體碰撞器。比較簡單,沒什麼好說的。

C、Rigidbody 

剛剛上面也說了,動态碰撞器的需要。

關于A、B、C這幾個元件上面就隻簡單介紹到這裡,這裡不展開讨論。如果有需要,我後面會再單獨寫文章介紹。

下面開始看看控制代碼部分。

D、DonePlayerMovement

這裡有個“DonePlayerMovement(Script)”元件,從名字上看就知道應該是控制移動的(是以說命名很重要)。我們打開這個元件的源碼,看看。

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. using UnityEngine;  
  2. using System.Collections;  
  3. public class DonePlayerMovement : MonoBehaviour  
  4. {  
  5.     public AudioClip shoutingClip;      // 玩家大喊的聲音  
  6.     public float turnSmoothing = 15f;   // 用于玩家平滑轉向的值  
  7.     public float speedDampTime = 0.1f;  // 用于控制從一個值變化到另一個的時間限制  
  8.     private Animator anim;                
  9.     private DoneHashIDs hash;           // 儲存各種動畫狀态的hash  
  10.     void Awake ()  
  11.     {  
  12.         anim = GetComponent<Animator>();  
  13.         hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();  
  14.         // Set the weight of the shouting layer to 1.  
  15.         anim.SetLayerWeight(1, 1f);  
  16.     }  
  17.     void FixedUpdate ()  
  18.     {  
  19.         // Cache the inputs.  
  20.         float h = Input.GetAxis("Horizontal");  
  21.         float v = Input.GetAxis("Vertical");  
  22.         bool sneak = Input.GetButton("Sneak");  
  23.         MovementManagement(h, v, sneak);  
  24.     }  
  25.     void Update ()  
  26.     {  
  27.         // Cache the attention attracting input.  
  28.         bool shout = Input.GetButtonDown("Attract");  
  29.         // Set the animator shouting parameter.  
  30.         anim.SetBool(hash.shoutingBool, shout);  
  31.         AudioManagement(shout);  
  32.     }  
  33.     void MovementManagement (float horizontal, float vertical, bool sneaking)  
  34.     {  
  35.         // Set the sneaking parameter to the sneak input.  
  36.         anim.SetBool(hash.sneakingBool, sneaking);  
  37.         // If there is some axis input...  
  38.         if(horizontal != 0f || vertical != 0f)  
  39.         {  
  40.             // ... set the players rotation and set the speed parameter to 5.5f.  
  41.             Rotating(horizontal, vertical);  
  42.             anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);  
  43.         }  
  44.         else  
  45.             // Otherwise set the speed parameter to 0.  
  46.             anim.SetFloat(hash.speedFloat, 0);  
  47.     }  
  48.     void Rotating (float horizontal, float vertical)  
  49.     {  
  50.         // Create a new vector of the horizontal and vertical inputs.  
  51.         Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);  
  52.         // Create a rotation based on this new vector assuming that up is the global y axis.  
  53.         Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);  
  54.         // Create a rotation that is an increment closer to the target rotation from the player's rotation.  
  55.         Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);  
  56.         // Change the players rotation to this new rotation.  
  57.         rigidbody.MoveRotation(newRotation);  
  58.     }  
  59.     void AudioManagement (bool shout)  
  60.     {  
  61.         // If the player is currently in the run state...  
  62.         if(anim.GetCurrentAnimatorStateInfo(0).nameHash == hash.locomotionState)  
  63.         {  
  64.             // ... and if the footsteps are not playing...  
  65.             if(!audio.isPlaying)  
  66.                 // ... play them.  
  67.                 audio.Play();  
  68.         }  
  69.         else  
  70.             // Otherwise stop the footsteps.  
  71.             audio.Stop();  
  72.         // If the shout input has been pressed...  
  73.         if(shout)  
  74.             // ... play the shouting clip where we are.  
  75.             AudioSource.PlayClipAtPoint(shoutingClip, transform.position);  
  76.     }  
  77. }  

這個腳本整體來說:

在Awake的時候,先找到腳本需要用到的Animator元件和DoneHashIds元件,然後緩存它們(在後面用到就不需要頻繁查找了,節省CPU)。

在FixedUpdate的時候,擷取玩家按下的移動鍵,并處理移動、角色朝向問題。

在Update的時候,擷取玩家是否按下“Attract”鍵,并确定是否播放動畫以及聲音。

OK,我們主要來看看MovementManagement這個方法:

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. void MovementManagement (float horizontal, float vertical, bool sneaking)  
  2. {  
  3.     // Set the sneaking parameter to the sneak input.  
  4.     anim.SetBool(hash.sneakingBool, sneaking);  
  5.     // If there is some axis input...  
  6.     if(horizontal != 0f || vertical != 0f)  
  7.     {  
  8.         // ... set the players rotation and set the speed parameter to 5.5f.  
  9.         Rotating(horizontal, vertical);  
  10.         anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);  
  11.     }  
  12.     else  
  13.         // Otherwise set the speed parameter to 0.  
  14.         anim.SetFloat(hash.speedFloat, 0);  
  15. }  

我們可以發現,除了Rotating方法,基本都是對anim做一些狀态設定的方法。那麼Rotating方法裡面是什麼呢?

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. void Rotating (float horizontal, float vertical)  
  2. {  
  3.     // 建立一個Vector3用于儲存輸入的水準位移方向  
  4.     Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);  
  5.     // 根據上面的方向計算這個方向指向的角度  
  6.     Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);  
  7.     // 建立一個從玩家目前方向旋轉到目标方向的旋轉增量  
  8.     Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);  
  9.     // 修改玩家的方向  
  10.     rigidbody.MoveRotation(newRotation);  
  11. }  

上面代碼其實看出,他也沒有實作玩家的位置移動。那是在其它元件裡面實作的還是什麼其它方式處理的呢?且聽下回分解。

---------------------------------------------------------------------------------------------------------------------

好的,這次距離上次時間有點久,本來說一周更新一次,但實在沒辦法。在公司用戶端、服務端都要搞,而且戰鬥系統重新重構優化,也花了不少時間。望見諒。

我們接上次的話題:代碼沒有實作玩家的位置移動的代碼,但是實際運作的時候卻可以看到角色會移動。這是為什麼?

我們一起看到“Hierarchy”視圖中的“char_ethan”。注意裡面的Animator元件(它勾選了“Apply Root Motion”):

手遊開發初探

我們看到官方文檔對于這個選項是這麼介紹的:

Root motion is the effect where an object's entire mesh moves away from its starting point but that motion is created by the animation itself rather than by changing the Transform position. 

這裡大緻的意思其實就是:這個選項會在由動畫産生移動時,使得物體的mesh會偏離起點。

那也就是物體的移動是做在動畫中的,然後勾選Animator的ApplyRootMotion選項來實作的。

關于角色控制中anim.SetFloat的方法的。我們在這裡暫時不講。這個涉及到動畫混合,以後會專門講。

那麼角色的移動、旋轉就都講了。接下來我們看看碰撞器。

4、碰撞器及其使用

前面我們說了碰撞器分靜态和動态兩種。

那麼角色“char_ethan”身上的肯定就是動态碰撞器了,注意到“char_ethan”對象有個“Capsule Collider”元件。這就是一個膠囊體碰撞器元件。然後角色還有個“rigidbody”元件,這個是剛體元件。也就是采用前面說的普通碰撞器+剛體元件。

關于各種内置碰撞器的知識。大家不了解的可以看看官網http://docs.unity3d.com/Manual/Physics3DReference.html(我不會告訴你我大部分U3D的知識都在官網學習的。)

這裡隻要知道碰撞器一個重要的選項“Is Trigger”

手遊開發初探

隻要兩個互相碰撞的碰撞器,有任何一個勾選了IsTrigger,那麼就會觸發這兩個碰撞器上面的OnTriggerEnter方法。

如果兩個互相碰撞的碰撞器,都沒勾選IsTrigger,那麼會觸發這兩個碰撞器上面的OnCollisionEnter,OnCollisionStay,OnCollisionExit方法。

隻有在不勾選IsTrigger的時候,才能使用Physics.Raycast進行射線檢測。

是以當你的碰撞方法沒有調用的時候,請確定IsTrigger和方法是符合的。

注意:這個遊戲中玩家走路的時候不會穿牆,因為牆的碰撞器和玩家身上的碰撞器都沒有勾選IsTrigger。

5、怪物AI

在“Hierarchy”視圖中找到三個怪物“char_robotGuard_001”、“char_robotGuard_002”、“char_robotGuard_003”,這三個怪物上面的元件其實都是一樣的。我們來看看“char_robotGuard_001”就可以了。看看怪物是怎麼巡邏、然後發現敵人進行追蹤的。

怪物身上有如下元件:

手遊開發初探

A、Animator

這裡不再贅述。

B、碰撞器

兩個碰撞器?為什麼這裡要用兩個碰撞器?我們先看看兩個碰撞器的屬性:

手遊開發初探

一個是膠囊體,沒有勾選IsTrigger。這個應該是配合走路,為了不讓角色穿過建築物的。

第二個是球體碰撞器,勾選了IsTrigger。幹嘛用的,别急,我們還是先來看那4個腳本吧。

C、DoneEnemySight

二話不說我們先上代碼

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. using UnityEngine;  
  2. using System.Collections;  
  3. public class DoneEnemySight : MonoBehaviour  
  4. {  
  5.     public float fieldOfViewAngle = 110f;               // Number of degrees, centred on forward, for the enemy see.  
  6.     public bool playerInSight;                          // Whether or not the player is currently sighted.  
  7.     public Vector3 personalLastSighting;                // Last place this enemy spotted the player.  
  8.     private NavMeshAgent nav;                           // Reference to the NavMeshAgent component.  
  9.     private SphereCollider col;                         // Reference to the sphere collider trigger component.  
  10.     private Animator anim;                              // Reference to the Animator.  
  11.     private DoneLastPlayerSighting lastPlayerSighting;  // Reference to last global sighting of the player.  
  12.     private GameObject player;                          // Reference to the player.  
  13.     private Animator playerAnim;                        // Reference to the player's animator component.  
  14.     private DonePlayerHealth playerHealth;              // Reference to the player's health script.  
  15.     private DoneHashIDs hash;                           // Reference to the HashIDs.  
  16.     private Vector3 previousSighting;                   // Where the player was sighted last frame.  
  17.     void Awake ()  
  18.     {  
  19.         // Setting up the references.  
  20.         nav = GetComponent<NavMeshAgent>();  
  21.         col = GetComponent<SphereCollider>();  
  22.         anim = GetComponent<Animator>();  
  23.         lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();  
  24.         player = GameObject.FindGameObjectWithTag(DoneTags.player);  
  25.         playerAnim = player.GetComponent<Animator>();  
  26.         playerHealth = player.GetComponent<DonePlayerHealth>();  
  27.         hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();  
  28.         // Set the personal sighting and the previous sighting to the reset position.  
  29.         personalLastSighting = lastPlayerSighting.resetPosition;  
  30.         previousSighting = lastPlayerSighting.resetPosition;  
  31.     }  
  32.     void Update ()  
  33.     {  
  34.         // If the last global sighting of the player has changed...  
  35.         if(lastPlayerSighting.position != previousSighting)  
  36.             // ... then update the personal sighting to be the same as the global sighting.  
  37.             personalLastSighting = lastPlayerSighting.position;  
  38.         // Set the previous sighting to the be the sighting from this frame.  
  39.         previousSighting = lastPlayerSighting.position;  
  40.         // If the player is alive...  
  41.         if(playerHealth.health > 0f)  
  42.             // ... set the animator parameter to whether the player is in sight or not.  
  43.             anim.SetBool(hash.playerInSightBool, playerInSight);  
  44.         else  
  45.             // ... set the animator parameter to false.  
  46.             anim.SetBool(hash.playerInSightBool, false);  
  47.     }  
  48.     void OnTriggerStay (Collider other)  
  49.     {  
  50.         // If the player has entered the trigger sphere...  
  51.         if(other.gameObject == player)  
  52.         {  
  53.             // By default the player is not in sight.  
  54.             playerInSight = false;  
  55.             // Create a vector from the enemy to the player and store the angle between it and forward.  
  56.             Vector3 direction = other.transform.position - transform.position;  
  57.             float angle = Vector3.Angle(direction, transform.forward);  
  58.             // If the angle between forward and where the player is, is less than half the angle of view...  
  59.             if(angle < fieldOfViewAngle * 0.5f)  
  60.             {  
  61.                 RaycastHit hit;  
  62.                 // ... and if a raycast towards the player hits something...  
  63.                 if(Physics.Raycast(transform.position + transform.up, direction.normalized, out hit, col.radius))  
  64.                 {  
  65.                     // ... and if the raycast hits the player...  
  66.                     if(hit.collider.gameObject == player)  
  67.                     {  
  68.                         // ... the player is in sight.  
  69.                         playerInSight = true;  
  70.                         // Set the last global sighting is the players current position.  
  71.                         lastPlayerSighting.position = player.transform.position;  
  72.                     }  
  73.                 }  
  74.             }  
  75.             // Store the name hashes of the current states.  
  76.             int playerLayerZeroStateHash = playerAnim.GetCurrentAnimatorStateInfo(0).nameHash;  
  77.             int playerLayerOneStateHash = playerAnim.GetCurrentAnimatorStateInfo(1).nameHash;  
  78.             // If the player is running or is attracting attention...  
  79.             if(playerLayerZeroStateHash == hash.locomotionState || playerLayerOneStateHash == hash.shoutState)  
  80.             {  
  81.                 // ... and if the player is within hearing range...  
  82.                 if(CalculatePathLength(player.transform.position) <= col.radius)  
  83.                     // ... set the last personal sighting of the player to the player's current position.  
  84.                     personalLastSighting = player.transform.position;  
  85.             }  
  86.         }  
  87.     }  
  88.     void OnTriggerExit (Collider other)  
  89.     {  
  90.         // If the player leaves the trigger zone...  
  91.         if(other.gameObject == player)  
  92.             // ... the player is not in sight.  
  93.             playerInSight = false;  
  94.     }  
  95.     float CalculatePathLength (Vector3 targetPosition)  
  96.     {  
  97.         // Create a path and set it based on a target position.  
  98.         NavMeshPath path = new NavMeshPath();  
  99.         if(nav.enabled)  
  100.             nav.CalculatePath(targetPosition, path);  
  101.         // Create an array of points which is the length of the number of corners in the path + 2.  
  102.         Vector3 [] allWayPoints = new Vector3[path.corners.Length + 2];  
  103.         // The first point is the enemy's position.  
  104.         allWayPoints[0] = transform.position;  
  105.         // The last point is the target position.  
  106.         allWayPoints[allWayPoints.Length - 1] = targetPosition;  
  107.         // The points inbetween are the corners of the path.  
  108.         for(int i = 0; i < path.corners.Length; i++)  
  109.         {  
  110.             allWayPoints[i + 1] = path.corners[i];  
  111.         }  
  112.         // Create a float to store the path length that is by default 0.  
  113.         float pathLength = 0;  
  114.         // Increment the path length by an amount equal to the distance between each waypoint and the next.  
  115.         for(int i = 0; i < allWayPoints.Length - 1; i++)  
  116.         {  
  117.             pathLength += Vector3.Distance(allWayPoints[i], allWayPoints[i + 1]);  
  118.         }  
  119.         return pathLength;  
  120.     }  
  121. }  
Awake 主要是擷取一些元件,在後面的邏輯中可以使用。
Update

檢查玩家最後一次被發現的位置是否一緻,不是則同步位置。

把playerInSight(是否發現玩家)這個狀态設定到Animator的狀态機裡面。

OnTriggerStay

沒錯,前面說到的第二個碰撞器正是這裡要用到的。第二個碰撞器勾選了IsTrigger,就是為了觸發這個方法。

當玩家進入這個碰撞器(也就是怪物的視野)觸發。

檢查玩家是否在怪物前方,而且玩家和怪物中間沒有遮擋物。那麼設定playerInSight為true。

OnTriggerExit 當玩家離開怪物的視野(碰撞器)。那麼設定playerInSight為false。
CalculatePathLength 計算怪物和玩家目前的距離。

基本這個元件就是用于檢測玩家是否在怪物視野内。然後設定玩家的一些位置資訊,以及是否被發現的資訊。

我們接着看DoneEnemyAI這個元件

D、DoneEnemyAI

還是上代碼:

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. using UnityEngine;  
  2. using System.Collections;  
  3. public class DoneEnemyAI : MonoBehaviour  
  4. {  
  5.     public float patrolSpeed = 2f;                          // The nav mesh agent's speed when patrolling.  
  6.     public float chaseSpeed = 5f;                           // The nav mesh agent's speed when chasing.  
  7.     public float chaseWaitTime = 5f;                        // The amount of time to wait when the last sighting is reached.  
  8.     public float patrolWaitTime = 1f;                       // The amount of time to wait when the patrol way point is reached.  
  9.     public Transform[] patrolWayPoints;                     // An array of transforms for the patrol route.  
  10.     private DoneEnemySight enemySight;                      // Reference to the EnemySight script.  
  11.     private NavMeshAgent nav;                               // Reference to the nav mesh agent.  
  12.     private Transform player;                               // Reference to the player's transform.  
  13.     private DonePlayerHealth playerHealth;                  // Reference to the PlayerHealth script.  
  14.     private DoneLastPlayerSighting lastPlayerSighting;      // Reference to the last global sighting of the player.  
  15.     private float chaseTimer;                               // A timer for the chaseWaitTime.  
  16.     private float patrolTimer;                              // A timer for the patrolWaitTime.  
  17.     private int wayPointIndex;                              // A counter for the way point array.  
  18.     void Awake ()  
  19.     {  
  20.         // Setting up the references.  
  21.         enemySight = GetComponent<DoneEnemySight>();  
  22.         nav = GetComponent<NavMeshAgent>();  
  23.         player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;  
  24.         playerHealth = player.GetComponent<DonePlayerHealth>();  
  25.         lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();  
  26.     }  
  27.     void Update ()  
  28.     {  
  29.         // If the player is in sight and is alive...  
  30.         if(enemySight.playerInSight && playerHealth.health > 0f)  
  31.             // ... shoot.  
  32.             Shooting();  
  33.         // If the player has been sighted and isn't dead...  
  34.         else if(enemySight.personalLastSighting != lastPlayerSighting.resetPosition && playerHealth.health > 0f)  
  35.             // ... chase.  
  36.             Chasing();  
  37.         // Otherwise...  
  38.         else  
  39.             // ... patrol.  
  40.             Patrolling();  
  41.     }  
  42.     void Shooting ()  
  43.     {  
  44.         // Stop the enemy where it is.  
  45.         nav.Stop();  
  46.     }  
  47.     void Chasing ()  
  48.     {  
  49.         // Create a vector from the enemy to the last sighting of the player.  
  50.         Vector3 sightingDeltaPos = enemySight.personalLastSighting - transform.position;  
  51.         // If the the last personal sighting of the player is not close...  
  52.         if(sightingDeltaPos.sqrMagnitude > 4f)  
  53.             // ... set the destination for the NavMeshAgent to the last personal sighting of the player.  
  54.             nav.destination = enemySight.personalLastSighting;  
  55.         // Set the appropriate speed for the NavMeshAgent.  
  56.         nav.speed = chaseSpeed;  
  57.         // If near the last personal sighting...  
  58.         if(nav.remainingDistance < nav.stoppingDistance)  
  59.         {  
  60.             // ... increment the timer.  
  61.             chaseTimer += Time.deltaTime;  
  62.             // If the timer exceeds the wait time...  
  63.             if(chaseTimer >= chaseWaitTime)  
  64.             {  
  65.                 // ... reset last global sighting, the last personal sighting and the timer.  
  66.                 lastPlayerSighting.position = lastPlayerSighting.resetPosition;  
  67.                 enemySight.personalLastSighting = lastPlayerSighting.resetPosition;  
  68.                 chaseTimer = 0f;  
  69.             }  
  70.         }  
  71.         else  
  72.             // If not near the last sighting personal sighting of the player, reset the timer.  
  73.             chaseTimer = 0f;  
  74.     }  
  75.     void Patrolling ()  
  76.     {  
  77.         // Set an appropriate speed for the NavMeshAgent.  
  78.         nav.speed = patrolSpeed;  
  79.         // If near the next waypoint or there is no destination...  
  80.         if(nav.destination == lastPlayerSighting.resetPosition || nav.remainingDistance < nav.stoppingDistance)  
  81.         {  
  82.             // ... increment the timer.  
  83.             patrolTimer += Time.deltaTime;  
  84.             // If the timer exceeds the wait time...  
  85.             if(patrolTimer >= patrolWaitTime)  
  86.             {  
  87.                 // ... increment the wayPointIndex.  
  88.                 if(wayPointIndex == patrolWayPoints.Length - 1)  
  89.                     wayPointIndex = 0;  
  90.                 else  
  91.                     wayPointIndex++;  
  92.                 // Reset the timer.  
  93.                 patrolTimer = 0;  
  94.             }  
  95.         }  
  96.         else  
  97.             // If not near a destination, reset the timer.  
  98.             patrolTimer = 0;  
  99.         // Set the destination to the patrolWayPoint.  
  100.         nav.destination = patrolWayPoints[wayPointIndex].position;  
  101.     }  
  102. }  

這個元件主要是:如果判斷玩家在視野中,則對玩家進行射擊。如果玩家最後一次被發現的點不是預設點,則進行追擊。否則就進行巡邏。

追擊、巡邏,主要都是對NavMeshAgent進行設定來實作控制怪物的移動。關于NavMeshAgent我們以後再專門來講解。

而射擊調用的Shooting方法裡面隻有一行代碼:nav.Stop。那麼射擊具體在哪裡實作的?接着看DoneEnemyShooting元件。

E、DoneEnemyShooting

你知道我肯定會說,看代碼。。。沒錯:

[csharp]  view plain copy

手遊開發初探
手遊開發初探
  1. using UnityEngine;  
  2. using System.Collections;  
  3. public class DoneEnemyShooting : MonoBehaviour  
  4. {  
  5.     public float maximumDamage = 120f;                  // The maximum potential damage per shot.  
  6.     public float minimumDamage = 45f;                   // The minimum potential damage per shot.  
  7.     public AudioClip shotClip;                          // An audio clip to play when a shot happens.  
  8.     public float flashIntensity = 3f;                   // The intensity of the light when the shot happens.  
  9.     public float fadeSpeed = 10f;                       // How fast the light will fade after the shot.  
  10.     private Animator anim;                              // Reference to the animator.  
  11.     private DoneHashIDs hash;                           // Reference to the HashIDs script.  
  12.     private LineRenderer laserShotLine;                 // Reference to the laser shot line renderer.  
  13.     private Light laserShotLight;                       // Reference to the laser shot light.  
  14.     private SphereCollider col;                         // Reference to the sphere collider.  
  15.     private Transform player;                           // Reference to the player's transform.  
  16.     private DonePlayerHealth playerHealth;              // Reference to the player's health.  
  17.     private bool shooting;                              // A bool to say whether or not the enemy is currently shooting.  
  18.     private float scaledDamage;                         // Amount of damage that is scaled by the distance from the player.  
  19.     void Awake ()  
  20.     {  
  21.         // Setting up the references.  
  22.         anim = GetComponent<Animator>();  
  23.         laserShotLine = GetComponentInChildren<LineRenderer>();  
  24.         laserShotLight = laserShotLine.gameObject.light;  
  25.         col = GetComponent<SphereCollider>();  
  26.         player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;  
  27.         playerHealth = player.gameObject.GetComponent<DonePlayerHealth>();  
  28.         hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();  
  29.         // The line renderer and light are off to start.  
  30.         laserShotLine.enabled = false;  
  31.         laserShotLight.intensity = 0f;  
  32.         // The scaledDamage is the difference between the maximum and the minimum damage.  
  33.         scaledDamage = maximumDamage - minimumDamage;  
  34.     }  
  35.     void Update ()  
  36.     {  
  37.         // Cache the current value of the shot curve.  
  38.         float shot = anim.GetFloat(hash.shotFloat);  
  39.         // If the shot curve is peaking and the enemy is not currently shooting...  
  40.         if(shot > 0.5f && !shooting)  
  41.             // ... shoot  
  42.             Shoot();  
  43.         // If the shot curve is no longer peaking...  
  44.         if(shot < 0.5f)  
  45.         {  
  46.             // ... the enemy is no longer shooting and disable the line renderer.  
  47.             shooting = false;  
  48.             laserShotLine.enabled = false;  
  49.         }  
  50.         // Fade the light out.  
  51.         laserShotLight.intensity = Mathf.Lerp(laserShotLight.intensity, 0f, fadeSpeed * Time.deltaTime);  
  52.     }  
  53.     void OnAnimatorIK (int layerIndex)  
  54.     {  
  55.         // Cache the current value of the AimWeight curve.  
  56.         float aimWeight = anim.GetFloat(hash.aimWeightFloat);  
  57.         // Set the IK position of the right hand to the player's centre.  
  58.         anim.SetIKPosition(AvatarIKGoal.RightHand, player.position + Vector3.up * 1.5f);  
  59.         // Set the weight of the IK compared to animation to that of the curve.  
  60.         anim.SetIKPositionWeight(AvatarIKGoal.RightHand, aimWeight);  
  61.     }  
  62.     void Shoot ()  
  63.     {  
  64.         // The enemy is shooting.  
  65.         shooting = true;  
  66.         // The fractional distance from the player, 1 is next to the player, 0 is the player is at the extent of the sphere collider.  
  67.         float fractionalDistance = (col.radius - Vector3.Distance(transform.position, player.position)) / col.radius;  
  68.         // The damage is the scaled damage, scaled by the fractional distance, plus the minimum damage.  
  69.         float damage = scaledDamage * fractionalDistance + minimumDamage;  
  70.         // The player takes damage.  
  71.         playerHealth.TakeDamage(damage);  
  72.         // Display the shot effects.  
  73.         ShotEffects();  
  74.     }  
  75.     void ShotEffects ()  
  76.     {  
  77.         // Set the initial position of the line renderer to the position of the muzzle.  
  78.         laserShotLine.SetPosition(0, laserShotLine.transform.position);  
  79.         // Set the end position of the player's centre of mass.  
  80.         laserShotLine.SetPosition(1, player.position + Vector3.up * 1.5f);  
  81.         // Turn on the line renderer.  
  82.         laserShotLine.enabled = true;  
  83.         // Make the light flash.  
  84.         laserShotLight.intensity = flashIntensity;  
  85.         // Play the gun shot clip at the position of the muzzle flare.  
  86.         AudioSource.PlayClipAtPoint(shotClip, laserShotLight.transform.position);  
  87.     }  
  88. }  

Update的時候可以發現它檢查動畫的shotFloat屬性。然後判斷是否應該射擊或取消射擊。

而射擊的雷射,這裡可以看到是采用LineRenderer實作的。

6、事件通知

以上關于怪物的AI我們就講完了。但這隻是單個怪物的AI。實際這個遊戲裡面還有用于監控玩家的攝像頭、還有紅外線牆檢測玩家是否通過。但是原理都一樣。都是通過監聽碰撞器的OnTriggerStay來設定玩家被發現的全局位置。也就是DoneLastPlayerSighting。

個人關于這個demo代碼設計的看法:

我個人覺得這種做法不好。大家都自己去擷取DoneLastPlayerSighting,然後自己檢查裡面的狀态。然後還可以設定裡面的狀态。這樣會導緻程式變量跟蹤困難。

如果是我的話,我會把DoneLastPlayerSighting做成是一個消息中心。然後每個敵人訂閱這個消息中心的消息(即玩家被發現的位置發生變化的消息),然後由消息中心通知所有訂閱的對象。然後訂閱的對象也可以送出自己發現的目标給消息中心,如果消息中心确認通過,再通知已經訂閱的其它對象。這樣做起來代碼會更清晰,更容易維護。

下一篇我會專門來講關于這部分代碼的一些優化。

7、渲染特效

A、霧效

你可以通過點選Unity的菜單欄“Edit”-“Render Settings”。這時候在右邊的Inspector面闆可以通過勾選“fog”選項來開啟霧效。“Fog Color”可以調整霧的顔色。

B、遊戲中螢幕投射到地上的亮點

在“Hiererchy”中找到螢幕物體“prop_cctvCam_001”,你會發現它下面有個子物體“cam_frustum_collision”,它上面有個Light元件,Light上的Cookie屬性就是産生亮點的貼圖。

C、雷射牆的雷射

其實就是用了一個"Self-Illumin/Diffuse"的shader,這是一個自發光shader,你可以嘗試一下,在相同的燈光條件下,采用自發光shader的物體總會比Diffuse的亮。

繼續閱讀