其他
编程实践|如何用Moonbit开发生命游戏?
距离我们在海外正式推出公测已有两周,在这两周中,我们看到不少的用户开始跃跃欲试,用Moonbit做各种项目的尝试。其中就有这样一位用户,他是来自知名公司GreenLabs的工程师Woonki,在Twitter上分享了自己最新的尝试:使用Moonbit重新构建经典的生命游戏,同时在构建生命游戏中,Moonbit卓越的编译速度也让他大开眼界。
什么是生命游戏
游戏规则
如果一个细胞周围只有1个或0个其他细胞,它将会孤独而死
如果一个细胞要想存活下来或繁衍,周围细胞的数量必须保持在2到3个之间
如果细胞周围有4个或更多细胞相邻,它将因过度拥挤而死
如果细胞周围有3个邻居,它将在空白处产生一个新细胞,游戏将继续繁衍
这些简单的规则创造出一个充满生命和变化的虚拟世界,令人着迷。正是这种复杂性吸引了众多数学、物理和计算机科学的学生和爱好者,他们将其视为一种宝贵的娱乐方式。
如何用 Moonbit + JS 编写生命游戏
Moonbit 编写生命游戏的逻辑
enum Cell {
Dead
Alive
}
使用一个结构体 Universe 来管理细胞的状态,
width
表示宽度,height表示高度,cells
存储了细胞状态的数组
struct Universe {
width : Int
height : Int
mut cells : Array[Cell]
}
live_neighbor_count
用于计算指定位置细胞周围存活细胞的数量,计算规则根据康威游戏的规则来func live_neighbor_count(self : Universe, row : Int, column : Int) -> Int {
var count = 0
let delta_rows = [self.height - 1, 0, 1]
let delta_cols = [self.width - 1, 0, 1]
var r = 0
while r < 3 {
var c = 0
while c < 3 {
if delta_rows[r] == 0 && delta_cols[c] == 0 {
c = c + 1
continue
}
let neighbor_row = (row + delta_rows[r]) % self.height
let neighbor_col = (column + delta_cols[c]) % self.width
let idx = self.get_index(neighbor_row, neighbor_col)
count = count + self.get_cell(idx)
c = c + 1
}
r = r + 1
}
count
}
tick
用于进行一次迭代,根据生命游戏规则更新细胞状态。pub func tick(self : Universe) {
let next : Array[Cell] = array_make(self.width * self.height, Dead)
var r = 0
while r < self.height {
var c = 0
while c < self.width {
let idx = self.get_index(r, c)
let cell = self.cells[idx]
let live_neighbor = self.live_neighbor_count(r, c)
let next_cell : Cell = match (cell, live_neighbor) {
(Alive, c) =>
if c < 2 {
Dead
} else if c == 2 || c == 3 {
Alive
} else {
Dead
}
(Dead, 3) => Alive
_ => cell
}
next[idx] = next_cell
c = c + 1
}
r = r + 1
}
self.cells = next
}
编写完成后执行
moon build
JS 加载代码和渲染
wat2wasm
来将 wat 转为 wasmwat2wasm target/build/main/main.wat --output=www/src/game_of_life.wasm
我们通过下面的代码从 wasm 导入相应的函数
WebAssembly.instantiateStreaming(fetch("src/game_of_life.wasm"), importObject)
.then((obj) => {
const universe_new = obj.instance.exports["moonbit_game_of_life/lib::new"];
const universe_tick =
obj.instance.exports[
"moonbit_game_of_life/lib::@moonbit_game_of_life/lib.Universe::tick"
];
const universe_cells =
obj.instance.exports[
"moonbit_game_of_life/lib::@moonbit_game_of_life/lib.Universe::get_cells"
];
const universe_height =
obj.instance.exports[
"moonbit_game_of_life/lib::@moonbit_game_of_life/lib.Universe::get_height"
];
const universe_width =
obj.instance.exports[
"moonbit_game_of_life/lib::@moonbit_game_of_life/lib.Universe::get_width"
];
const universe_get_cell =
obj.instance.exports[
"moonbit_game_of_life/lib::@moonbit_game_of_life/lib.Universe::get_cell"
];
初始化 canvas:
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;
我们需要不停的渲染画面,需要先调用使用 Moonbit 编写好的 tick 来更新细胞状态,然后调用 drawGrid
和 drawCell
来画图:
const renderLoop = () => {
universe.tick();
drawGrid();
drawCells();
requestAnimationFrame(renderLoop);
};
drawGrid
就是用于画一个个格子const drawGrid = () => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
for (let i = 0; i <= width; i++) {
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
}
for (let j = 0; j <= height; j++) {
ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
}
ctx.stroke();
};
drawCells
使用双重循环来遍历表格,调用 get_cell
来获取细胞状态,根据细胞状态来填上不同的颜色const drawCells = () => {
ctx.beginPath();
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const idx = getIndex(row, col);
ctx.fillStyle =
universe.get_cell(idx) === 0 ? DEAD_COLOR : ALIVE_COLOR;
ctx.fillRect(
col * (CELL_SIZE + 1) + 1,
row * (CELL_SIZE + 1) + 1,
CELL_SIZE,
CELL_SIZE
);
}
}
ctx.stroke();
};
完整代码:
https://github.com/mununki/moonbit-wasm-game-of-life
官方平台账号,欢迎扫码关注
Moonbit
知乎|@张宏波 / @Moonbit
Twitter丨@Moonbitlang
Bilibili丨张宏波的基础软件课程