BIG WORLD 客户端编程指南01
BIGWORLD_05_Client Programming Guide_01
1. 概述
本文档是3d引擎技术的客户端引擎的技术设计概述。它是描述整个系统的一组更大的文档的一部分。为了提供上下文,它只包含对BigWorld服务器的引用。只对BigWorld客户端工作感兴趣的读者可能会忽略服务器信息。
有关API的信息,请参阅在线文档。
1.1. Client in context
BigWorld客户端提供了 客户端/服务端架构的最终用户体验。在BigWorld客户端/服务器实现中,客户端通过Internet使用UDP/IP连接到服务器。
BigWorld客户端为用户提供逼真的沉浸式3D环境。该环境的内容是存储在客户机中的静态数据和从服务器发送的动态数据的组合。
用户通过自己控制的角色与世界进行互动。角色的移动和动作被传递给服务器。由其他连接用户控制的虚拟化身是服务器发送的动态数据的一部分。
开发人员可能会选择将客户端与他们自己的服务器技术集成,但如果他们这样做,他们将不得不解决BigWorld架构已经解决的问题,比如:
- 统一的碰撞场景(客户端和服务端端使用)。
- 统一的客户机/服务器脚本(用于客户端和服务端)。
- 生成服务器和客户端内容的工具。
- 优化的低带宽通信协议。
1.2. Outline
客户端初始化自己,连接到服务器,然后在它的主线程中运行一个标准的游戏循环(每一次迭代被称为一个帧):
- 接受输入
- 更新世界
- 绘制世界
框架的每一步(frame)描述如下:
• Input
使用DirectInput从附加的输入设备接收输入。在BigWorld客户端/服务器实现中,使用WinSock 2通过网络接收来自服务器的输入。
• Update
更新世界以说明自上次更新以来所经过的时间。
• Draw
游戏世界是使用3D引擎Moo绘制的,它使用的是Direct3D(版本9c)。具体操作请参见3D引擎。
许多其他对象也属于世界的“更新然后绘制”系统。其中包括十几种与天气和大气影响(雨、星星、雾、云等)有关的脚本辅助系统(瞄准、战斗)、水池、脚印和阴影。
还有其他线程用于后台场景加载和其他异步任务。
1.3. Resource search paths
BigWorld客户端是一个通用的可执行文件,可以通过游戏特定的资源进行完全配置。这些资源的位置必须提供给客户端,这样它才能正确初始化。
通常,至少需要指定两个资源路径-你的项目特定的资源路径和BigWorld资源路径(提供公共资源,如标准着色器,字体,脚本等)。例如,如果你的游戏位于“my_game”中,你将定义的两个资源路径是:
- my_game/res
- bigworld/res
当引擎试图访问一个资源时,它将按照资源树被赋予引擎的顺序查找每个资源树。例如,如果客户端脚本请求名为sets/models/foo的资源。模型它尝试以下位置:
my_game/res/sets/models/foo.model
bigworld/res/sets/models/foo.model
搜索将在找到第一个有效文件时停止。因此,可以通过将bigworld/res中指定的资源放置在my_game/res中的相同位置来覆盖它。
有两种方式可以指定搜索路径:
1.3.1. paths.xml
paths.xml是一个XML文件,引擎将在启动时查找它,它包含一个资源路径列表。客户机将首先尝试打开当前工作目录中的paths.xml。如果在当前目录中找不到它,那么它将尝试在与客户端可执行文件相同的文件夹中打开paths.xml。
1 | <root> |
路径是相对于paths .xml的位置定义的。
1.3.2. Command line switch
默认情况下,客户端将查找上面演示的paths.xml。然而,这可以通过命令行使用——res开关来重写。多条路径以分号分隔。例如:
1 | "bwclient.exe" --res ../../../my_game/res;../../../bigworld/res |
这些路径必须相对于可执行位置而不是当前工作目录来定义。
1.4. Configuration files
配置文件是相对于资源文件夹列表中的一个条目(或<res>)定义的。
1.4.1. File resources.xml
这个文件定义了客户端引擎运行所需要的游戏特定资源。
xml中的条目按照列出的顺序从资源文件夹列表(或<res>)中的各种条目中读取。只有缺失的条目才会在后续的文件夹中读取它们的值。
在bigworld/res文件夹下存在一个默认文件,它的任何资源都可以通过创建自己的文件<res>/resources.xml来覆盖。
下面的例子说明了这种机制:
1.4.2. File .xml
文件
这个文件的实际名称和位置是由resources.xml的<engineConfigXML>标记定义的。
xml文件的位置始终是相对于资源目录列表(或<res>)中的一个条目定义的,将按照它们列出的顺序对其进行搜索。
例如:
在XML文件的主要部分下,必须包含一个个性标签,命名要使用的个性脚本。
这个文件中包含的数据作为init方法的第二个参数传递给个性脚本(详情参照init),以DataSection对象的形式。
1.4.3. File .xml
XML文件
这个文件的实际名称和位置是由resources.xml的scriptsConfigXML标记定义的(参照
这个文件中包含的数据作为init方法的第一个参数传递给个性脚本。
1.4.4. File <preferences>.xml
文件<preferences>. XML用于保存视频和图形设置的用户首选项,具有预定义的语法。
该文件还嵌入了一个数据部分(称为scriptsPreference),脚本可以使用它来保持游戏首选项——这个部分没有固定的语法。
该文件的实际名称和位置在resources.xml的engineConfigXML标记指定的文件中定义,在preferences项标记中。
默认情况下,perferences XML文件是相对于客户端可执行位置的,但这可以通过指定pathBase子标记更改为许多其他基本路劲(例如,它可以被定义为相对于用户的My Documents目录)。基本路径可以为首选项标记的可选子标记。preferences.xml的可用路径基础是:
- EXE_PAHT——preferences XML文件将相对于客户端可执行文件的位置进行存储。如果没有提供,这是默认位置。
- CWD——preferencesXML文件将相对于当前工作目录存储。注意,如果工作目录在运行时发生更改,它将保存在新的工作目录中。
- ROAMING_APP_DATA——preferences XML文件将相对于当前用户的漫游AppData目录存储(Roaming)。换句话说,如果用户在域上,当用户登录和退出Windows时,数据将与域控制器同步。
- LOCAL_APP_DATA——preferences XML将相对于当前用户的本地AppData目录进行存储。
- APP_DATA——这与ROAMING_APP_DATA相同。
- MY_DOCS——preferences XML将相对于当前用户的My Documents目录进行存储。
- MY_PICTURES——preferences XML将相对于当前用户的My Pictures目录进行存储
- RES_TREE——preferences XML将相对于paths.xml中找到的第一个资源路径进行存储。
- 该文件的scriptsPreference中包含的数据以DataSection对象的形式,作为init方法的第三个参数传递给个性脚本。
1.5. Coordinate System
左手坐标系,大拇指是y轴正方向,四指方向为x正半轴,手心方向为z正半轴。
- yaw——y轴的旋转,正是右边,负数是左边。
- pitch——x轴的旋转,正是向下,负是向上。
- roll——z轴的旋转,正是左边,负数是右边。
2. User Input
BigWorld客户端结合使用Windows消息和用于键盘和鼠标输入的Windows原始输入API。它读取键盘上的上键、下键和字符按下事件,以及鼠标上的高分辨率移动事件。它使用DirectInput API从操纵杆读取按钮和轴的移动事件。
2.1. Key events
Key事件,由KeyEvent对象(BigWorld。(Python中的KeyEvent),由具有键或按钮的设备生成。这包括键盘、鼠标按钮和操纵杆按钮。
键事件的两种基本类型是key-down和key-up。
2.1.1. Character events
如果一个KeyEvent是由键盘生成的,它可能会附加一个字符。由特定键生成的字符由操作系统中当前设置的区域设置和输入语言决定,并由KeyEvent表示。字符成员(Unicode字符串)。
2.1.2. Auto-repeat
根据用户的操作系统设置(例如重复延迟),当按键被按下时,键盘将自动生成重复事件。鼠标和操纵杆不会产生自动重复事件。
自动重复事件作为额外的按下键事件发送,但是不希望处理重复事件的脚本可以调用KeyEvent。isRepeatedEvent方法来确定它是否是一个自动重复事件。
2.1.3. Sequence of events
当用户按下一个按钮(键盘、鼠标或操纵杆)时,事件序列如下:
- 第一个是key-down事件。
- 如果保持按下该键,则由于自动重复(仅键盘)而引发多个按下键事件。
- 当用户释放按钮时,会触发一个key-up事件。
用户输入模块的输出由许多其他模块处理,这些模块依次检查事件并使用或忽略它们。如果一个事件没有被任何模块使用,那么它将被丢弃。在事件中轮到的模块的顺序如下:
- Debug —— 特殊的keys、控制台等
- Personality script —— 全局的keys
- Application —— 硬编码的键,如QUIT。
- Player script —— 剩下的,是处理的主要部分。
注意,GUI系统不会自动接收输入,而是由脚本编写来选择何时接收输入。这可以是在个性脚本中,也可以是在玩家脚本中。最明显的地方是在个性脚本回调中,例如在个性脚本的handleKeyEvent中,您应该调用GUI.handleKeyEvent()并检查返回值。
类似地,活动摄像头也不会自动接收输入事件。由Python脚本决定相机何时何地接收用户输入。
2.1.4. Sinking events
BigWorld客户端执行事件匹配以确保模块行为的一致性。如果一个按下键事件被一个模块使用,那么该模块的标识符将被记录为事件键号的接收器。当相应的自动重复和key-up事件到达时,它将直接交付到该模块。例如,如果聊天控制台在玩家运行时弹出(并插入到列表中),用户随后释放了运行键,那么玩家脚本仍然会获得该键的key-up事件,并能够停止运行操作。
在某些情况下,可能希望暂时阻止将某些关键事件传递到脚本中。GUI脚本可以通过删除当前GUI屏幕并用新的GUI屏幕替换它来处理按下键事件。默认情况下,因为当handleKeyEvent返回时,新的GUI屏幕已经成为活动屏幕,旧的GUI任何相关的自动重复和键上事件将被发布到新的GUI屏幕,创建可能不想要的行为。
BigWorld.sinkKeyEvents函数可以可用于停止给定键代码的所有键事件到达脚本,直到(包括)下一个键上。
2.1.5. Mouse cursor position
当一个关键事件发生时,通常需要知道鼠标在哪里(即,而不是处理事件时鼠标在哪里),特别是在处理鼠标按钮事件时。因此,鼠标光标的位置可以通过KeyEvent.cursorPosition属性获得,应该尽可能用来代替GUI.mcursor().position。
2.2. Mouse
2.2.1. Movement
高分辨率的鼠标移动事件作为MouseEvent发送到脚本。该对象提供了三个方向增量。
- dx和dy成员是有符号整数,表示鼠标在X和Y方向的移动。
- dz成员表示鼠标滚轮的移动。
如果在一帧内从驱动程序到达多个鼠标增量,它们会累积到一个MouseEvent中。
与KeyEvent对象类似,事件发生时的鼠标光标位置可以作为MouseEvent对象的成员使用。
2.2.2. Buttons
鼠标按钮作为一个KeyEvent发送,但是它们不生成自动重复事件。
2.3. Joystick
BigWorld客户端将自动检测连接到系统的第一个操纵杆设备,并被设计用于双操纵杆风格的操纵杆。
2.3.1. Axis events
当轴事件发生时,它们将通过handleAxisEvent角色脚本函数作为AxisEvent对象发送到脚本。
2.3.2. Buttons
操纵杆按钮作为一个KeyEvent发送,但是它们不生成自动重复事件。
2.3.3. Controlling player direction
c++引擎会自动将轴事件传递给活动光标,因此使用BigWorld.setCursor将方向光标设置为活动光标,就可以通过操纵杆控制方向光标。方向光标将处理由右轴生成的任何事件。
2.3.4. Avatar movement
c++引擎也将给予物理子系统处理轴事件的机会。物理将轴输入作为一种特殊情况,并将根据用户向前推动操纵杆的距离来调整移动速度。
为了使操纵杆支持运动物理,设置Physics.joystickEnabled属性为True,并确保将joystickFwdSpeed和joystickBackSpeed属性设置为适合你的游戏的值。
3. Cameras
在一般的更新过程中,相机更新的位置是一个微妙的问题,因为相机依赖于一些组件已经在它之前更新,而其他组件依赖于相机在它们之前更新。
从概念上讲,相机有四种类型:fixed camera、FlexiCam、cursor camera和free camera。前三个是客户端控制的视图,范围从最小的用户交互到最大的用户交互。最后一个镜头是完全由用户控制的,但并不是真正的游戏玩法的一部分。
然而,只有三个相机类:FlexiCam、CursorCamera和FreeCamera。固定摄像机是用FlexiCam对象实现的。它们都派生自一个公共的BaseCamera类。
每次只有一个活动摄像头。个性脚本通常处理摄像机管理,因为摄像机是全局的,但任何脚本也可以处理摄像机,而玩家脚本通常就是这样做的(虽然通常是间接地,通过个性脚本)。
Python完全可以访问基类和所有派生类。任何摄像机都可以被创建并设置为脚本所希望的任何位置,包括另一个摄像机的位置。这在切换摄像机类型以删除任何不想要的“跳切”时特别有用。
3.1. The Cursor Camera
cursor camera是在游戏世界中跟随角色的摄像机。它总是将自己定位在一个以角色头部为中心的球体上。它主要与方向光标一起工作,以便在角色头部的方向上面对摄像机。你可以使用任何MatrixProvider来代替方向光标,也可以使用任何MatrixProvider来代替玩家的头部。
方向光标是一个输入处理程序,它将用户的设备输入转换为在一个看不见的球体上移动的假想光标的操作。这个光标由pitch和yaw来描述。它产生一个指向向量,从玩家角色在世界空间中的头部位置延伸到光标的pithc和yaw。方向光标是一个很有用的工具,它允许在不同的设备上进行瞄准。与其让每个设备影响摄像机和角色网格,不如让每个设备与方向光标对话,从而影响其在世界中的视觉向量。游标摄像机、目标跟踪器和动作匹配器然后读取方向游标,以获取关于需要做什么的信息。
光标摄像机占据了方向光标在球体上的位置,将线延伸回角色的头部,并沿着这条线直到它与另一侧的球体相交。这个交点就是光标相机的位置。摄像机的方向总是方向光标的方向。
游标摄像机是抽象概念InputCursor的一个实例。任何时候只能有一个活动的InputCursor, BigWorld会自动将键盘、操纵杆和鼠标事件转发给它。在启动时,光标摄像头默认是活动的InputCursor。您可以使用BigWorld.setCursor方法在任何时候更改活动的InputCursor。(例如将InputCursor改为鼠标指针)。
3.2. The Free Camera
自由相机是一个自由漫游的相机,它既不局限于空间中的一个固定点,也不跟随玩家的角色。它由鼠标(控制方向)和键盘(控制移动)控制,并允许用户在世界各地飞行。自由相机具有惯性,以便在运动中提供平滑、渐进的过渡。它不是一个游戏镜头,但对游戏的调试、开发和演示很有用。
3.3. The FlexiCam
FlexiCam是一个灵活的摄像机,可以跟踪游戏世界中的角色。它总是将自己定位在一个特定的点上,相对于角色的方向,并且总是看着一个特定的方向,相对于角色的脚的方向。
它被称为FlexiCam,因为它的移动有一定的弹性,可以直观地感受到速度。这使得它在追赶车辆时特别有用。
4. Terrain
BigWorld客户端使用的地形系统与分块系统巧妙地集成在一起。它允许以艺术的方式创建各种各样的地形,并有效地管理。有两种不同的地形渲染器,针对不同的机器规格。它们被称为高级地形和简单地形。
4.1. Advanced Terrain
4.1.1. Overview
这种先进的地形引擎使用一种高度地图,将高度地图分成100米乘100米的区块。每个块由高度和纹理信息、法线、洞图和LOD信息组成。
4.1.2. Key Features
可配置的高度映射分辨率
无限数量的纹理级别与可配置的混合分辨率和投影角度
可配置法线映射分辨率
可配置孔图分辨率
每像素照明
LOD系统
- 地形多重贴图和几何图形
- 法线贴图LOD
- 纹理LOD
- 高度图LOD
4.1.3. Texturing
纹理是通过将多个纹理层混合在一起来完成的,每个纹理层都有自己的投影角度和混合值来与其他纹理层混合。混合值的分辨率可按空间进行配置,而层本身则按块存储。纹理被假定为rgba与alpha通道用于高光值。
4.1.4. Lighting
高级地形的照明是按像素执行的。法线贴图存储在每个块上,用于照明计算,这与混合纹理结合输出最终的颜色。地形允许高达8漫射和6个镜面光每个块。关于如何计算高光照明的细节,请参照Terrain specular lighting。
4.1.5. Shadows
地形使用了一个地平线阴影地图进行阴影遮盖,这个地图存储了两个角度(东和西),在这两个角度之间有一个从地形上畅通无阻的天空视图。在地形着色器中,这些角度根据太阳角度进行检查,只有当太阳角度落在地平线角度之间时,太阳光线才会被应用。
4.1.6. LOD
LOD系统的目的是减少cpu和gpu渲染地形所花费的时间,地形LOD系统通过减少距离中的几何和纹理细节并根据需要加载/卸载高分辨率资源来实现这一点。
LODing被资源分割,这样纹理和几何细节就可以分别进行流处理。LOD距离在空间中可配置。配置文件请看 “terrain section in space.settings”
4.1.6.1. Geometry
几何LOD是通过geo-mipmaps和geo-morphing实现的。Geo-mipmaps是由地形块的高分辨率法线图生成的。根据距离摄像机的x/z距离,显示较低分辨率的地形块。为了避免在不同分辨率的高度地图之间改变时弹出,我们使用了地理变形,这允许引擎在两个高度地图关卡之间平滑地插入。退化三角形被插入到不同大小的块之间,以避免闪光。
4.1.6.2. Collision Geometry
碰撞几何使用两个不同的分辨率进行流处理(streamed)。低分辨率的碰撞总是存在的,而高分辨率的碰撞则取决于它们与摄像机的x-z距离。
4.1.6.3. Texture
纹理LOD是通过将多层混合替换为一个自上而下的地形块图像来实现的。基于与相机的x-z距离平滑地融合LOD图像。自顶向下的图像是在World Editor中生成的。
4.1.6.4. Normal maps
法线贴图LOD是通过使用低分辨率和高分辨率贴图来实现的。低分辨率的法线贴图总是可用的,而高分辨率的贴图则是基于与相机的x-z距离进行混合。法线贴图是在World Editor中生成的。LOD法线贴图的大小是法线贴图分辨率的16倍或32x32,取较大的值。
4.1.7. Memory footprint
由于高级地形允许许多配置选项,地形的内存占用取决于所选择的选项。
在Fantasydemo的例子中,地形开销如下(这些信息是通过Fantasydemo客户端中的资源计数器获取的,图像设置为高,远平面设置为1500米):
(这包括纹理层使用的纹理,也可能被其他资产使用)
Component | Size |
---|---|
Conllison data | 42453517 |
Vertex buffers | 6169008 |
Index buffers | 536352 |
Texture layers | 57541905 |
Shaowmaps | 26361856 |
LOD textures | 35148605 |
Hole maps | 4096 |
Normal maps | 6572032 |
Total | 174787371 |
地形的内存使用表
4.1.8. terrain2 resources
一个terrain2部分包含在一个chunk的.cdata文件中。它包含块中地形的所有资源。不同类型的地形数据将在以BNF格式进行描述。
4.1.8.1. heights sections
高度部分存储地形块的高度地图。在块中存储多个高度部分,每个LOD级别一个,每个高度部分以前一个分辨率的一半存储数据。高度部分被命名为“heights?” ?为具体高度。最高的分辨率高度图存储在名为heights的区域中,第二高的存储在名为heights1的区域中,一直到存储2x2高度的地图。这样,如果高度地图分辨率是128x128, 7个高度地图存储在文件中(heights, heights1,…heights6)
1 | <heightMap> ::= <header><heightData> |
<magic>
uint32 0x00706d68 (string “hmp\0”)
<width>
包含宽度数据的Uint32
<height>
包含高度数据的Uint32
<compression>
(未使用)包含压缩类型的uint32
<version>
包含数据版本的Uint32,当前为4
<minHeight>
浮点数,包含此块的最小高度
<maxHeight>
浮点数,包含此块的最大高度
<padding>
4个字节的填充使头部16字节对齐
<heightData>
PNG压缩块,长度为int32,存储高度,单位为毫米,尺寸=宽度*高度
4.1.8.2. layer sections
层段存储地形块的纹理层。多个层段存储在地形块中。每个部分描述一个纹理层。
图层部分命名为“layer ?”?被一个大于1的数字所代替。例如,如果块有3个层,那么3个层的section将被存储(“layer1”, “layer 2”, “layer 3”)
1 | <textureLayer> ::= <header><textureName><blendData> |
<magic>
uint32 0x00646c62 (string bld/0”)
<width>
包含宽度数据的Uint32
<height>
包含高度数据的Uint32
<bpp>
(未使用)uint32,包含层数据中条目的大小
<uProjection>
Vector4包含纹理层u坐标的投影
<vProjection>
Vector4包含纹理层v坐标的投影
<version>
包含数据版本的Uint32,当前为2
<padding>
12个字节的填充使头部16字节对齐
<length>
texturename字符串的长度
<string>
此层使用的纹理的名称
<blendData>
PNG压缩的uint8块定义了纹理层在每个x/z位置的强度
4.1.8.3. normals & lodNormals sections
法线部分存储地形块的高分辨率法线贴图。lodNormals部分存储高度块的LOD法线,LOD法线通常是法线大小的1/16。
1 | <normals> ::= <header><data> |
<magic>
uint32 0x006d726e (string “nrm/0”)
<version>
包含数据版本的Uint32,当前为1
<padding>
8个字节的填充使头部16字节对齐
<data>
对于法线的x和z分量,PNG压缩块存储2个符号字节,y分量在着色器中计算
4.1.8.4. holes section
洞部分存储地形块的全息图,只有当地形块中有洞时,这个部分才存储。
1 | <holes> ::= <header><data> |
<magic>
uint32 0x006c6f68 (string “hol/0”)
<width>
包含数据宽度的Uint32
<height>
包含数据高度的Uint32
<version>
包含数据版本的Uint32,当前为1
<data>
将洞数据存储在一个宽度*高度的位域中,数据中的每一行被四舍五入到1字节。如果位设置为1,则表示映射中有一个洞。
4.1.8.5. horizonShadows section
horizonShadows部分存储了地形块的地平线阴影。
1 | <shadows> ::= <header><data> |
<magic>
uint32 0x00646873 (string “shd/0”)
<width>
包含数据宽度的Uint32
<height>
包含数据高度的Uint32
<bpp>
(未使用)数据中每个条目的位数
<version>
包含数据版本的Uint32,当前为1
<padding>
12个字节的填充使头部16字节对齐
<data>
阴影数据,(uint16,uint16) 宽度,高度,水平阴影数据存储两个角度,它们之间没有任何地形或物体遮挡。
4.1.8.6. lodTexture.dds section
lodTexture.dds部分存储地形块的LOD纹理。LOD纹理是所有纹理层混合在一起的低分辨率快照。纹理以DXT5格式存储。关于dds纹理格式的更多信息请参考DirectX文档。
4.1.8.7. dominantTextures section
dominantTextures部分存储了主要的纹理图。 dominant texture地图存储地形块中每个x/z位置的最高混合纹理。
1 | <dominant> ::=<header><texNames><data> |
<magic>
uint32 0x0074616d (string “mat/0”)
<version>
包含数据版本的Uint32,当前为1
<numTextures>
包含主导纹理映射引用的纹理数量的Uint32
<texNameSize>
包含纹理条目大小的Uint32
<width>
包含数据宽度的Uint32
<height>
包含数据高度的Uint32
<padding>
8个字节的填充使头部16字节对齐
<texNames>
texNameSize大小的numTextures条目包含在这个地图中引用的主要纹理的名称。比texNameSize短的纹理名称用0填充
<data>
存储为一个压缩的bin节。宽度*高度的字节数组,每个条目都是纹理名称的索引,该索引索引了条目x/z位置的主要纹理
4.1.9. terrain section in space.settings
空间中的 space.settings 文件包含地形的配置选项。lodInfo和server部分中的值可以修改,但是根级别的值只能由World Editor修改。
1 | <version> 200 (int) </version> |
<version>
地形的版本,对于高级地形,这个值是200
<heightMapSize>
每个地形块的高度图的大小,这个值是4到256之间2的幂
<normalMapSize>
每个地形块的法线贴图的大小,这个值是32到256之间2的幂
<holeMapSize>
每个地形块的洞图的大小,可以是256以内的任何值
<shadowMapSize>
每个地形块的阴影贴图的大小,这个值是32到256之间2的幂
<blendMapSize>
每个地形块的混合贴图的大小,该值是32到256之间2的幂
<lodInfo>
本节包含地形LOD系统的配置
<startBias>
这个值是geo-morphing开始的偏差值,这个值定义一个LOD级别从哪里开始淡出到下一个级别。这个值是两个lodDistance之间差异的一个因素。
<endBias>
这个值是geo-morphing结束时的偏差值,这个值定义一个LOD级别在哪里完全淡出到下一个级别。这个值是两个lodDistance之间差异的一个因素。
<lodTextureStart>
这是混合LOD纹理的起始距离,在此距离之前,混合层用于渲染地形。
<lodTextureDistance>
这是lodtexture混合的距离,这个值是相对于lodTextureStart。
<blendPreloadDistance>
这是预加载混合的距离,这个值相对于lodTextureDistance和lodTextureStart
<lodNormalStart>
这是混合LOD法线的起始距离
<lodNormalDistance>
这是完整法线贴图混合的距离,这个值相对于lodNormalStart。
<normalPreloadDistance>
这是预加载完整法线贴图的距离。该值是相对于lodNormalStart和lodNormalDistance的
<defaultHeightMapLod>
这是要加载的高度贴图的默认LOD级别,0 =完整的高度贴图,1 =半分辨率,2 =四分之一分辨率等等。
<detailHeightMapDistance>
这是加载全高度地图的距离
<lodDistances>
本部分包含几何LOD距离。
<distance>
距离部分定义了每个几何LOD级别调和出来的距离。distance0表示第一个LOD等级,distance1表示第二个LOD等级,以此类推。LOD级别之间的距离必须至少是地形块对角线距离的一半(~71),这是因为我们只支持相邻块之间1 LOD级别的差异。
<server>
这部分包含服务器所使用的信息
<heightMapLod>
这定义了在服务器上加载哪个LOD级别,这个值用于加速服务器上的加载。
4.2. Simple Terrain
4.2.1. Key features
- 与 chunking system一起工作。
- 单个网格可从磁盘和内存寻址。
- 基于4x4米的网格(tiles),匹配门户(portal)的尺寸。
- 低内存占用。
- 低磁盘占用。
- 快速绘制。
- 自动混合。
- 简单的工具集成。
- 分层地形与平铺纹理为了简单的条带创建。
- 使用纹理投影来应用纹理坐标以节省顶点内存。
4.2.2. Overview
地形是一个巨大的高度地图,由规则的高度杆网格定义,每4 × 4米。地形被组织成100x100米的地形块。每个这些块可以有多达4个纹理,它们是在每个顶点(即每个极点)的基础上混合的。地形也是自遮蔽的,允许在上面挖洞,比如洞穴开口。地形还包含详细信息,以便详细对象可以匹配地形类型。
4.2.3. Chunking
地形与块体恰当地结合在一起:每个地形块是100x100米,这是外部块体的大小。地形块存储在单独的文件中,这样就可以根据需要打开它们。
4.2.4. Disk footprint
每个地形块包含一个块,每个块的尺寸为100x100米。它包含28x28的高度、混合、阴影和细节值(有两个额外的行和一个列允许边界插值)。每个地形块也存储25x25洞的值,每个4x4m瓷砖一个洞。
下表显示了每个区块的地形成本:
Component | Size calculation | Size |
---|---|---|
Headers | 256 (header) + 128 x 4 (texture names) | 768 |
Height | 28 x 28 x sizeof( float ) | 3136 |
Blend | 28 x 28 x sizeof( dword ) | 3136 |
Shadow | 28 x 28 x sizeof( word ) | 1568 |
Detail | 28 x 28 x sizeof( byte ) | 784 |
Hole | 25 x 25 x sizeof( bool ) | 625 |
Total | 10017 |
每个区块的地形成本
例如,对于一个15 × 15千米的世界,地形的总磁盘大小为:10017 × 150 × 150(Bytes) 约215MB。
4.2.5. Memory footprint
在视野为500m的情况下,每个地形块覆盖100x100米,一个典型的场景在任何时候都需要内存中大约160个地形块。
这么多地形的内存使用大约是2MB,加上数据管理开销。
4.2.6. Texture spacing
terraintexturespace标签包含在文件<res>/resources.xml的环境部分(具体查看1.4.1 resources.xml副本中条目的优先级)决定了纹理地图在应用于地形时将被拉伸/收缩的大小(以米为单位)。
这个值决定了纹理贴图的长度和高度。
4.3. Terrain specular lighting
镜面照明方程为:
1 | SpeClr = TerSpeAmt * ( (SpeDfsAmt * TerDfsClr) + SunClr) * SpeRfl |
下面的列表描述了每个变量:
SpeClr (高光颜色)
最终的颜色反射。
TerSpeAmt (镜面)
数值由4个地形纹理的alpha通道的加权混合给出。
SpeDfsAmt (镜面散射量)
初始值存储在效果文件bigworld/res/shaders/terrain/terrain.fx中的变量specularDiffuseAmount中。
它的值可以在运行时通过render/terrain/specularDiffuseAmount进行调整。
一旦获得了所需的结果,就可以将新值存储在效果文件中。
TerDfsClr (地形的漫反射r)
数值由4个地形纹理的RGB通道的加权混合给出。
SunClr (阳光颜色)
阳光照射下的颜色。
SpeFlr (镜面反射)
镜面反射值在给定像素,由镜面功率系数调整。
作为公式的结果,少量的地形漫反射颜色(TerDfsClr)被添加到阳光颜色(SunClr),以提供高光颜色(SpeClr)。
高光照明功率的初始值存储在效果文件bigworld/res/shaders/terrain/terrain.fx中的变量specularPower中。它的值可以在运行时通过render/terrain/specularPower进行调整。一旦获得了所需的结果,就可以将新值存储在效果文件中。
请注意,镜面功率只能在着色器硬件版本2.0及以后进行调整。着色器硬件的早期版本被限制在4的高光功率值(这是着色器硬件版本2.0和更高版本的默认值)。
最终应用于地形的高光照明的数量受文件bigworld/res/shaders/terrain/terrain.fx中的可变specMultiplier的影响。设置为1以外的任何东西来重新缩放高光照明,或0来完全禁用它。
地形高光照明可以通过TERRAIN_SPECULAR图形设置来关闭。
5. Cloud shadows
在BigWorld客户端引擎中绘制的所有对象都受到云阴影的影响。这个效果是按每个像素应用的,并使用作为投影在世界上的引擎中的纹理源来执行。
这个光照贴图通过在文件bigworld/res/shaders/ std_effects/stdinclude.fxh中定义的宏公开给效果系统。
默认情况下,纹理源被命名为skyLightMap,因此可以通过以下命令被Python访问:
1 | BigWorld.getTextureFeed( "skyLightMap" ) |
5.1. Requirements
云阴影需要每个材质有一个额外的纹理层。虽然固定函数管道对大多数材质支持这一点,但在凹凸映射和镜面对象上的云阴影需要四组以上的纹理坐标,这意味着对于凹凸映射对象,它将只在像素着色器2和更高版本上工作。
5.2. Implementation
天空灯光图由C++文件src/lib/romp/sky_light_map.cpp中的代码计算,并在绘制过程中由天空模块更新。
它通过自动效果常数暴露于效果引擎中。只有当创建新云或当前云集下风向移动超过25%时,光照图才会更新。
在更新之间,投影纹理坐标会随着风速滑动,这样云的阴影看起来就会随着云移动。所有的效果文件都包含了云的阴影效果,包括地形和植物。
5.3. Effect File Implementation
有两个效果常量公开在效果文件中,以帮助天空光映射:
- SkyLightMapTransform
设置“World to SkyLightMap”纹理投影。使用这个常量将x,z世界顶点位置转换为u,v纹理坐标。
SkyLightMap
显示天空光贴图纹理以创建效果文件。
文件bigworld/res/shaders/std_effects/stdinclude.fxh中有几个宏可以帮助将云阴影集成到效果文件中。
在下面的列表中描述了这些宏:
- BW_SKY_LIGHT_MAP_OBJECT_SPACE , BW_SKY_LIGHT_MAP_WORLD_SPACE
这些宏声明了纹理投影所需的变量和常量,包括:
- 世界空间摄影机位置
- 天空光贴图变换。
- 天空之光映射本身。
当使用在对象空间中执行照明的效果文件时(例如,如果您也在使用宏DIFFUSE_LIGHTING_OBJECT_SPACE),请使用变体BW_SKY_LIGHT_MAP_OBJECT_SPACE,那么它将声明世界矩阵。
BW_SKY_LIGHT_MAP_SAMPLER
此宏声明了在像素着色器中实现云阴影时使用的采样器对象。
BW_SKY_MAP_COORDS_OBJECT_SPACE,BW_SKY_MAP_COORDS_WORLD_SPACE
这些宏在给定的顶点位置上执行纹理投影,并将纹理坐标设置到给定的寄存器中。
请确保根据您所使用的宏集,在适当的参照系中传递这些位置。
BW_TEXTURESTAGE_CLOUDMAP
此宏定义了一个纹理阶段,它将前一个阶段的结果乘以适当的云阴影值。
它应在任何漫射照明计算之后使用,以及在任何反射或镜面照明之前使用。
SAMPLE_SKY_MAP
此宏对像素着色器中的天空光贴图进行采样,并返回一个一维浮点值,该值表示应该乘以漫反射照明值的量。
5.4. Tweaking
在根据当前云计算光照图之后,它被固定到一个最大值。这意味着云地图永远不会太暗,也不会对世界产生太大的影响。
例如,即使太阳在白天完全被云遮住,仍然会有足够的环境照明和来自云层本身的照明,这样太阳光仍然会起作用。
BigWorld观察客户端 Settings/Clouds/max 灯光贴图黑暗设置天空灯光贴图的最大值。值为1意味着天空光贴图能够完全模糊太阳(完全阴影)。默认值0.65表示BigWorld艺术家对云阴影的最佳值的最佳猜测。值为0将意味着云影对世界没有任何影响。
此值也将从灯光映射设置中的文件sky.xml中读取。它由最大的黑暗标签表示。有关天空光地图设置文件的详细信息,请参见22.1.4.1.2. Sky light map。
6. Chunks
由BigWorld客户端绘制的场景图是由空间的小凸块构建的。这样做有很多好处,包括易于流媒体、减少加载时间、并发世界编辑以及简化服务器场景更新和物理检查。
下面几节将描述分块系统的概念和实现。
6.1. Definitions
以下术语与BigWorld的分块系统有关:
空间是一个连续的三维笛卡尔介质。每个空间被分段分割成块,这些块占据整个空间,但不重叠。空间中的每个点都恰好在一个块中。空间按水平尺寸分为100x100米的柱,总垂直范围。独立空间的例子包括行星、平行空间、空间站和“独立的”公寓/地牢级别。
块是一种凸三维体。它包含了位于其中的场景对象的描述。场景对象包括模型、灯光、实体、地形块等,称为块项目。它还定义了构成其边界的一组平面。
列的外部块不受此限制,它只需要定义相邻列的四个平面。其他块的边界平面与网格方形重叠,用来构建内部空间划分的完整图像。
有些平面在它们上定义了protal,这表明通过它们可以看到相邻的块。
protal是一个多边形加上对通过该多边形可见的块的引用。它包含一个标志,用于指示是否允许对象通过它。有些protal的命名方式可能是脚本可以对它们进行地址定位,并更改它们的权限。
6.2. Implementation files
分块系统使用以下文件:
- 对于宇宙中的每个空间的一个 space.setting 文件(XML格式)
有关此文件语法的详细信息,请参见文档文件语法指南的space.settings。
<res>/spaces/<space>/space.settings
* 环境设置
* 网格正方形的边界矩形每个空间的多个.chunk文件(XML格式)
有关此文件语法的详细信息,请参见文档文件语法指南的 .chunk。
- <res>/spaces/<space>/XXXXZZZZo.chunk (o = outside)
- <res>/spaces/<space>/CCCCCCCCi.chunk (i = inside)
- 场景对象列表
- 使用的纹理集
- 边界平面和入口(包括对可见数据块的引用)
- 碰撞场景
每个空间的多个.cdata文件(二进制格式)
<res>/spaces/<space>/XXXXZZZZ.cdata
地形数据,如:
- 高度图数据
- 覆盖数据
- 使用的纹理
– or–
块中每个对象的照明数据的多个实例
- 静态照明数据
- 模型中每个顶点的颜色值
6.3. Details and notes
6.3.1. Includes
Includes在加载后是透明的(对客户端、服务器和脚本)。标签冲突可以通过向标签添加’_n’来处理,其中n是已经具有该标签的对象的数量。
Includes在遇到的地方内联展开,不需要有客户端或服务器的边界框。
世界编辑器不生成Includes。
6.3.2. Models
材质覆盖和动画声明仍然是模型文件的域。
6.3.3. Entities
只有已隐式实例化的实体才需要填写其ID字段。如果它为零或缺失,则从客户端池(如果是客户端实例化的实体)或创建单元格池(如果是服务器实例化的实体)为该实体分配唯一ID。
如果一个实体需要一个标签,则它必须在其正式类型定义中包含该标签作为一个属性。
6.3.4. Boundaries and portals
如果只有天空(渐变、云、太阳、月亮、星星等)要绘制在那里,就可以使用特殊块标识符heaven。如果要画地形的话,地球也一样。因此,外块有六面,上面是天块,下面是地块。
门户中没有块引用意味着它没有连接,并且不会在那里绘制任何东西。
如果一个块包含在另一个块中,那么它的边界平面将被忽略——只使用像它所包含的、模型、灯光和声音这样的东西。
内部门户意味着指定的边界不是实际的边界,而是它连接到的块(以及该块连接到的所有块)所占据的空间应该从逻辑上从该块拥有的空间中减去,由该块的非内部边界定义。这最初只用于“外部”块连接到“内部”块,但它可能很容易适用于“内部入口”,即“边界门户”的补充。
在门户定义中,二维多边形点的虚轴是由法线与u轴的交叉乘积找到的。
在边界定义中,法线应指向内。
6.3.5. Transforms
除边界框外,块中的所有内容都将在块的局部空间中进行解释(如在顶级变换部分中所指定的)。
6.3.6. Other items
以下项目也可以以块存在:
聚光灯
环境光
平行光
水(水体)
光晕(镜头光晕)
粒子系统
6.4. Loading and ejecting
在每一帧中,块管理器都会对它已经加载的所有块执行一个简单的图形遍历,以寻找要加载的新块。它沿着块之间的入口,跟踪它在扫描中“移动”的距离。它的扫描被限制在最大的可见距离内,即比较远的平面距离稍远一点。
它在这个遍历中找到的最近的卸载块是下一个加载的块。加载是在一个单独的线程中完成的,因此它不会干扰游戏的运行。类似地,任何超出扫描范围的数据块都可以被驱逐(卸载)。
6.5. Focus grid
焦点网格是围绕相机位置的一组柱子。每根柱子长100x100米,并与地形方格和外部块体对齐。焦点网格的大小刚好超过远平面。
例如,对于500米的远平面,焦点网格沿每个方向移动700米,使总共有14x14=196列。
聚焦网格中的列集取决于相机的位置。随着相机的移动,焦点网格处理不再在网格下的列,并“聚焦”于那些刚刚足够接近的列。
每一列包含一个船体树(hull tree)和一个四轴树(quad tree)。
6.5.1. Hull tree
船体树(hull tree)是针对凸船体(convex hunns)的一种二元空间(binary-space)划分树。它可以处理重叠的船体。它可以进行点测试和适当的线遍历。
船体树是由重叠于列的所有块的边界形成的。从这棵树中,可以快速地确定任何给定点所在的块。(例如,照相机的位置)
6.5.2. Quad tree
四叉树(quad tree)由重叠列的所有障碍的边界框(或者,可能是任何其他边界凸包)组成。此树用于碰撞场景测试。大块项目负责添加和实施障碍。目前只有模型和地形块项目添加了任何障碍。
如果一个块或障碍在多个列中,则将其添加到两个列的树中。
6.6. Collisions
使用障碍四叉树(quad tree)的焦点网格(focus grid),块空间类可以在其空间中扫描任何三维形状,并向回调对象报告所有的三角形碰撞。目前支持的3D形状是点和三角形,但任何其他形状都可以轻松添加。
最底层的碰撞检查由一个通用的障碍物界面处理,所以任何可能的障碍都可以添加到这个碰撞场景中,只要它可以快速确定另一个形状何时与它碰撞(在它自己的局部坐标中)。
有关更多详细信息,请参见文件bigworld/src/client/physics.cpp。
6.7. Sway items
摇摆项(sway item)是指受其他块项目的通过而影响的块项目。每当一个动态块项移动时,该块中的任何摇摆项都会得到调用它们的摇摆方法,从而指定移动的来源和命运。
目前唯一的用户是ChunkWater。它利用流过它表面的运动在水中产生涟漪。这就是为什么涟漪适用于任何类型的动态道具——从动态障碍/移动平台到玩家模型再到小子弹。
在mf/src/lib/chunk/chunk_water.cpp中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Constructor
*/
ChunkWater::ChunkWater() :
ChunkItem( 5 ),
pWater_( NULL )
{}
...
/**
* Apply a disturbance to this body of water
*/
void ChunkWater::sway( const Vector3 & src, const Vector3 & dst )
{
if (pWater_ != NULL)
{
pWater_->addMovement( src, dst );
}
}在mf/src/lib/chunk/chunk_item.hpp中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20...
class ChunkItem : public SpecialChunkItem
{
public:
ChunkItem( int wantFlags = 0 ) : SpecialChunkItem( wantFlags ) { }
};
...
typedef ClientChunkItem SpecialChunkItem;
...
class ClientChunkItem : public ChunkItemBase
{
public:
ClientChunkItem( int wantFlags = 0 ) : ChunkItemBase( wantFlags ) { }
};
...
ChunkItemBase( int wantFlags = 0 );
bool wantsDraw() const { return !!(wantFlags_ & 1); }
bool wantsTick() const { return !!(wantFlags_ & 2); }
bool wantsSway() const { return !!(wantFlags_ & 4); }
bool wantsNest() const { return !!(wantFlags_ & 8); }
7. Entities
实体是BigWorld关键的概念,它本身就涉及到一个大型系统。它们是客户端和服务器之间的链接,也是客户端和服务器之间的链接,也是BigWorld Technology最特别的功能。
本节仅处理客户端上的实体的管理方面。
根据实体类型,它可以存在于BigWorld的不同部分,如下所示:
Client-only
例如,一个安全摄像头道具,或一个信息图标。通过将“世界编辑器的属性”面板“仅客户端”属性设置为true来创建仅客户端实体。
Client and server
实体将在两个部分中处于同一位置。例如:player Avatar, NPCs,自动售货机。
Server-only
该实体将只在服务器上进行实例化。例如,一个NPC衍生点或远程传送目的地点。仅限服务器的实体在客户端上没有任何脚本。
7.1. Entity Manager
实体管理器存储两个实体列表:
Active List
包含当前世界中的实体,由服务器中转或由块文件指示。
Cached List
包含最近在世界上,但现在只是在客户的500米半径区域(AoI)之外的实体。
实体被缓存,以便如果它们在离开后不久返回客户端的AoI,服务器不必重新发送与该实体相关的所有数据;只需要更改的字段。
由于可能从服务器无序地收到消息,因此实体管理器对其订单不敏感。例如,如果一个实体进入了玩家的AOI,然后迅速离开它,BigWorld客户端的行为也会正确,即使它在其“输入AOI”消息之前收到了该实体的“离开AoI”消息。
实体管理器总是可以确定它应该从包的序列号接收到消息的相对时间,因为包是定期发送的。
7.2. Entity scripts
客户端上的实体脚本是来自BigWorld的Python类。实体类。这个基类公开了许多方法和属性,允许脚本控制实体的行为(例如,位置、方向和实体模型都通过BigWorld.Entity接口)。
除了公开方法和属性之外,当某些事件通过命名的事件处理程序发生时,客户端引擎还将通知实体脚本。
7.3. Entity resources
通常,每个实体类型都需要一些资源来操作,例如模型、纹理、着色器、声音或粒子系统。BigWorld客户端提供了几种方法来确保这些资源在实体进入世界时可用,从而避免使主线程停止。
7.3.1. Preloads
当客户端启动时,它将为每个实体Python模块查询一个名为perload的函数。此函数返回的资源名称将在客户端启动时加载,并在客户端的整个生命周期中保存在内存中(即它可以随时使用)。这对于常用的资产很有用,以避免以后可能出现的加载和重新加载。然而,这样做的代价是,客户端将需要更长的时间来启动,并使用更多的内存(如果资源在某个时候实际上没有被使用)。
若要使用预加载机制,请在相关的实体模块中创建一个名为preload的全局函数。它必须使用一个参数,该参数是一个包含要预加载的资源的Python列表。就地修改此列表(例如,使用list.append或列表连接),插入由客户端要预加载的每个资源的字符串名称。
例如:
1 | # Door.py |
可以预加载的资源的类型是:
- Fonts
- Textures
- Shaders
- Models
7.3.2. Prerequisites
当一个实体即将出现在客户端上的世界中时,引擎将对名为prerequisites的实体脚本执行一个回调。这允许实体脚本返回在实体进入世界之前必须加载的资源列表。这些资源由加载线程加载,以便不会中断渲染管道。
建议一个实体将其所需的资源作为先决条件进行公开,并在onEnterWorld的方法中加载它们。与使用预加载不同,先决条件不会泄漏引用,因此当实体离开该世界时,它将释放其资源。
实体管理器在允许实体进入世界之前调用Entity::checkPrerequisites 。此方法检查是否满足此实体进入世界的先决条件。如果他们满足,那么它就会开始满足他们的过程(如果还没有开始)。
请注意,当在实体上调用先决条件方法时,其脚本已经被初始化,并且已经设置了其属性。因此,该实体可以根据该实体的具体实例来专门化其先决条件列表。例如:
1 | def Door( BigWorld.Entity ): |
8. User Data Objects
用户数据对象(User Data Objects)是将用户定义的数据嵌入到块文件中的一种方法。每个用户数据对象类型都被实现为Python脚本的集合,以及一个基于xml的定义文件,它将这些脚本绑定在一起。这些脚本位于文件夹脚本下的资源树中。
用户数据对象与实体的不同之处在于它们是不可变的(即它们的属性不会改变),并且它们不会传播到其他单元格或客户端。这使得它们比实体要轻得多。
用户数据对象的一个关键特性是它们的可链接性。实体能够链接到用户数据对象,而用户数据对象能够链接到其他用户数据对象。这是通过在希望链接到另一个用户数据对象的用户数据对象或实体的定义文件中包含一个UDO_REF属性来实现的。
8.1. User Data Objects are Python script objects
每个用户数据对象都是一个Python脚本对象(PyObject)。根据用户数据对象的类型,它可以存在于BigWorld的不同部分,如下所示:
Client only
仅限客户端的用户数据对象是通过使用其定义文件中的域标记中的客户端域来创建的。仅限客户端的用户数据对象不应该有cell或base脚本。
对于一个仅客户端用户数据对象的例子,请参考CameraNode用户数据对象,实现在<res>/scripts/client/CameraNode.py和<res>/scripts/user_data_object_defs/CameraNode.def文件中。
Server only
仅在服务器上的用户数据对象仅在服务器上实例化,如果其域标记为CELL,则将在cell中实例化,如果域标记设置为BASE,则将在base实例化。
服务器用户数据对象的例子,请参考PatrolNode用户数据对象,实现在<res>/scripts/cell/PatrolNode.py和<res>/scripts/user_data_object_defs/PatrolNode.def文件中。
8.1. Accessing from the Client
客户端可以使用以下命令访问所有仅限客户端使用的用户数据对象:
1 | >>> BigWorld.userDataObjects |
返回一个Python字典,使用用户数据对象的唯一标识符作为键,并将其PyObject表示作为其值。用户数据对象的属性和脚本方法可以使用标准的点语法来访问:
1 | >>> patrolNode.patrolLinks |
9. Scripting
提供给脚本的工具极其重要,因为它们决定了客户端的通用性和可扩展性。对于脚本程序员来说,这个环境是客户端;就像对普通用户一样,窗口系统就是计算机。
脚本环境提供了尝试编写整个程序的很大的诱惑。这可能会很快导致程序缓慢而难以理解(特别是如果相同的编程规则不像C++一样应用于脚本语言时)。因此,我们建议只有在不需要在每帧调用时才能用脚本编写功能。此外,如果它是全局的,那么它应该在人格脚本中实现。因此,例如,一个全局聊天控制台将在人格脚本中实现,而一个目标系统,它需要检查每一帧的碰撞场景,最好在C++中实现。
重要的是,整个BigWorld技术的广泛集成允许游戏代码的快速开发和巨大的灵活性。
在BigWorld的Python集成中,垃圾收集被禁用了,因为垃圾收集是一种昂贵的操作,可以在任何时候发生,例如阻塞主线程并导致帧率峰值。
9.1. Functional components
本节从使用这些工具的脚本的角度,描述了(一般)C++实体的内容和服务。以下章节中描述的实体的功能组件如下:
Entity Skeleton(实体框架)
Python Script Object(python脚本对象)
Model Management(模型管理)
Filters(过滤器)
Action Queue(操作队列)
Action Matcher(行动匹配器)
Trackers (IK)(跟踪器-反向)
Timers and Traps(计时器和陷阱)
9.1.1 Entity Skeleton(实体框架)
实体类(entity.cpp)是C++容器,它将为特定实体使用的任何组件集合在一起。它从Python对象派生出来,并拦截某些访问,并将其不理解的访问传递给用户脚本。这允许脚本使用“自我”引用,透明地在它们自己(和其他实体)上调用C++函数。在服务器的单元格和基本组件中也使用了相同的集成技术。
这个类处理组件类所需的任何内务管理或粘合——就其他c++模块而言,它是实体的公共面。
该类的数据成员包括id、类型和位置。
9.1.2. Python script object
这是用户提供的脚本类的实例。该实体的类型将选择该类。它存储在实体类型的XML描述中定义的特定于类型的数据,以及脚本希望存储的任何其他内部使用的数据。
当从服务器发送字段数据时,此类将自动更新其字段(并通知其更改)。当从服务器(或客户端上的其他脚本)接收到消息时,将自动调用消息处理程序。实体类执行这种自动化——从脚本的角度来看,它似乎是自动的。
9.1.3. Model management
模型是BigWorld对网格的术语,再加上它上面使用的动画和动作。
模型管理组件允许实体管理在其位置绘制和动画的模型。模型可以在定义良好的连接点(硬点)上相互连接,或者它们可以独立存在。模型在加载时不会自动添加到场景中——它必须显式地放在场景中。
实体可以使用任意数量的独立(断开的)模型,但大多数将使用0或1。那些使用更多的,需要特殊的过滤器来合理地表现。
理解模型的最佳方法是熟悉它们的Python接口,Main → Client → BigWorld → Classes → PyModel
9.1.4. Filters
过滤器采用带有时间戳的位置更新,并对它们进行插值,以在任意时间生成实体的位置。
BigWorld只提供过滤器基类。游戏开发者将从中获得特定于游戏的过滤器。然后,每个实体可以从可用的变量中为自己选择一种过滤器。如果它需要,它可以动态地更改其过滤器。
每当一个移动更新来自服务器时,它就会被移交给选定的过滤器,以及为该事件计算的(现有的)游戏时间重建逻辑的时间。
过滤器还可以提供时间间隔和转换,例如,“在游戏时间x,向前移动y米,旋转z弧度,持续t秒”。这个过滤器(如果它足够聪明的话)可以将此整合到它的插值中。
过滤器还可以在流中的给定时间执行脚本回调。
过滤器完全可以从Python中访问。
9.1.5. Navigation
BigWorld通过客户端脚本提供了对一些导航方法的访问。BigWorld.navigatePathPoints()将返回给定源点和目标点之间路径的点列表,BigWorld.findRandomNeighbourPoint()和相关BigWorld.findRandomNeighbourPointWithRange()将返回连接的导航网格中返回一个随机点,该点将从给定点导航。
为客户端提供这些方法是为了允许使一些处理远离服务器,并允许移动更有响应性,从而消除了等待服务器提供路径的需要。
9.1.5.1. Configuring a Space to Use Navigation
如果没有使用客户端导航方法,加载导航网格将会导致不必要的额外内存使用(通常是10-50mb)。因此,默认情况下将不会加载导航网格。每个将使用导航的空间都必须配置为这样做。将以下部分添加到将使用客户端导航的任何space.settings文件中:
1 | <clientNavigation> |
这将确保客户端将加载与块数据一起存储的任何导航网格。
9.1.5.2. Distributing Navigation Meshes with the Client
如果没有为一个空间启用客户端导航,那么ResPacker将从客户端包的cdata文件中剥离导航网格。这是为了保持块资源文件尽可能小,以便于分发。
9.1.5.3. BigWorld.navigatePathPoints()
BigWorld.navigatePathPoints()将获取源点和目的地点,并返回它们之间的点的路径,这样,依次移动到每个点将导致一个实体成功地导航到目的地。
该方法具有以下语法:
1 | navigatePathPoints( src, dst, maxSearchDistance, girth ) |
这些参数如下:
src
Vertor3,其中包含当前空间中的源点。
dest
Vertor3,在当前空间中包含目标点。
maxSearchDistance
浮点数,包含从源点开始搜索路径的最大距离。(This deafaults to 500.)
girth
包含要使用的导航周网格的浮动。如果没有提供,则默认为0.5,并用于确保大型实体无法通过对它们来说太窄的区域。这个值必须对应girths文件中的一个girth条目,默认位于bigworld/res/helpers/girths.xml。
BigWorld.navigatePathPoints()返回一个包含该路径的向量3个点的列表。如果找不到一条路径,则它将引发一个异常。
例如,如果存在,以下调用将返回(100,10,200)和(150,20,50)之间路径上的点列表。所有点的路径宽度必须至少为4米,且总计必须小于300米:
1 | BigWorld.navigatePathPoints( (100, 10, 200), (150, 20, 50), 300, 4 ) |
该路径将作为一个列表返回:
1 | [(100,10,200), (121,3,148), (133,9,87), (144,8,61), (150,20,50)] |
9.1.5.4. BigWorld.findRandomNeighbourPoint()
BigWorld.findRandomNeighbourPoint()将获取一个源点,并在一个已连接的导航网格中返回一个随机点。保证得到的点与源相连:
1 | findRandomNeighbourPoint( position, radius, girth ) |
这些参数如下:
position
Vector3,其中包含当前空间中的源点。
radius
浮点数,包含从源点开始搜索路径的最大距离。
girth
包含要使用的导航周网格的浮动。如果没有提供,则默认为0.5。
BigWorld.findRandomNeighbourPoint()返回一个包含该随机点的向量3。如果找不到适当的点,它将引发一个异常。
例如,下面的调用将在点(100,10,200)的300m范围内返回一个随机点。到此点的路径在所有点至少0.5米宽。
1 | BigWorld.findRandomNeighbourPoint( (100, 10, 200), 300, 0.5 ) |
9.1.5.5. BigWorld.findRandomNeighbourPointWithRange()
此方法执行与BigWorld.findRandomNeighbourPoint()相同的功能,但使用一个指定最小半径的附加参数。这允许调用者指定到随机点的距离指定特定范围:
1 | findRandomNeighbourPointWithRange( position, minRadius, maxRadius, girth ) |
positon
源点
minRadius
浮点数,包含从源点搜索路径的最小距离。
maxRadius
浮点数,包含从源点搜索路径的最大距离。
girth
包含要使用的导航周网格的浮动。如果没有提供,则默认为0.5。
BigWorld.findRandomNeighbourPoint()返回一个包含该随机点的向量3。如果找不到适当的点,它将引发一个异常。
例如,下面的调用将返回一个超过200米的随机点,但在点(150,20,50)的400米以内。到这一点的路径必须在所有点上至少有4米宽。
1 | BigWorld.findRandomNeighbourPointWithRange( (150, 20, 50), 200, 400, 4 ) |
9.1.6. Action Queue
操作队列是BigWorld技术框架中的结构,它控制在模型上有效的动作队列(动作由操作队列中包含的动作队列对象包裹)。
动作队列处理层的组合,也应用适当的混合和混合时间。
ActionQueue还处理与动作播放相关的任何脚本回调函数,例如在动作的特定帧中调用声音播放回调。
操作在XML中描述,如下所示 example .model 文件(位于资源树下各种子文件夹中):
1 | <action> |
name
由脚本所使用的该操作的名称。这些都可以作为模型管理器返回的模型对象中的命名“常量”使用
animation
从中生成帧数据的基本动画。
blendInTime
动作完全融入的时间以秒为单位。
blendOutTime
以秒为单位的时间,动作需要完全混合。
filler
指定操作是否只是填充,如果队列中出现其他内容,则可以中断。
track
应该播放动作的曲目号。如果动作有一个轨迹,那么动画将被混合在任何其他动画之上——也就是说,它将绕过队列。
isMovement
指定动作使用简单的移动动画,如走、跑、侧走等。平移必须是线性的,并且在播放时从动作中减去(所以从原点移动的运行动画现在会显示在现场运行)。此设置需要设置PromoteMotion标志,这样做意味着此转换然后相应地移动模型。如果模型属于客户控制的实体,那么服务器端实体的位置将相应地更新。这个设置允许使用scalePlaybackSpeed。此设置不能与isCoordinated一起使用。
isCoordinated
指定动作的动画从偏移位置开始。用于需要与非玩家角色(NPC)协调的行动,为了匹配接触点(例如握手),非玩家角色需要相对于玩家角色的位置。这个设置不能和isMovement和isimpacting一起使用。
isImpacting
指定动作是一个复杂(非线性)的运动动画。如果实体是客户端控制的,那么它在服务器上的位置也会被更新。使用isimpact的动画包括跳跃、战斗和击倒、与实体互动等。此设置需要promotemmotion,不能与isMovement和isCoordinated一起使用。
match
如果这个部分存在,这意味着Action Matcher可以自动选择动作(参见下面)。
trigger (section match)
定义了用于确定何时启动操作的标准。
minEntitySpeed (section trigger)
确定要启动操作的实体的最小速度。
maxEntitySpeed (section trigger)
确定要启动操作的实体的最大速度。
minEntityAux1 (section trigger)
确定要开始操作的实体的最小俯仰(pitch)。
maxEntityAux1 (section trigger)
确定要开始操作的实体的最大俯仰(pithc)。
minModelYaw (section trigger)
确定开始动作时模型的最小偏航(yaw)。
maxEntityYaw (section trigger)
确定开始动作时模型的最大偏航(yaw)。
capsOn (section trigger)
确定启动操作需要打开哪些特殊情况字符状态。
capsOff (section trigger)
确定启动操作需要关闭哪些特殊情况字符状态。
cancel (section match)
本节定义了用于确定何时停止操作的标准。
minEntitySpeed (section cancel)
确定动作停止的实体的最小速度。
maxEntitySpeed (section cancel)
确定动作停止的实体的最大速度。
minEntityAux1 (section cancel)
确定要停止动作的实体的最小俯仰(pitch)
maxEntityAux1 (section cancel)
确定要停止动作的实体的最大俯仰(pitch)
minModelYaw (section cancel)
确定使动作停止的模型的最小偏航。
maxEntityYaw (section cancel)
确定使动作停止的模型的最大偏航。
capsOn (section cancel)
确定需要打开哪些特殊情况字符状态才能停止操作。
capsOff (section cancel)
确定需要关闭哪些特殊情况字符状态才能停止操作。
scalePlaybackSpeed (section match)
指定动画播放速度应按实体速度的函数缩放。这个设置需要isMovement。
feetFollowDirection (section match)
指定模型应该转向跟踪实体。在实践中,这意味着当这个动作匹配时,模型在现场旋转,而不是执行旋转动作。
oneShot (section match)
指定动作匹配器只会在一行中选择播放一次。注意this和filler之间的区别,其中filler与动作匹配器无关。
promoteMotion (section match)
指定动画中根节点的运动应该影响模型的位置(以及所有者实体的位置,如果实体是客户端控制的)。isImpacting或isMovement动作必须启用该设置才能正常工作。此设置不能与filler一起使用。
模型操作队列的Python接口允许在该模型上直接播放操作,或者将一组操作排队并按顺序播放。回调函数可以在操作完成时定义和播放。
在Python中,可以很容易地创建一个模型,并在其上播放一个动作,如下面的例子所示:
1 | class Jogger( BigWorld.Entity ): |
9.1.6.1. Debugging animations
如果模型没有按照预期的那样设置动画,那么在Python控制台中显示动作队列图就会很有用。
这可以通过调用BigWorld.debugAQ()方法来完成,它可以接收两种参数:
PyModel
显示指定模型的图形,以及每个操作的混合权重。每个动作都以任意选择的不同颜色表示。
None
关闭图形显示器。
Python控制台显示动作队列的调试图
上图中的图表显示了转左动画(绿色)和空闲动画(红色)上的执行情况。我们可以确定在图中的每个标记点中播放的动画:
A — Idle动画100%被播放
Between A and B — Idle 动画正在与TurnLeft动画混合。
B — Idle动画的50%正在播放,TurnLeft动画的50%正在播放。
C — 100%的TurnLeft动画正在播放。
有关更多细节,请参见Client Python API。
9.1.7. Action Matcher
给定一个模型、一个动作队列和一组动作,一个游戏对象/角色的所有行为都可以通过在动作队列上的事件驱动调用来指定。这最终会带来沉重的开销,因为许多对象状态的改变需要直接调用对象并解决应该播放哪个动画。
这个问题的解决方案是将角色的大部分行为内部化到一个称为Action Matcher的系统中,它会根据对象的状态自动更新对象的运动。它是一个简单的模式匹配器,根据模型的属性(如速度、方向等)来选择动画。
诸如速度、旋转或功率等状态参数被公开给Action Matcher,然后它选择最适合映射到这些参数的动作。这将使游戏从处理动画的许多方面中解放出来,允许它只需要更新状态参数,如位置
当模型(在玩家的AoI中)的动作队列中没有(未混合的)动作时,动作匹配器将接管并执行一个“空闲”动作。它触发这些“空闲”的动画,赋予游戏世界更多生命,因为真实的有生命的东西不是静止的。它也可以用来自动化低级的动画任务。
脚本约束是针对capsOn和capsOff能力位字段进行测试的。模型的Action Matcher有一组能力位,它的值是由脚本控制的。只有在动作匹切集合中所有的capsOn位都是开启的,而所有的capsOff位都是关闭的,动作才会被匹配。
然后它会查看姿态的变化(位置和方向-由过滤器设置),并选择与变化发生时间相匹配的第一个动作。触发器约束用于测试大多数操作。如果遇到与上一帧匹配的动作,则使用取消约束。这个特性主要是为了减少动作滞后。
动作匹配对象的更新函数取该对象之前更新的位置,从其匹配的动作列表中选择最佳匹配的动作,然后以正确的速度播放动画,使动作与速度平滑连接。当游戏角色开始快速移动时,动作匹配器可能会选择更快的运行周期来回放,并提高播放速度以精确匹配速度,因此不会看到脚滑动。
这种方法允许AI系统根据一些变量(如方向、速度和瞄准方向)定义行为,Action Matcher将把这些更新的参数转换成一个平滑移动的角色。
在客户端/服务器架构中,位置和方向是通过网络更新的,Action Matcher
(结合过滤器)可以通过让角色更快地跑到最新更新的位置来自动补偿由网络延迟引起的位置跳跃。
当加载一个操作时,它的数据会随着执行该操作将对根节点产生的姿态影响而增强。此信息用于上面描述的匹配。
现在已经选择了一个操作,它将被传递到操作队列。如果提供的动作与前一个动作相同,它将继续以正常方式播放(例如,frame += fps*deltaTime)。否则,它将被混合在几帧中,而另一帧则被混合出去。
9.1.7.1. Using the Action Matcher
设计用于匹配的每个动作都必须定义一个<match>节。这些是使用Actions窗口通过Model Editor添加的。匹配参数分为两部分:
Trigger parameters
定义何时可以启动一个操作
Cancel section
定义何时应停止一个操作
每个部分中的minEntitySpeed和minEntitySpeed标签定义了动作可以或不可以被触发或取消的速度。
辅助参数可用于定义用于匹配动作的俯仰范围。minModelYaw和maxModelYaw标签定义了一个偏航范围。主要匹配参数定义了动作应用的速度和方向范围,以及一组用户定义的匹配标志,用于特殊情况字符状态。这些标志可以用来设置游戏角色是否没有使用基本动作集,如受伤或生气。
BigWorld引擎使用matchCaps参数来存储操作的按位标志集。在任何给定的时间,模型本身都会有一个开/关标志设置来表示这些状态的状态,这将在每帧的每个动作的matchCaps中进行测试。
应该定义一组用户定义的操作状态(作为文本文件或模型文件中的注释部分创建)。下面是一个简单的例子:
- Capability flag: 1 — State: Angry
- Capability flag: 2 — State: Happy
- Capability flag: 3 — State: Sad
现在,只要在当前的matchCaps中添加愤怒标志,就可以使用动作匹配器访问愤怒动画的集合。在Python代码中,一个字符现在应该以愤怒的方式移动和行为的简单示例如下所示:
1 | class AngryMan( BigWorld.Entity ): |
愤怒模式下字符的Python实现
通过以这种方式使用Action Matcher,角色的自然运动可以被充实,而不需要显式地调用任何函数来播放动画。特定的基于事件的操作仍然可以通过Action Queue结构直接调用,但角色的背景行为可以使用Model Editor工具完全定义,并在代码库中适当地设置匹配标志。
9.1.8. Trackers
跟踪器对实体模型中的节点应用逆运动学,使它们看起来指向给定的“目标”实体定义的位置。一个实体可以有任意数量的跟踪器。
要将跟踪器添加到实体,请使用这样的脚本调用:
1 | tracker = BigWorld.Tracker() |
在Python中添加tracker
这个BigWorld.TrackerNodeInfo方法描述如下:
model
拥有跟踪器将影响的节点的PyModel
primaryNode
主节点引用。
节点引用从实体的独立模型(如果有多个,则为第一个模型)构建,遵循附加点路径,以节点名称结束。这允许跟踪器透明地通过附加的模型工作(即,他们被当作是焊接在模型的骨架上)。
secondaryNodeList
次要节点列表,包含形式为(节点,权重)的元组。
它们与主节点上执行的转换相同,与权重大小成比例。实际上,所有权值(包括主节点,其权值为1.0)都是求和的,每个节点都将得到应用于它们的权值占总权值的比例。
pointingNodeName
提供用于跟踪器操作的参考框架的节点名称。
如果指定的节点不存在,则使用模型的场景根节点。
minPitch,maxPitch,minYaw,maxYaw
追踪器允许使用的转弯限制,以度为单位。
angularVelocity
跟踪器将其影响的PyModelNode实例旋转的速度,以每秒度为单位。
9.1.9. Timers and Traps
这些都是提供给实体脚本的通用助手服务。
脚本可以设置计时器,使其在特定时间被回调,如下面的例子所示:
1 | BigWorld.callback( afterTime, fn ) |
陷阱(Traps)使用一个稍微不同的接口:
1 | trapRef = self.addTrap( radius, function ) |
如果任何实体在创建它的实体的给定半径内徘徊,则会触发陷阱,并且可以使用self.delTrap方法取消该陷阱。
还有另外两种类型的陷阱,根据它们的特定需要进行了优化,如下所述。
9.1.9.1. Pot
Pot是一个“只针对玩家的陷阱”,并且只会被玩家实体所触发。因此,它比一般的陷阱要便宜得多。
给定矩阵提供程序、半径和用于触发的脚本回调,就会添加一个Pot
1 | potHandle = BigWorld.addPot( trapMatrix, radius, callback ) |
9.1.9.2. Mat
Mat是一个“矩阵陷阱”,当一个区域被另一个区域触发时就会被触发。这之所以有意义,是因为矩阵提供程序是动态的。这些区域被定义为:
- 源矩阵转换和由矩阵中x、y和z轴的比例给出的边界区域。
- 目标矩阵的目标矩阵转换(作为一个点处理)。
Mat在Python中定义如下:
1 | matHandle = BigWorld.addMat( sourceMatrix, callback, destMatrix ) |
9.2. Personality script
个性脚本是一个Python脚本,用于处理来自BigWorld引擎的几个回调,用于初始化、清理、输入处理和环境更改。它还可以实现不属于游戏中任何特定实体的客户端功能,如游戏开始屏幕。
它的名称在resources.xml的engineConfigXML标记指定的文件中定义。
个性脚本通常声明一个类来包含脚本中函数共享的所有数据,并使用该类的构造函数声明一个全局变量。例如,个性脚本可能包括以下代码:
1 | class SharedData: |
下面的部分描述了可用的个性回调。
9.2.1. init
回调函数的语法如下:
1 | init(scriptsConfig, engineConfig, preferences, loadingScreenGUI = None) |
当引擎初始化时,脚本被调用一次,并接收以下参数:
scriptsConfig,engineConfig,preferences
PyDataSection对象,包含来自resources.xml的scriptsConfigXML标签中定义的XML文件的数据
loadingScreenGUI
可选参数表示加载屏幕GUI,如果在resources.xml中定义了一个GUI(有关详细信息,请参阅“File <preferences>.xml .xml”)
下面列出了可用的读取函数:
readBool
readFloat
readFloats
readInt
readInts
readMatrix34
readString
readStrings
readVector2
readVector2s
readVector3
readVector3s
readVector4
readVector4s
readWideString
readWideStrings
要从PyDataSection读取数据,调用与数据类型相关的read函数,将节名作为第一个参数传入,将默认值作为第二个参数传入。复数read函数(以’s’结尾)读取指定节中所有匹配子节的内容,并将结果作为值的元组返回。您可以使用正斜杠来引用子部分。
例如:
1 | username = userPreferences.readString("login/username", "guest") |
还可以通过调用带下划线前缀的节名来引用PyDataSection的子节:
1 | username = userPreferences._login._username.asString |
9.2.2. fini
回调函数的语法如下:
1 | fini() |
这个脚本在客户机引擎关闭时被调用。它用于在客户端退出之前执行任何必需的清理。这是对玩家执行注销过程的好地方。
例如:
1 | # note: you must set up a logOff method as a base method in the |
9.2.3. handleKeyEvent
回调函数的语法如下:
1 | handleKeyEvent( event ) |
event参数是一个PyKeyEvent,它包含关于键事件的信息。
通常,几个系统都可以处理键盘输入,因此参数依次传递给每个系统上的handleKeyEvent方法,直到有一个返回布尔值True,表明它已经处理了键事件。
例如:
1 | def handleKeyEvent( event ): |
注意,引擎也会在玩家控制的实体上调用handleKeyEvent方法。所有与玩家控制相关的键都在那里处理。
9.2.4. handleMouseEvent
该回调函的语法如下:
1 | handleMouseEvent( event ) |
每当鼠标移动时,将调用此脚本,并给出一个PyMouseEvent的实例。它的操作方式类似于处理按键事件。
例如:
1 | def handleMouseEvent( event ): |
9.2.5. handleAxisEvent
该回调函的语法如下:
1 | handleAxisEvent( event ) |
这个脚本在操纵杆轴事件发生时被调用,并被给出PyAxisEvent的一个实例。
例如:
1 | def handleAxisEvent( event ): |
9.2.6. handleIMEEvent
1 | handleIMEEvent( event ) |
当Input Method Editor (IME)相关事件发生时调用它,并分别用于更新GUI,并给出一个PyIMEEvent对象作为它的第一个参数。
9.2.7. handleLangChangeEvent
1 | handleLangChangeEvent() |
每当用户更改当前输入语言时,就会发生此事件。它对于在GUI编辑字段上更新语言指示器之类的任务非常有用。
9.2.8. onChangeEnvironments
1 | onChangeEnvironments( inside ) |
这个脚本是一个回调,确认玩家当前所处的环境类型。
目前只有内部和外部环境类型。参数是一个布尔值,指示玩家是否在内部。这对于修改第三人称模式下的摄像机行为可能是有用的。
1 | def onChangeEnvironments( inside ): |
9.2.9. onGeometryMapped
1 | onGeometryMapped(spaceID, spacePath) |
这个回调方法告诉玩家实体关于空间几何的变化。当几何图形映射到客户机上任何当前存在的空间时,调用它。空间ID和空间几何图形的名称作为参数传递。
第一个参数是几何图形要映射到的空间的ID。第二个参数是描述空间几何形状的名称。
9.2.10. onRecreateDevice
1 | onRecreateDevice () |
这个脚本是一个回调,提醒脚本Direct3D设备已经重新创建。当屏幕分辨率改变时,经常会发生这种情况。
引入这个回调主要是为了让脚本可以重新布局它们的GUI组件脚本,但它对于重新创建任何静态PyModelRenderer纹理也很有用(因为这些纹理不会自动更新,除非它们是动态的)。
例如:
1 | def onRecreateDevice(): |
9.2.11. onTimeOfDayLocalChange
1 | onTimeOfDayLocalChange( gameTime, secondsPerGameHour ) |
这个脚本是一个回调,允许脚本被通知游戏时间的变化。
它传递两个浮点数作为参数:第一个是以秒为单位的游戏时间,第二个是每个游戏小时的实时秒数。
9.2.12. start
1 | start() |
这个脚本在引擎初始化后被调用,并用于启动游戏。它可以用来打开开始菜单或登录屏幕。
10. Models
对于BigWorld客户端来说,模型是一个独立的几何单元,可以在世界空间变换矩阵的位置将自己绘制到3D场景中。此外,所有模型都可能具有随时间改变其外观的动画——一些模型显式支持其节点的动画(蒙皮),而其他模型则通过显示替代几何图形进行动画。
Moo是BigWorld 3D引擎,目前可以导出三种对象作为模型:
- Meshes with skeletons(网格骨架)
- Meshes without skeletons(没有骨骼的网格)
- Billboards (广告牌,一种特殊的简单几何形式)
然而,BigWorld客户机中的模型概念封装了符合上述定义的任何东西,并且很容易扩展实现以合并新类型的对象。
Moo命名其网格视觉效果(视觉效果实际上包含的不仅仅是一个简单的多边形网格。它们还可以有一组网格、骨骼、信封、材料和层次骨架),并且可以使用BigWorld输出器从3dsMax模型轻松地创建。
模型文件还可以指定在远处绘制时使用的更简单的父文件。由此形成的父模型文件链被称为详细级别(LOD)链。
10.1. Performance
为了提高呈现性能,可以在model Editor中将模型标记为批处理呈现,这将导致在生成的模型文件中创建批处理标记。
批处理渲染通过在遍历时缓存每个实例的数据(主要是光照条件和转换)来工作,一旦场景遍历,同时渲染所有实例,只更新每次渲染所需的数据。这节省了引擎设置顶点,索引和纹理多次批处理模型。它确实使用了更多的内存,但提供了相当大的性能提升。
在场景中有很多实例的模型上使用批处理标志是明智的。在FantasyDemo中,批处理标记被用于树、守卫、战斗、鸡和守卫的枪上。请注意,该标志不会影响使用着色的模型。
10.2. Hard Points
硬点,或附加点,可以被认为是符合逻辑的魔术贴补丁,让我们可以把枪放在手里,把背包挂在肩膀上,或在炮塔上放置安全摄像头。美术人员必须使用虚拟节点将硬点嵌入到their.visual文件中。
硬点使用定义良好的名称,前面加上’HP_’前缀,以便实体可以扫描硬点节点。重要信息为:
- Name
- Position
- Orientaiton
这一信息允许开发者在游戏逻辑的约束下将任何对象附加到其他对象上。
例如:
- HP_LeftHand
- HP_RightHand
- HP_Shoulder
- HP_Belt1
10.2.1. Naming scheme
为了避免中间步骤的需要,角色硬点和道具硬点之间需要明确的配对。每件物品都需要一个硬点来固定它。
以下便是关于人类角色和枪支的hard points。
In human.model:
• HP_Left_Hand
• HP_Right_Hand
• HP_Belt_1
• HP_Belt_2
• HP_Belt_3
• HP_Belt_4
• HP_Shoulder
• HP_Blade_Lower
• HP_Blade_Upper
In gun.model:
• HP_Left_Hand
• HP_Right_Hand
• HP_Belt_2
• HP_Shoulder
10.2.2. How it works
(客户端)Entity类提供了硬点列表,但只有专门化类询问时才会这样做。例如,当加载一个模型时,它将找到所有以(例如)为前缀的节点。“HP_”。然后将它们存储在模型拥有的hardPoint列表中。
当装备物品时,实体/游戏仲裁者将被要求为物品匹配正确的硬点。例如,如果玩家按下了“下一个武器”按钮,那么库存中的下一个武器就可以附着在右手的硬点上。
10.2.3. Syntax
使用来自Python的硬点的语法是优雅而强大的。要将模型枪连接到硬点手上的模型头像,请使用以下语法:
1 | avatar.hand = gun |
当模型被附加时,要检查它,以确保它没有附加到其他地方。要检索连接到角色手上的模型,只需使用:
1 | model = avatar.hand |
如果没有附加模型,则返回None
模型是通过引用附加的,而不是通过复制,因此可以对原始模型引用执行的任何操作都可以对从硬点检索到的引用执行。
例如,gun.Shoot()和avatar.hand.Shoot()具有相同的效果。
10.2.4. Data
模型定义是一个XML文件,指定:
基本模型定义引用(可选)
网格的可视化定义
网格可以使用的动画
模型可以执行的操作
模型编辑器(Model Editor)工具用于创建一个.model文件,该文件将对象的几何图形链接到它的动画,并定义了可能在该对象上播放的动画/动作集。
与BigWorld框架中的所有资源文件一样,.model文件是XML格式的。它们是分层的,并从它们的父亲继承数据,这样,应用于大量字符的基本动画就可以从更多特定字符的动画中分离出来,而无需重复。
下面的例子定义了一个非常简单的模型:
1 | <jogger.model> |
这个文件描述了一个角色(慢跑者),它具有由视觉文件慢跑者定义的几何形状,并通过<parent>标记拥有一组标准的两足动作,其中可能包括行走、奔跑和跳跃动画,但也指定慢跑动画。
动画部分包括:
动画名称
动画正常播放时的帧率.
标记<nodes>,它引用包含原始关键帧数据的动画文件的名称.
<action>部分描述了用于从Python代码中播放此操作的操作名称。
10.3. SuperModel
SuperModel是需要作为一个功能的模型的集合。SuperModel类是一个实用程序类,它为希望使用模型的模块形成了一个灵活的基础。它用于在块中指定的静态模型,以及实体可以操作的动态模型。
SuperModel提供了概念模型的有效表示(在内存和CPU中)。超模由可选择的部分组成,这些部分有一个继承树,用于指定动画、动作、材质覆盖,最重要的是,较低层次的细节(LOD)部分可以在适当的距离替换。
SuperModel将所有这些部件和它们的装饰组合到一个概念模型中,自动处理多部件和LODing特性产生的复杂性。
最基本的SuperModel的一个例子是一个单一的网格,它是使用BigWorld导出器从3ds Max导出的;变成3D引擎Moo理解的格式。
SuperModels并不存在于场景图或块中,也不提供给脚本系统接口。这些问题留给更高级的类去解决,他们被鼓励使用SuperModel来满足他们所有的模特需求。
10.3.1. Design
有模型文件,扩展名为.model,但没有显式的SuperModel文件。
有一个SuperModel类,它连接在一起并控制许多模型的动画和渲染。SuperModel是基于块和模型文件中的信息创建的(这些是类实例——没有任何东西可以与PyModelInfo类似)。
使用LOD比率,SuperModel类可以管理角色之间的转换,例如,使用三个独立的模型在高LOD渲染的化身,到仅使用一个模型在低LOD渲染之间的转换。
动画将在同一继承层次结构上累积,并使用最适合当前LOD级别的动画。所有LOD级别的所有动画都是从一个平面名称空间中解释和选择的。尝试播放不存在(或在当前LOD级别上不存在)的动画时,会选择模型初始姿态的无限长动画。必须注意确保多模型SuperModels不会浪费时间制作不必要的动画(例如,当所有的小部分都有一个动画覆盖了整个部分的动画时,应用整个部分的动画将是浪费时间)。
网格文件本身指定了一个部件如何连接到多部件supermodels的其他部件。
10.3.2. SuperModel classes
SuperModel类使用Moo提供的对象实现了BigWorld模型定义的基本功能。它将许多由模型编辑器(但通常只有一个)创建的. Model文件组合成一个概念模型,管理任何动画和细节级别链。
目前有两个类在BigWorld客户端中为两个不同的对象使用SuperModels:
- ChunkModel
- PyModel
下面的小节将对两者进行描述。
10.3.2.1. ChunkModel
ChunkModel是一个位于静态场景基础上的模型(例如,岩石,树木)。它与地形一起构成了客户端的世界,形成了大部分有趣的风景。
所有ChunkModels都会被添加到碰撞场景中,除非被标记。他们可以选择播放一个动画,在指定的速度倍增器-否则他们是静态的。
它们严重依赖SuperModel的服务,只提供在区块中生存所需的接口,以及播放单一动画的简单指令。
10.3.2.2. PyModel
这个类实现了在脚本环境中显示的模型。
它允许脚本执行以下操作:
- 获取来自模型定义的绑定操作。
- 参考硬点(详见Hard Point)。
- 指定位置、旋转和缩放。
- 添加电机在场景中移动(如动作匹配器)。
- 添加了用于反向运动学的跟踪器。
- 添加绑定到任何节点的粒子系统。
- 控制脚步和灰尘颗粒的产生。
- 控制能见度和阴影。