Pull to refresh

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

Reading time5 min
Views14K
Добрый день!

Меня зовут Сергей, я независимый разработчик игр. В сентябре 2014 года я поставил перед собой цель — реализовать игру во многом схожую с Hearthstone. Я долго размышлял перед тем, как взяться за этот проект: по силам ли мне это?! В тот момент задача казалась неподъемной для одного разработчика. Из титров к оригинальной игре было очевидно, что над ней трудятся не менее десяти человек, кроме того у 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 = unit
     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)
                               
     
 


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

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

Articles

Change theme settings