6

我正在尝试编写一个在浏览器中运行的简单游戏,考虑到浏览器、rust 和 wasm-bindgen 施加的限制组合,我很难对游戏循环进行建模。

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

function mainLoop() {
    update();
    draw();
    requestAnimationFrame(mainLoop);
}

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

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从自身内部引用,所以这不起作用。

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

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

4

2 回答 2

7

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

然而,在 Rust 中,您还可以利用这样一个事实,即实际上不捕获任何变量的闭包大小为零,这意味着Closure<T>该闭包不会分配内存,您可以放心地忘记它。例如,这样的事情应该可以工作:

#[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

于 2018-11-01T20:09:19.080 回答
2

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

您可以通过调用回调来消除有问题的window.request_animation_frame递归并同时实现 FPS 上限,该window.request_animation_frame回调window.set_interval检查 aRc<RefCell<bool>>或其他内容以查看是否仍有动画帧请求未决。我不确定非活动标签行为在实践中是否会有所不同。

我将 bool 放入我的应用程序状态,因为无论如何我都在使用Rc<RefCell<...>>to 来处理其他事件。我还没有检查以下是否按原样编译,但这是我如何执行此操作的相关部分:

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_closurein存储Option在您的游戏状态中,以便您的游戏可以在出于某种原因需要时自行清理(也许?我还没有尝试过,它似乎会导致释放self?)。除非损坏,否则循环引用不会自行擦除(然后您将Rcs 有效地存储到应用程序内部的应用程序)。它还应该使您能够在运行时更改最大 fps,方法是停止间隔并使用相同的闭包创建另一个。

于 2018-11-19T18:39:47.653 回答