看板 GameDesign 關於我們 聯絡資訊
http://wp.me/pBAPd-9K 作者:NDark 時間﹔201001 "容易被遺忘的遊戲設計模組"之二: Event Creating/Handling/Reporting http://www.ppxclub.com/attachments/2007/07/176821_200707102359271.jpg
事件是遊戲設計一個很重要的部分。 譬如說,攻擊這個動作可以分成好幾個步驟,包括 使用者按下攻擊鈕, 玩家角色發射動畫事件,音效事件,發射角色AI轉換, 受到攻擊的判定, 被打到物件的動畫事件,音效事件,扣血損傷事件,被打到的角色AI轉換。 全部的步驟可以都寫在一起,開發起來比較單純。 也可以分開撰寫,但必須適當的安排才不會造成除錯的困擾。 事件可以用幾種方式來分類: 從來源(種類)來分:鍵盤事件,滑鼠事件,網路事件,遊戲事件。 從結果來分:產生音效,狀態/數值改變,遊戲流程運作,再創造其他事件。 從流程來分:觸發事件,事件處理,事件傳遞。 我們要討論事件的處理, 是因為除了這是遊戲一個很重要的部分之外,他還很容易會造成開發上的潛在風險。 如前述, 最簡單的事件撰寫方式就是一次解決。 以骨灰遊戲外星人/小蜜蜂為例,按一次的左方向鍵會導致飛機往左移動一格。 [註]http://www.gamehome.tv/Article/UploadFiles/200908/20090826092346377.gif
所以他的事件流程大概是像這樣的。 if input.left is true NextPos.x = PlayerPos.x - 1 PlayerPos.x = NextPos.x end 這種寫法就是事件的發生(觸發)以及事件的處理在一起。 (這個例子還沒有事件的傳遞) 好處是直覺一目瞭然,某個功能出問題,就是去找哪區的程式碼。 壞處是當程式複雜的時候,很多事件沒辦法在瞬間處理完畢。 因此複雜一點的狀況是像這樣的 拿Time Crisis光線槍射擊機台來做例子,當玩家按下發射鈕, 會進行的流程步驟是這樣的 [註] http://en.wikipedia.org/wiki/Time_Crisis http://jetsetnick.files.wordpress.com/2009/03/time-crisis-4_deluxe.jpg
先檢查是不是蹲下狀態,假如蹲下就離開。 檢查子彈是不是用光狀態,假如是用光則必須產生Reload的閃爍字樣,並且離開。 檢查距離上次開火是不是太快,如果太快就離開。 (這個檢查並不特別必要, 但是這種檢查通常是避免玩家按鈕產生的bounce, 避免玩家用加速器,或是避免前一次開槍特效還沒播完) 正常開槍,子彈減少。 發出槍的聲及光特效,畫面的震動特效,力回饋的特效。 假如不是飛行道具(要等他飛到的那種),判斷有沒有擊中對手。 假如有擊中對手就依照對手的等級扣對手的生命值 假如擊中的是藥包則檢查自己的生命值是否已滿,沒滿就加血。 假如擊中的是武器則切換武器。 假如擊中的是火藥桶,則發生爆炸並且判定爆炸範圍是否有敵人, 有的話就扣那些對手的生命值。 沒擊中也會有彈痕特效。 可想而知這個判斷式寫起來會很"漂亮",很有成就感,然後只有你可以維護。 不過在這個事件的分析中我們可以發現一些比起小蜜蜂更進階的狀況。 譬如說"產生Reload的閃爍字樣"這種事件的傳遞: 閃爍這件事情不是瞬間完成的,所以必定是產生了另一個事件。 該事件是做了某種狀態變化。 (譬如說是有一個旗標負責決定目前要不要閃爍Reload) 我們也可以看到這個射擊事件有一些條件的判定檢查, 這些判定會導致事件的取消:就是按了按鈕沒反應這樣。 最後是某些事件的進行可能牽涉到不同對象的不同屬性。會導致不同的結果。 在介紹我們本篇的主角之前我先要請各位去翻一些Windows的視窗程式設計的範例。 基本上Windows視窗程式設計就是一個事件處理的架構。 你可以在程式的任何地方產生預設或是自訂的事件, 然後Windows會幫你把這些事件放到一個串列中。 一個個去該事件要做甚麼的地方做處理。(不管是預設的,覆寫的,還是自訂的事件) 這種架構的第一個好處就是事件的發生與處理是分離的, 事件發生當下並不用馬上處理事件的內容, 而是依序排隊讓每個事件處理自己要做甚麼事情。 另一個好處是事件的處理是集中管理,相對起來也是一目瞭然。 可想而知,事件的發生與處理就可以分開來維護。 第一個壞處就是事件在處理的時候除非特別設計, 否則通常是不知道這事件是哪裡發生的。 有時候在除錯時會造成若干困擾。 另一個壞處就是事件的處理順序是FIFO(First In, First Out)的, 如果使用多執行緒或是多優先權重的事件處理, 有需要更特別注意事件的觸發先後順序。 避免先觸發的事件被後觸發的事件搗亂的狀況。 我們先為了減少前面提到的事件的複雜度開始介紹我們今天的主角。 Event Creating / Handling / Reporting 事件創造/處理/傳遞(報告) 每個事件大概都可以分析為這樣的形式。 事件的創造跟處理大致上跟前述Windows架構類似, 事件的創造可能是鍵盤製造的,可能是網路或AI流程製造的, 也有可能是遊戲流程或是事件又另外產生的。 而事件的傳遞就是事件中產生的事件。 (請注意新產生的事件會放在事件串列的後方。) 完。。。。 哈,只講這些大概會被讀者殺了。 接下來是教你怎麼實作簡單的事件處理。 首先是事件的宣告,就是先宣告一下有哪些事件。 enum Event { E_TurnLeft 右轉 E_TurnRight 左轉 E_AddSpeed 加速 E_Break 煞車(檢速) E_Horn 鳴笛 E_SystemMenu 暫停(選單) } 第二步是做一個事件的類別,用來攜帶事件跟參數 class GameEvent { Event m_eEventID ; // 事件的識別標籤 double m_dValue ; // 最簡單的參數型式,帶一個變數 double m_vdValue[ MAX ] ;// 比較像硬體指令的參數型式, // 依照不同的事件攜帶不同數目的指令。 } 前兩步可以寫在一個或兩個標頭檔裡面 第三步是事件的容器,通常容器要在遊戲流程可以取得的位置。 我們這裡用std::list來做。list需要的一些實作請你在GameEvent自己處理。 std::list< GameEvent > gsEventList ; 增加事件的時候就是 gsEventList.push_back( GameEvent( E_TurnLeft ) ) ; 第四步是輸入裝置的事件觸發, 不管任何一種輸入裝置,大概都可以變成一個函式 InputHandle() { if button A is pressed ... if button up is pressed ... if left mouse button is clicked ... } 假如你的事件的觸發是由輸入裝置產生的,那麼就把事件的產生寫在那些...的位置。 第五步是事件處理的函式,通常會發生在遊戲流程的某一個環節。 我會建議在輸入/網路事件的後面 EventHandle() { // 我們用一個loop來處理一個個事件, // 但是並非一次的EventHandle就要清除所有的事件,端看如何設計。 while( ... ) { // 取出我們的事件 EventNow = gsEventList.front() ; switch() { case E_TurnLeft : ... break ; case E_TurnRight : ... break ; case E_AddSpeed : ... break ; case E_Break : ... break ; case E_Horn : ... break ; case E_SystemMenu : ... break ; } // 捨棄處理完的事件 gsEventList.pop_front() } } 同樣的,該事件要做甚麼事情就寫在各對應事件...的位置 這個函式隨著遊戲的規模會變得越來越龐大。 因此也許你可以用一個獨立的檔案來定義及維護它。 最後談到事件的傳遞(報告)-並非只是由事件產生事件這麼簡單。 某些複雜的情況是內層的物件往外把事件"報告"上去。請求上層物件代為處理。 通常這些往外傳遞的事件會堆積在另一條串列上。並且通知外層物件來處理。 通常的做法是 1. 閃一個旗標-掛求救旗; 2. 使用外層物件的指標通知-熱線電話; 3. 或由外層物件來巡視-定期檢查; 備註一: 因此一開始提到的射擊事件會變成像這樣 InputEvent() { ... // 只是單純檢查狀態的就放在這裡檢查 if( true == FireLastTimer.IsZero() && // 檢查上一次的開火 false == PlayerIsCroach ) // 檢查人物是否蹲下 { HitID = 3DCollisionCheck( MouseX , MouseY ) ; // 檢查碰撞 if( -1 != HitID ) gAddEvent( Event_Fire , HitID ) ; // 開火事件 gAddEvent( Event_ShotDecal , MouseX , MouseY ) ; // 不管有無擊中都有彈痕特效 } ... } EventHandle() { switch() { case Event_Fire : if( 0 == LoadedAmmoNum )// 檢查填彈量 { gAddEvent( Event_ShowReload ) ;// 顯示reload事件 break ; } LoadedAmmoNum -= 1 ;// 扣填彈量 gAddEvent( Event_Sound_GunFire ) ;// 開火音效事件 gAddEvent( Event_SceneVibration ) ;// 場景震動事件 gAddEvent( Event_GunShaking ) ;// 控制器震動事件 switch( HitID.type )// 到底打到什麼東西 { case Enemy : gAddEvent( Event_HitEnemy , HitID ) ;// 擊中敵人事件 break ; case Medic : if( PlayerHP + 1 <= HP_MAX ) { PlayerHP += 1 ;// 簡單處理的就直接來 } gAddEvent( Event_Sound_Medic ) ;// 補血音效事件 gAddEvent( Event_MedicGlow ) ; // 補血特效事件 break ; case Weapon : gAddEvent( Event_GainWeapon , HitID ) ;// 得到武器事件 break ; case PowderBarrel : gAddEvent( Event_PowderBarrelExplosion , HitID ) ; // 炸藥桶爆炸事件 break ; } break ; } } 雖然還是很複雜,但是加上一點註解的話已經並非完全看不懂的狀態了。 而且,與企劃的文件開始有一點類似的結構. 備註二: 在沙漠商旅C中我是採取三層的架構。分別是 輸入(包含滑鼠點選)觸發輸入事件(點到各選單按鈕的事件), 輸入事件又觸發遊戲事件(真的到右轉左轉這種遊戲事件) -- "May the Balance be with U"(願平衡與你同在) 視窗介面遊戲設計教學( http://0rz.tw/V28It ),討論,分享。歡迎來信。 視窗程式設計(Windows CLR Form)遊戲架構設計(Game Application Framework) 遊戲工具設計(Game App. Tool Design ) 電腦圖學架構及研究(Computer Graphics)論文代讀(含投影片製作) -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 140.96.77.176 ※ 編輯: NDark 來自: 140.96.77.176 (01/29 19:57)
silveriii:推 01/29 20:00
Eior:推 01/29 22:03
elfkiller:推 01/29 22:41
chchwy:正好切中我目前的疑惑 大推 01/29 23:41
etrexetrex:推 01/29 23:51
marksswy:獲益良多~ 推推 01/30 11:02
geken:推 01/30 17:10
※ 編輯: NDark 來自: 61.224.54.199 (01/31 00:24)
zzzdeath:受益許多,推! 01/31 12:24
wangm4a1:推 01/31 17:27
poorsen:推 01/31 23:42
NDark:彈痕特效那邊有點寫錯.如果有改版會再修正 02/01 09:57
chchwy:推推 02/06 12:47
biowave:像BIO5就沒有"狀態",整個gameplay顯得很智障 02/08 21:41
fasthall:樓上的是什麼意思 不太懂@@ 02/10 13:04