JavaScript游戏开发(2)(笔记)

素材可以去一位大佬放在github的源码中直接下,见附录。

五、傻瓜射击游戏(简单的游戏构建)

这一个项目让我想到了任天堂的《打野鸭》。

此处我们开始正式制作一个可游玩的游戏。我们要用鼠标去射击乌鸦。

5.1 准备

首先做好最基础的准备(记得下载对应的素材)

首页

<!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;

我们先把架子搭好

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

在这里插入图片描述

5.2 控制乌鸦的刷出频率

我们想要控制乌鸦的刷出频率,减小因为电脑配置不同导致的刷新速度不同。也就是控制游戏的刷新频率(帧数)。

我们首先添加如下变量

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

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

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

接着,我们先用一个方式计算以下requestAnimationFrame的默认帧数(多少秒刷新一帧)

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

可以看到。笔者的电脑目前是8毫秒刷新一次,即每秒 1000/8 = 125 帧左右。

在这里插入图片描述

有的电脑可能是16毫秒一次,有的可能是13毫秒一次。


// 此处会自动放入当前时间帧,变量名可以随意
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 回收不需要的乌鸦

接下来,回收多余的乌鸦。

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 绘制乌鸦图像以及动画

我们接下来引入乌鸦的图像。同时为其加上动画。

注意,为了减小设备影响,我们也应该为乌鸦添加间隔帧,来避免设备导致乌鸦的动作、速度不同的问题(视频部分没有控制乌鸦移速,只控制了动画播放,笔者这里也不做控制)。于是,我们在更新的地方,也传入间隔时间。

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


在这里插入图片描述

5.5 计分

准备计分用的变量以及绘制用的函数


// 计分
let score = 0;

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

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


在动画中绘制它们,注意,我们让分数位于最下面,因此要先绘制

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

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

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

在这里插入图片描述

5.6 简单的碰撞检测

接下来我们增加点击事件,来射击乌鸦

getImageData方法帮助我们获取点击区域的颜色

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

在这里插入图片描述

如果出现了如下错误,就发生了跨域问题。(或者画布受污染问题)
在这里插入图片描述
当然出现该问题的原因挺多的。网上大多解释为:

图片资源存储在本地时默认没有域名,而canvas的getImageData方法受同源策略限制,使用getImageData处理本地图片时浏览器会判定为跨域而报错。

处理方式网上有很多,此处我们通过排序让图片不会在第一时间绘制在一起,来解决该问题(大乌鸦先绘制,小乌鸦后绘制)。此时避免了画布受污染问题。

因此,我们通过width排序,即可(见之后的代码)。

如果无可避免的出现了重合绘制。同时我们又不方便用跨域的解决方案,可以参考-解决canvas画布污染的问题


通过上述的方式,我们可以获取点击地方的颜色。 我们在html中,新增一个canvas(碰撞画布) ```html ``` 接下来,我们用类似的方式,构建好这个画布。
const collisionCanvas = document.getElementById('collisionCanvas');
const collisionCtx = collisionCanvas.getContext('2d');

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

我们先看看我们想做什么。

在这个碰撞画布上上色,然后通过检测,我们获取到上层组件的颜色。

通过比对点击点颜色是否是该乌鸦自己的颜色,是的话就说明打中了。

我们先绘制出颜色看一看,碰撞盒效果如下。

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

在这里插入图片描述

然后,我们将碰撞层可见度设为0。就完成了该部分。

#collisionCanvas{
    
    
    opacity: 0;
}

如此,就完成了游戏基础的部分。
在这里插入图片描述

5.7 碰撞特效

用到了前一篇文章使用的代码逻辑。音效也是用的前文的音效下载地址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);
}

在这里插入图片描述

5.8 游戏结束

如果有乌鸦逃出了屏幕,我们设置游戏结束。(当然可以设置乌鸦总数,然后计分模式也可以)

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

//...

在这里插入图片描述

5.9 额外部分——为乌鸦加位移特效

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

在这里插入图片描述

六、敌人品种(参考结构)

视频中使用了extends,当然,无论是从设计模式还是从现代游戏的角度而言,都不推荐用继承,而是用组合去存放数据,用接口去设计实现方法。组合中的方法,可以采用委托等形式让外部调用。

6.1 准备

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

在这里插入图片描述

6.2 加入各种敌人

将对应的素材下载后,添加蠕虫等怪物。

由于笔者控制了整个更新的帧率,所以接下来的一些写法会和视频有所不同。

(视频用了时间补偿的逻辑,通过deltaTime去乘以运动速度,通过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);
})

在这里插入图片描述

附录

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

猜你喜欢

转载自blog.csdn.net/weixin_46949627/article/details/127722915