Предыдущая часть:
Требуется разработать модули клиента и сервера. Пока что я не использую никакой язык, так как проектирование будет чисто на архитектурном уровне. Программирование начнётся не ранее, чем будет готова архитектура.
Итак, чем занимаются клиент и сервер?
Клиент
- Получает состояние игры
- Отображает на экране состояние игры
- Обслуживает ввод с устройства игрока (клавиатура, мышь и т.д.)
- Транслирует ввод игрока в запросы к серверу
Сервер
- Владеет состоянием игры и игровой логикой
- Отправляет состояние игры на клиент
- Получает запросы от клиента
- Меняет состояние игры в соответствии с запросами и игровой логикой
- Отправляет клиенту частичные обновления состояния игры
Тут надо отметить, что раз они называются клиент и сервер и что-то друг другу отправляют, то взаимодействие между ними происходит по идее через сеть. Это одна из реализаций, но она не обязана быть именно такой.
Взаимодействовать могут и две части одной программы на одном и том же компьютере без всякой сети. Главное здесь то, что они концептуально отделены друг от друга. Клиент ничего не знает об игровой логике, а сервер ничего не знает о том, как отображается состояние игры и как обслуживаются устройства ввода игрока.
Состояние, Запрос, Ответ
Из вышеперечисленных списков можно выделить сущности – Состояние игры, Запрос клиента, Ответ сервера.
Состояние игры – назовём его классом GameState – это совокупность всех игровых параметров на данный момент. Текущая карта уровня, открытые клетки, параметры игрока, местонахождение монстров и т.д.
Клиент в начале игры должен получить GameState, чтобы что-то нарисовать.
- В игре, сделанной традиционным способом в виде единой неделимой программы, вопросов бы не было. Состояние игры доступно из любого места программы. Одна часть кода его меняет, и все изменения сразу же становятся доступны для другой части кода.
- Если клиент и сервер логически разделены, но находятся в одной программе, проблем по-прежнему нет. Они оба имеют доступ к GameState, но теперь клиенту можно его только читать. Чтобы изменить состояние, он должен вызвать метод сервера, сервер внесёт изменения, и потом клиент может считать эти изменения прямо из GameState.
- Если же клиент и сервер реально удалены друг от друга, начинаются проблемы. Теперь актуальный объект GameState находится на отдельном сервере, и клиент не может его даже читать. Он должен открыть канал связи с сервером, послать какой-то запрос и получить ответ, в котором будут содержаться данные о GameState.
Далее, при каких-то действиях в клиенте он может каждый раз получать GameState целиком для обновления. Но по факту придётся гонять по сети слишком много данных (если, конечно, планировать с размахом) каждый ход, что выглядит неправильным. Поэтому нужен какой-то протокол, который пересылал бы не состояние целиком, а только изменённые данные. И клиент должен уметь применять эти частичные изменения к своей текущей копии GameState.
Это радикально другой способ обмена, поэтому версии клиента с локальным и удалённым сервером не будут совместимы как по методам вызова, так и по представлению данных. Но нас это не устраивает.
Прокси
С самого начала мы хотели, чтобы клиент вообще не знал, с каким именно сервером он работает, и с сервером ли.
Поэтому между клиентом и сервером я внедряю посредника – Прокси. Его задача – транслировать запросы клиента на сервер и ответы сервера обратно на клиент. При этом ни клиент, ни сервер не должны знать, с каким каналом связи они работают.
Таким образом, меняя прокси, можно менять каналы связи. А все необходимые преобразования структур данных будут происходить внутри конкретного прокси. Он будет приводить их к общему формату, а клиент и сервер будут работать так, как будто они находятся в одной программе.
Посмотрим на схему взаимодействия, когда всё действительно находится внутри одной программы.
Прокси здесь – общий объект для клиента и сервера. И также у них общее GameState. Клиент знает о прокси и GameState, сервер знает о прокси и GameState, а прокси знает о сервере и клиенте.
Клиент отправляет запрос на прокси, чтобы что-то сделать. Прокси перекидывает запрос на сервер. Сервер что-то делает, меняет GameState и отправляет прокси ответ. Прокси перекидывает ответ клиенту. Клиент берёт изменённое GameState и рисует что-то.
Таким образом, прокси выполняет роль минимальной прокладки, просто перекидывая запрос от клиента к серверу и обратно, и не делая больше ничего. Здесь он выглядит лишним, но не будем забывать, что всё может измениться, а клиенту с сервером категорически нельзя пересекаться.
Теперь посмотрим схему сетевого взаимодействия, когда клиент и сервер находятся на разных машинах:
Здесь два экземпляра прокси, по одному на каждой машине. Они связаны друг с другом через некое соединение (connection), по которому идёт обмен данными.
Оригинал GameState находится на сервере, и к нему имеют доступ сервер и серверная прокси.
Серверная прокси через канал связи передаёт на клиентскую прокси данные о GameState, в результате чего на клиентской машине есть своя копия GameState, к которой есть доступ у клиента.
В данной схеме о наличии канала связи знают только прокси. А клиент с сервером имеют доступ каждый к своей копии GameState и к своему прокси и думают, что работают с общим прокси и общим GameState, как в первой схеме.
Прокси берут на себя проблемы передачи данных и синхронизации копий GameState. Это действительно проблемы, так как для сетевого соединения нужно кодировать и раскодировать игровое состояние в специальный формат. Но если лезть в сеть, формат будет нужен, здесь просто некуда деваться. Зато в клиенте и сервере менять ничего не надо.
При этом клиент может не иметь (и более того, не должен иметь) полной копии GameState. Например, на сервере есть уже полностью заполненная карта подземелья, но в клиентской копии есть только её часть.
Вышеприведённая схема кажется мне рабочей, но чтобы убедиться в отсутствии проблем, я следующим шагом сделаю простейшие клиент и сервер, работающие именно через сеть. Хотя я не планирую, что игра будет сетевой, этот тест должен показать, что всё работает как ожидается, и можно будет смело продолжать.
Читайте дальше: