5

我正在尝试为 MMO 游戏制作架构,但我无法弄清楚如何在 GameObjects 中存储尽可能多的变量,而无需在更新它们的同时进行大量调用以在线发送它们。

我现在拥有的是:

Game::ChangePosition(Vector3 newPos) {
    gameobject.ChangePosition(newPos);
    SendOnWireNEWPOSITION(gameobject.id, newPos);
}

它使代码变得垃圾,难以维护、理解和扩展。所以想想一个冠军的例子:

游戏对象冠军示例

我必须为每个变量创建很多函数。这只是对这个 Champion 的概括,每个 Champion 类型/“类”可能有 1-2 个其他成员变量。

如果我能够从 .NET 或类似的东西中获得OnPropertyChange ,那将是完美的。如果我有类似的东西,我试图猜测的架构会很好地工作:

对于惠普:当我更新它时,自动调用SendFloatOnWire("HP", hp);

对于位置:当我更新它时,自动调用SendVector3OnWire("Position", Position)

对于名称:当我更新它时,自动调用SendSOnWire("Name", Name);

究竟是什么SendFloatOnWire, SendVector3OnWire, SendSOnWire? 在 char 缓冲区中序列化这些类型的函数。

或方法 2(首选),但可能很昂贵

正常更新 Hp、Position,然后每个网络线程滴答扫描服务器上的所有 GameObject 实例以查找更改的变量并发送这些变量。

这将如何在大型游戏服务器上实现,我有哪些选择?对于这种情况有什么有用的书吗?

宏会变得有用吗?我想我接触到了一些类似的源代码,我认为它使用了宏。

先感谢您。

编辑:我想我找到了一个解决方案,但我不知道它实际上有多强大。我要去试一试,然后看看我的立场。https://developer.valvesoftware.com/wiki/Networking_Entities

4

2 回答 2

1

方法一:

这种方法可以相对“容易”使用通过 getter/setter 访问的映射来实现。一般的想法是这样的:

class GameCharacter {
    map<string, int> myints; 
    // same for doubles, floats, strings
public: 
    GameCharacter() {
        myints["HP"]=100; 
        myints["FP"]=50;  
    }
    int getInt(string fld) { return myints[fld]; }; 
    void setInt(string fld, int val) { myints[fld]=val; sendIntOnWire(fld,val); }
};

在线演示

如果您希望将属性保留在您的类中,您会选择指向指针或成员指针的映射,而不是值。在构建时,您将使用相关指针初始化地图。但是,如果您决定更改成员变量,则应始终通过 setter。

您甚至可以Champion通过将其设置为可以通过地图访问的属性和行为的集合来进一步抽象您的。此组件架构由Mike McShaffryGame Coding Complete(任何游戏开发者必读的书)中公开。这本书有一个社区站点,可以下载一些源代码。你可以看看actor.handactor.cpp文件。不过,我真的建议阅读本书中的完整解释。

组件化的优点是您可以将网络转发逻辑嵌入到所有属性的基类中:这可以将您的代码简化一个数量级。

关于方法2:

我认为基本想法非常合适,除了对所有对象的完整分析(或更糟糕的是,传输)将是一种矫枉过正。

一个不错的替代方案是在更改完成时设置一个标记,并在传输更改时重置。如果您传输标记的对象(并且可能只传输那些标记的属性),您将最小化同步线程的工作量,并通过汇集影响同一对象的多个更改的传输来减少网络开销。

于 2016-09-01T17:25:28.200 回答
0

我得出的总体结论:在我更新职位后再次打电话,还不错。它的代码行更长,但对于不同的动机来说更好:

  1. 这是明确的。你确切地知道发生了什么。
  2. 你不会通过各种技巧来降低代码的运行速度。
  3. 你不使用额外的内存。

我试过的方法:

  1. 正如@Christophe 所建议的那样,为每种类型提供地图。它的主要缺点是它不容易出错。您可以在同一个映射中声明 HP 和 Hp,这可能会增加另一层问题和挫败感,例如为每种类型声明映射,然后在每个变量前面加上映射名称。
  2. 使用与 Valve引擎类似东西:它为您想要的每个网络变量创建了一个单独的类。然后,它使用模板来包装您声明的基本类型(int、float、bool)以及该模板的扩展运算符。它使用了太多的内存和对基本功能的额外调用。
  3. 使用数据映射器为构造函数中的每个变量添加指针,然后将它们与偏移量一起发送。当我意识到代码开始变得混乱且难以维护时,我过早地离开了这个项目。
  4. 手动使用每次更改时发送的结构。这可以通过使用protobuf轻松完成。扩展结构也很容易。
  5. 每个滴答声,使用类的数据生成一个新结构并发送它。这使非常重要的内容始终保持最新,但会占用大量带宽。
  6. 在 boost 的帮助下使用反射。这不是一个很好的解决方案。

毕竟,我混合使用了 4 和 5。现在我正在我的游戏中实现它。protobuf 的一个巨大优势是能够从 .proto 文件生成结构,同时还为您提供结构的序列化。它的速度非常快。

对于那些出现在子类中的特殊命名变量,我制作了另一个结构。或者,在 protobuf 的帮助下,我可以拥有一系列简单的属性ENUM_KEY_BYTE VALUE:whereENUM_KEY_BYTE只是一个将 a 引用enum到诸如IS_FLYING, IS_UP,等属性的字节IS_POISONED,并且VALUE是 a string

我从中学到的最重要的事情是尽可能多地进行序列化。在两端使用更多的 CPU 比拥有更多的 Input&Output 更好。

如果有人有任何问题,请发表评论,我会尽力帮助您。

ioanb7

于 2016-09-11T17:57:44.660 回答