Первое знакомство с архитектурой коллекционной карточной игры «Last Argument»

Добрый день! Меня зовут Сергей, я независимый разработчик игр. В сентябре 2014 года я поставил перед собой цель — реализовать игру во многом схожую с Heartstone. Я долго размышлял перед тем, как взяться за этот проект: по силам ли мне это?! В тот момент задача казалась неподъемной для одного разработчика. Из титров к оригинальной игре было очевидно, что над ней трудятся не менее десяти человек, кроме того у Blizzard, есть уже сложившееся комьюнити и достаточно денег на маркетинг. Сама игра реализована по мотивам уже существующего игрового мира, что также несколько упрощает разработку самим близардам. Ничего из вышеописанных сопутствующих условий у меня нет и потому меня до сих пор преследуют сомнения относительно хоть кого-то ожидаемого успеха от данной инициативы. Тем не менее, я все таки взялся за этот проект — прежде всего потому, что мне нравятся коллекционный карточные игры и сама работа над подобной игрой приносит мне удовольствие. Я решил, что этот проект, так или иначе, даст мне возможность получить практический опыт в разработке подобных игр. Даже если с первой попытки мне не удастся трансформировать это в какое-то коммерчески успешное предприятие, общий совокупный опыт, полученный в ходе работы над этим проектом, даст мне возможность, в будущем, экспериментировать в этом жанре и, в конечном счете, я так или иначе все равно нащупаю ту оригинальную механику и сеттинг, которые позволят создать собственную игровую студию, специализирующуюся на разработке оригинальных коллекционных карточных игр.

В настоящий момент я работаю над игрой уже восьмой месяц и хотел бы этой публикацией начать цикл статей, который открывает некоторые нюансы того, как работают коллекционные карточные игры в принципе. Наверное, для многих опытных разработчиков эти статьи вряд ли смогут рассказать что-то принципиально новое; и все же, я надеюсь, что если вы никогда ранее не сталкивались с подобными играми — у вас проснется дополнительный интерес к ним и, возможно, вы начнете разрабатывать свою игру или, может быть, присоединитесь к данному проекту.

В своей работе я использую python 3.3, django, redis и tornado для серверной части проекта action script + robotlegs для клиентской. Я не исключаю того, что в ближайшем будущем я также начну писать клиент на С++ под Unreal Engine 4. До последнего времени я был сфокусирован на работе непосредственно над кодом, обеспечивающим игровую механику и потому на данном этапе для меня было не слишком важно, какую технологию использовать для написания клиентской части игры. Я просто выбрал то, что лучше знал.

Django используется для административной панели, которая позволяет настраивать эффекты при розыгрыше тех или иных карт, а также работает с запросами касающимися создания новых карт самих игроков и создания определенной колоды из того набора, который открыт у игрока. Сами матчи не используют базу данных — вместо этого они просто кешируют собственную модель в redis. Бой между двумя реальными игроками осуществляется через приложение tornado, использующее постоянное сокетное соединение между двумя клиентами.

Совсем коротко архитектура игры выглядит так:

image

У клиентов есть как сервисы для установления контакта с торнадо приложением в рамках какого-то конкретного матча, так и отдельные сервисы, касающиеся коллекции карт. Совершая те или иные ходы, один из клиентов информирует об этом сервер, на основании полученных данных сервер анализирует сделанное игроком действие и формирует игровой сценарий, который отправляет обратно обоим клиентам. Получив от сервера игровой сценарий, клиент отдает его команде, отвечающей за его проигрывание. В целом, плавное и комплексное проигрывание всех игровых событий происходит за счет двух рекурсивных функций: одна функция находится на сервере, она анализирует настройки той или иной карты, проходит по всем переменным конкретного эффекта и формирует массив игровых действий; вторая рекурсивная функция клиентская, последовательно проигрывает каждое игровое действие, обнаруженное в игровом сценарии.

Вероятно, я еще напишу отдельную статью о том, что представляют собой настройки той или иной карты. Сегодня я ограничусь лишь одним поверхностным примером, того как это может выглядеть в реальном матче: предположим, у игрока на руках есть карта с простой способностью «Провокация». Эта защитная способность вынуждает сначала атаковать существо со способностью «Провокация» и только после его смерти уже других существ и героя противника.

Изначально один из игроков оповещает сервер о том что разыграна та или иная карта. По индексу карты сервер определяет набор ее настроек и отдает настройки карты особому игровому реактору. Игровой реактор пропускает все настройки карты через свою рекурсивную функцию и возвращает уже готовый игровой сценарий. Конкретно способность «Провокация» срабатывает таким образом. Сама карта хранит описание способности в константах:

EtitudeType (Вид способности) EtitudePeriod (Момент срабатывания способности) EtitudeLevel (Уровень влияния способности) В нашем случае реактор будет пропускать все способности через период EtitudePeriod.SELF_PLACED. Это значит, что он пытается найти способность, которую необходимо активировать сразу же, как только фишка оказалась на поле. Как только он обнаружит эту способность, по уровню влияния он сможет понять, к кому нужно будет применить эту способность. В данном случае по константе EtitudeLevel.SELF он поймет, что способность нужно применить к самому существу, спровоцировавшему срабатывание этой способности. На третьем этапе рекурсивная функция установит тип способности EtitudeType.Provocation, далее реактор изменит характеристики этого существа в своей модели и сформирует игровой сценарий, указав индекс существа и способность, которую необходимо применить к этому существу. Сформированный сценарий реактор вернет торнадо приложению, в тот в свою очередь отдаст его своим соединениям.

Немного кода для полноты картины:

# match.py

def place_unit (self, index, attachment) unit = self.get_unit (index, attachment) scenario = [] reactor = new Reactor (scenario) scenario = reactor.place_unit (unit) return scenario # reactor.py

def place_unit (self, unit) self.initiator = initiator self.etitudes = initiator.etitudes[:] self.activate (EtitudePeriod.SELF_PLACED) return self.scenario

def activate (self, period): if len (self.etitudes): etitude = self.etitudes[0] del self.etitudes[0]

if period == etitude.period:

targets = self.get_targets (etitude.level) # some other etitudes … if etitude.type == EtitudeType.PROVOCATION: for target in targets: target.provocation = True action = {} action['type'] = 'provocation' action['target'] = {'index': target.index} self.scenario.append (action)

self.activate (period) На клиенте аналогичная рекурсивная функция перебирает компоненты игрового сценария, по индексу определяет, какой именно элемент (карта, существо) будет трансформирован и визуализирует тот или иной эффект в зависимости от его типа.

В общем, это все, что я хотел рассказать в своей вводной части. Благодарю за внимание!

© Habrahabr.ru