关于rust:在使用wasm-bindgen时,如何解决无法终生导出功能的问题?

How can I work around not being able to export functions with lifetimes when using wasm-bindgen?

我正在尝试编写一个可以在浏览器中运行的简单游戏,并且由于浏览器,rust和wasm-bindgen的限制,我很难建模一个游戏循环。

浏览器中的典型游戏循环遵循以下一般模式:

1
2
3
4
5
function mainLoop() {
    update();
    draw();
    requestAnimationFrame(mainLoop);
}

如果我要在rust / wasm-bindgen中建模这个确切的模式,它将看起来像这样:

1
2
3
4
5
let main_loop = Closure::wrap(Box::new(move || {
    update();
    draw();
    window.request_animation_frame(main_loop.as_ref().unchecked_ref()); // Not legal
}) as Box<FnMut()>);

与javascript不同,我无法从自身内部引用main_loop,因此这行不通。

有人建议的另一种方法是遵循"人生博弈"示例中说明的模式。从高层次上讲,它涉及导出包含游戏状态并包含公共tick()render()函数的类型,可以从javascript游戏循环中调用该函数。这对我不起作用,因为我的游戏状态需要生存期参数,因为它实际上只是package了规范WorldDispatcher结构,后者具有生存期参数。最终,这意味着我无法使用#[wasm_bindgen]导出它。

我很难找到解决这些限制的方法,并且正在寻找建议。


对此建模最简单的方法可能是将requestAnimationFrame的调用留给JS,而只是在Rust中实现更新/绘制逻辑。

但是,在Rust中,您还可以利用以下事实:实际上并未捕获任何变量的闭包大小为零,这意味着该闭包的Closure<T>不会分配内存,您可以安全地忘记它。例如,类似这样的方法应该起作用:

1
2
3
4
5
6
7
8
9
#[wasm_bindgen]
pub fn main_loop() {
    update();
    draw();
    let window = ...;
    let closure = Closure::wrap(Box::new(|| main_loop()) as Box<Fn()>);
    window.request_animation_frame(closure.as_ref().unchecked_ref());
    closure.forget(); // not actually leaking memory
}

如果状态中存在生命周期,那么很遗憾,这与返回JS不兼容,因为当您一路返回JS事件循环时,所有WebAssembly堆栈框架都会弹出,这意味着任何生命周期都将失效。这意味着您的游戏状态在main_loop的各个迭代中持续存在,将需要为'static


我是Rust的新手,但这是解决相同问题的方法。

您可以通过从window.set_interval回调中调用window.request_animation_frame来消除window.request_animation_frame递归问题,并同时实施FPS上限,该回调检查Rc<RefCell<bool>>或其他内容,以查看是否还有动画帧请求仍在等待处理中。我不确定无效的标签页行为在实践中是否会有所不同。

由于我一直在使用Rc<RefCell<...>>进行其他事件处理,因此我将bool设置为应用程序状态。我还没有检查下面的代码是否可以按原样编译,但这是我执行此操作的相关部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pub struct MyGame {
    ...
    should_request_render: bool, // Don't request another render until the previous runs, init to false since we'll fire the first one immediately.
}

...

let window = web_sys::window().expect("should have a window in this context");
let application_reference = Rc::new(RefCell::new(MyGame::new()));

let request_animation_frame = { // request_animation_frame is not forgotten! Its ownership is moved into the timer callback.
    let application_reference = application_reference.clone();
    let request_animation_frame_callback = Closure::wrap(Box::new(move || {
        let mut application = application_reference.borrow_mut();
        application.should_request_render = true;
        application.handle_animation_frame(); // handle_animation_frame being your main loop.
    }) as Box<FnMut()>);
    let window = window.clone();
    move || {
        window
            .request_animation_frame(
                request_animation_frame_callback.as_ref().unchecked_ref(),
            )
            .unwrap();
    }
};
request_animation_frame(); // fire the first request immediately

let timer_closure = Closure::wrap(
    Box::new(move || { // move both request_animation_frame and application_reference here.
        let mut application = application_reference.borrow_mut();
        if application.should_request_render {
            application.should_request_render = false;
            request_animation_frame();
        }
    }) as Box<FnMut()>
);
window.set_interval_with_callback_and_timeout_and_arguments_0(
    timer_closure.as_ref().unchecked_ref(),
    25, // minimum ms per frame
)?;
timer_closure.forget(); // this leaks it, you could store it somewhere or whatever, depends if it's guaranteed to live as long as the page

您可以在游戏状态下将set_intervaltimer_closure的结果存储在Option中,以便由于某些原因(如有需要,您的游戏可以自行清理)(也许?我没有尝试过,并且它似乎会导致释放self?)。除非损坏,否则循环引用不会擦除自身(然后将Rc s有效地存储到应用程序内部的应用程序中)。通过停止间隔并使用相同的闭包创建另一个帧,它还应该使您能够在运行时更改最大fps。