素材可以去一位大佬放在github的源码中直接下,见附录。
这一个项目让我想到了任天堂的《打野鸭》。
此处我们开始正式制作一个可游玩的游戏。我们要用鼠标去射击乌鸦。
首先做好最基础的准备(记得下载对应的素材)
首页
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 Gametitle>
<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();
我们想要控制乌鸦的刷出频率,减小因为电脑配置不同导致的刷新速度不同。也就是控制游戏的刷新频率(帧数)。
我们首先添加如下变量
// 累计的间隔时间
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);
接下来,回收多余的乌鸦。
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);
我们接下来引入乌鸦的图像。同时为其加上动画。
注意,为了减小设备影响,我们也应该为乌鸦添加间隔帧,来避免设备导致乌鸦的动作、速度不同的问题(视频部分没有控制乌鸦移速,只控制了动画播放,笔者这里也不做控制)。于是,我们在更新的地方,也传入间隔时间。
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);
准备计分用的变量以及绘制用的函数
// 计分
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());
//...
}
接下来我们增加点击事件,来射击乌鸦
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画布污染的问题
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;
}
如此,就完成了游戏基础的部分。
用到了前一篇文章使用的代码逻辑。音效也是用的前文的音效下载地址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);
}
如果有乌鸦逃出了屏幕,我们设置游戏结束。(当然可以设置乌鸦总数,然后计分模式也可以)
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);
}
//...
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,当然,无论是从设计模式还是从现代游戏的角度而言,都不推荐用继承,而是用组合去存放数据,用接口去设计实现方法。组合中的方法,可以采用委托等形式让外部调用。
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>gametitle>
<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);
})
将对应的素材下载后,添加蠕虫等怪物。
由于笔者控制了整个更新的帧率,所以接下来的一些写法会和视频有所不同。
(视频用了时间补偿的逻辑,通过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-视频的素材以及源码