作者NDark (溺於黑暗)
看板GameDesign
标题[心得] 容易被遗忘的游戏设计模组(2)
时间Fri Jan 29 19:57:19 2010
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)
1F:推 silveriii:推 01/29 20:00
2F:推 Eior:推 01/29 22:03
3F:推 elfkiller:推 01/29 22:41
4F:推 chchwy:正好切中我目前的疑惑 大推 01/29 23:41
5F:推 etrexetrex:推 01/29 23:51
6F:推 marksswy:获益良多~ 推推 01/30 11:02
7F:推 geken:推 01/30 17:10
※ 编辑: NDark 来自: 61.224.54.199 (01/31 00:24)
8F:推 zzzdeath:受益许多,推! 01/31 12:24
9F:推 wangm4a1:推 01/31 17:27
10F:推 poorsen:推 01/31 23:42
11F:→ NDark:弹痕特效那边有点写错.如果有改版会再修正 02/01 09:57
12F:推 chchwy:推推 02/06 12:47
13F:推 biowave:像BIO5就没有"状态",整个gameplay显得很智障 02/08 21:41
14F:推 fasthall:楼上的是什麽意思 不太懂@@ 02/10 13:04