python

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

  • среда, 29 апреля 2015 г. в 02:10:40
http://habrahabr.ru/post/256889/

Добрый день!

Меня зовут Сергей, я независимый разработчик игр. В сентябре 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 reactor.get_scenario()
 


# ractor.py

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

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)
                               
     
 


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

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