KBEngine
一个开源的、跨平台的MMO服务器引擎
服务器编程概述(一)
第1章:概览
这个文档包含了一些有关KBEngine引擎创建实体和用户数据结构的参考信息。这是描述KBEngine引擎系统的更详细的一篇文章。
如果要参考API信息,请参阅KBEngine 脚本API文档。
实体是组成游戏世界的一种数据结构。通过实体,你可以创建玩家、NPC、战利品、聊天室或者是其他一些可以在你游戏里面交互的东西。
每个实体都是由很多Python脚本作为具体实现,然后用XML文件把这些文件关联在一起组成的。这些文件都位于<res>文件夹下面的script文件夹里面(即<res>/scripts),这里的<res>的位置是由环境变量KBE_RES_PATH来定义的。
在官方的DEMO中,这个环境变量是这么写的:
Windows: KBE_RES_PATH =%KBE_ROOT%/kbe/res;%KBE_ROOT%/demo/;%KBE_ROOT%/demo/res/
Linux: KBE_RES_PATH=$KBE_ROOT/kbe/res/:$KBE_ROOT/demo/:$KBE_ROOT/demo/res/
因为有了环境变量,所以您随时可以更改res文件夹的所在位置。
下面的这个清单列举了有关实体的重要文件夹和文件:
|- res (工程的所有资源)
|- server (通常放置服务端相关的配置文件)
|- scripts (所有的游戏逻辑,python文件)
|- entities.xml (实体声明文件)
|- base (Base的Python逻辑)
|- cell (Cell的Python逻辑)
|- client (Client的Python逻辑)
|- bots (机器人的Python逻辑,压力测试)
|- common (逻辑公共文件夹)
|- data (游戏逻辑用到的数据资源)
|- db (dbmgr扩展脚本)
|- entity_defs (实体定义与声明)
|- <entity>.def (实体定义文件,这是定义entities.xml中每个实体的文件)
|- interfaces (实体的接口声明)
|- server_common (服务端逻辑公共)
|- user_type (自定义用户类型目录)
|- alias.xml (工程中数据类型的定义文件)
第2.1部分:entities.xml文件
文件<res>/script/entities.xml是用来声明哪些实体类型是有效、可用的。
这个文件中的每个标签均代表着一个实体类型。对于每个实体类型,必须在<res>/scripts/entity_defs里面有一个定义文件,还必须至少有一个Python脚本位于<res>/scripts/base里面
在这个文件中,实体类型声明的顺序与最后实体相关联的ID对应。
在最简单的使用形式中,里面列出的是实体文件的标签,用于每一个实体的加载。
假设我们要定义一个叫做NewEntityType的实体,我们只需要按照如下所示简单地添加一行代码:
<root>
...
<NewEntityType/>
</root>
第2.2部分:实体定义文件
文件<res>/scripts/entity_defs/<entity>.def就是实体定义文件,这个文件声明了你的实体是怎么和KBEngine交互的。
这将让KBEngine系统从抽象繁琐的发送和接收任务转换为简单地调用你写的实体脚本中的不同“方法”。
你可以这么理解,实体定义文件给你的实体提供了一个交互接口,Python脚本则提供了实体的具体实现。
下面这个图显示了一个KBEngine实体的概念部分:
每个实体都会有一个相应的实体定义文件,命名格式是“实体的名称.def”。例如,一个Seat实体将会有一个叫做Seat.def的定义文件。
对于我们来说,有一个“最简”的实体定义文件来协助我们快速定义一个新实体,并帮助我们来理解这篇文档的所写的内容,是很有用的。
那么,下面的内容就是一个最简的实体定义文件。
<root>
<Parent> optional parent entity</Parent>
<Implements>
<!-- interface references -->
</Implements>
<ClientName> optional client type</ClientName>
<Volatile>
<!-- volatile definitions -->
</Volatile>
<Properties>
<!-- properties -->
</Properties>
<ClientMethods>
<!-- declaration -->
</ClientMethods>
<CellMethods>
<!-- declaration -->
</CellMethods>
<BaseMethods>
<!-- declaration -->
</BaseMethods>
<LoDLevels>
<!-- levels of detail -->
</LODLevels>
<NetworkCompression>
<!-- internal and external networkcompression -->
</NetworkCompression>
</root>
现在我们有了实体定义文件,至于实体定义文件的细节,我们将会在之后学到。我们现在只需要知道其结构及其作用。有兴趣的,可以将其中的注释自行翻译来了解详细信息。
在这一部分的末尾,我们应该参阅服务器端的DEMO文件,查看各个占位符是怎么填写的。
第2.3部分:脚本文件
KBEngine将实体在游戏世界之中的处理分成了三个不同的种类:
实体种类 |
脚本文件位置 |
描述 |
Cell |
<res>/scripts/cell |
负责能够影响到它周围的空间的实体。处理过程发生在服务器集群上。 |
Base |
<res>/scripts/base |
负责不会影响到他周围空间的实体。处理过程发生在服务器集群上。 |
Client |
<res>/scripts/client |
负责需要大量感知周围环境的实体。 |
有可能一些实体没有这三个种类之中的一个。此外,一些实体类型或许不支持这三个种类之中的一个。对于每个实体类型,只要它支持这些实体种类,就会分别有一个CellAPP、BaseAPP、ClientAPP的脚本文件。
脚本文件是以“实体类型.py”的名称命名的。这个文件里面必须要包含一个以实体类型来命名的类。
例如,如果您有一个叫做Seat的实体类型,这个类型可以有Cell、Base和Client实体种类,那么将会有3个脚本文件,分别放在如下位置:
l <res>/scripts/cell/Seat.py
l <res>/scripts/base/Seat.py
l <res>/scripts/client/Seat.py
实体脚本文件中定义的基类声明了脚本文件的运行环境,如下表所述:
实体种类 |
实体基类 |
Cell |
KBEngine.Entity |
Base |
KBEngine.Base或KBEngine.Proxy |
Client |
KBEngine.Entity |
例如,Seat实体的Python脚本开始部分是这样写的:
Cell脚本文件-<res>/scripts/cell/Seat.py
import KBEngine
class Seat(KBEngine.Entity):
def __init__( self ):
KBEngine.Entity.__init__( self )
Base脚本文件-<res>/scripts/base/Seat.py
import KBEngine
class Seat(KBEngine.Base ):
def __init__( self ):
KBEngine.Base.__init__( self )
Client脚本文件-<res>/scripts/client/Seat.py
import KBEngine
class Seat(KBEngine.Entity ):
def __init__( self ):
KBEngine.Entity.__init__( self )
第3章:定义属性
属性,顾名思义,是描述实体状态的一种东西。像传统的对象控制系统一样,一个KBEngine的属性也有一个类型和名称。但是与传统的对象控制系统不同的是,一个属性对象局与分布式系统的特性。
属性声明在实体的定义文件里面(<res>/scripts/entity_defs/<entity>.def),具体位于这个文件的<Properties>标签之内。
属性的定义语法是下面的这样:
<root>
...
<Properties>
<propertyName>
<!-- type of this property -->
<Type> TYPE_NAME </Type>
<!-- Method of distribution -->
<Flags> DISTRIBUTION_FLAGS</Flags>
<!-- Default value (optional)-->
<Default> DEFAULT_VALUE</Default>
<!-- Is the property editable?(true/false) (optional) -->
<Editable> [true|false]</Editable>
<!-- Level of detail for thisproperty (optional) -->
<DetailLevel> LOD</DetailLevel>
<!-- Is the property persistent?-->
<Persistent> [true|false]</Persistent>
</propertyName>
</Properties>
...
</root>
第3.1部分:属性类型
作为一个服务器引擎的KBEngine,它需要在网络的各个组件中高效的传输数据。正是需要这种高效性,KBEngine通过定义文件定义了每个属性的数据类型(尽管实际上KBEngine使用的是Python语言——一个弱类型语言)。
要知道,对于MMO编程,带宽的占用量一定程度上决定了游戏的成本。所以节省带宽非常重要!你要尽可能的选择占用空间小的那个数据类型。这样,KBEngine就可以使用最节省的带宽来有效地传输数据。
第3.1.1部分:原始数据类型
下述的这些原始数据类型在KBEngine中是有效的:
BLOB —大小(单位是bytes): N+k
二进制数据,其结构类似于一个字符串,但是可以包含NULL字符。
在XML中以base64编码的形式储存,例如XML数据库中。
N是这个blob的字节数,k=4
FLOAT32 —大小(单位是bytes): 4
IEEE 32位浮点数。
FLOAT64 —大小(单位是bytes): 8
IEEE 64位浮点数。
INT8 —大小(单位是bytes): 1 —范围:从: -128 到: 127
8位的带符号整数。
INT16 —大小(单位是bytes): 2 —范围:从: -32,768 到: 32,767
16位带符号整数。
INT32 —大小(单位是bytes): 4 —范围:从: -2,147,483,648 到: 2,147,483,647
32位带符号整数。
INT64 —大小(单位是bytes): 8 —范围:从: -9,223,372,036,854,775,808 到: 9,223,372,036,854,775,807
64位带符号整数。
MAILBOX —大小(单位是bytes): 12
一个KBEnginemailbox.
将一个实体传输到MAILBOX的参数自动转换成一个MAILBOX。
PYTHON —大小(单位是bytes): 被pickle的字符串大小, 参照STRING
适用Pythonpickle来将任何Python数据类型打包成字符串, 并传输结果.
这个数据类型不应该在服务器和客户端之间适用,因为它是不安全的,而且是低效率的.
STRING —大小(单位是bytes): N+k
字符串 (非Unicode).
N是字符串中字符的数量, k=4.
UINT8 —大小(单位是bytes): 1 - 范围:从: 0 到: 255
8位无符号整数。
UINT16 —大小(单位是bytes): 2 —范围:从: 0 到: 65,535
16位无符号整数。
UINT32 —大小(单位是bytes): 4 —范围:从: 0 到:4,294,967,295
32位无符号整数。
这个数据类型可能会使用Python的long型来替代int,所以效率可能比INT32低。
UINT64 —大小(单位是bytes): 8 —范围:从: 0 到:18,446,744,073,709,551,615
64位无符号整数。
UNICODE_STRING — 大小(单位是bytes): 达到4N+k
字符串 (Unicode).
N是字符串中字符的数量, k=4. 流的格式为UTF-8.
VECTOR2 —大小(单位是bytes): 8
二维向量的32位浮点数. 代表着Python中含有两个数的tuple (或者Math.Vector2).
VECTOR3 —大小(单位是bytes): 12
三维向量的32位浮点数. 代表着Python中含有三个数的tuple (或者Math.Vector3).
VECTOR4 —大小(单位是bytes): 16
四维向量的32位浮点数. 代表着Python中含有四个数的tuple (或者Math.Vector4).
第3.1.2部分:复合数据类型
下述的这些复合数据类型在KBEngine中是有效的:
第3.1.2.1部分:ARRAY和TUPLE
KBEngine仍然支持数组和元组,可以用来创建一个任意传统类型的数组。
数据所占用的空间可以按照如下公式计算:
N*t+k
N - 数组中的元素数量
t - 数组中每个元素所占用的大小
k - 一个特殊的常数
KBEngine的TUPLE类型在Python代码中是用tuple类型来写的,ARRAY类型在Python代码中是用list类型写的。
Tuple的定义如下:
<Type> TUPLE<of> [TYPE_NAME|TYPE_ALIAS] </of> [<size> n </size>]</Type>
Array的声明如下:
<Type> ARRAY<of> [TYPE_NAME|TYPE_ALIAS] </of> [<size> n </size>]</Type>
如果TUPLE或者ARRAY的大小被指定了,那么它就必须有n个元素。给一个固定大小的ARRAY或者TUPLE添加或者删除元素是不允许的。如果元素的默认值没有被指定,那么固定大小的ARRAY数组或者TUPLE的n个元素将会含有指定元素类型的默认值。
数组有个特殊的方法,叫做equals_seq(),通过对元素进行布尔运算,从而比较不规则的Python序列(包括List和tuple)是否相等。例如:
self.myList = [1,2,3]
self.myList.equals_seq([1,2,3] )
# 将会返回True
self.myList.equals_seq((1,2,3) )
# 将会返回True
数组中元素的高效变化,包括指定单个元素、附加元素、拓展元素、删除元素、弹出元素和分配数值
例如下面的这个例子:
self.myList = [1, 2,3, 4, 5]
self.myList[ 3 ] = 8
self.myList.append( 6)
self.myList.extend([7, 8] )
self.myList += [9,10]
self.myList.pop()
self.myList.remove( 7)
self.myList[ 2 : 5 ]= [11, 12]
del self.myList[ 2 ]
del self.myList[ 1 :4 ]
第3.1.2.2部分:数据类型FIXED_DICT
FIXED_DICT数据类型允许使用一串关键字来定义一个类字典的属性。
关键字和关键字的类型都是预先指定的。
声明一个FIXED_DICT的方法如下:
<Type>FIXED_DICT
<Parent> ParentFixedDictTypeDeclaration</Parent>
<Properties>
<field>
<Type> FieldTypeDeclaration</Type>
</field>
</Properties>
<AllowNone> true|false</AllowNone>
</Type>
这个类型可以声明在任何类型声明可能出现的地方,例如,在<res>/scripts/entity_defs/alias.xml里面、<res>/scripts/entity_defs/<entity>.def里面、甚至是方法的形式参数等等。
下面的这个代码片段演示了如何声明一个FIXED_DICT类型。
<root>
<TradeLog> FIXED_DICT
<Properties>
<dbIDA>
<Type> INT64 </Type>
</dbIDA>
<itemsTypesA>
<Type> ARRAY <of> ITEM </of></Type>
</itemsTypesA>
<goldPiecesA>
<Type> GOLDPIECES </Type>
</goldPiecesA>
</Properties>
</TradeLog>
</root>
FIXED_DICT的实例可以像Python dictionary一样修改和访问,当然下面这两个例外:
1.关键字不可以被添加或者删除。
2.数值的类型必须和声明匹配。
例如:
if entity.TradeLog["dbIDA" ] == 0:
entity.TradeLog[ "dbIDA" ] = 100
另外,它也支持这种写法:
ifentity.TradeLog.dbIDA == 0:
entity.TradeLog.dbIDA = 100
注意 |
如果使用结构体,则可能会造成名称和FIXED_DICT的方法冲突的问题。 |
FIXED_DICT实例可以使用Python dictionary的superset的关键字。忽略dictionary中任何不需要的关键字。
例如:
entity.TradeLog = {"dbIDA" : 100, "itemsTypesA" : [ 1, 2, 3 ],
"goldPiecesA" : 1000,"redundantKey" : 12345 }
当FIXED_DICT的实例使用Python dictionary来设置的时候,Python dictionary中的数值将会被FIXED_DICT实例所引用。