RolePlayingDesign

来自gamedev
跳转到导航 跳转到搜索



这可能是构成Arianne的所有中间件中最复杂的部分.
Role Playing Design 角色扮演设计是决定创建一个新游戏有多 easy 的关键因素. 我们选择简化基于有限回合时间的游戏的创建。 Arianne在此类游戏(也称为实时游戏)上工作的更好。

角色扮演设计试图做到通用且与游戏无关(独立于所制作的游戏)。 RPManager幕后的基本思想是:

  forever
    {
    Execute Actions   // 执行动作
    Send Perceptions  // 发送感知
    Wait for next turn // 等待下一回合
    }

为此,我们使用以下几个类:

  • RPManager 代码在 Marauroa 中,不需要修改.
  • IRPRuleProcessor 是您需要修改以个性化游戏将执行的动作的接口.
  • RPWorld 是您需要扩展以实现onInit和onFinish方法的类,这些方法可以对初始化服务器时发生的情况以及在关闭服务器时发生的情况进行个性化.
  • IRPZone 如果您想实现最大程度的游戏个性化,这可能是您要实现的接口。但是, 建议使用 MarauroaRPZone, 因为它很好地实现了 Delta2 特点.


RPManager[编辑]

RP Manager 的目标是处理游戏中的RP. 也就是说:

  • run RPActions from clients // 从客户端执行RPAction
  • manage RPWorld // 管理RPWorld
  • control triggers for events // 控制事件的触发
  • control AI // 控制AI

这是一个非常复杂的任务。 因此,我们将它分为较小的子类.

RPManager 暴露一个简单的接口给 GameManager:

  • addRPAction
    此函数将特定玩家的动作排队,在下一个回合执行.
  • getRPObject
    这是管理RPWorld的接口,可在退出游戏时简化对RPObject的获取.
  • onInit Player
  • onExit Player
    这些是GameManager用来通知玩家已经进入或退出游戏时的回调函数.
  • transferContent
    回调函数。 RPRuleProcessor 调用这个函数,将游戏内容传给玩家.

RPManager 的主要流程如下:

forever
  {
  Proceed through every action in this turn   // 处理此回合中的所用动作
    {
    rpRuleProcessor executes action           // rpRuleProcessor执行动作
    }

  Build Perception                            // 构造感知
  Remove timed out players                    // 移除超时的玩家  

  Wait for Turn completion.                   // 等待回合结束
  Go to Next Turn                             // 进入下一回合
  }

RPScheduler 是处理每个玩家排队的动作的类。 动作管理的复杂性应该在这里处理。

RPRuleProcessor 是动作代码以及初始化和退出代码的包装类.
所有动作代码必须放在这里.
通过实现 RPRuleProcessor,您可以将 Marauroa 个性化为您想要制作的游戏。 但是,请记住,您仅限于实时风格的游戏。

对象和动作[编辑]

整个 Marauroa 系统由两个主要实体 RPAction 和 RPObject 管理。 还有几个辅助类,如 Attributes、RPSlot 和 RPClass

属性(Attributes)[编辑]

属性是名称-值形式的值对的集合. 我们可以在 Attribute 对象中存储几乎所有基本类型:

  • strings
  • integer
  • floats
  • boolean

我们不能在属性中存储结构,但您可以将一组数据转换为字符串并将其存储为字符串。 Marauroa 为此提供了助手方法(helper method)。

Objects[编辑]

Marauroa 服务器中的所有信息都包含在 RPObjects 中。 一个对象由几个属性(一个属性类似于一个变量,它有一个名称并包含一个值)和插槽组成。 Slot 是属于某个对象的容器或容器数组,用于在其中托管(存储)其他对象。

强制对象属性: id, type and zoneid

id 是对象的唯一标识,zoneid 是对象所在区域的标识,type 是对象的类型,也就是class,因此您可以为一个类的所有实例共享属性。

id 仅在包含该对象的区域内是唯一的。

注意: 引擎提供了两种特殊类型的属性: - 以 ! 开头的属性 对除对象所有者之外的所有其他用户隐藏。 - 以 # 开头的属性对所有用户隐藏。

对象类型的解释[编辑]

对象类是构建 Marauroa 数据结构的基本方式。 给定对象的属性类型必须等于您希望使用的类型类(type class)的 RPClass 名称。

类定义属性的类型、其可见性并为其分配一个内部代码,用于加速查找和节省带宽。 您可以基于另一个类创建新类,此功能称为继承(从已经存在的类创建一个新类,并获取所有原始类的方法和数据并对其进行扩展)。

可用的数据类型有:

  • Strings
  • Short strings ( 最多 255 字节 )
  • Integers ( 4 字节 )
  • Shorts ( 2 字节 )
  • Byte ( 1 字节 )
  • Flag ( 二进制属性 )

属性可以是可见的,这意味着客户在更改时可以看到它们,也可以是不可见的,那样客户就看不到它们。

Slots[编辑]

对象可以驻留在其他对象中,就像您将钥匙放在口袋里一样。 Slots 的目标是提供更丰富的游戏玩法,同时减少区域中的对象数量。

为了在里面放置对象,我们需要我们的宿主对象有插槽来放置它们。一个插槽只能处理一个对象。

例如,角色可以有:

  • left hand 左手
  • right hand 右手
  • backpack 背包
  • left pocket 左口袋
  • right pocket 右口袋

我们可以在每个插槽中存储对象。

一旦对象存储在对象槽中,访问存储对象的唯一方法是通过包含存储对象的对象。

和属性一样,插槽也有两种特殊类型:

  • 以 ! 开头的插槽名称 仅发送给拥有它的玩家。 (所以只有属主能看到)
  • 以 # 开头的插槽名称不会发送给玩家。 (所有人都看不到)

Actions[编辑]

为了表达客户端做某事的意愿,它必须向服务器发送 MessageC2SAction 消息。

一个动作由几个属性组成。 (属性类似于变量,因为它有一个名称并包含一个值)。

有可选和强制属性。 如果找不到强制属性,则 RPServerManager 会跳过该消息。

强制操作属性是 action_id 和 type。

action_id 用于识别感知中的结果响应中包含的动作

Perceptions[编辑]

向客户端发送的世界更新的基本结构称为感知。

有两种感知:

  • 同步感知:用于将客户端与服务器世界同步。 这是了解世界状态的唯一有效方式.
  • Delta 感知:这用于仅发送世界自上次感知以来的变化。

我们实际的感知系统称为 Delta2。 它与 Marauroa 核心紧密相连,因此我建议您使用它 :)

感知和动作是怎么工作的[编辑]

动作从客户端发送到服务器,以使角色执行动作。 为了让客户端知道操作的结果,服务器需要向客户端发送回复。 那么,这些是怎么完成的呢?

刚开始的时候,我们向客户端发送一个它的操作结果的操作。 然而,这使得代码变得非常困难,因为我们必须更新两个不同的东西,感知和动作。 一个直观的解决方案是:为什么不把动作结果和感知放在一起呢?

因此,动作结果存储在每个对象(执行动作)中,并带有一组属性,这些属性确定动作返回状态和属性。 这种发送结果的方式给 RPManager 编程带来一些困难,但它大大简化了新客户端的创建。

请参阅 Objects 文档中的 Actions 响应以了解返回的内容。 但是,请记住,返回结果取决于每个特定的游戏。

Delta2 感知算法[编辑]

DPA 背后的想法是避免每次都将所有对象发送给客户端,而只发送那些已修改的对象。

假设我们有 1000 个对象,只有 O1 和 O505 是活动对象,每回合都会修改。

传统方法是:

- 获取玩家应该看到的对象 ( 1000 objects )
- 将它们发送给玩家 ( 1000 objects )
- 下一回合
- 获取玩家应该看到的对象 ( 1000 objects )
- 将它们发送给玩家
- 下一回合
...

我希望你已经看出问题了……我们发送的对象每回合都没有改变。

delta感知算法:

- 获取玩家应该看到的对象 ( 1000 objects )
- 将列表缩减为修改后的列表( 1000 objects )
- 同时存储不再可见的对象 ( 0 objects )
- 将它们发送给玩家 ( 1000 objects )
- 下一回合
- 获取玩家应该看到的对象 ( 1000 objects )
- 将列表缩减为修改后的列表 ( 2 objects )
- 同时存储不再可见的对象 ( 0 objects )
- 将它们发送给玩家 ( 2 objects )
- 下一回合
...

delta 感知算法的下一步非常明确:delta2 想法是只发送发生变化的对象。 通过这种方式,我们可以节省更多带宽,使感知大小约为原始增量感知大小的 20%。

delta2 算法基于四个容器:

  • List of added objects
  • List of modified added attributes of objects
  • List of modified deleted attributes of objects
  • List of deleted objects

与 DPA 非常相关的一个领域是 RPZone(请参阅本文档后面部分)

如您所知,MPEG 视频每 X 帧添加一个完整帧,可以用作同步,以防文件损坏。 这个想法是,如果你无法继续解压缩数据,那么可以忽略一些数据,直到获得下一个完整的帧,从而再次同步。 这里的想法是类似的,如果我们无法与服务器同步,我们会向它发送一个 Out of Sync 消息,以便服务器发送一个同步感知,使客户端同步。 请记住,UDP 不是安全传输。

为了让感知起作用,调用 RPZone 中的 modify 方法很重要,这样修改的对象就会存储在修改列表中。

Zones and Worlds[编辑]

Marauroa 的世界可能非常大,我们需要把它们分成几块。 每一块都是我们所说的RPZone。

所以世界是由几个相互独立的 的RPZones 组成的。 要从一个RPZone移动到另一个RPZone,您必须在RPRuleProcessor中编写正确的行为。看看我们的代码示例就知道了。

RPWorld[编辑]

正如我们已经说过的,RPWorld存储了几个相互独立的RPZones。
RPWorld 提供了 onInit 和 onFinish 方法在服务器初始化和服务器结束时被调用,它们定义了如何处理这些事件。这其中不存在默认行为,你需要扩展这个类来重新定义行为。

它还提供了添加和获取新RPZones的方法:

  • addRPZone
  • getRPZone, 用于获取RPZone.ID或者RPObject.ID。

最后,它还包含管理RPObjects的方法:

  • addRPObject, 需要RPObject 在它的RPObject.ID中包含一个有效的RPZone.ID。
  • getRPObject
  • modifyRPObject
  • changeZone 将一个对象从旧zone移动到新zone并添加正确的有效id。

最后,RPWorld还包含一个名为nextTurn的方法,RPManager调用该方法来从一个回合移动到下一个回合。 它重置了delta2 数据。

RPZone[编辑]

对象必须存储在某个地方,我们使用区域Zones来存储它们。 区域Zone只是一个具有名称的对象容器。

每个RPZone必须 有一个惟一的名称。

为了改善Marauroa平台的可修改性,我们使RPZone成为接口,以便您可以根据需要实现它.

但在大多数情况下, 如果你认为Delta2 系统很好并且贴合游戏风格,您可以使用MarauroaRPZone,它是我们Delta2算法的参考实现。

实际的Marauroa RPZone由几个数据结构组成:

  • a HashMap of RPObject.ID to RPObject
  • a List of RPObject
  • a Perception

想法是事先已经在区域中计算了感知,从而节省了通常生成感知所需的大量时间。 所有数据结构都包含相同的对象,但 hashmap 用于使用 RPObject.ID 快速搜索对象。 这是定位具有已知 ID 的对象的最常用方法。 列表用于改善建立总体感知所需的时间。 感知用于预先计算增量感知(即查找区域的当前状态与上一回合发送给客户端的先前状态之间的变化)

区域内所有玩家的感知都是相同的.

为了使perceptions工作, 您必须手动调用modify方法,以便将角色或物品的更改告诉zone 。 {{#breadcrumbs: Marauroa | Internals | Role Playing Design }}