📜 ⬆️ ⬇️

Physics Snake. From scratch. Part one

An article with the tag "teaching material." From scratch, so we will write our own not difficult (for a start) physics engine and immediately not a difficult game (I chose a snake) on it. But the article will most likely not be about this, as this is not such a difficult task, but how it will all be in JavaScript, and with the most beautiful (correct) code (I expect all that can be done even better you describe in comments). "And in response, ripe tomatoes flew ..." Let's start.
(who read right up to here, hold the cookies, control the left-right arrows):
- that's what will happen: in part one
- and this is the same (dev-mode)

Let's start with index.html
look at the code
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <link rel="stylesheet" href="css/main.css" type="text/css" /> </head> <body> <div id="container"> <div id="bg"></div> <canvas id="canvas" width="900" height="600"></canvas> </div> <script src="js/LAB.min.js" type="text/javascript"></script> <script type="text/javascript"> $LAB .script("js/engine/CustomEvent.js").wait() .script("js/engine/Body.js").wait() .script("js/engine/World.js") .script("js/Bonus.js") .script("js/EnemyCloud.js") .script("js/Hero.js") .script("js/mainApp.js"); </script> </body> </html> 


We connect scripts before the closing body tag.
Why not in the head? As soon as the browser sees the script tag, it blocks the further construction of the page until the moment when the script is loaded. That is, before you will be just a white screen (and do not need here about the Gigabit Internet).

We have a lot of files. Ideally, everything needs to be obfuscated and put into one file. Or group the maximum in a couple of files. We will use the LAB.js library to load the remaining files with scripts.
Download files in parallel.
We use LAB.js (or else you can, for example, LazyLoad.js, etc.). It allows us to load script files in parallel (as you remember, it has already been mentioned above that construction stops at each insert script tag, that is, all scripts are loaded in turn in a row) plus we can control the load (for example, the wait function). We, for example, cannot use, say, a jQuery plugin, when jQuery itself has not loaded yet, of course, we put a pause on jQuery, and as soon as it loads, everything will go further.

Go to the mainApp.js file.
look at the code
 (function() { var game = { images: [ { url: 'images/en_blue.png' }, { url: 'images/en_white.png' }, { url: 'images/en_red.png' } ], loadCount: 0, load: function() { var self = this, max = this.images.length; for (var i = max; i--;) { this.images[i].img = new Image(); this.images[i].img.onload = function () { self.loadCount++; if (self.loadCount == max) { self.loadComplete(); } } this.images[i].img.src = this.images[i].url; } }, loadComplete: function() { this.initControll().initGame().startLoop(); }, canvas: document.getElementById('canvas'), config: { fps: 1000/30, stageW: 900, stageH: 600 }, bodies: [], enemies: [], bonuses: [], initControll: function() { var handlers = this.controllHandlers(); if (document.documentElement.addEventListener) { document.body.addEventListener('keydown', handlers.keyDown); document.body.addEventListener('keyup', handlers.keyUp); } else { document.body.attachEvent('keydown', handlers.keyDown); document.body.attachEvent('keyup', handlers.keyUp); } return this; }, controllHandlers: function() { var self = this; return { keyDown: function(e) { self.keyDownControll(e.keyCode); }, keyUp: function(e) { self.keyUpControll(e.keyCode); } } }, initGame: function() { //-- world this.world = new World(this.canvas, this.config.stageW, this.config.stageH); //-- bonuses this.initBonuses(); //-- enemies this.initEnemies(); //-- Hero this.hero = new Hero(); this.world.addChild(this.hero.body); this.hero.initTales(this.world); return this; }, initBonuses: function() { var self = this, all = 20, good = 10, offset = 20; for (var i = 0; i < all; i++) { if (i < good) { this.bonuses.push(new Bonus({ x: offset + Math.random()*(self.config.stageW - 2*offset) , y: Math.random()*self.config.stageH, bonuseType: true, radius: Math.random()*1+2 })); } else { this.bonuses.push(new Bonus({ x: offset + Math.random()*(self.config.stageW - 2*offset) , y: Math.random()*self.config.stageH, bonuseType: false, radius: Math.random()*1+2 })); } this.world.addChild(this.bonuses[i].body); } }, initEnemies: function() { var enemiesData = [ { x: 10, y: 470, speed: 3 }, { x: 10, y: 300, speed: 2 }, { x: 10, y: 130, speed: 1 } ]; for (var i=0, max = enemiesData.length; i < max; i++) { this.enemies.push(new EnemyCloud({ x: enemiesData[i].x, y: enemiesData[i].y })); this.enemies[i].body.speed.default_x = enemiesData[i].speed; this.world.addChild(this.enemies[i].body); //--bonuses this.enemies[i].bonuses = this.bonuses; this.enemies[i].world = this.world; } }, keyDownControll: function(_keyCode) { switch (_keyCode) { case 37: this.hero.controll.left = true; break; case 39: this.hero.controll.right = true; break; default: break; } }, keyUpControll: function(_keyCode) { switch (_keyCode) { case 37: this.hero.controll.left = false; break; case 39: this.hero.controll.right = false; break; default: break; } }, startLoop: function() { var self = this; this.enterFrame = setInterval(function() { self.loop(); }, this.config.fps); }, loop: function() { var hero = this.hero, enemies = this.enemies, bonuses = this.bonuses; this.world.update(); //-- controll hero if (hero.controll.left) { hero.speed.angle -= hero.speed.rotation; } if (hero.controll.right) { hero.speed.angle += hero.speed.rotation; } hero.body.speed.x = hero.speed.x*Math.cos(Math.PI*hero.speed.angle/180); hero.body.speed.y = hero.speed.y*Math.sin(Math.PI*hero.speed.angle/180); for (var i = 0, maxI = enemies.length; i<maxI; i++) { enemies[i].body.position.x += enemies[i].body.speed.default_x; if (enemies[i].body.position.x > this.config.stageW - enemies[i].body.config.width) { enemies[i].body.speed.default_x = -enemies[i].body.speed.default_x; } if (enemies[i].body.position.x < 0) { enemies[i].body.speed.default_x = -enemies[i].body.speed.default_x; } } for (var j = 0, maxJ = bonuses.length; j<maxJ; j++) { if (bonuses[j].body.userData.bonuseType) { bonuses[j].body.position.y += bonuses[j].speed.y; if (bonuses[j].body.position.y > this.config.stageH - 5) { bonuses[j].body.position.y = 5; } } } hero.enterFrame(); } }; game.load(); })(); 


The points:
Wrap everything in an anonymous function and call it.
The scope in JavaScript is determined by the function. The variable defined in the function will not be visible outside of it (the global namespace will not be littered). Plus, if you don’t yet define a variable, use it immediately, for example:
 function myFunc() { a = 123; } 

it becomes global (a property of a global object), this is the same thing that you would write
 function myFunc() { window.a = 123; } 

that, as you know, at some point will lead to a conflict of names.

The game itself is represented by the game object, that is, here we can say we have the namespace "game". How does it all start? That's right, with preloading images. Therefore, the only thing we have is called
  game.load(); 

We look at it, the first thing we see:
  var self = this, max = this.images.length; for (var i = max; i--;) { 

So: save the link to our object (game).
Where does this lead us?
This is determined at the time of the function call. Recall the same call, aplly, which explicitly defines the context of the call. The context, that is, the object pointed to by this. In our case, we see the onload function; if we turn to this, it will refer to the HTMLImageElement (the images that loaded).

We look further, we get the length of the array, and we wrote a loop for the form: for (var i = max; i--;) {
Cycle speed
Slowest cycle
 for in 
(I think it’s clear why), plus he iterates over the properties in a random order, which can often lead to logical errors, so we use it only in cases where it is not necessary to obtain all (and not known) properties of the object. Next is the view loop.
  for (var i = 0; i<this.images.length; i++;) { 

here, for some reason, each iteration we recalculate the length of the array (although most people don’t bother it for some reason, and they write like this all the time, although (in my experience, when sorting an already finished array) only in a very small part of cases, this length could change iteration time).
Next (logical) is
  for (var i = 0, max = this.images.length; i<max; i++;) { 

we get the length only once.
Well, in the end (as it turned out, iterating through the array from the end faster than from the beginning), ok, we do:
  for (var i = this.images.length; i>0; i--;) { 

and more beautiful:
  for (var i = this.images.length; i--;) { 


We look further, we installed the onload handler, after which we start uploading a picture
 this.images[i].img.src = this.images[i].url; 

First, the event listener, then the event itself.
If you change the order of the given strings (everything seems to be fine, the picture will not have time to load until the interpreter reaches the next row) everything will work. Almost everywhere. Yes, you guessed it, IE8 already tells you everything he thinks about you. Therefore, follow this.

After the download is complete, the control will be initialized, some kind of game data, the start of the cycle
  loadComplete: function() { this.initControll().initGame().startLoop(); } 

nice and simple .

Before moving to these functions, we see the following code:
  canvas: document.getElementById('canvas'), config: { fps: 1000/30, stageW: 900, stageH: 600 } 

We make links to html elements that will be used frequently.
DOM operations are the slowest. What happens if I go to the DOM in every frame and look for the canvas I need? And if also (which we all love) with jQuery, $ ('# canvas') - and this is in a loop, the creation of jQuery objects is also added. In general, we all understand that this is evil.

We put all the settings in the config field (everything is clear here).

We look at the initialization of the control from the keyboard
look at the code
  initControll: function() { var handlers = this.controllHandlers(); if (document.documentElement.addEventListener) { document.body.addEventListener('keydown', handlers.keyDown); document.body.addEventListener('keyup', handlers.keyUp); } else { document.body.attachEvent('keydown', handlers.keyDown); document.body.attachEvent('keyup', handlers.keyUp); } return this; }, controllHandlers: function() { var self = this; return { keyDown: function(e) { self.keyDownControll(e.keyCode); }, keyUp: function(e) { self.keyUpControll(e.keyCode); } } }, keyDownControll: function(_keyCode) { switch (_keyCode) { case 37: this.hero.controll.left = true; break; case 39: this.hero.controll.right = true; break; default: break; } }, keyUpControll: function(_keyCode) { switch (_keyCode) { case 37: this.hero.controll.left = false; break; case 39: this.hero.controll.right = false; break; default: break; } } 


Something is written, a lot of excess, no? Why not just do something in the index.html file
 <body onkeydown="eventHandler"> 

and so on.
We write unobtrusive JavaScript
We can talk about this a lot, let's start with Viky. In a nutshell, the point is that everything should be divided.

We look, we checked the first on IE, and, I could write
 if (navigator.userAgent.indexOf(' MSIE') ! == -1) { 

but it is anti-pattern (if we are already talking about the correct code), if for example
need addEventListener method ,
then only view verification
 if (document.documentElement.addEventListener) { 

we can definitely say something.

')
And all the functions in here were needed in order to "get rid" of the event.
"Unbound" from the events.
Look at the line:
 self.keyDownControll(e.keyCode); 

Here I don’t transfer the reference to the event , but .keyCode , that is, if suddenly I need to “manually” simulate actions on keyboard events, I’ll just write, for example, game.keyDownControll (37);


About physics:

It would not be bad to do that we would have a notion of a physical world with its own characteristics, and the created physical bodies were simply added to it by the addChild method, and then we don’t want to know what it does there. Plus there would be some sort of dispatcher for collisions of bodies, and some sort of console with the output of the necessary information during debugging. Well, look part of the function:
 initGame: function() { this.world = new World(this.canvas, this.config.stageW, this.config.stageH); this.hero = new Hero(); this.world.addChild(this.hero.body); }, 

Okay, they created the world, they transferred the canvas, where everything will be drawn, the width, the height of the world. We created an object of the Hero class, we add its physical component “body” to the world.
  this.world.addChild(this.hero.body); 

We look as we initialize body.
  this.body = Body.create('Circle', { x: 20, y: 20, radius: 18, gravity: 0.1, bgColor: "#eeeeee", fade: { x: 0.999, y: 0.999 } }); 

Body is the main class of the engine, Body.create is the factory method , it allows us to create different (so far we don’t have many of them) bodies. We created a circle. And passed the object with the settings. By variable names I think it is clear what is responsible for what.
Passed the object
Always transfer the object (unless of course the number of parameters passed is planned to be more than 2), then you will not have to do very popular (among beginners) things, like:
 myFunc(null, null, null, null, null, 123, null, 'string_lol'); 

(so, I did not miss the order of the parameter anywhere?) And this is also all clear.

Well, we created some kind of body, added it to the world, we look at what's next. Next you need (as in us a dynamic game) to create a timer, where everything will be updated according to the tick. We look part of the code
 startLoop: function() { var self = this; this.enterFrame = setInterval(function() { self.loop(); }, this.config.fps); }, loop: function() { var hero = this.hero, enemies = this.enemies, bonuses = this.bonuses; this.world.update(); //... 

I have only one permanent timer for the whole game (this.enterFrame).
You can always cope with one timer, if it seems to you that there is not, and that you have unique objects of some class there, that each object should have its own timer - most likely you need to revise the architecture of your application. They can be added and removed, but the monotonous actions that go on throughout the game cycle are done on a single timer.

As you can see, I call this.world.update(); each frame, it updates the whole world gives the results of the collisions and so on.
It is clear that the entire performance will take this function, since each frame is called, so you need to make it as “right” as possible (as far as possible), as you can see, I immediately
  hero = this.hero, enemies = this.enemies, bonuses = this.bonuses; 

I reassign global variables to local ones.
Variables and interpreter.
When the interpreter encounters a variable, it looks at how it is initialized, starting from the deepest level (that is, where the code is currently running, for example, a function) and to the top, that is, if for example we are accessing a global variable, we will have to go through all levels until you can find her. If you need to access global variables from some deep nesting more than once, you need to reassign them to local ones.

We also wanted to define as a collision event, with what exactly to know who faced whom.
We look at the class Hero, a function to initiate events
 Hero.prototype.initEvents = function() { var self = this; this.body.addHitListener('bodies', function(e){ self.hitAction(e.data); }); /*this.body.addHitListener('limits', function(e) { });*/ } 

That is, on this.body (as we remember this is a physical object, in fact it is the head of our snake), we hung up the event handler. They can be of two types: on a physical body and on going beyond the world. The e.data stores a reference to the object that this.body . Cool. All as we wanted.
Making your event is not so difficult (and if jQuery is used , it will be even easier). But we will write it ourselves. I rendered it into a separate class CustomEvent . It can be used in any other place where you need it.
look at the code
  function CustomEvent(_eventName, _target, _handler) { this.eventName = _eventName; if (_target && _handler) { this.eventListener(_target, _handler); } } CustomEvent.prototype.eventListener = function(_target, _handler) { if (typeof _target == 'string') { this.target = document.getElementById(_target); } else { this.target = _target; } this.handler = _handler; if (this.target.addEventListener) { this.target.addEventListener(this.eventName, this.handler, false); } else if (this.target.attachEvent) { this.target.attachEvent(this.eventName, this.handler); } } CustomEvent.prototype.eventRemove = function() { if (this.target.removeEventListener) { this.target.removeEventListener(this.eventName, this.handler, false); } else if (this.target.detachEvent) { this.target.detachEvent(this.eventName, this.handler); } } CustomEvent.prototype.eventDispatch = function(_data) { if (document.createEvent) { var e = document.createEvent('Events'); e.initEvent(this.eventName, true, false); } else if (document.createEventObject) { var e = document.createEventObject(); } else { return } e.data = _data; if (this.target.dispatchEvent) { this.target.dispatchEvent(e); } else if (this.target.fireEvent) { this.target.fireEvent(this.eventName, e); } } 


Everything should also be clear here. We go further. Everyone probably wonders how she (the snake) wraps her tail so beautifully? There is no secret here, we simply move each next element of the snake's “tail” to the position of the previous one, that is, in the cycle
  this.tales[j].moveTo(this.tales[j - 1].position.x, this.tales[j - 1].position.y, 5); 

this.tales [j] is a physical body, that is, in the engine there is also some other method moveTo(_x, _y, _speed) . Which as we see it just moves from the current point to the point with the given coordinates. How to do? Just get the coordinates between the two points by coordinates, divide by the number of transitions (_speed) and we will get this step in one transition. Everything.

We also want a dev-mod. And there is nothing difficult. Since this is set for the whole engine, it is logical that this will be a static property of the Body (I remind you, Body is the main class of the engine). We are looking at the Hero class constructor (since the debug information will be for it, then we initialize it here)
  Body.devMode.usePhysLimits = true; Body.devMode.display = true; Body.devMode.bodyId = this.body.id; Body.devMode.use = true; Body.useGravity = false; 



Here you go.

(who read right up to here, hold the cookies, control the left-right arrows):
- that's what will happen: in part one
- and this is the same (dev-mode)
and the link to the files maybe someone will be interested to look more carefully.

PS Well, in the second part we will try to do something more like a game from this.

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


All Articles