【问题标题】:Python: How to serialize objects for multiplayer game?Python:如何为多人游戏序列化对象?
【发布时间】:2021-01-23 23:53:23
【问题描述】:

我们正在开发一款类似于 Top-Down-RPG 的多人游戏,用于与一些朋友一起学习(而且很有趣!)。我们已经有一些游戏中的实体和输入正在工作,但是网络实现让我们头疼:D

问题

当尝试使用 dict 进行转换时,某些值仍将包含 pygame.Surface,我不想转移它,并且在尝试 jsonfy 时会导致错误。我想以简单的方式传输的其他对象,例如 Rectangle 无法自动转换。

已经可以使用

  • 客户端-服务器连接
  • 双向传输 JSON 对象
  • 异步网络和同步放入队列

情况

一个新玩家连接到服务器并想要获取所有对象的当前游戏状态。

数据结构

我们使用基于“实体-组件”的架构,因此我们将游戏逻辑非常严格地划分为“系统”,而数据存储在每个实体的“组件”中。 Entity 是一个非常简单的容器,只有一个 ID 和一个组件列表。

示例实体(缩短以提高可读性):

实体 |-- 组件(可移动) |-- 组件(图形) | |- 复杂的数据类型,如 pygame.SURFACE | `- (...) `- 组件(库存)

我们尝试了不同的方法,但所有方法似乎都不太合适或感觉“老套”。

泡菜

非常接近 Python,因此将来很难实现其他客户端。而且我已经阅读了有关以这种动态方式从网络创建项目时它提供的泡菜的一些安全风险。它甚至不能解决曲面/矩形问题。

__dict__

仍然包含对旧对象的引用,因此对不需要的数据类型的“清理”或“过滤”也会在源中发生。 deepcopy 抛出异常。

...\Python\Python36\lib\copy.py", line 169, in deepcopy
    rv = reductor(4)
TypeError: can't pickle pygame.Surface objects

显示一些代码

“实体管理器”类的方法应该生成所有实体的快照,包括它们的组件。此快照应无任何错误地转换为 JSON - 如果可能,此核心类中无需太多配置。

    class EnitityManager:
        def generate_world_snapshot(self):
            """ Returns a dictionary with all Entities and their components to send
            this to the client. This function will probably generate a lot of data,
            but, its to send the whole current game state when a new player
            connects or when a complete refresh is required """
            # It should be possible to add more objects to the snapshot, so we
            # create our own Snapshot-Datastructure
            result = {'entities': {}}
            entities = self.get_all_entities()
            for e in entities:
                result['entities'][e.id] = deepcopy(e.__dict__)
                # Components are Objects, but dictionary is required for transfer
                cmp_obj_list = result['entities'][e.id]['components']
                # Empty the current list of components, its going to be filled with
                # dictionaries of each cmp which are cleaned for the dump, because
                # of the errors directly coverting the whole datastructure to JSON
                result['entities'][e.id]['components'] = {}
                for cmp in cmp_obj_list:
                    cmp_copy = deepcopy(cmp)
                    cmp_dict = cmp_copy.__dict__
                    # Only list, dict, int, str, float and None will stay, while
                    # other Types are being simply deleted including their key
                    # Lists and directories will be cleaned ob recursive as well
                    cmp_dict = self.clean_complex_recursive(cmp_dict)
                    result['entities'][e.id]['components'][type(cmp_copy).__name__] \
                        = cmp_dict

            logging.debug("EntityMgr: Entity#3: %s" % result['entities'][3])
            return result

预期与实际结果

我们可以找到一种方法来手动覆盖我们不想要的元素。但是随着组件列表的增加,我们必须将所有过滤器逻辑放入这个核心类中,它不应该包含任何组件特化。

我们真的必须将所有逻辑放入 EntityManager 中以过滤正确的对象吗?这感觉不太好,因为我希望在没有任何硬编码配置的情况下完成所有到 JSON 的转换。

如何以最通用的方法转换所有这些复杂数据?

感谢您到目前为止的阅读,非常感谢您提前提供的帮助!

我们已经在工作的有趣文章抛出,可能对其他有类似问题的人有所帮助

更新:解决方案 - thx 2 懒惰

我们使用了以下架构的组合,到目前为止效果非常好,而且维护起来也很好!

Entity Manager 现在调用实体的 get_state() 函数。

class EntitiyManager:
    def generate_world_snapshot(self):
        """ Returns a dictionary with all Entities and their components to send
        this to the client. This function will probably generate a lot of data,
        but, its to send the whole current game state when a new player
        connects or when a complete refresh is required """
        # It should be possible to add more objects to the snapshot, so we
        # create our own Snapshot-Datastructure
        result = {'entities': {}}
        entities = self.get_all_entities()
        for e in entities:
            result['entities'][e.id] = e.get_state()
        return result


实体只有一些基本属性可以添加到状态中,并将 get_state() 调用转发给所有组件:

class Entity:
    def get_state(self):
        state = {'name': self.name, 'id': self.id, 'components': {}}
        for cmp in self.components:
            state['components'][type(cmp).__name__] = cmp.get_state()
        return state


组件本身现在从它们的新超类组件继承其 get_state() 方法,它只关心所有简单的数据类型:

class Component:
    def __init__(self):
        logging.debug('generic component created')

    def get_state(self):
        state = {}
        for attr, value in self.__dict__.items():
            if value is None or isinstance(value, (str, int, float, bool)):
                state[attr] = value
            elif isinstance(value, (list, dict)):
                # logging.warn("Generating state: not supporting lists yet")
                pass
        return state

class GraphicComponent(Component):
   # (...)


现在每个开发人员都有机会覆盖此函数,以便在需要时直接在组件类中(如图形、运动、库存等)为复杂类型创建更详细的 get_state() 函数以更准确的方式保护状态 - 这对于将来维护代码来说是一件大事,将这些代码片段放在一个类中。

下一步是实现从同一类中的状态创建项目的静态方法。这使得这项工作非常顺利。
非常感谢您的帮助。

【问题讨论】:

  • 我没有遵循 EnitityManager 类的代码中的所有逻辑,所以我不确定你在哪里/如何进行任何过滤,但无论如何,听起来你应该让它配置为关于过滤正确的对象。即让每个不同的Enitity 类型向它注册自己并告诉它它的基本组件是什么,以允许使用该信息来剥离/跳过组件专业化。
  • 感谢您的回答。在此函数中“清理”了组件字典:self.clean_complex_recursive(cmp_dict)。它只是删除所有不是 list、dict、str、int、None 的值(及其键)。过滤会导致在 EnitityManger 中使用转换规则进行较长的配置,因为 EntitiyManager 不应该关心如何详细处理组件内容并决定什么对安全/传输重要。

标签: python pygame game-engine


【解决方案1】:

我们真的必须将所有逻辑放入 EntityManager 中以过滤正确的对象吗?

不,你应该使用polymorphism

您需要一种可以在不同系统之间共享的形式来表示您的游戏状态的方法;所以也许给你的组件一个可以返回所有状态的方法,以及一个允许你从那个状态创建组件实例的工厂方法。

(Python已经有了__repr__魔术方法,但你不必使用它)

因此,与其在实体管理器中进行所有过滤,不如让他在所有组件上调用这个新方法,并让每个组件决定结果的样子。

类似这样的:

...
result = {'entities': {}}
entities = self.get_all_entities()
for e in entities:
    result['entities'][e.id] = {'components': {}}
    for cmp in e.components:
         result['entities'][e.id]['components'][type(cmp).__name__] = cmp.get_state()
...

一个组件可以这样实现它:

class GraphicComponent:
    def __init__(self, pos=...):
        self.image = ...
        self.rect = ...
        self.whatever = ...

    def get_state(self):
        return { 'pos_x': self.rect.x, 'pos_y': self.rect.y, 'image': 'name_of_image.jpg' }

    @staticmethod
    def from_state(state):
        return GraphicComponent(pos=(state.pos_x, state.pos_y), ...)

从服务器接收状态的客户端EntityManager 将迭代每个实体的组件列表并调用from_state 来创建实例。

【讨论】:

  • 非常感谢您快速详细的回答!!我想我理解了你的建议。我今天将尝试编写此代码,以充分了解如何适合我的代码。特别是如果我可以将您的大部分建议移到超类组件中,以使用通用方法来迭代“简单”属性,并有可能为通用表示跳过的属性实现额外的状态表示。这可能是感觉更好的解决方案,因为我将这段代码直接放在组件中。感谢你!我会告诉你的!
  • 我检查了有关您对__repr__ 的提示的文档,这似乎是正确的用例?您会建议在这种情况下使用这种方法还是我应该遵循 get_state() 方法?
  • @Greg 真的,这取决于你。曾经有人认为__repr__ 应该返回一个字符串,您可以将它与eval 一起使用来重新创建对象(例如,在我的示例中,它将返回字符串GraphicComponent((12, 43), 'name_of_image.jpg') 或类似的东西),但是,AFAIK,没有人真正这样做;而且您当然不想eval 通过网络收到的字符串。我认为cmp.get_state()repr(cmp) 更明确,因为repr 很少使用,至少在我的经验中,但最后,恕我直言,这真的没关系。
  • 好的,感谢您的估计,我们使用自定义方法解决了这个问题,cmp.get_statesounds 很合适。特别是我们可以在实体上调用它,并且实体在所有组件上迭代相同的方法,因为实体状态是其组件状态的总和。这对我们帮助很大!
  • 既然GraphicComponent不是从一个通用的基础组件派生出来的,那么这个“多态性”是怎么来的?
猜你喜欢
  • 1970-01-01
  • 2020-07-26
  • 2014-09-20
  • 2016-08-19
  • 1970-01-01
  • 1970-01-01
  • 2017-10-24
相关资源
最近更新 更多