003 Rust Assembly Realization of Conway's Game of Life

0 introduction

Video address: https://www.bilibili.com/video/BV1eg411g7c8
Related source code: https://github.com/anonymousGiga/Rust-and-Web-Assembly

In this section, we implement a simple game with WebAssembly.

1 game rules

In a two-dimensional grid, the state of each grid is "live" or "dead". Each square corresponds to a cell, and each cell is adjacent to its surrounding eight squares. During each time lapse, the following transitions occur:

1. Any living cell with fewer than two living neighbors dies.

2. Any living cell with two or three living neighbors survives to the next generation.

3. Any living cell with more than three neighbors will die.

4. Any dead cell with three live neighbors will become a live cell.

Consider the initial state:

----------------
|  |  |  |  |  |
----------------
|  |  |生|  |  |
----------------
|  |  |生|  |  |
----------------
|  |  |生|  |  |
----------------
|  |  |  |  |  |
----------------

According to the above rules, the next time point will become:

----------------
|  |  |  |  |  |
----------------
|  |  |  |  |  |
----------------
|  |生|生|生|  |
----------------
|  |  |  |  |  |
----------------
|  |  |  |  |  |
----------------

2 designs

2.1 Design Rules

2.1.1 The Design of the Universe

The so-called universe is just a two-dimensional grid design. Because the life cycle game is played in an infinite universe, but we do not have infinite memory and computing power, so we can design the entire universe in three ways:

1. The way of continuous expansion.

2. Create fixed-size universes where there are fewer cells on the edges than in the middle, a pattern with an end.

3. Create a universe of fixed size, but where the end on the left ends on the right.

2.1.2 Principles of interaction between Rust and Js

1. Minimize copying of WebAssembly linear memory. Unnecessary copies introduce unnecessary overhead.

2. Minimal serialization and deserialization. Like copies, serialization and deserialization incur overhead, and often also copying.

In general, a good javascript and WebAssembly interface design usually implements large, long-lived data structures as Rust types that reside in WebAssembly's linear memory, and pass them to JavaScript as opaque handles.

2.1.3 Rust and Js interaction design in our game

We can use an array to represent, 0 in each element means dead cells, 1 means living cells, therefore, the 4*4 universe is like this:

Indices: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15

        -------------------------------------------------
array  :|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
        -------------------------------------------------
Rows:   |   0       |     1     |         2 |    3     |

To find out the index value of a given row and column in the universe, the formula is as follows:

index(row, column, universe) = row * width(universe) + column

3 Rust implementation

Start modifying wasm-game-of-life/src/lib.rs to add code

3.1 Define the cell state enumeration type

code show as below:

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

In the above code, we define the state of each cell, 0 means dead and 1 means alive. The above #[repr(u8)] means that the following enumeration type occupies 8 bits of memory.

3.2 Defining the universe

Below we define the universe, the code is as follows:

#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}

The relevant methods are defined below:

#[wasm_bindgen]
impl Universe {
	//获取到对应的索引
	fn get_index(&self, row: u32, column: u32) -> usize {
		(row*self.width + column) as usize
	}

	//获取活着的邻居个数
	//相邻的都是-1, 0, 1
	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;
				let idx = self.get_index(neighbor_row, neighbor_col);
				count += self.cells[idx] as u8;
			}
		}
		count
	}

	//计算下一个滴答的状态
	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) {
					(Cell::Alive, x) if x < 2 => Cell::Dead,
					(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
					(Cell::Alive, x) if x > 3 => Cell::Dead,
					(Cell::Dead, 3) => Cell::Alive,
					(otherwise, _) => otherwise,
				};

				next[idx] = next_cell;
			}
		}
		self.cells = next;
	}
	
}

So far, we have basically written the core logic. However, we want to use black squares to represent living cells and empty squares to represent dead cells. We also need to write the following code:

use std::fmt;

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(())
	}
}

Next, we write the rest of the code, the code to create the universe and the code to populate it:

#[wasm_bindgen]
impl Universe {
	//创建
	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()
	}
	
	...
}	

3.3 compile

Compile with the following command:

wasm-pack build

4 call code writing

Modify wasm-game-of-life/www/index.html as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>game-of-life-canvas</title>
	<style>
  		body {
  		  position: absolute;
  		  top: 0;
  		  left: 0;
  		  width: 100%;
  		  height: 100%;
  		  display: flex;
  		  flex-direction: column;
  		  align-items: center;
  		  justify-content: center;
  		}
	</style>
  </head>
  <body>
	<pre id="game-of-life-canvas"></pre>
    <script src="./bootstrap.js"></script>
  </body>
</html>

Modify the code of index.js as follows:

import { Universe } from "wasm-game-of-life";

const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
//alert("+++++++++++");

function renderLoop() {
	pre.textContent = universe.render();
	universe.tick();
	window.requestAnimationFrame(renderLoop);
}

window.requestAnimationFrame(renderLoop);

5 calls

Go to the www directory and execute the command:

npm run start

Enter the following address in the browser to display the cell changes:

127.0.0.1:8080

Guess you like

Origin blog.csdn.net/lcloveyou/article/details/123344304