我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!”
写在前面
最近学习canvas
写了不少小demo,一直手痒想整个大活儿,正好趁这个挑战赛的机会,实现一款之前玩过的经典游戏《跳上一百层》
,游戏规则也很简单,控制角色一直往上跳,没有站在踏板上就会往下掉,然后游戏结束。
游戏规则
- 左右方向键移动
- 空格键跳跃,跳跃的同时可以左右移动
- 踏板会一直左右移动
- 游戏角色在踏板上也要左右移动保持不掉落
- 跳上第一块踏板开始判定游戏是否结束
先检讨
- 踏板的分布简单设置了一下,不是很合理
- 角色的动作没有加入重力,不是很平滑
- 游戏素材很粗糙,凑合着看吧,明白那个意思就行,主要是展示每个部分的机制
码上掘金
实现游戏角色
前面我们实现了canvas元素的帧动画,这里我们简单调整一下设计一个30x30
的不断走动的小人作为游戏角色。
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)
的结果恰好像一次跳跃的结果,加上振幅的设置,就能得到一个简单的跳跃了,然后逐渐把角色的高度更新就可以了
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
的时候就能实现一次跳跃了
实现踏板
踏板的逻辑很简单,就是一个长条条,默默在左右移动
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)
}
}
跳上踏板
重头戏来了,角色怎么站到踏板上呢?
我们在角色移动的过程中,与踏板进行碰撞检测,这里用分离轴检测算法,有兴趣的可以去了解一下,这里就不多介绍了。
我们的角色从下方碰到踏板的时候,把角色的y
设为踏板的y
,视为站上了,从上方碰到踏板的时候,停止更新角色的y
(角色一直会有一个下落的逻辑,y
不断的减少),那么角色和踏板就能保持在同一高度了,看上去就上是站在了踏板上,而一旦踏板移开,碰撞检测失效,角色本身的下落逻辑继续生效,角色就会继续往下掉落。
这样我们游戏的基本逻辑就完成了,游戏可以左右移动和跳跃,也能站上踏板和掉落。
游戏进度
随着游戏的进行,我们的游戏背景也会一直往上走,踏板的位置和游戏角色的位置也会更新,游戏背景参考我这篇无限循环的游戏背景,结合踏板的逻辑是这样的:
- 我们有
N
个踏板,每个踏板都有自己的xy
值 - 游戏窗口是
300x500
,随着游戏进行,y
值不断增加 - 我们始终显示
y
值在300 + y
到500 + y
的踏板 - 游戏角色往上跳,不断增加
y
值即可 - 游戏背景,角色,踏板都会跟着
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
}
})