文章目录
- 七、支持移动设备的横向卷轴游戏
- 准备
- 7.1 角色的简单移动
- 7.2 背景
- 7.3 加入敌人与帧数控制
- 7.4 碰撞、计分、重新开始
- 7.5 手机格式
- 7.6 全屏模式
- 7.7 存在的问题
- 附录
素材可以去一位大佬放在github的源码中直接下,见附录。
七、支持移动设备的横向卷轴游戏
使用前面我们所学习的部分,组合成为一个游戏。
是否玩过《疯狂喷气机》(手游)这类游戏,该部分试着做一个类似与它的简单的横板游戏。
准备
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript 2D Game</title>
<link rel="stylesheet" href="./stylte.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<img src="./player.png" id="playerImage" alt="playerImage">
<img src="./backgroundImage.png" id="backgroundImage" alt="backgroundImage">
<img src="./worm.png" id="enemyImage" alt="enemyImage">
<script src="./script.js"></script>
<script src="./script.js"></script>
</body>
</html>
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.speed
if (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.speed
if (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.speed
if (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%);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript 2D Game</title>
<link rel="stylesheet" href="./stylte.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<img src="./player.png" id="playerImage" alt="playerImage">
<img src="./backgroundImage.png" id="backgroundImage" alt="backgroundImage">
<img src="./worm.png" id="enemyImage" alt="enemyImage">
<button id="fullScreenButton">Toggle Fullscreen</button>
<script src="./script.js"></script>
</body>
</html>
function toggleFullScreen(){
if(!document.fullscreenElement){
canvas.requestFullscreen().then().catch(err=>{
alert(`错误,切换全屏模式失败:${err.message}`)
})
}else{
document.exitFullscreen()
}
}
fullScreenButton.addEventListener('click',toggleFullScreen)
7.7 存在的问题
- 碰撞盒太大了,我们可能需要移动和缩小,来让判定更准确,或者玩起来更容易
- 没有很好的填充满屏幕,需要相应的js算法来帮助
其他笔者未解决问题:
- 如上方式,在重新开始后,游戏角色动作”变快“(时间间隔仍旧一样)。
- 此外,我们重新开始后,必然立即刷一只怪物
- 一些浏览器的页面再切换后,我们隔一段时间再返回,可以刷出更多怪物
考虑如果自己通过循环来计数,是否可以解决部分问题。
附录
[1]源-素材地址
[2]源-视频地址
[3]搬运视频地址(JavaScript 游戏开发)
[4]github-视频的素材以及源码