【Canvas】童年玩过的雷霆战机你还记得吗?

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

按键机的年代,想必大家都玩过类似雷电,雷霆战机之类的飞行射击类游戏吧,今天我就试着用canvas来还原一下游戏场景。

素材准备

还原场景的第一步是准备素材,首先我们需要一架雷霆战机,经过九牛二虎之力,我在网上找到了心仪的战机,就是下面这架:

plane.png

战机的子弹也要准备一下,我给我的战机准备了黄色和蓝色两种子弹,如下:

Snipaste_2022-11-22_21-34-31.png

HTML部分

素材准备完成,我们就开始把它们渲染到页面上吧,首先先用3个img标签引入3个素材,并且设置img的display属性为none,使的它们不显示在页面上。同时,我们定义一个canvas元素,用来把我们的飞机渲染在上面。

<img src="../imgs/plane.png" alt="飞机" style="display: none" id="plane">
<img src="../imgs/blue-bullet.png" alt="蓝子弹" style="display: none" id="blue-bullet">
<img src="../imgs/yellow-bullet.png" alt="黄子弹" style="display: none" id="yellow-bullet">
<canvas id="space"></canvas>

4个元素我都设置了id属性,这是为了一会更方便的获取DOM元素。

CSS部分

样式部分,我们只需要使用通配符选择器,将页面自带的margin和padding设为0,防止页面有白边,影响体验感。

* {
    margin: 0;
    padding: 0;
}

JS部分

基础准备

首先,我们要先获取到canvas元素,然后设置它的宽高为浏览器页面的宽高,然后我们还要获取到3个img元素,因此img加载图片是异步的,所以我们需要通过轮询3个img元素的complete属性是否都为true来判断3张图片是否都加载好了,只有图片都加载好了,才可以执行我们的动画效果,进行战机的渲染。

// 获取canvas - 设置宽高
const space = document.getElementById('space') // 太空
space.width = window.innerWidth
space.height = window.innerHeight
// 获取上下文
const ctx = space.getContext('2d')
// 获取两个img元素
const plane = document.getElementById('plane') // 飞机
const blueBullet = document.getElementById('blue-bullet') // 蓝子弹
const yellowBullet = document.getElementById('yellow-bullet') // 黄子弹
// 判断三张图片是否都已加载完成
const timer = setInterval(() => {
    if (plane.complete && blueBullet.complete && yellowBullet.complete) {
        animate() // 执行动画
        clearInterval(timer)
    }
}, 50)

因为是在浏览器上,我们是通过移动鼠标来操作我们的战机,所以我们还需要对鼠标的位置进行监控,可以通过监听 mousemove 事件来获取到鼠标的位置,并且定义一个全局对象 mouse,用其中的xy属性来存放当前鼠标的位置,代码如下:

const mouse = {x: 0, y: 0}// 鼠标位置参数
// 监控鼠标位置改变
window.addEventListener('mousemove', (e) => {
    mouse.x = e.clientX
    mouse.y = e.clientY
})

构造函数

这一次我们需要两个构造函数,一个是战机子弹的构造函数 Bullet 另一个则是战机的构造函数 Plane,两个构造函数的结构大体相同,都会拥有xy属性,用于表示自身当前所在位置,有draw方法,基于自身的位置进行绘制,有update方法,对自身的位置进行更新并重绘。

不一样的是,子弹构造函数还拥有一个dy属性,表示子弹在y轴的速度,因为子弹是沿着一个方向运动的。

战机构造函数 Plane

function Plane(x, y) {
    this.x = x
    this.y = y
    // 绘制飞机
    this.draw = () => {
       
    }
    // 更新飞机位置
    this.update = () => {
        
    }
}

在我们使用 drawImage 进行战机绘制的时候,是以图像的左上角为起点的,也就是鼠标的光标会在战机的左上角,那感觉特别违和,如下图:

image.png

战机的素材大小为 140 * 96,我们想要让鼠标在战机中心的话,就要把鼠标的x减去战机宽度的一半,鼠标的y减去战机高度的一半。这一步我们直接放在构造函数的update方法中进行即可。

this.update = () => {
   // 飞机图像宽高 140 * 96 减去一半 鼠标正好在飞机中间
   this.x = mouse.x - 70
   this.y = mouse.y - 48
   this.draw()
}
// 绘制飞机
this.draw = () => {
   ctx.drawImage(plane, this.x , this.y)
}

image.png

这下舒服多了

战机子弹构造函数 Bullet

function Bullet(x, y) {
    this.x = x
    this.y = y
    this.dy = 120 // 子弹速度写死
    // 绘制子弹
    this.draw = () => {
        
    }
    // 更新子弹位置
    this.update = () => {
       
    }
}

子弹也有和战机一样的问题但又不尽相同,已知子弹素材的宽高为 38 * 90,因为子弹是要飞出去的,所以我们不必理会y,只需对子弹的x坐标进行计算,让子弹在绘制的时候,x坐标减去19,使的子弹看起来刚好是从机头那里发射出去的。

然后因为我希望我的战机可以发射两种颜色的子弹,间隔发射,所以我定义了一个子弹类型变量 bulletType ,每创建一颗子弹就会给它的值加1,如果是奇数就蓝色子弹,如果是偶数就黄色子弹。

有子弹,自然也要又弹夹,因此我还定义了一个全局弹夹 BULLET_POOL,每个创建的子弹都会push进去这里,然后通过遍历这个数组,调用里面每个子弹的update方法进行子弹位置的更新和绘制。

最后要考虑的就是,因为每颗子弹发射的频率相同,间隔也相等,就会导致这些子弹彷佛是静止的,就像下面这样:

image.png

所以我们在绘制的时候,要考虑加点随机因素进去,打破它的这种均衡,弹道的效果显得更加真实。

通过上面的分析,接下来将子弹的构造函数补充完整。

// 更新子弹位置
this.update = () => {
    // 随机发射
    this.y -= this.dy
    if (Math.random() > 0.5) {
        this.draw()
    }
}
// 绘制子弹
this.draw = () => {
    // 子弹图像的大小是38  * 90
    if (bulletType % 2 === 0) {
        ctx.drawImage(blueBullet, this.x - 19, this.y - 80)
    } else {
        ctx.drawImage(yellowBullet, this.x - 19, this.y - 80)
    }
    bulletType++ // 更新子弹颜色
}

一开始我曾说过不需要理会子弹的y坐标,其实不然,之所以减80,是因为我们需要让子弹看起来是从飞机头发射出去的,而不是从鼠标(飞机中心)发射出去。

场景还原

素材准备好了,构造函数也准备好了,接下来开始进行场景的还原。

第一步当然是先创建一个雷霆战机啦

// 创建一个雷霆战机
const fighter = new Plane(0, 0) // 飞机起始位置在左上角

第二步就是执行 requestAnimationFrame 来绘制飞机和子弹啦

function animate() {
    requestAnimationFrame(animate)
    // 清空画布
    ctx.clearRect(0, 0, space.width, space.height)
    // 子弹发射间隔
    if (interval % 1000 === 0) {
        const newBullet = new Bullet(mouse.x, mouse.y)  // 创建一个新的子弹
        BULLET_POOL.push(newBullet) // 加入子弹池
    } else {
        interval++
    }
    // 绘制子弹
    for (let bullet of BULLET_POOL) {
        bullet.update()
    }
    // 绘制飞机
    fighter.update()
}

这里要注意的是,要先绘制子弹,然后再绘制战机,因此有层级的存在,这个顺序搞乱了,会导致子弹挡住战机。然后子弹发射间隔那里,本来是想减缓子弹发射的速度的,好像没啥子用,我设置到了10w都没见子弹变慢,但是懒得删掉了。

最终效果

下面就是最后实现的效果啦

20221122_223557.gif

什么,你说雷霆战机的背景是外太空黑色的?我也想呀,但是我抠不出来图,只能用白色糊弄过去啦。

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容