JavaScript game development (2) (notes)

The material can go to a big guy and put it directly in the source code of github, see the appendix.

5. Dummies shooter (simple game build)

This one project reminds me of Nintendo's "Duck Shooting".

Here we start officially making a playable game. We're going to use the mouse to shoot crows.

5.1 Preparation

First make the most basic preparations (remember to download the corresponding materials)

front page

<!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 Game</title>
    <link rel="stylesheet" href="./index.css">
</head>

<body>
    <canvas id="canvas1"></canvas>
    <script src="./script.js"></script>
</body>

</html>

css

body{
    
    
    background-image:linear-gradient( to bottom,red,green,blue);
    width: 100vw;
    height: 100vh;
    overflow: hidden;
}

canvas{
    
    
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let's set up the shelf first

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 存储乌鸦
let ravens = [];

class Raven{
    
    
    constructor(){
    
    
        // 乌鸦的大小
        this.width = 100;
        this.height = 50;
        
        // 绘制点 X
        this.x = canvas.width;
        // 绘制点 Y ,做差 避免Y轴超出范围
        this.y = Math.random()* (canvas.height - this.height);
        
        // x轴方向 速度0 ~ 8 
        this.directionX = Math.random()* 5 + 3;
        // y轴方向 速度-2.5~2.5 
        this.directionY = Math.random()* 5 - 2.5;
    }

    update(){
    
    
        // 向左移动
        this.x -= this.directionX;
    }
    
    draw(){
    
    
        // 绘制
        ctx.fillRect(this.x,this.y,this.width,this.height);
    }
}

const raven = new Raven(); 

function animate(){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    raven.update();
    raven.draw();

    requestAnimationFrame(animate);
}

animate();

insert image description here

5.2 Control the spawning frequency of crows

We want to control the spawning frequency of crows and reduce the refresh rate caused by different computer configurations. That is to control the refresh rate (frame number) of the game.

We first add the following variables

// 累计的间隔时间
let timeToNextRaven = 0;

// 间隔值 500毫秒后刷出新乌鸦
let ravenInterval = 500;

// 上一次调用的时间戳
let lastTime = 0;

Next, we first use a method to calculate the default frame number of the following requestAnimationFrame (how many seconds to refresh a frame)

// 此处通过requestAnimationFrame,自动放入绘制的时间帧,变量名可以随意
// 我们通过比较它们差别来得到1帧过了多少毫秒
function animate(timeStamp){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    // 当前时间 - 上一次时间 = 间隔时间
     let deltaTime = timeStamp - lastTime;
    console.log(deltaTime);
    
    requestAnimationFrame(animate);
}

can be seen. The author's computer currently refreshes every 8 milliseconds, that is, about 1000/8 = 125 frames per second.

insert image description here

Some computers may be every 16 milliseconds, and some may be every 13 milliseconds.


// 此处会自动放入当前时间帧,变量名可以随意
function animate(timeStamp){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    // 当前时间 - 上一次时间 = 间隔时间
    let deltaTime = timeStamp - lastTime;

    lastTime = timeStamp;

    // 累计记录 间隔时间
    timeToNextRaven += deltaTime;
    
    // 当间隔时间大于我们设置的时间后,刷出
    // 设置为大于,而不是等于,因为可能不会刚好为这个值
    if(timeToNextRaven > ravenInterval){
    
    
        ravens.push(new Raven());
        // 清空
        timeToNextRaven = 0;
    }
    // 创建一个新的数组,并循环它,[...array1,...array2],这是一个很常见的生成一个新的数组的模式。这个新的数组,将会包含array1,array2的所有子数组,相当于取出array1,array2中的每一个元素,并放入一个新的数组
    // 这样做的好处,之后就可以看到
    [...ravens].forEach(item => item.update());
    [...ravens].forEach(item => item.draw());
    requestAnimationFrame(animate);
}

animate(0);

5.3 Recycling unwanted crows

Next, recycle the excess crows.

class Raven{
    
    
    constructor(){
    
    
        // 乌鸦的大小
        this.width = 100;
        this.height = 50;
        
        // 绘制点 X
        this.x = canvas.width;
        // 绘制点 Y ,做差 避免Y轴超出范围
        this.y = Math.random()* (canvas.height - this.height);
        
        // x轴方向 速度0 ~ 8 
        this.directionX = Math.random()* 5 + 3;
        // y轴方向 速度-2.5~2.5 
        this.directionY = Math.random()* 5 - 2.5;

        // 是否需要回收
        this.markedForDeletion = false;
    }

    update(){
    
    
        // 向左移动
        this.x -= this.directionX;
        if(this.x < 0 - this.width){
    
    
            this.markedForDeletion = true;
        }
    }
    
    draw(){
    
    
        // 绘制
        ctx.fillRect(this.x,this.y,this.width,this.height);
    }
}

const raven = new Raven(); 

// 此处会自动放入当前时间帧,变量名可以随意
function animate(timeStamp){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    // 当前时间 - 上一次时间 = 间隔时间
    let deltaTime = timeStamp - lastTime;

    lastTime = timeStamp;

    // 累计记录 间隔时间
    timeToNextRaven += deltaTime;
    
    if(timeToNextRaven > ravenInterval){
    
    
        ravens.push(new Raven());
        // 清空
        timeToNextRaven = 0;
    }

    [...ravens].forEach(item => item.update());
    // 过滤 !item.markedForDeletion 为 false 的结果
    ravens = ravens.filter(item => !item.markedForDeletion);
    [...ravens].forEach(item => item.draw());
    requestAnimationFrame(animate);
}

animate(0);

5.4 Draw crow image and animation

We next introduce the image of a crow. Also add animation to it.

Note that in order to reduce the impact of the device, we should also add interval frames for the crow to avoid the problem of different movements and speeds of the crow caused by the device (the video part does not control the speed of the crow, but only controls the animation playback, and the author does not control it here. ). Therefore, we also pass in the interval time in the update place.

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 累计的间隔时间
let timeToNextRaven = 0;

// 间隔值 500毫秒后刷出新乌鸦
let ravenInterval = 500;

// 上一次调用的时间戳
let lastTime = 0;

// 存储乌鸦
let ravens = [];

class Raven{
    
    
    constructor(){
    
            
        
        // x轴方向 速度0 ~ 8 
        this.directionX = Math.random()* 5 + 3;
        // y轴方向 速度-2.5~2.5 
        this.directionY = Math.random()* 5 - 2.5;

        // 是否需要回收
        this.markedForDeletion = false;

        // 图像
        this.image = new Image();
        this.image.src = './raven.png';
        // 每一帧的大小
        this.spriteWidth = 271;
        this.spriteHeight = 194;

        // 随机化乌鸦大小
        this.sizeModifer = Math.random()*0.6 + 0.4;

        // 乌鸦的大小
        this.width = this.spriteWidth * this.sizeModifer;
        this.height = this.spriteHeight * this.sizeModifer;

        // 绘制点 X
        this.x = canvas.width;
        // 绘制点 Y ,做差 避免Y轴超出范围
        this.y = Math.random() *(canvas.height - this.height);
    
        // 乌鸦动画帧
        this.frame = 0;
        this.maxFrame = 4;    
           
        // 控制乌鸦的动画帧数
        // 累计时间
        this.timeSinceFlap = 0;
        // 当达到该时间,进入下一帧
        this.flapInterval = Math.random() * 50 + 50;
    }

    // 传入变化的帧数
    update(deltaTime){
    
    
        // 向左移动
        this.x -= this.directionX;
        // 如果在Y轴上快要废除屏幕了,就换成反方向
        if(this.y < 0 || this.y > canvas.height - this.height){
    
    
            this.directionY = this.directionY*-1;
        }
        this.y += this.directionY;

        if(this.x < 0 - this.width){
    
    
            this.markedForDeletion = true;
        }
        this.timeSinceFlap += deltaTime;


        if(this.timeSinceFlap <= this.flapInterval){
    
    

            return;
        }

        // 归零
        this.timeSinceFlap = 0;

        if(this.frame > this.maxFrame){
    
    
            this.frame = 0;
        }
        else{
    
    
            this.frame++;
        }
    }
    
    draw(){
    
    
        // 绘制
        ctx.strokeRect(this.x,this.y,this.width,this.height);
        ctx.drawImage(this.image,this.frame*this.spriteWidth,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width,this.height);
    }
}

const raven = new Raven(); 

// 此处会自动放入当前时间帧,变量名可以随意
function animate(timeStamp){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // 当前时间 - 上一次时间 = 间隔时间
    let deltaTime = timeStamp - lastTime;

    lastTime = timeStamp;

    // 累计记录 间隔时间
    timeToNextRaven += deltaTime;
    
    if(timeToNextRaven > ravenInterval){
    
    
        ravens.push(new Raven());
        // 清空
        timeToNextRaven = 0;
    }

    [...ravens].forEach(item => item.update(deltaTime));

    // 过滤 !item.markedForDeletion 为 false 的结果
    ravens = ravens.filter(item => !item.markedForDeletion);
    
    [...ravens].forEach(item => item.draw());

    requestAnimationFrame(animate);
}

animate(0);


insert image description here

5.5 scoring

Prepare variables for scoring and functions for plotting


// 计分
let score = 0;

// 全局字体
ctx.font = '50px  Impact'

// 绘制分数
function drawScore(){
    
    
    ctx.fillStyle = 'white';
    ctx.fillText('Score: '+score,50,75)
}


draw them in the animation, note that we have the score at the bottom, so draw it first

function animate(timeStamp){
    
    
	//...
	 drawScore();

    [...ravens].forEach(item => item.update(deltaTime));

    // 过滤 !item.markedForDeletion 为 false 的结果
    ravens = ravens.filter(item => !item.markedForDeletion);
    
    [...ravens].forEach(item => item.draw());
    //...
}

insert image description here

5.6 Simple collision detection

Next we add the click event to shoot the crow

The getImageData method helps us get the color of the clicked area

window.addEventListener('pointerdown',function(e){
    
    
    // 扫描数据 起点 x y 扫描的 宽 高
    const detectPixelColor = ctx.getImageData(e.x,e.y,1,1);
    console.log(detectPixelColor);
})

insert image description here

If the following error occurs, a cross-domain problem has occurred. (or the problem of canvas contamination)
insert image description here
Of course, there are many reasons for this problem. Most of the explanations on the Internet are:

When image resources are stored locally, there is no domain name by default, and the getImageData method of canvas is restricted by the same-origin policy. When using getImageData to process local images, the browser will judge that it is cross-domain and report an error.

There are many processing methods on the Internet. Here we solve this problem by sorting the pictures so that they will not be drawn together at the first time (the big crow is drawn first, and the small crow is drawn later). At this point the canvas contamination problem is avoided.

Therefore, we can sort by width (see the following code).

If coincidence drawing is unavoidable. At the same time, it is inconvenient for us to use cross-domain solutions. You can refer to- Solve the problem of canvas pollution


Through the above method, we can get the color of the clicked place. We add a new canvas (collision canvas) in html ```html ``` Next, we build this canvas in a similar way.
const collisionCanvas = document.getElementById('collisionCanvas');
const collisionCtx = collisionCanvas.getContext('2d');

collisionCanvas.width = window.innerWidth;
collisionCanvas.height = window.innerHeight;

Let's see what we want to do first.

Color on this collision canvas, and then through detection, we get the color of the upper component.

By comparing whether the color of the clicked point is the color of the crow itself, if it is, it means that it has hit.

Let's first draw the color and have a look. The effect of the collision box is as follows.

class Raven{
    
    
    constructor(){
    
            
        //...

        // 随机颜色,当然有可能会有两个乌鸦颜色相同,不过概率较低。虽然不严谨,但是使用较简单。
        // 如果想要杜绝可能性,可以检测碰撞体积,及检测是否有乌鸦绘图坐标在此处,优化的话也可以在animate函数处做手脚,而不用再来一次循环见检测部分的代码
        this.randomColor = [Math.floor(Math.random()*255) ,Math.floor(Math.random()*255),Math.floor(Math.random()*255)];
        this.color = `rgb(${
      
      this.randomColor[0]},${
      
      this.randomColor[1]},${
      
      this.randomColor[2]})`
    }
    
	draw(){
    
    
        collisionCtx.fillStyle = this.color;
        // 绘制
        collisionCtx.fillRect(this.x,this.y,this.width,this.height);
    	// ...
    }
    //...
}

function animate(timeStamp){
    
    
    ctx.clearRect(0,0,canvas.width,canvas.height);
    collisionCtx.clearRect(0,0,canvas.width,canvas.height);
    // ...
	if(timeToNextRaven > ravenInterval){
    
    
		ravens.push(new Raven());
	    // 清空
	    timeToNextRaven = 0;
	    ravens.sort(function(pre,post){
    
    
	    // 通过宽度排序,让大乌鸦先绘制
	    return pre.width - post.width;
	    })
    }	
	// ...
}

// 检测
window.addEventListener('click',function(e){
    
    
    // 扫描数据 起点 x y 扫描的 宽 高
    const detectPixelColor = collisionCtx.getImageData(e.x,e.y,1,1);
    let pc = detectPixelColor.data;
    ravens.forEach(item =>{
    
    
    	// 遍历是否有颜色相同的,有的话就说明击中了(碰撞画布在上,因此没打中就是0,我们也可以首先进行一判断优化(当然,也有可能随机到0)
    	// 因此视频给的检测逻辑有很大BUG,虽然出问题概率较低
        if(item.randomColor[0]===pc[0] && item.randomColor[1]===pc[1] && item.randomColor[2]===pc[2]){
    
    
            item.markedForDeletion = true;
            score++;
        }
    }) 
});

insert image description here

Then, we set the collision layer visibility to 0. That's it.

#collisionCanvas{
    
    
    opacity: 0;
}

In this way, the basic part of the game is completed.
insert image description here

5.7 Collision effects

The code logic used in the previous article is used. The sound effects also use the previous sound effect download address Magic SFX Sample .

let explosions = [];
class Explosion{
    
    
    constructor(x,y,size){
    
    
        this.image = new Image();
        this.image.src = './boom.png';
        this.spriteWidth = 200;
        this.spriteHeight = 179;
        this.size = size;
        this.x = x;
        this.y = y;
        this.frame = 0;
        this.sound = new Audio();
        this.sound.src = './Fire impact 1.wav';
        // 也是控制动画播放速度
        this.timeSinceLastFrame = 0;
        this.frameInterval = 200;
        this.markedForDeletion = false;
    }
    update(deltaTime){
    
    
        if(this.frame === 0){
    
    
            this.sound.play();
        }
        this.timeSinceLastFrame += deltaTime;
        if(this.timeSinceLastFrame > this.frameInterval){
    
    
            this.frame++;
            this.timeSinceLastFrame = 0;
            if(this.frame > 5){
    
    
                this.markedForDeletion = true;
            }
        }
    }
    draw(){
    
    
         ctx.drawImage(this.image,this.frame* this.spriteWidth,0,this.spriteWidth,this.spriteHeight,this.x,this.y - this.size/4,this.size,this.size);
    }
}

window.addEventListener('click',function(e){
    
    
    // 扫描数据 起点 x y 扫描的 宽 高
    const detectPixelColor = collisionCtx.getImageData(e.x,e.y,1,1);
    let pc = detectPixelColor.data;
    ravens.forEach(item =>{
    
    
        if(item.randomColor[0]===pc[0] && item.randomColor[1]===pc[1] && item.randomColor[2]===pc[2]){
    
    
            item.markedForDeletion = true;
            score++;
            // 放入爆炸特效
            explosions.push(new Explosion(item.x,item.y,item.width));
        }
    }) 
});


// 此处会自动放入当前时间帧,变量名可以随意
function animate(timeStamp){
    
    
    //...

    [...ravens,...explosions].forEach(item => item.update(deltaTime));

    // 过滤 !item.markedForDeletion 为 false 的结果
    ravens = ravens.filter(item => !item.markedForDeletion);
    explosions = explosions.filter(item => !item.markedForDeletion);
    
    [...ravens,...explosions].forEach(item => item.draw());

    requestAnimationFrame(animate);
}

insert image description here

5.8 Game over

If any crow escapes the screen, we set the game over. (Of course, the total number of crows can be set, and then the scoring mode can also be used)

let gameOver = false;

class Raven{
    
    
 	//...

    // 传入变化的帧数
    update(deltaTime){
    
    
        //...

        if(this.x < 0 - this.width){
    
    
            gameOver =true;
        }

        if(this.timeSinceFlap <= this.flapInterval){
    
    

            return;
        }

        // 归零
        this.timeSinceFlap = 0;

        if(this.frame > this.maxFrame){
    
    
            this.frame = 0;
        }
        else{
    
    
            this.frame++;
        }
    }
    
  //...
}



function animate(timeStamp){
    
    
    //...
    
    if(!gameOver){
    
    
        requestAnimationFrame(animate);
    }
    else{
    
    
        drawGameOver();
    }
}
}

function drawGameOver(){
    
    
    ctx.textAlign = 'center';
    // 字的阴影部分
    ctx.fillStyle = 'black';
    ctx.fillText('GAME OVER, \n your score is ' + score,canvas.width/2,canvas.height/2 );
    // 字
    ctx.fillStyle = 'white';
    ctx.fillText('GAME OVER, \n your score is ' + score,canvas.width/2,canvas.height/2 + 5);
}

//...

insert image description here

5.9 Extra Part - Adding Displacement Effects to the Crow

let particles = [];

class Particle{
    
    
    constructor(x,y,size,color){
    
    
        this.size = size;
        this.x = x + this.size/2;
        this.y = y + this.size/3;
        // 半径
        this.radius = Math.random() * this.size/10;
        // 最大半径
        this.maxRadius = Math.random()*20 + 35;
        this.markedForDeletion = false;
        this.speedX = Math.random()*1 + 0.5;
        this.color = color;
    }
    update(){
    
    
        this.x += this.speedX;
        this.radius += 0.6;
        if(this.radius > this.maxRadius){
    
    
            this.markedForDeletion = true;
        }
    }
    draw(){
    
    
    	ctx.save();
        // 透明度
        ctx.globalAlpha = 1 - this.radius/ this.maxRadius;
        // 绘制一条路径
        // 代表接下来一系列设置在绘制之前都是设置的是径
        ctx.beginPath();
        
        ctx.fillStyle = this.color;
        // 绘制圆 位置 x,y 半径 起始角度 结束角度
        ctx.arc(this.x,this.y,this.radius,0,Math.PI * 2);

        // 填充颜色
        ctx.fill();

        ctx.restore();
    }
}




class Raven{
    
    
 	//...

    // 传入变化的帧数
    update(deltaTime){
    
    
        //...

        // 归零
        this.timeSinceFlap = 0;

        if(this.frame > this.maxFrame){
    
    
            this.frame = 0;
        }
        else{
    
    
            this.frame++;
        }
                // 放入粒子
        particles.push(new Particle(this.x,this.y,this.width,this.color))
    }
    
  //...
}


// 此处会自动放入当前时间帧,变量名可以随意
function animate(timeStamp){
    
    
    //...

    
    
    [...particles,...ravens,...explosions].forEach(item => item.update(deltaTime));

    // 过滤 !item.markedForDeletion 为 false 的结果
    ravens = ravens.filter(item => !item.markedForDeletion);
    explosions = explosions.filter(item => !item.markedForDeletion);
    particles = particles.filter(item => !item.markedForDeletion);

    [...particles,...ravens,...explosions].forEach(item => item.draw());

    requestAnimationFrame(animate);
}

insert image description here

6. Enemy species (reference structure)

Extends is used in the video. Of course, whether it is from the perspective of design patterns or modern games, it is not recommended to use inheritance, but to use combination to store data and use interfaces to design implementation methods. The methods in the combination can be called externally in the form of delegation.

6.1 Preparation

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>game</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <canvas id="canvas1"></canvas>
    <img src="./enemy_worm.png" id="worm" alt="worm">
    <img src="./enemy_ghost.png" id="ghost" alt="ghost">
    <img src="./enemy_spider.png" id="spider" alt="spider">
    <script src="./script.js"></script>
</body>

</html>

css

#canvas1{
    
    
    border: 3px solid black;
    width: 500px;
    height: 800px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);

}
img{
    
    
    display: none;
}

js

// 保证DOM加载完毕
document.addEventListener('DOMContentLoaded',function(){
    
    
    const canvas = document.getElementById('canvas1');
    const ctx = canvas.getContext('2d');

    let lastTime = 1;

    // 60帧每秒,那么每帧就是 1/60 秒
    const  intervalTime = 1000/60;
    // 记录当前间隔时间
    let deltaTime = 0;

    canvas.width = 500;
    canvas.height = 800;

    // 存储游戏资源
    class Game{
    
    
        constructor(ctx,width,height){
    
    
            this.enemies=[];
            
            this.ctx = ctx;

            this.width = width;
            this.height = height;
            // 创建敌人间隔,5s一个
            this.enemyInterval = 5000;
            this.enemyTimer = 0;

        }
        update(deltaTime){
    
    
            if(this.enemyTimer > this.enemyInterval){
    
    
                this.#addNewEnemy();
                this.enemyTimer = 0;

                // 我们新增的时候在判断,是否有不需要绘制的敌人
                this.enemies = this.enemies.filter(Object => !Object.markedForDeletion)
            }
            else{
    
    
                this.enemyTimer+= deltaTime;
            }

            this.enemies.forEach(Object =>{
    
    
                Object.update();
            })
        }   
        draw(){
    
    
            this.enemies.forEach(Object =>{
    
    
                Object.draw();
            })
        }
        // 私有
        #addNewEnemy(){
    
    
            this.enemies.push(new Enemy(this));
        }
    }


    class Enemy{
    
    
        constructor(game) {
    
    
            this.game = game;

            this.x = this.game.width;
            this.y = Math.random()*this.game.height;

            this.width= 100;
            this.height= 100;
            // 删除标记
            this.markedForDeletion = false;
        }

        update(){
    
    
            this.x--;
            if(this.x < 0 - this.width){
    
    
                this.markedForDeletion = true;
            }
        }

        draw(){
    
    
            ctx.fillRect(this.x,this.y,this.width,this.height);
        }
    }

    const game = new Game(ctx,canvas.width,canvas.height);

    function animate(timeStamp){
    
    
        ctx.clearRect(0,0,canvas.width,canvas.height);
        
        // 计算时间过了多久,控制帧数
        deltaTime += timeStamp - lastTime;
        lastTime = timeStamp;

        // 我们将与运动相关的放入这里,通过实际时间控制速度
        if(deltaTime >intervalTime){
    
    
            game.update(deltaTime);
            deltaTime = 0;
        }


        game.draw();
        requestAnimationFrame(animate);
    }
    animate(0);
})

insert image description here

6.2 Add various enemies

After downloading the corresponding materials, add monsters such as worms.

Since the author controls the frame rate of the entire update, some of the following writing methods will be different from the video.

(The video uses the logic of time compensation, multiplying the motion speed by deltaTime, and making the total motion speed consistent through the difference in deltaTime)

// 保证DOM加载完毕
document.addEventListener('DOMContentLoaded',function(){
    
    
    const canvas = document.getElementById('canvas1');
    const ctx = canvas.getContext('2d');

    let lastTime = 1;

    // 60帧每秒,那么每帧就是 1/60 秒
    const  intervalTime = 1000/60;
    // 记录当前间隔时间
    let deltaTime = 0;

    canvas.width = 500;
    canvas.height = 800;

    // 存储游戏资源
    class Game{
    
    
        constructor(ctx,width,height){
    
    
            this.enemies=[];
            
            this.ctx = ctx;

            this.width = width;
            this.height = height;
            // 创建敌人间隔,1s一个
            this.enemyInterval = 1000;
            this.enemyTimer = 0;
        }
        update(deltaTime){
    
    
            if(this.enemyTimer > this.enemyInterval){
    
    
                this.#addNewEnemy();
                this.enemyTimer = 0;

                // 我们新增的时候在判断,是否有不需要绘制的敌人
                this.enemies = this.enemies.filter(Object => !Object.markedForDeletion)
            }
            else{
    
    
                this.enemyTimer+= deltaTime;
            }

            this.enemies.forEach(Object =>{
    
    
                Object.update();
            })
        }   
        draw(){
    
    
            this.enemies.forEach(Object =>{
    
    
                Object.draw();
            })
        }
        // 私有
        #addNewEnemy(){
    
    
            this.enemies.push(this.create(Math.floor(Math.random()*3)));
            // 从上到下绘制
            this.enemies.sort(function(a,b){
    
    
                return a.y - b.y;
            })
        }

        // 类似工厂模式
        create(e){
    
    
            switch(e){
    
    
                case 0:
                    return new Worm(this);
                case 1: 
                    return new Ghost(this);
                case 2:
                    return new Spider(this);

                default:
                    return new Enemy(this);
            }
        }
    }


    class Enemy{
    
    
        constructor(game) {
    
    
            this.game = game;

            this.x = this.game.width;
            this.y = Math.random()*this.game.height;

            this.width= 100;
            this.height= 100;

            // 运动速度
            this.speed = 0;

            // 删除标记
            this.markedForDeletion = false;
        }

        update(){
    
    
            this.x = this.x - this.speed;
            if(this.x < 0 - this.width){
    
    
                this.markedForDeletion = true;
            }
        }

        draw(){
    
    
            ctx.fillRect(this.x,this.y,this.width,this.height);
        }
    }


    // 蠕虫
    class Worm extends Enemy{
    
    
        // 如果父类有,子类又一次定义,就会覆盖
        constructor(game){
    
    
            super(game);
            this.spriteWidth = 229;
            this.spriteHeight = 171;

            // 这里直接读取的是html中的素材,这样我们就不需要从文件中重复new 一个对象 去读取了
            this.image = worm;

            this.width = this.spriteWidth/2;
            this.height = this.spriteHeight/2;

            this.y = this.game.height - this.height;

            this.speed = Math.random() * 0.1 + 2;
            
            // 动画逻辑
            this.frameX = 0;
            // 最大帧
            this.maxFrame = 5;
            // 1帧动一次
            this.frameInterval = intervalTime;
        }

        update(){
    
    
            super.update();
            // 动画帧
            if(this.frameX < this.maxFrame){
    
    
                this.frameX++;
            }
            else{
    
    
                this.frameX = 0;
            }
        }

        // 重载
        draw(){
    
    
            ctx.drawImage(this.image,this.frameX*this.spriteWidth,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width,this.height);
        }


    }

    // 幽灵
    class Ghost extends Enemy{
    
    
        // 如果父类有,子类又一次定义,就会覆盖
        constructor(game){
    
    
            super(game);
            this.spriteWidth = 261;
            this.spriteHeight = 209;

            this.image = ghost;
            this.width = this.spriteWidth/2;
            this.height = this.spriteHeight/2;

            this.speed = Math.random() * 0.2 + 4;
            
            this.y = Math.random()*(this.game.height* 0.6);

            // 修改y轴移动
            this.angle = 0;
            this.curve = Math.random()*3;

            // 动画逻辑
            this.frameX = 0;
            // 最大帧
            this.maxFrame = 5;
            // 1帧动一次
            this.frameInterval = intervalTime;
        }


        // 重载
        update(){
    
    
            super.update();
            this.y += Math.sin(this.angle) *this.curve;
            // 这里不限制角度最大值,因为不会变的太大,就会被回收
            this.angle += 0.04;


            // 动画帧
            if(this.frameX < this.maxFrame){
    
    
                this.frameX++;
            }
            else{
    
    
                this.frameX = 0;
            }
        }


        draw(){
    
    
            ctx.save();
            // 虚化
            ctx.globalAlpha = 0.7;
            ctx.drawImage(this.image,this.frameX*this.spriteWidth,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width,this.height);
            ctx.restore();
        }


    }

    // 蜘蛛
    class Spider extends Enemy{
    
    
        // 如果父类有,子类又一次定义,就会覆盖
        constructor(game){
    
    
            super(game);
            this.spriteWidth = 310;
            this.spriteHeight = 175;

            this.image = spider;
            this.width = this.spriteWidth/2;
            this.height = this.spriteHeight/2;


            this.x = Math.random() * this.game.width ;
            this.y = 0 - this.height;

            // 蜘蛛爬行方向以及速度
            this.vx = 0;
            this.vy = Math.random() * 0.1 + 2;
            this.maxLength = Math.random() * this.game.height;

            // 动画逻辑
            this.frameX = 0;
            // 最大帧
            this.maxFrame = 5;
            // 1帧动一次
            this.frameInterval = intervalTime;
        }


        // 重载
        update(){
    
    
            super.update();
            // 竖直移动
            this.y += this.vy;
            if(this.y > this.maxLength){
    
    
                this.vy = -this.vy;
            }
            if(this.y < 0 - this.height){
    
    
                this.markedForDeletion = true;
            }


            // 动画帧
            if(this.frameX < this.maxFrame){
    
    
                this.frameX++;
            }
            else{
    
    
                this.frameX = 0;
            }
        }


        draw(){
    
    
            ctx.beginPath();
            // 开始点
            ctx.moveTo(this.x + this.width/2,0);
            // 结束点
            ctx.lineTo(this.x + this.width/2,this.y + 10);
            ctx.stroke();
            ctx.drawImage(this.image,this.frameX*this.spriteWidth,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width,this.height);
        }


    }


    const game = new Game(ctx,canvas.width,canvas.height);

    function animate(timeStamp){
    
    
        ctx.clearRect(0,0,canvas.width,canvas.height);
        
        // 计算时间过了多久,控制帧数
        deltaTime += timeStamp - lastTime;
        lastTime = timeStamp;

        // 我们将与运动相关的放入这里,通过实际时间控制速度
        if(deltaTime >intervalTime){
    
    
            game.update(deltaTime);
            deltaTime = 0;
        }


        game.draw();
        requestAnimationFrame(animate);
    }
    animate(0);
})

insert image description here

appendix

[1] Source - material address
[2] Source - video address
[3] Handling video address (JavaScript game development)
[4] github - video material and source code

Guess you like

Origin blog.csdn.net/weixin_46949627/article/details/127722915