概述
学习一段时间rust,开始使用rust 创建一些小项目,rust的一大用处,结合JS,算法部分使用rust进行编写将编译成webassembly 后通过js调用,提高运行速度 ,学习从0开始创建一个WebAssembly 游戏,学习rust如何与web 前端进行整合,该项目重点在于如何将rust编译成WebAssembly结合JS使用,至于游戏界面JS在此不进行详细介绍,直接使用模板编写。
wasm 与 wat
- 二进制格式(执行):
.wasm文件后缀 - 文本格式:
.wat文件后缀 wat 是基于文本助记表示形式WAT, wat 通过WABT工具编译成二进制文件wasm - chrome会将wasm 二进制编译成wat
- wat2wasm 网址:
项目创建
1. cargo new --lib wasm-game
2. 创建www 目录
3. 在www目录下面:
npm install --save-dev webpack-dev-server
npm install --save webpack-cli
npm install --save copy-webpack-plugin
- 加载wasm 文件必须得异步加载
wat2wasm 对于js 的使用wasm , 使用异步的方式对其进行加载
async function run() {
const response = await fetch("yz.wasm");
const buffer = await response.arrayBuffer();
const wasm = await WebAssembly.instantiate(buffer);
const addTwoFunction = wasm.instance.exports.addTwo;
const result = addTwoFunction(10, 20);
console.log(result);
}
run();
webAssembly中rust与js交互
- 前后端分离后,前端负责一部分,后端负责一部分内如,做一个互相的review
- 前往不要rust 写完部分功能后,把Trello卡片一移,交给JS这样主要树沟通成本太高。
1. 下载wasm-pack: https://rustwasm.github.io/wasm-pack/install
2. 配置crate-type
3. 配置wasm-bindgen
4. 配置wasm-opt:
[package.metadata.wasm-pack.profile.release]
wasm-opt= false
JS:
1. wasm-pack build --target web 生成包含wasm文件
2. 配置npm 的package.json 将PKG目录导入
3. 调用hello
[package]
name = "wasm_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = "0.2.78"
[lib]
crate-type = ["cdylib"]
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn hello(name: &str){
alert(name);
}
cargo build
wasm-pack build --target web
- 在 js的package.json 引入刚才生成的文件
"dependencies": {
"copy-webpack-plugin": "^10.2.0",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"wasm_game": "file:../pkg", // 名称和lib.rs 保持一致
"webpack": "^5.66.0",
"webpack-cli": "^4.9.1"
},
npm install 重新导入, npm run dev 重新运行 效果如下在wasm中调用 js中函数
weealloc 内存分配,bootstrap.js 捕获错误
wee_alloc webassembly属于一个轻量级的内存分配器
use wasm_bindgen::prelude::*;
use wee_alloc::WeeAlloc;
#[global_allocator]
static ALLOC: WeeAlloc = WeeAlloc::INIT;
#[wasm_bindgen]
extern "C" {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn hello(name: &str){
alert(name);
}
import ("./index.js").catch((e) => {
console.error("Error", e);
})
js 切换到TS
npm install typescript
npm install --save typescript ts-loader
创建画布
1.使用rust 存储蛇的长度,每个蛇的像素带你位置,向右移动 像素+1,注意边界问题 2. js 中使用canve 创建画布 3. 使用setTime 进行刷新
use wasm_bindgen::prelude::*;
use wee_alloc::WeeAlloc;
#[global_allocator]
static ALLOC: WeeAlloc = WeeAlloc::INIT;
#[wasm_bindgen(module = "/www/utils/random.js")]
extern "C" {
fn random(max: usize) -> usize;
}
#[wasm_bindgen]
#[derive(Copy, Clone)]
pub enum GameStatus {
Won,
Lost,
Played,
}
#[wasm_bindgen]
#[derive(PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(PartialEq, Clone, Copy)]
pub struct SnakeCell(usize);
struct Snake {
body: Vec<SnakeCell>,
direction: Direction,
}
impl Snake {
fn new(spawn_index: usize, size: usize) -> Self {
let mut body = Vec::new();
for i in 0..size {
body.push(SnakeCell(spawn_index - i))
}
Self {
body,
direction: Direction::Down,
}
}
}
#[wasm_bindgen]
pub struct World {
width: usize,
size: usize,
reward_cell: Option<usize>,
snake: Snake,
next_cell: Option<SnakeCell>,
status: Option<GameStatus>,
}
#[wasm_bindgen]
impl World {
pub fn new(width: usize, snake_index: usize) -> Self {
let size = width * width;
let snake = Snake::new(snake_index, 3);
Self {
width,
size: width * width,
reward_cell: Some(World::gen_reward_cell(size, &snake.body)),
snake,
next_cell: None,
status: None,
}
}
fn gen_reward_cell(max: usize, snake_body: &Vec<SnakeCell>) -> usize {
let mut reward_cell;
loop {
reward_cell = random(max);
if !snake_body.contains(&SnakeCell(reward_cell)) {
break;
}
}
reward_cell
}
pub fn start_game(&mut self) {
self.status = Some(GameStatus::Played);
}
pub fn game_status(&self) -> Option<GameStatus> {
self.status
}
pub fn game_status_info(&self) -> String {
match self.status {
Some(GameStatus::Won) => "You Won!".to_string(),
Some(GameStatus::Lost) => "You Lost!".to_string(),
Some(GameStatus::Played) => "You Playing...".to_string(),
None => "None!".to_string(),
}
}
pub fn reward_cell(&self) -> Option<usize> {
self.reward_cell
}
pub fn width(&self) -> usize {
self.width
}
pub fn snake_head_index(&self) -> usize {
self.snake.body[0].0
}
pub fn change_snake_direction(&mut self, direction: Direction) {
let next_cell = self.gen_next_snake_cell(&direction);
if self.snake.body[1].0 == next_cell.0 {
return;
}
self.snake.direction = direction;
}
pub fn snake_cells(&self) -> *const SnakeCell {
self.snake.body.as_ptr()
}
pub fn snake_length(&self) -> usize {
self.snake.body.len()
}
pub fn update(&mut self) {
let temp = self.snake.body.clone();
match self.next_cell {
Some(cell) => {
self.snake.body[0] = cell;
self.next_cell = None;
}
None => {
self.snake.body[0] = self.gen_next_snake_cell(&self.snake.direction);
}
}
let len = self.snake.body.len();
for i in 1..len {
self.snake.body[i] = SnakeCell(temp[i - 1].0);
}
if self.snake.body[1..len].contains(&self.snake.body[0]) {
self.status = Some(GameStatus::Lost);
}
if self.reward_cell == Some(self.snake_head_index()) {
if self.snake_length() < self.size {
self.reward_cell = Some(World::gen_reward_cell(self.size, &self.snake.body));
} else {
self.reward_cell = None;
self.status = Some(GameStatus::Won);
}
self.snake.body.push(SnakeCell(self.snake.body[1].0));
}
}
fn gen_next_snake_cell(&self, direction: &Direction) -> SnakeCell {
let snake_index = self.snake_head_index();
let row = snake_index / self.width;
return match direction {
Direction::Up => {
let border_hold = snake_index - row * self.width;
if snake_index == border_hold {
SnakeCell((self.size - self.width) + border_hold)
} else {
SnakeCell(snake_index - self.width)
}
}
Direction::Down => {
let border_hold = snake_index + ((self.width - row) * self.width);
if snake_index + self.width == border_hold {
SnakeCell(border_hold - (row + 1) * self.width)
} else {
SnakeCell(snake_index + self.width)
}
}
Direction::Left => {
let border_hold = row * self.width;
if snake_index == border_hold {
SnakeCell(border_hold + self.width - 1)
} else {
SnakeCell(snake_index - 1)
}
}
Direction::Right => {
let border_hold = (row + 1) * self.width;
if snake_index + 1 == border_hold {
SnakeCell(border_hold - self.width)
} else {
SnakeCell(snake_index + 1)
}
}
};
}
}
import init, { World, Direction, GameStatus } from "wasm_game";
import {random} from "./utils/random";
init().then(wasm => {
const CELL_SIZE = 20;
const WORLD_WIDTH = 4;
const snakeIndex = random(WORLD_WIDTH * WORLD_WIDTH);
const world = World.new(WORLD_WIDTH, snakeIndex);
const worldWidth = world.width();
const fps = 2;
const gameStatus = document.getElementById("game-status");
const gameControlBtn = document.getElementById("game-control-btn");
const canvas = <HTMLCanvasElement>document.getElementById("snake-world");
const context = canvas.getContext("2d");
canvas.width = worldWidth * CELL_SIZE;
canvas.height = worldWidth * CELL_SIZE;
gameControlBtn.addEventListener("click", ()=>{
const status = world.game_status();
if(status == undefined) {
gameControlBtn.textContent = "游戏中...";
world.start_game();
run();
} else {
location.reload();
}
})
document.addEventListener("keydown", e => {
switch (e.code) {
case "ArrowUp":
world.change_snake_direction(Direction.Up);
break;
case "ArrowDown":
world.change_snake_direction(Direction.Down);
break;
case "ArrowLeft":
world.change_snake_direction(Direction.Left);
break;
case "ArrowRight":
world.change_snake_direction(Direction.Right);
break;
}
})
function drawWorld() {
context.beginPath();
for (let x = 0; x < worldWidth + 1; x++) {
context.moveTo(CELL_SIZE * x, 0);
context.lineTo(CELL_SIZE * x, CELL_SIZE * worldWidth);
}
for (let y = 0; y < worldWidth + 1; y++) {
context.moveTo(0, CELL_SIZE * y);
context.lineTo(CELL_SIZE * worldWidth, CELL_SIZE * y);
}
context.stroke();
}
function drawSnake() {
const snakeCells = new Uint32Array(
wasm.memory.buffer,
world.snake_cells(),
world.snake_length()
);
snakeCells
.filter((cellIdx, i) => !(i>0 && cellIdx == snakeCells[0]))
.forEach((cellIndex, i)=> {
const col = cellIndex % worldWidth;
const row = Math.floor(cellIndex/worldWidth);
context.beginPath();
context.fillStyle = i === 0 ? '#787878':'#000000';
context.fillRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE);
})
context.stroke();
}
function drawReward() {
const index = world.reward_cell();
const row = Math.floor(index / worldWidth);
const col = index % worldWidth;
context.beginPath();
context.fillStyle = '#FF0000';
context.fillRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE);
context.stroke();
}
function drawGameStatus() {
gameStatus.textContent = world.game_status_info();
}
function draw() {
drawWorld();
drawSnake();
drawReward();
drawGameStatus();
}
function run() {
const status = world.game_status();
if (status === GameStatus.Won || status == GameStatus.Lost) {
gameControlBtn.textContent = "再玩一次?";
return;
}
setTimeout(() => {
context.clearRect(0, 0, canvas.width, canvas.height);
world.update();
draw();
requestAnimationFrame(run);
}, 1000 / fps);
}
draw();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.flex {
display: flex
}
.label {
font-weight: bold;
margin-right: 13px;
}
.game-content {
margin-bottom: 20px;
}
.content-wrapper {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>
</head>
<body>
<div class="content-wrapper">
<div class="game-content">
<div class="flex">
<div class="label">
Status:
</div>
<div id="game-status">
None
</div>
</div>
<div class="flex">
<button id="game-control-btn">
开始游戏
</button>
</div>
</div>
<canvas id="snake-world"></canvas>
</div>
<script src="./bootstrap.js"></script>
</body>
</html>
|