Card Game Event Queue + Angular Basics

  • Tutorial
Good day, beginners, today we will try to remake our toy , learning the basics of new “technologies” for us:

  • Angularjs
  • DataBoom

On the hangar we’ll try to rewrite the main parts of our application to figure out what it is and what it is eaten with. Therefore, in the first part I will try to describe the course in more detail for you, so that you do not crash against the pitfalls that are contained in considerable numbers on your way of exploring angular.

Well, in the second part, using DataBoom we will create a wonderful queue of events, as in the original game (I remind you that we are doing in the image and likeness of HeartStone). Looking ahead, I’ll say that next time we will completely get rid of the php server and completely switch to the Databoom, but this is a completely different article ...

image


Angularjs


It’s worth starting with the fact that to initialize an angular application, it’s not enough for you to simply connect the library and create a module in the js file. To work with angular, you will need to work with your html files as a view, which, in my opinion, is better for separating the view and the controller.

In the hangar there is such an entity as directives - special html-attributes, and we will use one of these to initialize our application. An application may not cover the entire page, but only a separate block on it, it all depends on where you initialize it. Initialization is done using the ng-app directive:


This suggests that this element and everything lower in the tree is a representation of the angular. We will manage all this with the help of controllers, which also need to be initialized in a similar way:


The controllers, in turn, do not hang in the air, but are tied to modules. The application itself is also a module (the main entry point to the application), and to specify the primacy of one of the modules, its name must be specified in the ng-app directive:


Modules are created by the module method of the angular object:

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

But the module itself is of little use without controllers. To create a controller on our main module, which we defined as an entry point for the application (for example), we need to call the controller method on the module object:

myModule.controller('Имя контроллера', ['$scope',function($scope){
		$scope.var = 'some';
		$scope.foo = function(){};
	}]);

The $ scope object defines all the variables and functions of the controller scope, that is, what we can use in the view. In this case, we can work with var and foo in our html files.

Variable values ​​are displayed using double curly braces {{var}}, that is:

html
{{var}}


will output "some".

Closer to the point


We will deal with the remaining subtleties immediately with examples, as There are some articles on Angular, although the documentation on angular.ru is not entirely clear (to me personally).

We will meet the first pitfall when we try to make the application modular (using requirejs as an example). If you immediately register the ng-app directive in html, and then connect the library with the angular method with the require method, we find that nothing works. This is because the DOM tree at the time of connecting the library has already been compiled.

In such a case, the angular object has a bootstrap method:

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

Thus, we bind the App module as the entry point to the application to document.

The first thing we will remake in our toy is the menu, namely the only thing that is there is a list of players.

The code
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;
})

Here letsFight () (invitation to battle) will be called by clicking on the corresponding button next to each player. In the hangar, this is specified by the ng-click directive:

  • {{user.name}}

  • Pay attention to the different function calls: with curly braces and without. The difference is that the expression in curly brackets is calculated immediately, without waiting for any action from the user, therefore, in the second case, we do not use curly braces, because we need the function to be called only by clicking.

    The ng-repeat directive works the same way as foreach in php or for of in ES6 - iterates over all objects in the list. The directive is indicated in the tag
  • , which means that we will repeat it exactly as many times as we have elements in the userlist array.

    But initially our list of players is empty, and we get it on websockets from the server. In the application, I tried to separate libraries, modules, and some simple sets of instructions (I called them Directives) in order to be able to easily rewrite them.

    Moreover, we have a separate handler for these directives (Directives.js), which simply calls the necessary directives by the name that we get from anywhere, for example by websockets or ajax. In the simplest case, this is just apply (), but I created a table in the database that describes the order in which directives are called. What I mean: when we try to call a directive named myDir, if there is a match in the table, then the directive that is indicated there is called, a kind of link. But the point of this base is that it is also convenient to set pre and post directives. That is, what will be called before and after the call of a specific directive.

    image

    And all these directives are stored in a separate folder, connecting when necessary:

    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 from the server, we get the directive name and arguments, call it using this module:

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

    In the same way, we get instructions for adding players to our empty list.

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

    I am sure that you have already noticed the dependence with the talking name angularСrutch. This module provides access to angular modules from the outside. The fact is that changing the angular controller data is not so simple. You can’t just call a method or rewrite the value of a parameter. Even if you assign the module angular to some variable, the scope of $ scope will still not be available to you directly.

    For these purposes, you can use this construction:

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

    Everything is great, there is access to $ scope, but here it is not so simple. You can change the $ scope data as much as you like, but nothing will change in the view. Angular simply did not notice that you changed something, he does not track any change in the parameters of $ scope, but does this only in his $ digest () loop, which is called during certain user actions. To call it manually, we call the $ apply method on our scope:

    Modified code
    scope.$apply(function () {
        scope.userlist.push(user);
    });
    

    Now everything is in order, the changes that we have made will be visible.

    I will not dwell on the controllers of the list of cards in the hands of players and in the arena, everything is the same here, but it’s better to move on to the implementation of the event queue.

    Queue on DataBoom


    You all (many) know that if you give several tasks in a row and quickly (attack, conjure, end the turn), everything will not be done at the same time, creating flickering of objects on the screen.

    In the database, the queue table looks like this:

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

    For which player is the action intended, the name of the instruction and the action id for the order.

    To create the queue, I created two instructions: the stack.js module with a single push method (the stack is not really a queue, I just like the word) and the push method of the DB module responsible for interacting with the DataBoom database:

    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;
    })
    


    Using such an interface is simple:

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

    It calls the myTimerStart statement for the user User.login. We extract the

    instructions with the simple setInterval () function every 2 seconds:

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

    To comply with the order, we need the global variable window.motionId, which contains the number of the instruction that has already been worked out (that is, the action is completed, move on). The getNextAction directive calls the database module method of the same name and describes the callback:

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

    We can search for the necessary instruction in the table due to the possibility of query queries, that is, queries with a filter, sorting and limit:

    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 // Ограничение на кол-во выбираемых записей
    })
    

    And everything would have looked simple if we hadn’t had complicated instructions like “the player put a card with such and such parameters on the table”. Here we need to know which card, what parameters it has. Yes, we will create one more field in the base “argument” or “map”. Make another query to the database for fetching map information?

    Thank God, the DataBoom has a solution in this regard - the expand option, which says that we need to expand the returned object with data from another table, in our case, a table with maps.

    Map table
    image

    The binding itself in the database looks like this:

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

    ID of the record in another table. Moreover, note that the square brackets clearly hint that this is an array, that is, the binding can be multiple.

    When expanding the response of the database with the expand instruction, we will get the same record from the database, where instead of the {"id": "probe"} object there will be a sample object by id from the corresponding table:

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

    Conclusion


    • All the basic wisdom of angular cannot be understood unceremoniously, it is unnecessarily complicated. It is difficult to interact from the outside, I did not like it.
    • I liked the DataBoom as a whole, although much is poorly documented in English (although the company, as far as I know, is Russian-speaking), I have to study some points by typing.

    Resources



  • Also popular now: