Создание игры - сложный, но интересный процесс.
Сначала определяемся с тем, какую игру мы хотим: двухмерную, трехмерную, платформер, РПГ, гонки и д.р.
В моем случае я хотел сделать игру наподобии 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;} |