Предыдущая часть:
Рассмотрим момент походовой игры.
- Эму атаковал игрока
- Здоровье игрока уменьшилось на 10
- Игрок атаковал эму
- Эму умер
Это один ход игры, но в нём произошло несколько действий.
Cервер их выполнит, и в результате получится конечное состояние: игроку поплохело на 10 здоровья, а эму исчез. Это состояние передаётся в клиент. Но игрок не имеет понятия, как оно получилось.
Значит, дополнительно сервер должен передать лог действий – что, с кем и когда произошло. Так и сделано в Rogue.
You miss, the emu misses – Вы промахнулись, эму промахнулся. Это лог обмена ударами.
Но наш клиент задуман таким, что в нём допускается, например, анимация ударов. Сначала игрок машет мечом и промахивается, затем эму пытается клюнуть игрока в ж и тоже промахивается.
Для того, чтобы отрисовать эти действия, клиенту уже недостаточно обычного текстового лога. Он должен знать, что где-то на карте есть объект "эму".
Значит, сервер в логе должен передать ещё служебную информацию о том, кто где находится и кто кого бьёт.
В схеме с раздельными клиентом и сервером это будет прекрасно работать:
- Сервер меняет состояние у себя
- Сервер передаёт лог изменений клиенту
- Клиент отрабатывает по логу, меняя состояние у себя
Но в игре, которая работает целиком на локальной машине, напрашивается делать не отдельные состояния у клиента и сервера, а одно общее.
И сразу же возникает проблема
Допустим, после совершения хода на сервере эму уже нет, он умер. Значит его нет и в состоянии, которое общее у клиента и сервера – сервер уже изменил его. Но в логе, переданном клиенту, эму есть. Клиенту нужно сыграть анимацию, как игрок бьёт эму или эму бьёт игрока. Но идентификатор объекта "эму", переданный сервером в этой части лога, уже невалиден.
Решение – по-прежнему держать два отдельных состояния у клиента и сервера. Но это решение на крайний случай, которое будет использовано, если нет никаких других. А пока можно поискать другие.
Фантомный эму
В лоб здесь ничего особо не придумывается. Допустим, сервер рассчитывает изменения, но не меняет состояние. Тогда изменения передаются на клиент, и состояние меняет уже он, применяя эти изменения.
Здесь плохо всё: во-первых, как серверу что-то обсчитывать, не меняя текущее состояние? Ему всё равно понадобится какая-то рабочая копия. Во-вторых, если клиент будет изменять состояние, которое должен изменять сервер, это будет несвойcтвенная для него функция.
666666666666666666665+
– написала моя собака носом, и я оставлю это здесь на память
Опущу все безуспешные попытки придумать что-то годное за несколько дней, и напишу финальный вариант, которым я не очень доволен, но надо двигаться дальше.
Я решил разделить обновления на два типа: когда клиент может воспользоваться текущим объектом, и когда нет. Например: эму атаковал игрока, но при этом никто не сдвинулся с места и не умер. Значит, клиент может играть анимации с тем, что у него есть сейчас.
Если же кто-то сдвинулся, сервер будет передавать фантомный объект, который соответствует состоянию до обновления. Клиенту всего лишь надо взять его не из финального состояния, а прямо из обновления.
Имея этот фантомный объект в его оригинальной позиции и состоянии, клиент может отрисовать обновление и прийти к финальному состоянию.
Точки изменений
Изначально мы имеем такой расклад: сервер меняет своё состояние, клиент меняет своё состояние.
Клиент, отделённый от сервера, просто обязан менять состояние на своей стороне, потому что оно – отдельное и его собственное. В то же время клиент, имеющий общее состояние с сервером, не должен ничего менять, так как это сделает сервер.
Необходимо, чтобы клиент работал в любом случае одинаково и ничего не знал о том, в какой конфигурации он запущен. Следовательно, нужно сразу отказаться от того, чтобы клиент менял какие-либо состояния. Но тогда, когда нужно будет это сделать, ему понадобится помощник. И этом помощником будет... правильно, клиентский прокси.
Тогда получается следующая схема:
- Сервер меняет состояние и передает на прокси изменения состояния и лог событий
- Прокси (в зависимости от конфигурации) либо ничего не делает с состоянием, либо применяет к нему изменения, и далее отдаёт лог событий клиенту.
- Клиент отрисовывает события, используя объекты из текущего состояния, либо фантомные объекты из лога.
Команды
Можно заметить, что изменением состояния занимаются две сущности: сервер и прокси. Очевидно, что эти изменения делаются идентично. Нам придётся писать в сервере и в прокси соответствующие методы, которые будут работать одинаково.
Либо же мы можем воспользоваться шаблоном проектирования Команда и формировать команды для изменений. Как говорилось в описании этого шаблона, команду можно даже передавать по сети, чем мы можем воспользоваться.
Итак, финальная схема:
- Сервер генерирует команду для изменения состояния.
- Сервер запускает команду, она меняет состояние
- Эта же команда передаётся в прокси (через сериализацию параметров)
- Прокси восстанавливает команду и тоже запускает её
- Команда отрабатывает и меняет состояние на стороне прокси.
Таким образом, для изменений на любой стороне нам требуется только один класс команды, а для разных конфигураций клиента-сервера мы можем сделать два варианта прокси – один будет получать и запускать команды, а другой просто игнорировать их.
Более подробно будет видно дальше, когда будет написан соответствущий код.
Читайте дальше: