使用Rust制作康威生命游戲的實現(xiàn)代碼
前言
之前學(xué)了幾遍,后來忘記了,通過制作該游戲再復(fù)習(xí)復(fù)習(xí)。
安裝準(zhǔn)備
- wasm-pack : https://rustwasm.github.io/wasm-pack/installer/
- cargo-generate:
cargo install cargo-generate
初始項目
初始rust項目
使用wasm的項目模板:
cargo generate --git https://github.com/rustwasm/wasm-pack-template
- 提示輸入project名wasm-game-of-life
- 在lib.rs中可以看見如下內(nèi)容:
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm-game-of-life!");
}- 它導(dǎo)入 window.alertJavaScript 函數(shù),并導(dǎo)出greet的Rust 函數(shù)。
Cargo.toml
- Cargo.toml預(yù)置了[lib]和[dependencies]。解釋一下crate-type中f=“https://users.rust-lang.org/t/what-is-the-difference-between-dylib-and-cdylib/28847”>cdylib和rlib的作用:
- cdylib:顧名思義,是C的動態(tài)鏈接庫的意思,可以被C和C++程序鏈接使用
- rlib:Rust靜態(tài)鏈接庫,用于靜態(tài)連接其他crates
- 依賴中使用的:
- wasm-bindgen可以將Rust編寫的函數(shù)和結(jié)構(gòu)體暴露到JS中或者把JS的方法引入到Rust中使用
- console_error_panic_hook提供了Wasm輸出Rust Panic的能力
- wee_alloc是一個輕量的Wasm內(nèi)存分配器,但是會比默認(rèn)分配器慢一些。
初始web項目
npm init wasm-app www
- 看到生成的pkg.json:
{
"name": "create-wasm-app",
"version": "0.1.0",
"description": "create an app to consume rust-generated wasm packages",
"main": "index.js",
"bin": {
"create-wasm-app": ".bin/create-wasm-app.js"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
},- html里導(dǎo)入boostrap.js,boostrap.js里導(dǎo)入index.js。 index.js里面導(dǎo)入了其已經(jīng)制作好的一個包:
import * as wasm from "hello-wasm-pack"; wasm.greet();
- 我們修改pkg.json,導(dǎo)入自己的包(該包需要使用
wasm-pack build生成)
"wasm-game-of-life": "file:../pkg"
- 將index.js更換下:
import * as wasm from "wasm-game-of-life"; wasm.greet();
- 使用npm i 安裝依賴。
- 使用npm run start 啟動頁面,打開http://localhost:8080/即可看見alert。
游戲規(guī)則
- Conway’s Game of Life是英國數(shù)學(xué)家約翰·何頓·康威在1970年發(fā)明的放置類無玩家參與的游戲
- 百度百科
- https://baike.baidu.com/item/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F/22668799?fr=aladdin主要規(guī)則如下:
- 1、任何少于兩個活鄰居的活細(xì)胞都會死亡,就像是由于人口不足造成的。
- 2、任何有兩三個活鄰居的活細(xì)胞都可以活到下一代。
- 3、任何有超過三個活鄰居的活細(xì)胞都會死亡,就像人口過剩一樣。
- 4、任何只有三個活鄰居的死細(xì)胞都會變成活細(xì)胞,就像通過繁殖一樣。
游戲設(shè)計
- 為啥說這個呢,因為2種語言去做這個東西會考慮哪個東西在哪個里面去實現(xiàn)。
- rust推薦大型、長壽命的數(shù)據(jù)結(jié)構(gòu)被實現(xiàn)為 Rust 類型,這些類型存在于 WebAssembly 線性內(nèi)存中,并作為不透明的句柄暴露給 JavaScript。JavaScript 調(diào)用導(dǎo)出的 WebAssembly 函數(shù),這些函數(shù)采用這些不透明的句柄、轉(zhuǎn)換它們的數(shù)據(jù)、執(zhí)行繁重的計算、查詢數(shù)據(jù)并最終返回一個可復(fù)制的結(jié)果。通過只返回計算結(jié)果,我們避免了在 JavaScript 垃圾收集堆和 WebAssembly 線性內(nèi)存之間來回復(fù)制和/或序列化所有內(nèi)容。
- 這個游戲中,會將universe的顯示效果暴露給js渲染,其余計算在rust去實現(xiàn)。
- 由于宇宙是n*n的,所以我們可以用一維數(shù)組去表示它,比如4x4的宇宙就是這樣:

- 將數(shù)組每個row換下來就是需要的4x4的顯示了。因為這種表現(xiàn)形式,所以我們需要對數(shù)組索引和行列進(jìn)行轉(zhuǎn)換,公式為:
index(row, column, universe) = row * width(universe) + column
- 就比如我要知道4行4列是索引幾,根據(jù)公式就是3*4 + 3。
- 每個單元格有一個字節(jié),其中0表示死亡,1表示存活。
Rust實現(xiàn)
首先我們需要定義每個單元格:
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}枚舉類型,0是死亡,1是存活,#[repr(u8)]表示一個單元格1字節(jié)。復(fù)習(xí)下:
| 長度 | 有符號 | 無符號 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
接下來定義宇宙:
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
- 宇宙是長寬和一個動態(tài)數(shù)組。
- 我們對universe實現(xiàn)一些方法便于操作:
#[wasm_bindgen]
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
}- get_index就是上面公式做索引。
- 從前面游戲規(guī)則上可知,我們需要對每個單元格求出周圍格子的存活數(shù)量,于是加上這個函數(shù):
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
println!("{},{}-s-", neighbor_row, neighbor_col);
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
- 解釋下這個函數(shù),其中迭代height-1 , 0 , 1 以及 width-1,0,1就是求傳入row與col的周圍的格子里存活數(shù)量。當(dāng)?shù)?,0時,這個格子代表其自身,所以直接忽略。
- 比如64x64的宇宙,查詢2,2周圍的格子就是:
1,1
1,2
1,3
2,1
2,3
3,1
3,2
3,3
- 邊界處理靠取余,這樣也能避免無符號向下溢出,所以0,0的周圍格子就是:
63,63
63,0-
63,1
0,63
0,1
1,63
1,0
1,1
- 再從當(dāng)前宇宙中獲取格子的狀態(tài),如果是0,那么加上也不會增加,這樣最終返回的就是周圍格子的存活數(shù)量了。
- 下面根據(jù)規(guī)則迭代每個細(xì)胞狀態(tài),暴露出來:
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
let cell = self.cells[idx];
let live_neighbors = self.live_neighbor_count(row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
- 最后需要對universe實現(xiàn)輸出功能,先將其輸出成文本,實現(xiàn)display方法:
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for line in self.cells.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '?' } else { '?' };
write!(f, "{}", symbol)?;
}
write!(f, "\n")?;
}
Ok(())
}
}最后進(jìn)行暴露初始化和渲染方法:
pub fn new() -> Universe {
let width = 64;
let height = 64;
let cells = (0..width * height)
.map(|i| {
if i % 2 == 0 || i % 7 == 0 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect();
Universe {
width,
height,
cells,
}
}
pub fn render(&self) -> String {
self.to_string()
}
- 使用wasm-pack build打包
- 使用js渲染,修改html加入標(biāo)簽:
<pre id="game-of-life-canvas"></pre>
index.js加入下面代碼:
import { Universe } from "wasm-game-of-life";
const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
const renderLoop = () => {
pre.textContent = universe.render();
universe.tick();
requestAnimationFrame(renderLoop);
};
renderLoop();- 即可看見效果。
- 下面使用canvas進(jìn)行渲染,將universe中暴露其屬性:
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn cells(&self) -> *const Cell {
self.cells.as_ptr()
}
- html中替換為canvas:
<canvas id="game-of-life-canvas"></canvas>
修改js:
import { Universe, Cell } from "wasm-game-of-life";
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";
const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";
const universe = Universe.new();
const width = universe.width();
const height = universe.height();
// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;
const ctx = canvas.getContext("2d");
const drawGrid = () => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
// Vertical lines.
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);
}
// Horizontal lines.
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();
};
const getIndex = (row, column) => {
return row * width + column;
};
const drawCells = () => {
const cellsPtr = universe.cells();
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
ctx.beginPath();
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const idx = getIndex(row, col);
ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR;
ctx.fillRect(
col * (CELL_SIZE + 1) + 1,
row * (CELL_SIZE + 1) + 1,
CELL_SIZE,
CELL_SIZE
);
}
}
ctx.stroke();
};
const renderLoop = () => {
universe.tick();
drawGrid();
drawCells();
requestAnimationFrame(renderLoop);
};
renderLoop();即可看見效果:

測試
- 一般代碼需要寫單元測試,看一下rust的測試怎么寫。
- 首先,對Universe增加2個實現(xiàn),可以將元組轉(zhuǎn)換為universe的cell:
impl Universe {
/// Get the dead and alive values of the entire universe.
pub fn get_cells(&self) -> &[Cell] {
&self.cells
}
/// Set cells to be alive in a universe by passing the row and column
/// of each cell as an array.
pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
for (row, col) in cells.iter().cloned() {
let idx = self.get_index(row, col);
self.cells[idx] = Cell::Alive;
}
}
}
新增重置的方法:
/// Set the width of the universe.
///
/// Resets all cells to the dead state.
pub fn set_width(&mut self, width: u32) {
self.width = width;
self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
}
/// Set the height of the universe.
///
/// Resets all cells to the dead state.
pub fn set_height(&mut self, height: u32) {
self.height = height;
self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
}
- 下面編寫測試,測試在tests文件夾下的web.rs中。
- 增加以下代碼:
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use std::assert_eq;
use wasm_bindgen_test::*;
extern crate wasm_game_of_life;
use wasm_game_of_life::Universe;
wasm_bindgen_test_configure!(run_in_browser);
#[cfg(test)]
pub fn input_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(1, 2), (2, 3), (3, 1), (3, 2), (3, 3)]);
universe
}
#[cfg(test)]
pub fn expected_spaceship() -> Universe {
let mut universe = Universe::new();
universe.set_width(6);
universe.set_height(6);
universe.set_cells(&[(2, 1), (2, 3), (3, 2), (3, 3), (4, 2)]);
universe
}
#[wasm_bindgen_test]
pub fn test_tick() {
// Let's create a smaller Universe with a small spaceship to test!
let mut input_universe = input_spaceship();
// This is what our spaceship should look like
// after one tick in our universe.
let expected_universe = expected_spaceship();
// Call `tick` and then see if the cells in the `Universe`s are the same.
input_universe.tick();
assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
}
- 然后使用
wasm-pack test --firefox --headless即可運(yùn)行測試結(jié)果。如果安裝瀏覽器失敗,可以使用谷歌,或者去掉無頭屬性,直接網(wǎng)頁上看測試結(jié)果。
調(diào)試
- 我們知道,web上使用console.log去輸出調(diào)試內(nèi)容,rust的代碼如何在web中調(diào)試呢?
- 這里需要安裝下web-sys
[dependencies.web-sys] version = "0.3" features = [ "console", ]
- 導(dǎo)入外部websys,制作自定義宏:
extern crate web_sys;
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}format宏與其他幾個輸出區(qū)別在于其使用write,不輸出到標(biāo)準(zhǔn)輸出中:
format!: write formatted text to String print!: same as format! but the text is printed to the console (io::stdout). println!: same as print! but a newline is appended. eprint!: same as format! but the text is printed to the standard error (io::stderr). eprintln!: same as eprint!but a newline is appended.
然后就可以在需要的地方console了,比如neighbours那:
let live_neighbors = self.live_neighbor_count(row, col);
log!(
"cell[{}, {}] is initially {:?} and has {} live neighbors",
row,
col,
cell,
live_neighbors
);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
log!(" it becomes {:?}", next_cell);
next[idx] = next_cell;打開web,即可看見console的內(nèi)容。
到此這篇關(guān)于使用Rust制作康威生命游戲的文章就介紹到這了,更多相關(guān)Rust康威生命游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust捕獲全局panic并記錄進(jìn)程退出日志的方法
本文提供了捕獲全局panic并記錄進(jìn)程退出日志的方法,首先使用 panic::set_hook 注冊異常處理及panic 觸發(fā)異常,結(jié)合實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-04-04
Rust結(jié)構(gòu)體的定義與實例化詳細(xì)講解
結(jié)構(gòu)體是一種自定義的數(shù)據(jù)類型,它允許我們將多個不同的類型組合成一個整體。下面我們就來學(xué)習(xí)如何定義和使用結(jié)構(gòu)體,并對比元組與結(jié)構(gòu)體之間的異同,需要的可以參考一下2022-12-12

