Создание игры - сложный, но интересный процесс.
Сначала определяемся с тем, какую игру мы хотим: двухмерную, трехмерную, платформер, РПГ, гонки и д.р.
В моем случае я хотел сделать игру наподобии Gravity Defied.
Таким образом наша игра - двухмерный симулятор.
Затем обдумываем технологию реализации. Для веб, выбор адекватных средств небольшой : WebGL или двухмерная в Canvas.
Игра двухмерная, поэтому будем создавать игру в Canvas.
Итак, создание canvas
Эта строчка создаст элемент, пригодный для рисования на нём.
Задаем начальные переменные
Во-первых это canvas, хранящая в себе ссылку на наш DOM-объект и context, для возможности рисования на "хосте".
Устанавливаем функцию на загрузку страницы
В ней мы инициализируем нужные переменные, задаем обработчики событий на клик, нажатие клавиш.
Также загружаем изображения игрока и фона.
Он содержит в себе свойства игрового объекта:
Перерисовка холста
Происходит с частотой 33 кадра в секунду.
Объекты, поверхность, фон отображаются на canvas с помощью этой функции.
Также задается ускорение игроку при перерисовке холста.
Так как стандартной функции для рисования повернутого изображения нет, то создаём такую универсальную функцию.
Функция клонирования javascript-объектов.
Необходима, так как присваивание вида a=level[3] не клонирует объект level[3] в "а", а создаст ссылку на него.
В таком случае при изменении "а", изменится и level[3].
Пример приложения
Управляется клавишами WAD, или стрелками ← ↑ →
Спасибо за внимание.
1 | <canvas id= 'canvas' width= "840px" height= "600px" ></canvas> |
- 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); } |
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(); } |
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; } |