- 发布于
Rust-Wasm在Web游戏开发中的应用:构建高性能的浏览器游戏
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
Rust-Wasm在Web游戏开发中的应用:构建高性能的浏览器游戏
Web游戏开发正在经历一场技术革命,Rust与WebAssembly的结合为开发者提供了构建高性能浏览器游戏的强大工具。本文将详细介绍完整的Web游戏开发方案。
游戏引擎架构设计
项目配置和依赖
# Cargo.toml - Web游戏引擎配置
[package]
name = "rust-wasm-game-engine"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
js-sys = "0.3"
web-sys = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
getrandom = { version = "0.2", features = ["js"] }
console_error_panic_hook = "0.1"
wee_alloc = "0.4"
# 游戏开发相关依赖
nalgebra = "0.32"
rapier2d = "0.17"
image = { version = "0.24", default-features = false, features = ["png", "jpeg"] }
rand = "0.8"
instant = { version = "0.1", features = ["wasm-bindgen"] }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Document",
"Element",
"HtmlElement",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"WebGlRenderingContext",
"WebGl2RenderingContext",
"WebGlProgram",
"WebGlShader",
"WebGlBuffer",
"WebGlTexture",
"WebGlUniformLocation",
"ImageData",
"Window",
"Performance",
"KeyboardEvent",
"MouseEvent",
"TouchEvent",
"AudioContext",
"AudioBuffer",
"AudioBufferSourceNode",
"GainNode",
]
# 优化配置
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
核心游戏引擎
// src/lib.rs - 游戏引擎核心
use wasm_bindgen::prelude::*;
use web_sys::*;
use nalgebra::{Vector2, Point2};
use std::collections::HashMap;
use instant::Instant;
// 全局设置
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
}
// 游戏对象组件系统
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct Transform {
pub position: Vector2<f32>,
pub rotation: f32,
pub scale: Vector2<f32>,
}
#[wasm_bindgen]
impl Transform {
#[wasm_bindgen(constructor)]
pub fn new(x: f32, y: f32) -> Transform {
Transform {
position: Vector2::new(x, y),
rotation: 0.0,
scale: Vector2::new(1.0, 1.0),
}
}
#[wasm_bindgen(getter)]
pub fn x(&self) -> f32 {
self.position.x
}
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: f32) {
self.position.x = x;
}
#[wasm_bindgen(getter)]
pub fn y(&self) -> f32 {
self.position.y
}
#[wasm_bindgen(setter)]
pub fn set_y(&mut self, y: f32) {
self.position.y = y;
}
#[wasm_bindgen]
pub fn translate(&mut self, dx: f32, dy: f32) {
self.position.x += dx;
self.position.y += dy;
}
#[wasm_bindgen]
pub fn rotate(&mut self, angle: f32) {
self.rotation += angle;
}
#[wasm_bindgen]
pub fn set_scale(&mut self, sx: f32, sy: f32) {
self.scale.x = sx;
self.scale.y = sy;
}
}
// 游戏实体
#[wasm_bindgen]
pub struct GameObject {
id: u32,
transform: Transform,
velocity: Vector2<f32>,
active: bool,
tag: String,
}
#[wasm_bindgen]
impl GameObject {
#[wasm_bindgen(constructor)]
pub fn new(id: u32, x: f32, y: f32) -> GameObject {
GameObject {
id,
transform: Transform::new(x, y),
velocity: Vector2::new(0.0, 0.0),
active: true,
tag: String::new(),
}
}
#[wasm_bindgen(getter)]
pub fn id(&self) -> u32 {
self.id
}
#[wasm_bindgen(getter)]
pub fn active(&self) -> bool {
self.active
}
#[wasm_bindgen(setter)]
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
#[wasm_bindgen]
pub fn get_transform(&self) -> Transform {
self.transform.clone()
}
#[wasm_bindgen]
pub fn set_transform(&mut self, transform: Transform) {
self.transform = transform;
}
#[wasm_bindgen]
pub fn set_velocity(&mut self, vx: f32, vy: f32) {
self.velocity.x = vx;
self.velocity.y = vy;
}
#[wasm_bindgen]
pub fn update(&mut self, delta_time: f32) {
if self.active {
self.transform.position += self.velocity * delta_time;
}
}
#[wasm_bindgen]
pub fn set_tag(&mut self, tag: &str) {
self.tag = tag.to_string();
}
#[wasm_bindgen]
pub fn get_tag(&self) -> String {
self.tag.clone()
}
}
// 游戏场景管理器
#[wasm_bindgen]
pub struct Scene {
objects: HashMap<u32, GameObject>,
next_id: u32,
camera_x: f32,
camera_y: f32,
}
#[wasm_bindgen]
impl Scene {
#[wasm_bindgen(constructor)]
pub fn new() -> Scene {
Scene {
objects: HashMap::new(),
next_id: 1,
camera_x: 0.0,
camera_y: 0.0,
}
}
#[wasm_bindgen]
pub fn create_object(&mut self, x: f32, y: f32) -> u32 {
let id = self.next_id;
self.next_id += 1;
let object = GameObject::new(id, x, y);
self.objects.insert(id, object);
id
}
#[wasm_bindgen]
pub fn get_object(&self, id: u32) -> Option<GameObject> {
self.objects.get(&id).cloned()
}
#[wasm_bindgen]
pub fn update_object(&mut self, id: u32, object: GameObject) {
self.objects.insert(id, object);
}
#[wasm_bindgen]
pub fn remove_object(&mut self, id: u32) {
self.objects.remove(&id);
}
#[wasm_bindgen]
pub fn update(&mut self, delta_time: f32) {
for object in self.objects.values_mut() {
object.update(delta_time);
}
}
#[wasm_bindgen]
pub fn get_all_objects(&self) -> Vec<u32> {
self.objects.keys().cloned().collect()
}
#[wasm_bindgen]
pub fn set_camera(&mut self, x: f32, y: f32) {
self.camera_x = x;
self.camera_y = y;
}
#[wasm_bindgen]
pub fn get_camera_x(&self) -> f32 {
self.camera_x
}
#[wasm_bindgen]
pub fn get_camera_y(&self) -> f32 {
self.camera_y
}
}
// 输入管理器
#[wasm_bindgen]
pub struct InputManager {
keys_pressed: HashMap<String, bool>,
keys_just_pressed: HashMap<String, bool>,
mouse_x: f32,
mouse_y: f32,
mouse_buttons: HashMap<i16, bool>,
}
#[wasm_bindgen]
impl InputManager {
#[wasm_bindgen(constructor)]
pub fn new() -> InputManager {
InputManager {
keys_pressed: HashMap::new(),
keys_just_pressed: HashMap::new(),
mouse_x: 0.0,
mouse_y: 0.0,
mouse_buttons: HashMap::new(),
}
}
#[wasm_bindgen]
pub fn key_down(&mut self, key: &str) {
if !self.keys_pressed.get(key).unwrap_or(&false) {
self.keys_just_pressed.insert(key.to_string(), true);
}
self.keys_pressed.insert(key.to_string(), true);
}
#[wasm_bindgen]
pub fn key_up(&mut self, key: &str) {
self.keys_pressed.insert(key.to_string(), false);
}
#[wasm_bindgen]
pub fn is_key_pressed(&self, key: &str) -> bool {
*self.keys_pressed.get(key).unwrap_or(&false)
}
#[wasm_bindgen]
pub fn is_key_just_pressed(&self, key: &str) -> bool {
*self.keys_just_pressed.get(key).unwrap_or(&false)
}
#[wasm_bindgen]
pub fn mouse_move(&mut self, x: f32, y: f32) {
self.mouse_x = x;
self.mouse_y = y;
}
#[wasm_bindgen]
pub fn mouse_down(&mut self, button: i16) {
self.mouse_buttons.insert(button, true);
}
#[wasm_bindgen]
pub fn mouse_up(&mut self, button: i16) {
self.mouse_buttons.insert(button, false);
}
#[wasm_bindgen]
pub fn is_mouse_button_pressed(&self, button: i16) -> bool {
*self.mouse_buttons.get(&button).unwrap_or(&false)
}
#[wasm_bindgen]
pub fn get_mouse_x(&self) -> f32 {
self.mouse_x
}
#[wasm_bindgen]
pub fn get_mouse_y(&self) -> f32 {
self.mouse_y
}
#[wasm_bindgen]
pub fn clear_just_pressed(&mut self) {
self.keys_just_pressed.clear();
}
}
// 游戏引擎主类
#[wasm_bindgen]
pub struct GameEngine {
scene: Scene,
input: InputManager,
last_time: f64,
running: bool,
canvas_width: f32,
canvas_height: f32,
}
#[wasm_bindgen]
impl GameEngine {
#[wasm_bindgen(constructor)]
pub fn new(canvas_width: f32, canvas_height: f32) -> GameEngine {
GameEngine {
scene: Scene::new(),
input: InputManager::new(),
last_time: 0.0,
running: false,
canvas_width,
canvas_height,
}
}
#[wasm_bindgen]
pub fn start(&mut self) {
self.running = true;
self.last_time = js_sys::Date::now();
}
#[wasm_bindgen]
pub fn stop(&mut self) {
self.running = false;
}
#[wasm_bindgen]
pub fn update(&mut self, current_time: f64) -> f32 {
if !self.running {
return 0.0;
}
let delta_time = ((current_time - self.last_time) / 1000.0) as f32;
self.last_time = current_time;
// 更新场景
self.scene.update(delta_time);
// 清理输入状态
self.input.clear_just_pressed();
delta_time
}
#[wasm_bindgen]
pub fn get_scene(&mut self) -> Scene {
// 注意:这里返回场景的克隆,实际应用中可能需要更好的设计
Scene::new() // 简化实现
}
#[wasm_bindgen]
pub fn get_input(&mut self) -> InputManager {
// 注意:这里返回输入管理器的引用,实际应用中需要更好的设计
InputManager::new() // 简化实现
}
#[wasm_bindgen]
pub fn create_object(&mut self, x: f32, y: f32) -> u32 {
self.scene.create_object(x, y)
}
#[wasm_bindgen]
pub fn get_object(&self, id: u32) -> Option<GameObject> {
self.scene.get_object(id)
}
#[wasm_bindgen]
pub fn update_object(&mut self, id: u32, object: GameObject) {
self.scene.update_object(id, object);
}
#[wasm_bindgen]
pub fn remove_object(&mut self, id: u32) {
self.scene.remove_object(id);
}
#[wasm_bindgen]
pub fn handle_key_down(&mut self, key: &str) {
self.input.key_down(key);
}
#[wasm_bindgen]
pub fn handle_key_up(&mut self, key: &str) {
self.input.key_up(key);
}
#[wasm_bindgen]
pub fn handle_mouse_move(&mut self, x: f32, y: f32) {
self.input.mouse_move(x, y);
}
#[wasm_bindgen]
pub fn handle_mouse_down(&mut self, button: i16) {
self.input.mouse_down(button);
}
#[wasm_bindgen]
pub fn handle_mouse_up(&mut self, button: i16) {
self.input.mouse_up(button);
}
#[wasm_bindgen]
pub fn is_key_pressed(&self, key: &str) -> bool {
self.input.is_key_pressed(key)
}
#[wasm_bindgen]
pub fn is_key_just_pressed(&self, key: &str) -> bool {
self.input.is_key_just_pressed(key)
}
#[wasm_bindgen]
pub fn get_mouse_position(&self) -> Vec<f32> {
vec![self.input.get_mouse_x(), self.input.get_mouse_y()]
}
#[wasm_bindgen]
pub fn set_camera(&mut self, x: f32, y: f32) {
self.scene.set_camera(x, y);
}
#[wasm_bindgen]
pub fn get_camera_position(&self) -> Vec<f32> {
vec![self.scene.get_camera_x(), self.scene.get_camera_y()]
}
}
物理引擎集成
// src/physics.rs - 物理引擎模块
use wasm_bindgen::prelude::*;
use rapier2d::prelude::*;
use nalgebra::{Vector2, Point2};
use std::collections::HashMap;
#[wasm_bindgen]
pub struct PhysicsWorld {
rigid_body_set: RigidBodySet,
collider_set: ColliderSet,
gravity: Vector2<f32>,
integration_parameters: IntegrationParameters,
physics_pipeline: PhysicsPipeline,
island_manager: IslandManager,
broad_phase: BroadPhase,
narrow_phase: NarrowPhase,
impulse_joint_set: ImpulseJointSet,
multibody_joint_set: MultibodyJointSet,
ccd_solver: CCDSolver,
query_pipeline: QueryPipeline,
physics_hooks: (),
event_handler: (),
body_to_entity: HashMap<RigidBodyHandle, u32>,
}
#[wasm_bindgen]
impl PhysicsWorld {
#[wasm_bindgen(constructor)]
pub fn new() -> PhysicsWorld {
PhysicsWorld {
rigid_body_set: RigidBodySet::new(),
collider_set: ColliderSet::new(),
gravity: Vector2::new(0.0, -9.81),
integration_parameters: IntegrationParameters::default(),
physics_pipeline: PhysicsPipeline::new(),
island_manager: IslandManager::new(),
broad_phase: BroadPhase::new(),
narrow_phase: NarrowPhase::new(),
impulse_joint_set: ImpulseJointSet::new(),
multibody_joint_set: MultibodyJointSet::new(),
ccd_solver: CCDSolver::new(),
query_pipeline: QueryPipeline::new(),
physics_hooks: (),
event_handler: (),
body_to_entity: HashMap::new(),
}
}
#[wasm_bindgen]
pub fn set_gravity(&mut self, x: f32, y: f32) {
self.gravity = Vector2::new(x, y);
}
#[wasm_bindgen]
pub fn create_dynamic_body(&mut self, entity_id: u32, x: f32, y: f32) -> u32 {
let rigid_body = RigidBodyBuilder::dynamic()
.translation(Vector2::new(x, y))
.build();
let handle = self.rigid_body_set.insert(rigid_body);
self.body_to_entity.insert(handle, entity_id);
handle.into_raw_parts().0 as u32
}
#[wasm_bindgen]
pub fn create_static_body(&mut self, entity_id: u32, x: f32, y: f32) -> u32 {
let rigid_body = RigidBodyBuilder::fixed()
.translation(Vector2::new(x, y))
.build();
let handle = self.rigid_body_set.insert(rigid_body);
self.body_to_entity.insert(handle, entity_id);
handle.into_raw_parts().0 as u32
}
#[wasm_bindgen]
pub fn add_box_collider(&mut self, body_handle: u32, width: f32, height: f32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(_) = self.rigid_body_set.get(handle) {
let collider = ColliderBuilder::cuboid(width / 2.0, height / 2.0)
.build();
self.collider_set.insert_with_parent(collider, handle, &mut self.rigid_body_set);
}
}
#[wasm_bindgen]
pub fn add_circle_collider(&mut self, body_handle: u32, radius: f32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(_) = self.rigid_body_set.get(handle) {
let collider = ColliderBuilder::ball(radius)
.build();
self.collider_set.insert_with_parent(collider, handle, &mut self.rigid_body_set);
}
}
#[wasm_bindgen]
pub fn set_body_velocity(&mut self, body_handle: u32, vx: f32, vy: f32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(body) = self.rigid_body_set.get_mut(handle) {
body.set_linvel(Vector2::new(vx, vy), true);
}
}
#[wasm_bindgen]
pub fn apply_force(&mut self, body_handle: u32, fx: f32, fy: f32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(body) = self.rigid_body_set.get_mut(handle) {
body.add_force(Vector2::new(fx, fy), true);
}
}
#[wasm_bindgen]
pub fn apply_impulse(&mut self, body_handle: u32, ix: f32, iy: f32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(body) = self.rigid_body_set.get_mut(handle) {
body.apply_impulse(Vector2::new(ix, iy), true);
}
}
#[wasm_bindgen]
pub fn get_body_position(&self, body_handle: u32) -> Vec<f32> {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(body) = self.rigid_body_set.get(handle) {
let pos = body.translation();
vec![pos.x, pos.y]
} else {
vec![0.0, 0.0]
}
}
#[wasm_bindgen]
pub fn get_body_rotation(&self, body_handle: u32) -> f32 {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(body) = self.rigid_body_set.get(handle) {
body.rotation().angle()
} else {
0.0
}
}
#[wasm_bindgen]
pub fn step(&mut self, delta_time: f32) {
self.integration_parameters.dt = delta_time;
self.physics_pipeline.step(
&self.gravity,
&self.integration_parameters,
&mut self.island_manager,
&mut self.broad_phase,
&mut self.narrow_phase,
&mut self.rigid_body_set,
&mut self.collider_set,
&mut self.impulse_joint_set,
&mut self.multibody_joint_set,
&mut self.ccd_solver,
Some(&mut self.query_pipeline),
&self.physics_hooks,
&self.event_handler,
);
}
#[wasm_bindgen]
pub fn raycast(&self, start_x: f32, start_y: f32, dir_x: f32, dir_y: f32, max_distance: f32) -> Option<Vec<f32>> {
let ray = Ray::new(
Point2::new(start_x, start_y),
Vector2::new(dir_x, dir_y)
);
if let Some((handle, toi)) = self.query_pipeline.cast_ray(
&self.rigid_body_set,
&self.collider_set,
&ray,
max_distance,
true,
QueryFilter::default()
) {
let hit_point = ray.point_at(toi);
Some(vec![hit_point.x, hit_point.y, toi])
} else {
None
}
}
#[wasm_bindgen]
pub fn remove_body(&mut self, body_handle: u32) {
let handle = RigidBodyHandle::from_raw_parts(body_handle, 0);
if let Some(_) = self.rigid_body_set.get(handle) {
self.rigid_body_set.remove(
handle,
&mut self.island_manager,
&mut self.collider_set,
&mut self.impulse_joint_set,
&mut self.multibody_joint_set,
true
);
self.body_to_entity.remove(&handle);
}
}
}
// 物理组件
#[wasm_bindgen]
pub struct PhysicsComponent {
body_handle: Option<u32>,
mass: f32,
friction: f32,
restitution: f32,
is_sensor: bool,
}
#[wasm_bindgen]
impl PhysicsComponent {
#[wasm_bindgen(constructor)]
pub fn new() -> PhysicsComponent {
PhysicsComponent {
body_handle: None,
mass: 1.0,
friction: 0.5,
restitution: 0.3,
is_sensor: false,
}
}
#[wasm_bindgen]
pub fn set_body_handle(&mut self, handle: u32) {
self.body_handle = Some(handle);
}
#[wasm_bindgen]
pub fn get_body_handle(&self) -> Option<u32> {
self.body_handle
}
#[wasm_bindgen]
pub fn set_mass(&mut self, mass: f32) {
self.mass = mass;
}
#[wasm_bindgen]
pub fn get_mass(&self) -> f32 {
self.mass
}
#[wasm_bindgen]
pub fn set_friction(&mut self, friction: f32) {
self.friction = friction;
}
#[wasm_bindgen]
pub fn get_friction(&self) -> f32 {
self.friction
}
#[wasm_bindgen]
pub fn set_restitution(&mut self, restitution: f32) {
self.restitution = restitution;
}
#[wasm_bindgen]
pub fn get_restitution(&self) -> f32 {
self.restitution
}
#[wasm_bindgen]
pub fn set_is_sensor(&mut self, is_sensor: bool) {
self.is_sensor = is_sensor;
}
#[wasm_bindgen]
pub fn is_sensor(&self) -> bool {
self.is_sensor
}
}
图形渲染系统
// src/renderer.rs - 渲染系统
use wasm_bindgen::prelude::*;
use web_sys::*;
use std::collections::HashMap;
#[wasm_bindgen]
pub struct Sprite {
texture_id: String,
width: f32,
height: f32,
source_x: f32,
source_y: f32,
source_width: f32,
source_height: f32,
}
#[wasm_bindgen]
impl Sprite {
#[wasm_bindgen(constructor)]
pub fn new(texture_id: &str, width: f32, height: f32) -> Sprite {
Sprite {
texture_id: texture_id.to_string(),
width,
height,
source_x: 0.0,
source_y: 0.0,
source_width: width,
source_height: height,
}
}
#[wasm_bindgen]
pub fn set_source_rect(&mut self, x: f32, y: f32, width: f32, height: f32) {
self.source_x = x;
self.source_y = y;
self.source_width = width;
self.source_height = height;
}
#[wasm_bindgen]
pub fn get_texture_id(&self) -> String {
self.texture_id.clone()
}
#[wasm_bindgen]
pub fn get_width(&self) -> f32 {
self.width
}
#[wasm_bindgen]
pub fn get_height(&self) -> f32 {
self.height
}
}
#[wasm_bindgen]
pub struct Renderer {
canvas: HtmlCanvasElement,
context: CanvasRenderingContext2d,
textures: HashMap<String, HtmlImageElement>,
camera_x: f32,
camera_y: f32,
canvas_width: f32,
canvas_height: f32,
}
#[wasm_bindgen]
impl Renderer {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<Renderer, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::<HtmlCanvasElement>()?;
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
let canvas_width = canvas.width() as f32;
let canvas_height = canvas.height() as f32;
Ok(Renderer {
canvas,
context,
textures: HashMap::new(),
camera_x: 0.0,
camera_y: 0.0,
canvas_width,
canvas_height,
})
}
#[wasm_bindgen]
pub fn load_texture(&mut self, texture_id: &str, image_url: &str) -> js_sys::Promise {
let texture_id = texture_id.to_string();
js_sys::Promise::new(&mut |resolve, reject| {
let image = HtmlImageElement::new().unwrap();
let image_clone = image.clone();
let texture_id_clone = texture_id.clone();
let onload = Closure::wrap(Box::new(move |_: Event| {
// 这里需要访问self,但在闭包中比较复杂
// 实际实现中需要更好的设计
resolve.call1(&JsValue::NULL, &JsValue::from_str("loaded")).unwrap();
}) as Box<dyn FnMut(Event)>);
let onerror = Closure::wrap(Box::new(move |_: Event| {
reject.call1(&JsValue::NULL, &JsValue::from_str("Failed to load image")).unwrap();
}) as Box<dyn FnMut(Event)>);
image.set_onload(Some(onload.as_ref().unchecked_ref()));
image.set_onerror(Some(onerror.as_ref().unchecked_ref()));
image.set_src(image_url);
onload.forget();
onerror.forget();
})
}
#[wasm_bindgen]
pub fn clear(&self, r: f32, g: f32, b: f32, a: f32) {
let color = format!("rgba({}, {}, {}, {})",
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
a
);
self.context.set_fill_style(&color.into());
self.context.fill_rect(0.0, 0.0, self.canvas_width as f64, self.canvas_height as f64);
}
#[wasm_bindgen]
pub fn set_camera(&mut self, x: f32, y: f32) {
self.camera_x = x;
self.camera_y = y;
}
#[wasm_bindgen]
pub fn draw_sprite(&self, sprite: &Sprite, x: f32, y: f32, rotation: f32, scale_x: f32, scale_y: f32) -> Result<(), JsValue> {
// 转换世界坐标到屏幕坐标
let screen_x = x - self.camera_x + self.canvas_width / 2.0;
let screen_y = y - self.camera_y + self.canvas_height / 2.0;
self.context.save();
// 应用变换
self.context.translate(screen_x as f64, screen_y as f64)?;
self.context.rotate(rotation as f64)?;
self.context.scale(scale_x as f64, scale_y as f64)?;
// 绘制精灵 (这里简化为矩形)
self.context.set_fill_style(&"#ff0000".into());
self.context.fill_rect(
-(sprite.width / 2.0) as f64,
-(sprite.height / 2.0) as f64,
sprite.width as f64,
sprite.height as f64
);
self.context.restore();
Ok(())
}
#[wasm_bindgen]
pub fn draw_rectangle(&self, x: f32, y: f32, width: f32, height: f32, color: &str) {
let screen_x = x - self.camera_x + self.canvas_width / 2.0;
let screen_y = y - self.camera_y + self.canvas_height / 2.0;
self.context.set_fill_style(&color.into());
self.context.fill_rect(
screen_x as f64,
screen_y as f64,
width as f64,
height as f64
);
}
#[wasm_bindgen]
pub fn draw_circle(&self, x: f32, y: f32, radius: f32, color: &str) {
let screen_x = x - self.camera_x + self.canvas_width / 2.0;
let screen_y = y - self.camera_y + self.canvas_height / 2.0;
self.context.begin_path();
self.context.arc(
screen_x as f64,
screen_y as f64,
radius as f64,
0.0,
2.0 * std::f64::consts::PI
).unwrap();
self.context.set_fill_style(&color.into());
self.context.fill();
}
#[wasm_bindgen]
pub fn draw_line(&self, x1: f32, y1: f32, x2: f32, y2: f32, color: &str, width: f32) {
let screen_x1 = x1 - self.camera_x + self.canvas_width / 2.0;
let screen_y1 = y1 - self.camera_y + self.canvas_height / 2.0;
let screen_x2 = x2 - self.camera_x + self.canvas_width / 2.0;
let screen_y2 = y2 - self.camera_y + self.canvas_height / 2.0;
self.context.begin_path();
self.context.move_to(screen_x1 as f64, screen_y1 as f64);
self.context.line_to(screen_x2 as f64, screen_y2 as f64);
self.context.set_stroke_style(&color.into());
self.context.set_line_width(width as f64);
self.context.stroke();
}
#[wasm_bindgen]
pub fn draw_text(&self, text: &str, x: f32, y: f32, font: &str, color: &str) {
let screen_x = x - self.camera_x + self.canvas_width / 2.0;
let screen_y = y - self.camera_y + self.canvas_height / 2.0;
self.context.set_font(font);
self.context.set_fill_style(&color.into());
self.context.fill_text(text, screen_x as f64, screen_y as f64).unwrap();
}
}
音频系统
// src/audio.rs - 音频系统
use wasm_bindgen::prelude::*;
use web_sys::*;
use std::collections::HashMap;
#[wasm_bindgen]
pub struct AudioManager {
context: AudioContext,
buffers: HashMap<String, AudioBuffer>,
master_gain: GainNode,
}
#[wasm_bindgen]
impl AudioManager {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<AudioManager, JsValue> {
let context = AudioContext::new()?;
let master_gain = context.create_gain()?;
master_gain.connect_with_audio_node(&context.destination())?;
Ok(AudioManager {
context,
buffers: HashMap::new(),
master_gain,
})
}
#[wasm_bindgen]
pub async fn load_audio(&mut self, audio_id: &str, url: &str) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let response = wasm_bindgen_futures::JsFuture::from(window.fetch_with_str(url)).await?;
let response: Response = response.dyn_into()?;
let array_buffer = wasm_bindgen_futures::JsFuture::from(response.array_buffer()?).await?;
let audio_buffer = wasm_bindgen_futures::JsFuture::from(
self.context.decode_audio_data(&array_buffer.dyn_into()?)?
).await?;
self.buffers.insert(audio_id.to_string(), audio_buffer.dyn_into()?);
Ok(())
}
#[wasm_bindgen]
pub fn play_sound(&self, audio_id: &str, volume: f32, pitch: f32) -> Result<(), JsValue> {
if let Some(buffer) = self.buffers.get(audio_id) {
let source = self.context.create_buffer_source()?;
let gain = self.context.create_gain()?;
source.set_buffer(Some(buffer));
source.set_playback_rate(pitch);
gain.gain().set_value(volume);
source.connect_with_audio_node(&gain)?;
gain.connect_with_audio_node(&self.master_gain)?;
source.start()?;
}
Ok(())
}
#[wasm_bindgen]
pub fn set_master_volume(&self, volume: f32) -> Result<(), JsValue> {
self.master_gain.gain().set_value(volume);
Ok(())
}
}
资源管理系统
// src/resources.rs - 资源管理
use wasm_bindgen::prelude::*;
use std::collections::HashMap;
#[wasm_bindgen]
pub struct ResourceManager {
textures: HashMap<String, String>, // texture_id -> url
audio_files: HashMap<String, String>, // audio_id -> url
loaded_count: usize,
total_count: usize,
loading_complete: bool,
}
#[wasm_bindgen]
impl ResourceManager {
#[wasm_bindgen(constructor)]
pub fn new() -> ResourceManager {
ResourceManager {
textures: HashMap::new(),
audio_files: HashMap::new(),
loaded_count: 0,
total_count: 0,
loading_complete: false,
}
}
#[wasm_bindgen]
pub fn add_texture(&mut self, texture_id: &str, url: &str) {
self.textures.insert(texture_id.to_string(), url.to_string());
self.total_count += 1;
}
#[wasm_bindgen]
pub fn add_audio(&mut self, audio_id: &str, url: &str) {
self.audio_files.insert(audio_id.to_string(), url.to_string());
self.total_count += 1;
}
#[wasm_bindgen]
pub fn get_loading_progress(&self) -> f32 {
if self.total_count == 0 {
1.0
} else {
self.loaded_count as f32 / self.total_count as f32
}
}
#[wasm_bindgen]
pub fn is_loading_complete(&self) -> bool {
self.loading_complete
}
#[wasm_bindgen]
pub fn mark_resource_loaded(&mut self) {
self.loaded_count += 1;
if self.loaded_count >= self.total_count {
self.loading_complete = true;
}
}
#[wasm_bindgen]
pub fn get_texture_urls(&self) -> js_sys::Array {
let array = js_sys::Array::new();
for (id, url) in &self.textures {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"id".into(), &id.clone().into()).unwrap();
js_sys::Reflect::set(&obj, &"url".into(), &url.clone().into()).unwrap();
array.push(&obj);
}
array
}
#[wasm_bindgen]
pub fn get_audio_urls(&self) -> js_sys::Array {
let array = js_sys::Array::new();
for (id, url) in &self.audio_files {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"id".into(), &id.clone().into()).unwrap();
js_sys::Reflect::set(&obj, &"url".into(), &url.clone().into()).unwrap();
array.push(&obj);
}
array
}
}
完整游戏示例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust-Wasm Game Demo</title>
<style>
body {
margin: 0;
padding: 0;
background: #000;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: Arial, sans-serif;
}
.game-container {
text-align: center;
}
canvas {
border: 2px solid #333;
background: #222;
}
.game-ui {
color: white;
margin-top: 10px;
}
.controls {
margin-top: 20px;
color: #ccc;
}
.loading {
color: white;
font-size: 18px;
}
.game-stats {
display: flex;
justify-content: space-between;
margin-top: 10px;
color: white;
}
button {
padding: 10px 20px;
margin: 5px;
background: #444;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background: #666;
}
</style>
</head>
<body>
<div class="game-container">
<div id="loading-screen" class="loading">
<h2>Loading Game...</h2>
<div id="loading-progress">0%</div>
</div>
<div id="game-screen" style="display: none;">
<canvas id="game-canvas" width="800" height="600"></canvas>
<div class="game-stats">
<div>Score: <span id="score">0</span></div>
<div>Lives: <span id="lives">3</span></div>
<div>FPS: <span id="fps">60</span></div>
</div>
<div class="game-ui">
<button onclick="startGame()">Start Game</button>
<button onclick="pauseGame()">Pause</button>
<button onclick="resetGame()">Reset</button>
</div>
<div class="controls">
<p>Controls: WASD or Arrow Keys to move, Space to shoot</p>
</div>
</div>
</div>
<script type="module">
import init, {
GameEngine,
PhysicsWorld,
Renderer,
AudioManager,
ResourceManager,
GameObject,
Transform,
Sprite
} from './pkg/rust_wasm_game_engine.js';
let gameEngine, physicsWorld, renderer, audioManager, resourceManager;
let gameState = {
running: false,
paused: false,
score: 0,
lives: 3,
player: null,
enemies: [],
bullets: [],
lastTime: 0,
frameCount: 0,
fpsTime: 0
};
async function loadGame() {
await init();
// 初始化系统
gameEngine = new GameEngine(800, 600);
physicsWorld = new PhysicsWorld();
renderer = new Renderer('game-canvas');
audioManager = new AudioManager();
resourceManager = new ResourceManager();
// 设置物理世界
physicsWorld.set_gravity(0, 0); // 太空游戏,无重力
// 加载资源
await loadResources();
// 设置事件监听
setupEventListeners();
// 隐藏加载屏幕,显示游戏
document.getElementById('loading-screen').style.display = 'none';
document.getElementById('game-screen').style.display = 'block';
console.log('Game loaded successfully!');
}
async function loadResources() {
// 添加资源到管理器
resourceManager.add_texture('player', 'assets/player.png');
resourceManager.add_texture('enemy', 'assets/enemy.png');
resourceManager.add_texture('bullet', 'assets/bullet.png');
resourceManager.add_audio('shoot', 'assets/shoot.wav');
resourceManager.add_audio('explosion', 'assets/explosion.wav');
// 模拟加载过程
const totalResources = 5;
for (let i = 0; i < totalResources; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
resourceManager.mark_resource_loaded();
const progress = resourceManager.get_loading_progress();
document.getElementById('loading-progress').textContent =
Math.round(progress * 100) + '%';
}
}
function setupEventListeners() {
// 键盘事件
document.addEventListener('keydown', (e) => {
gameEngine.handle_key_down(e.code);
e.preventDefault();
});
document.addEventListener('keyup', (e) => {
gameEngine.handle_key_up(e.code);
e.preventDefault();
});
// 鼠标事件
const canvas = document.getElementById('game-canvas');
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
gameEngine.handle_mouse_move(x, y);
});
canvas.addEventListener('mousedown', (e) => {
gameEngine.handle_mouse_down(e.button);
});
canvas.addEventListener('mouseup', (e) => {
gameEngine.handle_mouse_up(e.button);
});
}
window.startGame = function() {
if (!gameState.running) {
initializeGame();
gameState.running = true;
gameState.paused = false;
gameEngine.start();
gameLoop();
}
};
window.pauseGame = function() {
gameState.paused = !gameState.paused;
};
window.resetGame = function() {
gameState.running = false;
gameState.paused = false;
gameState.score = 0;
gameState.lives = 3;
gameState.enemies = [];
gameState.bullets = [];
// 清理游戏对象
if (gameState.player) {
gameEngine.remove_object(gameState.player);
gameState.player = null;
}
updateUI();
};
function initializeGame() {
// 创建玩家
gameState.player = gameEngine.create_object(400, 500);
let player = gameEngine.get_object(gameState.player);
if (player) {
player.set_tag('player');
gameEngine.update_object(gameState.player, player);
}
// 创建物理体
const playerBody = physicsWorld.create_dynamic_body(gameState.player, 400, 500);
physicsWorld.add_box_collider(playerBody, 40, 40);
// 生成初始敌人
spawnEnemies(5);
updateUI();
}
function spawnEnemies(count) {
for (let i = 0; i < count; i++) {
const x = Math.random() * 800;
const y = Math.random() * 200;
const enemyId = gameEngine.create_object(x, y);
let enemy = gameEngine.get_object(enemyId);
if (enemy) {
enemy.set_tag('enemy');
enemy.set_velocity(
(Math.random() - 0.5) * 100,
Math.random() * 50 + 25
);
gameEngine.update_object(enemyId, enemy);
}
const enemyBody = physicsWorld.create_dynamic_body(enemyId, x, y);
physicsWorld.add_box_collider(enemyBody, 30, 30);
gameState.enemies.push(enemyId);
}
}
function gameLoop() {
if (!gameState.running) return;
const currentTime = performance.now();
const deltaTime = gameState.lastTime ? (currentTime - gameState.lastTime) / 1000 : 0;
gameState.lastTime = currentTime;
if (!gameState.paused) {
update(deltaTime);
render();
}
updateFPS(deltaTime);
requestAnimationFrame(gameLoop);
}
function update(deltaTime) {
// 更新游戏引擎
gameEngine.update(currentTime);
// 更新物理世界
physicsWorld.step(deltaTime);
// 处理输入
handleInput();
// 更新游戏逻辑
updatePlayer(deltaTime);
updateEnemies(deltaTime);
updateBullets(deltaTime);
// 检查碰撞
checkCollisions();
// 清理无效对象
cleanup();
}
function handleInput() {
if (!gameState.player) return;
let player = gameEngine.get_object(gameState.player);
if (!player) return;
const speed = 300;
let vx = 0, vy = 0;
if (gameEngine.is_key_pressed('KeyW') || gameEngine.is_key_pressed('ArrowUp')) {
vy = -speed;
}
if (gameEngine.is_key_pressed('KeyS') || gameEngine.is_key_pressed('ArrowDown')) {
vy = speed;
}
if (gameEngine.is_key_pressed('KeyA') || gameEngine.is_key_pressed('ArrowLeft')) {
vx = -speed;
}
if (gameEngine.is_key_pressed('KeyD') || gameEngine.is_key_pressed('ArrowRight')) {
vx = speed;
}
player.set_velocity(vx, vy);
gameEngine.update_object(gameState.player, player);
// 射击
if (gameEngine.is_key_just_pressed('Space')) {
shoot();
}
}
function shoot() {
if (!gameState.player) return;
let player = gameEngine.get_object(gameState.player);
if (!player) return;
const transform = player.get_transform();
const bulletId = gameEngine.create_object(transform.x, transform.y - 20);
let bullet = gameEngine.get_object(bulletId);
if (bullet) {
bullet.set_tag('bullet');
bullet.set_velocity(0, -500);
gameEngine.update_object(bulletId, bullet);
}
const bulletBody = physicsWorld.create_dynamic_body(bulletId, transform.x, transform.y - 20);
physicsWorld.add_circle_collider(bulletBody, 5);
gameState.bullets.push(bulletId);
// 播放射击音效
audioManager.play_sound('shoot', 0.5, 1.0);
}
function updatePlayer(deltaTime) {
if (!gameState.player) return;
let player = gameEngine.get_object(gameState.player);
if (!player) return;
const transform = player.get_transform();
// 限制玩家在屏幕内
if (transform.x < 20) transform.set_x(20);
if (transform.x > 780) transform.set_x(780);
if (transform.y < 20) transform.set_y(20);
if (transform.y > 580) transform.set_y(580);
player.set_transform(transform);
gameEngine.update_object(gameState.player, player);
}
function updateEnemies(deltaTime) {
for (const enemyId of gameState.enemies) {
let enemy = gameEngine.get_object(enemyId);
if (!enemy) continue;
const transform = enemy.get_transform();
// 敌人到达屏幕底部时重新生成
if (transform.y > 650) {
transform.set_x(Math.random() * 800);
transform.set_y(-50);
enemy.set_transform(transform);
gameEngine.update_object(enemyId, enemy);
}
}
}
function updateBullets(deltaTime) {
gameState.bullets = gameState.bullets.filter(bulletId => {
let bullet = gameEngine.get_object(bulletId);
if (!bullet) return false;
const transform = bullet.get_transform();
// 移除屏幕外的子弹
if (transform.y < -50) {
gameEngine.remove_object(bulletId);
return false;
}
return true;
});
}
function checkCollisions() {
// 简化的碰撞检测
for (const bulletId of gameState.bullets) {
let bullet = gameEngine.get_object(bulletId);
if (!bullet) continue;
const bulletTransform = bullet.get_transform();
for (let i = gameState.enemies.length - 1; i >= 0; i--) {
const enemyId = gameState.enemies[i];
let enemy = gameEngine.get_object(enemyId);
if (!enemy) continue;
const enemyTransform = enemy.get_transform();
const distance = Math.sqrt(
Math.pow(bulletTransform.x - enemyTransform.x, 2) +
Math.pow(bulletTransform.y - enemyTransform.y, 2)
);
if (distance < 25) {
// 碰撞发生
gameEngine.remove_object(bulletId);
gameEngine.remove_object(enemyId);
gameState.bullets = gameState.bullets.filter(id => id !== bulletId);
gameState.enemies.splice(i, 1);
gameState.score += 100;
audioManager.play_sound('explosion', 0.7, 1.0);
// 生成新敌人
if (gameState.enemies.length < 3) {
spawnEnemies(1);
}
break;
}
}
}
}
function cleanup() {
// 清理无效的游戏对象
gameState.enemies = gameState.enemies.filter(id => {
return gameEngine.get_object(id) !== null;
});
gameState.bullets = gameState.bullets.filter(id => {
return gameEngine.get_object(id) !== null;
});
}
function render() {
// 清空画布
renderer.clear(0.1, 0.1, 0.2, 1.0);
// 渲染玩家
if (gameState.player) {
let player = gameEngine.get_object(gameState.player);
if (player) {
const transform = player.get_transform();
renderer.draw_rectangle(
transform.x - 20, transform.y - 20,
40, 40, '#00ff00'
);
}
}
// 渲染敌人
for (const enemyId of gameState.enemies) {
let enemy = gameEngine.get_object(enemyId);
if (enemy) {
const transform = enemy.get_transform();
renderer.draw_rectangle(
transform.x - 15, transform.y - 15,
30, 30, '#ff0000'
);
}
}
// 渲染子弹
for (const bulletId of gameState.bullets) {
let bullet = gameEngine.get_object(bulletId);
if (bullet) {
const transform = bullet.get_transform();
renderer.draw_circle(
transform.x, transform.y,
5, '#ffff00'
);
}
}
}
function updateFPS(deltaTime) {
gameState.frameCount++;
gameState.fpsTime += deltaTime;
if (gameState.fpsTime >= 1.0) {
const fps = Math.round(gameState.frameCount / gameState.fpsTime);
document.getElementById('fps').textContent = fps;
gameState.frameCount = 0;
gameState.fpsTime = 0;
}
}
function updateUI() {
document.getElementById('score').textContent = gameState.score;
document.getElementById('lives').textContent = gameState.lives;
}
// 启动游戏
loadGame();
</script>
</body>
</html>
总结
Rust-Wasm在Web游戏开发中的核心要点:
🎯 游戏引擎架构
- 组件系统:灵活的游戏对象和组件管理
- 场景管理:高效的游戏世界组织和更新
- 输入处理:完整的键盘、鼠标和触摸支持
- 时间管理:精确的帧率控制和时间计算
✅ 物理模拟
- Rapier2D物理引擎集成
- 刚体和碰撞体管理
- 力学模拟和碰撞检测
- 射线投射和查询系统
🚀 图形渲染
- Canvas 2D渲染系统
- 精灵和纹理管理
- 摄像机和视口控制
- 基础图形绘制功能
🎵 音频系统
- Web Audio API集成
- 音效播放和管理
- 音量控制和音调调节
- 异步音频资源加载
📦 资源管理
- 统一的资源加载系统
- 加载进度跟踪
- 纹理和音频资源管理
- 异步加载和缓存
💡 性能优化
- WebAssembly高性能计算
- 内存管理和对象池
- 批量渲染和剔除
- 帧率监控和优化
🎮 完整游戏示例
- 太空射击游戏演示
- 完整的游戏循环
- 碰撞检测和游戏逻辑
- UI界面和用户交互
掌握Rust-Wasm游戏开发,创造出色的浏览器游戏体验!
Web游戏开发的未来在于高性能和跨平台,Rust与WebAssembly的结合为开发者提供了实现这一目标的强大工具。