📜 ⬆️ ⬇️

HTML5 Canvas - creating arcade scrolling step by step

image

Foreword

This is an instruction for creating a game that I spent on a couple of evenings. The goal was not so much the creation of a worthy representative of the genre, as the verification of the capabilities of the Canvas and OOP in JavaScript. To make it more interesting, I set a condition - no external files with sprites, all graphics are drawn with built-in methods. Also, no frameworks or libraries are used. Just because in such a small game their use IMHO is not justified.

In general, the Canvas is a young platform, and may be of interest in transferring classical game concepts to it.
')
Task

Creating a classic arcade scroller, with an infinite number of enemies of different types that appear in waves. For the downed enemies give points, the best result is recorded.

Execution

Immediately I warn you, the article is quite long, because tried to describe every aspect of the game. Reference to the working example at the end.

First, let's declare the variables:

var c = document.getElementById('canv'); var ctx = c.getContext('2d'); var width = c.width; var height = c.height; var shipx = 100; var shipy = 100; var ship_w = 70;//  var ship_h = 15;// var r_border = width - ship_w;//  ,    var l_border = 0; var t_border = ship_h; var b_border = height; var bgr = new Array;//    var bullets = new Array; var enemies = new Array; var k_down = 0; var k_up = 0; var k_left = 0; var k_right = 0; var fires = 0;//   //   var vx = 0; var vy = 0; var cyclestep = 0; var game_over = 0; var score = 0; var cset = 0;//   


I think it’s pretty clear what the above-created variables are used for. If not, it will be obvious further from the code. Create your classes:

 function enemy(hp,dx,dy,type,x,y,width){ //  this.hp = hp; this.type = type; this.x = x; this.y = y; this.width = width; this.hwidth = width/2; this.dx = dx; this.dy = dy; } function bgObj(x,y,speed){ //   this.x = x; this.y = y; this.speed = speed; } function bullet(x,y){ //  this.x = x; this.y = y; this.dx = 12; this.dy = 0; } 


Note the hwidth parameter, which is nothing more than half the width of the object. It will be used in checking bullets in an enemy object. Well, dx and dy is the speed of the object along the corresponding axes.

Data about the player’s points will be stored, of course, in cookies (cookies). For convenience, let's declare the function that writes them:

 function setCookie(name,value){ var d = new Date(); d.setDate(d.getDate()+1); document.cookie = name + "="+ escape(value)+";expires="+d.toGMTString(); } 


As you can see, they are stored one day. Why one day ... I do not know. They are only needed until the php script (or what your page is on) compares them with the previously recorded record.
So, the main function of the game is draw ():

 function draw(){ //   if (k_left) vx -= 2; else if (k_right) vx +=2; if (k_up) vy -=4; else if (k_down) vy +=4; //  if(vx > 7) vx = 7; if(vy > 5) vy = 5; if (vy < -5) vy = -5; if (vx < -5) vx = -5; // if (fires == 1 && cyclestep % 8 == 0 && game_over != 1){ var b = new bullet(shipx+74,shipy-14); bullets.push(b); } draw_bg(); shoot(); if (game_over != 1) draw_ship(); move_ship(); draw_enemies(); enemy_ai(); if(game_over == 1){ ctx.fillStyle = "rgb(72,118,255)"; ctx.font = "bold 30px Arial"; ctx.textBaseline = "top"; ctx.fillText("GAME OVER",130,150); if(cset != 1){ var uname = prompt('Enter your name:','player'); if (uname == null || uname == "") uname = 'player'; setCookie('username',uname); setCookie('score',score); cset = 1; } } cyclestep++; if (cyclestep == 128) make_wave(1,4,30); if (cyclestep == 256){ cyclestep = 0; make_wave(2,4,20); } } 


The variable cyclestep stores the current iteration of the image drawing function. As you can see, this variable is used, for example, when processing a pressed fire key, when bullets fly out only if the current iteration is divided by 8 without a remainder. This is done so that the bullets do not fly out one by one, forming a line. Of course, it would be more correct to make a buffer so that the bullets would not disappear, but in order not to make an extra array, this implementation is quite acceptable. In addition, I appoint as you, and I almost never release the fire key in such games.

Now the background rendering function:

 function draw_bg(){ var distance; //""    ctx.fillStyle = "rgb(0,0,0)"; ctx.fillRect(0,0,width,height); for (var i = 0; i < bgr.length; i++){ distance = bgr[i].speed*40; if (distance < 100) distance = 100; //   ctx.fillStyle = "rgb("+distance+","+distance+","+distance+")"; //,    -   ctx.fillRect(bgr[i].x, bgr[i].y,1,1); bgr[i].x -=bgr[i].speed; if (bgr[i].x < 0){ //     ,   (  ) bgr[i].x += width; bgr[i].y = Math.floor(Math.random() * height); bgr[i].speed = Math.floor (Math.random() * 4) + 1; } } } 


Add stars to the array and set the frequency of starting the main drawing function every 40 milliseconds:

 for (var i = 1; i < 50; i++){ var b = new bgObj(Math.floor(Math.random()*height),Math.floor(Math.random()*width),Math.floor(Math.random()*4)+1); bgr.push(b); } setInterval("draw();", 40); 


You can now comment out the undefined functions in draw (), and start the application. A background with moving stars should appear. Now drawing the ship and points:

 function draw_ship(){ // var sbpaint = ctx.createLinearGradient(shipx,shipy,shipx,shipy-15);// sbpaint.addColorStop(0,'rgb(220,220,230)'); sbpaint.addColorStop(1,'rgb(170,170,180)'); ctx.fillStyle = sbpaint; ctx.beginPath(); ctx.moveTo(shipx,shipy); ctx.lineTo(shipx+60,shipy); ctx.lineTo(shipx+50,shipy-15); ctx.lineTo(shipx+10,shipy-15); ctx.lineTo(shipx,shipy); ctx.fill(); // var gpaint = ctx.createLinearGradient(shipx+50,shipy-12,shipx+70,shipy-12); gpaint.addColorStop(0,'rgb(190,190,200)'); gpaint.addColorStop(1,'rgb(120,120,130)'); ctx.fillStyle = gpaint; ctx.beginPath(); ctx.moveTo(shipx+50,shipy-13); ctx.lineTo(shipx+70,shipy-13); ctx.lineTo(shipx+70,shipy-8); ctx.lineTo(shipx+50,shipy-8); ctx.lineTo(shipx+50,shipy-13); ctx.fill(); //  ctx.fillStyle = "rgb(58,95,205)"; ctx.font = "14px Arial"; ctx.textBaseline = "top"; ctx.fillText("Score:"+score,3,3); } 


Of course, everything was painted first on paper. Here, for example, is a schematic representation of a ship (dimensions in pickels):

image

Since I promised not to use external files, I can only draw primitives. But so that the game does not look like a guest from the early 80s, I impose gradients on them.

To save space, I’ll give the following several features in a row:

 function move_ship(){//  -   shipx += vx; shipy +=vy; if (shipx>r_border){ shipx = r_border; vx = 0; } if (shipx<l_border){ shipx = l_border; vx = 0; } if (shipy>b_border){ shipy = b_border; vy = 0; } if (shipy<t_border){ shipy = t_border; vy = 0; } } function shoot(){ var dead_bullets = new Array; for (var i = 0; i < bullets.length; i++) { ctx.fillStyle = "rgb(173,216,230)"; ctx.fillRect(bullets[i].x,bullets[i].y,12,2);// , 122 //     if (bullets[i].x > width) dead_bullets.push(i); //    for (var j = 0;j < enemies.length;j++){ if (enemies[j].type > 0){ if (bullets[i].x >= enemies[j].x-enemies[j].hwidth && bullets[i].x < enemies[j].x+enemies[j].hwidth && bullets[i].y >= enemies[j].y-enemies[j].hwidth && bullets[i].y < enemies[j].y+enemies[j].hwidth){ enemies[j].hp--; } if(enemies[j].hp < 0){ enemies[j].type = -1; } } } bullets[i].x += bullets[i].dx; bullets[i].y += bullets[i].dy; } // ""  for (var i = dead_bullets.length-1; i >= 0; i--){ bullets.splice(dead_bullets[i],1); } } function make_wave(type,count,ewidth){ var h = Math.floor(Math.random()*(height-40))+40; for (var i = 0;i < count;i++){ var n = new enemy(2,Math.floor(Math.random()* -4)-1,0,type,width+i*20,h+i*21,ewidth); enemies.push(n); } } 


The function of checking the collision of an enemy with a bullet looks at first glance a bit ridiculous - it uses the parameter half the width of the enemy. But note that the function was sharpened under the enemies of a circular shape, in which the x, y coordinates are in the center of the circle, and the notorious half of the width is the radius. The negative type of enemy is an explosion. It is described with the rest of the vrazhin drawing:

 function draw_enemies(){ var dead_bad = new Array; for (var i = 1;i < enemies.length; i++){ // 1 -    if(enemies[i].type == 1){ var rg = ctx.createRadialGradient(enemies[i].x,enemies[i].y,0,enemies[i].x,enemies[i].y,enemies[i].hwidth); rg.addColorStop(0,"rgba(130,130,130,0.4)"); rg.addColorStop(0.5,"rgba(125,125,125,0.5)"); rg.addColorStop(1,"rgba(120,120,120,"+enemies[i].hp*0.4+")"); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(enemies[i].x,enemies[i].y,15,0,Math.PI*2,true); ctx.fill(); } // 2 -  if(enemies[i].type == 2){ var rg = ctx.createRadialGradient(enemies[i].x+10,enemies[i].y-10,0,enemies[i].x+10,enemies[i].y-10,enemies[i].width); rg.addColorStop(0,"rgba(240,240,0,"+enemies[i].hp*0.4+")"); rg.addColorStop(1,"rgba(240,240,0,0.6"); ctx.fillStyle = rg; ctx.beginPath(); ctx.moveTo(enemies[i].x,enemies[i].y); ctx.lineTo(enemies[i].x+10,enemies[i].y-20); ctx.lineTo(enemies[i].x+20,enemies[i].y); ctx.lineTo(enemies[i].x,enemies[i].y); ctx.fill(); } //!  if(enemies[i].type < 0){ ctx.fillStyle="rgb(250,250,250)"; ctx.beginPath(); ctx.arc(enemies[i].x,enemies[i].y,enemies[i].type * -4,0,Math.PI*2,true); ctx.fill(); } if(enemies[i].type < 0) enemies[i].type--; if(enemies[i].type < -4){ dead_bad.push(i); score+=2; } if(enemies[i].x + enemies[i].width < 0) dead_bad.push(i); if(enemies[i].y + 5 < 0) dead_bad.push(i); if(enemies[i].y > height+enemies[i].width) dead_bad.push(i); if(enemies[i].x < shipx+60 && enemies[i].x > shipx && enemies[i].y < shipy+15 && enemies[i].y > shipy) game_over = 1; enemies[i].x += enemies[i].dx; enemies[i].y += enemies[i].dy; } for (var i = 0;i < dead_bad.length;i++){ enemies.splice(dead_bad[i],1); } } function enemy_ai(){ for (var i = 0;i < enemies.length;i++){ if(enemies[i].type == 2){ if(cyclestep % 4 == 0){ if(shipy > enemies[i].y && enemies[i].y+20 < height && enemies[i].dy < 4 && enemies[i].x < width-100) enemies[i].dy++; if(shipy < enemies[i].y && enemies[i].y-20 > 0 && enemies[i].dy > -4 && enemies[i].x < width-100) enemies[i].dy--; } } } } 


The explosion, as you see, goes on as early as 4 iterations, increasing in diameter, and then disappears. I do not describe the collision check with the player and the AI ​​of the triangles, I think everything is obvious (the triangle chases the player, along the y coordinate). Well, finally, the processing of keystrokes:
 function get_key_down(e){ if (e.keyCode == 37) k_left = 1; if (e.keyCode == 38) k_up = 1; if (e.keyCode == 39) k_right = 1; if (e.keyCode == 40) k_down = 1; if(e.keyCode == 32) fires = 1; } function get_key_up(e){ if (e.keyCode == 37) k_left = 0; if (e.keyCode == 38) k_up = 0; if (e.keyCode == 39) k_right = 0; if (e.keyCode == 40) k_down = 0; if(e.keyCode == 32) fires = 0; } 


Everything! The working example is right here . The process of extracting cookies and writing them to a file on the server / reading the file on the screen will be omitted so that the article does not bloat at all.

Afterword


Of course, this example can be significantly improved. You can add difficulty levels, enemies, bosses, redraw the ship (to draw a semicircle on top - and you’ll have a full flying saucer).

Thanks for attention.

Outcome - conclusions
Chrome has chronic problems with gradients, so it’s not displayed as intended. The error is now caught and a simple fill is displayed if there is a problem with the gradient. It remains to fix a small problem with the cookies, but that's not the point. The main thing - Canvas is still very raw and seems to handle sprites better than some internal rendering methods. The experiment can be considered finished.
Thanks to all testers.

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


All Articles