Дизайн игрового движка: чистый поток действий между клиентом и сервером

Я разрабатываю многопользовательскую онлайн-игру ради обучения.

Несколько клиентов подключаются к серверу, который содержит истинное представление игрового мира. Каждый клиент отслеживает подмножество этого представления. Я храню свои представления в Context. Клиенты и сервер изменяют состояние глобального контекста, передавая различные классы действий. Один из примеров - MoveEntityAction, другой - TalkAction, и все они реализуют интерфейс под названием Action. В принципе, игра ведется потоком экземпляров Action.

Однако передача действий туда и обратно требует некоторой возни. Во-первых, дайте определение того, как они читаются и записываются в сеть. Во-вторых, протокол для их перевода между глобальным и локальным контекстами (разные системы координат и т. д.). В-третьих, позвольте серверу определить, какие действия должны предшествовать другому действию (я надеюсь, что это ясно из приведенного ниже псевдокода).

Прямо сейчас этот процесс опирается на несколько вспомогательных классов, каждый из которых реализует интерфейс под названием Writer, Reader, ContextSwitcher и PrerequisiteFinder.

Это заставило меня сделать следующее:

  • Создайте таблицу поиска ActionLibrary, которая определяет такие методы, как getReader(int actionId), getWriter(<Class <? extends Action> a) и т. д.
  • Классы, реализующие вспомогательные интерфейсы, сильно зависят от связанного с ними класса Action.

В моей игре будет довольно много классов, реализующих интерфейс Action, а это означает, что каждый такой класс потребует реализации 3 дополнительных вспомогательных классов, каждый из которых содержит протокол для работы с определенным действием. Например, MoveEntityReader, MoveEntityWriter, MoveEntityContextSwitcher, MoveEntityPrerequisiteFinder.

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


Ниже приведен пример кода того, как действие может «перетекать» между клиентом и сервером. Обратите внимание, что это псевдокод, написанный для этого примера, но, надеюсь, он отражает суть того, что делает мой настоящий код. В приведенном ниже коде каждую сущность можно рассматривать как клиента.

Человеческий ввод (перетаскивание элемента на экран) создает экземпляр moveEntityAction. Он передается в сетевой поток следующим образом

writer = ActionLibrary.getWriter(moveEntityAction);
writer.writeTo(outputStream, moveEntityAction);

Сервер получает пакет с идентификатором и переводит его в глобальный контекст, серверное представление игрового мира:

reader = ActionLibrary.getReader(Id);
localAction = reader.readFrom(inputStream);
globalAction = ActionLibrary.getContextSwitcher(localAction.getClass(), playerContext, globalContext).apply(localAction);

Убедившись, что действие в порядке, сервер решает передать действие всем остальным клиентам. Однако, прежде чем он сможет это сделать, он должен передать «предпосылки» этого действия каждому клиенту. Например, если MoveEntityAction перемещает объект на экран второго объекта, PlaceEntityAction должен быть сначала отправлен второму объекту, чтобы у него был объект для перемещения. Происходит это следующим образом:

List<Entity> spectators = globalContext.getActionSpectators(globalAction);

for (Entity spectator : spectators) {
    List<Action> prerequisiteActions = ActionLibrary.getPrerequisiteFinder(globalAction.getClass()).get(spectator, globalAction, globalContext);
    spectatorContext = globalContext.getContextOf(spectator);

    // Broadcast prerequisite actions (if any)
    for (Action globalPreAction : prerequisiteActions) {
        localPreAction = ActionLibrary.getContextSwitcher(globalPreAction.getClass(), globalContext, spectatorContext).apply(globalPreAction);

        writer = ActionLibrary.getWriter(localPreAction);
        writer.writeTo(outputStream);
    }

    // Finally, send the instigating action:
    localAction = ActionLibrary.getContextSwitcher(globalAction.getClass(), globalContext, spectatorContext).apply(globalAction);

    writer = ActionLibrary.getWriter(localAction); 
    writer.writeTo(outputStream);
}

// Execute the action on the globalContext
globalContext = globalAction.execute(globalAction);

затем клиент получает каждый пакет и его идентификатор, находит соответствующий считыватель и применяет его к своему локальному Context:

reader = ActionLibrary.getReader(Id);
localAction = reader.readFrom(inputStream);
context = localAction.execute(context);

Я в основном ищу некоторые мысли и советы по этому поводу. Как бы вы убрали этот беспорядок? Может быть, есть шаблон проектирования для ситуаций, подобных моей, когда я рискую запутаться в кучу вспомогательных классов.

Заранее спасибо!

Обновлено: я хотел бы избежать Serializable по нескольким причинам. Во-первых, клиент может быть написан не на Java. Еще я хочу подобрать здесь общий передовой опыт, потому что со временем я могу превратить его в частный сервер для другой игры, для которой у меня нет сетевого протокола.

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

markspace 18.03.2018 21:36

Различные системы координат были просто примером. Действия могут выглядеть по-разному в зависимости от контекста, в котором они просматривались. Одна сущность может быть частично ослеплена и, таким образом, не может видеть, кто совершил действие, но для других сущностей это может быть совершенно ясно. Контекст сущности помогает преобразовать «истинное» действие в соответствии с его восприятием мира. Я ценю совет, но если бы я использовал Serializable, я бы решил только половину проблемы, поскольку мне все еще нужно иметь возможность переключать контексты и находить необходимые действия.

mrak 18.03.2018 22:02

Лучше решить половину проблемы, чем ничего не решить. :-) Но также, когда вы описываете сущности в разных состояниях и как они реагируют на ввод, это звучит так, как будто попытка поместить всю эту логику в один большой Контекст также может быть частью проблемы. Посмотрите, как заставить сущности реагировать на ввод в зависимости от их собственного состояния. C.f. Видео Марка Брауна о "системных" играх: youtube.com/watch?v=SnpAAX9CkIc

markspace 18.03.2018 22:36

Ага! Это хороший ответ, но я все же хотел бы избежать Serializable (см. Мою правку в вопросе). Извините за то, что не понял этого с самого начала. О контексте: это просто набор переменных состояния игрового мира. Глобальный контекст используется в качестве входных данных для другого класса, который содержит логику для выбора его подмножества. Подмножество является другим контекстом, перспективой сущности (игрока, толпы). Класс ContextSwitcher, который я показываю выше, - это, по сути, способ связать действия, выполненные в одном контексте, с действиями другого). Я посмотрю это видео! Еще раз спасибо.

mrak 18.03.2018 22:48

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

mrak 18.03.2018 23:09
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
4
5
95
0

Другие вопросы по теме