company_banner

Пользовательские плагины в JavaScript играх

    Wargaming сейчас разрабатывает тактическую карточную игру WoT: Generals. Веб-версия написана на JS, используются LibCanvas и AtomJS. Я принимал непосредственное участие в разработке и хочу рассказать про функционал, который мне кажется интересным и может быть полезным во всех веб-играх. А именно — о системе плагинов игры, которая вдохновлялась пакетными менеджерами в Linux и имеет следующие возможности:

    — История изменений плагинов
    — Автоматическое обновление плагина при обновлении версии игры
    — Разработка плагинов на localhost
    — Неограниченное количество веток, например, для нестабильных версий
    — Зависимости (плагин А автоматически подключает плагин Б)
    — Встроенная возможность делать паки (следствие предыдущего пункта)
    — Легкое изменение любой части клиента игры
    — Полный административный контроль авторов игры над всеми плагинами
    — Поиск по базе плагинов
    — При этом простая установка юзером и удобная работа для плагинописцев.

    Игра


    Генералы — это тактическая карточная игра. Думаю, многие знакомы с Magic: The Gathering. Основной геймплей — это поле боя 5*3, где задача — карточками танков, взводов и приказов уничтожить штаб противника.



    Кроме экрана боя, есть такие экраны, как «Ангар» для выбора колоды, с которой вы пойдёте в бой, «Дерево исследований», “Редактор колод” и так далее.



    Объекты, с множеством анимаций (вроде карусели в ангаре, дерева исследований и, конечно, боя) написаны на LibCanvas и отрисовываются на html5 canvas. Интерфейсы попроще пишутся на html.

    Про систему плагинов


    Плагины — однозначно полезный функционал.

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

    И хотя официального релиза у нас ещё не было, но система плагинов работает уже полтора года. Я долго думал над тем, какими они должны быть. И ниже я расскажу историю, как и к чему мы пришли в итоге.

    Конечно, можно написать API, но мне не нравилась ограниченость такого подхода фантазией программиста и увеличенными затратами на поддержку. Хотелось иметь возможность изменить любую часть клиента игры.

    К счастью, это довольно просто по двум причинам:

    — Клиент очень тонкий и занимается только отображением — вся логика вынесена на сервер, который никак не подвергается влиянию плагинами
    — Все чувствительные операции вынесены на сторонние сервера — авторизация (где мог бы утечь пароль) и платежка (где юзер мог потерять деньги).

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

    Имеется три способа изменения поведения игры:

    1. Использование ограниченного API


    При создании плагина ему передается инстанс объекта Wotg.Plugins.Simple с базовыми методами, которые позволяют выполнять самые простые операции — подмена картинок, смещение элементов, изменение звуков и т. д. Такие операции используются для простых плагинов:



    2. Подписка на события


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

    3. Агрессивное изменение


    Это самый сложный но и самый глубокий метод ( если вы понимаете о чём я ;) ). Он позволяет изменить любой метод любого класса с возможностью вызова предыдущего варианта. Для примера посмотрите ниже часть плагина, которая позволяет сохранять реплеи на стороннем сервере. В этом случае изменяется метод save у ReplayManager.

    plugin.refactor( Wotg.Utils.ReplayManager, {
    
    	save: function method (battle) {
    		method.previous.call(this, battle);
    		
    		var replay, xhr;
    		
    		replay = this.getCompiledDataFrom(battle);
    
    		xhr = new XMLHttpRequest();
    		xhr.open("POST", MY_OWN_REPLAYS_SERVER, true);
    		xhr.send(
    			"player=" + replay.player.name +
    			"&opponent=" + replay.opponent.name +
    			"&replay=" + JSON.stringify(replay)
    		);
    	}
    	
    });
    
    


    Это дает возможность изменить любое поведение, вплоть до написания нового функционала, как, например, внутриигровой генератор карт:



    Вот такая она с точки зрения кода плагина. Но как все это организовать в плане подключения?

    История


    Изначально (ещё пару лет назад) было решено использовать встроенные в браузер аддоны и официальные магазины вроде chrome.google.com/webstore и addons.mozilla.org/uk/firefox. Но с этим были проблемы. Такие плагины было тяжело разрабатывать, пока игра не зарелизилась — их нельзя добавить в магазин и пользователям. В результате игрокам приходилось копипастом добавлять куски кода, вывешенные в теме на форуме игры. Использование unsafeWindow вместо классического window вносило путаницу для разработчиков. А потом с unsafeWindow появились дополнительные запреты. В общем — темные времена и стало ясно, что необходимо куда-нибудь двигаться.

    Хотелось чего-то прогрессивного и удобного. И потому было решено использовать GitHub. Один репозиторий на коммиты и пул-реквесты в репозиторий игры. Удобность работы выросла значительно, но возникло две проблемы — очень долгое обновление GitHub Pages и отсутствие возможности удобного администрирования. Зато было очевидно, что направление движения правильное. И уже было понятно, где наша земля обетованная.

    GitLab


    Мы подняли GitLab на нашем сервере и выделили его полностью под плагины. И это было божественно. Схема следующая:

    — Есть пользователи GitLab — наши плагинописцы
    — У каждого пользователя есть репозитории — по одному на каждый плагин
    — Репозитории могут иметь несколько бранчей. Например, master и unstable. Master включается по умолчанию.

    В итоге мы получаем следующие возможности:

    — Весь репозиторий находится под нашим административным контролем и имеет общий с нашим сервером Uptime.
    — Формат имени Owner:Title:Branch. Например, Shock:MyCoolPlugin — основной плагин, а Shock:MyCoolPlugin:Unstable — версия для разработки, которая мерджится в мастер по факту полной готовности. Путь к файлу определяется шаблоном gen-git.socapp.net{author}/{title}/raw/{branch}/{title}.js. Это дополнительно облегчает совместную работу над плагинами — один разработчик ответвляет от репозитория другого, получает плагин с таким же именем, но иным автором. Тот вносит свои изменения, может даже дать установить свой плагин пользователям, а потом создает пул-реквест в основной плагин.
    — При установке информация о плагине записывается в localStorage и его основной файл подключается каждый раз после загрузки всех классов но до вызова точки входа.
    — Каждый плагин имеет версию игры, для которой он предназначен, и при выходе обновления автоматически выключается, пока автор не изменит версию в соответствующей ветке, и тогда плагин снова автоматически включится для всех пользователей, у которых он установлен. При этом следующую версию можно заранее подготовить на супертесте и просто во время выхода обновления вмерджить её через веб-интерфейс одной кнопкой.
    — В коде плагина достаточно написать require, и автоматически подтянутся необходимые плагины (зависимости). Они подтянутся в корректном порядке и будут доступны из тела плагина как показано ниже.
    — Также можно при помощи include включать дополнительные классы плагина. Приблизительно так:

    new Wotg.Plugins.Simple({
    	version: '0.6.0',
    	require: [ 'Another:Plugin' ],
    	include: [ 'AnotherClass' ],
    }, function (plugin, events, required) {
    	console.log( required['Another:Plugin'] ); // Ссылка на необходимый плагин
    	console.log( plugin.included['AnotherClass'] ); // Ссылка на необходимый класс
    });
    

    — При помощи команды plugin.addStyles( 'my.css' ) подключить свои стили.
    — Каждый плагин имеет свои собственные настройки, которые можно получить командой plugins.getConfig( 'index' ).
    — Самые простые плагины можно изменять через веб-интерфейс GitLab, а более сложные благодаря git clone разрабатывать локально. Для этого достаточно по шаблону сконфигурировать nginx Благодаря настройкам в игре, соответствующая директория станет использоваться как репозиторий плагинов. И тогда любое изменение в директории подключенного плагина будет отображаться без коммитов в игре разработчика.
    — У GitLab есть API. Был зарегистрирован фейковый пользователь и благодаря его приватному токену любой веб-клиент игры может отправлять запросы в API. Это позволяет сделать поиск плагинов, валидацию названий и т. п.

    # plugins add Test:CardCreate
    No such plugin. Did you mean:
      - Shock:CardCreate 
    
    # plugins add Test:Ca
    Min plugin title length is 4: Test:Ca
    
    # plugins add Test:Card
    No such plugin. Did you mean:
      - Shock:CardCreate 
    
    # plugins add Test:Exa
    Min plugin title length is 4: Test:Exa
    
    # plugins add Test:Exam
    No such plugin. Did you mean:
      - Shock:Example 
      - Isk1n:Example
    
    # plugins add Shock:Unknown
    No such plugin, I dont know, what you want to install
    
    # plugins add Shock:Example:Test213
    No such plugin. Did you mean:
      - Shock:Example:master 
      - Shock:Example:new-test
    


    Console vs GUI


    На данный момент всё управление и установка плагинов производится через внутриигровую консоль, которая открывается по Ctrl ~. Всем достаточно удобно (хотя иногда пользователи спрашивают, где тильда). Но в далёких планах есть создание GUI — из localStorage получаем список установленных плагинов, а благодаря GitLab API реализуем их поиск и отображение информации о них.

    Если кому интересно — можно почитать документацию для разработчиков, посмотреть раздел плагинов, или посмотреть как всё это выглядит в WoT: Generals.

    Я специально сюда не включил реализацию именно в плане кода, т. к. она довольно проста. Для меня, самое интересное — это именно использование GitLab как пакетного менеджера. Но если есть вопросы к технической реализации или к идее — жду в комментариях.
    Wargaming
    84,32
    Компания
    Поделиться публикацией

    Похожие публикации

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

      +5
      Зашёл на веб-версию, просит зарегаться. Блин, опять выдумывать логин-пароль. О, есть кнопка логина через G+! Жму, разрешаю права, и… получаю ту же форму регистрации с предзаполненными полями мыла и логина, но надо всё равно придумывать пароль. Шта??? Всё, на этом игра для меня закончилась, конверсия не произошла.
        +1
        У меня получилось не через создание аккаунта, а через линк «Войти», пароль не потребовали —
        вот так
        Раз:
        image

        Два:
        image

        Три:
        image
        +2
        Идея восхитительна!

        Вы с самого начала задумывали систему плагинов, учитывая её при разработке, или пришли к ней позже?
          +1
          Спасибо) Я с самого начала хотел плагины, но пришёл к ним позже.
          На самом деле много кода пришлось переписать (эволюционно, мелкими итерациями) и ещё много — переписывается.
          Основная задача (для возможностей «Агрессивного изменения») — это максимально минимальные методы. Чем меньше — тем лучше. (прям как в книгах пишут). Чем меньше метод — тем меньше влияния оказывает изменение автора плагина и легче переносится на следующую версию. Потому у нас практически не создаются функции в методах, а сначала мы перешли на bind, а потом вообще на подход с передачей объекта с нужными методами. Пример:
          // Было
          declare( 'Handler', {
            run: function () {
              this.button.addEvent( 'click', function () {
                // do stuff
              });
            }
          });
          
          // Первая замена
          declare( 'Handler', {
            run: function () {
              this.button.addEvent( 'click', this.onClick.bind(this) );
            },
          
            onClick: function () {
                // do stuff
            }
          });
          
          // Вторая замена
          declare( 'Handler', {
            run: function () {
              this.button.addListener( this );
            },
          
            onClick: function () {
                // do stuff
            }
          });
          


          Но, по сути, когда прикрутилась система плагинов — это всё постепенно улучшалось в рамках разработки задач и ежедневного рефакторинга старого кода.
          0
          Эм… Ждите ботов на свою игру :D Которые и играть будут лучше чем 51% игроков =)
            0
            Боты — не вопрос API. Точно так же можно написать через распознавание картинок (благо, достаточно сравнивать по паттерну) и кликам.
              0
              Распознавание картинок дело сложное и неблагородное. А вот подключится напрямую к методам это уже очень и очень круто. И никакая обфускация тут не поможет.
              Просто я имею небольшой опыт написания ботов, и вот эта игра выглядит как очень хорошо и легко автоматизируемая, причём абсолютно безпалевно.

              Не представляю себе как WG будет с этим бороться.
                0
                Почему бы нет? Первая игра, в которой будут развлекаться программисты, а не задроты.
                  0
                  На самом деле я тоже имею кое-какой опыт в этом и знаю, что обфускация — это вообще не решение))
                  Кроме парсинга картинок и кликов (я его сказал как крайний случай когда всё остальное закрыто) можно проанализировать запросы-ответы сервера и представляться настоящим клиентом. Без всякого API. Небольшой скриптик, скажем, на node.js.

                  Я к тому, что вопрос ботоводства и вопрос API — независимые вопросы).

                  Способы борьбы с ботами, к сожалению, обсуждать не могу.
              +1
              жду статьи о технической стороне )
                0
                Там на самом деле нечего писать — всё самое интересное уже тут, а осталось только очевидное, как на меня. Если есть вопросы — я лучше на них тут отвечу)
                  0
                  я вообще не могу понять как вы функции подменяете
                    +2
                    Ну нас самом деле в JavaScript это проще простого. Вот, допустим, есть класс в игре:

                    function GameObject (name) {
                       this.name = name;
                    }
                    
                    GameObject.prototype.getName = function () {
                       return 'Getter:' + this.name;
                    };
                    


                    Пишем в нашем API подменятор:
                    Plugins = {};
                    
                    Plugins.refactor = function (Class, methodName, newCallback) {
                       // чтобы можно было обратиться к пред версии
                       newCallback.previous = Class.prototype[methodName];
                    
                       Class.prototype[methodName] = newCallback;
                    };
                    


                    Теперь в нашем плагине просто подменяем метод:
                    Plugins.refactor( GameObject, 'getName',
                       function method () { // обратите внимание - сохранил ссылку на колбек
                          return 'Changed:' + method.previous.call(this);
                       });
                    


                    Пробуем:
                    // Changed:Getter:FooBar
                    console.log( new GameObject('FooBar').getName() );
                    
                0
                Эхх. Очень нравятся такие речи:
                о системе плагинов игры, которая вдохновлялась пакетными менеджерами в Linux

                Или вот, например, статья, что Linux Foundation объявила о включении в свои ряды компанию Wargaming.
                Пруф. (правда старенький, но я думаю ничего не изменилось, правда ведь?)

                Внимание вопрос. Почему компания, входящая в Linux Foundation и берущая идеи из Linux, до сих пор не имеет нативного клиента для своей игры?
                Зато какие пафосные речи:
                “Our decision to join The Linux Foundation was influenced by the accelerated change of the game industry,” said Maksim Melnikau, Developer at Wargaming. “Having been in this genre for almost 15 years, we seek to reap enormous value from such a strong community and for Linux to advance the gaming industry.”

                Да, в этом вы проигрываете Gaijin Entertainment, которая просто берет и делает. Хотя-бы клиент под Linux.
                Про внутреннюю механику игр и тему доната затрагивать не хочу. Просто как-то не честно к пользователям и игрокам, не находите?
                  0
                  у них движок не кросс платформенный вроде бы.
                    0
                    Почему компания, входящая в Linux Foundation и берущая идеи из Linux, до сих пор не имеет нативного клиента для своей игры?

                    Я как-бы с Linux прекрасно играю в нашу игру и разрабатываю её. Более того — скриншоты в топике сделаны как раз в Kubuntu.
                      0
                      То, что вы используете костыли и велосипеды для игры и разработки, не делает игру лучше
                      image

                      Т.е. это как бы не оправдание.
                        0
                        Думаю, вы путаете игру, что обсуждается в этом топике (WoT Generals — карточная игра типа Хартстоуна), и сам WoT. :)
                          0
                          Нет, никогда такого не было!
                          Я изначально наезжал на Wargaming и WoT, про WoT Generals я ни одним словом не заикнулся :)
                            +1
                            Так а топик то про WoT Generals.

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

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