2.3.1 物品、迷题和开关
非常高级的第一人称射击游戏世界需要给人一种“真实感”。理论上说,当你和你身边的所有物品发生交互时,无论是当你使用它的时候,激活它的时候,射击它的时候,向它投掷手榴弹的时候,还是其他的那些像你使用柳条箱或者计算机终端的时候,都应该有所反应。
如果你在墙上看到一个日光灯的开关,你就可以使用它来打开或者关掉日光灯。如果你想打开的那扇门是锁着的,并且你在这个房间的对面发现了一台计算机,那么你就很有可能使用这台计算机来打开这扇门。当一个手榴弹在附近爆炸时,柳条箱、木桶,以及其他各种存储容器(越有毒越好)都应该能够爆炸,至少也得破成碎片。当你使用桥梁上相应的控制杆时,桥梁应该能够伸长和缩短;当窗户被射击后,它应当破得粉碎;当日光灯被射中后,它应该破裂而且屋子里面变得更加昏暗。现在,你应该已经明白了。关键问题是游戏世界中的物品需要对你的行为做出反应,而且它们应该根据你选择的交互方式产生不同的反应。
但是,这并不完全是物品的破坏问题。有意思的是,炸毁木桶,打破玻璃,毁坏照明设施,和其他的游戏物品进行交互,同时也是游戏玩家提高自身级别的一种常用方法。伸长一座横跨深坑的桥梁可能要求你必须找到一个隐蔽的开关,找到那台计算机可能是解除围绕在你想摧毁的反应堆周围的保护物的唯一途径等。在这些情况下,这些物品就不再是自包含的,自行操作的实体了。现在,它们之间协调作用,创建出复杂而又相互关联的系统,甚至组合在一起形成了精心设计的谜题,如图2.11所示,这是一个第一人称射击游戏中模仿的走廊场景。在这样的场景中,脚本作为功能体连接在一起,形成了一个基本的通信网络。拉动控制杆就会给桥梁发送一条信息,要么告诉它伸长,要么告诉它缩短。同时,这座桥梁可能也想向控制杆发送信息,通知它变换位置。这种对象对对象的通信方式(object-to-object communication)在这种游戏中是很常见的。

图2.11 一个第一人称射击游戏中模仿的走廊场景
第一人称射击游戏通常使用迷题和开关来提高游戏的深度。当那些向外星人和野人投掷炸药的游戏变得非常老套之后,现在游戏玩家会更加关注那些对于智能的挑战。
解决方法
几乎第一人称射击游戏中的每样东西都具有一个相关联的脚本。这些脚本为游戏世界中的每个物体都提供了一些量身打造的功能,并在当这些物品和某种来自外部的力量发生联系的时候执行,这些外部力量包括爆炸带来的冲击波、几百发的子弹,或者是玩家的 手等。
在脚本内部,通过将代码段和事件相结合,功能函数得到了更好的定义和组织。这些事件通知脚本是谁或者是什么东西具体地调用了它,并允许基于这些信息采取适当的行动。事件是必需的,因为即便是最简单的物体也需要根据周围环境的不同采用不同的行为。当你只是轻轻地推动了一下那个柳条箱它就发生了剧烈的爆炸,这个并没有什么值得大惊小怪的;但是如果这个柳条箱受到了核导弹的袭击却仍然只滚动了几英尺,这个就有点令人费解了。
在典型的第一人称射击游戏中,事件与居住在游戏世界中的游戏玩家及其敌人的能力是有关系的。例如,游戏玩家可能能够完成下列动作:
● 射击:使用游戏玩家现已配备的武器进行射击。
● 使用:试图使用游戏玩家面前的一切物品。“使用”一个柳条箱可能作用很小或者没有什么作用,但是如果使用一台计算机却很有可能导致很多事情的发生。这些行为同样可以是合上开关,拉动控制杆,打开门等。
● 推动/移动:游戏玩家对他前面的任何物体施加一个很小的力以试图将物体四处移动。例如,如果游戏玩家想要摸到几英尺高处的通风孔,那么他或她可能就需要推动附近的一个柳条箱并用它来充当垫脚石。
● 碰撞:简单地说,也就是和其他东西走到了一起。与其说这是一个动作,还不如说这是某个无意中的行为所造成的结果。
这些通过使用事件影响正在讨论的目标,从而在事件和脚本之间基本上形成了很好的一对一对应关系。例如,射击一个柳条箱使得游戏引擎可以通过向柳条箱发送SHOT或者DESTROYED事件消息的方式来通知柳条箱对应的脚本程序它正遭到别人的射击。它甚至可能通知柳条箱这是一种什么类型的武器以及是谁在进行射击。使用一台计算机终端就可能会向终端的脚本程序发送一条USE事件信息,诸如此类。一旦这些事件被脚本接收到,它们就会调用特定的一段代码,随后也就会采取相应的动作。下面看一些代码实例,它列举3个物品的脚本代码:一个是柳条箱的,一个是开门使用的开关的,另外一个是用于电栅栏的。
为了便于说明这些事例,我们假定这里有一个结构体,它里面包含每个物体的属性,例如这个物体的可视性和具体的位置信息。同样,每个事件(event)也具有一个结构体,这个结构体包含有该事件的相关信息,例如,事件的类型、导致事件发生的实体、力的方向和大小。很明显,InvokingEvent是一个事件的实例,它由主应用程序(游戏引擎,the game engine)自动地向每个事件脚本的main()函数进行发送。
下面描述的就是柳条箱:
/*
* 柳条箱
*
* 游戏玩家可以射击和摧毁柳条箱,同时他们也可以四处移动这种箱子
*/
main ( Event InvokingEvent )
{
switch ( InvokingEvent.Type )
{
case SHOT:
{
/*
柳条箱已经被击中,从而也就被摧毁了,因此,我们首先要使它消失
*/
this.bIsVisibile = FALSE;
/*
现在来通知游戏引擎在柳条箱原来所在的位置产生一个爆炸效果
*/
CreateExplosion ( this.iX, this.iY, this.iZ );
/*
为了使得这个效果变得更加完美,我们需要通知游戏引擎以爆炸地点为中心
在周围产生一些爆炸后的木头碎片
*/
CreateParticleSystem ( this.iX, this.iY, this.iZ, WOOD );
break;
}
case PUSH:
{
/*
某个东西或者是某个人正在移动这个柳条箱,这涉及到的不过就是一个按照它的方向移动柳条箱的问题。我们假定游戏引擎能够处理碰撞检测问题。力矢量中存放有沿着每个坐标轴方向的力,因此,我们需要实现的也就是将它们加到柳条箱的位置坐标之中。
*/
this.iX += InvokingEvent.ForceVector.iX;
this.iY += InvokingEvent.ForceVector.iY;
this.iZ += InvokingEvent.ForceVector.iZ;
}
}
}
接下来描写的是门的开关:
/*
* 门的开关
*
* 你可以射击并摧毁它,它同时也可以被用于打开和关上一扇门
*/
main ( Event InvokingEvent )
{
switch ( InvokingEvent.Type )
{
case SHOT:
{
/*
为了方便起见,我们认为这个开关非常容易损坏
如果击中了这个开关就会摧毁它并且使得它毫无用处
*/
this.bIsBroken = TRUE;
/*
为了使事情变得更加逼真,我们就来编写一段能够产生塑料碎片的小程序
*/
CreateParticleSystem ( this.iX, this.iY, this.iZ, PLASTIC );
break;
}
case USE:
{
/*
这是开关的主要功能。我们假定大门存放在一个数组之中,并且假定我们想要打开和
关上的这一扇门位于索引值为0的地方
*/
if ( Door [ 0 ].IsOpen )
CloseDoor ( 0 );
else
OpenDoor ( 0 );
break;
}
}
}
最后是电栅栏:
/*
* 电栅栏
*
* 它的作用是使任何一个接近它的人或者物品受到电击
*/
main ( Event InvokingEvent )
{
switch ( InvokingEvent.Type )
{
case COLLIDE:
{
/*
因为电栅栏的唯一作用就是电击任何一个接触到它的人或物体,因此它也就只需要对
COLLIDE事件进行响应。本质上,这就意味着它会减少任何一个接触到它的物体的健
康值。事件结构体将会通知我们到底是哪个实体(这里包括游戏玩家和敌人)接触到
了这个电栅栏
*/
Entity [ InvokingEvent.iEntityIndex ].Health -= 10;
/*
不过,如果没有视觉效果,那么这个电栅栏又有什么意思呢
*/
CreateParticleSystem ( this.iX, this.iY, this.iZ, SPARKS );
PlaySound ( ZAP_AND_SIZZLE );
}
}
}
好了,现在你已经全部实现这3种物品了。这就是3个功能齐全的第一人称射击游戏世界中的物品,它们将会被安装或者放置到外部走廊、军事区域,或者是战场上。就像你可以看到的那样,这个系统的核心就是游戏引擎要具备将事件信息传送给脚本的能力。一旦这个实现了,游戏世界中的物体在游戏期间就可以通过游戏引擎相互之间进行通信,并进而形成动态的、逼真的游戏系统。开关可以打开门,游戏玩家和敌人可以炸毁煤油桶,其他的一些事情也都可能会发生。
基于事件的脚本通信(event-based script communication)是一个非常重要的概念。在后面的章节中,我们将会多次涉及到这个名词。事实上,我们正在讨论一个即将获得广泛应用的内容。






