Python: Как сериализовать объекты для многопользовательской игры? - PullRequest
4 голосов
/ 26 марта 2019

Мы работаем над многопользовательской игрой типа Top-Down-RPG для целей обучения (и веселья!) С некоторыми друзьями.У нас уже есть некоторые сущности в игре, и входы работают, но сетевая реализация доставляет нам головную боль: D

Проблемы

При попытке конвертировать с помощью dict некоторые значения будут по-прежнему содержать pygame.Surface, который я не хочу передавать, и это вызывает ошибки при попытке их jsonfy.Другие объекты, которые я хотел бы перенести простым способом, например Rectangle, не могут быть автоматически преобразованы.

Уже работоспособно

  • Клиент-серверное соединение
  • Передача JSONобъекты в обоих направлениях
  • Асинхронное сетевое взаимодействие и синхронное помещение в очередь

Ситуация

Новый игрок подключается к серверу и хочет получить текущее состояние игры свсе объекты.

Структура данных

Мы используем архитектуру на основе «сущности-компонента», поэтому мы очень строго разделили игровую логику на «системы», в то время как данные хранятся в «компоненты "каждого субъекта.Entity - это очень простой контейнер, в котором нет ничего, кроме идентификатора и списка компонентов.

Пример Entity (сокращено для лучшей читаемости):

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

Мы пробовали разные подходы, но все, кажется, не очень хорошо подходят или чувствуют себя "хаки".

pickle

Очень близко к Python, поэтому нелегко внедрить других клиентов в будущем.И я читал о некоторых рисках безопасности при создании элементов из сети таким динамичным образом, как это предлагает.Он даже не решает проблему Поверхность / Прямоугольник.

__dict__

Все еще содержит ссылку на старые объекты, поэтому в источнике также происходит «очистка» или «фильтр» для нежелательных типов данных.При глубоком копировании создается исключение.

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

Показать некоторый код

Метод класса "EnitityManager", который должен создавать моментальный снимок всех сущностей, включая их компоненты.Этот снимок следует преобразовать в 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 sloth

Мы использовали комбинацию следующегоАрхитектура, которая до сих пор прекрасно работает и также хороша в обслуживании!

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 () от своих новых компонентов суперкласса, чтоh просто заботится обо всех простых типах данных:

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


NКаждый разработчик имеет возможность наложения этой функции на создание более подробной функции get_state () для сложных типов непосредственно в классах компонентов (таких как Графика, Движение, Инвентарь и т. д.), если это необходимо для обеспечения безопасности. более точное состояние - что очень важно для поддержания кода в будущем, чтобы эти куски кода были в одном классе.

Следующим шагом является реализация статического метода для создания элементов из состояния в том же классе. Это делает эту работу действительно гладкой.
Большое спасибо ленивцу за помощь.

1 Ответ

2 голосов
/ 26 марта 2019

Неужели нам нужно поместить всю логику в EntityManager для фильтрации нужных объектов?

Нет, вы должны использовать полиморфизм .

Вам нужен способ представить состояние игры в форме, которая может быть разделена между различными системами;поэтому, возможно, дайте вашим компонентам метод, который будет возвращать все их состояние, и фабричный метод, который позволяет вам создавать экземпляры компонентов из этого самого состояния.

(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, чтобы создать экземпляры.

...