Очередь событий в карточной игре + основы Angular

Доброго дня, новички, сегодня мы попытаемся переделать нашу игрушку, разучивая основы новых для нас «технологий»:

  • AngularJS
  • DataBoom


На ангуларе попробуем переписать основные части нашего приложения, чтобы разобраться, что это такое и с чем его едят. Поэтому в первой части я постараюсь наиболее подробно описать вам курс, чтобы вы не разбились о подводные камни, которые в немалом количестве содержатся на вашем пути знакомства с angular.

Ну, а во второй части с помощью DataBoom создадим замечательную очередь событий, как в оригинальной игре (напоминаю, что делаем по образу и подобию HeartStone). Забегая вперед скажу, что в следующий раз мы вообще избавимся от php сервера, и полностью перейдем на Databoom, но это уже совсем другая статья…

image


AngularJS


Начать стоит с того, что для инициализации ангуляр-приложения вам недостаточно будет просто подключить библиотеку и создать модуль в js файле. Для работы с angular вам потребуется работа с вашими html-файлами как с представлением (view), что, на мой взгляд, лучше для разделения представления и контроллера.

В ангуляре есть такая сущность как директивы — особые html-атрибуты, и одним таким мы воспользуемся для инициализации нашего приложения. Приложение может не охватывать всю страницу, а только отдельный блок на ней, все зависит от того, где вы его инициализируете. Инициализация происходит с помощью директивы ng-app:




Это говорит о том, что этот элемент и все ниже по дереву и есть представление ангуляр. Управлять всем этим мы будем с помощью контроллеров, которые тоже надо инициализировать подобным образом:


Контроллеры в свою очередь не висят в воздухе, а привязаны к модулям. Само приложение тоже является модулем (главным, точкой входа в приложение), и, чтобы задать главенство одного из модулей, его название надо указать в директиве ng-app:




Модули создаются методом module объекта angular:

angular.module('Имя модуля', ['зависимость_1'])


Но сам по себе модуль мало полезен без контроллеров. Чтобы создать контроллер на нашем главном модуле, который мы определили точкой входа приложение (например), необходимо на объекте модуля вызвать метод controller:

myModule.controller('Имя контроллера', ['$scope',function($scope){

                $scope.var = 'some';
                $scope.foo = function(){};

        }]);


Объект $scope задает все переменные и функции области видимости контроллера, то есть то, что мы можем использовать в представлении. В данном случае в наших html-файлах мы можем работать с var и foo.

Вывод значений переменных осуществляется с помощью двойных фигурных скобок {{var}}, то есть:

html
{{var}}


выведет «some».

Ближе к сути


Разбираться с остальными тонкостями будем сразу на примерах, т.к. по ангулару есть некоторые статьи, хотя документация на angular.ru не вполне понятная (мне лично).

Первый подводный камень мы встретим, когда попытаемся сделать приложение модульным (на примере requirejs). Если сразу же прописать в html директиву ng-app, а после подключить библиотеку ангуляр методом require, то мы обнаружим, что ничего не работает. Так происходит потому, что DOM дерево на момент подключения библиотеки уже составлено.

На такой случай у объекта angular есть метод bootstrap:

    require(['domReady!'], function (document) {
        ng.bootstrap(document, ['App']);
    });


Таким образом мы привязываем модуль App как точку входа в приложение к document.

Первое, что мы переделаем в нашей игрушке — это меню, а именно единственное, что там есть — список игроков.

Код
define(['angularControllersModule', 'User', 'Directive'],function(controllers, User, Directive){

        /////////// Контроллер userList
        controllers.controller('userlistCtrl', ['$scope',function($scope){

                $scope.userlist = []; // Список игроков, пока пустой
                $scope.isMe = function(name){ // Функция
                        if (name == User.login) {
                                return 'me';
                        };
                }
                $scope.letsFight = function(name){ // Функция, которую будем вызывать по клику
                        return Directive.run('figthRequest',name);
                }

        }]);

        return controllers;

})


Здесь метод letsFight () (приглашение на бой) будет вызываться по клику на соответствующую кнопку около каждого игрока. В ангуларе это задается директивой ng-click:

        
  • {{user.name}}

  • Обратите внимание на разные вызовы функций: с фигурными скобками и без. Разница в том, что выражение в фигурных скобках вычисляется сразу, не дожидаясь каких-либо действий от пользователя, поэтому во втором случае мы фигурные скобки не применяем, тк надо, чтобы функция вызывалась только по клику.

    Директива ng-repeat работает аналогично foreach в php или for of в ES6 — перебираются все объекты списка. Директива указана в теге

  • , что означает, что повторять мы будем именно его столько раз, сколько у нас элементов в массиве userlist.

    Но изначально наш список игроков пуст, а получаем мы его по websockets с сервера. В приложении я попытался разделить библиотеки, модули и некие простые наборы инструкций (я назвал их Directives) для того, чтобы безболезненно иметь возможность их переписать.

    Более того, у нас есть отдельный обработчик этих директив (Directives.js), который просто вызывает нужные директивы по имени, которое мы получаем откуда угодно, например по websockets или ajax. В простейшем случае это просто apply (), но я создал таблицу в базе данных, которая описывает порядок вызова директив. Что я имею в виду: когда мы пытаемся вызвать директиву с именем myDir, если в таблице есть соответствие, то вызывается та директива, которая там указана, своего рода ссылка. Но смысл это базы в том, чтобы еще и удобно задавать pre и post директивы. То есть то, что будет вызываться до и после вызова определенной директивы.

    image

    И хранятся все эти директивы в отдельной папке, подключаясь тогда, когда надо:

    modules/directive.js
    define(['DB','is'],function(DB){
    
            var path = 'Actions/';
    
    
            var Directive = {
                    run: function(directiveName, args){
    
                            var args = args || [''];
                            if (!is.array(args)) {
                                    args = [args]
                            };
                            DB.getDerictive(directiveName, exec);
    
                            function exec(directiveName){
                                    // Directive.preAction ?
                                    if (typeof directiveName == undefined || typeof directiveName == 'string') {
                                            action = directiveName;
                                    }else{
                                            action = directiveName.action;
                                    }
                                    Directive._apply(action,args);
                            }
    
                    },
                    _apply: function(actionName,args){
    
                            require([path + actionName],function(action){
                                    action.run.apply(action,args);
                            });     
                            
                    }
            }
    
            return Directive;
    })
    
    


    По websockets с сервера мы получаем имя директивы и аргументы, вызываем её с помощью этого модуля:

      socket.onmessage = function (e){
                    if (typeof e.data === "string"){
                            var request = JSON.parse(e.data);
                            Directive.run(request.function,request.args);
                    };
            }
    
    


    Таким же образом мы получаем инструкцию на добавление игроков в наш пустой список.

    Код
    define(['angularСrutch'],function(angularСrutch){
    
            var action = {
                    run: function(list){
    
                            for(key in list){
                                    angularСrutch.scopePush('userListTpl', 'userlist',{name: key});
                            }
    
                    }
            }
    
            return action;
    })
    
    


    Уверен, вы уже обратили внимание на зависимость с говорящим названием angularСrutch. Этот модуль обеспечивает доступ к модулям ангуляр извне. Дело в том, что изменить данные контроллера ангуляр не так просто. Нельзя просто вызвать какой-то метод или переписать значение параметра. Даже если вы присвоите модуль ангуляр какой-то переменной, область видимости $scope все равно для вас останется не доступной напрямую.

    Для этих целей можно воспользоваться такой постройкой:

    var el = document.querySelector( mySelector );
    var scope = angular.element(el).scope();
    scope.userlist.push(user);
    
    


    Все замечательно, есть доступ к $scope, но и здесь все не так просто. Изменять данные $scope можно сколько угодно, но в представлении ничего не изменится. Ангуляр попросту не заметил, что вы что-то поменяли, он не отслеживает любое изменение параметров $scope, а делает это только в своем цикле $digest (), который вызывается при определенных действиях пользователя. Чтобы вызвать его вручную, вызовем метод $apply на нашем scope:

    Измененный код
    scope.$apply(function () {
        scope.userlist.push(user);
    });
    
    


    Теперь всё в порядке, изменения, которые мы внесли будут видны.

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

    Очередь на DataBoom


    Вы все (многие) знаете, что если дать несколько заданий подряд и быстро (атаковать, колдовать, закончить ход), всё не будет делаться одновременно, создавая мелькание объектов на экране.

    В базе данных таблица очереди выглядит так:

    {"for":"user_1","motion":"opGetCard","motionId":2},
    {"for":"user_2","motion":"opGetCard","motionId":1},
    
    


    Для какого игрока предназначено действие, имя инструкции и id действия для очерёдности.

    Для организации очереди я создал две инструкции: модуль stack.js с единственным методом push (стек на самом деле не очередь, просто слово навится) и метод push модуля DB, отвечающего за взаимодействие с базой DataBoom:

    stack.js
    define (['DB', 'User'], function (DB, User){

    var module = {
    push: function (forWho, motion, expandObj) {

    var expandObj = expandObj || null;
    var motionObj = {
    'for': forWho,
    'motion': motion
    };

    if (expandObj) {
    motionObj[expandObj.prop] = [{id: expandObj.id}];
    };

    DB.push ('motionQueue', motionObj);

    }
    }

    return module;
    })


    Использовать такой интерфейс просто:

    stack.push(User.login,'myTimerStart');
    
    


    Вызывает инструкцию myTimerStart для пользователя User.login

    Извлекать инструкции будем простой функцией setInterval () каждые 2 секунды:

    setInterval(function(){
            Directive.run('getNextAction');
    }, 2000);
    
    


    Для соблюдения очередности нам понадобится глобальная переменная window.motionId, содержащая в себе номер инструкции, которая уже отработана (то есть действие совершено, двигаемся дальше). Директива getNextAction вызывает одноименный метод модуля базы данных и описывает колбэк:

    DB.getNextAction(User.login, this.actionStart); // Для кого ищем инструкции и колбэк
    
    


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

    var config = {
            baseHost: 'https://t014.databoom.space',
            baseName: 'b014'
    }
    
    var db = databoom(config.baseHost, config.baseName); // Инициализируем базу данных
    
    var filter = "(motionId gt " + window.motionId + ") and (for eq '" + forWho + "')"; // Условия выборки
    
    /*
    Поддерживаются все стандартные условия:
    eq - равно
    ne - не равно
    lt - меньше
    le - меньше или равно
    gt - больше
    ge - больше или равно
    */
    
    db.load('motionQueue',{
            filter: filter,
            orderby: "motionId", // Сортировка
            top: 1 // Ограничение на кол-во выбираемых записей
    })
    
    


    И всё бы выглядело просто, если бы мы не имели сложных инструкций типа «игрок положил карту с такими-то параметрами на стол». Тут нам необходимо знать, какая карта, какие у нее параметры. Да, создадим еще одно поле в базе «аргумент» или «карта». Делать еще один запрос к базе для выборки информации о карте?

    У DataBoom, слава богам, есть решение на этот счет — опция expand, которая говорит о том, что надо расширить возвращаемый объект данными из другой таблицы, в нашем случае таблицы с картами.

    Таблица карт
    image


    Сама по себе привязка в базе выглядит так:

    ... ,"card":[{ "id": "probe"}], ...
    
    


    ID записи в другой таблице. Причем заметьте, квадратные скобки явно намекают на то, что это массив, то есть привязка может быть множественной.

    При расширении ответа базы инструкцией expand мы получим ту же самую запись из базы, где вместо объекта { «id»: «probe»} будет объект выборки по id из соответствующей таблицы:

    {
            id:"...",
            collections:[{ id: "motionQueue"}],
            "for":"user_1",
            "motion":"opPutCard",
            "motionId":2
            card:[
                    {
                    id:"probe",
                    title:"probe",
                    mana:1,
                    attack:1,
                    health:1
                    }
            ],
    }
    
    


    Заключение


    • Все основные премудрости angular нахрапом понять не получается, излишне сложен. Сложно взаимодействовать извне, не понравилось.
    • DataBoom понравился в целом, хотя многое плохо документировано и на английском (хотя компания, насколько мне известно, русскоязычная), приходится изучать методом тыка какие-то моменты.


    Ресурсы


    © Habrahabr.ru