Как можно обойтись без возможности экспортировать функции со временем жизни при использовании wasm-bindgen?
Я пытаюсь написать простую игру, которая запускается в браузере, и мне трудно смоделировать игровой цикл, учитывая комбинацию ограничений, наложенных браузером, 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
изнутри, так что это не работает.
Альтернативный подход, который кто-то предложил, состоит в том, чтобы следовать шаблону, проиллюстрированному на примере игры из жизни. На высоком уровне, это включает в себя экспорт типа, который содержит состояние игры и включает в себя публичный tick()
а также render()
функции, которые можно вызывать из игрового цикла javascript. Это не работает для меня, потому что моему игровому состоянию требуются параметры времени жизни, так как это фактически просто оборачивает спецификации World
а также Dispatcher
структура, последняя из которых имеет параметры времени жизни. В конечном итоге это означает, что я не могу экспортировать его, используя #[wasm_bindgen]
,
Я с трудом нахожу способы обойти эти ограничения и ищу предложения.
2 ответа
Самый простой способ смоделировать это, вероятно, оставить вызовы 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
Я новичок в Rust, но вот как я решил ту же проблему.
Вы можете устранить проблемные window.request_animation_frame
рекурсии и реализовать ограничение FPS в то же время, вызывая window.request_animation_frame
из window.set_interval
обратный вызов, который проверяет Rc<RefCell<bool>>
или что-нибудь, чтобы видеть, ожидает ли еще запрос кадра анимации. Я не уверен, будет ли поведение неактивной вкладки отличаться на практике.
Я помещаю bool в состояние моего приложения, так как я использую 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 во время работы, останавливая интервал и создавая другой, используя то же самое закрытие.