Я разрабатываю многопользовательскую онлайн-игру ради обучения.
Несколько клиентов подключаются к серверу, который содержит истинное представление игрового мира. Каждый клиент отслеживает подмножество этого представления. Я храню свои представления в 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. Еще я хочу подобрать здесь общий передовой опыт, потому что со временем я могу превратить его в частный сервер для другой игры, для которой у меня нет сетевого протокола.
Различные системы координат были просто примером. Действия могут выглядеть по-разному в зависимости от контекста, в котором они просматривались. Одна сущность может быть частично ослеплена и, таким образом, не может видеть, кто совершил действие, но для других сущностей это может быть совершенно ясно. Контекст сущности помогает преобразовать «истинное» действие в соответствии с его восприятием мира. Я ценю совет, но если бы я использовал Serializable, я бы решил только половину проблемы, поскольку мне все еще нужно иметь возможность переключать контексты и находить необходимые действия.
Лучше решить половину проблемы, чем ничего не решить. :-) Но также, когда вы описываете сущности в разных состояниях и как они реагируют на ввод, это звучит так, как будто попытка поместить всю эту логику в один большой Контекст также может быть частью проблемы. Посмотрите, как заставить сущности реагировать на ввод в зависимости от их собственного состояния. C.f. Видео Марка Брауна о "системных" играх: youtube.com/watch?v=SnpAAX9CkIc
Ага! Это хороший ответ, но я все же хотел бы избежать Serializable (см. Мою правку в вопросе). Извините за то, что не понял этого с самого начала. О контексте: это просто набор переменных состояния игрового мира. Глобальный контекст используется в качестве входных данных для другого класса, который содержит логику для выбора его подмножества. Подмножество является другим контекстом, перспективой сущности (игрока, толпы). Класс ContextSwitcher, который я показываю выше, - это, по сути, способ связать действия, выполненные в одном контексте, с действиями другого). Я посмотрю это видео! Еще раз спасибо.
Я посмотрел видео, и, хотя он мне показался довольно интересным, но я не уверен, что это решение моей проблемы. Входные данные сущности, которые он описывает, кажутся аналогичными тому, что я называю локальным контекстом. В случае объекта игрока контекст также будет использоваться для рендеринга. :)




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