BIGWORLD 服务端编程指南01
BIGWORLD_Server Programming Guide_01
1. Overview
这部分文档包含了为BigWorld服务器创建实体和用户数据对象的技术信息。它是描述整个BigWorld系统的更大文档集的一部分。
2. Directory Structure for Entity Scripting
实体是组成游戏世界的对象。使用实体,你可以在游戏中创建玩家、npc、战利品、聊天室和许多其他互动元素。
2.1. The entities.xml File
BigWorld引擎使用<res>/scripts/entities.xml文件来确定可用的实体类型。
这个文件中的每个标签代表一个实体类型,并且在<res>/scripts/entity_defs目录中必须有一个对应的定义文件,以及<res>/scripts/base或<res>/scripts/cell目录下的至少一个Python脚本文件。它也可能在<res>/scripts/client中有也有一个脚本文件。
在此文件中声明实体类型的顺序与与每个实体类型关联的最终实体ID相对应。
2.2. The Entity Definition File
实体定义文件<res>/scripts/entity_defs/<entity>.def决定了脚本在BigWorld中的通信方式。这允许BigWorld系统将发送和接收消息的任务抽象为简单地调用实体上的不同脚本方法。在某种意义上,定义文件提供了一个到实体的接口,Python脚本提供了实现。
2.3. The Entity Script Files
BigWorld Technology将游戏世界中的实体处理划分为三种不同的执行环境:
Entity type | Script file location | Description |
---|---|---|
Cell | <res>/scripts/cell | 负责实体中影响其周围空间的部分。处理在服务器集群上进行。 |
Base | <res>/scripts/base | 负责实体中不影响其周围空间的部分(以及可能充当玩家的代理)。处理在服务器集群上进行。 |
Client | <res>/scripts/client | 负责一个实体中需要高度了解周围环境的部分。 |
有些实体实例可能没有这三个部分中的一个。此外,一些实体类型可能不支持拥有这些部分中的一个。对于每种实体类型,如果该类型支持该执行环境,每个CellApp、BaseApp和Client都有一个脚本文件。
这些脚本文件以实体类型命名,扩展名为’.py’。此文件必须包含一个名称为实体类型的类。
例如,如果您有一个实体类型Seat,它可以有cell、base和client执行环境,那么将有三个脚本文件,每个都有类的实现:
Script file execution context | Entity’s base class |
---|---|
Cell | BigWorld.Entity |
Base | BigWorld.Base or BigWorld.Proxy |
Client | BigWorld.Entity |
Seat实体的脚本的开头可以实现如下:
• Cell script file ‐ <res>/scripts/cell/Seat.py
1 | import BigWorld |
• Base script file ‐ <res>/scripts/base/Seat.py
1 | import BigWorld |
• Client script file ‐ <res>/scripts/client/Seat.py
1 | import BigWorld |
3. Directory Structure for Service Scripting
Services类似于基础实体的脚本对象。它们被设计为与游戏服务器集成额外的功能。该功能通常涉及外部流程。
每个服务类型都被实现为Python脚本的集合,以及将这些脚本联系在一起的基于xml的定义文件。这些脚本位于文件夹脚本下的资源树中(例如,<res>/ scripts,其中<res>是定义的虚拟树~/.bwmachine .conf)。
下面的列表总结了<res>中服务的重要文件和目录:
- <res> ‐ 在~/.bwmachine .conf中定义的资源树。
- scripts - 包含所有服务文件的文件夹。
- services.xml - 列出启动时要加载到服务器中的所有服务。
- base - 文件夹包含带有base组件的实体的Python脚本。
- common - 所有组件的Python搜索路径中列出的文件夹。用于普通游戏代码。
- lib - 所有组件的Python搜索路径中列出的文件夹。用于普通游戏代码。
- service - 文件夹包含要在ServiceApps上运行的服务的Python脚本。
- service_defs - 为<res>/scripts/ services.xml文件中列出的每个服务包含一个XML .def文件。
- <service>.def - 服务定义文件。对于<res>/ scripts/services.xml中定义的每个服务,都有一个这样的文件。
- scripts - 包含所有服务文件的文件夹。
3.1 The services.xml File
BigWorld引擎使用<res>/scripts/services.xml文件来确定可用的服务类型。
这个文件包含了一个Services元素列表,作为根节点的子节点:
1 | <root> |
3.2. The Service Definition File
每个服务类型都有一个对应的定义文件,以服务的类型名称命名,扩展名为’.def’。例如,一个NoteStore服务类型会有一个名为NoteStore.def的文件。
以下文件是一个“最小”服务定义文件,以帮助快速定义一个新服务:
1 | <root> |
3.3. The Service Script Files
BigWorld技术处理服务执行环境中所有服务的处理。因此,为这个环境提供了每个服务类型的脚本文件。
例如,一个NoteStore服务将有一个包含类实现的脚本文件,位于<res>/scripts/service/NoteStore.py
脚本文件中定义的服务基类是BigWorld.Service,因为文件表示服务执行环境。
NoteStore服务脚本的开头可以实现如下:
1 | import BigWorld |
4. Directory Structure for User Data Object Scripting
用户数据对象是在Chunk文件中嵌入用户定义数据的一种方法。每个用户数据对象类型都被实现为一个Python脚本集合,以及一个将这些脚本绑定在一起的基于xml的定义文件。这些脚本位于文件夹脚本下的资源树中(例如,<res>/scripts,其中<res>是定义的虚拟树~/.bwmachine .conf)。
用户数据对象与实体的区别在于它们是不可变的(即它们的属性不会改变),而且它们不会传播到其他cell或客户端。这使得它们比实体轻得多。
用户数据对象的一个关键特性是它们的可链接性。实体能够链接到用户数据对象,而用户数据对象能够链接到其他用户数据对象。这是通过在希望链接到另一个用户数据对象的用户数据对象或实体的定义文件中包含UDO_REF属性来实现的。
下面的列表总结了<res>中用户数据对象的重要文件和目录:
- <res> - 在~/. bwmachinedconf中定义的资源树。
- scripts - 包含所有实体文件的文件夹。
- user_data_objects.xml - 列出启动时要加载到客户机或服务器中的所有用户数据对象。
- base - 文件夹包含带有基本组件的用户数据对象的Python脚本。
- cell - 文件夹包含用于带有单元格组件的用户数据对象的Python脚本。
- client - 文件夹包含带有客户端组件的用户数据对象的Python脚本。
- common - 所有组件的Python搜索路径中列出的文件夹。用于普通游戏代码。
- lib - 所有组件的Python搜索路径中列出的文件夹。用于普通游戏代码。
- user_data_object_defs - 包含用户数据对象定义文件。
- 用户数据对象定义文件。对于<res>/scripts/user_data_objects.xml中定义的每个用户数据对象,都有一个这样的文件。 - interfaces - 用户数据对象接口定义文件。
- scripts - 包含所有实体文件的文件夹。
4.1. The user_data_objects.xml File
BigWorld引擎使用<res>/scripts/user_data_objects.xml文件来确定可用的用户数据对象的类型。
文件结构与<res>/entities/entities.xml匹配。
4.2. The User Data Object Definition File
用户数据对象定义文件<res>/scripts/user_data_object_defs/
1 | <root> |
4.3. The User Data Object Script Files
BigWorld Technology根据其Domain将游戏世界中用户数据对象的处理划分为三种不同的执行环境:
- User Data Object Domain: Cell — Script File Location: <res>/scripts/cell
- User Data Object Domain: Base — Script File Location: <res>/scripts/base
- User Data Object Domain: Clien — Script File Location: <res>/scripts/client
用户数据对象的大多数实现将只存在于计算单元或客户端中。关于驻留在计算单元中的用户数据对象的示例,请参阅<res>/scripts文件夹中的PatrolNode用户数据对象脚本和定义文件。对于仅针对客户机的用户数据对象的示例,请在相同的位置查找CameraNode用户数据对象的脚本和定义文件。
5. Properties
属性描述实体的状态。像传统的对象系统一样,BigWorld属性有一个类型和一个名称。与传统的对象系统不同,属性还具有分布属性,影响它在系统中的分布位置和分布频率。
属性在实体的定义文件中声明(命名为<res>/scripts/entity_defs/<entity>.def),在一个名为Properties的节中。
1 | <root> |
5.1. Property Types
BigWorld需要在其各个组件之间通过网络有效地传输数据。为了这个目的,BigWorld定义文件描述了实体的每个属性的类型(尽管BigWorld的脚本使用的是Python —— 一种无类型语言)。
5.1.1. Primitive Types
下面的列表总结了BigWorld属性可用的基本类型:
BLOB — Size (bytes): N+K
二进制数据。类似于字符串,但可以包含NULL字符。
在XML中,例如在XML数据库中,以base-64编码存储。
N是blob中的字节数,k=4。
FLOAT32 — Size (bytes): 4
32位浮点数。
FLOAT64 — Size (bytes): 8
64位浮点数。
INT8 — Size (bytes): 1 — Range: From: -128 To: 127
带符号的8位整形
INT16 — Size (bytes): 2 — Range: From: -32,768 To: 32,767
带符号的16位整形
INT32 — Size (bytes): 4 — Range: From: -2,147,483,648 To: 2,147,483,647
带符号的32位整形
INT64 — Size (bytes): 8 — Range: From: -9,223,372,036,854,775,808 To: 9,223,372,036,854,775,807
带符号的64位整形
MAILBOX — Size (bytes): 12
一个BinWorld的邮箱。
将实体传递给MAILBOX参数会自动将其转换为MAILBOX。
PYTHON — Size(bytes): Size of pickled string, as per STRING
使用Python 拾取程序将任何Python类型打包到一个字符串中,并传输结果。
这不应该在客户端和服务器之间使用,因为它是不安全的和低效的。
建议在生产代码中使用用户数据类型。
STRING — Size (bytes): N+K
字符串(unicode)。
N是字符串中的字符数,k=4。
UINT8 — Size(bytes): 1 - Range: From: 0 To: 255
无符号8位整数
UINT16 — Size(bytes): 2 — Range: From: 0 To: 65,535
无符号16位整数
UINT32 — Size(bytes): 4 — Range: From: 0 To: 4,294,967,295
无符号32位整数
UINT64 — Size(bytes): 8 — Range: From: 0 To: 18,446,744,073,709,551,615
无符号64位整数
UNICODE_STRING — Size (bytes): Up to 4N+K
字符串(Unicode)。
N是字符串中的字符数,k=4。stream为utf - 8。
VECTOR2 — Size(bytes): 8
二维32位浮点向量。在Python中表示为两个数字的元组(或Math.Vector2)。
VECTOR3 — Size(bytes): 12
二维32位浮点向量。在Python中表示为三个数字的元组(或Math.Vector3)。
VECTOR4 — Size(bytes): 16
二维32位浮点向量。在Python中表示为四个数字的元组(或Math.Vector4)。
5.1.2. Composite Types
以下部分描述了BigWorld中可用的复合类型
5.1.2.1. ARRAY and TUPLE Types
BigWorld还有ARRAY和TUPLE类型,它们可以创建任何BigWorld原语类型的值数组。
ARRAY类型的属性的字节大小由下面的公式计算:
1 | N*t +k |
- N - 数组中的元素数。
- t - 数组中所包含的类型的大小
- k - 常量
BigWorld TUPLE类型在脚本中由Python元组类型表示,而BigWorld ARRAY类型在脚本中由Python列表类型表示。
元组(Tuples)的指定如下:
1 | <Type> TUPLE <of> [TYPE_NAME|TYPE_ALIAS] </of> [<size> n </size>] </Type> |
数组(Arrays)指定如下:
1 | <Type> ARRAY <of> [TYPE_NAME|TYPE_ALIAS] </of> [<size> n </size>] </Type> |
如果指定了ARRAY或TUPLE的大小,那么它必须声明n个元素。不允许向固定大小的ARRAY或TUPLE添加或删除元素。如果没有指定默认值,那么一个固定大小的ARRAY或TUPLE将包含n个元素类型的默认值。
数组(Arrays)有一个名为equals_seq()的特殊方法,可用于对任意Python序列(包括Python列表和元组)执行元素布尔相等性测试。例如:
1 | self.myList = [1,2,3] |
数组(Arrays)有效地传播更改。这包括对单个元素的赋值、追加、扩展、移除、弹出和切片赋值。
数组不仅可以包含有别名的数据类型,而且本身也可以有别名。
5.1.2.2. FIXED_DICT Data Type
FIXED_DICT数据类型允许使用一组固定的字符串键定义类似字典的属性。键和键值的类型是预定义的。
FIXED_DICT的声明如下所示:
1 | <Type> FIXED_DICT |
这个数据类型可以在任何类型声明可能出现的地方声明,例如,在<res>/scripts/entity_defs/ alias.xml,在<res>/scripts/entity_defs/<entity>.def,作为方法调用参数,等等。
1 | <root> |
FIXED_DICT的实例可以像Python字典一样被访问和修改,但有以下例外:
- Keys不能添加或删除
- Value的类型必须与声明匹配。
例如:
1 | if entity.TradeLog[ "dbIDA" ] == 0: |
另外,它还支持以下内容
1 | if entity.TradeLog.dbIDA == 0: |
使用struct语法可能会导致与FIXED_DICT方法的名称冲突问题。
FIXED_DICT实例可以使用具有所需键的超集(superset)的Python字典来设置。字典中任何不必要的键都会被忽略。
例如:
1 | entity.TradeLog = { "dbIDA" : 100, "itemsTypesA" : [ 1, 2, 3 ], |
FIXED_DICT值的更改将有效地传播到整个属性的更改将被传播的任何地方,例如,到ghost和客户端-包括ownClients。
FIXED_DICT数据类型的默认值可以在实体属性级别指定。例如:
1 | <root> |
如果<Default>部分没有指定,那么FIXED_DICT数据类型的默认值将依赖于<allowNone>标签的值,如下所示:
表没有部分的FIXED_DICT的默认值
<AllowNone> | FIXED_DICT 默认值 |
---|---|
True | python中 Node 对象 |
False | 具有类型定义中指定的键的Python字典。 每个键值将根据其类型有一个默认值。例如,INT类型的键值的默认值为0。 |
5.1.3. Custom User Types
有两种方法将用户定义的Python类合并到BigWorld实体中:包装FIXED_DICT数据类型,或实现USER_TYPE。
FIXED_DICT数据类型支持被用户定义的Python类型包装。当一个FIXED_DICT被包装时,BigWorld将实例化用户定义的Python类型来代替FIXED_DICT实例。这允许用户定制FIXED_DICT数据类型的行为。
类型系统也可以使用USER_TYPE类型进行任意扩展。与包装的FIXED_DICT类型不同,USER_TYPE类型的结构对BigWorld是完全不透明的。因此,USER_TYPE类型的实现就更加复杂了。类型操作的实现是由用户编写的Python对象(例如类的实例)执行的。Python对象作为该类型实例的工厂和序列化器,它可以选择使用它认为合适的任何该类型的Python表示——它可以像整数一样简单,也可以是Python类的实例。
5.1.4. Alias of Data Types
BigWorld还允许创建类型的别名。别名是一个类似于c++类型定义的概念,列在XML文件<res>/scripts/entity_defs/alias.xml中。格式如下:
1 | <root> |
下面列出了一些有用别名的例子:
Alias | Maps to | Description |
---|---|---|
ANGLE | FLOAT32 | 以弧度度量的角 |
BOOL | INT8 | 布尔类型(编码为零=假,非零=真) 映射到INT8,最小的BigWorld类型。 |
INFO | UNIT16 | 关于任务的信息元素。 |
MISSIN_STATS | ARRAY <of> INFO </of> | 任务信息数据元素数组(即INFO类型别名)。 注意,这是一个别名数组,其元素的类型也是别名类型。 |
OBJECT_ID | INT32 | 另一个实体的句柄。该名称表明该属性包含实体的句柄。 |
STATS_MATRIX | ARRAY <of> MISSION_STATS </of> | 任务信息数据元素矩阵(即INFO类型别名)。 注意,这是一个别名数组,其元素的类型是另一个别名数组。 |
使用上面描述的别名定义的语法,我们有以下文件:
1 | <root> |
通过别名,还可以定义自定义Python数据类型,这些数据类型在网络上有自己的流语义。我们在<res>/scripts/entity_defs/alias.xml文件中声明这些类型如下:
1 | <root> |
5.2. Server to Client bandwidth usage of Property updates
当服务器向客户端发送一个属性更新时,它通常包含一个表示流数据长度的字节。但是,如果服务器能够确定特定属性的大小在流到客户机时始终相同,则可以删除该字节。
如果一个属性是以下任意组合,服务器会认为它是固定大小的类型:
- 具有常量大小的基本类型。
- 具有声明大小并包含固定大小类型的ARRAY或TUPLE。
- FIXED_DICT不能为None,它只包含固定大小的类型,并且没有封装在实现addToStream的类中。
对ARRAY元素的切片赋值以及对单个ARRAY或TUPLE元素或FIXED_DICT值的更新不能从这种固定大小类型的优化中获益。
通过确保同时更新的值大小固定,并捆绑到FIXED_DICT结构中,可以优化服务器和客户机的带宽使用。如果一次性更新整个FIXED_DICT,那么只需要发送一条消息,不需要表示消息长度的字节。
通常单独更新的属性最好不捆绑到FIXED_DICTs中,因为更新FIXED_DICT的单个元素比更新顶级属性使用更多的带宽。
尽可能避免将大小可变的属性传播到客户端。不过,可变大小属性的代价只有一个字节,因此,例如,只有当ARRAY的长度始终是这个长度时,才应该指定它的大小,而不是指定感兴趣的最大长度。
另一件事需要注意的是,有一个数量有限的标识符属性传播到客户端,如果你有更多的属性为一个给定的实体类型,多余的属性将发送稍微低效率(通常每个值更新一个字节),而且它们的大小是可变的。服务器倾向于使用这种“溢出”机制发送大的固定大小或可变大小的属性。
dumpEntityDescription DBMgr配置选项可用于检查您的实体属性和方法的客户端到服务器的带宽需求。
5.3. Default Values
当一个实体被创建时,它的属性被初始化为它们的默认值。可以在属性级别(在实体定义文件中)或类型级别(在alias.xml中)重写默认值。
每种类型的默认值和覆盖它的语法如下所示:
ARRAY — Default: []
1
2
3
4
5<Default>
<item> Health potion </item>
<item> Bear skin </item>
<item> Wooden shield </item>
</Default>构造等价于Pyhton列表 [ ‘Health potion’, ‘Bear skin’, ‘Wooden shield’ ].
TUPLE — Default: ()
和ARRAY类似
BLOB — Default: ‘’
1
2<Default> SGVsbG8gV29ybGQhB </Default>1
<!-Hello World! -->1:必须指定base6编码的字符串值。
FIXED_DICT
FLOAT32 — Default: 0.0
1
<Default> 1.234 </Default>
FLOAT64 — Default: 0.0
1
<Default> 1.23456789 </Default>
INT8, INT16, INT32, INT64 — Default: 0
1
<Default> 99 </Default>
MAILBOX — Default: None
默认值不能被重写。
PYTHON — Default: None
1
2
3<Default>
{ "Strength": 90, "Agility": 77 }
</Default>STRING — Default: ‘’
1
<Default> Hello World! </Default>
UINT8, UINT16, UINT32, UINT64 — Default: 0
1
<Default> 99 </Default>
UNICODE_STRING — Default: u’’
1
<Default> Hello World! (this is a UTF-8 string) </Default>
值必须不带引号,并且必须编码为UTF-8
USER_TYPE ——默认值:用户定义的defaultValue()函数的返回值
1
2
3
4
5
6
7
8
9
10<Default>
<intVal> 100 </intVal>
<strVal> opposites </stringVal>
<dictVal>
<value>
<key> good </key>
<value> bad </value>
</value>
</dictValue>
</Default>VECTOR2 ——默认值:PyVector的适当长度为0.0
1
<Default> 3.142 2.71 </Default>
VECTOR3 ——默认值:PyVector的适当长度为0.0
1
<Default> 3.142 2.71 1.4 </Default>
VECTOR4 ——默认值:PyVector的适当长度为0.0
1
<Default> 3.142 2.71 1.4 3.8 </Default>
5.4. Data Distribution
属性表示实体的状态。一些状态只与单元相关,其他状态只与基状态相关,还有一些状态只与客户机相关。然而,有些属性与以上的每一个都有关联。
每个属性都有一个分布类型,它向BigWorld指定哪个执行环境(单元、基或客户端)负责更新属性,以及在系统中的何处传播其值。
数据分布是通过在文件<res>/scripts/entity_defs/.def中指定<Properties>的<Flags>来设置的。
5.4.1. Valid Data Distribution Combinations
下面的列表描述了上述位标志的有效组合:
ALL_CLIENTS
属性对cell和client上的所有实体可用。
例子包括:
球员的名字。
玩家或生物的生命状况。
BASE
属性仅在base可用。
例如
- 聊天室成员名单
- 角色库存物品
BASE_AND_CLIENT
属性在base和自己客户端可用
只有在创建客户端实体时才同步此类型的属性。当属性更改时,客户机和基库都不会自动更新。方法必须用于传播新值,这很简单,因为只有一个玩家需要接收它。
CELL_PRIVATE
属性仅对其实体可用,且仅对单元格可用。
例子包括:
AI算法中npc“思想”的属性。
与游戏玩法相关的玩家属性,但让玩家看到很危险(例如,战斗后的治疗时间)
CELL_PUBLIC
属性仅在单元格和其他单元格上可用,并可用于其他实体。
例子包括:
玩家的法力值(只能被敌人看到,其他玩家看不到)。
敌人NPC组队的呼号。
CELL_PUBLIC_AND_OWN
属性可用于单元格上的其他实体,以及单元格和客户端上的这个实体。与OWN_CLIENT不同,这些数据也会被ghost,因此可以使用单元格上的其他实体。
EDITOR_ONLY
在使用BaseApp中的BigWorld.fetchEntitiesFromChunks时,这个值可能很有用。它可以用来以编程方式决定是否应该加载一个特定的实体。例如,您可以将一个难度级别与每个实体关联起来,因此,只有当任务的难度级别足够高时,该实体才会被加载。
OTHER_CLIENTS
属性从客户端可用于非该玩家角色的实体。其他实体也可在单元上使用。
例子包括:
动态世界道具的状态(如门,战利品容器和按钮)。
粒子系统效应的类型。
当前坐在座位上的玩家。
OWN_CLIENT
属性仅对计算单元和客户端上的此实体可用。
例子包括:
玩家的角色类别。
玩家的经验值。
5.4.2. Using Distribution Flags
在为属性选择分布标志时,考虑以下几点:
- 哪些方法需要该属性?
- 该属性是否需要由其他实体访问?
- 客户端是否直接对这个值感兴趣?
- 一个玩家会因为看到这个属性而作弊吗?
- 任何属性只能有一个主值
5.4.3. Data Propagation
数据传播发生在首次创建实体时。对属性的后续修改将仅局限于组件,除非修改发生在CellApp中,在这种情况下,更改将自动传播到所有感兴趣的各方。例如,CELL_PUBLIC属性被传播到具有实体虚影的所有其他CellApps, OTHER_CLIENTS属性被传播到在其AoI中具有实体的所有客户端,等等。
当更改CellApp以外的组件中的属性值时,可以使用远程方法调用手动传播更改。
5.4.3.1. Property Callbacks On Ghosted Entities
当修改一个属性,将传播到一个实体的幽灵在相邻的单元,即为CELL_PUBLIC, OTHER_CLIENTS和ALL_CLIENTS属性,可选的回调可以在单元实体类上实现,以响应那些ghost单元实体的属性更新。它们类似于客户端回调:
@bwdecorators.callableOnGhost
def set_<property name>( self, oldValue )
当属性发生更改时,将调用此方法。oldValue参数是属性的旧值,新值已经设置。
如果更改是嵌套的,并且实现了setNested_<属性名>,则不会调用此方法。类似地,如果更改是切片更改,并且实现了setSlice_<属性名>,则不会调用此方法。
@bwdecorators.callableOnGhost
def setNested_<property name>( self, changePath, oldValue )
对嵌套属性更改调用此方法,例如当数组元素或FIXED_DICT子属性发生更改时。如果此方法不存在,则将调用set_回调。
changePath参数包含更改的路径,对于ARRAY值,使用数组中的索引,对于FIXED_DICT,使用键的字符串作为路径组件。
例如,假设我们有如下属性定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<myProperty>
<Type> ARRAY <of>
FIXED_DICT
<Properties>
<a>
<Type> ARRAY <of> INT32 </Type>
</a>
<b>
<Type> STRING </Type>
</b>
</Properties>
</of>
</Type>
<Flags> CELL_PUBLIC </Flags>
</myProperty>假设以下声明cell实体运行:
1
3].a[5] = 8 entity.myProperty[
这将导致调用setNested_myProperty,并将changePath设置为:
1
[3, 'a', 5]
@bwdecorators.callableOnGhost
def setSlice_<property name>( self, changePath, oldValue )
当对ARRAY属性进行切片更改时,将调用此方法。如果这个方法不存在,set_<property>回调函数将被调用,如果它存在的话。
Examples of slice changes are below:
1
23].a.append( 10 ) entity.myProperty.append[
3:9] = [50, 6, 9, 10] entity.myProperty[
请注意,这些回调永远不会调用真正的单元实体。因为它们可以在ghosted单元格实体上调用,所以必须使用bwdecorators模块中的callableOnGhost decorator函数来装饰它们
5.4.3.2. Forcing Data Propagation for Python and Custom User Types
对python和自定义用户类型属性的更改不会自动传播,除非已对该属性进行重新分配。
这种行为主要影响像字典、数组和类这样的复合Python类型,因为对对象的修改不会导致数据传播,除非该属性被重新分配给自己。
例如,如果实体e具有如下所示的属性:
1 | <pythonProp> |
将新值赋给pythonProp将导致数据传播:
1 | e.pythonProp = {'gold':100} |
但是,修改该值不会导致数据传播:
1 | e.pythonProp[ 'gold' ] = 50 |
实体的不同部分会看到不同的pythonProp值,除非通过将该属性重新赋值给自身来手动触发数据传播:
1 | e.pythonProp = e.pythonProp |
5.5. Implementing Custom Property Data Types
自定义数据类型对于实现具有复杂行为的数据结构非常有用,这些数据结构在不同的组件之间共享,或者必须附加到单元实体(在这种情况下,它们必须能够从一个单元转移到另一个单元)。
5.5.1. Wrapping a FIXED_DICT Data Type
默认情况下,FIXED_DICT数据类型的行为类似于Python字典。可以通过将类字典的FIXED_DICT类型替换为另一种Python类型(在本文档中称为包装器类型)来更改此行为。
为此,指定一个类型转换对象在< implementedBy >部分FIXED_DICT类型声明。例如:
1 | <Type> |
CustomTypeConverterInstance必须是一个在FIXED_DICT实例和包装器实例之间转换的Python对象。
它必须实现以下方法:
表格应该由包装器类型实现的方法。
Method | Description |
---|---|
addToStream( self, obj ) | 可选方法,将包装器实例转换为适合通过网络传输的字符串。 obj参数是一个包装器实例。这个方法应该返回一个obj的字符串表示。通常,这是使用cPickle模块完成的。 如果该方法存在,则createFromStream也必须存在。 如果这个方法不存在,那么包装器实例将通过网络传输,首先使用getDictFromObj方法将它们转换为FIXED_DICT实例,然后使用createObjFromDict方法在接收端重新创建。 |
createFromStream( self, stream ) | 可选方法,从其字符串网络形式创建包装器类型的实例。 stream参数是通过调用addToStream方法获得的Python字符串。此方法应该返回一个包装器实例由数据流。 如果存在此方法,则必须提供addToStream。 |
createObjFromDict( self, dict ) | 将FIXED_DICT实例转换为包装器实例方法。 关键字参数是FIXED_DICT实例。这个方法应该返回从dict中的信息构造的包装器实例。 |
getDictFromObj( self, obj ) | 将包装器实例转换为FIXED_DICT实例的方法。 obj参数是一个包装器实例。这个方法应该返回一个Python字典(或类字典对象),它包含与FIXED_DICT实例相同的一组键。 |
isSameType( self, obj ) | 方法检查对象是否为包装器类型。 任意Python对象中的obj形参。如果obj是一个包装器实例,这个方法应该返回True。 |
5.5.1.1. Example of Wrapping FIXED_DICT with a Class
通常需要包装FIXED_DICT数据类型与一个类来促进面向对象编程
1 | import cPickle |
<res>/scripts/common/MyCustomTypeImpl.py — 包装器类型和类型转换器对象
1 | <Type> |
封装的FIXED_DICT类型声明的摘录
上面的示例使FIXED_DICT类型表现为具有成员a和b的类,而不是具有相同键的字典。
为了确保实体e的所有副本都有更新的值,该属性必须设置为MyCustomType的一个不同的实例,并使用更新的值:
1 | e.custType = MyCustomType( { "a": 100, "b": 200 } ) |
1 | class MemberProxy( object ): # descriptor class |
在上面的例子中,MyCustomType在其fixedDict成员中引用了原始FIXED_DICT实例。对成员a或b的访问将通过描述符类重定向到fixedDict成员。当对FIXED_DICT实例的更新自动传播到其他组件时,对成员a和b的更新也会自动传播。
这种方法的缺点是不可能定制流媒体。如果实现了addToStream和createFromStream方法,则直接从流创建自定义对象。由于在Python脚本中不可能实例化FIXED_DICT对象,因此自定义对象不可能引用将传播部分更改的FIXED_DICT对象。
5.5.1.2. Implementing a USER_TYPE Data Type
USER_TYPE数据类型早于FIXED_DICT数据类型,它的大部分功能可以通过包装FIXED_DICT数据类型来实现。但是,USER_TYPE数据类型还允许将其表示定制为<DataSection>。
USER_TYPE数据类型由以下几部分组成:
实现USER_TYPE数据类型的Python实例的声明。例如:
1
<Type> USER_TYPE <implementedBy> UserType.instance </implementedBy> </Type>
但是,建议在<res>/scripts/entity_defs/alias.xml中声明USER_TYPE数据类型,我们可以在实体定义文件中使用它(命名<res>/scripts/entity_defs /<entity>. def)。
一个类,它定义了从不同位置读取和写入此数据类型的方法。
一个模块,包含上面的类和该类的一个实例,将用于序列化和反序列化自定义数据类型。
这个自定义数据类型也可以在运行时声明一个表示该类型的Python类。Python列表、字典或其他本地Python数据类型也可以表示它。
我们实现的类提供了方法来序列化我们用来表示概念的任何Python类型。这意味着我们可以通过网络传输类并将其序列化到数据库,只需在该类中编写适当的方法。
5.6. Volatile Properties
有些属性比其他属性更新得更频繁,因此几乎所有实体都有一组属性需要特别处理。这些属性称为volatile属性,是由BigWorld引擎预先定义的。
通常,用OTHER_CLIENTS(或ALL_CLIENTS)标记的属性只在属性更改时发送给适当的客户机应用程序。这些更改是可靠发送的。每次AoI(通过其优先级队列机制)考虑实体时,被认为是易失性的属性就会发送给客户端。这些是不可靠地发送的,因为在下一次考虑该实体时将发送更新的值。
BigWorld定义的默认volatile属性如下:
Property | 描述 |
---|---|
position | 实体的(x,y,z)位置。在Python中表示为三个浮点数的元组。 |
yaw | (-180,180)y轴的旋转偏移 |
pitch | (-90,90)x轴的旋转偏移 |
roll | (-180,180)z轴的旋转偏移 |
这些属性通过客户端和服务器之间使用的优化协议进行更新,以最小化带宽。
不稳定的属性分别列出的正常属性文件 <res>/scripts/entity_defs/<entity>.def.
每个实体都可以决定自动更新这些volatile属性中的哪一个。此外,它们可以有优先级。该优先级决定了该属性不再发送的实体与实体之间的距离。
语法如下:
1 | <root> |
- 如果一个易变状态没有被指定,它将永远不会被更新(BigWorld.VOLATILE_NEVER)。
- 如果指定了易变状态:
- 如果未指定优先级,则无论与实体的距离如何,属性将始终被更新(BigWorld.VOLATILE_ALWAYS).
- 如果指定了优先级,则该值将用作与实体的属性的最大距离(单位为米),该属性仍将被更新。
假设一个实体的易变性定义如下:
1 | <root> |
对于上面的例子,我们为每个属性设置了如下代码:
- position - 总是更新
- yaw - 更新到30米
- pitch - 更新到25米
- roll - 没有更新(BigWorld.VOLATILE_NEVER)
只有定义不移动的实体时才应该没有volatile属性。
5.7. LOD (Level of Detail) on Properties
有时,通过不将信息分发给遥远的客户端,可以优化带宽使用。我们可以通过将<DetailLevel>标记附加到属性来实现这一点。这个标记决定了属性更改之后不会发送到客户端的距离。
请注意,这纯粹是对属性的优化。只有在带宽使用率过高的情况下才应该使用此选项。如果该属性启用了这一功能,那么你必须非常仔细地测试它,以检查游戏玩法所获得的结果是否符合你的预期。
文件<res>/scripts/entity_defs/中属性的LOD(详细级别)的定义<entity>.def的描述如下:
1 | <root> |
上面的例子为属性声明了一个标记为NEAR的LOD。NEAR的实际值在实体文件中<LodLevels>节的<level>子节中定义。
例如,要将AoI细分为NEAR、MEDIUM和FAR(当实体在彼此的AoI中时,远于FAR的内容将被传输),实体的定义文件将包括以下几行:
1 | <root> |
在上面的例子文件中为实体指定的lod如下所示:
Detail levels继承自父定义文件。任何与父级具有相同标签的级别都将修改该级别,并添加任何新的级别。
5.7.1. LOD and Hysteresis
除了参数<label>外,<level>子节还可以有<hyst>参数。
它的定义如下所示:
1 | <root> |
这个参数定义了一个从LOD的外边界开始向外移动的迟滞区域。他可以防止频繁更改属性的LOD,从而节省了单元格的大量处理时间,因为属性不必经常更改它们的优先级。为此<hyst>指定LOD级别边界周围的缓冲区,实体在更改为更低的LOD之前必须完全通过该缓冲区。
<hyst>参数的声明是可选的,如果没有声明,它将默认为10米。
举个例子,假设一个静止的实体,和另一个实体穿过点A、B、C、D、E,最后回到a,如下图所示:
表格实体在另一个实体的lod周围移动。
位置 | LOD | Reason |
---|---|---|
A | NEAR | 不受滞后影响 |
B | NEAR | 实体已经从NEAR移动到MEDIUM,但还没有完全通过滞后。 |
C | MEDIUM | 实体已经从NEAR移动到MEDIUM,并且完全通过滞后 |
D | MEDIUM | 实体仍然在MEDIUM中。 |
E | MEDIUM | 实体仍然在MEDIUM中。 |
A | NEAR | 实体已经从MEDIUM移动到NEAR。 |
5.8. Bandwidth Optimisation: Send Latest Only
当实体的OTHER_CLIENTS属性更改时,将创建一个事件对象,并将其添加到事件历史记录中。当更新在其AoI中具有此实体的客户端应用程序时,将使用此事件历史记录。当考虑此实体时,自上次考虑此实体以来添加的任何事件都将发送给客户端。在一次更新中可能会发送对单个属性的多个更改。
如果在属性上设置了SendLatestOnly标志,则事件历史中只保留最新的更改。这样可以避免发送多个更改。这可以节省发送到客户端的带宽,并可以在属性频繁更改的cellapp上保存一些内存。
此数值默认为false。客户端方法也有此标志。
注意,如果属性是SendLatestOnly,应该避免更改ARRAY和FIXED_DICT数据类型实例的内容,因为这需要重新发送整个属性。
5.9. Bandwidth Optimisation: Is Reliable
当实体的OTHER_CLIENTS属性发生更改时,将向适当的客户端应用程序发送一条消息,以更新它们对该实体的视图。默认情况下,此消息是可靠发送的,因此即使有丢包情况也能接收到更改。在极少数情况下,发送这些不可靠的信息可能是首选。这通常只与“SendLatestOnly”选项和连续更改的属性一起使用。
将SendLatestOnly设置为true, IsReliable设置为false,并在每个游戏tick中改变一个值会导致类似于volatile position和direction值的行为。
注意,如果属性IsReliable设置为false,应该避免更改ARRAY和FIXED_DICT数据类型实例的内容,因为这需要重新发送整个属性。
5.10. Detailed Position
发送给客户端的实体位置更新通常是相对于客户端位置的。这些值的大小被限制在maxAoIRadius,这允许它们被压缩以节省带宽。
但是,如果一个实体没有任何volatile属性,它将隐式地将其详细位置发送给感兴趣的客户端。详细位置由空间上的绝对坐标组成。由于该位置没有被归类为volatile,所以每当它的值发生变化时,它将被发送给所有对该实体感兴趣的客户端。
对于具有volatile属性的实体类型,可以使用DetailedPosition选项将详细位置发送给感兴趣的客户端。如果一个实体的位置更新需要比通常的压缩值更高的精度,这个选项是有用的。但是,应该谨慎地使用它,因为它使用更大的类型,因此使用更多的带宽。
DetailedPosition选项可以有一个SendLatestOnly标志,如果没有指定,默认为false。
DetailedPosition选项的语法如下所示:
1 | <root> |
5.11. Appeal Radius
有时希望将来自客户端AoI外部实体的属性发送给客户端。例如,一条巨龙可能在许多公里外都能看到。将AoI半径增加到一个更大的值远非理想,因为来自更多实体的不必要更新会大幅增加带宽使用。
非零的AppealRadius值指定实体周围的区域。如果这个区域与客户的相交属性更新将被发送到客户端,就像实体在其AoI中一样。例如,如果客户端的aoiRadius是500米,而一个实体的AppealRadius设置为1500米,那么玩家将能够看到2000米以内的这个实体。具体来说,如果角色和实体之间的距离小于aoiRadius和实体的AppealRadius之和(在X轴和Z轴上),客户端将收到实体的属性更新。
设置AppealRadius的实体类型将隐式地设置DetailedPosition/SendLatestOnly选项。通常为一个实体发送的相对位置更新仅限于客户的AoI中的值,使得这个区域以外的实体太远,无法表示它们的位置。
5.12. Temporary Properties
临时属性(Temporary properties)可用于不需要备份或与实体一起卸载的属性。
临时属性定义的语法如下所示:
1 | <root> |
这些属性通常很少见,但对于不能流化的属性很有用,例如套接字或在恢复时重新创建的属性。这些都适用于单元格实体和基本实体。
5.13. Persistent
通常情况下,游戏实体数据库中至少有一个数据库表与每个实体类型相关联。这是该类型的实体可以持久化的地方。通常,有些实体类型永远不需要持久化。
通过将表的Persistent属性设置为false,可以避免在数据库中创建表。
1 | <root> |
默认为true。将此设置为false的唯一真正好处是减少创建的表的数量。没有实际的性能影响。
5.14. User Data Object Linking With UDO_REF Properties
有一种特殊的属性类型,UDO_REF,可以在实体和用户数据对象中使用。通过此属性类型,可以在实体和用户数据对象之间或两个用户数据对象之间创建连接。这种属性类型是用户数据对象的一个关键特性,因为它允许创建由不同类型的用户数据对象和实体组成的复杂图形,实体脚本可以根据需要使用这些对象和实体。UDO_REF属性只不过是对用户数据对象的引用。
此属性类型最重要的示例是PatrolNode用户数据对象。旧的巡逻路径系统,包括旧的PATROL_PATH属性类型,已经被弃用。Patrol功能现在通过PatrolNode用户数据对象实现,可以通过UDO_REF属性数组将其链接到其他PatrolNode对象。希望在PatrolNode对象图中巡逻的实体只需要有一个链接到PatrolNode的UDO_REF属性。
6. Methods
方法允许在实体的不同执行环境(即单元、基础、客户端)之间以及不同实体之间传播事件。BigWorld根据执行环境将实体方法划分为不同的类别。
一般来说,方法不应该用于传播状态。为此建议使用属性。例如,持枪的玩家应该是属性,而射击的玩家应该是方法。
这些方法的类别包括:
Category | Runs on | Common uses |
---|---|---|
<BaseMethods> | BaseApp | 更新基类上的属性。 作为根点,向相关事物传播消息。 |
<CellMethods> | CellApp | 通知单元的变化,以响应玩家的互动。 允许相邻实体之间的通信。 |
<ClientMethods> | Clients | 通知客户端事件,以便玩家能够看到它们。隐式set_ |
方法声明的语法如下:
1 | <[ClientMethods|CellMethods|BaseMethods]> |
6.1. Basic Method Specification
所有类别中的所有方法都有一些基本的共同特征。它们在<res>/scripts/entity_defs/<entity>.def文件的相关部分声明,每个方法都有一个XML标记。
该方法的参数和返回值(如果有的话)也在文件中定义,其类型的指定方式与属性类型相同。
为了在单元格上声明一个名为yell的方法,该方法接收一个名为phrase的字符串参数,我们需要如下代码行:
1 | <root> |
一旦声明了方法,还需要在适当的Python实现文件中声明它。每个执行环境(单元、基础和客户端)都有一个包含每个实体脚本的文件夹。
在我们的示例中,该方法被添加到<CellMethods>部分,因此将在cell实体上执行。
这个实体的单元格脚本名为<res>/scripts/cell/<entity>.py,需要定义yell方法,如下图所示:
1 | import BigWorld |
6.2. Two-way calls
远程双向方法调用可用于从进程间的Python调用中获取返回值。实体方法的返回值声明格式与实体定义文件中的方法参数相同,只是它们列在ReturnValues标签下而不是Args下。例如,Avatar的webCreateAuction Base方法有如下定义:
1 | <webCreateAuction> |
此方法接受多个参数,其中包含auction的物品的详细信息,并返回唯一的auctionID。但是,这个方法对可能在另一个进程上的实体进行远程调用,这阻止我们以通常的方式接收返回值。相反,该方法将返回一个Twisted Deferred对象,该对象将在接收到返回值后作为参数发送给回调方法。
6.2.1. Twisted Deferred Objects
为了获得双向方法的返回值,使用Twisted Deferred对象。调用方法时,返回Deferred对象。然后可以将两种回调方法添加到该对象中:用于成功调用的回调方法,以及用于发生错误的errback方法。当方法被执行后,它将调用两个回调方法中的一个,这取决于调用的成功与否。也可以为成功和失败链接多个回调。
例如,我们将实现remoteTakeDamage,这是一个使Avatar失去一些生命值并返回其剩余生命值的基础方法。这个方法将调用takeDamage,这是Avatar的Cell方法之一。
takeddamage将以伤害数量作为参数,并返回两个值:实体的剩余生命值和最大生命值。这些值将被打印,而剩余的健康值将作为Deferred对象的最终值从remoteTakeDamage返回。
由于Avatar的Base实体和Cell实体位于不同的进程上,因此调用将使用Deferred对象来有效地实现异步双向调用。这些方法在实体定义文件Avatar.def中有以下定义:
1 | ... |
remoteTakeDamage在fantasydemo/res/scripts/base/Avatar.py中有以下实现
1 | # base/Avatar.py |
因为对takeddamage的调用是异步的,所以我们不会立即收到返回值。相反,将返回Deferred引用的Deferred对象。一旦结果可用,它将以包含返回值的元组的形式存储在Deferred对象中。由于我们无法立即接收和使用这些值,所以一旦它们可用,我们可以使用这些值作为参数指定一个要调用的方法。
addCallback方法用于指定在此结果可用时调用的方法。这个方法的参数是结果元组。在我们的示例中,一旦结果在Deferred对象,它将作为最后一个参数发送给onDamageTaken方法。如果使用addCallback指定了其他回调方法,则将依次调用它们。每个回调函数的结果将作为新的结果存储在Deferred对象中,并作为列表中的下一个回调方法的参数发送。这叫做”回调链”。
当链中没有其他回调方法时,最终结果将存储在Deferred对象中。此时,添加的任何其他回调函数都将立即被调用,因为已经有了一个可用的结果。
最后,onDamageTaken的结果将存储在Deferred对象中,并发送给remoteTakeDamage的调用者。
还可以指定方法,以便在远程方法调用失败时调用。这是使用addErrback方法完成的,例如:
1 | deferred.addErrback( onError ) |
如果调用返回一个error对象,它将被用作onError的参数。
通过多次调用addErrback, Errbacks可以像回调一样被链接起来。addCallbacks方法向延迟对象同时添加回调和errback,但与使用addCallback和addErrback分别添加这两个对象相比,它的行为略有不同。addCallbacks将这两个方法添加到调用链中的同一级别,而分别添加它们将使每个方法处于自己的级别。
在cell/Avatar.py中,takeDamage的实现如下:
1 | # cell/Avatar.py |
以这种方式调用的方法必须返回包含返回值的元组或Deferred对象。如果一个元组从takeDamage返回,它的值将被存储在remoteTakeDamage方法中,并作为参数发送到它的回调方法onDamageTaken,如上所述。但是,如果返回一个Deferred对象,它的结果元组和为它指定的任何回调将被传播到remoteTakeDamage的Deferred对象,onDamageTaken回调将被添加到链的末尾。
对于创建Deferred对象的远程方法的例子,添加回调和错误回调到它们并返回它们,参考FantasyDemo的Avatar和AuctionHouse实体中的web接口Base方法,位于FantasyDemo /res/scripts/base。
在我们的takeDamage实现中,一个错误会导致CustomErrors。InvalidFieldError对象(从Exception派生的自定义错误)使用deferred .fail返回。这个方法创建一个特殊的错误对象:一个已经调用errback的Deferred对象。使用此方法是向原始调用者返回错误的一种简单方法。一旦错误对象被原始调用者接收,如果有errback方法,它将被用作第一个errback方法的参数。而不是叫defer。失败,则会引发异常:
1 | raise CustomErrors.InvalidFieldError( "Avatar can not take negative damage" ) |
这将与使用defer返回错误对象的结果相同。失败,并将错误的调用堆栈发送给MessageLogger。
在本例中,我们使用CustomErrors对象。这些自定义错误派生自BWError,是在双向调用中发送错误的推荐方式。
6.2.2. Error objects
如果双向调用失败,它将向Deferred对象发送一个错误对象,如果指定了errback方法,则调用第一个errback方法。该节点是Exception的实例。
在bigworld/res/scripts/server_common/BWTwoWay.py中定义了许多自定义异常类BWError派生的错误类型。这些可以被扩展以适应游戏脚本中可能出现的错误。BWError的两个子类被定义:BWStandardError,主要用于作为服务器二进制文件中出现错误的基类;BWCustomError,用于作为游戏脚本中出现错误的基类。
6.2.2.1. BWStandardError
源自服务器二进制文件的错误对象派生自BWStandardError类型,并在bigworld/res/scripts/server_common/BWTwoWay.py中声明。这些错误类型如下:
BWAuthenticateError
玩家不能使用他们提供的凭证进行身份验证
BWInternalError
服务器遇到了一个由内部导致的错误。
BWInvalidArgsError
使用无效参数调用方法。
BWMercuryError
没有收到对请求的回应。
BWNoSuchCellEntityError
无法找到一个单元格实体。
BWNoSuchEntityError
找不到实体。
6.2.2.2. BWCustomError
应该实现新的错误类型,以使错误尽可能有帮助。BWCustomError应该被用作游戏脚本特定的错误类范围的基类。例如,FantasyDemo为它的拍卖行实现的双向调用定义了许多自定义错误类,例如InsufficientGoldError和InvalidItemError。
自定义错误声明在<res>/scripts/server_common/CustomErrors.py中。上面remoteTakeDamage的实现展示了一个自定义错误是如何作为延迟对象而不是返回值返回的:
1 | return defer.fail( CustomErrors.<Error Type>( <Args> ) ) |
6.3. Service Methods
服务方法的定义方式与基本方法、单元方法和客户端方法非常相似。但是,这里有一些细微的区别,因为与其他类别不同,服务不是实体。服务方法在<res>/scripts/service_defs/< Service >.def文件中声明,每个方法都有一个XML标记。
服务方法属于methods标签下,而不是属于特定的类别,如BaseMethods或CellMethods。
为了在服务上声明一个名为addNote的方法,该方法接收一个名为newNote的字符串参数,我们将有以下几行:
1 | <root> |
<res>/scripts/service_defs/<service>.def ‐ 服务方法addNote的声明
一旦声明了方法,还需要在服务Python实现文件中实现它。服务执行上下文有一个目录,其中包含每个服务的脚本。
这个实体的脚本名为<res>/scripts/service/<service>.py,需要定义addNote方法,如下图所示:
1 | import BigWorld |
<res>/scripts/service/<entity>.py ‐ Definition of service method addNote
6.4. Intra-Entity Communication
实体的不同执行环境通过调用其他执行环境的方法来相互通信。这些是作为实体的特殊属性公开的。
作为快速参考,现将可用的对象说明如下:
allClients - 可用:cell
何时/如何使用:在此实体的所有客户端实例上调用客户端方法。
例如:self.allClients.someMethod ()
base - 可用:Cell,Client
何时/如何使用:调用此实体的基方法。对该对象的调用在基脚本上执行。客户端不能直接调用基于其他实体的方法。
例如:self.cell.someMethod()
cell - 可用:Base,Client
何时/如何使用:调用此实体的单元格方法。对该对象的调用在单元格脚本上执行。所有客户端实例都可以访问它们的单元对象(当实体存在于单元上时)。
例如:self.cell.someMethod ()
otherClient - 可用:Cell
何时/如何使用:在这个实体的所有客户端实例上调用客户端方法,除了它自己的。
例如:self.otherClients.someMethod ()
ownClient - 可用: Cell,Base
何时/如何使用:只在这个实体的客户端上调用客户端方法。这个对象只在客户端应用程序中作为这个实体“播放”的实体上调用这个方法,而不是在其他可以看到这个实体的客户端应用程序上。
例如:self.ownClient.someMethod ()
附近实体的方法可以从客户端直接调用该实体的单元部分。
BigWorld会自动将这些对象公开给相关的脚本类。
这意味着,任何作为实体(单元、基础或客户端部分)一部分的脚本都可以调用属于同一实体的其他脚本。定义文件(<res>/scripts/entity_defs/<entity>.def)描述哪些方法公开给不同的执行环境。
6.5. Bandwidth Optimisation: Send Latest Only
当在Entity.otherClients(或Entity.allClients)上调用一个方法时,将创建一个事件对象并将其添加到事件历史记录中。当更新在其AoI中具有此实体的客户端应用程序时,将使用此事件历史记录。当这个实体进入其AoI时,自上次考虑此实体以来添加的任何事件都将发送给客户端。在单个更新中可能会发送对单个方法的多个调用。
如果在客户端方法上设置了SendLatestOnly标志,则事件历史中只保留最近的调用。这样可以避免发送多个调用。这可以节省发送到客户端的带宽,并且如果一个方法被频繁调用并且只需要最近一次调用,可以节省CellApps上的一些内存。
默认情况下,此值为false。
6.6. Bandwidth Optimisation: Is Reliable
当在Entity.otherClients(或Entity.allClients)上调用方法时,消息将发送到AoI中包含该实体的适当客户端应用程序。默认情况下,这些消息是可靠地发送的。如果在客户端方法上将“isReliable”标志设置为false,则此消息将不可靠地发送。
6.7. Sending Auxiliary Data to the Client Via Proxy
辅助数据(Auxiliary Data)可以通过代理(proxy)流到客户端,不影响正常的游戏流量。当带宽可用时,将这些数据机会适当地流到客户机。
辅助数据流使用的带宽可以通过<baseApp/downloadStreaming>中的bw.xml选项来控制。
这个流数据中的所有数据类型都是用户定义的,因为BigWorld没有任何内部用途。
通过streamStringToClient方法将数据添加到代理:
1 | id = Proxy.streamStringToClient(id,data) |
或通过方法streamFileToClient:
1 | id = Proxy.streamFileToClient(id,resource) |
参数:
id
16位数据ID。如果接收到-1,则选择当前没有使用的下一个ID。这个方法的调用者负责管理这个参数。将方法使用的相同ID值返回给调用者。
data
要发送到附加客户端的数据。必须为字符串格式。
resource
要发送给客户端的资源的字符串名称。
一旦客户端接收到整个数据字符串,就会在客户端上调用回调BWPersonality.onStreamComplete。
6.8. Exposed Methods ‐ Client-to-Server Communication
因为mmo游戏是在网络上运行的,为了防止玩家作弊,服务器方法(那些在单元或基础上的)不允许由客户端自动调用。
为了使服务器方法可以从客户端调用(这样世界就可以提供交互),它的声明必须包含标签<Exposed/>,如下图所示:
1 | <root> |
<res>/scripts/entity_defs/<entity>.def
标签<Exposed/>完成了两件事:
- 它使客户端可以使用该方法。
- 在单元格上,它充当额外的参数标记,该参数标记自动填充调用它的客户机的实体ID。
客户端实例实际调用方法时使用的参数比单元实体接收到的参数少一个,这可以防止在安全服务器环境之外的“伪造实体”。在调用服务器组件的公开方法时,需要将实体ID作为参数传递。
必须对单元格上的方法的定义进行扩展,以获取此参数,如下图所示:
1 | import BigWorld |
<res>/scripts/cell/<entity>.py
在客户端,可以用下面的代码调用方法yell:
1 | self.cell.yell( "BigWorld message test" ) |
如果cell方法yell实现了对self.id的sourceEntityID的检查,则只有它的客户端能够调用它。其他客户端将无法执行它。
在某些情况下,该方法可能使用不同的sourceEntityID运行。例如,对于一个名为shakeHand的方法,在继续操作之前,最好检查源实体与自我实体之间的距离是否在几米之内(也许两者都没有死亡)。
6.8.1. Security Considerations of Exposed Methods
脚本编写者应该意识到,在操作任何公开方法的参数之前,都需要在服务器端仔细检查。
处理参数传递的底层c++代码确保只有在以下情况下服务器端才会调用该方法:
- 传递正确数量的参数。
- 参数具有预期的类型。
然而,除此之外,底层体系结构不能对客户端传递给服务器上的方法调用的值提供任何进一步的约束。
例如,整数可以接受任何有效的32位值,字符串和数组可以是任何长度,….脚本编写者需要确保公开方法的参数具有在该方法上下文中有意义的值。
6.9. Server to Client bandwidth usage of Method calls
从服务器到客户端的方法调用的参数与优化属性更新的方式相同。
6.10. Client callbacks on property changes
当服务器更改客户端的属性时,实体将调用一个回调方法,以允许客户端采取适当的操作。
当实体的属性被替换为新值时,调用Set_
如果修改现有属性,则调用setNested__
6.10.1. Implicit set_**** Methods
当服务器更新带有分布标志ALL_CLIENTS, OTHER_CLIENTS或OWN_CLIENT的属性时,隐式方法set_<在客户端调用。
这个方法不需要在定义文件的<ClientMethods>部分声明,因为它是由BigWorld自动提供的。
所有隐式定义的set_
1 | class Seat( BigWorld.Entity ): |
<res>/scripts/client/Seat.py
请注意,如果修改而不是替换现有属性,并且合适的setNested_
6.10.2. Implicit setNested_**** Methods
如果修改现有属性的嵌套属性,则调用setNested_
例如:
1 | class Seat( BigWorld.Entity ): |
第一个参数表示已更改属性的路径。例如,如果更改为:
1 | ... |
path的值为:
1 | ["rightHandItem", "weight"] |
如果你有一个属性是ARRAY <of> ARRAY <of> INT32 </of> </of>。
1 | self.myArray[ 5 ][ 3 ] = 8 |
将导致path的值为:
1 | (5,3) |
6.10.3. Implicit setSlice_**** Methods
如果修改了数组的一个切片,则调用setSlice_
例如,if self.myArray是一个已有的5个元素的数组:
1 | self.myArray.append( 10 ) |
将导致setSlice_myArray被调用:
1 | path = [(5,6)] |
如果数组是FIXED_DICT的一部分,并且数组有5个元素,最后一个值是21。
1 | del self.myFixedDict.myArray[-1] |
将导致:
1 | path = ["myArray", (4,4)] |
6.11. LOD on Methods
与属性一样,有些方法只需要传播给附近的实体。
例如,即使一个实体可能在AoI距离上可见(例如500米),但玩家似乎不太可能区分微笑和不微笑的实体。
为了体现他的,微笑法的细节水平,可以声明如下图:
1 | <root> |
<res>/scripts/cell/<entity>.py
方法的LOD规范要比属性的LOD规范简单得多。这是因为如果LOD增加,则非广播属性更改可能必须稍后发送,而同一场景中的非广播方法则不必发送。
因此,该方法只需要内联声明一个标记<DetailDistance>,当它在公开对象allClients或otherClients上被调用时,它只会广播给实体周围指定距离内的客户端。
6.12. Inter-Entity Communication
一旦实体有了分配给它们的方法,能够调用其他实体的方法就变得很有用。
如果你有一个ent对象作为另一个实体的Python脚本表示,那么你可以调用ent.somemethod()来在ent上调用那个方法。这假设someemethod()在您所在的执行环境中运行。例如,如果您在单元格上,那么ent.somemethod()必须由ent的实体类型定义和提供。
如果你在cell上,你可以调用base上的一个方法,代码如下:
1 | ent.base.otherMethod() |
这意味着一旦您能够获得实体对象,就会有大量的选项用于在不同实体实例的不同执行环境中调用方法。
6.12.1. Entity IDs
为了唯一地识别游戏世界中的每个对象,BigWorld为每个实体分配了一个唯一的数字。这被称为实体ID。
在BigWorld Python模块(在大多数脚本的开头导入)中,有一个对象,它将实体id映射到相应的实体对象。该对象与Python字典具有相同的接口,名为BigWorld.entities。
给定一个实体id entityID,它的实体对象可以通过下面的代码来检索:
1 | ent = BigWorld.entities[ entityID ] |
人们应该非常小心实体id,并总是在假设可以安全使用相应实体之前检查它是否存在。大世界。如果查找的实体不存在,实体将抛出异常。
每个执行环境都有一个BigWorld.entities的版本对象,其中包含不同实体的实体对象。
BigWorld.enyities 包含与它所在的执行上下文相关的实体,如下所示:
执行环境 | 在BigWorld.entities中列出的实体 |
---|---|
Cell | 真实和鬼影实体位于由该cell的CellApp管理的所有cell。 |
Base | 位于BaseApp上的实体 |
Client | 客户端AoI中的实体。 |
可以通过多种方式获取实体id:
从公开的方法(客户端发送实体ID作为参数)。
来自实体对象的id属性。
您可以将对象的ID从一个执行环境传递到另一个执行环境(例如,从客户端传递到单元格),以便另一个环境可以获得相应的对象。您还可以使用此属性获取新创建实体的ID。
从可以用来查找实体引用的各种实用程序方法。
其中一个方法是entitiesInRange。此方法为每个单元实体定义,并返回位于与调用实体一定距离内的所有实体对象。结果输出可以再次查询,以获得更具体的搜索结果。
例如,要找到当前实体100米内的所有守卫实体,你可以使用以下代码:
1
2
3
4
5
6def findGuards():
output = []
for entity in self.entitiesInRange( 100 ):
if entity.__class__.__name__ == "Guard":
output += [entity]
return output在使用这种方法时需要注意搜索范围不能太大。
6.12.2. Retrieving Services
实体是通过entityID来标识的,而服务是通过名称来标识的,而不管有多少ServiceApps提供它。每个服务实例被称为一个服务片段。
在BigWorld Python模块中,有一个对象,它将服务的名称映射到提供该服务的ServiceApps上相应的服务片段。这个对象被称为BigWorld.services,如BigWorld.entities,它具有与Python字典相同的接口。
服务serviceName可以使用下面的代码检索:
1 | serviceMailbox = BigWorld.services[ serviceName ] |
以这种方式检索的服务将是对应服务片段的邮箱。一旦检索到此邮箱,就可以调用服务的方法。例如:
1 | BigWorld.services[ 'ExampleService' ].myTest( u"This is a test" ) |
6.13. Mailboxes
邮箱用于与远程实体(即不在当前进程上的实体)通信。
实体只能访问在同一执行环境中运行的其他实体。然而,能够向其他执行环境中的实体发送消息通常是有用的。例如,一个实体可能希望向一个聊天通道的所有成员发送一条消息,但是该通道可能不会将其所有成员与执行实体位于同一个BaseApp上。
邮箱用于实现实体的以下属性:
- self.cell
- self.base
- self.ownClient
还有一组特殊的邮箱可供单元使用,它们引用与当前实体相关的其他实体。
前面提到的这些属性(cell, base, ownClient)允许您引用位于不同进程上的对象,并且可以像任何其他值一样发送,使用方法调用和MAILBOX数据类型。
您可以像使用普通实体引用一样使用MAILBOX,并在其他进程上的其他实体上调用实体定义文件中声明的方法。
考虑到一个实体B(在下面的例子中引用为另一个实体)有一个方法heyThere,它接受一个MAILBOX类型的参数,实体a可以将其基本邮箱从单元格传递给实体B,如下面的例子:
1 | anotherEntity.heyThere( self.base ) |
在接收实体B(在示例中引用为另一个实体)上,调用实体(实体A)的基本邮箱(通过方法参数接收)可以用于在实体A上调用方法someMethod。例如:
1 | def heyThere( self, originalEntity ): |
也可以将邮箱作为属性存储在类中。但是,这只对基本实体邮箱有用,因为当实体在空间中移动时,它们不会更改地址。
通过使用邮箱,还可以调用与被调用邮箱不同的执行环境中的方法。例如,使用实体的基本邮箱(以下代码中的baseMB),可以如下调用基本实体的cell方法:
1 | baseMB.cell.cellMethod() |
该调用首先被发送到基本实体,然后该实体从基本实体所在的位置调用cell方法。虽然它比调用基方法(基方法反过来调用cell方法)更方便,但调用cell实体仍然需要两个跃点。
可用的用法是:
- baseMB.cell
- baseMB.ownClient
- cellMB.base
- cellMB.ownClient
这些实际上是mailbox的实例,可以作为方法参数传递。然而,同样的限制也适用于cellMB.base和cellMB.base对于cell邮箱来说——它们可以随着实体的移动而改变,因此不应该被储存起来供以后使用。
注意baseMB.ownClient和 cellMB.ownClient仅指它们own client。没有对otherClients和allClients的快捷调用。
获取其他实体邮箱的一种方便方法是在BaseApp上使用BigWorld.lookUpBaseByDBID 和 BigWorld.lookUpBaseByName 更多详细信息,请参见 BaseApp Python API’s entry Main Page → BigWorld (Module) → Member Functions.
6.13.1. Special Mailboxes
在前面讨论的邮箱之上,CellApp还提供了一组特殊的邮箱,可用于与引用实体的AoI内的实体通信。
可供使用的邮箱如下:
- entity.allClients
- entity.otherClients
- entity.clientEntity(EntityID)
这个allClients邮箱将调用存在于引用实体的AoI中的所有客户实体以及引用实体的ownClient上的一个方法。
下图演示了对实体a上的allClients邮箱的调用,例如A.allClients.chat()。这导致在ClientApps A上的实体A上调用chat()方法,B和C。

otherClients邮箱几乎与allClients邮箱相同,不同的是所调用的方法不会在引用实体的ownClient上执行。这对于引用实体的客户端是操作的发起者的情况很有用。
下图演示了对实体a上otherClients邮箱的调用,例如A.otherClients.jump()。这导致在ClientApps B和C上的实体A上调用jump()方法。

最复杂的邮箱是clientEntity(EntityID)。此邮箱用于调用在引用实体的ClientApp上表示的实体上的方法。这对于导致特定于单个客户端的行为只被感兴趣的客户端看到是很有用的,例如任务给予者与玩家交谈。
下面的图表说明了对实体B上的clientEntity邮箱的调用,它提供了C的EntityID,例如B.clientEntity(C.id).wave()。这导致在ClientApp B的实体C上调用wave()方法。

6.14. Method Execution Context
所有跨物理机器的实体方法调用都是异步的。例如,如果您在基本实体上执行self.cell.cellMethod()或self.client.clientMethod(),调用将立即返回不带任何值的结果。实际的方法执行发生在实体的单元或客户端部分驻留的机器上。
要将执行结果通知调用实体,必须从方法内部调用一个函数。
下面的例子描述了类Avatar的客户实体启动一系列操作来打开一扇门,执行以下步骤:
- 类Avatar的客户端方法openDoor调用它的单元格方法openDoor
- 然后,该方法调用类Door的单元格方法unlock,并传递self作为参数。通过这种方式,Door接收单元实体Avatar的邮箱,稍后将使用该邮箱(与公开的对象客户端一起)调用Avatar上适当的cell方法。
- 然后,该方法检查密钥卡,并使用单元格邮箱(sourceEntity)直接调用类avatar的适当客户端方法(在本例中,我们假设它是成功的)。
Avatar类的客户端实体:
1
2
3
4
5
6
7
8
9
10
11
12class Avatar( ):
...
def openDoor( self, doorID ):
# Call the cell method to open the door
self.cell.openDoor( doorID )
...
def doorOpenFailed( self, doorID, keycard ):
# Animation shows the Avatar scratching his head
...
def doorOpenSucceeded( self, doorID, keycard ):
# Animation shows the door with corresponding doorID opening
...Avatar类的cell实体:
1
2
3
4
5
6
7
8class Avatar( ):
...
def openDoor( self, doorID ):
# locate the door
door = self.locateTheDoor( doorID )
keycard = self.getKeycardFromInventory()
door.unlock( self, keycard )
...门类的单元实体:
1
2
3
4
5
6
7
8
9
10
11class Door( BigWorld.Entity ):
...
def unlock( self, sourceEntity, keycard ):
# check source is close enough
# check keycard is good
if not self.isGoodKeycard( keycard ):
sourceEntity.client.doorOpenFailed( self.id, keycard )
else:
self.isOpen = True
sourceEntity.client.doorOpenSucceeded( self.id, keycard )
...使用相同的回调技术从一个被调用的方法中返回值。
下面的例子描述了类Avatar的客户实体启动了一系列的操作,根据库存索引查询物品的描述,执行以下步骤:
- 客户端类Avatar的investigateInventory 方法调用它的基方法itemDescriptionRequest。
- 然后该方法调用其客户端方法itemDescriptionReply。
- 然后,该方法显示项目的描述。
Avatar类的客户端实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Avatar( ):
...
def investigateInventory( self, indexInInventory ):
# first get the details from the server
self.base.itemDescriptionRequest( indexInInventory )
# maybe have a timeout in case server doesn't reply
...
def itemDescriptionReply( self, indexInInventory, desc ):
# call the callback
if desc == []:
GUI.addAlert( "No such item" + str(indexInInventory) )
return
GUI.displayItemWindow( indexInInventory, desc )
...cell类的基本实体:
1
2
3
4
5
6
7
8
9class Avatar( ):
...
def itemDescriptionRequest( self, indexInInventory ):
try:
desc = self.generateDescription( indexInInventory )
except:
desc = [] # in case no such index
self.client.itemDescriptionReply( indexInInventory, desc )
...
对于驻留在同一进程中的那些实体,方法调用是同步进行的。但是,由于不能保证调用实体和被调用实体总是在同一个进程中,因此最好采用回调解决方案。
一个特殊情况是实体的定义文件(<res>/scripts/ entity_defs/<entity>.def‐详细信息见 “The Entity Definition File”)中没有定义的实体方法。在这种情况下,方法总是同步执行,并且只在调用者的进程中执行。例如,对影实体的方法调用通常会委托给真实的实体。然而,如果该方法没有在实体的定义文件中定义,它将被视为一个普通的Python方法,并在本地运行。
这种机制确实节省了服务器组件之间的一些网络通信量,并且可以立即向调用者返回结果,但它也受到了限制,因为它只能访问只读的ghosting属性。尝试访问非ghost属性或写入只读属性将导致意外错误。除非经过仔细的规划,否则不应该利用这个特性。
7. Inheritance in BigWorld
BigWorld使用三个单独的类(单元、基础和客户端实体部分)来实现实体,并使用一个定义文件(<res>/scripts/entity_defs/<entity>.def)将它们捆绑在一起。因此,实体或实体的一部分可以有多种方式在其规范和实现中使用继承。
在BigWorld中有三种不同的方式来声明继承关系,它们都满足不同的需求。
7.1. Python Class Inheritance
Python语言允许相互派生类。
例如,定义从a派生而来的类B,以及它们各自的方法,你可以有以下代码:
1 | class A: |
然后假设你有一个程序,代码如下:
1 | x = B() |
当在实体中使用时,这种形式的继承允许在实体类型之间共享公共实现细节。允许多重继承,因此您可以使用许多Python类来帮助在某些实体中实现不同的特性,例如:

下面的代码片段展示了如何在实体DerivedEntity中使用Python类CommonBase
如果基类的cell脚本(<res>/scripts/cell/CommonBase.py)定义如下:
1
2
3
4
5
6
7
8# note that this class is not derived from BigWorld.Entity
# so it is just an ordinary Python class
class CommonBase:
...
def readyForAction( self ):
# implement method's logic
return True
...如果派生实体的单元格脚本(<res>/scripts/cell/DerivedEntity1.py)定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import BigWorld
from common import CommonBase
...
# derive from CommonBase, so you can use the method readyForAction
class DerivedEntity1( BigWorld.Entity, CommonBase ):
...
def __init__( self ):
BigWorld.Entity.__init__( self )
CommonBase.__init__( self )
...
def someAction( self ):
if self.readyForAction():
print "action performed"
...然后你可以从基类中调用方法,如下图所示:
1
DerivedEntity1.readyForAction()
7.2. Entity Interfaces
BigWorld还以类似于Java接口系统的形式支持继承。可以有一个文件夹<res>/ scripts/defs/interfaces,可以用来声明实体的公共部分。这允许将定义放在常用声明的一个位置。
这个概念说明如下:

实体接口定义文件的格式与实体定义文件的格式类似,只是接口定义文件没有<Parent>部分。
接口定义文件的概要描述如下(所有部分都是可选的):
1 | <root> |
与实体不同,实体接口不需要有相关的Python实现文件,尽管这可能是一个好主意。
下面的代码片段演示了在实体定义文件中使用接口的结果:
如果一个实体被定义为实现一个接口(<res>/scripts/entity_defs/ someEntity.def),如下所示:
1
2
3
4
5
6
7
8<!-- someEntity -->
<root>
...
<Implements>
<Interface> someInterface </Interface>
</Implements>
...
</root>如果实现的接口定义如下(<res>/scripts/entity_defs/interfaces/someInterface.def):
1
2
3
4
5
6
7
8
9<!-- someInterface -->
<root>
<Properties>
<name>
<Type> STRING </Type>
<Flags> ALL_CLIENTS </Flags>
</name>
</Properties>
</root>那么在概念上,得到的实体定义如下:
1
2
3
4
5
6
7
8
9
10
11<!-- someEntity -->
<root>
...
<Properties>
<name>
<Type> STRING </Type>
<Flags> ALL_CLIENTS </Flags>
</name>
</Properties>
...
</root>
如果需要更改描述,则可以重写来自接口的属性。在这种情况下,整个属性描述将被替换为新的描述,因此需要指定所有适当的字段。
7.3. Entity Parents
通常可以将提供其他实体类型通用功能的实体定义为单个基本实体。例如,一组npc可能会共享它们的大部分执行,但需要一些特定的调整才能将它们变成守卫或店主:

下面的代码片段演示了这种形式的继承。
定义基本实体GenericEntity (<res>/scripts/entity_defs/GenericEntity.def):
1
2
3
4<!-- GenericEntity -->
<root>
<!-- common properties and methods -->
</root>定义GenericEntity的基脚本:
1
2
3
4
5
6import BigWorld
class GenericEntity( BigWorld.Base ):
...
def __init__( self ):
BigWorld.Base.__init__( self )
...定义GenericEntity的单元格脚本:
1
2
3
4
5
6import BigWorld
class GenericEntity( BigWorld.Entity ):
...
def __init__( self ):
BigWorld.Entity.__init__( self )
...定义派生实体:
1
2
3
4
5
6<!-- SpecificEntity -->
<root>
<!-- inheritance is defined in this tag -->
<Parent> GenericEntity </Parent>
<!-- add more properties and methods here -->
</root>定义SpecificEntity的基础脚本:
1
2
3
4
5
6
7import BigWorld
import GenericEntity
class SpecificEntity( GenericEntity.GenericEntity ):
...
def __init__( self ):
GenericEntity.GenericEntity.__init__( self )
...定义SpecificEntity的单元格脚本:
1
2
3
4
5
6
7import BigWorld
import GenericEntity
class SpecificEntity( GenericEntity.GenericEntity ):
...
def __init__( self ):
GenericEntity.GenericEntity.__init__( self )
...
7.4. Client Entity Reuse
有时,可能只需要在服务器上指定一个实体类型。在.def文件中使用可选<ClientName>部分允许为客户端实体使用不同的(通常是父的)实体类型。
例如,如果NPC来自Avatar,并且NPC包含了客户端不需要访问的附加属性,那么NPC对象就可以作为Avatar对象发送给客户端。这意味着客户端不需要特定的脚本来处理npc。
7.5. User Data Object Interfaces and Parents
为实体描述的接口和父级的继承也适用于用户数据对象。由于用户数据对象与普通实体相似( 具体查看”Entity Interfaces”和”Entity Parents”)
关于在用户数据对象中继承的例子,请参见<res>/scripts/user_data_object_defs/ testtem .def和<res>/scripts/user_data_object_defs/testParent.def。
8. Entity Instantiation and Destruction
由于BigWorld实体必须建立和相互链接的方式,它们的实例化方式必须与Python中其他对象的实例化方式不同。同样地,因为在破坏时,部件必须断开连接,所以有一些特殊的方法来完成这一点。
正如在前面 “The Entity Script Files” 一节中提到的,实体可以有位于单元格上的一部分(以真实和幽灵的形式),一部分在基础上,另一部分在客户端上,在他们的AoI中有实体。不同的实体类型可能只支持它们的一个、两个或所有三个实例。此外,实体类型的实例可能比其类型支持的部分更少。
通常,首先创建实体的基本部分,然后(如果合适的话)创建其单元格部分。这有很多原因。
- 基本实体可以直接从数据库创建,而单元实体不能。
- 基本实体可以创建其单元格部分,但反之则不行。
- 单元实体需要关联的基本实体才能容错。
- 单元实体需要关联的基本实体来将自身写入数据库。
对于具有基本部分和单元格部分的实体类型,基本部分总是在单元格部分之前创建,然后在它之后销毁。也可以创建一个没有基本部分的单元实体。
8.1. Entity Instantiation on the BaseApp
基本实体可以通过以下方式创建:
- 直接从脚本,使用方法BigWorld.createBaseAnywhere,BigWorld.createBaseLocally, 或者BigWorld.createBaseRemotely。
- 从数据库中,使用方法BigWorld.createBaseFromDBID 或者BigWorld.createBaseFromDB。
BigWorld.createBaseAnywhere方法可以指定基实体和单元实体属性,并具有以下签名:
1 | def createBaseAnywhere( entityTypeName, *args, **kwargs ): |
参数entityTypeName是一个字符串,包含要实例化的实体类型的名称。例如,要实例化一个实体ExampleEntity,该参数将是“ExampleEntity”。
在最简单的形式中,它使用所有默认值创建实体,并如下例调用:
1 | newEntity = BigWorld.createBaseAnywhere( "ExampleEntity" ) |
此方法可以选择接受一列其他参数,搜索这些参数以创建基本实体值和单元实体值。这些参数可以是:
- 关键词参数
- 字典
- ResMgr.DataSection
首先搜索关键字参数,然后是字典,最后是DataSection。如果没有为实体的任何属性找到值,则为该属性/数据类型分配默认值。
在实体定义中找不到的关键字参数和字典值被设置为基本实体属性。
8.1.1. ServiceApps
ServiceApp是BaseApp的一种专门类型。它不会在登录过程中创建玩家实体,也不会使用BigWorld.createBaseAnywhere()创建基础实体。
虽然很少,实体可以使用bigworld.createbasellocal()在ServiceApps上创建。这可能很有用,例如,为了存储状态,将一个实体与一个Service片段关联起来。实体也可以使用bigworld.createbaseremote()创建。
8.2. Cell Entity Creation From BaseApp
方法BigWorld.createBaseAnywhere只创建实体的基表示。如果需要单元实体,则基本实体有责任实例化其关联的单元实体。
要创建关联的单元实体,可以使用以下方法:
- Base.createCellEntity
- Base.createInNewSpace
- Base.createInDefaultSpace
这些方法读取基本实体的特殊变量base.cellData(在创建基本实体时使用单元实体的数据进行初始化)来获取单元实体的初始化值。如果实体类型不支持单元实体,则基本实体将没有cellData。
变量cellData的行为就像一个字典,包含实体定义文件中定义的所有单元格属性(<res>/scripts/entity_defs/<entity>.def)。
它还有另外三个成员:
- position - 三个浮点数(x, y, z)的序列,或者Vector3的位置来创建新的实体
- direction - 三个浮动序列(横摇,俯仰和偏航)与新单元实体的方向。
- spaceID - 如果没有以不同的方式指定空间,将在其中创建单元格实体的空间ID。
一旦单元实体成功创建,将执行以下步骤:
- 删除变量cellData。
- 使用单元实体的邮箱创建一个名为cell的变量
- 回调Base.onGetCell将被调用
8.2.1. Creation Near an Existing Cell Entity
下图说明了使用BigWorld模块的方法Base.createCellEntity创建单元格实体。当已创建单元格实体时,将无法使用此方法。
Python中createCellEntity方法的声明如下所示
1 | class Base: |
参数邮箱是一个单元实体邮箱。新单元实体将在与邮箱引用相同的空间和单元中创建(如果邮箱不是None)。理想情况下,两个实体接近,因为这增加了实体从正确的单元开始的可能性。
下图显示了在正确的单元格上创建实体时的通信流:
下图显示了在错误的单元格上创建实体时的通信流:

8.2.2. Creation in a Numbered Space
也可以通过在属性cellData中为spaceID设置一个适当的值来创建单元实体。这是应该避免的,因为它需要通过CellAppMgr发送请求,这可能会导致瓶颈。
一旦创建了单元实体,就会在基本实体上调用通知方法onGetCell。这是一个信号,表明现在可以安全地开始使用单元实体self.cell的邮箱了。
对于实体someEntity, onGetCell方法可以如下图所示定义:
1 | import BigWorld |
8.2.3. Creation in a New Space
该方法Base.createInNewSpace向CellAppMgr发送一个请求来创建一个新的空间和它上面的实体。
产生的消息跟踪如下图所示:
8.2.4. Creation in Default Space
方法Base.createInDefaultSpace与方法Base.createInNewSpace类似,只是没有创建一个新的空间。
只有在配置文件/server/bw.xml中标记设置为true时,这才可用。
8.3. Entity Destruction
基本实体总是在单元实体之前创建,并在单元实体之后销毁。
一个单元实体被破坏后所发生的一系列事件描述如下:
步骤 | Base | Cell |
---|---|---|
1 | 调用方法destoryCellEntity | 调用方法destory |
2 | 方法onDestory自动被调用 | |
3 | 自动调用onLoseCell方法。如果base要被销毁, 这是调用destroy方法的好地方。 |
|
4 | cell属性丢失 | |
5 | 恢复cellData属性,并保留其销毁时的值。 |
Base.destroy方法有两个布尔关键字参数:
- deleteFromDB - 默认值为fasle
- writeToDB - 如果实体之前已写入数据库,则默认值为true
8.4. Entity Instantiation From The CellApp
在创建单元实体时,可以使用对应的基实体创建单元实体,也可以不创建单元实体。下面的小节描述了这两种方法。
8.4.1. Instantiation With No Base Counterpart
可以调用方法BigWorld.createEntity来创建一个没有关联的基实体的单元格实体。此方案如下图所示:
方法BigWorld.createEntity具有以下签名:
1 | def createEntity( entityTypeName, spaceID, position, direction, properties ): |
详情查看CellApp Python API’s entry Main Page → Cell →BigWorld → Function → createEntity.
8.4.2. Instantiation With Base Counterpart
方法BigWorld.createEntityOnBase允许CellApp创建基本实体。它具有以下签名:
1 | def createEntityOnBaseApp( entityTypeName, properties ): |
这个函数接受以下参数:
- entityTypeName - 需要创建的实体名字
- properties - 在实体的定义文件中列出的在base上的属性字典。
这个函数向一个BaseApp发送一个消息来创建一个基本实体,这个基本实体稍后可以调用createCellEntity方法来创建单元实体。
8.5. Loading Entities From Chunk Files
世界编辑器可用于将实体占位符插入到数据块中。这些占位符可以通过服务器上的Python脚本读取,从而使用BaseApps上的BigWorld.fetchEntitiesFromChunks方法将这些实体加载到游戏世界中。详情查看fantasydemo/res/scripts/base/TeleportPoint.py
9. The Database Layer
数据库层是BigWorld的持久实体仓库。它允许将特定实体写入在线存储(通常写入数据库表或磁盘文件),并在稍后再次检索它们。
数据库层不是每个实体都要频繁访问的,而是只在实体创建和销毁时(可能在关键的交易点)才访问。你不应该试图访问数据库来响应每个角色的每一个动作——让灾难恢复机制来处理游戏的完整性。
本章详细介绍如何从数据库中存储和检索实体。
9.1. Persistent Properties
持久化实体的第一步是编辑它的定义文件(命名为<res>/scripts/ entity_defs/<entity>.def),并指定要持久化的属性。
持久属性集通常是实体属性的一个小子集。例如,角色扮演游戏通常有一组核心属性(力量、敏捷等),以及一组需要临时修改的衍生属性(也许角色在登录时总是获得完整的生命力,所以生命力点不需要持续)。
要将一个实体属性标记为persistent,它需要添加标签< persistent >,如下图所示:
1 | <root> |
如果类型是FIXED_DICT,那么可以为FIXED_DICT数据类型的每个属性指定<Persistent>标记。
1 | <root> |
在上面的例子中,someFixedDictProperty.a是持久的,但是someFixedDictProperty.b是。如果<someFixedDictProperty>级别的<Persistent>标记为false,那么a和b都不是持久的。默认情况下,FIXED_DICT字段级别的<Persistent>标记为true,因此没有必要指定它,除非有选择地关闭某些字段的持久性
可以为MySQL数据库引擎的持久属性设置其他参数。
9.1.1. Non-Persistent Properties
每次创建实体时重置的属性不应该持久化。例如,实体的A.I.和GUI状态通常是非持久性的。减少持久属性的数量将减少数据库上的负载。如果一个属性不是持久的,当从数据库加载实体时,它的值将被设置为默认值。
邮箱属性总是非持久的。
9.1.2. Built-In Properties
以下内置属性是持久的:
- Base:databaseID
- Cell:position,direction,spaceID
所有其他内置属性都是非持久化的。
实体的内置id属性不是持久的。它将在每次重新创建实体时更改。这包括通过灾难恢复机制自动重新创建实体的情况。因此,当存储其他实体的实体id时,它们应该存储在非持久属性中,以便在灾难恢复机制重新创建实体时,它们将自动重置为属性的默认值。这避免了存储无效实体id的可能性。
当实体被我们的容错机制恢复时,实体的id属性是不变的。
使用实体的数据库ID作为对实体的长期引用。
9.1.3. Database Indexing
一个简单的属性可以在数据库数据定义中建立索引,这样索引就可以帮助查找这些属性,比如在使用BaseApp的Python API调用BigWorld.lookUpBasesByIndex()。每个属性可以有唯一索引或非唯一索引。如果未指定,则不会为该属性创建索引。
下面是一个在属性上指定索引的示例:
1 | <root> |
索引属性时有一些限制和条件:
- 仅在使用MySQL数据库时支持索引。
- 只有持久属性是可索引的。
- 只有UNICODE_STRING, STRING, BLOB, FLOAT,UINT和INT变量是可索引的。例如,组合类型如ARRAY和FIXED_DICT是不可索引的。
- UNICODE_STRING, STRING只有前255个字符是可索引的。BLOB类型的前255个字节是可索引的。
在定义索引时,如果省略了Unique部分,则该索引被假定为非惟一的。
9.1.4. The Identifier Tag
<Identifier>标记是持久化STRING或BLOB实体属性的可选标记。它指定一个属性作为该实体类型的标识符。可以使用实体的标识符而不是数据库ID从数据库中检索实体。因此,所有相同类型的实体必须具有惟一标识符。每个实体最多只能有一个属性被标记为标识符。
例如:
1 | <root> |
然后假设上述实体类型有三个实例,它们可以如下表所示:
playerNickname | someProperty2 |
---|---|
playerNickname1 | “cfeh” |
playerNickname2 | “fwep” |
playerNickname3 | “fwep” |
注意,<someProperty1>在数据库中没有表示,因为它没有被指定为持久的。
可以使用BigWorld.lookUpBaseByName和BigWorld.createBaseFromDB等方法按名称搜索具有属性的实体类型。有关详细信息,请参见BaseAppPythonAPI。
将属性标记为标识符属性还会向该属性添加唯一索引。(见Database Indexing)
9.2. Reading and Writing Entities
数据库提供了保存实体的方法,并在以后将它们返回到世界中。它还保证每个保存的实体在世界中只能有一个实例。这可以确保正确地执行实体对数据库的任何写入操作。
为了使用这个功能,您必须首先创建一个持久实体。这样的实体必须存在于BaseApp,可以是BigWorld.base。库或BigWorld.Proxy。您可以使用任何常规技术创建它。
持久化实体的关键是其属性databaseID,并其实体类型。属性databaseID是一个64位整数,在相同类型的实体中是唯一的,通常对应于数据库表中的一个自动增量字段。当使用任何常用技术创建实体时,其数据库ID设置为0,表示从未写入数据库。
要将新创建的实体添加到数据库中,必须调用它的方法writeToDB(从单元格或基库)。
如果在基本实体上调用,writeToDB会接收一个可选参数,指定回调方法。完成后,writeToDB将调用回调函数,并传递一个布尔参数(指示向数据库写入是成功还是失败)和调用该方法的基本实体。使用通知方法,因为数据库写操作是异步操作。
下面的代码片段演示了如何使用基类中的方法writeToDB。
在someEntity的基础脚本(<res>/scripts/base/someEntity.py)中,为writeToDB定义回调方法:
1
2
3
4
5
6
7
8
9import BigWorld
class someEntity( BigWorld.Base )
...
def onWriteToDBComplete( successful, entity ):
if successful:
print "write %i OK. databaseID = %i" % (entity.id, databaseID)
else:
print "write %i was not successful" % entity.id
...调用方法来创建base并将其添加到数据库:
1
2ent = BigWorld.createBase( "someEntity" )
ent.writeToDB( onWriteToDBComplete )BaseApp中显示的结果:
1
write 92 OK. databaseID = 376182
下一次这个实体被销毁时(通过调用ent.destroy方法),它将被“注销”——数据库层会跟踪这个实体是否在世界上。
稍后可以使用方法BigWorld.createBaseFromDBID和数据库中存储的属性将被销毁的实体带回世界,如下所示:
1 | BigWorld.createBaseFromDBID( "someEntity", 376182, optionalCallbackMethod ) |
由于从数据库加载一个已销毁的实体也是一个异步操作,如果你希望这个过程的完成得到通知,你需要传递一个回调函数作为方法bigworld . createbasefrombid的第三个参数。回调函数接收实体标识符作为唯一参数,如果实体加载成功,则为databaseID,否则为None。
下面的代码片段演示了从数据库重新加载实体的请求:
在someEntity的基础脚本(<res>/scripts/base/someEntity.py)中,定义createBaseFromDBID的回调方法:
1
2
3
4
5
6import BigWorld
def onComplete( entity ):
if entity is not None:
print "entity successfully created"
else:
print "entity was not created"使用有效的databaseID调用createBaseFromDBID:
1
BigWorld.createBaseFromDBID( "someEntity", 376182, onComplete )
BaseApp中显示的结果:
1
entity successfully created
使用无效的databaseID调用createBaseFromDBID:
1
BigWorld.createBaseFromDBID( "someEntity", 10000000000, onComplete )
在BaseApp中显示的结果:
1
entity was not created
9.3. Mapping BigWorld Properties Into SQL
在设计持久属性时,了解数据库层如何执行从BigWorld类型到SQL类型的映射是很有用的。此信息可用于性能调优或手动修改数据库。
9.3.1. Entity Tables
每个实体类型在数据库中都有一个主实体表,以及零个或多个子表。
实体类型的主表名为tbl_
除了ARRAY和TUPLE属性外,每个实体的数据都存储为实体类型的主表中的一行。
9.3.2. The databaseID property
一个实体的databaseID属性存储在主表的id列中——这就是为什么没有持久属性的实体仍然有一个主实体表。
9.3.3. Simple Data Types
具有简单数据类型的属性被映射到具有可容纳的类型的单个SQL列(名为sm_
下表描述了每个BigWorld简单数据类型,以及它映射到的MySQL类型:
表格将简单的BigWorld数据类型映射到SQL。
BigWorld data type | 映射到MySQL类型 |
---|---|
INT8 | TINYINT |
UINT8 | TINYINT UNSIGNED |
INT16 | SMALLINT |
UINT16 | SMALLINT UNSIGNED |
INT32 | INT |
UINT32 | INT UNSIGNED |
INT64 | BIGINT |
UINT64 | BIGINT UNSIGNED |
FLOAT32 | FLOAT |
FLOAT64 | DOUBLE |
9.3.4. VECTOR Data Types
具有向量类型的属性被映射到MySQL类型FLOAT的适当列数‐命名为vm_<index>_
表格将BigWorld VECTOR数据类型映射到MySQL。
BigWorld data type | #of columns | 映射到MySQL类型 |
---|---|---|
VECTOR2 | 2 | FLOAT |
VECTOR3 | 3 | FLOAT |
VECTOR4 | 4 | FLOAT |
9.3.5. STRING, UNICODE_STRING, BLOB, and PYTHON Data Types
STRING、UNICODE_STRING、BLOB和PYTHON类型的属性将被映射到列sm_
下面的列表总结了STRING、UNICODE_STRING、BLOB和PYTHON数据类型的映射:
PYTHON
- DatabaseLength < 256 ‐ TINYBLOB
- DatabaseLength >= 256 and < 65536 ‐ BLOB
- DatabaseLength >= 65536 and < 16777215 ‐ MEDIUMBLOB
STRING
- DatabaseLength < 256 ‐ VARBINARY
- DatabaseLength >= 256 and < 65536 ‐ BLOB
- DatabaseLength >= 65536 and < 16777215 ‐ MEDIUMBLOB
- DatabaseLength >= 16777216 ‐ LONGBLOB
UNICODE_STRING
UNICODE_STRING类型映射到下面列出的MySQL字符串类型。用于在数据库中存储这些字符串的字符编码由bw.xml选项dbMgr/unicodeString/characterSet的值决定。
- (DatabaseLength x 3) < 256 ‐ VARCHAR
- (DatabaseLength x 3) >= 256 and < 65536 ‐ TEXT
- (DatabaseLength x 3) >= 65536 and < 16777215 ‐ MEDIUMTEXT
- DatabaseLength >= 16777216 ‐ LONGTEXT
<DatabaseLength>的定义如下所示:
1 | <root> |
9.3.6. PATROL_PATH and UDO_REF Data Types
PATROL_PATH类型已被废弃,以支持使用用户数据对象,并且应该避免使用,因为它们将在未来的版本中被删除。用户数据对象将替换旧系统的站点节点。
具有UDO_REF类型的属性被映射到一个二进制类型的列,名为sm_<property_name>列宽度为16字节,对应于标识巡逻路径或用户的128位GUID数据对象类型。
128位GUID作为四组32位无符号整数存储在列中。每个整数都是小端顺序的。例如,GUID为00112233.44556677.8899AABB.CCDDEEFF,则列中的字节值将为3322110077665544BBAA9988FFEEDDCC。
9.3.7. ARRAYs and TUPLEs
每个ARRAY或TUPLE属性都映射到一个SQL表,称为实体主表的子表,命名为
<parent_table_name>是实体类型的主表的名称,除非数组或元组嵌套在另一个数组或元组属性中,在这种情况下
ARRAY或TUPLE子表有一个parentID列,用于存储与数据关联的父表中的行id。子表还将有一个id列来维护元素的顺序,并在有子表的子表的情况下提供行标识符。
子表的其他列将由ARRAY或TUPLE的元素类型(例如,一个ARRAY <of> INT8 </of>将产生一个额外的TINYINT类型的列)。大多数BigWorld类型只需要一个额外的列,该列将被称为sm_value。
9.3.7.1. Storing ARRAYs and TUPLEs as a BLOB
可以将每个ARRAY或TUPLE配置为在MEDIUMBLOB列中以内部二进制格式存储其数据,而不是将ARRAY和TUPLE数据存储在单独的表中。该行为由<persistAsBlob>选项控制:
1 | <root> |
<persistAsBlob>默认为假。
将数据存储为blob可以显著提高数据库性能,特别是对于深度嵌套的数组。但是,二进制数据是BigWorld的内部格式,不应该直接使用修改SQL语句。只能通过将实体加载到BigWorld、在Python中修改数据,然后将实体写回数据库来修改数据。
9.3.7.2. The <DatabaseLength> Attribute
如果数组的元素类型是STRING、BLOB或PYTHON,则ARRAY或TUPLE属性的<DatabaseLength>属性应用于该数组的元素类型。
其他类型要么不使用<DatabaseLength>修饰符,要么像FIXED_DICT的情况一样,使用自己的方法指定<DatabaseLength>。
9.3.8. FIXED_DICTs
如果实体类型包含FIXED_DICT属性,那么该属性的字段就会映射到数据库,就像它们包含实体的属性一样。
FIXED_DICT列比非FIXED_DICT列有更详细的名称:
sm_*
_ * 如果FIXED_DICT属性包含一个ARRAY或TUPLE字段,那么相应的子表的名称会更详细:
如果FIXED_DICT类型用作ARRAY或TUPLE的元素,则字段被映射到ARRAY或TUPLE子表的列中。列将命名为sm_<字段名>。
如果FIXED_DICT属性将<AllowNone>属性设置为true,那么将向表中添加一个名为fm_
<DatabaseLength>属性应该在FIXED_DICT属性的字段级别指定——在属性级别指定的属性被忽略。
9.3.9. USER_TYPEs
如果您有一个USER_TYPE数据类型,那么您可以指定它应该如何映射到SQL。
为了提供这种映射,需要在USER_TYPE中实现名为bindSectionToDB的方法。此方法接收一个对象作为其参数,用于声明数据绑定。例如,对于一个由TestUserType类型的实例实现的USER_TYPE:
1 | ... |
bindSectionToDB接收到的用于执行类型映射的对象(上例中的绑定器)提供了以下方法:
bind(property,type,databaseLength)
将数据区中的属性(基于名称)绑定到当前SQL表中的一个或多个字段,并创建一个名为sm_
的列。 参数类型是一个字符串,对应于属性的XML定义的< type >字段。例如:
Data type XML type Simple <Type> INT </Type> INT ARRAY <Type> ARRAY <of> INT</of> </Type ARRAY<of> INT<of> Custom <Type> USER_TYPE <implementedBy>
module.instance </implementedBy> </Type>USER_TYPE <implementedBy>
module.instance </implementedBy>参数databaseLength是可选的(默认为255),它决定了STRING映射的大小和数据类型。
beginTable(property)
开始指定一个新的SQL表(称为
_ ),用于绑定Python复合对象,如列表、元组、字典。在beginTable调用之后,任何对方法bind的调用都将绑定到新表中的字段,直到endTable被调用。 通常,beginTable只用于绑定包含可变数量复合对象的Python复合对象,例如元组列表。对于简单的列表和元组,调用bind ‘ARRAY<of><simple_type></of>’作为类型就足够了。对于包含固定数量项的复合对象,将每个项作为父表中的一个单独字段绑定,而不是创建一个新表,这样更有效。
由beginTable创建的所有表都有一个附加字段parentID,用于将新表中的行与父表关联起来。
endTable()
完成由方法beginTable启动的新SQL表的规范。
完成后,将恢复父表的指定。
方法bindSectionToDB的实现必须与方法addToStream的实现匹配。绑定调用的顺序和参数类型必须与属性序列化的顺序匹配。
下表显示了addToStream实现和相应的bindSectionToDB实现:
addToStream实现 | 对应的bindSectionToDB实现 |
---|---|
stream += struct.pack( “b”, obj.intValue ) | binder.bind( “intValue”, “INT8” ) |
stream += struct.pack( “B”, obj.intValue ) | binder.bind( “intValue”, “UINT8” ) |
stream += struct.pack( “h”, obj.intValue ) | binder.bind( “intValue”, “INT16” ) |
stream += struct.pack( “H”, obj.intValue ) | binder.bind( “intValue”, “UINT16” ) |
stream += struct.pack( “i”, obj.intValue ) | binder.bind( “intValue”, “INT32” ) |
stream += struct.pack( “I”, obj.intValue ) | binder.bind( “intValue”, “UINT32” ) |
stream += struct.pack( “q”, obj.intValue ) | binder.bind( “intValue”, “INT64” ) |
stream += struct.pack( “Q”, obj.intValue ) | binder.bind( “intValue”, “UINT64” ) |
stream += struct.pack( “f”, obj.floatValue ) | binder.bind( “floatValue”, “FLOAT32”) |
stream += struct.pack( “b”, len(obj.stringValue) )+ stringValue | binder.bind( “stringValue”, “STRING”, 50) |
stream += struct.pack ( “i”, len(obj.listValue) ) for item in obj.listValue stream += struct.pack ( “f”, item ) |
binder.beginTable( “listValue” ) binder.bind( “value”, “FLOAT32” ) binder.endTable() or binder.bind( “listValue”, “ARRAY <of> FLOAT32 </of>”) |
9.3.9.1. Examples
- Mapping a simple user-defined data type
- Mapping a complex user-defined type
(之后再回来看)
9.4. Execute Arbitrary Commands on Database
BigWorld为开发人员提供了在底层数据库上执行任意命令的工具。通过使用BigWorld.executeRawDatabaseCommand可以执行自定义语句或命令,并访问不符合标准BigWorld数据库模式的数据。
每个数据库接口都可以解释数据(命令)并将其转换为预期的格式。例如,MySQL接口需要一条SQL语句,XML接口需要一条Python语句。
9.4.1. Execute Commands on SQL Database
当在SQL数据库上执行命令时,BigWorld.executeRawDatabaseCommand有如下签名:
1 | BigWorld.executeRawDatabaseCommand( sql_statement, sqlResultCallback ) |
sql_statement
要执行的SQL语句。例如:’SELECT * FROM tbl_Avatar’。
sqlResultCallback
使用SQL的结果调用的Python回调函数。
可以使用多条命令,以逗号隔开。
对于单结果集命令,以下参数被传递给回调:
resultSet (List of list of strings)
对于返回结果集(如SELECT)的SQL语句,这是一个行列表,每一行是一个字符串列表。
对于不返回结果集(如DELETE)的SQL语句,这是None。
affectedRows (Integer)
对于返回结果集(如SELECT)的SQL语句,这是None。
对于不返回结果集(如DELETE)的SQL语句,这是受影响的行数
error (String)
如果在执行SQL语句时出现错误,则这是一条错误消息。否则,这是None。
对于多结果集命令,将传递一个列表,其中包含每个元素的元组。这三个元素对应于上面的参数(resultSet、affectedRows和errors),对于每个返回的结果集,列表都包含这些元组中的一个。
9.4.2. Execute Commands on XML Database
在XML数据库上执行命令时,BigWorld.executeRawDatabaseCommand有如下签名:
1 | BigWorld.executeRawDatabaseCommand( python_statement, pythonResultCallback ) |
python_statement
要执行的Python表达式
pythonResultCallback
使用Python表达式的结果调用的Python回调函数。
XML数据库存储在一个名为BigWorld.dbRoot的全局数据部分中。数据段的结构由实体定义文件(<res>/scripts/entity_defs/<entity>.def)定义。
resultSet (List of list of strings)
Python表达式的输出,作为字符串。
字符串嵌入在两层列表中,因此resultSet[0][0]检索字符串。
affectedRows (Integer)
该参数将始终为None。
error (String)
如果在执行Python表达式时出现错误,则这是错误消息。否则,这是None。
下面的代码片段在XML数据库上执行一个命令:
请求一个名为“Fred”的角色的生命值等级:
1
2BigWorld.executeRawDatabaseCommand("[a[1]['health'].asInt for a in BigWorld.dbRoot.items() if a[0]=='Avatar' and a[1]['playerName'].asString == 'Fred']",healthCallback
)实现Python回调:
1
2
3
4
5def healthCallback( result, dummy, error ):
if (error):
print "Error:", error
return
print "Health:", result[0][0]如果角色的生命值是87,那么输出将是:
1
Health: [87]
9.5. Secondary Databases
辅助数据库是一个可选特性,通过将数据库写入分布到带有BaseApp进程的机器上,可以帮助减少主数据库的负载。
此处不做过多讨论,启用辅助数据库时持久实体数据流:
10. Character Sets and Encodings
在处理ASCII范围以外的字符时,可能需要将多字节值转换为定义良好的格式,以便网络传输和存储在文件或数据库中。服务器的所有区域和服务器工具默认使用UTF-8字符编码。
在讨论Unicode和字符编码时,我们使用一些常见的术语。
Unicode
表示任何语言文本(字符和符号)的标准。
每种语言的字符都由唯一的代码点表示。在讨论代码点时,为了清晰起见,通常会在代码点前加上U或U+。
Character encoding
在应用程序之间传输数据时表示多字节值的标准,例如3字节Unicode码点。
例如:UTF-8, Big5, GB18030, GB2312, KOI8-R
Encode
将Unicode码位(或一系列码位)转换为特定字符编码的过程。
Decode
将一个字节(或字节数组)从字符编码转换为一组Unicode码位的过程。
10.1. Python and Entity Properties
在每个Python解释器中,都有一个用于从字符串对象转换为unicode对象的默认编码。当前的默认编码可以通过在Python解释器中运行以下代码来查看:
1 | import sys |
在BigWorld FantasyDemo源代码中,默认编码由变量控制DEFAULT_ENCODING在文件fantasydemo/res/scripts/common/BWAutoImport.py中。默认情况下,该值被设置为utf-8,但是可以根据需要将其更改为任何有效的Python编码。
由于Python是用于与实体及其属性进行交互的脚本语言,所以理解实体属性的默认Python编码的含义是很重要的。这主要影响两种实体属性数据类型,STRING和UNICODE_STRING。
10.1.1. STRING
使用STRING数据类型的实体属性作为字节数组在网络上传输,不需要任何修改,并作为BLOB存储在MySQL数据库中。这种类型的属性被期望直接映射到Python字符串对象。
当将unicode字符串赋值给string属性时,程序员必须显式地encode()该字符串。例如,假设默认编码为UTF-8:
1 | u"\u4e04".encode() self.string_property = |
由于STRING属性以二进制BLOB的形式存储在数据库中,因此可以使用此方法使用任何字符编码,因为程序员需要确保对字符串的所有Python脚本引用都使用相同的编码。
10.1.2. UNICODE_STRING
使用UNICODE_STRING数据类型的实体属性应该是一个Python unicode类型对象,可以根据需要调用它们的encode()和decode()方法,分别从Python字符串对象转换为Python字符串对象。
为了在网络周围传输UNICODE_STRING属性,它们被BigWorld引擎编码到UTF-8,然后在解流后解码到Python单节点对象。这是由bigworld/src/lib/entitydef/data_types中的UnicodeStringDataType类执行的。
UNICODE_STRING属性的MySQL存储与常规的字符串对象略有不同。这些属性会导致数据库中的TEXT列或varchar列,在每个表和列上都有一个特定的字符集编码。
10.2. DBMgr and Encodings
在考虑DBMgr和MySQL对字符编码的使用时,我们必须清楚使用字符集的所有区域。
10.2.1. UNICODE_STRING storage
数据类型为UNICODE_STRING的实体属性存储在MySQL数据库中为TEXT或VARCHAR列取决于实体定义文件中是否指定了<DatabaseLength>。
为了在MySQL中更有效地存储数据,可以更改的存储类型UNICODE_STRING属性列使用dbMgr/unicodeString/characterSet3 bw.xml选项。修改此值的效果可以通过使用示例来最好地看到。
使用中文字符3 (unicode码位U+4E09),我们可以从以下Python代码中看到,该字符在GB23124字符集中的字节表示比在UTF-8中更小。
1 | print repr( three ) |
10.2.1.1. Storing invalid characters
因为可以修改MySQL中存储的UNICODE_STRING属性的字符集,所以理解MySQL如何处理无法编码到列字符集的数据的情况是很重要的。
为了说明这个例子,我们将从一个简单的Python示例开始。如果我们试图将()代码点U+4E04编码为ASCII字符编码,将引发如下异常:
1 | >>> print u"\u4E04".encode( "ascii" ) |
不幸的是,这种行为没有在MySQL中复制,而是会无声地失败并插入?用字符代替无效的字符。由于这个失败是无声的,所以有可能通过有一个dbMgr/unicodeString/characterSet,它不能完全覆盖可能提供给MySQL的值的范围,因为会不知不觉地破坏数据库中的数据。这也是我们建议您将存储类型保留为UTF-8的原因之一,除非是绝对需要的。
10.2.2. Sorting search results
由于每种语言都有自己的关于一组值排序顺序的约定,MySQL也提供了在查询数据库时修改搜索结果行为的能力。用于定义排序顺序的规则称为排序规则。
MySQL中可用的每个字符集都有一个或多个排序规则。例如,MySQL中的UTF-8字符集有21种排序规则,可以通过运行以下命令看到:
1 | mysql> SHOW COLLATION LIKE 'utf8_%'; |
这与您可能在BigWorld实体数据库上执行的自定义搜索结果有关,以及通过其<Identifier>属性查找实体而执行的内部服务器查找。
排序通常被称为以下其中之一:
- Case sensitive
- Case insensitive
- Binary
根据你的游戏需求,你可能希望用dbMgr/unicodeString/collation8 bw.xml选项修改默认的UNICODE_STRING排序规则。
11. Profiling
分析服务器可以采用多种形式,这取决于您所处的开发阶段以及您试图隔离的问题类型。下面的列表是您可能希望概述的不同领域的简要概述。
- Entities
- Python script
- Server Processes
- Client Communication
- Server Process Communication
11.1. Profiling Entities
由于实体是BigWorld引擎所有组件使用的主要游戏对象,所以确保你的游戏实体尽可能有效地执行是很重要的。这包括:
- 最小化持久化属性。
- 确保属性具有最小的适用数据类型(同时考虑长期扩展)。
- 确保属性具有分配的最适当的数据传播标志。
- 确保属性在适当的地方具有AoI。
目前对实体大小进行分析的最佳机制是使用WebConsole过滤观察者视图来收集关于每个实体类型的属性影响的信息,再加上一个同行评审系统,以确保多人了解属性类型的影响。
11.1.1. Persistent Properties
持久属性会导致CellApp、BaseApp和DBMgr之间的网络传输成本,以及在不使用辅助数据库时将负载放置到DBMgr上。通过最小化持久性属性的数量以及减少持久性属性的大小,您将在集群中获得长期的性能提升。
确定持久属性可以通过使用WebConsole过滤的监视页面来执行。
11.1.2. Property Data Types
选择可用于属性的尽可能小的数据类型将有助于降低集群中的网络负载。这包括所有属性,无论它们是否被持久化。
11.1.3. Property Data Propagation
通常,开发游戏时最简单的解决方案是将所有属性设置为CELL_PUBLIC或ALL_CLIENTS,因为它提供了属性的最大可视性。当在办公室的单机服务器上进行开发时,这种方法通常非常有效,因为单机服务器的负载不是很重,但是当执行负载测试和将游戏扩展到生产时,这可能会导致大问题。
理想情况下,在决定与属性一起使用的传播标志时,理想的最佳方法是使用面向对象的设计理念,即在对象(在本例中是实体)上调用方法,请求它为您执行工作,而不是您自己访问它的私有信息。
一个常见的例子是一个实体的运行状况或HP。该值可能由于随着时间的推移而损坏或恢复健康状况而不断变化,并且通常只与属性关联的实体直接相关。虽然其他实体可能对这个属性感兴趣,但他们只是短暂感兴趣,比如当他们在一起战斗时,或者实体已经组成了一组玩家。在这种情况下,将传播标志作为CELL_PRIVATE是有意义的,可以根据方法调用的需要请求。这减少了每次更新运行状况属性时通过广播所产生的网络流量,但仍然允许在需要时访问该属性。
11.2. Python Game Script
1 | control_cluster.py pyprofile |
分析Python脚本通常是由于在脚本中识别出导致服务器进程问题的特定瓶颈而发生的,例如长时间滴答导致通过SIGQUIT信号终止进程。
剖析服务器端脚本使用pyprofile命令与control_cluster.py脚本一起执行。该命令只适用于运行Python脚本的服务器组件,如CellApp, BaseApp和DBMgr
11.2.1. Understanding the output
PyProfile将以内部时间和累计时间两种顺序输出查询的每个进程的结果。这两个结果集的输出格式是相同的。该概要文件旨在帮助识别游戏实现的最耗时的Python脚本方法。
1 | 6681 function calls in 0.106 CPU seconds |
生成这些报告的hotshot Python模块在引用相同信息时使用两个不同的名称。内部时间报告对应于下表中列出的总时间列以及其他列说明。
Column Name | Description |
---|---|
ncalls | 此函数的调用总数。 |
tottime | 仅在当前方法内部执行代码所花费的时间(以秒为单位)。从此方法调用的方法不包括在内。 |
percall | percall列直接引用它们左边的列,并表示它们所引用的配置文件时间的每个调用的平均时间。 |
cumtime | 此方法执行所花费的时间(以秒为单位),包括从该方法调用的任何其他方法的执行时间。 |
filename:lineno(function) | 包含该配置文件所引用的函数的文件名和行数。 |
1 | $ control_cluster.py help pyprofile |
11.2.2. Increasing Memory Usage / Entity Count
BigWorld中已禁用了自动垃圾收集,以避免Python引擎以意想不到的间隔使用大量CPU时间。为了允许删除Python对象,服务器进程只会在对象的引用计数达到0时删除对象。这种方法的副作用是不会删除具有循环引用的对象或被具有循环引用的对象引用的对象。
11.3. Profiling Server Processes (C++ Code)
当您的服务器出现负载峰值,而负载峰值不容易归因于某个特定的原因时,分析c++服务器代码有助于缩小花费时间最多的区域。BaseApp和CellApp都支持使用control_cluster.py命令cprofile。
(目前不详细研究)
11.4. Client Communication
1 | control_cluster.py eventprofile |
服务器将非易失性数据和易失性数据都发送给客户机。易失性数据是位置和姿态更新的形式,而非易失性数据包括实体的属性更新和对该实体的客户端方法调用,发送给任何可以看到该实体的玩家。通过分析非易失性数据,可以了解哪些方法和属性更新的吞吐量特别高,从而有效地优化实体脚本的网络影响。
(目前不详细研究)
11.5. Server Communication
1 | control_cluster.py mercuryprofile |
服务器通信主要通过Mercury接口进行。这些可以在监视树路径nub/interfacesByName下的服务器进程中看到。
mercuryprofile的输出提供了一个结果压缩表,可以在运行配置文件时使用选项根据需要对其进行排序。下面提供了一个CellApp实例的输出示例。
1 | cellapp01 - Internal Nub |
(目前不详细研究)
12. Proxies and Players
12.1. Proxies
在BaseApp上的BigWorld.Proxy拓展了BigWorld.Base,为了支持受玩家控制的实体。
通过从BigWorld.proxy派生出一个实体。你可以在服务器上实现玩家角色、他们的帐户和任何其他相关的玩家控制的对象。
来自BigWorld的实体。每当客户端登录到服务器时,都会从数据库创建代理。关于BigWorld如何决定加载哪个代理的详细信息。以这种方式创建的具有名为password属性的代理,将该属性的值设置为登录密码。
一个BigWorld.Peoxy的实例既不需要单元实体,也不需要客户端实体。可以使用BigWorld.creeteBase方法创建代理实体,就像其他实体一样(详情见“Entity Instantiation on the BaseApp” )。
与其他基本实体一样,可以从数据库中保存和加载代理实体。最初,这些重新加载的代理实体将在没有附加客户端的情况下创建。现有的代理可以稍后将其客户端移交给重新加载的代理,在这种情况下,重新加载的代理将处理客户端连接。
要将客户机的控制权从一个代理传递给另一个代理,可以使用giveClientTo方法,如下面的示例:
1 | clientControlledProxy.giveClientTo( nonClientControlledProxy) |
每当客户端在代理之间移动,或者客户端所连接的代理的单元实体被销毁时,客户端会收到一个onEntitiesReset的调用,以清除其当前的世界知识。这将有效地中断所有游戏交流,并迫使客户端进行刷新。如果只有单元实体被销毁,那么客户机关于其代理的知识将被保留。
如果由于目标代理的问题导致giveClientTo没有成功,onGiveClientToFailure将在源代理上调用。
12.2. Witnesses
每当带有附加客户端的代理具有相应的单元实体时,一个名为witness的额外对象就会附加到单元实体。
该对象管理实体的AoI,并将更新发送给代理,代理将更新转发给客户端。
这些更新包含大量与游戏相关的信息,例如:
- 实体的位置更新。
- 实体属性更新。
- 方法调用。
- 空间数据更改。
- 实体进入和离开AoI的通知。
12.3. Entity Control
默认情况下,每个单元实体都被认为是由服务器控制的。当一个实体合并一个wintess对象时,它被认为是由附加到相应代理的客户端控制的。
但是,可以使用实体属性 .controlledby显式地分配和查询实体的控制权。此属性可以设置为None,以指示服务器控件,或者设置为BaseEntityMailBox,以指示由连接到该代理的客户端进行的控制。
在这种情况下,控制意味着对实体的位置和方向的所有权和责任。客户(和代理)被告知他们可以控制的实体集的更改。代理可以通过它们的属性区读取这个集合。
12.4. Physics Correction
当实体由客户端控制时,将属性Entity.topSpeed设置为大于零的值可以启用物理检查。默认情况下,top速度允许在所有3轴上进行物理检查,但这可能并不总是合适的。例如,如果你的游戏环境的重力使y轴加速,导致最高速度超过允许的最大X/Z轴最高速度。为了适应这一点,有一个名为Entity.topSpeedY的次要属性,当设置为值大于零时优先。只有当topSpeedY和top都大于0时才使用topSpeedY。
通过以下方式验证实体移动:
Speed
Geometry of the scene
Custom physics validator
12.4.1. Avoiding Y-axis rubber-banding.
由于物理验证发生的方式,如果超过了最高速度,服务器将强制更新客户端最后一个已知有效位置的位置。然而,这有一个不幸的副作用,即产生一个实体,如果在y轴上超过了最高速度,它就不会掉落。将topSpeedY设置为高于topSpeed在这种情况下会有所帮助,但最终,由于长时间下落时重力的加速度,y速度将大于topSpeedY。
为了解决这个问题,建议编写一个自定义物理验证器,同时为topSpeedY设置一个大的值。然后,自定义物理验证器可以执行自己的验证,并在返回false之前用递减的y轴位置更新实体位置,然后将更新的位置强制发送给客户端。
13. Entities and the Universe
BigWorld中的实体包含在游戏世界中。宇宙由空间组成,空间由单元组成。
每个空格可以包含:
- 空间数据,用于整个空间必须可用的信息。
- 几何定义实体可以移动的位置。
- 一天中的时间,客户使用它来确定白天/夜晚的周期。
13.1. Multiple Spaces
BigWorld支持在一个宇宙中拥有多个独立的几何空间。每个空间都可以有一组不同的几何体映射到其中,以及一组不同的实体存在于其中。每个CellApp可以处理不同空间中的多个细胞。
让管理变得更简单的一种常见设计技巧是创建实体空间(通常根据空间的用途命名,如任务或任务)。这样的实体将在一个新空间中创建,然后负责配置该空间以便开始游戏。然后玩家就可以传送到新的空间去探索它,玩他们的任务等等。
Base script
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Space( BigWorld.Base ):
def __init__( self ):
# create our cell entity in a new space. self.onGetCell() will be
# called when the cell entity is created.
self.createInNewSpace()
def onGetCell( self ):
# create any predefined entities in the space
# may want to use ResMgr to load details from an XML file
# we want to make sure that each entity's createCellEntity()
# method is passed the appropriate cell mailbox (this entity's
# cell mailbox) so that it is created in the correct space
# for example:
BigWorld.createBase( "Monster", arguments, createOnCell=self.cell )Example file <res>/scripts/base/Space.py
Cell script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Space( BigWorld.Entity ):
def __init__( self ):
# Register our mailbox for getting the callback when the space
# geometry finishes loading. You can choose any arbitrary string
# as the key so long you can find this entry again.
BigWorld.cellAppData[ 'SpaceLoader:' + str(self.spaceID) ] = self
# Add the geometry mapping. This maps the set of .chunk files we
# want into the space. BWPersonality.onAllSpaceGeometryLoaded will
# be called when BigWorld finished loading the geometry.
BigWorld.addSpaceGeometryMapping( self.spaceID, None,
"geometry/path" )
def onGeometryLoaded( self ):
# we can now also teleport in any additional entities that already
# existed in the world (we'd probably store a mailbox somewhere in
# the construction sequence to make this possible)
# see the cell entity teleport() method for details
playerMB.teleport( self, position, direction )Example file <res>/scripts/cell/Space.py
Cell personality script
1
2
3
4
5
6
7def onAllSpaceGeometryLoaded( spaceID, isBootstrap, lastPath ):
if (isBootstrap):
# Find the registered loader and tell it to load the entities into
# the space.
loaderKey = 'SpaceLoader:' + str(spaceID)
if BigWorld.cellAppData.has_key( loaderKey ):
BigWorld.cellAppData[ loaderKey ].onGeometryLoaded();Example file <res>/scripts/cell/BWPersonality.py
为了创建这个实体,下面的代码应该是这样写的:
1 | newSpace = BigWorld.createBaseAnywhere( "Space", ... ) |
注意,这段代码中有四个关键步骤:
- 在一个新空间中创建单元格实体(在__init__方法中)。
- 创建单元格实体时,在空间包含的任何几何图形中进行映射。
- 创建世界中需要存在的任何实体。
- 当空间几何体被加载时,将任何需要的玩家带到空间中。
很可能会有针对所需空间风格的管理代码(这些代码很大程度上取决于你的游戏需求)。根据实体实例化需求,还可以在单元和基础之间执行各种步骤。
当一个空间中的所有实体被移除时,这个空间将被摧毁。或者,空间中的任何实体都可以调用entity . destroyspace方法来销毁该实体所在的当前空间。空间中的每个实体在实际销毁之前都会调用它的onSpaceGone方法。
13.1.1. Spaces Pool
当你的游戏需要一个空间的多个实例(例如任务空间)时,在游戏启动时创建一个可重复使用的空间实例池是有利的。
这一机制消除了之后需要根据需求加载区块的需求,当玩家请求进入一个空间时,将从池中选择一个实例。当玩家离开场地后,你可以将场地移回池中,以便以后使用。这种机制可以大大加快玩家在服务器上的游戏加载速度。
13.2. Navigation System
以下各小节描述导航系统的功能
13.2.1. Key Features
该系统的主要特点是:
- 室内和室外块导航
- 室内外无缝切换
- 导航多边图的动态加载
- 路径缓存,提高效率
13.2.2. Navpoly Data Format
世界被分成了许多块。每个chunk是一个凸包,由一个chunk ID字符串唯一标识,例如0000ffffo。
为了导航的目的,每个块被分解成一组凸多边形棱镜。这种棱镜被称为navpoly,并由单个块唯一的整数navpolyID标识。一组导航多边区域称为导航网格。
navpoly上的每条边都可以与相邻区域的边共享。这意味着这两个区域之间的移动是允许的。或者,一条边可以被标记为与不同的块相邻。
navpoly也有相应的高度。这是navpoly区域的最大高度,这样客户端可以从这个Y值做一个下降测试,并总是在navpoly的底部结束。
navpoly中的顶点按顺时针顺序定义,并将其XZ坐标存储在XY字段中。
第三个坐标用于存储这个顶点和下一个顶点之间形成的边的邻接信息。这第三个坐标的值要么是邻近navpoly的navpoly ID,要么是障碍物类型的编码。块边界上的边由相邻块ID的单独标记表示。在这种情况下,不使用第三个顶点坐标。
13.2.3. Script Interface
当一个实体想要导航到一个位置时,它使用Python实体。navigateStep脚本方法,如下所示:
1 | self.controllerID = self.navigateStep( destination, velocity, maxMoveDistance, |
- **destination ** - 目标位置。
- velocity ‐ 移动速度,单位是m/s。
- maxMoveDistance ‐ 在触发onMove回调之前移动的最大距离。
- maxSearchDistance ‐ 搜索路径的最大距离。
- faceMovement ‐ 实体是否应该面向运动方向。
- girth ‐ 实体可以通过的最小宽度。
- userData ‐ 传递给导航回调的整数值。
如果没有到目的地的有效路径,那么navigateStep将失败,并抛出一个脚本异常。否则,它将返回一个控制器ID。这是一个唯一的ID,可以用来取消移动请求,像这样:
1 | self.cancel( self.controllerID ) |
在路径上的每个waypoint,实体的onMove方法将被调用,controllerID和userData作为参数。继续路径,必须再次调用Entity.navigateStep以将控制器推进到下一步,否则实体将停止。如果没有再次调用navigateStep,或者再次调用navigateStep失败,所有与控制器相关的资源将被释放,不需要通过调用Entity.cancel来释放它们。
1 | def onMove( self, controllerID, userData ): |
如果onMove通知方法调用Entity.navigateStep超过100次,而没有以指定的速度移动一次移动所需的距离,则调用onMove失败通知方法。
1 | def onMoveFailure( self, controllerID, userData ): |
13.2.4. Navigate
当调用Entity.navigateStep脚本方法时,服务器将执行以下步骤:
- 从源位置解析ChunkID和WaypointID。
- 从目标位置解析ChunkID和WaypointID。
- 如果ChunkIDs不同,那么在块级别上执行一个图搜索。否则,如果waypointid不同,则在navpoly级别上执行一个图搜索。如果这两个测试都失败,则沿直线移动到指定位置。
13.2.5. Graph Searches
A搜索用于块图搜索和navpoly图搜索。ChunkState和WaypointState类都实现了A搜索所需的接口,并分别用于搜索块图和navpoly图。
对于块图搜索,距离是基于实体进入和退出块的近似点来计算的。
导航多边形图上的距离是根据通过导航多边形的实际路径计算的。
给定一个源位置在navpoly内部,目标位置在navpoly外部,算法如下:
- 找到从源到目的地的直线与多边形边界相交的点。
- 如果这个点在有邻接的边上,直接移动到交点。
- 否则,移动到有邻接关系的顶点,以便使此路径和期望路径之间的夹角最小化。
这是一种简单的方法,并不总是最优的,但在大多数情况下都能正常工作。
PathCache类被用作执行块和navpoly图搜索的包装器。它为每个实体缓存一条路径,并在该路径中存储当前跳索引。每次图搜索发生时,PathCache检查目标是否与缓存的目标相同。如果是,则返回路径中的下一个状态,并在适当的情况下增加跳索引。
13.2.6. Auto-Generation of Navpoly Regions
BigWorld提供了两种不同的工具来生成navpoly区域:NavGen(用于生成NavGen网格)和Offline Processor(用于生成Recast网格)。每个空间将被配置为使用这些生成器之一。两者都生成了可以被服务器端导航使用的导航网格,并在本节的后面会有更详细的解释。
这些工具根据区块文件中的地形和其他几何信息生成凸导航多边形棱镜(命名: <res>/spaces/<space>/
然后将结果写入二进制.cdata文件
文件bigworld/res/helpers/girths.xml说明了不同的实体配置文件,例如它们的大小和其他物理属性。这由周长值表示。可以指定多个周长,在这种情况下,将生成和维护多个导航网格。对于每个周长,可以设置不同的物理参数(例如,有一个用于实体高度的洪水填充参数)。对于类人实体来说,2.0米是不错的默认设置。
navmesh的生成方法由文件space.setting中的navmeshGenerator的值决定。。修改这个值不会自动导致块被脏,所以如果一个navmesh已经存在,那么下一代将不得不在覆盖模式下运行。
13.2.6.1. Configuring Girth Information for Navmesh Generation
13.2.6.2. Generating a NavGen Mesh: The NavGen Tool
13.2.6.3. Generating a Recast Mesh: The Offline Processor Tool
(以上几节此处不作过多介绍)
13.3. Time
我们已经看到实体是如何定义游戏不同部分的行为的。游戏玩法涉及到随着时间的推移而改变实体的状态,所以很重要的一点是要很好地理解BigWorld环境中如何管理时间。
在BigWorld中讨论时间时,思考不同类型的时间会有所帮助。这些类型将在下面的小节中讨论。
13.3.1. Real Time
“真实时间”只是现实世界时钟上的时间。实时时间被用作BigWorld中定义其他类型时间的基础。
13.3.2. Server Time
在服务器上,基于<res>/server/bw.xml中的<gameUpdateHertz>配置选项,游戏时间以离散的单位递增
服务器保持一个整数计数器,该计数器以这个速度递增,在服务器启动时其初始值为0。
计算服务器时间(以秒为单位)的公式如下:
1 | serverTime = serverTimestamp / gameUpdateHertz |
这大约是:
1 | serverTime ~= currentRealTime - serverStartRealTime |
每个客户端机器都会计算一个时间的同步版本,可以通过脚本方法BigWorld.serverTime获得。
13.3.3. Game Time
游戏时间是玩家在游戏世界中感知的时间。
大型在线持久化游戏通常会在虚拟世界中设置虚拟天数和虚拟月份。例如,你可能希望在Real Time中每小时运行一个游戏世界小时。为了支持这一点,BigWorld有一个标准的空间数据块,用于计算一天中的时间
该空间数据记录了以下数字:
- initialTimeOfDay - 游戏服务器启动时间。
- gameSecondsPerSecond - 转换系数,从服务器时间更新速率到实时。
它们一起被用来定义游戏时间:
1 | gameTimeOfDay = ( serverTime - initialTimeOfDay ) * gameSecondsPerSecond |
为了修改整个空间的游戏时间,可以使用CellApp的Python方法叫BigWorld.setSpaceTimeOfDay。该方法接受以下三个参数:
- spaceID - 应该被影响的空间的ID。
- initialTimeOfDay - 服务器启动时的一天时间。
- gameSecondsPerSecond - 每个实时秒中经过的游戏秒数。
要根据时间只修改客户端可视化,请参考client Python方法BigWorld.spaceTimeOfDay。
13.4. Initialisation: Personality script, eload, and runscript
默认情况下,当服务器启动时,会创建单个默认空间,不包含实体和几何图形。
为了让游戏变得有趣,脚本必须填充这个空间,并可能创造其他空间。当服务器运行时,您可能希望运行专业脚本来更改元素的属性。这些任务可以使用个性脚本和两个服务器端工具:加载和runscript来完成。
人格脚本可以包含一个Python函数,在每个BaseApp上运行一个可用的CellApp后立即执行。通过这种方式,可以保证您可以从脚本创建单元实体和基本实体。
要执行的脚本在文件<res>/server/bw.xml中指定,如下所示:
1 | <root> |
对应的文件(在上面的例子中是personalityscript.py)被放置在<res>/scripts/base目录中,将在适当的时间执行。
如果没有定义个性脚本,则使用默认文件名BWPersonality.py。
个性脚本中的onAppReady方法由BaseApp调用。方法接收一个布尔参数,其值定义如下:
- true - 如果BaseApp是服务器集群中第一个准备好的。
- false - 如果BaseApp不是集群中的第一个准备好的。
人格脚本可以调用任何BigWorld模块方法,建议执行以下操作:
- 通过从单元格实体调用addSpaceGeometryMapping函数向空间添加几何图形。
- 通过调用cell实体中的setSpaceTimeOfDay方法来初始化游戏时间。
- 初始化任何自定义空间数据。
- 通过创建实体来填充世界。
在执行个性脚本函数的过程中,BaseApp无法响应来自其他服务器组件的消息。耗时的个性脚本可能会使服务器集群的BaseApp超时。建议及时扩展实体创建,以便顺利、稳健地启动。
下面演示了这个示例,其中包含个性、基础和客户端脚本的代码
In the personality script:
1
2
3
4
5
6
7
8...
def onAppReady( bool isBootStrap ):
if isBootStrap:
BigWorld.createBase( "SpaceManager", {}, {} )
BigWorld.createBase( "EntityLoaderManager", {}, {} )
# every BaseApp needs an EntityLoader
BigWorld.createBase( "EntityLoader", {}, {} )
...Example personality script <res>/scripts/base/
.py On the BaseApp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49class SpaceManager( BigWorld.Base ):
def __init__( self ):
# create the cell entity in a new space
self.createInNewSpace( (0,0,0), (0,0,0), None )
class EntityLoaderManager( BigWorld.Base ):
def __init__( self ):
# register globally under a well-known name (ELM for example)
# so the EntityLoaders can register with me
self.registerGlobally( "ELM", onRegister )
# add a timer that calls back every 1 second.
# User data is 999 (only for identification purpose)
self.addTimer( 1, 1, 999 )
def onRegister( self, succeeded ):
# callback from registerGlobally(). Argument succeeded should always
# be True there is only one EntityLoaderManager in whole system
if not succeeded:
# should not be possible, try re-register
self.registerGlobally( "ELM", onRegister )
def registerLoader( self, entityLoader ):
# append the mailbox of an entityLoader into our list
# might have to verify it is not re-registered though
self.entityLoaderList.append( entityLoader )
def onTimer( self, timerId, userData ):
if userData == 999:
# distribute entity creation tasks to every registered
# EntityLoader in a load spreading manner
for i in range( len( self.entityLoaderList ) ):
# prepare the argument for entity creation
args = ...
self.entityLoaderList[i].createEntities( args )
if allJobFinished:
# remove the timer if not required any more
delTimer( timerId )
class EntityLoader( BigWorld.Base ):
def __init__( self ):
self.registerWithELM()
def registerWithELM():
if BigWorld.globalBases.has_key( "ELM" ):
# if EntityLoaderManager is available register with it now
elm = BigWorld.globalBases["ELM"]
elm.registerLoader( self )
else:
# otherwise wait a bit
self.addTimer( 1 )
def onTimer( self, timerId, userData ):
# retry registering
self.registerWithELM()
def createEntities( self, args ):
# create the entities according to the argumentsExample file <res>/scripts/base/SpaceManager.py
On the CellApp:
1
2
3
4
5class SpaceManager( BigWorld.Entity ):
def __init__( self ):
# add the geometry mapping
# this maps the set of .chunk files we want into the space
BigWorld.addSpaceGeometryMapping( self.spaceID, None, "geometry/path" )Example file <res>/scripts/cell/SpaceManager.py
13.5. Global Data
BigWorld提供了几种将全局数据分发到其组件的机制。这些机制中的大多数还在修改特定全局数据时提供回调,从而有效地将它们转化为全局事件分布机制。
与大多数编程环境一样,应该谨慎对待全局数据,因为它们给代码维护带来了挑战。在像BigWorld这样的分布式系统中,globals应该更少地使用,因为数据分布对性能的影响,以及竞争条件的风险。
13.5.1. globalData, baseAppData and cellAppData
BigWorld提供了三个在BigWorld组件中复制的Python字典。它们的复制范围不同:
- BigWorld.globalData - 在所有的BaseApps和CellApps上复制。
- BigWorld.baseAppData - 在所有BaseApps上复制。
- BigWorld.cellAppData - 在所有的CellApps上复制。
键和值必须是可pickle的Python对象。值类型可以是任何可pickle的Python对象。如果该值是一个BigWorld实体,那么它将被转换为该实体当前不驻留的组件上的邮箱。
当字典中的条目被修改时,会调用以下回调函数:
Global | Add or modified | Deleted |
---|---|---|
baseAppData | BWPersonality.onBaseAppData | BWPersonality.onDelBaseAppData |
cellAppData | BWPersonality.onCellAppData | BWPersonality.onDelCellAppData |
globalData | BWPersonalityonGlobalData | BWPersonalityonDelGlobalData |
通过操作全局数据调用的回调函数。
BigWorld只在项目被分配给不同的对象时检测到它的变化,而不是当对象的一部分发生变化时。例如:
1 | BigWorld.globalData[ "list" ] = [1, 2, 3] # addition is detected |
如果未检测到修改,则更改将不会复制到其他组件,从而导致本地副本和远程副本之间不一致。
每个值对象都是单独pickle的。这将导致该值是原始值的副本。例如:
1 | drinks = [ "juice", "wine" ] |
13.6. Space Data
13.7. Global Bases
14. XML Data File Access
14.1. ResMgr.DataSection
服务器组件脚本可以访问存储在XML文件中的自定义数据。它们通常用于存储游戏数据资源,例如从玩法表到配置数据的任何内容。数据存储在XML层次结构中,可以通过遍历每个XML文件中定义的树来访问。
14.2. Accessing Data
假设一个数据文件定义如下:
1 | <root> |
Example XML file ‐ <res>/scripts/data/Characters.xml
您可以通过使用ResMgr创建一个新的DataSection来访问这些数据。openSection方法。使用的路径参数是相对于资源路径的。下面的例子说明了这一点:
1 | ds = ResMgr.openSection( 'scripts/data/Characters.xml' ) |
14.2.1. Opening a Section Within an XML File
你可以通过在ResMgr.openSection的路径末尾添加section名来访问XML文件中的section:
1 | dsChild = ResMgr.openSection( 'scripts/data/Characters.xml/character' ) |
14.3. Data Types
可用的数据类型有:
Data type | Accessed by |
---|---|
64位浮点数 | .asDouble |
64位的整数 | .asInt64 |
BLOB数据 | .asBlob |
浮点数 | .asFloat |
整数 | .asInt |
矩阵 | .asMatrix |
XML节点的原始二进制表示形式 | .asBinary |
string | .asString |
Vector2 | .asVector2 |
Vector3 | .asVector3 |
Vector4 | .asVector4 |
Wide strings | .asWideString |
要了解更多细节,请参见客户端Python API的条目Class List→DataSection。
14.4. Writing Data
可以通过引用适当的.as<数据类型>属性写入属性,然后保存XML文件。
这个特性只用于服务器工具,你应该避免在游戏脚本中使用它。
需要注意的一个重要限制是,只能保存通过引用XML文档打开的DataSection。不可能直接保存由文件中子元素的路径检索的部分。
例如,下面的代码将不起作用:
1 | # this will not work, throws IOError |
另一方面,下面的代码将会工作:
1 | # this will work |
你也可以在每个数据段中添加或删除子元素:
1 | # get the document data section |
运行下面的代码摘录,并假设一个Characters.xml如“Accessing Data” 中所述,结果将是下面的文件:
1 | <root> |
Resulting <res>/scripts/data/Characters.xml
14.5. Performance Issues
访问磁盘上的XML文件可能会暂停游戏处理。这可以通过磁盘I/O和解析结果数据来实现。在读取和写入XML文件时都可能出现这种处理停顿,应该尽可能避免。
14.6. API Reference
ResMgr记录了数据部分的方法,可以在BaseAPpPythonAPI、CellAppPythonAPI和Client PythonAPI中找到。
15. External Services
从你的游戏脚本,你可能想访问外部服务,如计费或购物系统。当这样做时,重要的是不要阻塞I/O,因为暂停太长时间的进程可能会被其他服务器组件认为是死的。为了避免I/O阻塞,你可以这样做:
- 使用非阻塞方法并处理通知(反应器模式)。
- 从后台线程调用阻塞方法(线程池模式)。
(不作过多研究)
16. Fault Tolerance
16.1. CellApp Fault Tolerance
16.1.1. Overview
每个单元实体的完整副本定期备份到基本实体上。只有具有关联基本实体的单元实体是容错的。CellApp备份周期指定单元实体备份到基本实体的频率,并在bw.xml选项< CellApp /backupPeriod>中指定。
如果CellApp进程不可用,位于该进程上的单元上的实体将由其对应的基本实体恢复到其他CellApp。恢复的单元实体的单元数据状态与从单元实体到基本实体的最近备份所给出的状态相同。
16.1.2. Restoration process
CellApp恢复过程通常遵循以下步骤:
- CellApp进程不可用。
- 在现在不可用的CellApp进程上具有单元实体的基本实体将其相应的真实实体恢复到其他CellApp。
- 恢复的单元格实体调用了onRestore()回调函数。由于恢复的单元数据是从最后一次备份到基本单元实体的单元数据中获取的,因此此副本最多可以是备份周期的两倍。这个回调应该检查实体的属性是否处于一致状态。
- 对于播放器单元实体,它们对应的客户端播放器实体会调用onRestore()回调函数。
在单元实体上调用回调onRestore()来通知它正在恢复。
下面的代码片段说明了它在cell实体上的实现:
1 | class SomeEntity( BigWorld.Entity ): |
Example file <res>/scripts/cell/SomeEntity.py
16.1.3. Example

在上图中CellApp中有三个cell实体4156,5712和2114,其中4156和2114分别在BaseApp1,BaseApp2中有base实体,假如此时CellApp4崩溃,4156和5712将会从BaseApp中恢复上次备份的数据,并创建cell4156和cell5712而cell2114则不会被创建。
恢复的可能与当前丢失的cell实体不一致,cell实体的备份可以在多步骤事务的中间进行备份。脚本回调Entity.onRestore()可以用于检查未处理事务的状态,并且可以在脚本中决定回滚或继续它们。
16.2. BaseApp Fault Tolerance
有了BaseApp的容错功能,BaseApp会备份基本实体数据,并定期将所有基本实体的数据cell备份到其他BaseApp进程。
如果主BaseApp不可用,那么它的所有实体都将从备份进程中恢复。在这种情况下,BaseApp调用基实体或代理实体上的onRestore回调,其过程与CellApp恢复的过程类似。此回调应确保实体上的所有属性处于一致状态。
详情(Server Overview’s section Server Components → “BaseApp” → “Fault Tolerance”)
17. Disaster Recovery
BigWorld的容错能力确保了在单个进程丢失时,服务器将继续运行。服务器还提供了称为灾难恢复的第二级容错。服务器的状态可以定期写入数据库。如果整个服务器都出现故障,则可以使用此信息重新启动服务器。
这个归档的速率由配置选项在文件<res>/server/bw.xml的< baseApp / archivePeriod >和< cellAppMgr / archivePeriod >中指定。
CellAppMgr进程负责将空间数据和游戏时间写入数据库。
具有有效数据库条目的实体也会定期存档(通过基本实体上的非零databaseID表示)。要将实体写入数据库,从而启用其归档,请在基实体或单元实体上调用writeToDB()方法。
具有有效数据库条目的实体也会定期存档(通过基本实体上的非零databaseID表示)。要将实体写入数据库,从而启用其归档,请在基实体或单元实体上调用writeToDB()方法。
每次存档实体时,都会调用它的onWriteToDB()回调函数。
18. Controlled Startup and Shutdown
有时可能需要关闭服务器,然后以类似的状态重新启动。本章描述了该场景下脚本的相关细节。
18.1. Controlled Shutdown
控制停机过程如下:
LoginApp进程接收到USR1信号。
LoginApp进程立即关闭。
CellAppMgr收到一条消息来安排关闭(在游戏时间内)。
CellAppMgr向其他进程发送一条消息,通知它们何时计划关闭。
CellApp个性脚本的onAppShutingDown回调被调用。
这一步和下一步的个性脚本应该执行适当的完成任务,比如结束长时间的任务,如战斗或交易,通知玩家,停止新的长时间任务。
调用BaseApp个性脚本的onAppShuttingDown回调函数。
一旦执行了这些回调,将调用方法BigWorld.isShuttingDown返回True。
其他服务器进程(CellApps, BaseAppMgr, BaseApps, Backup BaseApps, DBMgr, Reviver)不会立即停止,而是执行任何完成的任务。
这个延迟可以通过使用配置选项< shutingdowndelay >在res/server/bw.xml文件中指定。
关闭游戏时间达到。
游戏停止运行,但进程没有关闭。
这意味着游戏时间不再增加,也没有勾选游戏对象。
当准备关闭时,CellAppMgr将游戏时间写入数据库。如果配置为archiiving, CellAppMgr也会将空间数据写入数据库。
此步骤与第11步并行进行。
每个BaseApp执行以下步骤:
接收一条消息以断开任何已连接的客户端。
在断开客户端连接之前调用带有0参数的onAppShutDown回调函数。
对于每个断开连接的客户机,将调用代理的回调onClientDeath。
在将每个带有数据库条目的实体写入数据库之前,调用参数为1的onAppShutDown回调函数
调用参数为2的onAppShutDown回调函数。
所有服务器进程关闭。
18.2. Controlled Startup
当启动时,DBMgr最初会等待,直到所有组件都准备好。BaseApp和CellApp进程的最小数目可以在bw.xml中通过 <desiredBaseApps> 和<desiredCellApps>。一旦准备好,DBMgr将空间数据加载回系统(如果它是由CellAppMgr归档的)。
然后通过创建基本实体将自动加载实体加载到系统中。
如果需要,则由脚本创建单元实体。在启动期间创建单元实体通常与在其他时间创建单元实体不同。通常是Base方法。使用单元实体邮箱调用createCellEntity,以指示实体的空间。但是在启动期间,实体的空间ID会被恢复并设置在base.cellData地图。基本实体脚本可以通过调用不带参数的base.createCellEntity方法。
一旦服务器准备好开始运行,来自人格脚本的onAppReady回调将在BaseApps和CellApps上调用。
19. Transactions and Handling Fault Tolerance and Disaster Recovery
19.1. Transaction logic
我们在这里给出了一个交易事务的例子,即在两个玩家实体之间传送一个道具。
19.2. Fault Tolerance Behaviour
19.2.1. CellApp Fault Tolerance
如果Alice和/或Bob的单元实体所在的CellApp退出,所有具有基本实体的单元实体将被恢复到另一个CellApp。在这个示例场景和描述的事务中,没有太多关于恢复单元实体的行为的问题,因为事务只涉及到BaseApps。
然而,假设库存系统的实现是这样的,玩家单元实体需要关于道具的知识,例如,玩家手中持有什么道具,这将需要OTHER_CLIENTS或ALL_CLIENTS单元实体属性,以便其他玩家能够查看玩家所持有的道具。如果单元实体在最后一次备份到基本实体时从其单元实体数据的旧版本恢复,则单元实体状态可能与基本实体状态不一致。
例如,如果Alice被恢复到另一个CellApp,她的单元实体可以检查她的基础实体是否仍然拥有她持有的物品,如果不是,她的单元实体应该删除该物品。
被恢复的单元实体不会调用它们的_init_()方法,相反,在使用来自基本实体的单元备份数据恢复它们之后,它们会调用它们的onRestore()方法,并且可以在此方法中执行诸如此类的检查,以确保状态与基本实体状态一致。
19.2.2. BaseApp Fault Tolerance
如果Alice和/或Bob的base实体所在的BaseApp退出,如果这些base实体存在,它们将被恢复到其他BaseApp(如果只有一个BaseApp,它们无法恢复)。
与单元实体一样,恢复的基本实体没有_init_()调用,相反,当从它们最近的基本实体备份数据恢复它们时,它们有onRestore()调用。这是检查未完成事务的好地方。
例如,如果包含Alice的BaseApp退出,并且Alice被恢复到另一个BaseApp上(也许Bob也被恢复到另一个BaseApp上,它可能是一个不同的BaseApp),那么我们需要重播可能正在进行的任何事务。
对于Alice事务列表中的每个事务条目,实体需要根据它所处的状态重放每个事务。
例如,如果它处于BEGIN状态,则通过查找Bob的基本实体,并继续从步骤3恢复事务。
如果我们是Bob,我们可能有处于REMOVE状态的事务,因此我们从步骤继续事务6,我们告诉Alice(或者交易玩家的名字指的是谁),他们应该在自己的一端完成交易。
19.3. Disaster Recovery Behaviour
当我们启动服务器并从数据库中恢复时,基本实体将被恢复,并且每个实体都将对它们调用_init_()。对于恢复的基本实体,变量BigWorld.hasStarted将为False,因此我们可以进行与BaseApp容错部分类似的检查。基本实体也要负责重新创建单元实体,通常是通过createCellEntity().。空间ID在它被写入数据库时与实体一起存档,它出现在基实体的cellData字典中。
20. Implementing Common Systems
本章讨论了程序员在执行游戏系统时应该记住的一些一般性问题,并提供了一些mmog中常用系统的设计和实现示例。
20.1. General Scalability
一般来说,如果玩家和实体密度保持不变,服务器处理负载、内网带宽和外网带宽与玩家数量呈线性关系。随着密度的增加,会有一个小的额外成本。
容量可通过以下方式增加:
- 增加更多的BaseApps以获得更多的外部连接点和连接处理能力
- 增加更多的CellApps以获得更多的空间处理能力
- 添加更多BaseApps和更多CellApps的组合,以增加游戏脚本处理能力
CellAppMgr、BaseAppMgr和DBMgr是单一实例,理论上是扩展瓶颈。CellAppMgr和BaseAppMgr只关心管理CellApp和基础应用程序,并且负载非常低。它们可以扩展到处理成千上万的基本应用程序和单元应用程序。虽然BigWorld的设计没有大量使用数据库,但缩放的主要问题是DBMgr。下面将在BigWorld数据库可伸缩性中讨论。
20.2. Internal inter-component communication
为实体提供充分服务所需的BaseApps和CellApps的数量通常应该随实体的数量线性增长。实体之间的大多数通信都是与邻近的实体进行的。这是通过将这些实体尽可能多地放在CellApps上来处理的。其他通信包括使用远程方法调用的点对点通信。这里的主要问题是尽量减少需要在全球范围内查找实体的情况。
用于写入实体状态的DBMgr功能分布在每个baseapp的辅助数据库中,并在实体退役时进行整合。
解决游戏脚本瓶颈的一般策略是尽可能避免全局游戏系统,例如让单一实体控制游戏的某些操作,例如交易。通常情况下,这些瓶颈可以通过重组游戏脚本和使用分布式对象方法来实现全局子系统来避免,而不是将请求处理委托给单个实体。下面给出了一个这样的例子(请参阅下面基于aoi的交易)。
20.3. Player AoI Updates
对玩家AoI中某些实体的更新就会被传播到玩家的客户端(默认情况下,游戏更新赫兹是10Hz)。发送到播放器客户端的更新数据量被限制为一个下游比特率(默认情况下,发送到第二个客户端的比特率是20kbps)。
这些更新包括属性更改和方法调用。每个单元实体都保存这些更改的历史,对于玩家AoI中的每个实体,玩家都会定期逐步更新该实体。实体的位置和方向数据经过特别处理,以便只将这些属性的最新值(所谓的volatie属性)发送给客户,而不是属性的完整历史。
在内部,AoI中的实体位于优先级队列中。玩家的实体在AoI中的优先级决定了该实体的下一次更新需要多长时间。
然而,非常高的实体密度可能会导致问题,因为这会导致对玩家客户端的每次定期更新都被过量的实体事件数据溢出。回想一下,下游带宽的数量是一个可配置的常数。由于在玩家的AoI中实体的变化事件的优先级,如果有更多的实体更靠近玩家,这可能会导致更远的实体的更新被饥饿。增加下游带宽可以改善这种情况下,但最终,通常是客户端成为限制因素。每个实体的处理成本,例如:
- 处理每个实体的位置和方向的通知
- 处理每个实体属性更改的通知
- 处理每个实体的方法调用的通知
- 将物理规则应用于每个实体
- 每个实体的渲染
玩家能够理解的信息量也是有限的。当附近有大量的实体时,远处的实体需要的信息就会更少。
通过良好的游戏设计,可以避免可能会对最终用户体验产生负面影响的极端实体密度。
20.4. BigWorld Database Scalability
当实体签出数据库时,它们被分配给负载最少的BaseApp。一旦实体被加载到一个BaseApp上,它们通常不会从这个BaseApp上迁移出去,除非这个BaseApp进程终止,在这种情况下,它们会在系统中的其他BaseApp上恢复。
对于驻留在它上面的每个实体,BaseApp负责收集该实体在签出生命周期内的所有显式脚本写入(从对BigWorld.writeToDB()的调用),以及对该实体的定期备份。这些写操作是在存储在BaseApp机器上的辅助数据库上执行的。
20.5. Player Look-up
20.5.1. Requirements
每个玩家必须能够通过名字查询另一个玩家的状态:
- 不管他们是否已经登录。
- 如果他们已经登录,就获取他们的玩家邮箱
20.5.2. Design
使用BigWorld.lookUpBaseByName()会导致对DBMgr的查询(这会导致对主数据库的读取),虽然对于许多场景来说足够了(并且在许多基于bigworld发布的游戏中都有效),但也引入了一个潜在的瓶颈。下面的讨论概述了一个玩家姓名到玩家邮箱的分布式映射设计,它有效地将这个负载转移到BaseApp游戏脚本,可以通过添加更多的BaseApp来扩展。
我们的想法是拥有多个包含玩家姓名到玩家邮箱的分布式映射的PlayerRegistry Base实体。这些PlayerRegistry实体没有地理空间表示,它们只作为系统服务存在,因此不会产生与AoI更新相关的负载。
每个BaseApp都有一个对应的PlayerRegistry实体——这将PlayerRegistry实体扩展开来,防止BaseApp失败。每个BaseApp拥有一个以上的PlayerRegistry实体不会增加任何额外的冗余好处。
PlayerRegistry实体实例在全局注册自己。全局注册的基的邮箱在全局基映射中的字符串键下注册,该映射在每个baseapp之间同步。玩家名称根据已知的玩家注册表数量进行散列,并且一个特定的玩家注册表实例通过全局基础机制进行定位。
当玩家实体被创建时,他们通过将自己的名字散列到适当的PlayerRegistry实体将自己添加到分布式注册表中,并向PlayerRegistry实体注册自己的基本邮箱。注销时,他们联系同一PlayerRegistry通知它注销,这将导致删除该玩家姓名和该玩家邮箱之间的映射。当添加或删除一个新的PlayerRegistry时,我们可以实现一个重新平衡玩家注册表条目的方案,即在PlayerRegistry实体间重新平衡条目。
对于特定玩家名的查询是通过首先将玩家名散列到适当的PlayerRegistry中进行查找,然后通过远程方法查询多个PlayerRegistry实体中的一个,以及带有邮箱的回调远程方法。对玩家查找的请求是异步的,调用者实体实现一个回调方法,当查找完成时被回调。
为了容错,每个PlayerRegistry都需要一个持久的邮箱列表,这样,如果它之前所在的BaseApp失败,注册表就会连同PlayerRegistry实体一起恢复到另一个BaseApp。在这种情况下,它很可能会被恢复到另一个已经有自己的PlayerRegistry的BaseApp,所以应该重新平衡,然后销毁恢复的PlayerRegistry。
通过增加BaseApps的数量来处理查询,这个系统可以进行扩展。分层请求模式还可以用来避免大量全局注册的基础实体成为瓶颈。
20.6. Friends lists
20.6.1. Requirements
每个玩家都维护一个其他玩家的列表,他们可以用于以下目的:
- 联系朋友
- 给朋友发私人信息
- 存在更新
20.6.2. Design
假设友谊关系是对称的,所以如果A在B的朋友列表上,那么B在A的朋友列表上。一个朋友列表可以被实现为一个数组的FIXED_DICT,包含一个STRING名称属性,和播放器的邮箱(或None,如果离线),utit8布尔标志hasresponsiveness表明这个播放器已经响应了我们的请求,添加该播放器为朋友。
20.6.2.1. Adding new friends
让添加朋友的玩家被称为玩家A,而被添加到玩家A的列表中的朋友被称为玩家B。
玩家A检查玩家B是否还不在A的朋友列表中。玩家A使用玩家B的名称通过玩家查找机制查找B的状态和邮箱(如果在线)。
如果B不在线,那么我们的操作就失败了。可以实施适应这种情况的方案,但为了简单起见,这里不讨论。
如果玩家B是在线的,那么玩家A将玩家B添加到它的好友列表中,将已响应的标记设置为False,并使用Base.writeToDB()将自己写入数据库,在数据库写完成时注册一个回调。
如果写入失败,那么我们通过删除玩家B的FIXED_DICT元素来回滚好友列表,并中止这个过程,并通知玩家A的客户端系统错误。
否则,写入将成功完成,因此玩家A通过远程方法调用通知玩家B,将玩家A添加到玩家B的列表中,并传递玩家A的名称和邮箱。
玩家A会周期性地重新发送这些未完成的请求(通过好友列表中的hasresponses为False来表示),每隔3秒。每次重发邮件时都要查看邮箱,以防玩家B已经恢复到另一个BaseApp,或者玩家B已经注销和/或再次登录。如果玩家B在重试期间不在线,那么操作将失败,玩家a的客户端将被告知玩家B不在线。
通常情况下,玩家B不会已经有玩家A作为好友,所以玩家B通过为玩家A创建包含玩家A姓名和邮箱的FIXED_DICT元素来添加本地好友列表,并将hasresponses标志设置为True。用回调请求对数据库的写操作。
玩家B的好友列表中可能已经有玩家A的条目。如果玩家A和玩家B同时试图添加对方为好友,就会发生这种情况(在这种情况下,hasresponses将为False)。如果玩家B被恢复到另一个BaseApp,或者在等待写数据库的过程中被销毁并重新创建,或者写数据库花费了很长时间,玩家A重新发送了请求,在这些情况下,hasResponded将会是True。
如果hasresponses标志为True,那么它立即向玩家A发出操作成功的信号。如果hasresponses标志为False,那么它应该设置为True,并且在向玩家A发送成功信号之前将数据库写入并从数据库中回调。
通常情况下,写入成功,玩家B就会回调玩家A以表明请求成功。
在特殊情况下,写操作可能会失败。玩家B在好友列表中删除玩家A的FIXED_DICT条目,并回调玩家A以指示操作失败。
在这种情况下,玩家A应该尝试删除玩家B的FIXED_DICT元素,这应该通过将玩家A写入数据库来持久化。然而,玩家a有可能在第二次数据库写入失败,而之前的数据库写入成功,这使得玩家a的好友列表在数据库中不一致。有一些方法可以处理这种情况:
- 不要在玩家A的列表中删除玩家B的FIXED_DICT条目,而是让玩家A定期向玩家B重试请求,直到玩家B成功回应。
- 在玩家A写入数据库时,定期删除玩家B的FIXED_DICT条目。
这两种方法都假定数据库写失败是暂时现象。可能是由于BaseApp辅助数据库没有足够的磁盘空间,当系统管理员创建更多空间时就会清理磁盘空间。可以保留一个重试计数,在重试计数超过某个阈值后从朋友列表中删除FIXED_DICT条目,并且应该通知玩家A的客户端失败。
当玩家B成功回调时,玩家a将hasresponses标志设置为True。此时不需要写数据库,因为可以依赖定期的备份和归档系统来最终保存它。在系统重启或Player A被恢复到另一个BaseApp的情况下,定期重试hasresponder设置为False的FIXED_DICT条目将获得第二次成功的回调,并最终被写入。
对于所有玩家来说,添加好友列表并不是一种频繁的操作,玩家通常分布在可用的BaseApps中。
将玩家从好友列表中移除也可以采用类似的方法。
20.6.2.2. Private messages to friends
请参阅下面的聊天部分。一旦有了玩家邮箱,就可以使用一个简单的远程方法调用向他们发送聊天消息。
20.6.2.3. Presence information
存在通知可以简单地通过在每个玩家的朋友列表中调用一个方法来实现,表明他们已经登录或注销(这表示邮箱是无效的,应该在相应的FIXED_DICT在数组中设置为None)。
玩家状态通知(例如离开键盘)也可以以类似的方式完成。玩家基础实体会通知他们的客户端好友状态的任何变化,这样他们就可以更新好友列表的用户界面。
20.6.2.4. Cache of friend player mailboxes
当好友登录时,好友列表可以作为玩家邮箱的缓存,并且不需要使用通用的玩家查找机制来与他们的好友玩家实体通信。当朋友注销时,朋友邮箱被设置为“无”。
缓存不需要是持久性的,因此不会向数据库添加任何额外的处理成本。
20.6.2.5. Fault tolerance handling
当一个玩家被恢复时,一些朋友可能已经在线或离线(或离线,然后在线),因为该玩家和好友列表上次被备份。在恢复或初始化时,玩家实体应该对其好友列表中的所有玩家执行查找。它还应该在恢复时通知所有在线的朋友它的新邮箱。
20.7. Chat
- P2P chat
- AoI-based chat
- Channel-based chat(包括guid聊天,世界聊天)
20.7.1. P2P
20.7.1.1. Requirements
玩家需要能够向其他玩家发送信息。玩家是按名字命名的
20.7.1.2. Design
参见上文玩家查找部分。玩家间的聊天涉及以下内容:
- 需要获取目标播放器的邮箱。这可以通过以下方式之一实现:
- 由玩家单元实体提供,因为目标实体在玩家的AoI中
- 本地查找您的朋友列表邮箱缓存
- 使用上面描述的玩家查找机制查找他们的邮箱
- 使用聊天消息内容调用该邮箱上的聊天远程方法。
为了节省查找的远程方法成本,玩家邮箱可以作为非持久实体属性缓存在玩家实体上。例如,发送给非好友玩家的私人消息往往会导致对话,所以将玩家姓名映射到玩家邮箱的本地缓存将在每次发送进一步聊天消息时节省查找时间。
20.7.2. AoI-based broadcast chat
20.7.2.1. Requirements
玩家需要能够在他们邻近的空间附近向玩家广播信息。
20.7.2.2. Design
基于AoI的聊天可以通过广播远程方法调用实现给在他们的AoI中有说话播放器的所有播放器实体。这并不需要在脚本中遍历所有实体,并且在CellApp上得到了有效的实现。chat方法调用使用与任何其他广播方法调用相同的机制广播给客户实体,或者当ALL_CLIENTS或OTHER_CLIENTS属性改变时。
可以为聊天方法调用指定易失性距离约束,这样只有初始玩家的特定半径内的玩家才能收到方法调用消息。
20.7.3. Non-AoI-based broadcast chat
20.7.3.1. Requirements
非基于aoi的聊天通道是不需要在相同空间位置的实体的聊天通道。这可以用于公会范围的聊天和世界范围的聊天。
20.7.3.2. Design
非基于aoi的通道可以实现为ChatChannel实体,它包含连接到该聊天通道的播放机的播放机邮箱列表。
当玩家想要连接到一个频道,一个频道查找被执行为特定的ChatChannel实体。这可以通过与上述玩家查找机制类似的机制来实现。一旦找到通道的邮箱,播放器将其基本邮箱注册到ChatChannel实体,该实体将其添加到连接的播放器邮箱列表中。
一个连接的播放器通过一个带有该频道内容的远程方法调用广播到该频道。ChatChannel实体负责广播该消息到每个连接的播放器基础邮箱。
20.8. Mail
20.8.1. Requirements
每个玩家都必须有能力向其他玩家发送邮件。这封邮件包括一些文本和可选的游戏内项目。
20.8.2. Design
这里可以利用SMTP/IMAP邮件服务器的可伸缩性。请注意,这些游戏邮件服务器完全是游戏内部的——不允许公众访问(尽管这取决于游戏设计)。
每个玩家都有一个相关的电子邮件地址。BaseApps可以使用BigWorld注册的TCP套接字异步查询IMAP服务器,而不会阻塞游戏脚本。Python很好地支持通过套接字与IMAP通信(参见Non-Blocking Socket I/O Using Mercury)
道具可以使用特殊的附件或特殊的邮件标题赠送,这取决于所使用的道具系统。道具数据永远不会通过电子邮件直接发送,相反地,通过电子邮件赠送的道具将被托管,就像基于aoi的玩家道具交易一样。参见下面的库存和道具交易。
20.9. Inventory System
20.9.1. Requirements
在相同空间附近的玩家实体必须能够协商交易他们所拥有的道具。
每个玩家向对方提出报价,将他们所提供的物品放在托管中。一旦双方玩家都接受对方玩家的提议,交易就成功了,物品就可以交易了。如果一名玩家取消交易,所有提供的道具将返还给各自的玩家。
物品买卖交易不得造成物品重复或物品丢失。
20.10.2. Design
BigWorld可以随时提供玩家AoI中任何玩家实体的基本邮箱。否则,如果与玩家AoI之外的特定人物进行交易,就需要玩家查找。
托管(Escrow)实体是在交易的整个生命周期中创建的,并持有两个实体的邮箱。托管实体持久化到数据库。贸易包括两个阶段,谈判阶段和转移阶段。托管实体在最少加载的BaseApp上创建。
谈判阶段是一系列从玩家实体到托管实体的报价操作,然后每个报价都被转发给对立的玩家实体。
如果服务器在事务中间停止,托管(Escrow)实体有足够的持久信息来在恢复时取消自己,并将项目返回给他们拥有的玩家实体。
服务器上的玩家实体以第三方托管实体的远程方法请求的形式向其他玩家提供道具(以响应来自他们的玩家客户端的GUI交互)。在此过程中,他们将这些道具从库存转移到服务器上的玩家实体的一个特殊存放区域。玩家的客户端不能以任何其他目的进入这个保存区域,除非将道具从当前的供应中移除,这样就会将道具移回他们的库存中。
每次从玩家库存转移到交易持有区都会导致:
- 通过远程方法调用向托管实体提供的项目更改通知。
- 数据库写入托管实体。
- 从托管实体返回到原始播放器实体的确认远程方法调用。
- 从保存区域移除物品,并在数据库中写入玩家实体。
如果由于某种原因(临时的或其他原因),数据库写入失败,整个交易将被取消,道具将通过远程方法调用返回给玩家,玩家将通过远程方法调用返回到托管实体。当托管实体收到来自两个玩家的确认交易已经取消,它从数据库中删除自己。
每个玩家都可以向托管实体发出信号,表示愿意接受交易的现状。一旦托管实体收到双方的积极通知,它就会通过向玩家发送他们已经交易的道具数据,将道具所有权转移给相应的对手玩家。
- 托管实体将所有权转移给每个玩家他们相应的交易道具。
- 在收到物品后,每个玩家开始写入数据库。当这被确认为OK时,玩家实体通过调用托管实体来承认他们拥有道具。
- 托管实体等待两个确认返回,然后销毁自己并从数据库中删除自己。
总数据库写:每个报价2个,至少有2个报价。3为过渡阶段。
这说明,就磁盘写操作而言,交易可能是一种昂贵的操作。然而,所有的写操作都分布在相关的实体中,并且大多数会被写到辅助数据库中。当第三方托管实体被销毁时,只有一个数据库写操作会导致主数据库被使用,以便从持久存储中删除第三方托管实体。
请注意,交易事务中的每个参与实体不需要处于相同的进程中。这可以很好地扩展,因为可以有任意数量的BaseApps,玩家和托管实体将均匀分布在BaseApps中。回想一下,虽然CellApps有映射到它们所在空间的播放器分布,但BaseApps上的基本实体没有这种空间关系。
与每个托管实体的创建和销毁相关联的主数据库会有成本。这种设计可以通过合并托管操作来改进,以目标为预先存在的EscrowManager实体,而不是创建和销毁托管实体。一个类似的方案可以通过每个BaseApp有一个EscrowManager实体来实现PlayerRegistry实体。交易实体将提名并同意随机使用一个第三方托管管理器用于他们的交易交易。