游戏服务器的概念设计3

游戏服务器的概念设计2中,zack记录了将游戏视为一个rule-based system的想法。而对于组成游戏世界的最基本元素,概念上可以抽象为resource和pool,而通过pool就可以组合出不同的游戏世界中需要的entity。

那么,从数据驱动的角度来看,也许我们需要的基本数据文件有三个。假定这些数据以json的格式来描述。

  • resources.json
  • pools.json
  • entites.json

resources.json存储了一些游戏中需要使用的基本元素的定义。比如HP, EXP, GOLD等等。pools.json中则是一些特定的resource的归类组合,用于代表一些固定并经常用到的分组的概念。比如人物基本属性这样的概念是可以作为一个预先定义好的pool存放于pools.json。这可以在描述entity时减少一些重复。

在准备好了resource和pool之后,就可以开始对entity进行组合和设计了。游戏中世界中的物件都可以视作entity,只是携带了不同的pool而已。在entites.json中,entity可以包含在pools.json中定义好的pool,也可以添加额外的pool。

至此,其实已经可以开始实现entity之间的简单交互了。正如pool所能支持的交互一样,entity之间可以执行pull或者gain的操作,将resource进行转化。简单的例子比如说玩家角色使用一个药瓶恢复HP,或者玩家角色对一个怪物造成伤害,减少了怪物的HP。这些都可以抽象为entity之间的互动。

再进一步,进行这些互动如果每次都是一样的,并且互动造成的结果没有任何反馈的话,其实也很没有趣味。因此,需要引入两个新的概念来丰富这个过程。

一个概念是Trigger。Trigger的主要作用用于检查entity所具有的resource的变化,例如,如果entity的HP变为0,那么某种意义上我们认为这个entity将表现出“死亡”的反馈,同时entity也可以从世界中销毁。Trigger其实就是一组规则的集合,所有的entity都应该具有的共同Trigger特性是,如果entity所持有的resource全部耗尽,那么这个entity将从世界中销毁。

第二个概念是Gate。Gate是处在交互发生的路径上,对交互能否发生做出限制,以及对交互发生的结果进行修正的元件。比如,玩家角色通过一个技能想对怪物做出伤害,那么技能就是一个Gate,这个Gate或许要求玩家消耗一定量的MP,然后根据玩家具有的一些resource按一定规则重新计算将要从怪物身上pull的HP量。同时,在最后的结算之前,怪物身上也同样会有一个Gate,这个Gate会根据怪物的resource重新对最终pull的HP量进行修订。

这样的话,从数据的角度来看,又可以分离出gates.json和trigger.json的定义。而这里引入的一个新问题是,我们也许对交互的操作也需要一些定义,以保证操作的结算过程选择了正确的路径。因为从概念设计的角度来看,Trigger附着于某个entity自身,而Gate总需要连接Source和Target,或者一组Source和Target,而选择哪个Gate就需要用对应的Action加以区分了。

这里会遇到的一个新问题是,并非所有交互都是entity之间直接完成的,有时候是通过某种中间体。比如要处理交易的问题,就需要重新设计一个Trader的过程,Trader本身并不一定要作为一个实例化的entity存在。

还有一种比较特殊一点的情况,就是类似商店的系统。这种系统本身并不直接拥有resource,但是却可以通过gain resource的交互制造entity。尽管如果就商店而言,本身作为一个entity存在从概念上也能符合逻辑,但zack觉得这类机制和系统,包括比较常见的如装备强化,物品合成等,单独作为一个概念来实现要更清晰一些,zack将其称为Engine。Engine可以作为实例化的entity存在,也可以不是。

Engine可以有几个特性,比如Static, Dynamic和Converter。Static的就是例如最简单的NPC商店,玩家为Engine提供一定的resource,就能按规则产出resource或者entity。而Dynamic的则是玩家可以提供一定的resource,从而改变Engine的产出规则。Converter很好理解,就是可以将resource或entity进行转化,产生或组合出新的entity和resource。

Engine与entity一样,同样可以附着不同的Trigger,交互中也受Gate的影响。之所以和entity的概念做一定区分,主要是为了后续考虑游戏的narrative和progression这样重要概念时的便利。

到目前为止,zack所记录的,都是一个游戏模拟需要的最底层数据概念模型的描述以及基本的规则关联机制。通过这些,已经可以开始实现简单的世界定义和基本的游戏核心规则。在现代的多人在线游戏里,还有个两个非常核心和关键的部分需要建立在这个基础之上,就是关于叙事(Narrative)和社交(Social)。这在之后的文章里会依次思考和记录。

现在大部分的游戏引擎都是基于entity的框架,下面的代码里是用scala编写的一个entity最基本的定义,包括一些组合和混入的特性,这在之后的具体实现里都会经常用到(实际上也是目前zack在使用的)。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
trait Entity {
  def accessAs[T](t: Class[T]): T
}
 
trait EntityInterface {
  def owner: Entity
}
 
trait Component {
  protected var _owner: Entity = null
  def owner: Entity = _owner
  def isAttached: Boolean = _owner != null
 
  def onAttached(owner: Entity) {
    _owner = owner
  }
}
 
trait Adapter {
  def apply[T](e: Entity, t: Class[T]): T
}
 
trait Mixinable { this: Entity =>
  def owner: Entity = this
  def mixin[T](t: Class[T]): T = {
    if (t.isInstance(this)) asInstanceOf[T] else null.asInstanceOf[T]
  }
}
 
trait MixinableEntity extends Entity with Mixinable {
  def accessAs[T](t: Class[T]): T = mixin(t)
}
 
trait Attachable { this: Entity =>
  var components: List[Component] = List()
 
  def attach(c: Component) {
    components ::= c
    c.onAttached(this)
  }
 
  def component[T](t: Class[T]): T = {
    val c = components find { t.isInstance } getOrElse null
    c.asInstanceOf[T]
  }
}
 
trait AttachableEntity extends Entity with Attachable {
  def accessAs[T](t: Class[T]): T = component(t)
}
 
trait Adaptive { this: Entity =>
  def adapter: Adapter
  def adoptTo[T](t: Class[T]): T = {
    var adapter = this.adapter
    if (adapter != null) adapter(this, t) else null.asInstanceOf[T]
  }
}
 
trait AdaptiveEntity extends Entity with Adaptive {
  def accessAs[T](t: Class[T]): T = adoptTo(t)
}
 
trait CompositEntity extends Entity with Mixinable with Attachable {
  def accessAs[T](t: Class[T]): T = {
    if (t.isInstance(this)) mixin(t) else component(t)
  }
}