1.面向实时处理的概念
面向实时处理的方法是专门为实时系统设计的脚本组织方法。面向实时处理的方法包含3类对象:实时处理(下面简称处理)、库、组。它们以处理为中心完成相应的工作。从形式上说,处理和库都是实现了特定功能的代码片段,而组能够包含任意数量的处理和库,同时也能够包含其他的组。这三类逻辑单元组成了一棵描述整个游戏逻辑的“逻辑树”。“逻辑树”可以有任意层,每一层可以有任意多的结点。处理和库只能是树的叶子结点,只有组能够有子结点,游戏项目就是树的“根”。处理有两种状态:激活态和非激活态,当处理处于激活态时,每一帧都会被执行一次,否则将不会被执行。在处理的执行过程中可以激活或停止其他的处理,处理之间严格的通过树型结构访问。库用于定义全局的方法和属性,所有的库在脚本系统初始化的时候被载入。库中可以包含全局属性和方法以外的代码片段,但是该片段只会在脚本系统初始化时被执行一次,可以将整个游戏系统的初始化代码放在库中编写。组本身没有任何执行功能,但是仍然可以包含文本,可以在组中编写文档。
面向实时处理的方法与面向过程以及面向对象的方法属于不同的范畴。面向对象和面向过程的方法是对问题域的描述方法,而面向实时处理的方法是脚本程序的组织方法,其关键在于直接定义了一组在激活状态下能够自动循环执行的脚本代码片段:实时处理。
下图描述了消息机制和面向实时处理机制的区别和联系:
使用这个机制主要出于这样几点考虑:
1.当需要将一个角色在指定时间内移动到指定位置时,只要激活一个预先写好的处理,这个处理每次执行都把角色向指定位置移动一定的距离,并在角色到达指定位置后自动关闭。
2.这样可以降低处理之间的耦合,因为主处理只要调用一次激活/关闭方法就可以完成一个持续一段时间的处理过程。
2.为什么面向实时处理的方法能够加快游戏开发速度
游戏软件是典型的实时系统。在游戏软件中程序的运行状态时时刻刻都在发生着变化。下面以一个简单的例子说明游戏的实时处理。
假设游戏中需要一个角色在1秒钟内从a点移动到b点。很显然的,为了完成这个过程需要一个循环,该循环每执行一次就把角色向b点移动一小段距离并且把它“画”出来。一般说来所有的实时程序都有一个“主循环”,该循环负责更新并“画”所有的物体。由于整个游戏会有大量不同种类的更新操作,主循环可能会变的十分臃肿和难以维护。虽然可以通过建立一个机制将需要更新的物体有机的组织在一起(比如说提供一个继承层次),并通过固定的方法对各个物体进行更新。(比如说通过一组虚函数)但是这种方法仍然难以避免的会增加大量冗余的代码。(为了增加一种处理你可能需要增加一个类)如何才能够在不修改主循环并且不增加大量冗余代码的前提下增加各种新的实时处理,一直是游戏开发中难以有效解决的问题。
脚本作为主程序的扩展一直是游戏领域广泛使用的技术,本课题使用Lua语言作为游戏引擎的脚本语言,利用脚本语言扩展游戏引擎的功能,并通过对“实时处理”的组织,(即3.1中所提到的树型组织方式)有效的解决了实时处理的扩展问题。由于处理能够自动循环执行的特性,使得脚本的编写者能够将注意力集中在如何为游戏设计各种处理,而不是如何将各种处理“集成”到主循环中。
3. 如何使用游戏集成开发环境进行面向实时处理的游戏开发
本节通过一个例子进一步说明面向实时处理的方法,该例子直接在本课题的“游戏集成开发环境”中编写,实现了一部分角色扮演游戏所必须具备的基本功能。在讲解例子的同时演示了“游戏集成开发环境”的使用方法。
3.1. 场景设计过程
在正式开始编写脚本代码之前,我们至少需要建立一个场景,然后才能在这个场景中实现各种游戏逻辑。建立场景的过程十分简单,不用编写一行代码就可以完成。所有工作都是在“场景视图”中通过各种属性编辑器完成的。图3-1显示了“场景视图”的构成。场景设计过程按照:“新建游戏对象->编辑物理属性->编辑渲染属性”的顺序进行直到所有组成场景的“物体”都被正确的添加。
场景视图
3.2. 实时处理的设计
在场景的视觉效果基本满意后可以开始进行游戏逻辑的设计工作。游戏逻辑的设计是在“脚本视图”中完成的。图3-2显示了脚本视图的基本构成。从图中可以看出脚本视图主要
脚本视图
由处理浏览器、脚本编辑器、引擎层次浏览器组成。处理的设计是指:利用处理浏览器构造出描述游戏逻辑的“处理树”,这个过程应该尽可能在编码开始之前完成,因为事实上这也是对整个游戏逻辑进行概要设计的过程。当然,如果在一开始无法拿出准确设计,在编码进行到一定阶段再修改处理的层次结构也是可行的。处理浏览器可以在任何时候修改处理树的层次结构。
下面将举例说明如何具体的设计处理的层次。
在这个例子中我们打算实现这样几个功能:
1. 当用户按住鼠标右键的时候,角色会转向鼠标所指的方向,并且按照一定的速度向该方向移动,在角色移动的过程中播放角色行走的骨骼动画。
2. 在角色行走过程中碰到障碍物时不能穿过,而应该沿着障碍物的边缘继续移动。
3. 角色移动时应该能够按照地面的高度调整自身的高度。
4. 当角色移动的时候镜头始终转向角色,当角色离镜头距离过近时镜头应该能够自动远离角色,反之应该靠近角色。
5. 当用户滚动鼠标滚轮的时候,镜头会按照滚轮的方向以一定的速度围绕着角色旋转。
6. 当角色走在水面上时,应该可以在水里看见角色、天空以及周围环境的倒影,甚至能够看见天空中云彩的飘动。
在处理的设计过程中最重要的就是从需求中导出独立的处理。可以用以下几点来大致的说明设计处理的原则:
1. 如果一种需求要求在一个时间段内完成相应的工作,那么就可以将其作为
一个独立的处理。(如需求1,3,5)
2. 如果几个需求面向同一组数据或是属于同一类问题,应尽可能的在一个处理中完成这些需求,这样可以有效的防止处理之间相互篡改数据,保证处理之间的独立性。(如需求1和3以及4和5)
3. 有些处理和某些独立的数据紧密相关,或着处理着同一类的问题。此时可以将其放置在同一个分组下,充分利用树型结构提高可读性。(如需求6)
4. 在编码开始之前应该充分考虑哪些功能已经可以通过引擎提供的库实现,因为整个游戏开发环境是建立在游戏引擎的基础之上的,编码过程本质上就是对引擎功能的再一次组织和扩展。(如需求1、2、3、4、5、6)
根据上面所提到的各项基本原则,可以得出如下图所示的处理层次结构:
处理层次结构
3.3. 开始编码
由图3-3可以发现整个树结构中只有一个处理处于默认激活状态:全局处理。(左边的选择框处于选中状态)这个处理就好比是一个“主函数”,负责“管理”其他的处理。但是,需要强调的是处理与处理之间并不是“调用”关系。每个处理独立的处理自己的任务,一个处理只能激活或关闭其它的处理,而不能进行别的干涉。保持默认状态下只有一个处理处于激活状态可以使程序看起来更加清晰易懂。
为了使得代码更加容易阅读和编写,我在开发环境中使用各种不同的颜色标注不同类型的关键字。比如:系统常量是灰色,注释是绿色,引擎方法和Lua标准库是红色,用户关键字是黑色等。同时,为了避免用户记忆大量的引擎方法,开发环境还提供了辅助输入系统。如图3-4所示。
图3-4辅助输入系统
任何集成开发环境都必须具备程序的调试功能。本课题的游戏集成开发环境同样具备了程序调试的功能,不但可以查出所有的语法错误,而且不会放过任何运行时错误,也包括各种造成引擎异常的错误调用等。能够准确无误的指出出错的代码行,如图3-5所示。
下面简要的介绍各个处理的代码。
3.3.1. 全局处理
if
CurrentStageId==-1
then
InitStage()
local
p=
Mode.GetProcessByPath
("
更新镜头
")
--
激活更新镜头的处理
Mode.SetProcessActived
(p,
true
)
local
p=
Mode.GetProcessByPath
("
角色移动
")
--
激活角色移动的处理
Mode.SetProcessActived
(p,
true
)
local
p=
Mode.GetProcessByPath
("
场景
1:
动态场景更新
")
--
激活场景更新的处理
Mode.SetProcessActived
(p,
true
)
end
全局处理调用了一个函数:InitStage(),该函数在“场景1:初始化”库中定义。之后激活了三个处理,这三个处理将在脚本运行过程中独立执行。
只要输入函数的前几个关键字就会出现如图3-4所示的辅助窗口,这时选择正确的关键字就可完成输入。
3.3.2. 全局变量定义
maincharacter=
nil
--
主角游戏对象
ground=
nil
--
地面游戏对象
CurrentStageId=-1
--
当前场景号
-1
表示还没有装入任何场景
CameraAngleDelta=0
--
摄象机需要旋转的角度
所有全局变量的定义都被放在该库中定义。
3.3.3. 场景1:初始化
function
InitStage()
Game.LoadFromFile
("s1//1.gal")
--载入场景
Physical.SetDrawPhyicalObjects
(
false
)
--不绘制辅助物体,如包围盒等
--设置全局变量
maincharacter=
Game.GetGameObjByName
("S1//MAINCHARACTER.GAM")
ground=
Game.GetGameObjByName
("S1//TEST3.GAM")
CurrentStageId=1
--设置本场景的碰撞检测属性
local
mainRobj=
Game.GetRenderByIndex
(maincharacter,0)
local
mainpobj=
Game.GetPhysicalByIndex
(maincharacter,0)
local
fountainpobj=
Game.GetPhysicalByIndex
(
Game.GetGameObjByName
("S1//FOUNTAIN.GAM"),0)
local
housepobj=
Game.GetPhysicalByIndex
(
Game.GetGameObjByName
("S1//WOODHOUSE.GAM"),0)
Physical.AddCollisionObject
(mainpobj,fountainpobj)
—
加入碰撞列表
Physical.AddCollisionObject
(mainpobj,housepobj)
—
加入碰撞列表
Common.SetCameraMode
(
CAMERA_FREE
)
end
这个库由一个函数的定义组成,只要是在库中定义的方法都可以在任何一个处理中调用。InitStage()方法完成了场景的初始化工作。
3.3.4. 角色移动
if
maincharacter==
nil or
ground==
nil then
Common.EngineERR
(
"
主角或地形没有设定
")
return
--
只在场景和主角被正确设置后才能运行
end
local
mainpobj=
Game.GetPhysicalByIndex
(maincharacter,0)
local
mainRobj=
Game.GetRenderByIndex
(maincharacter,0)
-------------------------------------------------------------
--首先保持主角的高度始终和地面一样
local
groundpobj=
Game.GetPhysicalByIndex
(ground,0)
Physical.ChangeHeightToGroundMesh
(mainpobj,groundpobj)
-------------------------------------------------------------
--
检测鼠标右键是否按下,
没有按下则停止播放动画
if
Common.GetMouseButtonState
(
MOUSE_RBUTTON)
~=
true then
Render.SetAniSpeed
(mainRobj,0)
return
end
-------------------------------------------------------------
--按下则移动角色,否则停止播放骨骼动画
Render.SetAniSpeed
(mainRobj,4)
-------------------------------------------------------------------
--计算鼠标指向的方向,移动角色
local
x,y=
Common.GetMousePos
()
local
MouseInWorldSpace,success=
Common.ScreenPosToWorldSpace
(x,y)
if
success==
false then return end
--鼠标在窗口外点击
local
upvec=
VEC3.new
(
0,1,0)
local
chplane=
PLANE.CreateFromPointNormal
(
Physical.GetPos
(
mainpobj),upvec)
local
MouseProjectOnChPlane=
PLANE.IntersectLine
(chplane,
Common.GetCameraEye
(
),MouseInWorldSpace)
-------------------------------------------------------------------
--保证只有观察点在角色上方时才起作用
if
VEC3.Dot
(
VEC3.Subtract
(MouseInWorldSpace,
Common.GetCameraEye
()),upvec)<0
then
Physical.FaceTo
(mainpobj,MouseProjectOnChPlane)
--
使角色正面面向鼠标的世界空间位置在角色平面
(right front
所在平面
)
上的投影
Physical.MoveForward
(mainpobj,10,0.1)
end
3.3.5. 更新镜头
if
maincharacter==
nil
or
ground==
nil
then
Common.EngineERR
("主角或地形没有设定
")
return
--
只在场景和主角被正确设置后才能运行
end
-------------------------------------------------------------------
--
初始化变量
local
MaxCameraDistance=400
local
MinCameraDistance=50
Common.CameraTurn
(0
)
--
将摄象机放置在初始位置
local
mainpobj=
Game.GetPhysicalByIndex
(maincharacter,0)
local
mainpos=
Physical.GetPos
(mainpobj)
--
记录角色的位置
local
camerapos=
Common.GetCameraEye
()
--------------------------------------------------------------------
--
保证观察点始终指向角色
Common.SetCameraLookAt
(mainpos)
--
保证视野范围
<400
-------------------------------------------------------------------
local
DistanceToCamera=
VEC3.Distance
(
mainpos,camerapos)
--
角色到观察点的距离
if
DistanceToCamera>MaxCameraDistance
--
距离大与
400
then
local
n=
VEC3.Normalize
(
VEC3.Subtract
(camerapos,mainpos) )
--
计算当前观察方向
camerapos=
VEC3.Add
(mainpos,
VEC3.Mult
(n,MaxCameraDistance) )
Common.SetCameraEye
(camerapos)
end
-------------------------------------------------------------------
--
保证视野范围
>50
if
DistanceToCamera<MinCameraDistance
--
距离小于
50
then
local
n=
VEC3.Normalize
(
VEC3.Subtract
(camerapos,mainpos) )
--
计算当前观察方向
camerapos=
VEC3.Add
(mainpos,
VEC3.Mult
(n,MinCameraDistance))
Common.SetCameraEye
(camerapos)
end
-------------------------------------------------------------------
--保持观察点在角色上方100单位
local
x,height,z=
VEC3.GetValue
(camerapos)
local
chX,chY,chZ=
VEC3.GetValue
(mainpos)
--
获得角色高度
if
height<chY+50
then
--
保证最小高度为角色高度
+100
VEC3.SetValue
(camerapos,x,chY+50,z)
Common.SetCameraEye
(camerapos)
end
-------------------------------------------------------------------
--
保持角色和观察点在
XZ
平面上的距离
>100
local
dx=x-chX
local
dz=z-chZ
local
DistanceInXZ=(dx^2+dz^2)^0.5
if
DistanceInXZ<100
then
--保证XZ距离大与100
local
aspectsXZ=dx/dz
local
newdz=100/(1+ aspectsXZ^2)^0.5
if
dz<0
then
newdz=-newdz
end
local
newdx=newdz*aspectsXZ;
local
newx=chX+newdx
local
newz=chZ+newdz
local
cx,cy,cz=
VEC3.GetValue
(camerapos)
VEC3.SetValue
(camerapos,newx,cy,newz)
Common.SetCameraEye
(camerapos)
end
-------------------------------------------------------------------
--
鼠标滚轮旋转视角
if
Common.GetMouseWheel
()~=0
then
CameraAngleDelta=
Common.GetMouseWheel
()/360
end
local
speed=2
--
旋转速度
local
AngleThisFrame=speed*
Common.GetElapsedTimeSinceLastFrame
()
--
计算本此旋转值
if
math.abs
(CameraAngleDelta)>
math.abs
(AngleThisFrame)
then
if
CameraAngleDelta<0
then
AngleThisFrame=-AngleThisFrame
end
Common.CameraTurn
(AngleThisFrame)
CameraAngleDelta=CameraAngleDelta-AngleThisFrame
end
3.3.6. 场景更新
if
CurrentStageId==1
then
Game.SetAnimation
(
Game.GetAnimationByName
(
"skyrotation"),
Common.GetTime
(
)*0.03,
true)
end
这里实际上只用了一条语句就完成了动态场景的更新,这是因为所有的动态更新已经在场景视图中使用动画编辑器以所见即所得的方式制作出来,这里只需要调用相应的方法循环播放现有的动画就可以了。