简体   繁体   English

Python:如何为多人游戏序列化对象?

[英]Python: How to serialize objects for multiplayer game?

We are working on a Top-Down-RPG-like Multiplayer game for learning purposes (and fun!) with some friends. 我们正在与一些朋友一起开发一款类似RPG的多人游戏,用于学习目的(和乐趣!)。 We already have some Entities in the Game and Inputs are working, but the network implementation gives us headache :D 我们已经在游戏中有一些实体并且输入正在运行,但网络实现让我们头疼:D

The Issues 问题

When trying to convert with dict some values will still contain the pygame.Surface, which I dont want to transfer and it causes errors when trying to jsonfy them. 当尝试使用dict进行转换时,某些值仍将包含pygame.Surface,我不想传输它,并且在尝试jsonfy时会导致错误。 Other objects I would like to transfer in a simplyfied way like Rectangle cannot be converted automatically. 我想以简单的方式传输的其他对象(如Rectangle)无法自动转换。

Already functional 已经功能齐全

  • Client-Server connection 客户端 - 服务器连接
  • Transfering JSON objects in both directions 在两个方向上传输JSON对象
  • Async networking and synchronized putting into a Queue 异步网络和同步放入队列

Situation 情况

A new player connects to the server and wants to get the current game state with all objects. 新玩家连接到服务器并希望获得所有对象的当前游戏状态。

Data-Structure 数据结构

We use a "Entity-Component" based architecture, so we separated the game logic very strictly into "systems", while the data is stored in the "components" of each Entity. 我们使用基于“实体 - 组件”的架构,因此我们将游戏逻辑严格分为“系统”,而数据则存储在每个实体的“组件”中。 The Entity is a very simple container and has nothing more than a ID and a list of components. 实体是一个非常简单的容器,只有一个ID和一个组件列表。

Example Entity (shorten for better readability): 示例实体(缩短以获得更好的可读性):

Entity
      |-- Component (Moveable)
      |-- Component (Graphic)
      |         |- complex datatypes like pygame.SURFACE
      |         `- (...)
       `- Component (Inventory)

We tried different approaches, but all seems not to fit very well or feel "hacky". 我们尝试了不同的方法,但似乎都不太适合或感觉“hacky”。

pickle 泡菜

Very Python near, so not easy to implement other clients in future. 非常接近Python,因此将来很难实现其他客户端。 And I´ve read about some security risks when creating items from network in this dynamic way how pickle it offers. 我已经从这个动态的方式从网络创建项目时了解了一些安全风险。 It does not even solve the Surface/Rectangle issue. 它甚至没有解决Surface / Rectangle问题。

__dict__ __dict__

Still contains the reference to the old objects, so a "cleanup" or "filter" for unwanted datatypes happens also in the origin. 仍包含对旧对象的引用,因此对于不需要的数据类型的“清理”或“过滤器”也会在原点中发生。 A deepcopy throws Exception. deepcopy抛出异常。

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

Show some code 显示一些代码

The method of the "EnitityManager" Class which should generate the Snapshot of all Entities, including their components. “EnitityManager”类的方法,它应该生成所有实体的快照,包括它们的组件。 This Snapshot should be converted to JSON without any errors - and if possible without much configuration in this core-class. 应该将此快照转换为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 

Expectation and actual results 期望和实际结果

We can find a way to manually override elements which we dont want. 我们可以找到一种手动覆盖我们不想要的元素的方法。 But as the list of components will increase we have to put all the filter logic into this core class, which should not contain any components specializations. 但随着组件列表的增加,我们必须将所有过滤器逻辑放入此核心类中,该核心类不应包含任何组件特化。

Do we really have to put all the logic into the EntityManager for filtering the right objects? 我们是否真的必须将所有逻辑放入EntityManager以过滤正确的对象? This does not feel good, as I would like to have all convertion to JSON done without any hardcoded configuration. 这感觉不太好,因为我希望在没有任何硬编码配置的情况下完成所有转换为JSON。

How to convert all this complex data in a most generic approach? 如何以最通用的方法转换所有这些复杂数据?

Thanks for reading so far and thank you very much for your help in advance! 感谢您的阅读,非常感谢您的帮助!

Interesting articles which we were already working threw and maybe helpful for others with similar issues 我们已经在工作的有趣文章投掷了,也许对有类似问题的其他人有帮助

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

We used a combination of the following architecture, which works really great so far and is also good to maintain! 我们使用了以下架构的组合,到目前为止工作非常好,并且维护起来也很好!

Entity Manager now calls the get_state() function of the entity. 实体管理器现在调用实体的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):
   # (...)


The Entity has only some basic attributes to add to the state and forwards the get_state() call to all the Components: 实体只有一些基本属性可以添加到状态,并将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 


The components itself now inherit their get_state() method from their new superclass components, which simply cares about all simple datatypes: 组件本身现在从其新的超类组件继承它们的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): # (...) 


Now every developer has the opportunity to overlay this function to create a more detailed get_state() function for complex types directly in the Component Classes (like Graphic, Movement, Inventory, etc.) if it is required to safe the state in a more accurate way - which is a huge thing for maintaining the code in future, to have these code pieces in one Class. 现在,每个开发人员都有机会覆盖此函数,以便直接在组件类 (如图形,移动,库存等)中为复杂类型创建更详细的get_state()函数,如果需要更准确地保护状态方式 - 这对于将来维护代码来说是一件巨大的事情,将这些代码片段放在一个类中。

Next step is to implement the static method for creating the items from the state in the same Class. 下一步是实现静态方法,以便从同一个类中的状态创建项目。 This makes this working really smooth. 这使得这项工作非常顺利。
Thank you so much sloth for your help. 非常感谢你的帮助。

Do we really have to put all the logic into the EntityManager for filtering the right objects? 我们是否真的必须将所有逻辑放入EntityManager以过滤正确的对象?

No, you should use polymorphism . 不,你应该使用多态

You need a way to represent your game state in a form that can be shared between different systems; 您需要一种以可在不同系统之间共享的形式表示您的游戏状态的方法; so maybe give your components a method that will return all of their state, and a factory method that allows you create the component instances out of that very state. 所以也许给你的组件一个返回所有状态的方法,以及一个允许你创建那个状态的组件实例的工厂方法。

(Python already has the __repr__ magic method, but you don't have to use it) (Python已经有__repr__魔术方法,但你不必使用它)

So instead of doing all the filtering in the entity manager, just let him call this new method on all components and let each component decide that the result will look like. 因此,不要在实体管理器中进行所有过滤,只需让他在所有组件上调用这个新方法,让每个组件决定结果如何。

Something like this: 像这样的东西:

...
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()
...

And a component could implement it like this: 一个组件可以像这样实现它:

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), ...)

And a client's EntityManager that recieves the state from the server would iterate for the component list of each entity and call from_state to create the instances. 并且从服务器接收状态的客户端的EntityManager将迭代每个实体的组件列表并调用from_state来创建实例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM