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

    Добрый день!

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


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

    В общем, это все, что я хотел рассказать в своей вводной части. Благодарю за внимание!
    • +7
    • 12,4k
    • 2
    Поделиться публикацией

    Комментарии 2

      +2
      Так а игра-то где сама?
        0
        Я пока ее не опубликовывал, есть только несколько видео роликов о том как игра выглядит в настоящий момент:

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое