开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
在上一章中,我们成功在canvas
地图上初始化了蛇头,同时也留下了课后作业-如何渲染奖励点。
那么,答案现在揭晓:
渲染奖励点
在index.ts
文件加上这个函数:
//奖励
const drawReward = (world, context, worldWidth, cell_size) => {
const index = world.get_reward_cell()
const row = Math.floor(index / worldWidth)
const col = index - row * worldWidth
context.beginPath()
context.fillStyle = '#FF0000'
context.fillRect(col * cell_size, row * cell_size, cell_size, cell_size)
context.stroke()
}
其实渲染方法和蛇头是一样的,不过参数改为了随机生成的奖励点编号。
这时候,地图初始化的要素已经筹齐了,我们写个公共函数把初始化的元素整合起来:
//游戏地图初始化
const initMap = (wasm, world, context, worldWidth, worldHeight, cell_size) => {
initCanvas(context, worldWidth, worldHeight, cell_size)
drawSnakeCells(wasm, world, context, worldWidth, cell_size)
drawReward(world, context, worldWidth, cell_size)
}
改造一下init
函数:
init().then(wasm => {
....
//清除栅格
context.clearRect(0, 0, canvas.width, canvas.height)
initMap(wasm, world, context, worldWidth, worldHeight, cell_size)
})
然后打包一下js文件(打包方法前面已经提过,不记得的可以回去看第二篇文章)。蛇头和奖励点都渲染出来了。
蛇的运动
蛇的运动,这个就是整个游戏的核心逻辑了。
在编码之前,我们先来分析一下这个蛇的运动:
- 蛇头是火车头,带动整条蛇往前运动。但是运动方向不能与蛇身冲突(即180度原地掉头),也不能接触蛇身或者地图边界
- 目前蛇身结构为一维数组,蛇前进时,后面的蛇身会接替前一格的位置。
- 蛇头运动方向为上下左右,蛇头坐标位置通过可以编号进行计算得到。
梳理清楚思路之后,接下来怎么要编码呢?
这个问题就像如何把大象关进冰箱一样:
- 蛇头运动的坐标计算
- 蛇身交替前行
- 蛇头触碰地图边界或者蛇身,运动停止
那么编码开始,在lib.rs
中新增游戏状态枚举:
#[wasm_bindgen]
#[derive(Debug, Clone, Copy)]
pub enum GameState {
//失败
Failed,
//暂停
Stop,
//运行
Run,
}
在World
结构体中新增更新状态函数(即蛇的运动函数):
#[wasm_bindgen]
impl World {
...
pub fn snake_spawn(&self) -> i32 {
self.snake.body[0].0
}
pub fn update_snake(&mut self, input: Option<Direction>) -> GameState {
//游戏状态
let mut state = GameState::Run;
//蛇头
let snake_head = self.snake_spawn();
//缓存蛇身旧数据
let temp = self.snake.body.clone();
//蛇头运动方向
let direction = match input {
Some(direction) => direction,
None => self.snake.direction,
};
//蛇头坐标
let (row, col) = (
snake_head / self.width,
snake_head - self.width * (snake_head / self.width),
);
let (x, y) = match self.snake.direction {
Direction::Up if direction != Direction::Down => {
self.snake.direction = direction;
if row - 1 < 0 {
state = GameState::Failed;
}
((row - 1), col)
}
Direction::Down if direction != Direction::Up => {
self.snake.direction = direction;
if row + 1 >= self.height {
state = GameState::Failed;
}
((row + 1), col)
}
Direction::Left if direction != Direction::Right => {
self.snake.direction = direction;
if col - 1 < 0 {
state = GameState::Failed;
}
(row, (col - 1))
}
Direction::Right if direction != Direction::Left => {
self.snake.direction = direction;
if col + 1 >= self.width {
state = GameState::Failed;
}
(row, (col + 1))
}
_ => {
state = GameState::Stop;
(row, col)
}
};
if state == GameState::Run {
//蛇头前进
self.snake.body[0].0 = (self.width * x) + y;
//蛇身交替前行
let len = self.snake.body.len();
for i in 1..len {
self.snake.body[i] = temp[i - 1];
}
//蛇头触碰蛇身
if self.snake.body[1..len].contains(&self.snake.body[0]) {
state = GameState::Failed;
}
}
state
}
}
我们一步步来看蛇的运动函数:
- 假设游戏在运行中,获取蛇头编号之后,通过蛇头编号计算出在地图上的坐标
- 根据(可能会输入)运动方向对坐标进行进一步计算,同时判断蛇头超出地图边界的失败情况
- 将最新的坐标转换为编号并赋值回去
- 没有失败则蛇身交替前行,后一个接替前一个的位置
- 判断蛇头是否接触到蛇身
- 最后返回游戏状态
就这样短短几十行代码,浓缩了整个游戏的精华!
动态页面渲染
前面我们已经渲染了静态的初始化地图、蛇头、奖励点,接下来如何让蛇头动起来呢? 同样是如何把大象放进冰箱的问题。。。
- 触发蛇头的运动函数,蛇就会前进
- 需要定时器重复触发,蛇才能不断前进
- 每次前进都要重新渲染蛇和地图
进入index.ts
继续对init
的函数内容进行修改:
import init, { World, Direction, GameState } from 'wasm_snake'
init().then(wasm => {
....
//清除栅格
context.clearRect(0, 0, canvas.width, canvas.height)
let gameRunner: number
let gameCanvas: number
const run = () => {
gameRunner = setTimeout(() => {
context.clearRect(0, 0, canvas.width, canvas.height)
let state = world.update_snake()
if (state === GameState.Run) {
initMap(wasm, world, context, worldWidth, worldHeight, cell_size)
gameCanvas = window.requestAnimationFrame(run)
} else if (state === GameState.Failed) {
clearTimeout(gameRunner)
window.cancelAnimationFrame(gameCanvas)
alert('游戏失败')
}
}, 1000)
initMap(wasm, world, context, worldWidth, worldHeight, cell_size)
game_control(run)
snake_move(world)
})
const game_control = run => {
let controlBtn = <HTMLElement>document.getElementById('game_control')
controlBtn.addEventListener('click', () => {
run()
})
}
这里最主要的就是新增了requestAnimationFrame和cancelAnimationFrame这两个方法,用来处理蛇前进动画的。有兴趣的同学可以跳转链接去MDN查看更详细的描述。
简单讲述一下新增的代码内容,我们把蛇的运动和画面渲染封装在定时器里面,通过requestAnimationFrame
方法来执行游戏动画的更新。游戏失败则通过cancelAnimationFrame
方法取消游戏动画。另外,把开始游戏来用触发蛇头开始运行。
接着再加上运动方向的控制:
//控制方向
const snake_move = world => {
document.addEventListener('keyup', e => {
switch (e.code) {
case 'ArrowDown':
world.update_snake(Direction.Down)
break
case 'ArrowUp':
world.update_snake(Direction.Up)
break
case 'ArrowLeft':
world.update_snake(Direction.Left)
break
case 'ArrowRight':
world.update_snake(Direction.Right)
break
}
})
}
重新打包一下项目,打开index.html
文件。点击开始游戏,蛇头就会往下走,可以用键盘的方向键控制运动方向。当触碰到地图边界,则弹窗提示游戏失败!
好了,我们的贪吃蛇终于可以动起来了!但是,吃奖励的功能还没实现!如何让蛇在运动的时候吃上奖励并且身体变长呢?我们下期继续探究。