【问题标题】:Rust - adding event listeners to a webassembly gameRust - 将事件侦听器添加到 WebAssembly 游戏
【发布时间】:2019-08-18 19:45:07
【问题描述】:

我正在尝试在 Web 程序集中创建游戏。我选择在 rust 中准备它并使用 cargo-web 编译它。我设法获得了一个有效的游戏循环,但由于生锈借用机制,我在添加 MouseDownEvent 侦听器时遇到了问题。我非常喜欢编写“安全”代码(不使用“不安全”关键字)

此时游戏只是将一个红色框从 (0,0) 移动到 (700,500),速度取决于距离。我希望下一步使用用户点击更新目的地。

这是游戏的简化和工作代码。

静态/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

我非常想知道如何正确实现将用户输入读取到游戏中的方法。它不需要是异步的。

【问题讨论】:

    标签: events rust webassembly game-loop


    【解决方案1】:

    我认为在这种情况下编译器错误消息非常清楚。您试图在 'static 的生命周期中借用闭包中的 game,然后您还试图移动 game。这是不允许的。我建议再次阅读The Rust Programming Language 书。关注第 4 章 - 了解所有权。

    为了简短起见,您的问题可以归结为 - 如何共享可以改变的状态。有很多方法可以实现这一目标,但这实际上取决于您的需求(单线程或多线程等)。我将使用Rc & RefCell 来解决这个问题。

    Rc (std::rc):

    Rc&lt;T&gt; 类型提供在堆中分配的T 类型值的共享所有权。在Rc 上调用clone 会生成一个指向堆中相同值的新指针。当指向给定值的最后一个Rc 指针被销毁时,指向的值也被销毁。

    RefCell(std::cell):

    Cell&lt;T&gt;RefCell&lt;T&gt; 类型的值可以通过共享引用(即常见的 &amp;T 类型)进行变异,而大多数 Rust 类型只能通过唯一(&amp;mut T)引用进行变异。我们说 Cell&lt;T&gt;RefCell&lt;T&gt; 提供了“内部可变性”,而典型的 Rust 类型则表现出“继承的可变性”。

    这是我对你的结构所做的:

    struct Inner {
        time: f64,
        location: Point,
        destination: Point,
    }
    
    #[derive(Clone)]
    pub struct Game {
        inner: Rc<RefCell<Inner>>,
    }
    

    这是什么意思? Inner 保存游戏状态(与旧的 Game 相同的字段)。新的Game 只有一个字段inner,其中包含共享状态。

    • Rc&lt;T&gt;(在这种情况下TRefCell&lt;Inner>)- 允许我多次克隆inner,但它不会克隆T
    • RefCell&lt;T&gt;(在这种情况下,TInner)- 允许我不可变地或可变地借用 T,检查是在运行时完成的

    我现在可以多次克隆Game 结构,它不会克隆RefCell&lt;Inner&gt;,只克隆GameRc。这就是 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.);
    }
    

    附:请在以后分享任何代码之前,安装并使用rustfmt

    【讨论】:

    • 我现在明白了将游戏作为一个局部变量实际上是多么的麻烦。因为在应用程序的整个生命周期中总是只有一个游戏应该是活着的,所以将它作为一个单例来亲吻它更有意义。从我所见,lazy_static 和 Mutex 是创建单例的完美方式。实际上,我以这种方式重构了我的代码,并实现了一些让我闻起来很香的东西。感谢 rustfmt 我将使用它。我真的很讨厌克娄巴特拉括号。
    【解决方案2】:

    在您的示例中,game_loop 拥有game,因为它被移入循环。所以任何应该改变游戏规则的事情都需要在game_loop 内部发生。要将事件处理融入其中,您有多种选择:

    选项 1

    game_loop 轮询事件。

    您创建一个事件队列,您的game_loop 将有一些逻辑来获取第一个事件并处理它。

    您必须在这里处理同步,所以我建议您阅读MutexConcurrency。但是一旦掌握了窍门,这应该是一项相当容易的任务。您的循环获得一个引用,每个事件处理程序获得一个,都尝试解锁互斥锁,然后访问队列(可能是向量)。

    这将使您的game_loop 成为他们所有的单一真理,这是一种流行的引擎设计,因为它很容易推理和开始。

    但也许你希望不那么集中。

    选项 2

    让事件发生在循环之外

    这个想法将是一个更大的重构。你可以把你的Game 放在一个lazy_static 中,周围有一个互斥锁。

    game_loop 的每次调用都会尝试获取所述 Mutex 的锁定,然后执行游戏计算。

    当输入事件发生时,该事件也会尝试获取Game 上的互斥锁。这意味着当 game_loop 正在处理时,不会处理任何输入事件,但它们会尝试在滴答之间进入。

    这里的挑战是保持输入顺序并确保输入的处理速度足够快。要完全正确,这可能是一个更大的挑战。但设计会给你一些可能性。

    这个想法的充实版本是Amethyst,它是大规模并行的,并且设计简洁。但他们在引擎背后采用了相当复杂的设计。

    【讨论】:

    • 实际上带有 Mutex 的lazy_static 并不是一个大的代码重构。通过这种方式,我实现了单例模式,这对我来说是实现此逻辑的正确方法。
    猜你喜欢
    • 2021-10-26
    • 2021-04-17
    • 1970-01-01
    • 2011-05-29
    • 2019-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多