Найти тему
ZDG

Пирог #1. Структуры данных для клиента и сервера

Оглавление

Вся подборка по рогаликам

Сделаем самую примитивную реализацию клиент-серверной игры для тестирования архитектуры.

  • Стало однозначно понятно, на каком языке писать. На Питоне, потому что тогда игру можно назвать Пирог (PyRogue).

Если вам нужно освежить знания по ООП в Питоне:

ООП в Python: особенности реализации
ZDG10 октября 2020

Нам понадобятся следующие структуры данных: GameState, Request, Response, Command.

Это краеугольные камни, на которых будет громоздиться вся немыслимая сложность игры. Поэтому разобраться с ними надо хорошенько.

GameState

-2

Хранит состояние игры. В дальнейшем там будет куча всего, но сейчас нужен максимальный примитив. Атрибут map – карта уровня в виде строки, где @ обозначает позицию игрока:

--------@--------

Ходить можно только влево и вправо. Это частный случай двумерной карты, так что всё в порядке.

У GameState есть метод set_data(), чтобы обновлять данные. Это делается без затей – map просто переприсваивается.

Request

-3

Чтобы совершить действие, клиент должен послать серверу запрос на выполнение действия. Запрос оформляется в виде класса Request, который должен быть понятен серверу. В текущем виде он имеет два атрибута: type сообщает серверу, какого действия от него хотят, а params, если необходимо, предоставляет дополнительные параметры.

Клиенту сейчас доступны только два действия – ходить вправо и ходить влево. Поэтому у Request есть две константы для type: MOVE_L и MOVE_R.

Клиент может сразу генерировать класс Request, но это не обязательно. Транслировать любой запрос клиента в класс Request – задача прокси.

Response

Сделав обновления в GameState, сервер возвращает ответ в виде списка объектов класса Response.

-4

У Response тоже есть тип (type) и структура дополнительных данных (data). Пока что сервер возвращает всего два типа ответов: INIT в самом начале и UPDATE в дальнейшем, а data у них это просто карта из GameState.

Класс Response должен быть понятен клиенту. Хотя сервер может сразу генерировать его, это не обязательно. Переделывать ответы сервера в объекты класса Response – также задача прокси.

Зачем нужны Response?

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

Но даже в локальном варианте они будут нужны.

Рассмотрим такой случай. Игрок перешёл из клетки в клетку, а рядом оказались крыса, снеговик и сонная тетеря. За один ход произошли следущие события:

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

Конечный итог, который присутствует в GameState:

  • крыса исчезла с карты
  • изменились статусы игрока – у него уменьшилось здоровье, он болеет чумой, спит и заморожен

Если просто отрисовать текущее состояние игры, оно будет корректным, но будет непонятно, что и почему произошло.

Другое дело, когда у нас есть список из объектов Response. Первый описывает, как игрока укусила крыса. Второй описывает, как игрок задавил крысу. И так далее. Если мы отрисуем финальное состояние и также выведем все полученные Response в лог, то игрок сможет по логу понять, что случилось.

При этом трактовать Response можно очень широко. Например, клиент может не выводить сообщения в лог, а изображать в анимированном виде, как мышь атакует игрока. Все необходимые для этого данные должны быть в Response – кто атаковал, кого атаковал, каким способом атаковал, с каким результатом. Даже тексты для лога могут или приходить от сервера, или генерироваться клиентом самостоятельно на основе данных в Response.

Command

Если Request это запрос на выполнение действия, то Command – это непосредственно действие.

-5

Класс Command делается в соответствии с шаблоном проектирования Команда, то есть инкапсулирует в себе всё необходимое, чтобы совершить какие-то изменения в GameState.

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

Клиент сейчас может послать два типа Request: MOVE_L и MOVE_R. Оба типа запросов транслируются в класс MoveCommand, отнаследованный от Command:

-6

В качестве ресивера, то есть того объекта, над которым производятся изменения, используется атрибут game_data. Было бы вернее назвать его именно receiver, так как команды могут работать с чем угодно. Но сейчас они работают с только с game_data.

Метод execute() исполняет команду. Команда смотрит на свой дополнительный параметр: если он равен 'L', то смещает игрока влево. Если 'R', то вправо. И обновляет game_state.map.

Теперь мы можем написать ещё тысячу команд, и все они будут работать, и нам не надо будет каждый раз лезть в код сервера и что-то там ломать по пути.

Кроме того, каждая команда после работы будет возвращать Response, и у нас будет чёткий механизм получения результатов.

В продолжении будет разработка непосредственно клиента и сервера, которые уже написаны и работают:

-7

Читайте дальше:

Наука
7 млн интересуются