2D игра на JS. Canvas, поворот изображения.

Создание игры - сложный, но интересный процесс. Сначала определяемся с тем, какую игру мы хотим: двухмерную, трехмерную, платформер, РПГ, гонки и д.р. В моем случае я хотел сделать игру наподобии Gravity Defied. Таким образом наша игра - двухмерный симулятор. Затем обдумываем технологию реализации. Для веб, выбор адекватных средств небольшой : WebGL или двухмерная в Canvas. Игра двухмерная, поэтому будем создавать игру в Canvas. Итак, создание canvas Эта строчка создаст элемент, пригодный для рисования на нём.
1
<canvas id='canvas' width="840px" height="600px"></canvas>
Задаем начальные переменные Во-первых это canvas, хранящая в себе ссылку на наш DOM-объект и context, для возможности рисования на "хосте".
  • globalX - смещение двухмерного мира относительно камеры по оси X.
  • image и bg - изображения для персонажа и фона соответственно.
  • finishTime, startTime - переменные, хранящие время начала этапа и конца.
  • level - массив точек поверхности (по этим точка будет строится кривая "земли")
  • keyBoard={} массив зажатых клавиш.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var canvas, context,
  //изображение игрока и фона
  image, bg,
  keyBoard = {}, globalX = 0, gameOver = false,
  finishTime = 0, startTime = new Date().getTime();
 
//массив точек X,Y поверхности
var level = [
  [-1000, 100], [-500, 0], [210, 10], [250, -100], [300, -100],
  [350, 10], [1500, 40], [1600, -140], [1700, 40], [3700, 200], [4800, 80],
  [5500, 150], [5600, 90], [5700, -500], [6000, 40], [6800, 250],
  [7400, 150], [7550, 150], [7600, -160], [7700, -160], [7950, 60],
  [8700, 260], [10700, 100], [13000, 300],
];
Устанавливаем функцию на загрузку страницы В ней мы инициализируем нужные переменные, задаем обработчики событий на клик, нажатие клавиш. Также загружаем изображения игрока и фона.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
$(function () {
  canvas = document.getElementById("canvas");
  context = canvas.getContext("2d");
  context.strokeStyle = "#ffaa00";
  context.lineWidth = 60;
  context.lineCap = "round";
  context.fillStyle = "#fff";
  context.textAlign = "center";
  context.textBaseline = "middle";
  canvas.onclick = function () {
    //Событие сработает, если игра закончена
    //игра считается законченой, если gameOver!=false
    if (gameOver) {
      player.x = 0;
      player.y = 0;
      player.speed = 0;
      globalX = 0;
      gameOver = false;
      startTime = new Date().getTime();
    }
  };
  //Создаем и загружаем изображения
  bg = new Image();
  bg.src = "/images/news/104-lovely-mountain.jpg";
  bg.onload = repaint;
 
  image = new Image();
  image.src = "/images/news/109-person.png";
  image.onload = function () {
    player = new gameObject(context, image);
  };
 
  //При движении мыши перерисовываем наш холст
  canvas.onmousemove = function (e) {/*repaint(e.offsetX,e.offsetY);*/
  }
 
  document.onkeydown = function (e) {
    console.log(e.keyCode);
    keyBoard[e.keyCode] = true;
 
    switch (e.keyCode) {
      case 87:
      case 38:
      case 32:
      case 65:
      case  37:
      case  68:
      case 39:
        return false;
      default:
        break;
    }
  }
  document.onkeyup = function (e) {
    keyBoard[e.keyCode] = false;
  }
  //Заставляет Canvas перерисовываться каждые 30-тысячных секунды.
  //Примерно 33 кадра в секунду
  setInterval(repaint, 30);
});

Создаем класс игрового объекта


Он содержит в себе свойства игрового объекта:
  • контекст, на котором рисуется: 

    context

  • изображение объекта: 

    image

  • положение в мире: 

    x,y

  • горизонтальное отражение объекта: 

    flip

  • предоставляемое ускорение: 

    acceleration

  • логическая переменная, указывающая на то, что объект глобальный: 

    globalObject

  • текущее ускорение: 

    speedX, speedY

  • скорость перерасчета положения, скорости и т.д: 

    updateRate


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
var gameObject = function (context, image, x, y) {
  this.context = context;
  this.image = image;
  this.x = x || 0;
  this.y = y || 0;
  this.flip = 1;
  this.acceleration = 4;
  this.globalObject = false;
  this.speedX = this.speedY = this.rotation = 0;
  this.updateRate = 20;
 
  //Получение наклона объекта
  this.getAngle = function () {
    var X1 = this.x - globalX;
    var X2 = this.x - globalX + this.image.width;
    var Y1 = this.rayToGround(X1);
    var Y2 = this.rayToGround(X2);
    var A = Math.atan2(Y2 - Y1, X2 - X1) / Math.PI * 180;
    A = (A < 0) ? A + 360 : A;
    return A;
  }
 
  //Нахождение точек, между которыми находится объект
  this.getPoints = function (X) {
    X = (X || this.x - globalX || 0);
    var leftPoint = clone(level[0]);
    var rightPoint = clone(level[level.length - 1]);
    for (var i = 0; i < level.length; i++) {
      var e = clone(level[i]);
      if ((e[0] <= X) && (e[0] > leftPoint[0])) {
        leftPoint[0] = e[0];
        leftPoint[1] = e[1];
      }
      if ((e[0] > X) && (e[0] < rightPoint[0]) && (e[0] != leftPoint[0])) {
        rightPoint[0] = e[0];
        rightPoint[1] = e[1];
      }
    }
    ;
    return [leftPoint, rightPoint];
  }
 
  //Получения высоты поверхности в данной точке
  this.rayToGround = function (X) {
    X = (X || this.x - globalX || 0);
    var points = this.getPoints();
    var leftPoint = points[0];
    var rightPoint = points[1];
    var percent = (X - leftPoint[0]) / (rightPoint[0] - leftPoint[0]);
    var top = (leftPoint[1]) * (1 - percent) + (rightPoint[1]) * percent;
    return top;
  }
 
  //Проверка находится-ли объект на земле
  this.isGrounded = function (X, Y) {
    X = (X || this.x - globalX || 0);
    Y = (Y || this.y || 0);
    if (!this.image || !this.context) return true;
    if (Y + (Y == this.y ? this.speedY : 0) - this.rayToGround() <= 0) {
      //this.y=this.rayToGround();
      return true;
    }
    return false;
  }
 
  //Перестчет скорости, положения и поворота объекта
  this.update = function () {
    if (this.isGrounded()) {
      this.speedY = 0;
      this.speedX -=
        (Math.abs(this.rotation) > 180 ? -(360 + this.rotation) : -this.rotation) / 15;
      this.speedX /= 1.1;
      this.y = this.rayToGround();
      this.rotation = -this.getAngle() - this.speedX / 3;
    } else {
      this.y += this.speedY;
      this.speedY -= 1;
    }
    if ((this.x > canvas.width * 0.45 || this.speedX > 0) &&
      (this.speedX < 0 || this.x < this.context.canvas.width * 0.55 - this.image.width)) {
      this.x += this.speedX;
    }
  }
  //Функция перерисовки объекта
  this.repaint = function () {
    if (this.speedX < -2)
      this.flip = -1;
    else if (this.speedX > 2)
      this.flip = 1;
    drawRotated(
      this.context,
      this.image,
      this.x - this.image.width / 2 + (this.globalObject === true ? globalX : 0),
      this.context.canvas.height - this.y - this.image.height,
      this.rotation,
      this.flip);
  }
  //Интервал вызывает функцию this.update, которая
  //делает перерасчет скорости, положения и т.д.
  this.interval = setInterval(function (self) {
    return function () {
      self.update();
    }
  }(this), this.updateRate);
}
Перерисовка холста Происходит с частотой 33 кадра в секунду. Объекты, поверхность, фон отображаются на canvas с помощью этой функции. Также задается ускорение игроку при перерисовке холста.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
function repaint(X,Y){
  if((keyBoard[87] || keyBoard[38] || keyBoard[32]) && player.isGrounded()) {
    player.speedY+=13;
  }
  if((keyBoard[65] || keyBoard[37]) && player.isGrounded()) {
    player.speedX-=player.acceleration;
  }
  if((keyBoard[68] || keyBoard[39])&& player.isGrounded()) {
    player.speedX+=player.acceleration;
  }
    
  if(bg.width){
      context.drawImage(bg,globalX%bg.width,0);
      if(globalX>0){
          context.drawImage(bg,globalX%bg.width-bg.width,0);
      }else if(globalX<0){
          context.drawImage(bg,globalX%bg.width+bg.width,0);
      }
  }else{
      context.clearRect(0,0,canvas.width,canvas.height);
  }
                             
  //Рисование "земли" (линий)
  context.beginPath();
  for(var i=0; i<level.length; i++){
    var e=clone(level[i]);
    context.lineTo(e[0]+globalX,canvas.height-e[1]);
    context.moveTo(e[0]+globalX,canvas.height-e[1]);
  };
  context.closePath();
  context.stroke();
  context.fill();
  
  //Отрисовка игрока
  if(player) player.repaint();
  //Измненение смещение игрового мира, относительно камеры (холста Canvas)
  if(player.x+player.image.width>canvas.width*0.55 && player.speedX>0){
      globalX-=player.speedX;
  }else if(player.x<canvas.width*0.45 && player.speedX<0){
      globalX-=player.speedX;
  }
    
  //Отрисовка потраченного времени
  context.beginPath();
  context.arc(canvas.width/2, canvas.height/2,50,0,2*Math.PI);
  context.closePath();
  context.fillStyle="rgba(0,0,0,0.1)";
  context.fill();
  
  context.lineWidth=1;
  context.font="40px Roboto";
  context.fillStyle="#fff";
  var textDate=(((new Date().getTime()-startTime)/1000)+"0000").substr(0,5);
  context.fillText(textDate, canvas.width/2, canvas.height/2);
  context.fillStyle="#000";
  
  //Проверка не упал-ли игрок в яму
  if(player.y<0 && !gameOver){
    finishTime=0;
    gameOver="Проигрыш! Вы упали в яму!";
  }
  //Проверка на завершение уровня
  if(player.x-globalX>level[level.length-1][0]-canvas.width && !gameOver){
    finishTime=new Date().getTime();
    gameOver="Выигрыш! Вы победили!";
  }
  //Проверка gameOver, отрисовка текста
  if(gameOver){
      context.clearRect(0,0,canvas.width,canvas.height);
      context.fillText(gameOver, canvas.width/2, canvas.height/2);
      if(finishTime){
          context.fillText("Потраченое время : "+
              (finishTime-startTime)/1000,
              canvas.width/2,
              canvas.height/2+40
          );
          context.fillStyle="#FD2";
          context.fillText(
            (finishTime-startTime)/1000<14? "★★★" :
            (finishTime-startTime)/1000<14.5? "★★☆""★☆☆",
            canvas.width/2, canvas.height/2+100);
          context.fillStyle="#000";
      }
      context.fillText("Кликните для перезапуска",
          canvas.width/2,
          canvas.height/2+40+(finishTime?120:0)
      );
  }
  context.lineWidth=60;
}

Функция поворота и отрисовки изображения


Так как стандартной функции для рисования повернутого изображения нет, то создаём такую универсальную функцию.
1
2
3
4
5
6
7
8
9
function drawRotated(context,image,x,y,degrees,flip){
    if(!context || !image) return;
    context.save();
    context.translate(x+image.width/2,y+image.height/2);
    context.rotate(degrees*Math.PI/180);
    context.scale(flip,1);
    context.drawImage(image,-image.width/2,-image.height/2);
    context.restore();
}
Функция клонирования javascript-объектов. Необходима, так как присваивание вида a=level[3] не клонирует объект level[3] в "а", а создаст ссылку на него. В таком случае при изменении "а", изменится и level[3].
1
2
3
4
5
6
7
8
function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}
Пример приложения Управляется клавишами WAD, или стрелками ← ↑ →
Спасибо за внимание.
Если статья Вам показалась незаконченной или Вы знаете как её улучшить, пожалуйста сообщите мне e@gohtml.ru