Коптер

Создано на основе материалов с сайта HTML Game Example.

Напишем простую игру, напоминающую "Коптер", когда объект уворачивается от препятствий, которые движутся навстречу.

Создадим игровое поле на основе Canvas.


<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
canvas {
    border: 1px solid #d3d3d3;
    background-color: #f1f1f1;
}
</style>
</head>

<body onload="startGame()">
<script>

function startGame() {
    myGameArea.start();
}

var myGameArea = {
    canvas : document.createElement("canvas"),
    start : function() {
        this.canvas.width = 480;
        this.canvas.height = 270;
        this.context = this.canvas.getContext("2d");
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
    }
}

</script>

<p>Мы создали игровое поле.</p>

</body>
</html>

Функция startGame() вызывает метод start() объекта myGameArea. Метод start() создаёт холст для игры с указанными размерами и вставляет его как первый дочерний элемент body.

Теперь добавим новый объект, которым будем управлять. Для простоты будем использовать красный прямоугольник.

Создадим конструктор component, который позволит нам добавлять игровые объекты в игровое поле. В качестве параметров нужно будет указать размеры, цвет и позицию.


function component(width, height, color, x, y) {
  this.width = width;
  this.height = height;
  this.x = x;
  this.y = y; 
  ctx = myGameArea.context;
  ctx.fillStyle = color;
  ctx.fillRect(this.x, this.y, this.width, this.height);
}

Добавим новую переменную copter для красного прямоугольник (это наш коптер) и создадим новый компонент игры в методе startGame().


var copter;

function startGame() {
    myGameArea.start();
	copter = new component(40, 30, "red", 10, 120);
}

В позиции (10, 120) появится красный квадрат размером 40х30 пикселей.

Для динамичной игры нам следует обновлять экран 50 раз в секунду, как кадры киноплёнки для создания иллюзии движения.

Создадим новую функцию updateGameArea().


function updateGameArea() {
  myGameArea.clear();
  copter.update();
}

В объекте myGameArea добавим интервал, который будет запускать функцию updateGameArea() каждую двадцатую миллисекунду (50 раз в секунду). А также добавим функцию clear(), которая будет очищать холст игры.


var myGameArea = {
    ...
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
		this.interval = setInterval(updateGameArea, 20);
    },
	clear : function() {
	    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
	}
}

В конструкторе component добавим функцию update(), который будет отвечать за отрисовку компонента.


function component(width, height, color, x, y) {
  this.width = width;
  this.height = height;
  this.x = x;
  this.y = y; 
  this.update = function(){
    ctx = myGameArea.context;
    ctx.fillStyle = color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
}

В функции updateGameArea() вызываем оба метода clear() и update(). В результате наших изменений наш красный прямоугольник рисуется и стирается 50 раз в секунду. На неподвижном объекте это пока не заметно.

Заставим объект двигаться по горизонтали, меняя его позицию x на один пиксель при каждом обновлении экрана.


function updateGameArea() {
  myGameArea.clear();
  copter.x += 1;
  copter.update();
}

Для понимания, зачем нужна функция clear(), временно закомментируйте строчку myGameArea.clear(); и посмотрите на результат. Объект будет двигаться, оставляя за собой шлейф. Получится большая красная полоса. Уберите комментарий, чтобы восстановить первоначальный вариант.

Вернёмся к нашему коптеру. Мы можем менять его размеры, начальную позицию и цвет. Кстати, цвет можно задавать не только предопределёнными именами, но и в формате RGBA.


// полупрозрачный квадрат
copter = new component(30, 30, "rgba(0, 0, 255, 0.5)", 10, 20);

Если вам нужны дополнительные объекты, то добавляем их по аналогии


var copter2, copter3;

function startGame() {
    copter = new component(40, 30, "red", 10, 120);
	// зададим новым объектам другие цвета и позицию
    copter2 = new component(40, 30, "blue", 10, 170);
    copter3 = new component(40, 30, "green", 10, 60);
    myGameArea.start();
}

function updateGameArea() {
  myGameArea.clear();
  copter.x += 1;
  copter.update();
  copter2.update();
  copter3.update();
}

Двигаться объекты также могут в разных направлениях.


function updateGameArea() {
  myGameArea.clear();
  // по горизонтали
  copter.x += 1;
  
  // по диагонали вниз
  copter2.x += 1;
  copter2.y += 1;
  
  // по диагонали вверх
  copter3.x += 1;
  copter3.y -= 1;
  
  copter.update();
  copter2.update();
  copter3.update();
}

Добавим возможность управления объектом. Создадим четыре кнопки для движения объекта в разные стороны. Напишем функции для каждой кнопки для движения объекта в выбранном направлении.


function moveup() {
  copter.speedY -= 1; 
}

function movedown() {
  copter.speedY += 1; 
}

function moveleft() {
  copter.speedX -= 1;
}

function moveright() {
  copter.speedX += 1;
}

Добавим кнопки на страницу.


<button onclick="moveup()">UP</button>
<button onclick="movedown()">DOWN</button>
<button onclick="moveleft()">LEFT</button>
<button onclick="moveright()">RIGHT</button>

Сначала добавим два новых свойства в конструктор, которые помогут управлять скоростью движения. А также добавим функцию в этот же конструктор, который будет изменять значения скорости.


function component(width, height, color, x, y) {
  this.width = width;
  this.height = height;
  this.x = x;
  this.y = y; 
  this.speedX = 0;
  this.speedY = 0;
  this.update = function(){
    ctx = myGameArea.context;
    ctx.fillStyle = color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
  this.newPos = function() {
    this.x += this.speedX;
    this.y += this.speedY; 
  }
}

Функцию newPos() следует вызывать в updateGameArea() до рисования объекта.


function updateGameArea() {
  myGameArea.clear();
  copter.newPos()
  copter.update();
}

Если мы хотим, чтобы объект прекращал движение при отпускании кнопки, а при нажатой продолжал движение, то следует видоизменить код для кнопок.


function stopMove() {
  copter.speedX = 0;
  copter.speedY = 0; 
}

<button onmousedown="moveup()" onmouseup="stopMove()" ontouchstart="moveup()">UP</button>
<button onmousedown="movedown()" onmouseup="stopMove()" ontouchstart="movedown()">DOWN</button>
<button onmousedown="moveleft()" onmouseup="stopMove()" ontouchstart="moveleft()">LEFT</button>
<button onmousedown="moveright()" onmouseup="stopMove()" ontouchstart="moveright()">RIGHT</button>

Добавим поддержку клавиатуры. Управлять будем клавишами-стрелками. В коде нужно проверять нажатия и отпускания нужных клавишей.


var myGameArea = {
    canvas : document.createElement("canvas"),
    start : function() {
        this.canvas.width = 480;
        this.canvas.height = 270;
        this.context = this.canvas.getContext("2d");
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
		this.interval = setInterval(updateGameArea, 20);
		window.addEventListener('keydown', function (e) {
            myGameArea.key = e.keyCode;
        })
        window.addEventListener('keyup', function (e) {
            myGameArea.key = false;
        })
    },
	clear : function() {
	    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
	}
}

function updateGameArea() {
  myGameArea.clear();
  
  copter.speedX = 0;
  copter.speedY = 0; 
  if (myGameArea.key && myGameArea.key == 37) {copter.speedX = -1; }
  if (myGameArea.key && myGameArea.key == 39) {copter.speedX = 1; }
  if (myGameArea.key && myGameArea.key == 38) {copter.speedY = -1; }
  if (myGameArea.key && myGameArea.key == 40) {copter.speedY = 1; }
  
  copter.newPos()
  copter.update();
}

Если нужна поддержка одновременных нажатий нескольких клавиш, то следует создать массив для объекта.


window.addEventListener('keydown', function (e) {
    myGameArea.keys = (myGameArea.keys || []);
    myGameArea.keys[e.keyCode] = true;
})
window.addEventListener('keyup', function (e) {
    myGameArea.keys[e.keyCode] = false; 
})

  if (myGameArea.keys && myGameArea.keys[37]) {copter.speedX = -1; }
  if (myGameArea.keys && myGameArea.keys[39]) {copter.speedX = 1; }
  if (myGameArea.keys && myGameArea.keys[38]) {copter.speedY = -1; }
  if (myGameArea.keys && myGameArea.keys[40]) {copter.speedY = 1; }

Теперь при нажатии сразу двух клавиш можно добиться движения по диагонали.

Если мы хотим управлять объектом с помощью мыши, то добавим код в myGameArea и обновим updateGameArea(). Объект будет следовать за курсором мыши (кстати, он не будет виден на холсте).


var myGameArea = {
    canvas : document.createElement("canvas"),
    start : function() {
        this.canvas.width = 480;
        this.canvas.height = 270;
		
		this.canvas.style.cursor = "none"; //hide the original cursor
		
        this.context = this.canvas.getContext("2d");
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
		this.interval = setInterval(updateGameArea, 20);

		window.addEventListener('mousemove', function (e) {
            myGameArea.x = e.pageX;
            myGameArea.y = e.pageY;
        })
    },
	clear : function() {
	    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
	}
}

function updateGameArea() {
  myGameArea.clear();
  if (myGameArea.x && myGameArea.y) {
    copter.x = myGameArea.x;
    copter.y = myGameArea.y; 
  }
  copter.update();
}

Если вместо мыши нужна поддержка тачскринов, то меняем код.


window.addEventListener('touchmove', function (e) {
    myGameArea.x = e.touches[0].screenX;
    myGameArea.y = e.touches[0].screenY;
})

function updateGameArea() {
  myGameArea.clear();
  if (myGameArea.touchX && myGameArea.touchY) {
    myGamePiece.x = myGameArea.x;
    myGamePiece.y = myGameArea.y; 
  }
  myGamePiece.update();
}

Можно нарисовать на холсте свои собственные кнопки - Controllers on The Canvas (https://www.w3schools.com/graphics/game_controllers.asp).

Теперь создадим препятствия (Obstacles).


var obstacle;

function startGame() {
    copter = new component(40, 30, "red", 10, 120);
	obstacle = new component(10, 200, "green", 300, 120);
    myGameArea.start();
}

function updateGameArea() {
  myGameArea.clear();
  obstacle.update();
  copter.newPos();
  copter.update();
}

Появится вертикальная зелёная полоска приблизительно по центру в нижней части. А сверху останется место для пролёта.

Коптер может свободно пролететь сквозь препятствие. Необходимо добавить обработку столкновений.

Любое столкновение - это конец игры. Создадим новый метод в конструкторе component, который будет отслеживать столкновения с другим объектами. Функция должна вызываться каждый раз при обновлении кадров (50 раз в секунду).

Также добавим метод stop() объекту myGameArea, который будет сбрасывать интервал.


function component(width, height, color, x, y) {
  this.width = width;
  this.height = height;
  this.speedX = 0;
  this.speedY = 0;
  this.x = x;
  this.y = y; 
  this.update = function(){
    ctx = myGameArea.context;
    ctx.fillStyle = color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
  this.newPos = function() {
    this.x += this.speedX;
    this.y += this.speedY; 
  },
  this.crashWith = function(otherobj) {
    var myleft = this.x;
    var myright = this.x + (this.width);
    var mytop = this.y;
    var mybottom = this.y + (this.height);
    var otherleft = otherobj.x;
    var otherright = otherobj.x + (otherobj.width);
    var othertop = otherobj.y;
    var otherbottom = otherobj.y + (otherobj.height);
    var crash = true;
    if ((mybottom < othertop) ||
    (mytop > otherbottom) ||
    (myright < otherleft) ||
    (myleft > otherright)) {
      crash = false;
    }
    return crash;
  }
}

var myGameArea = {
    canvas : document.createElement("canvas"),
    start : function() {
        this.canvas.width = 480;
        this.canvas.height = 270;
        this.context = this.canvas.getContext("2d");
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
		this.interval = setInterval(updateGameArea, 20);
    },
	clear : function() {
	    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
	}
	stop : function() {
        clearInterval(this.interval);
    }
}

function updateGameArea() {
    if (copter.crashWith(obstacle)) {
        myGameArea.stop();
    } else {
        myGameArea.clear();
        obstacle.update();
        copter.x += copter.speedX;
        copter.y += copter.speedY;    
        copter.update();
    }
}

Неподвижные препятствия не слишком интересны. Заставим их двигаться, создавая иллюзию полёта. Изменим значение свойство obstacle.x при каждом обновлении.


function updateGameArea() {
    if (copter.crashWith(obstacle)) {
        myGameArea.stop();
    } else {
        myGameArea.clear();
		obstacle.x += -1;
        obstacle.update();
        copter.x += copter.speedX;
        copter.y += copter.speedY;    
        copter.update();
    }
}

Теперь препятствие само движется на наш коптер. Если его обогнуть, то оно уйдёт за пределы экрана.

Одного препятствия мало. Добавим ещё. Нам понадобится свойство для подсчёта кадров и метод, который будет выполняться с заданной частотой кадра.


var myGameArea = {
    canvas : document.createElement("canvas"),
    start : function() {
        this.canvas.width = 480;
        this.canvas.height = 270;
        this.context = this.canvas.getContext("2d");
        document.body.insertBefore(this.canvas, document.body.childNodes[0]);
		this.frameNumber = 0;
		this.interval = setInterval(updateGameArea, 20);
    },
	clear : function() {
	    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
	},
	stop : function() {
        clearInterval(this.interval);
    }
}

function everyinterval(n) {
  if ((myGameArea.frameNumber / n) % 1 == 0) {return true;}
  return false;
}

Функция everyinterval() возвращает true, если текущий frameNumber совпадает с заданным интервалом.

Для создания множества препятствий понадобится массив.


var obstacles = [];

Внесём изменения в функцию updateGameArea().


function updateGameArea() {
    var x, y;
    for (i = 0; i < obstacles.length; i += 1) {
        if (copter.crashWith(obstacles[i])) {
            myGameArea.stop();
            return;
        } 
    }
    myGameArea.clear();
    myGameArea.frameNumber += 1;
    if (myGameArea.frameNumber == 1 || everyinterval(150)) {
        x = myGameArea.canvas.width;
        y = myGameArea.canvas.height - 200;
        obstacles.push(new component(10, 200, "green", x, y));
    }
    for (i = 0; i < obstacles.length; i += 1) {
        obstacles[i].x += -1;
        obstacles[i].update();
    }
    copter.newPos();    
    copter.update();
}

Новое препятствие будет появляться на каждом 150 кадре. У нас обновление 50 кадров в секунду, таким образом препятствие будет появляться каждую третью секунду. В цикле проверяем столкновение и останавливаем игру в случае необходимости.

Добавим случайность в формировании препятствий.


function updateGameArea() {
    var x, y;
    for (i = 0; i < obstacles.length; i += 1) {
        if (copter.crashWith(obstacles[i])) {
            myGameArea.stop();
            return;
        } 
    }
    myGameArea.clear();
    myGameArea.frameNumber += 1;
 if (myGameArea.frameNumber == 1 || everyinterval(150)) {
        x = myGameArea.canvas.width;
        minHeight = 20;
        maxHeight = 200;
        height = Math.floor(Math.random() * (maxHeight - minHeight + 1) + minHeight);
        minGap = 50;
        maxGap = 200;
        gap = Math.floor(Math.random() * (maxGap - minGap + 1) + minGap);
        obstacles.push(new component(10, height, "green", x, 0));
        obstacles.push(new component(10, x - height - gap, "green", x, height + gap));
    }
    for (i = 0; i < obstacles.length; i += 1) {
        obstacles[i].x += -1;
        obstacles[i].update();
    }
    copter.newPos();    
    copter.update();
}

Отобразим подсчёт очков. Добавим табло на холст. А также изменим конструктор, чтобы он принимал новые значения для отображения текста.


var score;

function startGame() {
    copter = new component(40, 30, "red", 10, 120);
	score = new component("30px", "Consolas", "black", 280, 40, "text");
    myGameArea.start();
}

function component(width, height, color, x, y, type) {
  this.type = type;
  this.width = width;
  this.height = height;
  this.speedX = 0;
  this.speedY = 0;
  this.x = x;
  this.y = y; 
  this.update = function(){
    ctx = myGameArea.context;
	if (this.type == "text") {
      ctx.font = this.width + " " + this.height;
      ctx.fillStyle = color;
      ctx.fillText(this.text, this.x, this.y);
    } else {
    ctx.fillStyle = color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
    }
  }
  this.newPos = function() {
    this.x += this.speedX;
    this.y += this.speedY; 
  }
  this.crashWith = function(otherobj) {
    var myleft = this.x;
    var myright = this.x + (this.width);
    var mytop = this.y;
    var mybottom = this.y + (this.height);
    var otherleft = otherobj.x;
    var otherright = otherobj.x + (otherobj.width);
    var othertop = otherobj.y;
    var otherbottom = otherobj.y + (otherobj.height);
    var crash = true;
    if ((mybottom < othertop) ||
    (mytop > otherbottom) ||
    (myright < otherleft) ||
    (myleft > otherright)) {
      crash = false;
    }
    return crash;
  }
}

Для подсчёта будем использовать значение frameNumber.


function updateGameArea() {
    var x, y;
    for (i = 0; i < obstacles.length; i += 1) {
        if (copter.crashWith(obstacles[i])) {
            myGameArea.stop();
            return;
        } 
    }
    myGameArea.clear();
    myGameArea.frameNumber += 1;
    if (myGameArea.frameNumber == 1 || everyinterval(150)) {
        x = myGameArea.canvas.width;
        minHeight = 20;
        maxHeight = 200;
        height = Math.floor(Math.random() * (maxHeight - minHeight + 1) + minHeight);
        minGap = 50;
        maxGap = 200;
        gap = Math.floor(Math.random() * (maxGap - minGap + 1) + minGap);
        obstacles.push(new component(10, height, "green", x, 0));
        obstacles.push(new component(10, x - height - gap, "green", x, height + gap));
  }
    for (i = 0; i < obstacles.length; i += 1) {
        obstacles[i].x += -1;
        obstacles[i].update();
    }
	score.text = "SCORE: " + myGameArea.frameNumber;
	score.update();
    copter.newPos();    
    copter.update();
}
https://www.w3schools.com/graphics/game_images.asp

Demo

Реклама