我正在尝试编写一个可以在浏览器中运行的简单游戏,并且由于浏览器,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
,所以这行不通。
有人建议的另一种方法是遵循game of life example中说明的模式。从高层次上讲,它涉及导出包含游戏状态并包括公共tick()
和render()
函数的类型,可以从javascript游戏循环中调用它们。这对我不起作用,因为我的游戏状态需要生命周期参数,因为它实际上只是包装了specs World
和Dispatcher
结构,后者具有生命周期参数。最终,这意味着我无法使用#[wasm_bindgen]
导出它。
我很难找到解决这些限制的方法,并且正在寻找建议。
答案 0 :(得分:5)
对此进行建模的最简单方法可能是将requestAnimationFrame
的调用留给JS,而只是在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
答案 1 :(得分:1)
我是Rust的新手,但这是我解决相同问题的方法。
您可以通过从window.request_animation_frame
回调中调用window.request_animation_frame
来检查window.set_interval
或要查看的内容,从而消除有问题的Rc<RefCell<bool>>
递归并实现FPS上限如果仍然有动画帧请求待处理。我不确定无效的标签页行为在实践中是否会有所不同。
因为我一直使用Rc<RefCell<...>>
来进行其他事件处理,所以我将布尔值置于应用程序状态。我尚未检查下面的代码是否可以按原样编译,但这是我执行此操作的相关部分:
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_interval
和timer_closure
的结果存储在Option
中,以便您的游戏可以出于某些原因(如有必要)进行清理(也许?没有尝试过,这似乎会导致self
释放?)。循环引用除非被破坏(除非将Rc
有效地存储到应用程序内部的应用程序中),否则不会擦除自身。它还应该使您可以在运行时更改最大fps,方法是停止间隔并使用相同的闭包创建另一个。