📜 ⬆️ ⬇️

Its game with JavaScript and Canvas

image Not long ago, I was curious how tolerably modern browsers support HTML5 and I have not found the best
Ways to write the simplest 2D platformer. In addition to the pleasure of developing toys and improving skills in the use of JavaScript, during the entertainment of painstaking work, a certain amount of experience was gained and the main rakes were found empirically, many of which I had to step on. In this article I will try to briefly and with examples summarize what I learned from my work. Those wishing to create their own high-performance JavaScript application that effectively works with graphics, please under the cat.

General remarks


JavaScript code is very critical to platform resources. Despite the fact that almost all modern engines have ceased to interpret the JS code stupidly, the speed of its execution is still very much inferior to the speed of the “native” code. Meanwhile, even the simplest game is a lot of code that should have time to execute between rendering two adjacent animation frames. In addition, JS is a very specific language and writing a bulk code on it is associated with a number of difficulties. Taken together, this can cause the JS application to stop meeting expectations and quickly bring disappointment . I will try to systematize a little the conclusions to which I arrived through experiments.

1. Compatibility
If we decided to use HTML5 and Canvas in particular, then let us no longer be concerned about compatibility with old browsers - under them still will not work. Thus, you can safely use the basic innovations of ECMAScript 5. On the other hand, do not offend the contempt of the good old users of software, like IE6. It is advisable to notify them of the reasons why they see a gray square as a figure , instead of our wonderful animation. Make it simple, it’s enough to diagnose support for Canvas and used language constructs.

<canvas id="gameArea"> <span style="color:red">Your browser doesn't support HTML5 Canvas.</span> </canvas> <script type="text/javascript"> (function(){ if(typeof ({}.__defineGetter__) != "function" && typeof (Object.defineProperty) != "function") alert("Your browser doesn't support latest JavaScript version.");})() </script> 

Everything is good, but the trouble is that the problems of cross-browser compatibility are not completely exhausted. Among modern browsers there is no consensus about the names of standard functions. The solution is either to refuse to use them altogether, or to make expensive adapters. For example, I could not deny myself the use of property descriptors and this had its negative consequences. How to use them cross-browser is well described here and here . But how to get them to work quickly remains a mystery.
')
2. Code optimization is easy to break
Not so long ago, a very useful article about the V8 engine for Chromium jumped on Habré. The most important thing that I was able to learn is the hidden classes and code optimization for working with them. Indeed, JS often provokes a change in the structure of an object after its construction. You should not do this if the goal is to create fast and easily supported code. As soon as I realized this, the work on the game went more fun, and the code became cleaner and faster.

 function myObject() { }; var mo = new myObject(); mo.id = 12; //    //     . var v; v = 12; //,  var v = 12; v = “12”; //  ,        var v = 15; //  ,      

You also need to strive to reduce the scope of the variable to the minimum - this increases the likelihood of code optimization.

3. In JS, there are no classes, inheritance, or other class-oriented programming.
You should not strain the engine with the implementation of classes using prototyping - the benefit is doubtful, and the code slows down several times (Opera)! Complicated prototypical inheritance and honestly organized transfer of the basic functionality to the heirs are brought down by the already not the best optimization.

4. Pay for using
In the course of developing a game or any other resource-intensive application, inevitably you have to cache “expensive” resources, for example, pre-calculated animations or dynamically loaded scripts. Any resource has a lifetime, after which it is not needed. And here it is important to get rid of it correctly.

 var resCache = { res1 : new getCostlyResource() }//      resCache.res1 = null; 

Most likely, the memory will not be freed by the garbage collector (GC). It will be collected at an arbitrary point in time, and it will turn out to be the most inappropriate, because GC will try to remove all the garbage that has accumulated by that moment. This is better:

 delete resCache.res1; resCache.res1 = null; //   

At first glance - nothing complicated, but in more complex cases, nuances appear, and the behavior of delete is not always obvious .

5. Closures and properties are enemies of speed
Closures are a basic feature of a functional language. It seems that this particular place should be optimized as much as possible with the JS engine. But, practice shows that it is not. Here is a small test that compares the speed of various ways to access object data ( test code ).
Result for different browsers and platforms (ms):
Windows XP (x86), Core 2 Duo, 3 GHzOpera 12Firefox 17Chrome 23
No closures, direct access to object fields9617
No closures, data access via methodssixteeneleven28
Closures, access via methods341223
Closures, access through properties387899489
Windows 7 (x64), Core i3-2100, 3.1 GHzOpera 12Chrome 23IE 10
No closures, direct access to object fields7five15
No closures, data access via methods13eleven13
Closures, access via methods27914
Closures, access through properties22231599
Surprisingly, Opera looks better in the test. Unfortunately, the overall conclusion is disappointing, closures are optimized only in Chrome, and access through properties is a great luxury that can slow down the application by an order of magnitude.

Notes on the graphics engine


Programming graphics in JavaScript is a separate large topic that I can touch on later if there is a demand. Here I tried to highlight a few simple tricks that were easy to use and they gave a good result.

1. Frame by frame animation.
There are two approaches to creating animations: event and frame by frame. The first one is suitable mainly for simple tasks, like the button illumination when hovering the mouse, and can be executed in the corresponding handlers. The second is suitable for performing complex animation tasks that must “live their own lives”, regardless of user actions, for example, dynamic games.
When creating a game, the easiest way (and cheaper in terms of computing resources) is to rely on the gameplay, relying on frame rate stability. You can try to use the frame request animation from those browsers that support it. This is not so easy to do, because this method has become the de facto standard and for some reason did not fall into ECMAScript 5.

 var raf = window.requestAnimationFrame || window.msRequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame; //     : var myRedrawFunc = function () { /*     */ raf(myRedrawFunc) } raf(myRedrawFunc); 

The benefit of RequestAnimationFrame is mainly that when the animation is not loaded (when the rendering process itself takes less than half the frame time), it allows for greater smoothness of animation and reduces resource consumption on mobile platforms. But in reality this may not be the case. To the inconvenience of its use can be attributed to a fixed frame rate (60 fps) and the lack of compensation for the duration of the next frame, if the previous one was delayed.
However, what to do if raf === null? This can happen if your application falls into the hands of Opera, which traditionally goes its own way. Then the good old setTimeout will help us. As a result, the code will look something like this:

 var fps = 60; var frameTimeCorrection = 0; var lastFrameTime = new Date(); var to = 1000/fps; var myRedrawFunc = function() { /*      */ var now_time = new Date(); frameTimeCorrection += now_time - lastFrameTime - to; if(frameTimeCorrection >= to) frameTimeCorrection = to - 1; //   lastFrameTime = now_time; if(raf) raf(redrawFunc); else setTimeout(myRedrawFunc, to - frameTimeCorrection); }; myRedrawFunc (); 

The disadvantage of this approach is obvious - if the frame calculation will slow down more than can be adjusted - the game process will cease to be calculated in real time. But you can go to the trick. As a rule, the cause of the brakes is the drawing of elements of the next frame, since for the browser engine this is an invoice operation. Therefore, you can write the code so that when the time correction does not cope (the condition if (frameTimeCorrection> = to) works), in the next frame you can only calculate the game world without redrawing it. There will be so-called. " Lag " which in the game looks less annoying than slow motion.

2. Draw only what is visible on the canvas.
The most simple and proven over the years the way of animation - sprites . The peculiarity of this method lies in the fact that in order to create the illusion of movement, the sprites move in the game space by changing the coordinates of their rendering. As a rule, the gaming space significantly exceeds the size of the frame rendering area and if the gaming space is large and the sprites draw a lot of time, they will take a lot of time. The context methods of the canvas are elements of the DOM, and accessing them is very expensive. One of the optimizations is to draw only what is visible in the frame. In the above test , initially 9000 “smart” sprites are created and displayed on the canvas, which, when redrawing, monitor their coordinates, and do not use canvas methods if they are out of frame. Then 9000 “silly” sprites are created that don't follow the scope ( test code ).
Test results (fps):
Windows XP (x86), Core 2 Duo, 3 GHzOpera 12Firefox 17Chrome 23
Smart Sprites473525
"Stupid" sprites151412
Windows 7 (x64), Core i3-2100, 3.1 GHzOpera 12Chrome 23IE 10
Smart Sprites563261
"Stupid" spritesnineteen1551
The difference is palpable (and Chrome oh again let us down - the myth has been debunked).

3. We cache rasterization
Rasterization by means of the graphic engine is a rather resource-intensive occupation. Therefore, an important optimization is caching the rasterization results in memory. The easiest way is to create a separate outline and rasterize vector graphics in it. Remember the pointer to it in the cache and in the main drawing area display the memorized result. For illustration, take the rasterization of 1000 text sprites. In the performance test , 800 text sprites are drawn alternately at intervals of 20 seconds. First with caching the result of rasterization, then without caching ( test code ).
Test results (fps):
Windows XP (x86), Core 2 Duo, 3 GHzOpera 12Firefox 17Chrome 23
Rasterization caching233260
No cachingfive1247
Windows 7 (x64), Core i3-2100, 3.1 GHzOpera 12Chrome 23IE 10
Rasterization caching336161
No cachingfive5623
With this approach, it is important to maintain a balance between memory, cache reset rate and rasterization speed. So, if the text changes dynamically and rather intensively (say, once in 10 animation frames), then its caching can only worsen the overall performance, since the caching operation itself will incur more overhead than rasterization.

4. Dynamic resource loading
If the animation is based on bitmap sprites, then before you can draw them on the canvas, you should load these same maps into the browser's image cache. To do this, it is enough to create an Image element and transfer the url of the image resource as a source. The difficulty is to wait for the moment when the browser loads the image into its cache. To do this, you can use the onload event, in which, increment the counter of already loaded images. As soon as the value of this counter matches the number of pictures added to the download, the resource will become persistent, and we can execute the basic game code.

 function Cache() { var _imgs = {}; var _addedImageCount = 0; var _loadedImgsCount = 0; this.addSpriteSource = function(src) { var img = new Image(); img.onload = function() { _loadedImgsCount++; }; img.src = src; _imgs[src] = img; _addedImageCount++; } this.getLoadedImagePc() { return _loadedImgsCount * 100 / _addedImageCount; } this.getImage = function(src) { return _imgs[src]; } } //  Cache.addSpriteSource("img1.jpg"); Cache.addSpriteSource("img2.jpg"); //,    function waitImagesLoading() { var pc = Cache. getLoadedImagePc(); if(pc < 100) setTimeout(waitImagesLoading, 200); /*        */ } waitImagesLoading(); 

In my toy, I decided to describe each level in a separate script file. It is clear that static loading of such scripts is harmful, since only one of them is needed at a time. The problem was solved by the same approach as in the case of loading images.
There is only one caveat - the Script object has no events, but this is not a problem, since In the code for dynamically loaded scripts, you can insert the global function of registering the script in the cache. Then we proceed in the same way as loading images - we wait asynchronously until the script registers the types described in it, and then we create the necessary instances of the registered types.
In order for the user to not be bored - you can show the percentage of loading all the necessary resources.

5. Fractional coordinates
Drawing on the canvas raster or vector primitives, you can specify fractional coordinates and dimensions. As a result, the graphics engine of the browser is forced to smooth the displayed image. If simplified, this happens because the virtual pixel of the rasterized image will not match the pixel on the screen. As a result, smoothing algorithms will turn on, which can significantly affect performance.
In the performance test , alternately with an interval of 20 seconds, sprites with integer and fractional coordinates ( test code ).
Test results (fps):
Windows XP (x86), Core 2 Duo, 3 GHzOpera 12Firefox 17Chrome 23
Whole coordinates and sizes576060
Fractional coordinates and sizes505260
Windows 7 (x64), Core i3-2100, 3.1 GHzOpera 12Chrome 23IE 10
Whole coordinates and sizes606161
Fractional coordinates and sizes606161
It should be clarified here that in the case of a 64-bit platform, the situation is saved by a better graphics adapter, which obviously takes on the task of anti-aliasing and anti-aliasing. In the case of relatively fast moving sprites (tens of pixels per second), you can get along with whole coordinates and sizes. However, the coordinates and dimensions themselves must be considered in fractional values ​​in order not to lose accuracy with a smooth change of parameters. This approach is quite justified, when all values ​​of coordinates and dimensions are calculated and stored without rounding, and before direct output to the canvas, they are rounded off using Math.floor.

Instead of a conclusion.


The modern development of JavaScript, HTML5 and support of these features by various browsers already allow writing productive interactive graphic applications, which in most tasks will give odds to traditional flash programming.

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


All Articles