iPhone 游戏开发教程 游戏引擎 (6)是本我要介绍的内容,继续上一章开始介绍,本节主要介绍了事件的相关内容,先来看本文详解。
解决高层次事件
一旦判定了用户执行的物理动作,你的代码必须能将它们转换为游戏逻辑组件可以使用的形式。具体怎么做需要依赖于你的游戏的上下文,但是这里有几种典型的形式:
如果玩家准备控制虚拟人偶,在玩家和游戏之间通常会有连续的交互。经常需要存储当前用户输入的表现形式。比如,如果输入装置为遥杆,你可能需要在主循环中记录当前点的x轴坐标和y轴坐标,并修正虚拟人偶的动量。玩家和虚拟人偶之间的是紧密地耦合在一起的,所以控制器的物理状态代表着虚拟人偶的高层次的状态模型。当遥杆向前拨动时,虚拟人偶向前移动;当“跳跃”按钮按下时,虚拟人偶跳起。
如果玩家正与游戏地图进行交互,那么需要另外一种间接的方式。比如,玩家必须触摸游戏地图中的一个物体,代码必须将玩家在屏幕上的触摸坐标转化为游戏地图的坐标以判定用户到底触摸到了什么。这可能只是简单的将y轴坐标减去2D摄像机坐标的偏移量,也可能是复杂到3D场景中的摄像机光线碰撞侦测。
最后,用户可能进行一些间接影响到游戏的动作,如暂停游戏、与GUI交互等。这时,一个简单的消息或者函数会被触发,去通知游戏逻辑应该做什么。
游戏逻辑
游戏逻辑是游戏引擎中是你的游戏独一无二的部分。游戏逻辑记录着玩家状态、AI状态、判定什么时候达到目的地、并生成所有的游戏规则。给出两个相似的游戏,他们的图像引擎与物理引擎可能只有细微差别,但是它们的游戏逻辑可能会有很大差异。
游戏逻辑与物理引擎紧密配合,在一些没有物理引擎的小游戏中,游戏逻辑负责处理所有物理相关内容。但是,当游戏引擎中有游戏引擎的时候,需要确保两者的独立。达到此目的的最好方式就是通过物理引擎向游戏逻辑发送高层次的游戏事件。
高层次事件
游戏逻辑代码应该尽可能仅处理高层次问题。它不应该处理当用户触摸屏幕时需要以什么顺序将什么描画到屏幕上,或者两个矩形是否相交等问题。它应该处理玩家希望向前移动,什么时候一个新的游戏物体应当被创建/移除以及当两个物体相互碰撞后应该做什么。
为了维持概念上的距离,处理低层次概念(诸如用户输入与物理引擎等)的代码应当创建高层次的消息并发送给游戏逻辑代码去处理。这不仅能保持代码的独立性与模块化,还会对调试有所帮助。通过查看高层次消息传递的日志,你可以判定是没有正确处理消息(游戏逻辑代码的问题),还是没有在正确的时机传送消息(低层次代码问题)。
一个非常基本的传递高层次消息的技术是写一个String并传递它。假如玩家按下了上箭头键,它的虚拟人偶必须向上移动。
- void onPlayerInput( Input inputEvt ) {
- if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP ) {
- g_myApp->sendGameLogicMessage( "player move forward" );
- }
- }
虽然上面的代码对程序员来说通俗易懂,但对于电脑来说却并不高效。它需要更多的内存与处理,远比实际需要的多。我们应该用提示来替代用户输入方法。比起一个字符串,它使用一个"type"和"value"。由于可能的事件都是结构化的和有限的,因此我们可以使用整数和枚举类型来我们消息中的事件信息。
首先,我们定义一个枚举类型来标识事件类型:
- enumeration eGameLogicMessage_Types {
- GLMT_PLAYER_INPUT,
- GLMT_PROJECTILE_WEAPON,
- GLMT_GOAL_REACHED,
- };
接着我们再创建一个枚举类型来标识事件的值:
- enumeration eGameLogicMesage_Values {
- GLMV_PLAYER_FORWARD,
- GLMV_PLAYER_BACKWARD,
- GLMV_PLAYER_LEFT,
- GLMV_PLAYER_RIGHT,
- GLMV_ROCKET_FIRED,
- GLMV_ROCKET_HIT,
- };
现在我们定义一个结构体来存储我们的消息数据:
- view plaincopy to clipboardprint?struct sGameLogicMessage {
- short type;
- short value;
- } Message;
现在,我们就可以像上一个例子代码一样,用一个对象来传递我们的消息:
- void onPlayerInput( Input inputEvt ) {
- if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP ) {
- Message msg;
- msg.type = GLMT_PLAYER_INPUT;
- msg.value = GLMV_PLAYER_FORWARD;
- g_myApp->sendGameLogicMessage( msg );
- }
这看起来作了更多的工作,但它运行起来会更有效率。前一个(坏的)例子用了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。
比如,让我们以玩家移动为例。新手程序员可能写出这样的代码:
- void onPlayerInput( Input inputEvent ) {
- if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP) {
- //apply movement based on the user input
- playerAvatar.y += movementSpeed;
- }
- }
每当玩家按下按键,虚拟人偶像前移动一点。这是基于帧的逻辑,因为每次移动的变化都会潜在的伴随着新的帧。事实上,在这个的例子中,每次玩家输入事件都会发生移动。这或多或少有点像主循环的迭代。移动的可视化影响只有在主循环的下次迭代中才会反映,所以任何迭代中间的虚拟人偶移动都会浪费计算。让我们做一下改进:
- void onPlayerInput( Input inputEvent ) {
- if(inputEvt.type == IE_KEY && inputEvt.value == KEY_UP) {
- //save the input state, but don't apply it
- playerAvatar.joystick = KEY_UP;
- }
- if(inputEvt.type == IE_KEY_RELEASE) {
- playerAvatar.joystick = 0;
- }
- }
- void Update() {
- //update the player avatar
- if( playerAvatar.joystick == KEY_UP ) {
- playerAvatar.y += movementSpeed;
- }
- }
现在我们知道,在键被按下的过程中,每次游戏循环中都只会被赋予一次速度。但是,这仍然是基于帧的逻辑。
基于帧的逻辑的问题是,帧变化不会总是以相同的时间间隔发生。如果在游戏循环中,渲染或者游戏逻辑会比通常耗费更多的时间,它可能会被推迟到下一次循环中。所以,有时你需要有60帧每秒(fps),有时,你只需要30fps。由于移动是适用于帧的,有时你只会以通常的一半速度来移动。
你可以用基于时间的逻辑来准确的表达移动。通过记录自从上次帧更新的时间,你可以适用部分移动速度。用这种方式,你可以以每秒为单位来标识移动速度,而不必关心当前帧速率是多少,玩家虚拟人偶的速度是一致的:
- void Update( long currTime ) {
- long updateDT = currTime - lastUpdateTime;
- //update the player avatar
- if( playerAvatar.joystick == KEY_UP ) {
- //since currTime is in milliseconds, we have to divide by 1000
- // to get the correct speed in seconds.
- playerAvatar.y += (movementSpeed * updateDT)/1000;
- }
- lastUpdateTime = currTime;
- }
在这个例子中,移动速度的总量将会是相同的,不管是2fps还是60fps。基于时间的逻辑需要一点额外的代码,但是它可以使程序更精确而不必在乎暂时的延迟。
当然可以用基于帧的逻辑来开发游戏。重要的是,不要混合它们。比如,如果你的图形代码使用基于时间的逻辑来渲染玩家虚拟人偶的移动动画,但是游戏逻辑代码却使用基于帧的逻辑在游戏世界中来移动它,这样移动的动画将不能玩玩家移动的距离完全同步。
如果可能的话,请尽量移除基于帧的逻辑。基于时间的逻辑将会对你有更大的帮助。
游戏逻辑组织结构
游戏逻辑代码的核心功能就是管理游戏状态的规则与进度。根据你的游戏设计,这可能意味着任何事情。但是,还是有一些基本模式基于制作的游戏的类型。
游戏逻辑不与任何一个特定的类相关联,它游戏状态对象中表现出来。当主游戏状态被初始化后,它将会为关卡载入与初始化必要的资源。例如猜谜游戏中的一组提示与单词、玩家虚拟人偶的图片数据以及玩家当前所在区域的图片数据。在游戏循环中,游戏逻辑将会接受用户输入,运行物理模拟,并负责处理所有的碰撞结局消息,模拟AI动作,执行游戏规则。最后,当应用程序需要终止主游戏状态,它会释放释放所有的游戏资源,并可能将游戏状态保存到硬盘驱动器上。
根据游戏的复杂度,你可能会发现很方便进一步分解游戏逻辑。比如,如果你在开发一款冒险游戏,你可能有一个充满环境数据(地面、建筑、河流、树等)、可以移动、与玩家交互的实体(玩家虚拟人偶、敌人、非玩家角色、开关、障碍物等),各种GUI使玩家作出特殊动作和显示重要信息的游戏世界。每种游戏特征都必须有大量的代码。虽然它们合在一起才能组成完整的游戏,但是你还是可以保持它们的工作模块化。
你可以创建一个Level Manager类来处理游戏关键,包括载入和卸载显示在游戏世界中的物理与图像数据与调用游戏引擎来侦测实体与游戏世界的碰撞。你还可以创建另外一个类或者一些类来处理游戏世界中存在的实体。每个类都载入和卸载渲染那些物体的必要的物理和图片数据,以及包括控制它们的AI。
最后,你可能创建另外一个单独的类来处理游戏中用户交互,以保持代码与三大概念独立。
这个体系结构适用于任何类型的游戏。首先评估游戏设计的主要特性,接着以某种方式组合,将相近的功能与数据组合在一起。
小结:
小结:iPhone 游戏开发教程 游戏引擎 (6)的内容介绍完了,希望本文对你有所帮助!你应该对创造一个游戏引擎时必须完成的任务有了一个基本的理解。这将会帮助我们在下一节创建这些元素,为我们的游戏做准备。 想要深入了解iPhone 游戏引擎的更多内容,请参考以下几篇文章: