📜 ⬆️ ⬇️

Modeling objects for animation on Canvas

Quick and easy API, browser support is what makes Canvas appealing. But, as is often the case, simplicity is also a weakness. Without difficulty, for example, you can draw a rectangle, a circle, a line or hang an image. But developing useful content on this simple basis is a bit more difficult.

Image - the power of canvas

On the example of game development, an approach to animation and control of a game object is shown.

The motive to play around with the development of the game on Canvas were two of these examples:

Examples are illustrative, but, after a couple of minutes chasing the balls and tearing half a dozen nets, the excitement was gone. There is no plot, no goal, no sense - in the end, cool, but not interesting.
')

Prologue


Working with Canvas was described in the publication of the HTML page on Canvas . In general, the exact same approach, including architecture and development tools, was applied here.

Compatibility was tested on FF (Windows, Linux Mint), CR and IE (Windows). Check on the available gadgets, too, was, but without a special result (about this at the end).

GitHub code:
Arcad
Spots
Tens
Tetr

GitHub demo (with offline mode):
Arcad
Spots
Tens
Tetr

Game is model management


The game consists of: game model, model management functional and model display functionality. The ultimate essence of the game comes down to changing the model.

The model consists of game objects. Objects from basic primitives: rectangles, circles, lines.

The model and the functionality that provides the dynamics of the model, do not know anything about how the model will be shown. The Canvas functionality only knows how to display the static state of the model at the time of rendering. In the process of drawing, model changes are not allowed. Fortunately, this is not necessary to follow (provided by single-loop event loop-a).

The Canvas API is native, and therefore fast, and can display a large number of base primitives in real time. Therefore, the number (tens and even hundreds) of simple objects is not so critical for Canvas. But complex computational objects implemented in JavaScript, this is something that should be avoided. The same can be attributed to the implementation of algorithms for changing the model. The simpler and easier they are (the more native, in this case, the Canvas API will be taken into account), the better. If, according to the game, it is necessary to implement complex and / or time-consuming computational algorithms in it, then, for such purposes, you can use web workers (not discussed here).

The design of the model must be considered carefully from the very beginning. If the development process has to significantly change the model, this will entail reworking the control and display code.

Model development


Tetris made a good demo to test the approach (I'm not the first here).

Model concept:


Description of the well and cell size:
APELSERG.CONFIG.SET.CellSize = 20; //      APELSERG.CONFIG.SET.WellColumn = 5; //      APELSERG.CONFIG.SET.WellRow = 20; //      

Base object (cell):
 APELSERG.MODEL.Cell = function (cellRow, cellCol, cellColor) { this.Row = cellRow; //     this.Col = cellCol; //     this.Color = cellColor; //  } 

Object (block):
 APELSERG.MODEL.Block = function (blockType) { this.type = blockType; //     this.idx = 0; //     ( ) this.cells = [[], [], [], []]; //     } 

The redundancy of the block model is visible, but due to this, two tasks are solved: 1. a block with all states is formed at a time (it is easy to continue to control the block); 2. cell colors are preserved when the block is rotated.

Blocks are created in game mode:
 APELSERG.MODEL.GetNewBlock = function() { var newBlock = APELSERG.CONFIG.PROC.NextBlock; //       APELSERG.CONFIG.PROC.NextBlock = APELSERG.MODEL.GetBlock(); //   -   if (!APELSERG.MODEL.CheckBlockCross(newBlock)) { // ,     APELSERG.CONFIG.PROC.GameStop = true; //   window.clearTimeout(APELSERG.CONFIG.PROC.TimeoutID); //   APELSERG.CONFIG.SetResult(); //   } return newBlock; } 

New unit:
 APELSERG.MODEL.GetBlock = function() { var blockType = APELSERG.MODEL.GetBlockType(); //     var newBlock = new APELSERG.MODEL.Block(blockType); //     var newColor = ""; //   var baseRow = 1; var baseCol = Math.round(APELSERG.CONFIG.SET.WellColumn / 2); switch (blockType) { case 101: //-- [1] newColor = APELSERG.MODEL.GetCellColor(); newBlock.cells[0][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor); newBlock.cells[1][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor); newBlock.cells[2][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor); newBlock.cells[3][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor); break; //      

Here you can add blocks of any configuration. The main thing is to assign a new type to the block and add this type to the array in the function APELSERG.MODEL.GetBlockType ().

Model management


Management takes place from the keyboard in a typical way:
 window.addEventListener('keydown', function (event) { ... } 

Blocks fall on the timer (but not on window.requestAnimationFrame). The APELSERG.CONFIG.SET.LevelTick array stores the time period value for the current level:
 APELSERG.MAIN.Animation = function (makeStep) { if (makeStep) { APELSERG.MODEL.BlockShift('DOWN'); //     -       } APELSERG.CANVA.WellRewrite(APELSERG.CONFIG.PROC.CellPool); if (!APELSERG.CONFIG.PROC.GameStop && !APELSERG.CONFIG.PROC.GamePause) { APELSERG.MAIN.RequestAnimationFrame(function () { APELSERG.MAIN.Animation(true); }); } } APELSERG.MAIN.RequestAnimationFrame = function (callback) { if (APELSERG.CONFIG.PROC.FastDownFlag) { //    DOWN APELSERG.CONFIG.PROC.TimeoutID = window.setTimeout(callback, 10); } else { APELSERG.CONFIG.PROC.TimeoutID = window.setTimeout(callback, APELSERG.CONFIG.SET.LevelTick[APELSERG.CONFIG.SET.Level]); } } 

To check whether a block can move or rotate in a given direction, a virtual block is created and conditions are checked. If the verification conditions are met, the real block is moved to a new location. Check function:
 APELSERG.MODEL.CheckBlockCross = function(block) { var canShift = true; //--    //-- for (var n = 0 in block.cells[block.idx]) { if (block.cells[block.idx][n].Col < 1 || block.cells[block.idx][n].Col > APELSERG.CONFIG.SET.WellColumn || block.cells[block.idx][n].Row < 1 || block.cells[block.idx][n].Row > APELSERG.CONFIG.SET.WellRow) { canShift = false; break; } } //--    //-- if (canShift) { for (var n = 0 in block.cells[block.idx]) { for (var q = 0 in APELSERG.CONFIG.PROC.CellPool) { var cell = APELSERG.CONFIG.PROC.CellPool[q]; if (block.cells[block.idx][n].Col == cell.Col && block.cells[block.idx][n].Row == cell.Row) { canShift = false; break; } } if (!canShift) { break; } } } return canShift; } 

Block offset occurs by simply assigning cell numbers:
 APELSERG.MODEL.ShiftBlockColumn = function(block, num) { for (var x = 0 in block.cells) { for (var n = 0 in block.cells[x]) { block.cells[x][n].Col = block.cells[x][n].Col + num; } } } 

When the bottom is reached, the cells from the block are moved to the pool of the well with the function APELSERG.MODEL.DropBlockToPool (). At the same time points are awarded.

The functionality that is implemented, but, in my opinion, was not very successful (it is not in the settings):


Rendering on Canvas


The APELSERG.CANVA.WellRewrite () function is responsible for drawing the model on the Canvas. It is very simple and well documented. All it does is clear the Canvas and consistently draw the model primitives.

User interface


Settings, Results, Help

After the model came to life, it became clear that for the holistic game of one revived model is not enough. This is how the UI modules appeared:

This is a typical dynamic DOM, perhaps not the most successful (the code is simple, I will not describe).

Local storage

To maintain interest in the game, you need to store settings, results, and perhaps offline mode is useful. For storage, localStorage is used. Technologically, everything is implemented in a typical way, but it is useful to trace the connection with global objects APELSERG.CONFIG.SET and APELSERG.CONFIG.RESULT.

A few notes:

The configuration is stored in two objects:

The storage name must be unique and is formed from a combination of several static variables:
 APELSERG.CONFIG.SET.Version = "0-1-0" APELSERG.CONFIG.SET.LocalStorageName = "APELSERG-ArcadPlain"; APELSERG.CONFIG.GetLocalStorageConfigName = function () { return APELSERG.CONFIG.SET.LocalStorageName + "-Config-" + APELSERG.CONFIG.SET.Version; } 

The configuration is saved when it changes. This is done simply (even for a separate function is not drawn). In the function APELSERG.UI.ApplySettings () (module UI), added two lines:
 var configName = APELSERG.CONFIG.GetLocalStorageConfigName(); localStorage[configName] = JSON.stringify(APELSERG.CONFIG.SET); 

When the application starts, the presence of the saved configuration in localStorage is checked and, if the configuration has been saved, it is restored:
 APELSERG.CONFIG.GetConfigOnLoad = function () { if (APELSERG.CONFIG.PROC.LoadFromWeb) { var configName = APELSERG.CONFIG.GetLocalStorageConfigName(); if (localStorage[configName] !== undefined) { APELSERG.CONFIG.SET = JSON.parse(localStorage[configName]); } } } 

LocalStorage may be empty or not used at all. Empty localStorage happens: 1. at the first start; 2. if not saved; 3. if has been cleared. Configuration cleaning may be needed mainly during the development process. For example, the configuration has changed - variables have been added or removed, and the application continues to work, as if it does not see any changes, as the old configuration object is restored from the storage.

If the application was launched from a local disk, the local storage is disabled. This is done because browsers do not support this mode very well. But the rest of the functionality of the application is preserved. Starting from a local disk is controlled at startup:
 window.location.protocol == "file:" ? APELSERG.CONFIG.PROC.LoadFromWeb = false : APELSERG.CONFIG.PROC.LoadFromWeb = true; 

Results are stored in APELSERG.CONFIG.RESULT. Functionally, the storage of results is identical to the storage configuration.

Autonomous work

The offline mode (Application Cache or AppCache) allows you to continue working with the web application when the Internet is turned off. In general terms, setting up different offline conditions can be quite complicated. But, in our case, this is one of the simplest procedures.

You need to prepare a manifest file for offline mode (game_arcad_plain.appcache.txt):
 CACHE MANIFEST
 # Ver 0.1.0
 # 001
 game_tetr_plain.htm
 game_tetr_plain_canva.js
 game_tetr_plain_config.js
 game_tetr_plain_lang.js
 game_tetr_plain_main.js
 game_tetr_plain_model.js
 game_tetr_plain_model_blocks.js
 game_tetr_plain_ui.js


It is necessary to add a link to this file in the HTML element of the web page:
 <html manifest="game_arcad_plain.appcache.txt"> 

Thin moment with the extension "txt". The “appcache” or “manifest” extension is recommended with the text / cache-manifest MIME type. The demo is done this way because it was curiously lazy .

"# 001" is needed to reload files on the client at the initiative of the server. If files are updated on the server, they will not get to the client until the manifest file changes. And what can be changed in it? - a comment on "# 002".

Other games


After the first game was developed, the rest of the games were stamped on the model. 80 percent of the code remained identical, and the changes concerned, basically, only models (the model and control code became simpler). Therefore, to describe these games separately does not make sense, except for a few nuances:


Brief test findings


What well:


What is not very good:


useful links


HTML5 Canvas
HTML5 Local Storage
HTML5 Application Cache

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


All Articles