JavaScript游戏开发(3)(笔记)
创始人
2024-02-21 06:30:39

文章目录

  • 七、支持移动设备的横向卷轴游戏
    • 准备
    • 7.1 角色的简单移动
    • 7.2 背景
    • 7.3 加入敌人与帧数控制
    • 7.4 碰撞、计分、重新开始
    • 7.5 手机格式
    • 7.6 全屏模式
    • 7.7 存在的问题
  • 附录

素材可以去一位大佬放在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();});

7.1 角色的简单移动

我们通过如下代码控制角色移动

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

如下,我们完成了通过箭头移动角色

在这里插入图片描述

7.2 背景

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

7.3 加入敌人与帧数控制

笔者修改了视频的一些代码,并修改了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);});

在这里插入图片描述

7.4 碰撞、计分、重新开始

我们碰撞盒采用圆形,来做简单的碰撞检测

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

在这里插入图片描述

7.5 手机格式

我们进入浏览器的开发者模式,将浏览器设置为手机。

*{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();  

7.6 全屏模式

#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)

在这里插入图片描述

7.7 存在的问题

  1. 碰撞盒太大了,我们可能需要移动和缩小,来让判定更准确,或者玩起来更容易
  2. 没有很好的填充满屏幕,需要相应的js算法来帮助

其他笔者未解决问题:

  1. 如上方式,在重新开始后,游戏角色动作”变快“(时间间隔仍旧一样)。
  2. 此外,我们重新开始后,必然立即刷一只怪物
  3. 一些浏览器的页面再切换后,我们隔一段时间再返回,可以刷出更多怪物

考虑如果自己通过循环来计数,是否可以解决部分问题。

附录

[1]源-素材地址
[2]源-视频地址
[3]搬运视频地址(JavaScript 游戏开发)
[4]github-视频的素材以及源码

相关内容

热门资讯

江苏高考作文题目及:乐水者新... 江苏高考作文题目及:乐水者新说 篇一乐水者新说水,是生命之源,也是人类赖以生存的必需品。然而,水资源...
高考冬奥人物热点作文素材(最... 高考冬奥人物热点作文素材 篇一标题:高考冬奥人物:励志与奋斗的化身近年来,随着冰雪运动的兴起和中国冬...
我的高考作文【精简6篇】 我的高考作文 篇一 我的高考作文 篇二我的高考作文 篇三  距离201x高考还剩90天……  青春,...
全国新高考Ⅰ卷高考优秀作文【... 全国新高考Ⅰ卷高考优秀作文 篇一:尊重他人是我们的责任尊重他人是我们的责任在当今社会,人们之间的相互...
鼓舞士气的高考句子【精彩6篇... 鼓舞士气的高考句子 篇一高考是每个学生都要经历的一场考试,它对于每个人来说都至关重要。在备考阶段,很...