Rust - 向 webassembly 游戏添加事件监听器
Rust - adding event listeners to a webassembly game
我正在尝试在 Web 程序集中创建游戏。我选择用 rust 准备它并使用 cargo-web 编译它。我设法获得了一个有效的游戏循环,但由于生锈借用机制,我在添加 MouseDownEvent 侦听器时遇到了问题。我非常愿意编写 "safe" 代码(不使用 "unsafe" 关键字)
此时游戏只是简单地将一个红色方框从 (0,0) 移动到 (700,500),速度取决于距离。我希望下一步使用用户点击更新目的地。
这是游戏的简化代码。
static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Game!</title>
</head>
<body>
<canvas id="canvas" width="600" height="600">
<script src="game.js"></script>
</body>
</html>
src/main.rs
mod game;
use game::Game;
use stdweb::console;
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::CanvasRenderingContext2d;
use stdweb::web::html_element::CanvasElement;
use stdweb::web::event::MouseDownEvent;
fn main()
{
let canvas: CanvasElement = document()
.query_selector("#canvas")
.unwrap()
.unwrap()
.try_into()
.unwrap();
canvas.set_width(800u32);
canvas.set_height(600u32);
let context = canvas.get_context().unwrap();
let game: Game = Game::new();
// canvas.add_event_listener(|event: MouseDownEvent|
// {
// game.destination.x = (event.client_x() as f64);
// game.destination.y = (event.client_y() as f64);
// });
game_loop(game, context, 0f64);
}
fn game_loop(mut game : Game, context : CanvasRenderingContext2d, timestamp : f64)
{
game.cycle(timestamp);
draw(&game,&context);
stdweb::web::window().request_animation_frame( |time : f64| { game_loop(game, context, time); } );
}
fn draw(game : &Game, context: &CanvasRenderingContext2d)
{
context.clear_rect(0f64,0f64,800f64,800f64);
context.set_fill_style_color("red");
context.fill_rect(game.location.x, game.location.y, 5f64, 5f64);
}
src/game.rs
pub struct Point
{
pub x: f64,
pub y: f64,
}
pub struct Game
{
pub time: f64,
pub location: Point,
pub destination: Point,
}
impl Game
{
pub fn new() -> Game
{
let game = Game
{
time: 0f64,
location: Point{x: 0f64, y: 0f64},
destination: Point{x: 700f64, y: 500f64},
};
return game;
}
pub fn cycle(&mut self, timestamp : f64)
{
if timestamp - self.time > 10f64
{
self.location.x += (self.destination.x - self.location.x) / 10f64;
self.location.y += (self.destination.y - self.location.y) / 10f64;
self.time = timestamp;
}
}
}
main.rs 中注释掉的部分是我尝试添加 MouseDownEvent 侦听器。不幸的是它产生了一个编译错误:
error[E0505]: cannot move out of `game` because it is borrowed
--> src\main.rs:37:15
|
31 | canvas.add_event_listener(|event: MouseDownEvent|
| - ----------------------- borrow of `game` occurs here
| _____|
| |
32 | | {
33 | | game.destination.x = (event.client_x() as f64);
| | ---- borrow occurs due to use in closure
34 | | game.destination.y = (event.client_y() as f64);
35 | | });
| |______- argument requires that `game` is borrowed for `'static`
36 |
37 | game_loop(game, context, 0f64);
| ^^^^ move out of `game` occurs here
我非常想知道如何正确实现一种将用户输入读取到游戏中的方法。它不需要是异步的。
在您的示例中,game_loop
拥有 game
,因为它已移入循环。因此,任何应该改变游戏规则的事情都需要在 game_loop
内部发生。为了适应事件处理,您有多种选择:
选项 1
让 game_loop
轮询事件。
您创建了一个事件队列,您的 game_loop
将有一些逻辑来获取第一个事件并处理它。
您将不得不在这里处理同步,所以我建议您阅读 Mutex and Concurrency 的一般内容。但是一旦你掌握了它,这应该是一个相当容易的任务。您的循环获得一个引用,每个事件处理程序获得一个,都尝试解锁互斥锁,然后访问队列(可能是向量)。
这将使您的 game_loop
成为所有这些的唯一真理,这是一种流行的引擎设计,因为它易于推理和着手。
但也许你想要不那么集中。
选项 2
让事件在循环外发生
这个想法将是一个更大的重构。你会把你的 Game
放在一个 lazy_static
中,周围有一个 Mutex。
每次调用 game_loop
它都会尝试获取所述 Mutex 的锁,然后执行游戏计算。
当输入事件发生时,该事件也会尝试获取 Game
上的互斥锁。这意味着当 game_loop
正在处理时,不会处理任何输入事件,但它们会尝试进入滴答之间。
这里的一个挑战是保持输入顺序并确保输入处理得足够快。要完全正确,这可能是一个更大的挑战。但是设计会给你一些可能性。
这个想法的充实版本是 Amethyst
,它是大规模并行的,并且有利于简洁的设计。但他们在引擎背后采用了更为复杂的设计。
我认为在这种情况下,编译器错误消息非常清楚。您试图在 'static
生命周期内借用闭包中的 game
,然后您还试图移动 game
。这是不允许的。我建议再次阅读 The Rust Programming Language 这本书。关注第 4 章 - 理解所有权。
为了让它更短,您的问题可以归结为 - 如何共享状态,可以改变状态。有很多方法可以实现这个目标,但这实际上取决于您的需要(单线程或多线程等)。我将使用 Rc
& RefCell
来解决这个问题。
Rc
(std::rc):
The type Rc<T>
provides shared ownership of a value of type T
, allocated in the heap. Invoking clone
on Rc
produces a new pointer to the same value in the heap. When the last Rc
pointer to a given value is destroyed, the pointed-to value is also destroyed.
RefCell
(std::cell):
Values of the Cell<T>
and RefCell<T>
types may be mutated through shared references (i.e. the common &T
type), whereas most Rust types can only be mutated through unique (&mut T
) references. We say that Cell<T>
and RefCell<T>
provide 'interior mutability', in contrast with typical Rust types that exhibit 'inherited mutability'.
这是我对你的结构所做的:
struct Inner {
time: f64,
location: Point,
destination: Point,
}
#[derive(Clone)]
pub struct Game {
inner: Rc<RefCell<Inner>>,
}
这是什么意思? Inner
保存游戏状态(与旧 Game
相同的字段)。新 Game
只有一个字段 inner
,其中包含共享状态。
Rc<T>
(在这种情况下,T
是 RefCell<Inner
>)- 允许我多次克隆 inner
,但它不会克隆 T
RefCell<T>
(T
在这种情况下是 Inner
)- 允许我不可变或可变地借用 T
,检查在运行时完成
我现在可以多次克隆 Game
结构,它不会克隆 RefCell<Inner>
,只会克隆 Game
和 Rc
。 enclose!
宏在更新后的 main.rs
:
中的作用
let game: Game = Game::default();
canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
game.set_destination(event);
}));
game_loop(game, context, 0.);
没有 enclose!
宏:
let game: Game = Game::default();
// game_for_mouse_down_event_closure holds the reference to the
// same `RefCell<Inner>` as the initial `game`
let game_for_mouse_down_event_closure = game.clone();
canvas.add_event_listener(move |event: MouseDownEvent| {
game_for_mouse_down_event_closure.set_destination(event);
});
game_loop(game, context, 0.);
已更新game.rs
:
use std::{cell::RefCell, rc::Rc};
use stdweb::traits::IMouseEvent;
use stdweb::web::event::MouseDownEvent;
#[derive(Clone, Copy)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl From<MouseDownEvent> for Point {
fn from(e: MouseDownEvent) -> Self {
Self {
x: e.client_x() as f64,
y: e.client_y() as f64,
}
}
}
struct Inner {
time: f64,
location: Point,
destination: Point,
}
impl Default for Inner {
fn default() -> Self {
Inner {
time: 0.,
location: Point { x: 0., y: 0. },
destination: Point { x: 700., y: 500. },
}
}
}
#[derive(Clone)]
pub struct Game {
inner: Rc<RefCell<Inner>>,
}
impl Default for Game {
fn default() -> Self {
Game {
inner: Rc::new(RefCell::new(Inner::default())),
}
}
}
impl Game {
pub fn update(&self, timestamp: f64) {
let mut inner = self.inner.borrow_mut();
if timestamp - inner.time > 10f64 {
inner.location.x += (inner.destination.x - inner.location.x) / 10f64;
inner.location.y += (inner.destination.y - inner.location.y) / 10f64;
inner.time = timestamp;
}
}
pub fn set_destination<T: Into<Point>>(&self, location: T) {
let mut inner = self.inner.borrow_mut();
inner.destination = location.into();
}
pub fn location(&self) -> Point {
self.inner.borrow().location
}
}
已更新main.rs
:
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::event::MouseDownEvent;
use stdweb::web::html_element::CanvasElement;
use stdweb::web::CanvasRenderingContext2d;
use game::Game;
mod game;
// https://github.com/koute/stdweb/blob/master/examples/todomvc/src/main.rs#L31-L39
macro_rules! enclose {
( ($( $x:ident ),*) $y:expr ) => {
{
$(let $x = $x.clone();)*
$y
}
};
}
fn game_loop(game: Game, context: CanvasRenderingContext2d, timestamp: f64) {
game.update(timestamp);
draw(&game, &context);
stdweb::web::window().request_animation_frame(|time: f64| {
game_loop(game, context, time);
});
}
fn draw(game: &Game, context: &CanvasRenderingContext2d) {
context.clear_rect(0., 0., 800., 800.);
context.set_fill_style_color("red");
let location = game.location();
context.fill_rect(location.x, location.y, 5., 5.);
}
fn main() {
let canvas: CanvasElement = document()
.query_selector("#canvas")
.unwrap()
.unwrap()
.try_into()
.unwrap();
canvas.set_width(800);
canvas.set_height(600);
let context = canvas.get_context().unwrap();
let game: Game = Game::default();
canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
game.set_destination(event);
}));
game_loop(game, context, 0.);
}
P.S。请在以后共享任何代码之前,安装并使用 rustfmt.
我正在尝试在 Web 程序集中创建游戏。我选择用 rust 准备它并使用 cargo-web 编译它。我设法获得了一个有效的游戏循环,但由于生锈借用机制,我在添加 MouseDownEvent 侦听器时遇到了问题。我非常愿意编写 "safe" 代码(不使用 "unsafe" 关键字)
此时游戏只是简单地将一个红色方框从 (0,0) 移动到 (700,500),速度取决于距离。我希望下一步使用用户点击更新目的地。
这是游戏的简化代码。
static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Game!</title>
</head>
<body>
<canvas id="canvas" width="600" height="600">
<script src="game.js"></script>
</body>
</html>
src/main.rs
mod game;
use game::Game;
use stdweb::console;
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::CanvasRenderingContext2d;
use stdweb::web::html_element::CanvasElement;
use stdweb::web::event::MouseDownEvent;
fn main()
{
let canvas: CanvasElement = document()
.query_selector("#canvas")
.unwrap()
.unwrap()
.try_into()
.unwrap();
canvas.set_width(800u32);
canvas.set_height(600u32);
let context = canvas.get_context().unwrap();
let game: Game = Game::new();
// canvas.add_event_listener(|event: MouseDownEvent|
// {
// game.destination.x = (event.client_x() as f64);
// game.destination.y = (event.client_y() as f64);
// });
game_loop(game, context, 0f64);
}
fn game_loop(mut game : Game, context : CanvasRenderingContext2d, timestamp : f64)
{
game.cycle(timestamp);
draw(&game,&context);
stdweb::web::window().request_animation_frame( |time : f64| { game_loop(game, context, time); } );
}
fn draw(game : &Game, context: &CanvasRenderingContext2d)
{
context.clear_rect(0f64,0f64,800f64,800f64);
context.set_fill_style_color("red");
context.fill_rect(game.location.x, game.location.y, 5f64, 5f64);
}
src/game.rs
pub struct Point
{
pub x: f64,
pub y: f64,
}
pub struct Game
{
pub time: f64,
pub location: Point,
pub destination: Point,
}
impl Game
{
pub fn new() -> Game
{
let game = Game
{
time: 0f64,
location: Point{x: 0f64, y: 0f64},
destination: Point{x: 700f64, y: 500f64},
};
return game;
}
pub fn cycle(&mut self, timestamp : f64)
{
if timestamp - self.time > 10f64
{
self.location.x += (self.destination.x - self.location.x) / 10f64;
self.location.y += (self.destination.y - self.location.y) / 10f64;
self.time = timestamp;
}
}
}
main.rs 中注释掉的部分是我尝试添加 MouseDownEvent 侦听器。不幸的是它产生了一个编译错误:
error[E0505]: cannot move out of `game` because it is borrowed
--> src\main.rs:37:15
|
31 | canvas.add_event_listener(|event: MouseDownEvent|
| - ----------------------- borrow of `game` occurs here
| _____|
| |
32 | | {
33 | | game.destination.x = (event.client_x() as f64);
| | ---- borrow occurs due to use in closure
34 | | game.destination.y = (event.client_y() as f64);
35 | | });
| |______- argument requires that `game` is borrowed for `'static`
36 |
37 | game_loop(game, context, 0f64);
| ^^^^ move out of `game` occurs here
我非常想知道如何正确实现一种将用户输入读取到游戏中的方法。它不需要是异步的。
在您的示例中,game_loop
拥有 game
,因为它已移入循环。因此,任何应该改变游戏规则的事情都需要在 game_loop
内部发生。为了适应事件处理,您有多种选择:
选项 1
让 game_loop
轮询事件。
您创建了一个事件队列,您的 game_loop
将有一些逻辑来获取第一个事件并处理它。
您将不得不在这里处理同步,所以我建议您阅读 Mutex and Concurrency 的一般内容。但是一旦你掌握了它,这应该是一个相当容易的任务。您的循环获得一个引用,每个事件处理程序获得一个,都尝试解锁互斥锁,然后访问队列(可能是向量)。
这将使您的 game_loop
成为所有这些的唯一真理,这是一种流行的引擎设计,因为它易于推理和着手。
但也许你想要不那么集中。
选项 2
让事件在循环外发生
这个想法将是一个更大的重构。你会把你的 Game
放在一个 lazy_static
中,周围有一个 Mutex。
每次调用 game_loop
它都会尝试获取所述 Mutex 的锁,然后执行游戏计算。
当输入事件发生时,该事件也会尝试获取 Game
上的互斥锁。这意味着当 game_loop
正在处理时,不会处理任何输入事件,但它们会尝试进入滴答之间。
这里的一个挑战是保持输入顺序并确保输入处理得足够快。要完全正确,这可能是一个更大的挑战。但是设计会给你一些可能性。
这个想法的充实版本是 Amethyst
,它是大规模并行的,并且有利于简洁的设计。但他们在引擎背后采用了更为复杂的设计。
我认为在这种情况下,编译器错误消息非常清楚。您试图在 'static
生命周期内借用闭包中的 game
,然后您还试图移动 game
。这是不允许的。我建议再次阅读 The Rust Programming Language 这本书。关注第 4 章 - 理解所有权。
为了让它更短,您的问题可以归结为 - 如何共享状态,可以改变状态。有很多方法可以实现这个目标,但这实际上取决于您的需要(单线程或多线程等)。我将使用 Rc
& RefCell
来解决这个问题。
Rc
(std::rc):
The type
Rc<T>
provides shared ownership of a value of typeT
, allocated in the heap. Invokingclone
onRc
produces a new pointer to the same value in the heap. When the lastRc
pointer to a given value is destroyed, the pointed-to value is also destroyed.
RefCell
(std::cell):
Values of the
Cell<T>
andRefCell<T>
types may be mutated through shared references (i.e. the common&T
type), whereas most Rust types can only be mutated through unique (&mut T
) references. We say thatCell<T>
andRefCell<T>
provide 'interior mutability', in contrast with typical Rust types that exhibit 'inherited mutability'.
这是我对你的结构所做的:
struct Inner {
time: f64,
location: Point,
destination: Point,
}
#[derive(Clone)]
pub struct Game {
inner: Rc<RefCell<Inner>>,
}
这是什么意思? Inner
保存游戏状态(与旧 Game
相同的字段)。新 Game
只有一个字段 inner
,其中包含共享状态。
Rc<T>
(在这种情况下,T
是RefCell<Inner
>)- 允许我多次克隆inner
,但它不会克隆T
RefCell<T>
(T
在这种情况下是Inner
)- 允许我不可变或可变地借用T
,检查在运行时完成
我现在可以多次克隆 Game
结构,它不会克隆 RefCell<Inner>
,只会克隆 Game
和 Rc
。 enclose!
宏在更新后的 main.rs
:
let game: Game = Game::default();
canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
game.set_destination(event);
}));
game_loop(game, context, 0.);
没有 enclose!
宏:
let game: Game = Game::default();
// game_for_mouse_down_event_closure holds the reference to the
// same `RefCell<Inner>` as the initial `game`
let game_for_mouse_down_event_closure = game.clone();
canvas.add_event_listener(move |event: MouseDownEvent| {
game_for_mouse_down_event_closure.set_destination(event);
});
game_loop(game, context, 0.);
已更新game.rs
:
use std::{cell::RefCell, rc::Rc};
use stdweb::traits::IMouseEvent;
use stdweb::web::event::MouseDownEvent;
#[derive(Clone, Copy)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl From<MouseDownEvent> for Point {
fn from(e: MouseDownEvent) -> Self {
Self {
x: e.client_x() as f64,
y: e.client_y() as f64,
}
}
}
struct Inner {
time: f64,
location: Point,
destination: Point,
}
impl Default for Inner {
fn default() -> Self {
Inner {
time: 0.,
location: Point { x: 0., y: 0. },
destination: Point { x: 700., y: 500. },
}
}
}
#[derive(Clone)]
pub struct Game {
inner: Rc<RefCell<Inner>>,
}
impl Default for Game {
fn default() -> Self {
Game {
inner: Rc::new(RefCell::new(Inner::default())),
}
}
}
impl Game {
pub fn update(&self, timestamp: f64) {
let mut inner = self.inner.borrow_mut();
if timestamp - inner.time > 10f64 {
inner.location.x += (inner.destination.x - inner.location.x) / 10f64;
inner.location.y += (inner.destination.y - inner.location.y) / 10f64;
inner.time = timestamp;
}
}
pub fn set_destination<T: Into<Point>>(&self, location: T) {
let mut inner = self.inner.borrow_mut();
inner.destination = location.into();
}
pub fn location(&self) -> Point {
self.inner.borrow().location
}
}
已更新main.rs
:
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::event::MouseDownEvent;
use stdweb::web::html_element::CanvasElement;
use stdweb::web::CanvasRenderingContext2d;
use game::Game;
mod game;
// https://github.com/koute/stdweb/blob/master/examples/todomvc/src/main.rs#L31-L39
macro_rules! enclose {
( ($( $x:ident ),*) $y:expr ) => {
{
$(let $x = $x.clone();)*
$y
}
};
}
fn game_loop(game: Game, context: CanvasRenderingContext2d, timestamp: f64) {
game.update(timestamp);
draw(&game, &context);
stdweb::web::window().request_animation_frame(|time: f64| {
game_loop(game, context, time);
});
}
fn draw(game: &Game, context: &CanvasRenderingContext2d) {
context.clear_rect(0., 0., 800., 800.);
context.set_fill_style_color("red");
let location = game.location();
context.fill_rect(location.x, location.y, 5., 5.);
}
fn main() {
let canvas: CanvasElement = document()
.query_selector("#canvas")
.unwrap()
.unwrap()
.try_into()
.unwrap();
canvas.set_width(800);
canvas.set_height(600);
let context = canvas.get_context().unwrap();
let game: Game = Game::default();
canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
game.set_destination(event);
}));
game_loop(game, context, 0.);
}
P.S。请在以后共享任何代码之前,安装并使用 rustfmt.