首頁 > 軟體

使用Rust製作康威生命遊戲的實現程式碼

2022-09-30 14:01:43

前言

之前學了幾遍,後來忘記了,通過製作該遊戲再複習複習。

安裝準備

cargo install cargo-generate

初始專案

初始rust專案

使用wasm的專案模板:

cargo generate --git https://github.com/rustwasm/wasm-pack-template
  • 提示輸入project名wasm-game-of-life
  • 在lib.rs中可以看見如下內容:
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!");
}
  • 它匯入 window.alertJavaScript 函數,並匯出greet的Rust 函數。

Cargo.toml

  • Cargo.toml預置了[lib]和[dependencies]。解釋一下crate-type中f=“https://users.rust-lang.org/t/what-is-the-difference-between-dylib-and-cdylib/28847”>cdylib和rlib的作用:
  • cdylib:顧名思義,是C的動態連結庫的意思,可以被C和C++程式連結使用
  • rlib:Rust靜態連結庫,用於靜態連線其他crates
  • 依賴中使用的:
  • wasm-bindgen可以將Rust編寫的函數和結構體暴露到JS中或者把JS的方法引入到Rust中使用
  • console_error_panic_hook提供了Wasm輸出Rust Panic的能力
  • wee_alloc是一個輕量的Wasm記憶體分配器,但是會比預設分配器慢一些。

初始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裡匯入boostrap.js,boostrap.js裡匯入index.js。 index.js裡面匯入了其已經制作好的一個包:
import * as wasm from "hello-wasm-pack";
wasm.greet();
  • 我們修改pkg.json,匯入自己的包(該包需要使用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。

遊戲規則

  • Conway’s Game of Life是英國數學家約翰·何頓·康威在1970年發明的放置類無玩家參與的遊戲
  • 百度百科
  • 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主要規則如下:
  • 1、任何少於兩個活鄰居的活細胞都會死亡,就像是由於人口不足造成的。
  • 2、任何有兩三個活鄰居的活細胞都可以活到下一代。
  • 3、任何有超過三個活鄰居的活細胞都會死亡,就像人口過剩一樣。
  • 4、任何只有三個活鄰居的死細胞都會變成活細胞,就像通過繁殖一樣。

遊戲設計

  • 為啥說這個呢,因為2種語言去做這個東西會考慮哪個東西在哪個裡面去實現。
  • rust推薦大型、長壽命的資料結構被實現為 Rust 型別,這些型別存在於 WebAssembly 線性記憶體中,並作為不透明的控制程式碼暴露給 JavaScript。JavaScript 呼叫匯出的 WebAssembly 函數,這些函數採用這些不透明的控制程式碼、轉換它們的資料、執行繁重的計算、查詢資料並最終返回一個可複製的結果。通過只返回計算結果,我們避免了在 JavaScript 垃圾收集堆和 WebAssembly 線性記憶體之間來回複製和/或序列化所有內容。
  • 這個遊戲中,會將universe的顯示效果暴露給js渲染,其餘計算在rust去實現。
  • 由於宇宙是n*n的,所以我們可以用一維陣列去表示它,比如4x4的宇宙就是這樣:

  • 將陣列每個row換下來就是需要的4x4的顯示了。因為這種表現形式,所以我們需要對陣列索引和行列進行轉換,公式為:
index(row, column, universe) = row * width(universe) + column
  • 就比如我要知道4行4列是索引幾,根據公式就是3*4 + 3。
  • 每個單元格有一個位元組,其中0表示死亡,1表示存活。

Rust實現

首先我們需要定義每個單元格:

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}

列舉型別,0是死亡,1是存活,#[repr(u8)]表示一個單元格1位元組。複習下:

長度有符號無符號
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

接下來定義宇宙:

#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
  • 宇宙是長寬和一個動態陣列。
  • 我們對universe實現一些方法便於操作:
#[wasm_bindgen]
impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }
}
  • get_index就是上面公式做索引。
  • 從前面遊戲規則上可知,我們需要對每個單元格求出周圍格子的存活數量,於是加上這個函數:
  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
    }
  • 解釋下這個函數,其中迭代height-1 , 0 , 1 以及 width-1,0,1就是求傳入row與col的周圍的格子裡存活數量。當迭代到0,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

  • 再從當前宇宙中獲取格子的狀態,如果是0,那麼加上也不會增加,這樣最終返回的就是周圍格子的存活數量了。
  • 下面根據規則迭代每個細胞狀態,暴露出來:
 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實現輸出功能,先將其輸出成文字,實現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(())
    }
}

最後進行暴露初始化和渲染方法:

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加入標籤:
 <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進行渲染,將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個實現,可以將元組轉換為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即可執行測試結果。如果安裝瀏覽器失敗,可以使用谷歌,或者去掉無頭屬性,直接網頁上看測試結果。

偵錯

  • 我們知道,web上使用console.log去輸出偵錯內容,rust的程式碼如何在web中偵錯呢?
  • 這裡需要安裝下web-sys
[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]
  • 匯入外部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宏與其他幾個輸出區別在於其使用write,不輸出到標準輸出中:

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的內容。

到此這篇關於使用Rust製作康威生命遊戲的文章就介紹到這了,更多相關Rust康威生命遊戲內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com