📜 ⬆️ ⬇️

Interactive online game of HTML, CSS and JavaScript

Having somehow played at hexbug in the office, the idea was born to write a toy for similar reasons.
I am a web developer by current activity and therefore I wanted the game to use only HTML, JavaScript and CSS - tools familiar to every web developer. No flash or canvas. It sounds hardcore, but in fact now HTML + CSS3 is a very powerful and flexible means of visualization, and writing game code in JavaScript is a pleasure. In addition, I wanted the game to be with network multiplayer, moreover, interactive - there are no drafts, card games, step-by-step strategies, everything should be in action and movement.

Here's what happened in the end:


')
In the article I will leave a set of notes arising from the writing of a prototype of a toy, aimed more at the “how to make it easier and faster” approach. I think the article may be useful as a kind of help for newcomers in this exciting business.



Gameplay


The task of the game is to grow your colony of beetles and destroy all enemy units. The beetles randomly run around the playing field, and the player’s task is to help them find bonuses in the form of various foods and assist their units that were attacked.

Bonuses in the game:
cupcake - gives 5xp, when typing 15xp the beetle multiplies
Apple - restores 50hp, if the beetle is completely healthy then it adds 15 additional hp
pepper - increases attack by 5dm
acorn - gives 2xp and is thrown at the nearest enemy, on hit it does triple damage
the fly agaric - gives 1xp and allows you to make a poisonous shot, on hit deals 1/2 damage and slows the victim


Can play from 2 to 4 people. You can also simply connect to the server from different browser tabs, and play alone.
You can try to play here .
Sources on github .
Archive with the game .

Graphics


HTML and CSS are certainly not very quick in terms of performance when it comes to rendering graphics required in interactive games. But if our goal is to write a prototype of a toy, then this option will completely go away. In the end, the “narrow moments” in the form of drawing the main game scene can be further quickly transferred to the canvas.

To work with graphics in a 2D game, we need to move, rotate and scale the sprites.

Move the sprite by setting its position: absolute and changing the left and top



To rotate the sprites, use transform: rotate . And with the help of transform: origin, you can specify the axis of rotation (by default, it is in the center of the sprite).



For scaling, we change the sprite size using the width and height properties, before setting the appropriate value in the background-size:



Hardware acceleration


To improve the performance and therefore the smoothness of the animation, you can force the browser to use the GPU to draw animations. To do this, you need to work with sprites as with three-dimensional objects. Now let's do the operations of moving, rotating and scaling through translate3d, rotate3d and scale3d:







All these operations were enough to collect the graphics in the game from several sprites drawn in the “paint”.

Physics


In addition to drawing game objects, you also need to adjust their interaction with each other.
In bugsarena, all the interaction is in the handling of collisions of sprites.
Since it is planned to do everything as simple as possible, we restrict ourselves to school mathematics.
Probably one of the most frequent mathematical operations in games is finding the distance between two points. According to the sute, the task is to find the hypotenuse in the triangle:



We get the formula:



Now, thanks to this simple formula, you can do a lot of operations, such as finding the distance to an object, finding the nearest and furthest objects, finding objects in a given radius, as well as detecting objects in the form of a circle.
All game objects are drawn in fairly small sprites 20x20 in size, you can neglect their shape and calculate collisions as if they are all inscribed in a circle with a diameter of 20. Then you can say that 2 objects collided when the distance between their centers is less than or equal to the sum of their radii.



And a few more notes:


Vector class example
//  function Vec (x_, y_) { if (typeof x_ == 'object') { this.setV(x_); return; } this.x= typeof x_ == 'number' ? x_ : 0; this.y= typeof y_ == 'number' ? y_ : 0; } Vec.prototype = { //   0 setZero: function() { this.x = 0.0; this.y = 0.0; }, //   x  y set: function(x_, y_) {this.x=x_; this.y=y_;}, //     setV: function(v) { this.x=vx; this.y=vy; }, //   negative: function(){ return new Vec(-this.x, -this.y); }, //   copy: function(){ return new Vec(this.x,this.y); }, //    add: function(v) { this.x += vx; this.y += vy; return this; }, //   mubtract: function(v) { this.x -= vx; this.y -= vy; return this; }, //    multiply: function(a) { this.x *= a; this.y *= a; return this; }, //    div: function(a) { this.x /= a; this.y /= a; return this; }, //    length: function() { return Math.sqrt(this.x * this.x + this.y * this.y); }, //   (     = 1) normalize: function() { var length = this.length(); if (length < Number.MIN_VALUE) { return 0.0; } var invLength = 1.0 / length; this.x *= invLength; this.y *= invLength; return length; }, //    angle: function () { var x = this.x; var y = this.y; if (x == 0) { return (y > 0) ? (3 * Math.PI) / 2 : Math.PI / 2; } var result = Math.atan(y/x); result += Math.PI/2; if (x < 0) result = result - Math.PI; return result; }, //      (     ) distanceTo: function (v) { return Math.sqrt((vx - this.x) * (vx - this.x) + (vy - this.y) * (vy - this.y)); }, //      x,y     x,y   vectorTo: function (v) { return new Vec(vx - this.x, vy - this.y); }, //      rotate: function (angle) { var length = this.length(); this.x = Math.sin(angle) * length; this.y = Math.cos(angle) * (-length); return this; } }; 



Used design patterns


In a few words, the game logic can be described as follows: There is an object of the Game class describing the game world which has an array of objects inheriting from the GameObject class — these are all objects of the game world. Each Game game frame is traversed across all game objects and calls the step method for each. The step method of each object describes what it should do for this frame (move, handle collisions, destroy, etc.). To implement the OOP in the game, use the Class object from John Resig's Simple JavaScript Inheritance , modified to support mixins and static properties.
Probably one of the most successful patterns for creating new objects in games is the use of the factory method . The bottom line is that we will not directly call new Create objects, but use the method that does it for us. The factory method will save us from the fuss of connecting a new object to the game world.

For example, we want to create an object of the class Block to include it in the game world and place it in a given place:
  game.create('Block', {x: 100, y: 150}); 


The code of the create method:
  create: function (objectName, params) { //             Game.classes //       Game.classes var object = new Game.classes[objectName](params); //     object.id = ++this.idx; //       object.game = this; //        this.objects[object.id] = object; //          //        if (object.isColliding) this.collidingObjects[object.id] = object; //          //     birth,       object.birth(); //    return object; }, 


Creating game cards


So when the game is already written I want to diversify it with several game cards. Creating all game objects with code (calling method after method) is very tedious and unattractive. Writing your map editor will take a lot of time. But there is a simple way - you can use a text editor or your ide for visual creation using the following approach:

  !function () { var WIDTH = 20; var HEIGHT = 12; var B = 'Block'; var P = 'Bonus'; var MAP = [ , , , , , , , , , , , , , , , , , , , , , B, , B, , B, B, B, , B, , , , B, , , , B, B, B, , B, , B, , B, , , , B, , , , B, , , , B, , B, , B, B, B, , B, B, B, , B, , , , B, , , , B, , B, , B, , B, , B, , , , B, , , , B, , , , B, , B, , B, , B, , B, B, B, , B, B, B, , B, B, B, , B, B, B, , , , , , , , , , , , , , , , , , , , , , B, , B, , B, B, B, , B, B, B, , B, B, B, , , , , , B, , B, , B, , B, , B, , B, , B, , B, , , , , , B, B, B, , B, B, B, , B, B, , , B, B, , , , , , , B, , B, , B, , B, , B, , B, , B, , B, , , , , , B, , B, , B, , B, , B, B, B, , B, , B, , P, , , ]; Game.maps['Hello'] = Game.Map.extend({ build: function () { var blockSize = 20; for (var i = 0; i < HEIGHT; i++) { for (var j = 0; j < WIDTH; j++) { var index = WIDTH * i + j; if (MAP[index]) this.game.create(MAP[index], {x: blockSize * j, y: blockSize * i}); } } } }) }(); 

Result:



Network code


The network code is written using web sockets using the socket.io library, the game server is written on nodejs.
To make a simple implementation of an interactive network game, and with the condition that only TCP is available to us, that is also a problem.
Now for such games they use fast UDP protocol which unfortunately is unavailable through socket.io, though if you have a strong desire you can look towards WebRTC. It is important that the game goes smoothly without jerks and is synchronized on all clients. The server will be simple and will only deal with the transmission of customer messages, since only their actions affect the course of the game. He will not be engaged in transferring the states of game objects, and just do not know anything about the game world, except for the state of the game - waiting for players / game is in progress

The entire temporary tape of the game can be divided into frames. Clients send messages about their actions to the server, the server accumulates these messages, and after a certain number of frames sends the accumulated to clients. This is like a variation of a very accelerated turn-based strategy - all players are given only a few frames to make their turn (send messages to the server). After these frames, the server sends to the clients all the actions for the previous turn, which immediately begin to play. At the same time, players can make a new move.



This approach is good because it is simple and customers always know in which frame they start the actions coming from the server and can safely continue the game until this frame. The server, on the other hand, must send client actions some time before the onset of this keyframe, so that clients do not stand idle. The disadvantage of this approach is not too fast reaction to the actions of the players, and some active shooter would be difficult to play.

One may wonder - if we transfer only the actions of clients, then how to synchronize the behavior of objects based on randomness? After all, various bonuses appear in completely random places, but all customers must have the same place. Beetles run very chaotically, constantly changing the direction of their run, and at the same time all this “chaos” should be completely the same and follow the same scenario for everyone. The problem with the synchronization of such behavior can be solved so that everywhere where random variables are used, not to use Math.random for this, but to use your own pseudo-random number generator (PRNG). The point is the following - before starting the game, the server generates a random number and transfers it to each joining client. With the help of this number, the client initializes the PRNG that on all clients will produce the same sequence of pseudo-random numbers. The simplest implementation of such GPSNG is the Miller Park-generator.
Implementation on js:

  var ParkMillerGenerator = function (initializer) { this.a = 16807; this.m = 2147483647; this.val = initializer || Math.round(2147483647 / 3); } ParkMillerGenerator.prototype = { next: function () { this.val = (this.a * this.val) % this.m; return (this.val / 1000000) % 1; } } 


Using:
  var initializer = 333; //   ,        var gen = new ParkMillerGenerator(initializer); //   gen.next(); // 0.5967310000000001 gen.next(); // 0.46109599999999773 gen.next(); // 0.07891199999994569; 


We do service from nodejs of application


Maybe not a little bit, but also a useful note. When the server is written, it would be nice to run it on the combat machine as a service for permanent work. I will describe how this can be done on the example of Ubuntu.
Go to /etc/init.d and create a shell script with the name of our service there, I will have bugsarena. I’ll draw your attention to the fact that the block that starts with “BEGIN INIT INFO” is not just a comment, but you shouldn’t delete our service settings.

 #!/bin/sh ### BEGIN INIT INFO # Provides: bugsarena # Required-Start: $local_fs $remote_fs $network $syslog # Required-Stop: $local_fs $remote_fs $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: starts the bugsarena servers # Description: starts the bugsarena servers ### END INIT INFO #        (  ) NODE=/usr/bin/node DAEMON_SERVER=/home/me/projects/bugs-arena/server/server.js SERVER_PARAMS="name=Arena-Dogfight map=Dogfight port=8090" NAME=bugsarena DESC="bugsarena servers" #    3  - start, stop  restart. #     start() { #  nodejs        pid   start-stop-daemon --start --make-pidfile --background --pidfile /var/run/$NAME-server.pid \ --exec $NODE -- $DAEMON_SERVER $SERVER_PARAMS } stop() { #  nodejs  echo -n "Stopping $DESC: " start-stop-daemon --stop --quiet --pidfile /var/run/$NAME-server.pid } case "$1" in start) start ;; stop) stop ;; restart) stop sleep 1 start ;; *) echo "Usage: $NAME {start|stop|restart}" >&2 exit 1 ;; esac exit 0 


Making the file executable.
 sudo chmod +x bugsarena 


Now you can use the commands
service bugsarena start
and
service bugsarena stop
to start and stop the service.
You can also make the game server start at system startup by executing
update-rc.d bugsarena defaults


Do not forget about XSS!


Lastly, you just need to remind you of a very simple attack typical of browser games. Imagine that we have a list of players in some div. And a player with the name "<script> alert ('Vasya comes into the game!") </ Script> "comes into our game. His name is added to the player list div, and all customers receive an annoying alert message. And it still flowers. Through XSS vulnerability, you can safely upload any script from any site. So do not forget about the screening of data transmitted from clients.

Source: https://habr.com/ru/post/225669/


All Articles