Как можно обойтись без возможности экспортировать функции со временем жизни при использовании wasm-bindgen? - PullRequest
0 голосов
/ 26 октября 2018

Я пытаюсь написать простую игру, которая работает в браузере, и мне трудно моделировать игровой цикл, учитывая комбинацию ограничений, наложенных браузером, 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.Это не работает для меня, потому что моему игровому состоянию требуются параметры времени жизни, поскольку он просто оборачивает структуры specs World и Dispatcher, последняя из которых имеет параметры времени жизни.В конечном итоге это означает, что я не могу экспортировать его, используя #[wasm_bindgen].

Мне трудно найти способы обойти эти ограничения и ищу предложения.

Ответы [ 2 ]

0 голосов
/ 19 ноября 2018

Я новичок в 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 s в состоянии вашей игры, чтобы ваша игра могла по какой-то причине очиститься, если это необходимо (может быть? Я не пробовал этого, и это, похоже, приводит к освобождению self?).Циркулярная ссылка не будет стираться сама по себе, если она не нарушена (тогда вы эффективно сохраняете Rc s для приложения внутри приложения).Он также должен позволять вам изменять максимальный fps во время работы, останавливая интервал и создавая другой, используя то же замыкание.

0 голосов
/ 01 ноября 2018

Самый простой способ смоделировать это, вероятно, оставить вызовы 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

...