Логин :
Пароль :
Зарегистрироваться
Закрыть
Вход / Регистрация
По дате добавления
По сложности

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

Аватар новости
Создание игры - сложный, но интересный процесс.
Сначала определяемся с тем, какую игру мы хотим: двухмерную, трехмерную, платформер, РПГ, гонки и д.р.

В моем случае я хотел сделать игру наподобии Gravity Defied.

Таким образом наша игра - двухмерный симулятор.

Затем обдумываем технологию реализации. Для веб, выбор адекватных средств небольшой : WebGL или двухмерная в Canvas.

Игра двухмерная, поэтому будем создавать игру в Canvas.

Потом можно будет и переделать для 3-x мерного пространства, а пока оставим олдскул


Итак, создание canvas


Эта строчка создаст элемент, пригодный для рисования на нём.

<canvas id='canvas' width="840px" height="600px"></canvas>


Задаем начальные переменные



Во-первых это

canvas

, хранящая в себе ссылку на наш DOM-объект и

context

, для возможности рисования на "хосте".

image и bg

- изображения для персонажа и фона соответственно.

globalX

- смещение двухмерного мира относительно камеры по оси X.

finishTime, startTime

- переменные, хранящие время начала этапа и конца.

level

- массив точек поверхности (по этим точка будет строится кривая "земли")

keyBoard={}

массив зажатых клавиш.


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],
];


Устанавливаем функцию на загрузку страницы


В ней мы инициализируем нужные переменные, задаем обработчики событий на клик, нажатие клавиш.
Также загружаем изображения игрока и фона.

$(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="http://gohtml.ru/images/news/104-lovely-mountain.jpg";
bg.onload=repaint;

image=new Image();
image.src="http://gohtml.ru/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




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);
console.log("Y1", Y1, "; Y2", Y2);
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 с помощью этой функции.
Также задается ускорение игроку при перерисовке холста.

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;
}



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


Так как стандартной функции для рисования повернутого изображения нет, то создаём такую универсальную функцию.

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].

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, или стрелками ← ↑ →








Спасибо за внимание.
Задавайте свои вопросы в комментариях.
Случайные уроки по этой теме:
Новогодние снежинки на JS
Cookie в JS
Функции
Гирлянды на JS
Возможности WebGL. 3D в браузере. Библиотека Three.js
Для написания комментария
войдите или зарегистрируйтесь