【Iphone 遊戲開發】遊戲引擎剖析

 李華明Himi 原創,轉載務必在明顯處註明:
轉載自【黑米GameDev街區】 原文連接: http://www.himigame.com/iphone-object/406.html
css


爲了解決「如何在IPHONE上建立一個遊戲」這個大問題,咱們須要首先解決諸如「如何顯示圖像」與「如何播放聲音」等一系列小問題。這些問題關係到建立部分遊戲引擎。就像人類的身體同樣,遊戲引擎的每一個部分雖然不一樣,可是卻都不可或缺。所以,首先從遊戲引擎剖析開始本章。咱們將會討論一個遊戲引擎的全部主要部分,包括應用程序框架、狀態機、圖像引擎、物理引擎、聲音引擎、玩家輸入和遊戲邏輯。html

    寫一個好玩的遊戲是一項牽扯到不少代碼的大任務。很是有必要從一開始就對項目進行良好的,有組織的設計,而不是隨着進度的進行而處處雜亂添加代碼。就像建造房屋同樣,建築師爲整幢房屋勾畫藍圖,建築工人以此來建造。可是,許多對遊戲編程不熟悉的編程人員會從根據導讀建造出房屋的一部分,並隨着學習的進行爲其添加房間,這無疑將會致使很差的結果。程序員

 圖2-1 遊戲引擎的功能結構編程

 

    圖2-1顯示了一個適用於大部分遊戲的遊戲引擎結構。爲了理解一個遊戲引擎的全部部分和它們是如何工做在一塊兒的,咱們能夠先爲整個遊戲作設計,而後再建立咱們的應用程序。在如下的幾個小節中,咱們的講解內容將會涵蓋圖2-1的每一個部分。緩存

  • 應用程序框架
  • 遊戲狀態管理器
  • 圖像引擎

    應用程序框架

     應用程序框架包含使應用程序工做的必須代碼,包括建立一個應用程序實例和初期化其餘子系統。當應用程序運行時,會首先建立一個框架類,並接管建立和銷燬狀態機、圖像引擎和聲音引擎。若是咱們的遊戲足夠複雜以致於它須要一個物理引擎,框架也會管理它。安全

    框架必須適應於咱們所選擇的平臺的獨特性,包括相應任何的系統事件(如關機與睡眠),以及管理載入與載出資源以使其餘的代碼只須要集中與遊戲。併發

   主循環

    框架會提供主循環,它是一切互動程序後的驅動力量。在循環中的每一次迭代過程當中,程序會檢查和處理接受到的事件,運行遊戲邏輯中的更新並在必要時將內容描畫到屏幕上。(參見圖2-2)app

  圖2-2 主循環序列框架

 

    主循環如何實現依賴於你使用的系統。對於一個基本的控制檯程序,它多是一個簡單的while循環中調用各個函數:iphone

 

  1. while( !finished ) {  
  2.     handle_events();  
  3.     update();  
  4.     render();  
  5.     sleep(20);  
  6. }  

 

    注意到這裏的sleep函數。它使得代碼休眠一小段時間不致於佔用所有的CPU。

    有些系統徹底不想讓用戶代碼那些寫,它們使用了回調系統以強制程序員常規的釋放CPU。這樣,當應用程序執行後,程序員註冊一些函數給系統在每次循環中回調:

 

  1. void main(void) {  
  2.     OS_register_event_handler( myEventHandler );  
  3.     OS_register_update_function( myUpdate );  
  4.     OS_register_render_function( myRender );  
  5. }  

 

    一旦程序執行後,根據必要狀況,那些函數會間隔性的被調用。IPHONE是最接近後面這個例子。你能夠在下一章和IPHONE SDK中看到它。

   遊戲狀態管理器

    一個好的視頻遊戲不只有一組動做來維持遊戲:它會提供一個主菜單容許玩家來設定選項和開始一個新遊戲或者繼續上次的遊戲;製做羣屏將會顯示全部辛勤製做這款遊戲的人員的名字;並且若是你的遊戲沒有用戶指南,應該一個幫助區域會給用戶一些提示告訴他們應該作什麼。

    以上任何一種場合都是一種遊戲狀態,而且表明中一段獨立的應用程序代碼片斷。例如,用戶在主菜單調用的函數與導航與用戶在製做羣屏調用的是徹底不一樣的,因此程序邏輯也是不一樣的。特別的是,在主菜單,你可能會放一張圖片和一些菜單,而且等待用戶選擇哪一個選項,而在製做羣屏,你將會把遊戲製做人員的名字描繪在屏幕上,而且等待用戶輸入,將遊戲狀態從製做羣屏改成主菜單。最後,在遊戲中狀態,將會渲染實際的遊戲並等待用戶的輸入以與遊戲邏輯進行交互。

    以上的全部遊戲狀態都負責相應用戶輸入、將內容渲染到屏幕、併爲該遊戲狀態提供相對應的應用程序邏輯的任務。你可能注意到了這些任務都來自於以前討論的主循環中,這是由於它們就是一樣的任務。可是,每一個狀態都會以它們本身的方式來實現這些任務,這也就是爲何要保持他們獨立。你沒必要在主菜單代碼中尋找處理遊戲中的事件的代碼。

   狀態機

    狀態管理器是一個狀態機,這意味着它跟蹤着如今的遊戲狀態。當應用程序執行後,狀態機會建立基本的狀態信息。它接着建立各類狀態須要的信息,並在離開每種狀態時銷燬暫時存儲的信息。

    狀態機維護着大量不一樣對象的狀態。一個明顯的狀態是用戶所在屏幕的狀態(主菜單、遊戲中等)。可是若是你有一個有着人工智能的對象在屏幕上時,狀態機也能夠用來管理它的「睡眠」、「攻擊」、「死亡」狀態。

    什麼是正確的遊戲狀態管理器結構?讓咱們看看一些狀態機並決定哪一種最適合咱們。

    有許多實現狀態機的方式,最基本的是一個簡單的switch語句: 

  1. class StateManager {  
  2.     void main_loop() {  
  3.         switch(myState) {  
  4.         case STATE_01:  
  5.             state01_handle_event();  
  6.             state01_update();  
  7.             state01_render;  
  8.             break;  
  9.         case STATE_02:  
  10.             state02_handle_event();  
  11.             state02_update();  
  12.             state02_render;  
  13.             break;  
  14.         case STATE_03:  
  15.             state03_handle_event();  
  16.             state03_update();  
  17.             state03_render;  
  18.             break;  
  19.         }  
  20.     }  
  21. };  

 

    改變狀態時全部須要作的事情就是改變myState變量的值並返回到循環的開始處。可是,正如你看到的,當咱們加入愈來愈多的狀態時,代碼塊會變得愈來愈大。並且更糟的是,爲了使程序按咱們預期的執行,咱們須要在程序進入或離開某個狀態時執行整個任務塊,初始化該狀態特定的變量,載入新的資源(好比圖片)和釋放前一個狀態載入的資源。在這個簡單的switch語句中,咱們須要加入更多的程序塊並保證不會漏掉任何一個。

    以上是一些簡單重複的勞動,可是咱們的狀態管理器須要更好的解決方案。下面一種更好的實現方式是使用函數指針:

 

  1. class StateManager {  
  2.     //the function pointer:  
  3.     void (*m_stateHandleEventFPTR) (void);  
  4.     void (*m_stateUpdateFPTR)(void);  
  5.     void (*m_stateRenderFPTR)(void);  
  6.     void main_loop() {  
  7.         stateHandleEventFPTR();  
  8.         m_stateUpdateFPTR();  
  9.         m_stateRenderFPTR();  
  10.     }  
  11.     void change_state(  void (*newHandleEventFPTR)(void),  
  12.                     void (*newUpdateFPTR)(void),  
  13.                     void (*newRenderFPTR)(void)  
  14.     ) {  
  15.         m_stateHandleEventFPTR = newHandleEventFPTR;  
  16.         m_stateUpdateFPTR = newUpdateFPTR;  
  17.         m_stateRenderFPTR = newRenderFPTR  
  18.     }  
  19. };  

 

    如今,即便咱們處理再多狀態,主循環也足夠小並且簡單。可是,這種解決方案依然不能幫助咱們很好的解決初始化與釋放狀態。由於每種遊戲狀態不只包含代碼,還有各自的資源,因此更恰當的作法是將遊戲狀態做爲對象的屬性來考慮。所以,接下來,咱們將會看看面向對象(OOP)的實現。

    咱們首先建立一個表示遊戲狀態的類:

 

  1. class GameState  
  2. {  
  3.     GameState();        //constructor  
  4.     virtual ~GameState();    //destructor  
  5.     virtual void Handle_Event();  
  6.     virtual void Update();  
  7.     virtual void Render();  
  8. };  

 

    接着,咱們改變咱們的狀態管理器以使用這個類:

 

  1. class StateManager {  
  2.     GameState* m_state;  
  3.     void main_loop() {  
  4.         m_state->Handle_Event();  
  5.         m_state->Update();  
  6.         m_state->Render();  
  7.     }  
  8.     void change_state( GameState* newState ) {  
  9.         delete m_state;  
  10.         m_state = newState;  
  11.     }  
  12. };  

 

    最後,咱們建立一個指定具體遊戲狀態的類:

 

  1. class State_MainMenu : public GameState  
  2. {  
  3.     int m_currMenuOption;  
  4.     State_MainMenu();  
  5.     ~State_MainMenu();  
  6.     void Handle_Event();  
  7.     void Update();  
  8.     void Render();  
  9. };  

 

    當遊戲狀態以類來表示時,每一個遊戲狀態均可以存儲它特有的變量在該類中。該類也能夠它的構造函數中載入任何資源並在析構函數中釋放這些資源。

    並且,這個系統保持着咱們的代碼有很好的組織結構,由於咱們須要將遊戲狀態代碼分別放在各個文件中。若是你在查找主菜單代碼,你只須要打開State_MainMenu類。並且OOP解決方案使得代碼更容易重用。

    這個看起來是最適合咱們須要的,因此咱們決定使用它來做爲咱們的狀態管理器。

   圖像引擎

    圖像引擎負責視覺輸出,包括用戶藉以交互的圖形用戶界面(GUI)對象,2D精靈動畫或3D模型動畫,並渲染的背景與特效。

    雖然渲染2D與3D圖片的技術不盡相同,但他們都完成相同的一組圖形任務,包括紋理和動畫,它們的複雜度是遞增的。

    紋理

    對於顯示圖片,紋理是中心。2D時,一個平面圖片是以像素爲單位顯示在屏幕上,而在3D時,一組三角行(或者被稱爲網格)在數學魔法做用下產平生面圖片並顯示在屏幕上。這之後,一切都變得複雜。

    像素、紋理與圖片

    當進行屏幕描繪時,基本單位是像素。每一個像素均可以被分解爲紅、綠、藍色值和咱們立刻要討論的Alpha值。

    紋理是一組關於渲染一組像素的數據。它包含每一個像素的顏色數據。

    圖片是一個更高層的概念,它並不是與一組特殊的像素與紋理相關聯。當一我的看到一組像素,他的大腦會將它們組合成一幅圖片,例如,若是像素以正確的順序表示,他可能會看到一幅長頸鹿的畫像。

    保持以上這些概念獨立是很是必要的。紋理可能包含構成長頸鹿圖片的像素:它可能包含足夠的像素來構成一隻長頸鹿的多幅圖片,或者僅包含構成一幅長頸鹿圖片的像素。紋理自己只是一組像素的集合,它並不固有的知道它包含的是一幅圖片。

    透明度

    在任一刻,你的遊戲會有幾個或者多個物體渲染在屏幕上,其中一些會與另一個重疊。問題是,如何知道哪一個物體的哪一個像素應該被渲染出來呢?

    若是在最上層的紋理(在其餘紋理以後被描畫)是徹底不透明的,它的像素將會被顯示。可是,對於遊戲物體,多是非矩形圖形和部分透明物體,結果會致使兩種紋理的結合。

    2D圖片中最經常使用的混合方式是徹底透明。假如咱們想畫一幅考拉(圖2-3)在爬在桉樹頂上(圖2-4)的的圖片。考拉的圖片以矩形紋理的方式存儲在內存中,可是咱們不想畫出整個矩形,咱們只想畫出考拉身體的像素。咱們必須決定紋理中的每一個像素是否應該顯示。

    圖2-3 考拉紋理

 

 

 

圖2-4 桉樹紋理

 

    有些圖片系統經過添加一層遮罩來達到目的。想象咱們在內存中有一幅與考拉紋理大小同樣的另一份紋理,它只包含白色和黑色的像素。遮罩中每一個白色的像素表明考拉應該被描畫出來的一個像素,遮罩中的黑色像素則表明不該該被描畫的像素。若是當咱們將考拉描畫到屏幕上時,有這樣的一個遮罩層,咱們就能夠檢查考拉對應的像素並僅將須要描畫的像素表示出來。若是每一個像素有容許有一組範圍值而不是二進制黑/白值,那麼它還能夠支持部分透明(參見圖2-5)。

 

 圖2-5 考拉遮罩紋理

 

    紋理混合

    爲紋理而準備的存儲容量大到足以支持每一個像素都有一個範圍值。典型的是,一個Alpha值佔一個字節,即容許0-255之間的值。經過合併兩個像素能夠表現出有趣的視覺效果。這種效果一般用於部分透明化,例如部分或徹底看透物體(圖2-6)。

 

 

圖2-6 部分透明的綠色矩形

 

    咱們能夠爲每一個像素來設定Alpha以決定它們如何被混合。若是一個像素容許的範圍值爲0-255,Alpha的範圍值也一樣應當爲0-255。儘管紅色值爲0表示當描畫時不該該使用紅色,但Alpha值爲0則表示該像素根本不該該被描畫。一樣,128的紅色值表示描畫時應該使用最大紅色值的一半,128的Alpha值表示當與另一個像素混合時,應該使用該像素的一半顏色值。

 

    當混合物體時,正確的排列物體順序是很是重要的。由於每一個混合渲染動做都只會渲染源物體與目標物體,首先被描畫的物體不會與後描畫的物體發生混合。儘管這在2D圖片中很容易控制,可是在3D圖片中變得很是複雜。

 

    旋轉
   

    在2D圖片中,大部分的紋理都會被直接渲染到目標上而不須要旋轉。這是由於標準硬件不具有旋轉功能,因此旋轉必須在軟件中計算完成。這是一個很慢的過程,並且容易產商低質量的圖片。

 

    一般,遊戲開發人員會經過預先渲染好物體在各個方向的圖片,並當物體某個方向上時,爲其在屏幕上描畫正確的圖片來避免以上問題的發生。

 

    在3D圖片中,旋轉的計算方式與照明相同,是硬件渲染處理過程當中的一部分。

 

    剪貼

    因爲某些在後面章節解釋的緣由,紋理的另一個重要方面是剪貼。儘管咱們目前的例子都是將源紋理直接描畫到目標紋理上,可是常常會出現的狀況是,須要將部分源紋理描畫到目標紋理的有限的一部分上。

 

    例如,若是你的源紋理是在一個文件中含有多幅圖片,裁剪容許你僅將但願描畫的部分渲染出來。

 

    剪貼一樣容許你進行描畫限制到目標紋理的一小部分上。它能夠幫你經過紋理映射以3D方式渲染物體,將紋理鋪蓋到三角形組成的任意形狀的網格上。例如,一個紋理能夠表示衣服或動物的毛皮,並且當3D角色穿着它移動的死後可能產生褶皺。這時候的紋理一般被稱做皮膚。

 

    動畫

    經過渲染連續的圖片,咱們能夠確保玩家看到一個移動的物體,儘管他所作的僅僅是在一樣的像素上,但這些像素在快速的改變顏色。這就是動畫的基本概念。2D動畫很簡單,但3D動畫一般牽扯到更多的物體與動做,所以更復雜。

 

    除了討論動畫技巧,這一節還會討論主要的優化類型可使得咱們的圖像引擎有效的和可靠的完成複雜的不可能以原始方式來完成的圖形任務。一些主要的優化技巧包括淘汰、紋理排序、使用智能紋理文件、資源管理和細節級別渲染。

 

    2維動畫:精靈

    在2D圖像中,若是咱們要渲染馬兒奔馳的完整場景,咱們能夠先建立出馬兒的奔馳各個姿態的圖片。這種圖片成爲一幀。當一幀接一幀的渲染到屏幕上時,馬兒動起來了(見圖2-7)。這和電影建立動畫的方式很是類似,電影也是經過展現連續的幀來達到移動效果。

 

 

 

    圖2-7 斯坦福德的馬的動做

 

    爲了將這些幀保存在一塊兒,咱們將它們放在同一個紋理中,稱爲精靈。經過前面章節咱們描述的裁剪方法,將只包含當前幀內容的部分渲染到屏幕上。

 

    你能夠將每一幀渲染屢次直到渲染該序列的下一幀。這取決於你但願你的動畫播放的多快,以及提供了多少幀圖片。事實上,經過渲染的幀速和順序,你能夠創造出多種特效。

 

    3維動畫:模型

    與2D動畫中每次重畫時都維護一幅用來渲染的圖片--精靈不一樣,3D動畫是經過實際的計算的計算運動的幾何效果。正如咱們以前描述的,全部的3D物體都由包含一個或多個三角形構成,被稱做網格。有多種可使網格動起來的方法,這些技術與遊戲發展與圖形硬件有關。這些技術後的基本概念都是:關鍵幀。

    關鍵幀與咱們以前討論的2D動畫中的幀有些許不一樣。2維動畫的美術人員畫出每一幀並保存在紋理中。可是在3D中,只要咱們保存了最特殊的幾幀,咱們就能夠經過數學計算獲得其餘幀。

    最開始的使用網格動畫的遊戲實際上存儲了網格的多個拷貝,每個拷貝都是都在不一樣的關鍵幀方向上。例如,若是咱們在3D中渲染馬兒,咱們應該爲上面精靈的每個關鍵幀都建立網格。在time(1),第一幀被描畫出來,在time(2),第二針被描述出來。

    在主要關鍵幀之間,使用一種叫作「插值」的技術方法。由於咱們知道time(1)的關鍵幀和time(2)的關鍵幀有着相同數量的三角形,可是方向稍有區別,咱們能夠建立當前時間點的臨時的,融合了前面兩個網格的網格。因此在時間time(1.5),臨時網格看起來正好介於time(1)與time(2)之間,而在time(1.8),看起來更偏向於time(2)。

    以上技術效率低下的緣由是很明顯的。它僅在只有少許的三角形和少許的關鍵幀時纔是可接受的,可是現代圖像要求有高解析度與生動細節的動畫。幸運的是,有更好的存儲關鍵幀數據的方法。

    這就技術叫作「骨骼動畫」(skeletal animation, or bone rigging)。仍是以馬兒爲例,你可能注意到了大多數的三角形都是成組的移動,好比頭部組、尾部組和四肢組。若是你將它們都當作是骨頭關聯的,那麼將這些骨頭組合起來就造成了骨骼。

    骨骼是由一組能夠適用於網格的骨頭組成的。當一組骨骼在不一樣方向連續的表示出來的時候,就造成了動畫。每一幀動畫都使用的是相同的網格,可是都會有骨頭從前一方位移動到下一個方位的細小的動做變化。

    經過僅存儲在某一個方位的網格,而後在每一關鍵幀時都利用它,咱們能夠建立一個臨時的網格並將其渲染到屏幕上。經過在兩個關鍵幀之間插值,咱們能夠以更小的成原本建立相同的動畫。

    動畫控制器

    動畫控制器對象在抽象低層次的任務很是有用,如選擇哪一幀來渲染,渲染多長時間,決定下一幀代替前一幀等。它也起到鏈接遊戲邏輯與圖像引擎等動畫相關部分的做用。

   在頂層,遊戲邏輯只關心將設某些東西,如播放跑動的動畫,和設定它的速度爲可能應該每秒跑動數個單位距離。控制器對象知道哪一個幀序列對應的跑動動畫以及這些幀播放的速度,因此,遊戲邏輯沒必要知道這些。

    粒子系統

    另一個與動畫控制器類似的有用對象是粒子系統管理器。當須要描畫高度支離破碎的元素,如火焰、雲朵粒子、火苗尾巴等時可使用粒子系統。雖然粒子系統中的每一個對象都有有限的細節與動畫,它們組合起來卻能造成富有娛樂性的視覺效果。

    淘汰

    最好的增長每秒鐘描畫到屏幕上的次數的方法是在每次迭代中都減小描畫在屏幕上的數目的總量。你的場景可能同時擁有成百上千的物體,可是若是你只須要描述其中的一小部分,你仍然能夠將屏幕渲染得很快。

    淘汰是從描畫路徑上移除沒必要要的物體。你能夠在多層次上同時進行淘汰。例如,在一個高層次,一個用戶在一間關閉了門的房間裏面是看不到隔壁房間的物體的,因此你沒必要描畫出隔壁其餘物體。

    在一個低層次,3D圖像引擎會常常移除部分你讓它們描畫的網格。例如,在任意合適的給定時間點,半數的網格幾何體在攝影機背面,你從攝像機中看不到這些網格,看到的只是攝影機前方的網格,所以,當網格被渲染時,全部的在攝影機背後的網格都會被忽略。這叫作背面淘汰。

    紋理排序

    每次當一個物體被渲染到屏幕上時,圖形硬件都會將紋理源文件載入到內存中。這是被稱做上下文交換(context switching)的一部分。

    若是要將三幅圖片描畫到屏幕上,而其中兩幅圖片共用同一個紋理資源,有兩種辦法來處理紋理排序:高效的方法是連續的渲染兩幅共享資源的圖片,這樣只須要以此上下文交換,而低效的方法則須要兩次上下文交換。你不該該將第三幅圖片放在共享紋理的兩幅圖片之間描畫。

    在渲染處理過程當中,經過排列共享紋理的物體能夠減小上下文交換的次數,從而提升渲染速度。

    紋理文件

    在一開始就計劃好紋理組織結構能夠幫助你以最優化方式排列你的紋理。假設你準備在你的遊戲中描畫幾何體,一個主角和一些生物。

    若是前兩個關卡是草地,接下來的關卡是沙漠,你能夠將全部的樹木、草、灌木、岩石以及花兒的圖片來放到一塊兒來渲染前兩關,並將沙子圖片放在另一個紋理文件中用來渲染第三關。一樣的,你能夠將玩家虛擬人偶放到一個紋理中。若是全部的生物在全部關卡中都用到了,最優的方式多是將它們放在一個紋理文件中。可是,若是第一關有吼猴與鼯鼠,而第二關只有森林鼠與蘇里南蛤蟆,你能夠將第一次前兩種動物放在一個紋理中,將後兩種放在一個紋理中。

    資源管理

    大部分的視頻遊戲在一個時間點只會渲染它們全部圖片內容的一小部分。將全部紋理同時載入內存是很是低效的。

    幸運的是,遊戲設計一般決定了哪些資源在遊戲的各個章節是可見的。經過保留必須的紋理爲載入狀態並卸載不使用的紋理,能夠最有效的利用有限的內存資源。

    仍是使用前一節的例子,當遊戲引擎載入第一關時,資源管理代碼必須確保 吼猴與鼯鼠的紋理被載入到內存中。當程序進行到下一關時,資源管理代碼會卸載那些紋理,由於它已經知道它們不會在第二關被使用。

    細節層次

    另一個優化技巧,尤爲是對3D圖像,叫作細節層次。考慮當一個物體遠離攝像機時,它看起來很是小,並且大部分細節都丟失了。你能夠描畫一個一樣大小,卻僅擁有簡單網格的物體,或者甚至一張平面貼圖。

    經過保留不一樣細節層次的物體的副本在內存中,圖像引擎能夠根據與攝像機的距離決定使用哪一個副本。

   物理引擎

    物理引擎是遊戲引擎中負責距離、移動與其它遊戲物理相關的部分。不是全部的遊戲引擎都須要物理引擎。可是全部的圖形遊戲都在某種程度上有物理相關代碼。

    不相信嗎?用「井字遊戲」(tic-tac-toe)來舉 個例子。確實是一個很是簡單的遊戲,可是即便這個遊戲也有物理部分。當玩家選擇一個正方形用來標記的時候,咱們必須檢查選擇的正方形是否有效。若是是,咱們就將打上標記並判斷玩家是否獲勝。這就是物理引擎所完成的兩項基本任務的例子:偵測與解決。

    碰撞偵測與碰撞解決

    在你腦海中保持這兩方面的獨立性很是重要。在遊戲代碼中,偵測是獨立於斷定的。不是全部的物體與其它物體會以相同的方式發生碰撞,進而不是被偵測到的全部碰撞都會以相同的方式來解決。

    例如,讓咱們假想一個遊戲:O’Reilly野生冒險。假如玩家的虛擬人偶發如今本身無心間來到了O’Reilly野生保護區,它必須避免奇怪和危險的動物並回到安全的地方。

    這個遊戲中會發生如下幾種物理交互:

    1.玩家與地圖的碰撞

    2.動物與地圖的碰撞

    3.玩家與動物的碰撞

    4.玩家與目的地的碰撞

    第一種,玩家與地圖的碰撞,很是簡單。咱們檢測玩家的身體區域邊界與關卡中的牆。若是玩家將與牆發生碰撞,咱們稍微位移一下玩家,以使其不會與牆發生碰撞。

    第二種交互稍微複雜一點。咱們使用一樣的偵測方法:檢測動物的身體區域與關卡中的牆。可是咱們的解決方法有一些不一樣,由於動物不是玩家控制,而是由電腦控制。咱們解決狀況1時,咱們稍微位移一下玩家,以使其不會進入到牆裏面。這是用來提醒玩家他正在撞向牆並且必須改變方向。

    若是咱們對狀況2作一樣的解決,AI不會認識到它正撞向牆,並且會繼續走向牆裏面。所以,咱們分兩步解決這種狀況,首先稍微位移一下動物以使其不會與牆發生碰撞,而後通知AI動物撞上了一面牆。這樣,遊戲邏輯會控制動物改變移動方向。

    第三種狀況,咱們首先檢查玩家身體區域邊界與動物身體區域。一旦咱們偵測到了他們將發生碰撞,可能會發生不一樣的結果。若是這個動物是眼鏡猴,他可能會逃跑;若是這個動物是毒蛇或者獅子,它可能會攻擊玩家;駱駝可能會忽略玩家;蝗蟲可能會被踩扁。

    最後,第四種狀況是一種新的狀況。目的地與地圖、玩家與動物是不一樣的,由於它沒有圖形表示。它是一個隱含的觸發區域,當玩家踏入它

時,它會發出一個事件。幸運是,儘管它沒有圖形表示,它仍然具備物理表示。因此,咱們依然能夠偵測玩家的身體區域邊界與目的地的區域邊界。若是咱們發現玩家到達了目標,就通知遊戲邏輯,使其進入「玩家勝利」的遊戲狀態。

    二維碰撞偵測

    二維碰撞偵測是一個相對簡單的處理過程。大部分均可以總結爲以下的一段偵測常規:矩形對矩形、矩形包含點、圓對矩形、圓包含點與圓對圓(有可能也須要檢查線段,不過那一般能夠被避免)。

    因爲這些常規可能被每秒鐘使用屢次,所以確保它儘量高效是很是重要的。爲了達到這個目的,咱們將會進行一系列低成本的測試來證實兩個物體碰撞以前是沒有碰撞在一塊兒的:

 

  1. bool cd_rectangleToRectangle( Rect r1, Rect r2)  
  2. {  
  3. //can't be colliding because R1 is too far left of R2  
  4. if( r1.x + r1.width < r2.x ) return FALSE;  
  5. //can't be colliding because R1 is too far right of R2  
  6. if( r1.x > r2.x + r2.width ) return FALSE;  
  7. //can't be colliding because R1 is too far below R2  
  8. if( r1.y + r1.width < r2.y ) return FALSE;  
  9. //can't be colliding because R1 is too far above R2  
  10. if( r1.y < r2.y + r2.width ) return FALSE;  
  11. //if we get here, the two rects MUST be colliding  
  12. return TRUE;  
  13. }  

 

    儘管這樣,當物體碰撞時,仍是會有更多的代碼被執行。大部分時候,物體都不會相互碰撞,咱們針對於此進行了優化,由於它更有效率。

    在咱們繼續以前,讓咱們看一個很是重要的概念:遊戲對象的圖形表示獨立於它的物理表示。計算機每秒鐘只能提供有限的計算總量,進行實時地物理模擬很是困難。正由於如此,遊戲物理代碼有一個悠長而驕傲的傳統,那就是隻要精確到讓玩家以爲遊戲正確便可。

    好比,在咱們上面提到的遊戲中,咱們玩家的虛擬人偶即將撞上一頭犀牛(很明顯,要麼咱們的玩家不太專心,要們就是被一隻生氣的母老虎追趕)。咱們的玩家在跑動過程當中,會有不少四肢動做,犀牛也是同樣,它伸出它頭上的牛角。

    儘管玩家在跑動過程當中,圖形表示須要出玩家的手和腳,可是在物理表示中,咱們真的須要知道四肢的具體座標嗎?咱們真的須要去檢查玩家的雙手、雙腳以及頭是否與犀牛的頭、腳、及牛角發生了碰撞(還有別忘記了尾巴!)?固然不,由於咱們的遊戲是按照咱們的須要簡單的用矩形表示玩家和用矩形表示犀牛。

    三位碰撞偵測

    三維碰撞偵測要比二維困難不少。你必須很熟悉三維數學計算,好比線形代數。並且,即便數學要求不高,3D遊戲也擁有更復雜的遊戲場景。幸運的是,你能夠依賴合適的技術來幫助減小計算次數。固然,仍是要精確到讓玩家以爲遊戲正確便可。

    就像咱們以前討論的,一個物體的圖像表示不一樣於它的物理表示。可是,有時咱們須要確保他們之間的差異越小越好。想象一下第一人稱射擊遊戲,咱們不只要知道一個玩家知否射中了另一個玩家,還要知道他是否取得了一個爆頭。很明顯,一個簡單的盒子邊界不能知足需求,不過咱們也沒法提供對每一顆子彈都檢查路徑,判斷其穿過的每個虛擬人偶的每一個三角形的檢查。

    咱們使用一樣的3D偵測,就像咱們在2D中使用的偵測同樣:在執行精確的與昂貴的測試以前,先執行一系列低成本的否認測試。以子彈爲例,咱們首先測試子彈有沒有劃過哪一個玩家的邊界框。若是沒有擊中他們,咱們退出碰撞檢查。對於擊中了的邊界框,找到起始擊中點,並對更箇中的全部三角形都作更多詳細的測試。這是圖形優化中的細節層次的物理版本。若是在最近的玩家身上的檢查失敗了,咱們將會在下個玩家身上作詳細檢查。用這種方式,咱們能夠高效並準確的更復雜的場景,好比一顆子彈從一個玩家的雙腿間飛過,擊中了他身後的另一位玩家。

    碰撞解決

    當碰撞被偵測到時,就要解決它。首先要考慮的是,什麼樣的低層次的或高層次的動做必須發生。低層次動做是指物理代碼能夠解決的,如調整玩家的位置以使其在地面上而不會摔到地面下。高層次的動做是指發送到遊戲引擎的遊戲邏輯部分的信號。這些信號可讓遊戲邏輯知道一個動物什麼時候否跑進了一堵牆裏或者一個玩家是否到達了目的地。

    有些碰撞響應須要同時多重高層次的和低層次的響應。例如,在彈鋼珠遊戲中,一個鋼珠打到了防撞杆上,它應該正確的彈離防撞杆(低層次),也會同時向遊戲邏輯發送一個信號,使得防撞杆產生動畫效果、聲音效果以及增長玩家的得分(高層次)。

    當測試低層次的碰撞解決的代碼時,要特別注意細節。最影響遊戲感受的重要元素之一就是物理代碼。虛擬人偶能夠快速的響應玩家的輸入嗎?賽車遊戲能真實地模擬出懸掛系統與變速系統嗎?當玩家發生大炮的時候,屏幕會抖動嗎?

    根本上,這是由設計人員的工做來讓遊戲感受很好,可是這須要程序員的工做來實現它。不要懼怕寫一些特殊的測試用例以獲得正確的結果。

    例如,測試時經常使用的一項技術是爲對象設置「flag」。當一個對象與另一個對象「ground」接觸時,你能夠爲這個對象設置一個「grounded」 flag爲true。當grounded flag爲true時,能夠認爲這個物體在休息,不須要對其施加劇力影響,或偵測它與地圖的碰撞。這不但能夠幫助你防止在一大堆靜止物體上運行碰撞偵測,也能夠避免在某些物理模擬鍾,物體在地面附近產生抖動現象。

   聲音引擎

    聲音是遊戲開發中常常忽略的一的環節,可是當你知道聲音構成了人類玩視頻遊戲的1/3的感受時,你確定會很困窘。在合適的時機播放正確的聲音使得程序員僅做很小的工做就能夠爲遊戲增長分數。

    基本的音效特徵包括載入和卸載聲音樣本、播放聲音、中止播放、暫停播放以及循環播放。典型的,全部的聲音均可以以一樣的音量播放,可是你可能但願玩家調整以使全部的聲音都符合他們的喜愛。

    聲音樣本

    在播放音效以前,你須要從文件或者緩存中載入聲音樣本。IPHONE API支持AAC與MP3格式的高品質的聲音,也支持PCM和IMA4的小樣品,以及其餘一些格式。

   播放聲音

    一旦載入了聲音樣本,API提供了函數以開始和中止播放樣本。大多數API還提供了暫停和繼續功能,還有一些容許你從特定點開始播放。雖然還有更多高級特性,可是基本上以上就是程序員所有所須要的了。

   多聲道聲音

    由於聲音播放依賴於硬件,你在同一時間能夠聽到的聲音是有限的。每種聲音播放時都使用一個聲道。在IPHONE中,全部的MP3和AAC樣本共用相同的硬件聲道,並且只有一個是可用的,多聲道支持PCM和IMA4。

    這意味着同一時間,只有一個MP3/AAC格式的樣本能夠被播放。典型的,但多個PCM/IMA4樣本能夠同時播放多個樣本(同時與MP3/AAC播放)。

    音樂與SFX

    遊戲中的大部分聲音均可以分爲兩大類:環境音(典型的,背景音樂)和音效(SFX)。有時,用環境音代替音樂,但他們都共享一樣的特色。

    音樂通常是重複播放一個相對長的樣本或者引出下一段音樂樣本。一般狀況下,只有一首音樂在同一時間播放,使得MP3/AAC格式限制變成了一個不成問題的問題。

    聲效要短的多,並且須要許多不一樣的樣本,在同一時間重疊播放。PCM/IMA4格式很好的知足了這個需求。

    因爲PCM/IMA4也只有有限的聲道數,所以若是你打算同時播放不少的SFX樣本,其中一些可能不能被播放。因此,爲SFX音效設定優先級以確保某些音效必定能播放就顯得很重要了。

    例如,讓咱們討論以前提到的咱們的玩家虛擬人偶走進了一個盡是憤怒的犀牛的房間。每頭犀牛都會播放憤怒的鼻息聲,可能還會接着播放撞人的聲音,而玩家虛擬人偶則會發出懼怕的哭叫聲。咱們會給與玩家虛擬人偶聲效更高的優先級,以使其不會被犀牛的撞人聲效所淹沒。

    幸運的是,目前的IPHONE支持至少32聲道,因此通常不太可能須要在這個平臺上去刻意安排優先級。

    輸出設備與干擾

    IPHONE支持內置的對講機與聽筒做爲輸出設備。建議不要同時使用它們。當玩家將聽筒插入到IPHONE中時,音頻會自動改成從聽筒播放。

    因爲IPHONE是移動設備,有可能玩家在路上在的時候,聽筒會掉落。一個好的設計選擇是當聽筒被移除的時候暫停遊戲以給玩家足夠的時間讓他從新接入耳機。此時,你也能夠選擇同時提供中止播放聲音。

    最重要的是,應該從玩家的用戶角度來考慮聲效。玩你的遊戲並非用戶使用IPHONE的惟一標準,因此不要讓你的聲效的優先級打擾到用戶,不然玩家會將其關掉。

   用戶輸入

    遊戲引擎的玩家輸入部分集中於來接收自於操做系統的低層次的事件,而後將其轉化爲高層次的事件,這樣,遊戲邏輯代碼能夠在PC遊戲中使用它。低層次的事件能夠是鼠標和鍵盤事件。對於控制檯遊戲,他們可能產生於控制器的手、觸發器與按鈕。在咱們的例子裏面,IPHONE會處理觸摸與旋轉事件。

   觸摸事件

    觸摸屏接口的設計方式與其餘大多數移動手機、控制檯與PC平臺的接口設計方式有着根本的區別。在用戶觸摸屏幕與應用程序接收到該事件之間有延遲(儘管IPHONE已經在將縮短延遲方面作的足夠好了),可是真正的問題是,無論什麼時候用戶作出觸摸動做,他的手指都會遮蓋住部分屏幕,大大下降了遊戲畫面的可見性。

    你能夠經過提供一個圖形按鈕給用戶來點擊(回到了按鈕點擊系統,代價是屏幕空間),或者提供一個聰明的隱喻來解決這個問題。好比,若是玩家點擊了屏幕上的一塊空間,你可讓虛擬人偶朝那個方向走。這樣能夠省去用戶的連續點擊輸入。

    儘管用戶接口設計是遊戲設計人員的責任,可是編程人員須要告訴設計人員這個平臺能夠作什麼和不能作什麼。IPHONE支持如下觸摸事件:

  • 觸摸開始
  • 觸摸移動
  • 觸摸結束
  • 觸摸取消

    你可能會問什麼狀況下會觸發「觸摸取消」事件。當某個事件將你的應用程序掛起在「觸摸開始」事件與「觸摸結束」事件之間時,程序會收到「觸摸取消」事件通知你不會收到其餘的事件(如觸摸結束)。

    爲了處理多點觸摸,包含一個UITouch對象list的UIEvent對象被髮送到你的應用程序中。若是隻有一個指頭觸摸到屏幕,你只會接收到一個UITouch對象;若是兩個指頭觸摸到屏幕,你會接收到兩個UITouch對象等等。並且IPHONE能夠追蹤正在發生或最近發生的連續的5次輕擊事件(一個觸摸開始事件後緊跟一個觸摸結束事件)。

    不幸的是,經過以上事件來判斷用戶是單擊、雙擊、掃過或者壓縮動做會可能變得比較麻煩。雖然不是很困難,可是在一開始並非很容易正確處理。看如下例子:

 

  1. Time 0: TouchStart - numTaps(0) numTouches(1) Touches { (40,40) }  
  2. Time 100: TouchEnd - numTaps (1) numTouches(1) Touches { (40,40) }  
  3. Handle single-tap..  

 

    到目前爲止,用戶觸摸了屏幕一次,你的代碼能夠執行相應的單擊處理邏輯。可是稍等!

 

  1. Time 0: TouchStart - numTaps(0) numTouches(1) Touches { (40,40) }  
  2. Time 100: TouchEnd - numTaps (1) numTouches(1) Touches { (40,40) }  
  3. Handled single-tap.. INCORRECTLY  
  4. Time 200: TouchStart - numTaps (1) numTouches(1) Touches { (40,40) }  
  5. Time 300: TouchEnd - numTaps (2) numTouches(1) Touches { (40,40) }  
  6. FAILED to handle double-tap  

 

    用戶第二次輕擊了屏幕。若是你已經在收到觸摸結束的事件時進行了處理,可能錯誤的處理了用戶實際上的雙擊處理。

    咱們應該如何正確處理這種狀況呢?解決方法是將第一次觸摸結束事件推遲爲定時回調。當第一次接收到觸摸結束事件時,咱們設置一個回調。若是咱們在回調以前接收到了第二次觸摸結束事件,咱們能夠斷定用戶進行了雙擊,並取消回調,執行雙擊處理。咱們接收到了回調,咱們認爲用戶沒有進行雙擊並應該進行單擊處理。

    這裏有表達兩種狀況的例子:

 

  1. Time 0: TouchStart - numTaps(0) numTouches(1) Touches { (40,40) }  
  2. Time 100: TouchEnd - numTaps (1) numTouches(1) Touches { (40,40) }  
  3. Initiate callback timer  
  4. Time 200: TouchStart - numTaps (1) numTouches(1) Touches { (40,40) }  
  5. Time 300: TouchEnd - numTaps (2) numTouches(1) Touches { (40,40) }  
  6. Handle double-tap, cancel callback  

 

    此次,玩家進行了雙擊並且代碼進行了正確的處理。

 

  1. Time 0: TouchStart - numTaps(0) numTouches(1) Touches { (40,40) }  
  2. Time 100: TouchEnd - numTaps (1) numTouches(1) Touches { (40,40) }  
  3. Initiate callback timer  
  4. Time 500: Callback recieved  
  5. Handle single-tap  

 

    如今,玩家進行了單擊並且代碼也進行了正確的處理。

    注意,你沒必要爲那些僅期待單擊事件的接口加上這些處理。

    偵測諸如掃過的動做會更麻煩一點,但也更容易正確處理。代碼中必須爲每次觸摸保存起始點與終點,並算出用戶劃的線的方向是向上、向下、向左、仍是向右。還要判斷他的手劃過的是否足夠快。

    解決高層次事件

    一旦斷定了用戶執行的物理動做,你的代碼必須能將它們轉換爲遊戲邏輯組件可使用的形式。具體怎麼作須要依賴於你的遊戲的上下文,可是這裏有幾種典型的形式:

  • 若是玩家準備控制虛擬人偶,在玩家和遊戲之間一般會有連續的交互。常常須要存儲當前用戶輸入的表現形式。好比,若是輸入裝置爲遙杆,你可能須要在主循環中記錄當前點的x軸座標和y軸座標,並修正虛擬人偶的動量。玩家和虛擬人偶之間的是緊密地耦合在一塊兒的,因此控制器的物理狀態表明着虛擬人偶的高層次的狀態模型。當遙杆向前撥動時,虛擬人偶向前移動;當「跳躍」按鈕按下時,虛擬人偶跳起。
  • 若是玩家正與遊戲地圖進行交互,那麼須要另一種間接的方式。好比,玩家必須觸摸遊戲地圖中的一個物體,代碼必須將玩家在屏幕上的觸摸座標轉化爲遊戲地圖的座標以斷定用戶到底觸摸到了什麼。這可能只是簡單的將y軸座標減去2D攝像機座標的偏移量,也多是複雜到3D場景中的攝像機光線碰撞偵測。
  • 最後,用戶可能進行一些間接影響到遊戲的動做,如暫停遊戲、與GUI交互等。這時,一個簡單的消息或者函數會被觸發,去通知遊戲邏輯應該作什麼。

    遊戲邏輯

    遊戲邏輯是遊戲引擎中是你的遊戲獨一無二的部分。遊戲邏輯記錄着玩家狀態、AI狀態、斷定何時達到目的地、並生成全部的遊戲規則。給出兩個類似的遊戲,他們的圖像引擎與物理引擎可能只有細微差異,可是它們的遊戲邏輯可能會有很大差別。

    遊戲邏輯與物理引擎緊密配合,在一些沒有物理引擎的小遊戲中,遊戲邏輯負責處理全部物理相關內容。可是,當遊戲引擎中有遊戲引擎的時候,須要確保二者的獨立。達到此目的的最好方式就是經過物理引擎向遊戲邏輯發送高層次的遊戲事件。

    高層次事件

    遊戲邏輯代碼應該儘量僅處理高層次問題。它不該該處理當用戶觸摸屏幕時須要以什麼順序將什麼描畫到屏幕上,或者兩個矩形是否相交等問題。它應該處理玩家但願向前移動,何時一個新的遊戲物體應當被建立/移除以及當兩個物體相互碰撞後應該作什麼。

    爲了維持概念上的距離,處理低層次概念(諸如用戶輸入與物理引擎等)的代碼應當建立高層次的消息併發送給遊戲邏輯代碼去處理。這不只能保持代碼的獨立性與模塊化,還會對調試有所幫助。經過查看高層次消息傳遞的日誌,你能夠斷定是沒有正確處理消息(遊戲邏輯代碼的問題),仍是沒有在正確的時機傳送消息(低層次代碼問題)。

    一個很是基本的傳遞高層次消息的技術是寫一個String並傳遞它。假如玩家按下了上箭頭鍵,它的虛擬人偶必須向上移動。

 

  1. void onPlayerInput( Input inputEvt ) {  
  2.     if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP ) {  
  3.         g_myApp->sendGameLogicMessage( "player move forward" );  
  4.     }  
  5. }  

 

    雖然上面的代碼對程序員來講通俗易懂,但對於電腦來講卻並不高效。它須要更多的內存與處理,遠比實際須要的多。咱們應該用提示來替代用戶輸入方法。比起一個字符串,它使用一個"type"和"value"。因爲可能的事件都是結構化的和有限的,所以咱們可使用整數和枚舉類型來咱們消息中的事件信息。

    首先,咱們定義一個枚舉類型來標識事件類型:

 

  1. enumeration eGameLogicMessage_Types {  
  2.     GLMT_PLAYER_INPUT,  
  3.     GLMT_PROJECTILE_WEAPON,  
  4.     GLMT_GOAL_REACHED,  
  5. };  

 

    接着咱們再建立一個枚舉類型來標識事件的值:

 

  1. enumeration eGameLogicMesage_Values {  
  2.     GLMV_PLAYER_FORWARD,  
  3.     GLMV_PLAYER_BACKWARD,  
  4.     GLMV_PLAYER_LEFT,  
  5.     GLMV_PLAYER_RIGHT,  
  6.     GLMV_ROCKET_FIRED,  
  7.     GLMV_ROCKET_HIT,  
  8. };  

 

    如今咱們定義一個結構體來存儲咱們的消息數據:

 

  1. struct sGameLogicMessage {  
  2.     short type;  
  3.     short value;  
  4. } Message;  

 

   如今,咱們就能夠像上一個例子代碼同樣,用一個對象來傳遞咱們的消息:

 

  1. void onPlayerInput( Input inputEvt ) {  
  2.     if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP ) {  
  3.         Message msg;  
  4.         msg.type = GLMT_PLAYER_INPUT;  
  5.         msg.value = GLMV_PLAYER_FORWARD;  
  6.         g_myApp->sendGameLogicMessage( msg );  
  7.     }  
  8. }  

 

    這看起來做了更多的工做,但它運行起來會更有效率。前一個(壞的)例子用了20個字節來傳遞消息(20個字符各佔一個字節,別忘了終止符)。第二個例子只用了4個字節來傳遞一樣的消息。可是更要的是,當sendGameLogicMessage()處理方法的時候,它只須要分析兩個switch語句就能夠找到正確的響應,而前一個例子則組要從字符串進行解析,速度很慢。

    人工智能

    遊戲邏輯的另一個職責就是管理AI代理。兩類典型的遊戲須要用到AI系統:一種是玩家與電腦競賽;另一種是在遊戲世界中有半自主系統的敵人。在這兩種狀況下,AI代理爲遊戲世界中的物體的動做接受輸入並提供輸出。

    在第一種類型遊戲裏,AI被稱做專家系統。它被期待用來模擬理解遊戲規則的人的行爲動做,並能夠採起具備不一樣難度的策略來挑戰玩家。AI具備與玩家相似的輸入與輸出,能夠近似的模擬玩家的行爲。因爲人類比如今的AI代理更擅長處理複雜信息,有時爲專家系統提供的輸入信息要多於給玩家的,以使AI系統看起來更智能。

    例如,在即時戰略遊戲(RTS)中,戰爭迷霧用來限制玩家的視野,但AI敵人能夠看見地圖上全部的單位。儘管這樣提升AI對抗更高智慧玩家的能力,可是若是優點變的太大,會讓人以爲AI在做弊。記住,遊戲的重要點是讓玩家得到樂趣,而不是讓AI擊敗他們。

    在第二種類型的遊戲中,可能有許多AI代理。每個都獨立,其不是很是智能。在某些狀況下,AI代理會直接面對玩家,而有些多是中立狀態,甚至還有一些是前面兩種狀態的結合。

    有些代理多是徹底愚笨的,提供特定的、有限的行爲並且並不關心遊戲世界中發生的事情。在走廊裏面來來回回走動的敵人就是一個例子。有些多是稍微有些愚笨,只有一個輸入和一個輸出,好比玩家能夠打開和關閉的門。還有一些可能很是複雜,甚至懂得將它們的行爲組合在一塊兒。爲AI代理選擇恰當的輸入容許你模仿「意識」和增長現實性。

    不論AI代理有多麼簡單,通常都會它們使用狀態機。例如,第一個例子中的徹底愚笨的物體必須記錄它在朝哪一個方向走動;稍微愚笨的物體須要記錄它是開的狀態仍是關的狀態。更復雜的物體須要記錄「中立」與「進攻性之間的」動做狀態,如巡邏、對抗與攻擊。

    透明的暫停與繼續

    將遊戲視做具備主要遊戲狀態的模擬是很是重要的。不要將現實世界時間與遊戲時間混淆。若是玩家決定休息會兒,遊戲必須能夠暫停。以後,遊戲必須能夠平滑的繼續,就像任何事情都沒有發生同樣。因爲IPHONE是移動設備,保存與繼續遊戲狀態變得尤爲重要。

    IPHONE上,在一個時間點只容許一個應用程序運行,用戶也但願這些應用程序可以很快載入。同時,他們但願可以繼續他們在切換應用程序以前所作的事情。這意味着咱們須要具備在設備上保存遊戲狀態,並儘量快的繼續遊戲狀態的能力。對於開發遊戲,一項任務是要求保持如今的關卡並能夠從新載入它使玩家即便在從新啓動應用程序後也能夠繼續遊戲。你須要選擇保存哪些數據,並以一種小巧的、穩定的格式將其寫到磁盤上。這種結構化的數據存儲被稱爲序列化。

   根據遊戲類型的不一樣,這可能比聽起來要困難的多。對於一個解謎遊戲,你將僅須要記錄玩家在哪一個關卡、以及如今記分板看起來是什麼樣的。可是在動做類遊戲中,除了記錄玩家虛擬人偶以外,你可能還須要記錄關卡中的每一個物體的位置。在一個特定時間點,這可能變得難以管理,特別是當但願它可以很快完成。對於這種狀況,你能夠在遊戲設計階段採起一些措施以確保成功。

    首先,你必須決定什麼東西是在保存遊戲狀態時必須保存的。火焰粒子系統中的每根小火苗的位置並不重要,可是在粒子系統的位置在大型遊戲中可能很重要。若是它們能從關卡數據中得到,那麼遊戲中每一個敵人的狀態可能並不重要。用這種方式進一步考慮,若是你能夠簡單的讓玩家的虛擬人偶從check point開始的話,那玩家虛擬人偶的確切狀態與位置也可能不須要保存。

    基於幀的邏輯與基於時間的邏輯

    基於幀的邏輯是指基於單獨的幀的改變來更新遊戲物體。基於時間的邏輯雖然更復雜但卻與實際遊戲狀態更緊密,是隨着時間的流逝而更新遊戲物體。

    不熟悉遊戲開發的程序員老是犯了將基於幀的邏輯與基於時間的邏輯混合的錯誤。 它們在定義上的區別是微妙的,不過若是處理不得當,會形成很是明顯的BUG。

    好比,讓咱們以玩家移動爲例。新手程序員可能寫出這樣的代碼:

 

  1. void onPlayerInput( Input inputEvent ) {  
  2.     if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP) {  
  3.         //apply movement based on the user input  
  4.         playerAvatar.y += movementSpeed;  
  5.     }  
  6. }  

 

    每當玩家按下按鍵,虛擬人偶像前移動一點。這是基於幀的邏輯,由於每次移動的變化都會潛在的伴隨着新的幀。事實上,在這個的例子中,每次玩家輸入事件都會發生移動。這或多或少有點像主循環的迭代。移動的可視化影響只有在主循環的下次迭代中才會反映,因此任何迭代中間的虛擬人偶移動都會浪費計算。讓咱們作一下改進:

 

  1. void onPlayerInput( Input inputEvent ) {  
  2.     if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP) {  
  3.         //save the input state, but don't apply it  
  4.         playerAvatar.joystick = KEY_UP;  
  5.     }  
  6.     if(inputEvt.type == IE_KEY_RELEASE) {  
  7.         playerAvatar.joystick = 0;  
  8.     }  
  9. }  
  10. void Update() {  
  11.     //update the player avatar  
  12.     if( playerAvatar.joystick == KEY_UP ) {  
  13.         playerAvatar.y += movementSpeed;  
  14.     }  
  15. }  

 

    如今咱們知道,在鍵被按下的過程當中,每次遊戲循環中都只會被賦予一次速度。可是,這仍然是基於幀的邏輯。

    基於幀的邏輯的問題是,幀變化不會老是以相同的時間間隔發生。若是在遊戲循環中,渲染或者遊戲邏輯會比一般耗費更多的時間,它可能會被推遲到下一次循環中。因此,有時你須要有60幀每秒(fps),有時,你只須要30fps。因爲移動是適用於幀的,有時你只會以一般的一半速度來移動。

    你能夠用基於時間的邏輯來準確的表達移動。經過記錄自從上次幀更新的時間,你能夠適用部分移動速度。用這種方式,你能夠以每秒爲單位來標識移動速度,而沒必要關心當前幀速率是多少,玩家虛擬人偶的速度是一致的:

 

  1. void Update( long currTime ) {  
  2.     long updateDT = currTime - lastUpdateTime;  
  3.     //update the player avatar  
  4.     if( playerAvatar.joystick == KEY_UP ) {  
  5.         //since currTime is in milliseconds, we have to divide by 1000  
  6.         // to get the correct speed in seconds.  
  7.         playerAvatar.y += (movementSpeed * updateDT)/1000;  
  8.     }  
  9.     lastUpdateTime = currTime;  
  10. }  

 

    在這個例子中,移動速度的總量將會是相同的,無論是2fps仍是60fps。基於時間的邏輯須要一點額外的代碼,可是它可使程序更精確而沒必要在意暫時的延遲。

    固然能夠用基於幀的邏輯來開發遊戲。重要的是,不要混合它們。好比,若是你的圖形代碼使用基於時間的邏輯來渲染玩家虛擬人偶的移動動畫,可是遊戲邏輯代碼卻使用基於幀的邏輯在遊戲世界中來移動它,這樣移動的動畫將不能玩玩家移動的距離徹底同步。

    若是可能的話,請儘可能移除基於幀的邏輯。基於時間的邏輯將會對你有更大的幫助。

    遊戲邏輯組織結構

    遊戲邏輯代碼的核心功能就是管理遊戲狀態的規則與進度。根據你的遊戲設計,這可能意味着任何事情。可是,仍是有一些基本模式基於製做的遊戲的類型。

    遊戲邏輯不與任何一個特定的類相關聯,它遊戲狀態對象中表現出來。當主遊戲狀態被初始化後,它將會爲關卡載入與初始化必要的資源。例如猜謎遊戲中的一組提示與單詞、玩家虛擬人偶的圖片數據以及玩家當前所在區域的圖片數據。在遊戲循環中,遊戲邏輯將會接受用戶輸入,運行物理模擬,並負責處理全部的碰撞結局消息,模擬AI動做,執行遊戲規則。最後,當應用程序須要終止主遊戲狀態,它會釋放釋放全部的遊戲資源,並可能將遊戲狀態保存到硬盤驅動器上。

    根據遊戲的複雜度,你可能會發現很方便進一步分解遊戲邏輯。好比,若是你在開發一款冒險遊戲,你可能有一個充滿環境數據(地面、建築、河流、樹等)、能夠移動、與玩家交互的實體(玩家虛擬人偶、敵人、非玩家角色、開關、障礙物等),各類GUI使玩家做出特殊動做和顯示重要信息的遊戲世界。每種遊戲特徵都必須有大量的代碼。雖然它們合在一塊兒才能組成完整的遊戲,可是你仍是能夠保持它們的工做模塊化。

    你能夠建立一個Level Manager類來處理遊戲關鍵,包括載入和卸載顯示在遊戲世界中的物理與圖像數據與調用遊戲引擎來偵測實體與遊戲世界的碰撞。你還能夠建立另一個類或者一些類來處理遊戲世界中存在的實體。每一個類都載入和卸載渲染那些物體的必要的物理和圖片數據,以及包括控制它們的AI。

    最後,你可能建立另一個單獨的類來處理遊戲中用戶交互,以保持代碼與三大概念獨立。

    這個體系結構適用於任何類型的遊戲。首先評估遊戲設計的主要特性,接着以某種方式組合,將相近的功能與數據組合在一塊兒。

    總結

    你應該對創造一個遊戲引擎時必須完成的任務有了一個基本的理解。這將會幫助咱們在下一節建立這些元素,爲咱們的遊戲作準備。 


原文連接: http://blog.csdn.net/xiaominghimi/article/details/6556289
相關文章
相關標籤/搜索