irpas技术客

从零开始创建WebAssembly 游戏_上后左爱_webassembly 游戏

网络投稿 1347

概述

学习一段时间rust,开始使用rust 创建一些小项目,rust的一大用处,结合JS,算法部分使用rust进行编写将编译成webassembly 后通过js调用,提高运行速度 ,学习从0开始创建一个WebAssembly 游戏,学习rust如何与web 前端进行整合,该项目重点在于如何将rust编译成WebAssembly结合JS使用,至于游戏界面JS在此不进行详细介绍,直接使用模板编写。

wasm 与 wat 二进制格式(执行): .wasm文件后缀文本格式:.wat文件后缀 wat 是基于文本助记表示形式WAT, wat 通过WABT工具编译成二进制文件wasmchrome会将wasm 二进制编译成watwat2wasm 网址: 项目创建 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(); // 获取web wasm const wasm = await WebAssembly.instantiate(buffer); // 从 wasm 获取其中方法 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 第一步在Cargo.toml 配置 [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 lib.rs 中写调用jS中alert use wasm_bindgen::prelude::*; // rust 掉头JS #[wasm_bindgen] extern "C" { // 需要在 rust中表明这个是一个外部的方法 pub fn alert(s: &str); } #[wasm_bindgen] pub fn hello(name: &str){ alert(name); } 第二步:在当前rust目录下: cargo build // 生成target文件 wasm-pack build --target web // 生成wasm pkg目录 在 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; // rust 掉头JS #[wasm_bindgen] extern "C" { // 需要在 rust中表明这个是一个外部的方法 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; // rust use js module #[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, } // Directions to move #[wasm_bindgen] #[derive(PartialEq)] pub enum Direction { Up, Down, Left, Right, } #[derive(PartialEq, Clone, Copy)] pub struct SnakeCell(usize); // snake element 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 } // 调用gen_next_snake_cell 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(); // 调用gen_next_snake_cell 使用Option来提高性能 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>


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #WebAssembly #游戏 #游戏学习rust如何与web #wasm