用canvas实现经典游戏《跳上一百层》

1,490 阅读2分钟

1662542543(1).jpg

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

写在前面

最近学习canvas写了不少小demo,一直手痒想整个大活儿,正好趁这个挑战赛的机会,实现一款之前玩过的经典游戏《跳上一百层》,游戏规则也很简单,控制角色一直往上跳,没有站在踏板上就会往下掉,然后游戏结束。

游戏规则

  • 左右方向键移动
  • 空格键跳跃,跳跃的同时可以左右移动
  • 踏板会一直左右移动
  • 游戏角色在踏板上也要左右移动保持不掉落
  • 跳上第一块踏板开始判定游戏是否结束

先检讨

  • 踏板的分布简单设置了一下,不是很合理
  • 角色的动作没有加入重力,不是很平滑
  • 游戏素材很粗糙,凑合着看吧,明白那个意思就行,主要是展示每个部分的机制

码上掘金

在线体验传送门

实现游戏角色

前面我们实现了canvas元素的帧动画,这里我们简单调整一下设计一个30x30的不断走动的小人作为游戏角色。

run5.gif

class Person {
    constructor(option) {
        this.offsetX = option.offsetX
        this.offsetY = option.offsetY
        this.w = option.w
        this.h = option.h
        this.image = new Image()
        this.image.src = "./player.png"
        
        this.run = false
        this.runIndex = 0
        this.lastTime = 0
        this.timer = null
        this.fps = 100
    }
    draw(ctx) {
        ctx.drawImage(this.image, 0 + this.runIndex % 8 * this.w, 0, this.w, this.h, this.x + this.offsetX, this.y + this.offsetY, this.w, this.h);
    }
    animate() {
        this.timer = new Timer(() => {
            this.runIndex++
        }, this.fps)
    }
    play() {
        this.run = true
        this.animate()
    }
    stop() {
        this.run = false
        this.timer.clear()
    }
}

给角色加上左右移动,每次move更新x的位置即可,加上一个简单的边界检测

class Person {
    move() {
        switch (this.direction) {
            case "right": 
                if(this.container.x < 300 - this.w - 3) {
                    this.x += 3
                }
                break
            case "left":
                if(this.x > 3) {
                    this.x -= 3
                }
                break
            default: 
                break
        }
    }
}

然后是跳跃动作,这里偷懒一下,采用sin算法,简单来说,就是sin(0)sin(180)的结果恰好像一次跳跃的结果,加上振幅的设置,就能得到一个简单的跳跃了,然后逐渐把角色的高度更新就可以了

image.png

class Person {
    jump() {
        // 
        let dx = 0, dy = 0
        let y0 = this.y // 记录初始的y值
        this.jumpTimer = new Timer(() => {
            if(dx < 180 && dx > -180) {
                dx += 5
                // 70为振幅,我们的角色高位30,这里跳70效果比较好
                dy = Math.floor(Math.sin(Math.abs(dx) % 180 * Math.PI / 180) * 70)
            } else {
                this.jumpTimer.clear()
                this.jumpTimer = null
            }
            this.y = y0 + dy
        }, this.fps)
    }
}

这样每次调用jump的时候就能实现一次跳跃了

run5.gif

实现踏板

踏板的逻辑很简单,就是一个长条条,默默在左右移动

class Brick {
    constructor(option) {
        this.x = option.x
        this.y = option.y
        this.w = option.w
        this.h = option.h
        this.type = "Brick"
        this.image = new Image()
        this.image.src = "./brick.png"
        this.fps = 16
        this.timer = null
        this.animate()
    }

    draw(ctx: CanvasRenderingContext2D) {
        ctx.drawImage(this.image, this.x, this.y, this.w, this.h);
    }
    // 默默的左右移动,方向一开始随机
    animate() {
        let x = this.x - this.w
        let x2 = this.x + this.w
        let f = Math.random() < 0.5 ? -1 : 1
        this.timer = new Timer(() => {
            this.x += 1 * f
            if(this.x > x2) {
                f = -1
            }
            if(this.x < x) {
                f = 1
            }
        }, this.fps)
    }
}

run5.gif

跳上踏板

重头戏来了,角色怎么站到踏板上呢?

我们在角色移动的过程中,与踏板进行碰撞检测,这里用分离轴检测算法,有兴趣的可以去了解一下,这里就不多介绍了。

我们的角色从下方碰到踏板的时候,把角色的y设为踏板的y,视为站上了,从上方碰到踏板的时候,停止更新角色的y(角色一直会有一个下落的逻辑,y不断的减少),那么角色和踏板就能保持在同一高度了,看上去就上是站在了踏板上,而一旦踏板移开,碰撞检测失效,角色本身的下落逻辑继续生效,角色就会继续往下掉落。

run5.gif

这样我们游戏的基本逻辑就完成了,游戏可以左右移动和跳跃,也能站上踏板和掉落。

游戏进度

随着游戏的进行,我们的游戏背景也会一直往上走,踏板的位置和游戏角色的位置也会更新,游戏背景参考我这篇无限循环的游戏背景,结合踏板的逻辑是这样的:

  1. 我们有N个踏板,每个踏板都有自己的xy
  2. 游戏窗口是300x500,随着游戏进行,y值不断增加
  3. 我们始终显示y值在300 + y500 + y的踏板
  4. 游戏角色往上跳,不断增加y值即可
  5. 游戏背景,角色,踏板都会跟着y值更新,y就相当于我们的游戏进度了

就这样一个完整的游戏就完成了。

// 初始化场景
let s2 = new Stage(document.getElementById("stage"))

// 初始化背景
let bg = new Background()
s2.add(bg)

// 初始化砖块
let shut = [[180, 430], [100, 320], [50, 220], [170, 120],[80, 20], [100, -120], 
            [40, -220], [200, -320],[180, -430], [100, -500], [40, -620], 
            [200, -720],[180, -800], [100, -920], [40, -1020], [200, -1120]]
let bs = []
shut.forEach(item => {
    let b = new Brick({
        x: item[0],
        y: item[1],
        w: 50,
        h: 10
    })
    bs.push(b)
    s2.add(b)
})

// 初始化角色
let p = new Player({
    x: 32,
    y: 470,
    w: 30,
    h: 30,
})
s2.add(p)

// 给角色加上交互即可
document.addEventListener("keyup", (e) => {
    switch(e.code) {
        case "ArrowRight": 
            p.setDirection("right")
            p.move()
            break
        case "ArrowLeft":  
            p.setDirection("left")
            p.move()
            break
        case "Space":  
            p.jump(1)
            // 跳了之后更新进度,更新背景,踏板,角色
            let delta = 5
            let riseTimer= new Timer(() => {
                y += delta
                bg.rise(delta)
                bs.forEach(item => {
                    item.y += delta
                })
            }, 16
            break
        default: 
            break
    }
})