素材可以去一位大佬放在github的源码中直接下,见附录。
使用前面我们所学习的部分,组合成为一个游戏。
是否玩过《疯狂喷气机》(手游)这类游戏,该部分试着做一个类似与它的简单的横板游戏。
html
JavaScript 2D Game
![]()
![]()
css
body{background: black;
}#canvas1{position: absolute;top:50%;left: 50%;transform: translate(-50%,-50%);border: 5px solid white;
}#playerImage,#backgroundImage,#enemyImage{display: none;
}
JavaScript
window.addEventListener('DOMContentLoaded',function(){const canvas = this.document.getElementById('canvas1');const ctx = canvas.getContext('2d');canvas.width = 800;canvas.height = 720;let enemies = [];// 输入处理class InputHandler{}class Player{}class Background{}class Enemy{}function handleEnemies(){}function displayStatusText(){}function animate(){requestAnimationFrame(animate);}animate();});
我们通过如下代码控制角色移动
window.addEventListener('DOMContentLoaded',function(){const canvas = this.document.getElementById('canvas1');const ctx = canvas.getContext('2d');canvas.width = 800;canvas.height = 720;let enemies = [];// 输入处理class InputHandler{constructor(){this.keys = [];// 加入上、下、左、右按键,此处使用 indexof保证键唯一,就无视本次输入window.addEventListener('keydown',e=>{if ((e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight') &&this.keys.indexOf(e.key) === -1) {this.keys.push(e.key)}console.log(e.key,this.keys);});// 移除按键window.addEventListener('keyup',e=>{if( e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight'){this.keys.splice(this.keys.indexOf(e.key), 1)}console.log(e.key,this.keys);});}}class Player{constructor(gameWidth,gameHeight){this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.width = 200;this.height = 200;this.x = 0;this.y = this.gameHeight - this.height;this.image = playerImage;this.frameX = 0;this.frameY = 0;// 速度// x轴this.speedX = 0;// y轴this.speedY = 0;// 重量this.weight = 1;}draw(context){context.fillStyle = 'white';context.fillRect(this.x,this.y,this.width,this.height);context.drawImage(this.image,this.frameX*this.width,this.frameY*this.height,this.width,this.height,this.x,this.y,this.width,this.height);}update(input){// 检测X轴按键if(input.keys.indexOf('ArrowRight') > -1){this.speedX = 5;}else if(input.keys.indexOf('ArrowLeft') > -1){this.speedX = -5;}else{this.speedX = 0;}// 检测Y轴按键,且只能从地面上起跳if(input.keys.indexOf('ArrowUp') > -1 && this.onGround()){this.speedY = -32;}this.x = this.x + this.speedX;this.y = this.y + this.speedY;// 避免出界if(this.x < 0){this.x = 0}else if(this.x > this.gameWidth - this.width){this.x = this.gameWidth - this.width;}// 跳跃限制if(!this.onGround()){this.speedY += this.weight;this.frameY = 1;}else{this.speedY = 0;this.frameY = 0;}// 避免陷入地面if(this.y > this.gameHeight - this.height){this.y = this.gameHeight - this.height;}}onGround(){return this.y >= this.gameHeight - this.height;}}class Background{}class Enemy{}function handleEnemies(){}function displayStatusText(){}const input = new InputHandler();const player = new Player(canvas.width,canvas.height);function animate(){ctx.clearRect(0,0,canvas.width,canvas.height);player.draw(ctx);player.update(input);requestAnimationFrame(animate);}animate();});
如下,我们完成了通过箭头移动角色
class Background{constructor(gameWidth, gameHeight) {this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.image = backgroundImage;this.x = 0;this.y = 0;this.width = 2400;this.height = 720;this.speed = 7;}draw(context) {context.drawImage(this.image, this.x, this.y, this.width, this.height);context.drawImage(this.image, this.x + this.width, this.y, this.width, this.height);}update() {this.x -= this.speedif (this.x < 0 - this.width){this.x = 0;} }}function animate(){ctx.clearRect(0,0,canvas.width,canvas.height);background.draw(ctx);background.update();player.draw(ctx);player.update(input);requestAnimationFrame(animate);}
笔者修改了视频的一些代码,并修改了player中的一些代码
window.addEventListener('DOMContentLoaded',function(){const canvas = this.document.getElementById('canvas1');const ctx = canvas.getContext('2d');canvas.width = 800;canvas.height = 720;let enemies = [];// 输入处理class InputHandler{constructor(){this.keys = [];// 加入上、下、左、右按键,此处使用 indexof保证键唯一,就无视本次输入window.addEventListener('keydown',e=>{if ((e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight') &&this.keys.indexOf(e.key) === -1) {this.keys.push(e.key)}});// 移除按键window.addEventListener('keyup',e=>{if( e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight'){this.keys.splice(this.keys.indexOf(e.key), 1)}});}}class Player{constructor(gameWidth,gameHeight){this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.width = 200;this.height = 200;this.x = 0;this.y = this.gameHeight - this.height;this.image = playerImage;this.frameX = 0;this.frameY = 0;this.maxFrame = 8;// 速度// x轴this.speedX = 0;// y轴this.speedY = 0;// 重量this.weight = 1;//动画20帧this.fps = 20;this.frameTimer = 0;this.frameInterval = 1000/this.fps;}draw(context){context.drawImage(this.image,this.frameX*this.width,this.frameY*this.height,this.width,this.height,this.x,this.y,this.width,this.height);}update(input,deltaTime){// 检测X轴按键if(input.keys.indexOf('ArrowRight') > -1){this.speedX = 5;}else if(input.keys.indexOf('ArrowLeft') > -1){this.speedX = -5;}else{this.speedX = 0;}// 检测Y轴按键,且只能从地面上起跳if(input.keys.indexOf('ArrowUp') > -1 && this.onGround()){this.speedY = -32;this.frameY = 1;this.frameX = 0;this.maxFrame = 5;this.y = this.y + this.speedY;}if(this.frameTimer > this.frameInterval){if(this.frameX >= this.maxFrame){this.frameX = 0;}else{this.frameX++;}this.frameTimer = 0;}else{this.frameTimer += deltaTime; }this.x = this.x + this.speedX;// 避免出界if(this.x < 0){this.x = 0}else if(this.x > this.gameWidth - this.width){this.x = this.gameWidth - this.width;}// 跳跃限制if(!this.onGround()){this.speedY += this.weight;this.y = this.y + this.speedY;if(this.onGround()){this.y = this.gameHeight - this.height;this.speedY = 0;this.frameY = 0;this.maxFrame = 8;}}}// 是否在地面onGround(){return this.y >= this.gameHeight - this.height;}}class Background{constructor(gameWidth, gameHeight) {this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.image = backgroundImage;this.x = 0;this.y = 0;this.width = 2400;this.height = 720;this.speed = 7;}draw(context) {context.drawImage(this.image, this.x, this.y, this.width, this.height);context.drawImage(this.image, this.x + this.width, this.y, this.width, this.height);}update() {this.x -= this.speedif (this.x < 0 - this.width){this.x = 0;} }}class Enemy{constructor(gameWidth, gameHeight) {this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.width = 160;this.height = 119;this.image = enemyImage;this.x = this.gameWidth;this.y = this.gameHeight - this.height;this.frameX = 0;this.maxFrame = 5;this.speed = 8;// 敌人动画20帧this.fps = 20;this.frameTimer = 0;this.frameInterval = 1000/this.fps;this.markedForDeletion = false;}draw(context) {context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height)}update(deltaTime) {if(this.frameTimer > this.frameInterval){if(this.frameX >= this.maxFrame){this.frameX = 0;}else{this.frameX++;}this.frameTimer = 0;}else{this.frameTimer += deltaTime; }if(this.x < 0 - this.width){this.markedForDeletion = true;}this.x -= this.speed;}}function handleEnemies(deltaTime){if(enemyTimer > enemyInterval + randomEnemyInterval){enemies.push(new Enemy(canvas.width,canvas.height));randomEnemyInterval = Math.random()*1000 + 500;enemyTimer = 0;}else{enemyTimer += deltaTime;}let flag = false;enemies.forEach(e => {e.draw(ctx);e.update(deltaTime);if(!flag && e.markedForDeletion){flag = true;}})if(flag){enemies = enemies.filter(e=>!e.markedForDeletion);}}function displayStatusText(){}const input = new InputHandler();const player = new Player(canvas.width,canvas.height);const background = new Background(canvas.weight,canvas.height);let lastTime = 0;let enemyTimer = 0;let enemyInterval = 2000;// 让敌人刷出时间不可预测let randomEnemyInterval = Math.random()*1000 + 500;// 60帧,游戏画面的更新帧let frameTimer = 0;let frameInterval = 1000/60;function animate(timeStamp){const deltaTime = timeStamp - lastTime;lastTime = timeStamp; frameTimer += deltaTime; if(frameTimer > frameInterval){ctx.clearRect(0,0,canvas.width,canvas.height);background.draw(ctx);// background.update();handleEnemies(deltaTime);player.draw(ctx);player.update(input,deltaTime);frameTimer = 0;}requestAnimationFrame(animate);}animate(0);});
我们碰撞盒采用圆形,来做简单的碰撞检测
window.addEventListener('DOMContentLoaded',function(){const canvas = this.document.getElementById('canvas1');const ctx = canvas.getContext('2d');canvas.width = 800;canvas.height = 720;let enemies = [];// 输入处理class InputHandler{constructor(){this.keys = [];// 加入上、下、左、右按键,此处使用 indexof保证键唯一,就无视本次输入window.addEventListener('keydown',e=>{if ((e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight') &&this.keys.indexOf(e.key) === -1) {this.keys.push(e.key)}else if(e.key === 'Enter' && gameOver){gameReStart();}});// 移除按键window.addEventListener('keyup',e=>{if( e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight'){this.keys.splice(this.keys.indexOf(e.key), 1)}});}}class Player{constructor(gameWidth,gameHeight){this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.width = 200;this.height = 200;this.x = 0;this.y = this.gameHeight - this.height;this.image = playerImage;this.frameX = 0;this.frameY = 0;this.maxFrame = 8;// 速度// x轴this.speedX = 0;// y轴this.speedY = 0;// 重量this.weight = 1;//动画20帧this.fps = 20;this.frameTimer = 0;this.frameInterval = 1000/this.fps;}draw(context){context.strokeStyle = 'white';context.strokeRect(this.x,this.y,this.width,this.height);context.beginPath();context.arc(this.x + this.width/2,this.y+this.height/2,this.width/2,0,Math.PI*2);context.stroke();context.drawImage(this.image,this.frameX*this.width,this.frameY*this.height,this.width,this.height,this.x,this.y,this.width,this.height);}update(input,deltaTime){// 碰撞检测enemies.forEach(e=>{const dx = (e.x + e.width/2) - (this.x + this.width/2);const dy = (e.y + e.height/2) - (this.y + this.height/2);const distance = Math.sqrt(dx*dx + dy*dy);if(distance < e.width/2 + this.width/2){gameOver = true;}});// 检测X轴按键if(input.keys.indexOf('ArrowRight') > -1){this.speedX = 5;}else if(input.keys.indexOf('ArrowLeft') > -1){this.speedX = -5;}else{this.speedX = 0;}// 检测Y轴按键,且只能从地面上起跳if(input.keys.indexOf('ArrowUp') > -1 && this.onGround()){this.speedY = -32;this.frameY = 1;this.frameX = 0;this.maxFrame = 5;this.y = this.y + this.speedY;}if(this.frameTimer > this.frameInterval){if(this.frameX >= this.maxFrame){this.frameX = 0;}else{this.frameX++;}this.frameTimer = 0;}else{this.frameTimer += deltaTime; }this.x = this.x + this.speedX;// 避免出界if(this.x < 0){this.x = 0}else if(this.x > this.gameWidth - this.width){this.x = this.gameWidth - this.width;}// 跳跃限制if(!this.onGround()){this.speedY += this.weight;this.y = this.y + this.speedY;if(this.onGround()){this.y = this.gameHeight - this.height;this.speedY = 0;this.frameY = 0;this.maxFrame = 8;}}}// 是否在地面onGround(){return this.y >= this.gameHeight - this.height;}restart(){this.x = 0;this.y = this.gameHeight - this.height;this.frameInterval = 0;this.maxFrame = 8;this.frameY = 0;}}class Background{constructor(gameWidth, gameHeight) {this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.image = backgroundImage;this.x = 0;this.y = 0;this.width = 2400;this.height = 720;this.speed = 5;}draw(context) {context.drawImage(this.image, this.x, this.y, this.width, this.height);context.drawImage(this.image, this.x + this.width, this.y, this.width, this.height);}update() {this.x -= this.speedif (this.x < 0 - this.width){this.x = 0;} }restart(){this.x = 0;}}class Enemy{constructor(gameWidth, gameHeight) {this.gameWidth = gameWidth;this.gameHeight = gameHeight;this.width = 160;this.height = 119;this.image = enemyImage;this.x = this.gameWidth;this.y = this.gameHeight - this.height;this.frameX = 0;this.maxFrame = 5;this.speed = 8;// 敌人动画20帧this.fps = 20;this.frameTimer = 0;this.frameInterval = 1000/this.fps;this.markedForDeletion = false;}draw(context) {context.strokeStyle = 'white';context.strokeRect(this.x,this.y,this.width,this.height);context.beginPath();context.arc(this.x + this.width/2,this.y+this.height/2,this.width/2,0,Math.PI*2);context.stroke();context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height)}update(deltaTime) {if(this.frameTimer > this.frameInterval){if(this.frameX >= this.maxFrame){this.frameX = 0;}else{this.frameX++;}this.frameTimer = 0;}else{this.frameTimer += deltaTime; }if(this.x < 0 - this.width){this.markedForDeletion = true;score++;}this.x -= this.speed;}}function handleEnemies(deltaTime){if(enemyTimer > enemyInterval + randomEnemyInterval){enemies.push(new Enemy(canvas.width,canvas.height));randomEnemyInterval = Math.random()*1000 + 500;enemyTimer = 0;}else{enemyTimer += deltaTime;}let flag = false;enemies.forEach(e => {e.draw(ctx);e.update(deltaTime);if(!flag && e.markedForDeletion){flag = true;}})if(flag){enemies = enemies.filter(e=>!e.markedForDeletion);}}const input = new InputHandler();const player = new Player(canvas.width,canvas.height);const background = new Background(canvas.weight,canvas.height);let lastTime = 0;let enemyTimer = 0;let enemyInterval = 2000;// 让敌人刷出时间不可预测let randomEnemyInterval = Math.random()*1000 + 500;// 60帧,游戏画面的更新帧let frameTimer = 0;let frameInterval = 1000/60;let score = 0;let gameOver = false;function displayStatusText(context){context.textAlign = 'left';context.fillStyle = 'black';context.font = '40px Helvetica';context.fillText('score:'+score,20,50);context.fillStyle = 'white';context.font = '40px Helvetica';context.fillText('score:'+score,22,52);if(gameOver){context.textAlign = 'center';context.fillStyle = 'black';context.fillText('Game Over,press "Enter" to restart!',canvas.width/2,200);context.fillStyle = 'white';context.fillText('Game Over,press "Enter" to restart!',canvas.width/2,200);}}function animate(timeStamp){const deltaTime = timeStamp - lastTime;lastTime = timeStamp; frameTimer += deltaTime; if(frameTimer > frameInterval){ctx.clearRect(0,0,canvas.width,canvas.height);background.draw(ctx);background.update();handleEnemies(deltaTime);player.draw(ctx);player.update(input,deltaTime);displayStatusText(ctx);frameTimer = 0;}if(!gameOver){requestAnimationFrame(animate);}}animate(0);function gameReStart(){player.restart();background.restart();score = 0;enemies = [];gameOver = false;frameTimer = 0;enemyTimer = 0;lastTime = 0;randomEnemyInterval = Math.random()*1000 + 500;animate(0);}
});
我们进入浏览器的开发者模式,将浏览器设置为手机。
*{margin: 0;padding:0;box-sizing: border-box;
}
body{background: black;
}#canvas1{position: absolute;top:50%;left: 50%;transform: translate(-50%,-50%);border: 5px solid white;max-width: 100%;max-height: 100%;
}#playerImage,#backgroundImage,#enemyImage{display: none;
}
输入的指令如下
this.touchY = ''; // Y 轴滑动this.touchThreshold = 30 ;// 超过30认为滑动window.addEventListener('keydown', e => {if ((e.key === 'ArrowDown' ||e.key === 'ArrowUp' ||e.key === 'ArrowLeft' ||e.key === 'ArrowRight') &&this.keys.indexOf(e.key) === -1) {this.keys.push(e.key);}else if(e.key==='Enter'&&gameOver) restartGame()})// 手指、指针起始位置window.addEventListener('touchstart',e=>{this.touchY=e.changedTouches[0].pageY;})// 手指、指针移动中window.addEventListener('touchmove',e=>{const swipeDistance=e.changedTouches[0].pageY-this.touchY;if(swipeDistance<-this.touchThreshold && this.keys.indexOf('swipe up')===-1) {this.keys.push('swipe up');}else if(swipeDistance>this.touchThreshold && this.keys.indexOf('swipe down')===-1) {this.keys.push('swipe down');if(gameOver) restartGame();}}) // 手指、指针移动结束window.addEventListener('touchend',e=>{console.log(this.keys);this.keys.splice(this.keys.indexOf('swipe up'),1);this.keys.splice(this.keys.indexOf('swipe down'),1);})
判断时,只需要在执行处加入相应的标志即可。
同理,我们可以加入横向滑动操作,随着如果手指沿着X轴移动,我们可以认为X轴方向移动角色。X轴位移不为0则加入,为0则停止。
如果进入手机模式,滑动时,窗口也跟着滑动,可以试着加入如下代码
function stopScroll() {var html = document.getElementsByTagName('html')[0];var body = document.getElementsByTagName('body')[0];var o = {};o.can = function () {html.style.overflow = "visible";html.style.height = "auto";body.style.overflow = "visible";body.style.height = "auto";},o.stop = function () {html.style.overflow = "hidden";html.style.height = "100%";body.style.overflow = "hidden";body.style.height = "100%";}return o;}const scroll = stopScroll();scroll.stop();
#fullScreenButton{position: absolute;font-size: 20px;padding: 10px;top: 10px;left: 50%;transform: translateX(-50%);
}
JavaScript 2D Game
![]()
![]()
function toggleFullScreen(){if(!document.fullscreenElement){canvas.requestFullscreen().then().catch(err=>{alert(`错误,切换全屏模式失败:${err.message}`)})}else{document.exitFullscreen()}}fullScreenButton.addEventListener('click',toggleFullScreen)
其他笔者未解决问题:
考虑如果自己通过循环来计数,是否可以解决部分问题。
[1]源-素材地址
[2]源-视频地址
[3]搬运视频地址(JavaScript 游戏开发)
[4]github-视频的素材以及源码