徜徉在知识海洋的一群鲸鱼
canvas开放世界探索小游戏!
canvas开放世界探索小游戏!

canvas开放世界探索小游戏!

开发步骤:

搭建项目–创建玩家–添加重力–玩家动作设计–设计sprite图片–碰撞块–碰撞检验–sprite动画–HitBox实现(键盘和摇杆)–sprite过度–进入大门的动作逻辑–改变关卡–下一步

我打算用kingland作为整个项目的名字,因为整个游戏是关于一个小人在房间里探索,那他就是这个游戏里的国王

然后我们要新建index文件,加上canvas标签,接下来要考虑怎么实现人物动画了,

我们在script里面进行一些测试

首先要获取到canvas对象,我选择用document.getElmentById来获取,给他命名为canvas

接下来我们要开始画画了,那么我们需要用到canvas.getContext(‘2d’)来表示我们要开始在2维上面进行画画,我们把这个赋值给ctx,

然后我们要考虑我们的游戏背景是多大,大部分的显示都是按照16:9的比例来设置宽高的,所以我们的背景也这么大吧,然后这个游戏是一个像素小游戏,而像素小游戏,基本上都是64*64,所以,我们的宽高就可以这样来表示,64*16,64*9。

然后我们要在canvas画一个大的长方形来作为我们的背景,这里我们要用到,fillRect和fillStyle,第一个是来画一个长方形的方法,他需要我们指定这个长方形从哪里开始画,以及大小,第二个是填充颜色,接收颜色参数,那么背景我们就从原点开始,大小就是我们的canvas.width和height了,颜色,我们先用白色代替。

接下来就是创建玩家了

创建之前,我们先想一想,玩家会做什么,以及需要用什么方法来实现,首先玩家,我们可以用一个矩形代替,然后玩家会移动,移动到边缘也会碰壁,玩家会跳跃,那么我们可以像上面实现背景那样,实现一个小矩形代替玩家,然后还需要调用widow的requestAnimationFrame方法来实现动画效果,我们可以先实现一个小测试

基于上面的想法,我们可以创建一个Player类,里面要包含玩家的位置和大小,然后我们还要实现一个动画方法,这个方法用于实现玩家的简单移动,这里需要用到清除画布的方法,因为canvas的绘画,如果你不清除,他会一直存在,所以我们要清除,然后还需要加上之前已经画的背景,这个确实很麻烦,但是我们先这样实现着,我的做法是,让玩家一直向下,直到碰到地板停下来,那么动画停止的条件就是玩家的位置,那么我们要实现这个效果,需要在我们的玩家类里面加上一个draw方法,用来在画布上画我们的玩家,然后我们要实现物理碰撞,那就加一个update方法,实现玩家碰壁就停止,但是如何知道玩家会碰壁呢,我们现在只有玩家位置和大小,那么我们就利用玩家的位置和大小来实现玩家碰壁的检测,玩家的底部位置可以表示为玩家的y坐标+玩家高度,

代码看起来是这样的

那么接下来,我们开始处理重力

涉及到物理知识了,有点小生疏,大家都知道重力的原因,我们在下降的时候会有重力加速度,

x = v平均t,v1=v0+at

那么我们可以模拟一下这个过程,那么我们需要给我们的玩家定义一个速度,这个速度分为x方向的和y方向的,velocity,然后我们给我们的y加上速度,再进行判断,如果到地面了,速度要变成0,同时我们判断地面的条件也需要加上我们的速度来判断,这个条件就变成了,bottom+yelocity小于canvas的height,这里我不知道为什么

接下来需要完善玩家的移动,也就是按键操作玩家运动

接下来,我们要在js里面添加EventListener,w作为跳跃键,要实现跳跃,很简单,给y的速度设置为-10,然后我们要保证一次只能限制一次跳跃,就需要加上判断,只有在velocity.y为0才能执行跳跃,然后是左右移动,如果和跳跃一样实现,会出现左右移动不够丝滑,我们新建keys变量,存储左右键,有一个pressed属性,默认为false,然后给animate中间加判断,如果d是true设置velocity.x为正,否则a为true,velocity.x为负,最后为了实现玩家x移动,要给玩家设置一下x位置为初始x加velocity.x

代码是这样的

接下来是要设置游戏贴图,雪碧图

新建一个Spritejs文件,新建类,构造函数,我们需要图片资源,

class Sprite {
    constructor({position, imageSrc, frameRate = 1,
        animations, frameBuffer = 2, loop = true,
        autoplay = true
    }) {

        this.position = position
        this.image = new Image()
        this.image.onload = () => {
            this.loaded = true
            this.width = this.image.width / this.frameRate
            this.height = this.image.height
        }
        this.image.src = imageSrc
        this.loaded = false
        this.frameRate = frameRate
        this.currentFrame = 0
        this.elapsedFrames = 0
        this.frameBuffer = frameBuffer
        this.animations = animations
        this.loop = loop
        this.autoplay = autoplay
        this.currentAnimation

        if(this.animations) {
            for(let key in this.animations) {
                const image = new Image()
                image.src = this.animations[key].imageSrc
                this.animations[key].image = image
            }
        }
    }
    draw() {
        if(!this.loaded) return

        const cropbox = {
            position: {
                x: this.width * this.currentFrame,
                y: 0
            },
            width: this.width,
            height: this.height
        }

        c.drawImage(
            this.image, cropbox.position.x, cropbox.position.y,
            cropbox.width, cropbox.height, this.position.x,
            this.position.y, this.width, this.height
        )

        this.updateFrames()
    }

    play() {
        this.autoplay = true
    }

    updateFrames() {
        if(this.autoplay === false) return

        this.elapsedFrames += 1

        if(this.elapsedFrames % this.frameBuffer === 0) {            
            if(this.currentFrame < this.frameRate -1) {
                this.currentFrame += 1
            }
            else if(this.loop) {
                this.currentFrame = 0
            }
        }
        if(this.currentAnimation?.onComplete) {
            if(this.currentFrame === this.frameRate - 1 && !this.currentAnimation.isActive) {
                this.currentAnimation.onComplete()
                this.currentAnimation.isActive = true
            }
        }
    }
}

然后我们需要处理碰撞物体,如果玩家碰到了墙壁,那么他就被堵住了,所以,我们要有一个地图,然后需要设置碰撞,tiled这个能帮助我们自动生成地图上的碰撞代码,也就是把地图转化成代码

我们把生成的地图数据,放到data下面的关卡文件里面,collisionsjs,

const collisionsLevel1 = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0,
    0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
    0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
    0, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
    0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

const collisionsLevel2 = [
    292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    292, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    292, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    292, 292, 292, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 292, 0, 0, 292, 0, 0, 292, 292, 292, 292, 292, 292, 0,
    0, 292, 292, 292, 0, 0, 292, 292, 292, 292, 0, 0, 0, 0, 292, 0,
    0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
    0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 292, 292, 292, 292, 292, 0,
    0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0]


const collisionsLevel3 = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 0,
    0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0,
    0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0,
    0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 250, 250, 250, 0,
    0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 0,
    0, 250, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 0, 0,
    0, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

我们拿到这些地图数据,然后就需要处理,按照每行16列来处理数据,我们定义一个rows存储行,

Array.prototype.parse2D = function() {
    const rows = []
    for (let i = 0; i < this.length; i += 16) {
        rows.push(this.slice(i, i + 16))
    }
    return rows
}

然后我们能看到数据中哪些250,这些就代表墙壁,我们需要处理一下他们

class CollisionBlock {
    constructor({position}) {
        this.position = position
        this.width = 64
        this.height = 64
    }
    draw() {
        c.fillStyle = 'rgba(255, 0, 0, 0.3)'
        c.fillRect(this.position.x, this.position.y, this.width, this.height)
    }
}

新建墙壁类,接收位置,设置大小和颜色,然后利用canvas填充进去,

然后我们要转化墙壁

Array.prototype.createObjectsFrom2D = function () {
    const objects = []
    this.forEach((row, y) => {
        row.forEach((symbol, x) => {
            if (symbol === 292 || symbol === 250) {
                // push a new collision into collisionblocks array
                objects.push(
                    new CollisionBlock({
                    position: {
                        x: x * 64,
                        y: y * 64,
                    },
                }))
            }
        })
    })
    return objects
}

遍历行对象,获取x和y的值,然后新建墙体对象