Article directory
The material can go to a big guy and put it directly in the source code of github, see the appendix.
7. Horizontal scrolling games that support mobile devices
Using the parts we learned earlier, the combination becomes a game.
Have you ever played games like "Crazy Jet" (mobile game), this part tries to make a simple horizontal board game similar to it.
Prepare
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 Simple movement of characters
We control the movement of the character through the following code
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();
});
As follows, we have completed moving the character through the arrow
7.2 Background
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 Adding enemies and frame rate control
The author modified some codes of the video, and modified some codes in the 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 Collision, Scoring, Restart
Our collision box uses a circle for simple collision detection
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 Mobile phone format
We enter the developer mode of the browser and set the browser to the mobile phone.
*{
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;
}
The command entered is as follows
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);
})
When judging, you only need to add the corresponding flag at the execution point.
In the same way, we can add a horizontal sliding operation, and if the finger moves along the X-axis, we can think that the character moves in the X-axis direction. Add if the X-axis displacement is not 0, and stop if it is 0.
If you enter the mobile phone mode, when you slide, the window will also slide, you can try to add the following code
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 Full screen mode
#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 Existing problems
- The collision box is too big, we may need to move and shrink it to make the judgment more accurate, or make it easier to play
- The screen is not well filled, and the corresponding js algorithm is needed to help
Unresolved issues by other authors:
- In the above way, after restarting, the game character moves "faster" (the time interval is still the same).
- In addition, after we restart, we must immediately spawn a monster
- After switching the pages of some browsers, we will return after a while to spawn more monsters
Consider whether you can solve part of the problem if you count through the loop yourself.
appendix
[1] Source - material address
[2] Source - video address
[3] Handling video address (JavaScript game development)
[4] github - video material and source code