Home > Enterprise >  Easiest way to loop until async function returns in Rust
Easiest way to loop until async function returns in Rust

Time:10-31

I'm writing a program to play Chess. My Game class has the async function play. This function returns when the game ends, returning a Winner enum, which is either a stalemate or a win for some colour.

In my main fn, I want to run game.play, then run game.render() repeatedly, until game.play returns a Winner, at which point the loop should stop.

Having attempted to read the docs, I don't really understand why Pinning or Boxing is neccesary in order to poll the future, and I haven't been able to get it working.

The desired code for main should look something like this:

    let textures = piece::Piece::load_all_textures().await;

    let mut game = game::Game::new(player::Player::User, player::Player::User);
    let future = game.play();

    while None = future.poll() {
        game.render(textures).await;
        next_frame().await;
    }

    // Do something now that the game has ended

I also understand that I will run into issues with borrow checking with this code, but that's a problem for me to deal with once I figure this one out.

What is the easiest way of attempting what I am trying to do?

CodePudding user response:

You can simply use loop

let textures = piece::Piece::load_all_textures().await;

let mut game = game::Game::new(player::Player::User, player::Player::User);
let future = game.play();

loop {
    game.render(textures).await;
    next_frame().await;
}

This will make the game run at the highest frame rate possible though, which is usually over kill, so you might want to add a sleep

loop {
    game.render(textures).await;
    next_frame().await;
    async_std::task::sleep(Duration::from_millis(15)).await;
}

This will run it at around 60 frames/second. If you want to be very accurate you can use Instant

loop {
    let now = Instant::now();
    game.render(textures).await;
    next_frame().await;
    let elapsed_ms = now.elapsed().as_millis();
    // rendering could take more than 17ms in that case substraction can 
    // overflow
    let sleep_ms = 17.checked_sub(elapsed_ms).unwrap_or_default();
    async_std::task::sleep(Duration::from_millis(sleep_ms)).await;
}

This would result in a 58.8 frames per second (you can use nanoseconds for 60frames/second which needs 16.66 ms per frame.)

CodePudding user response:

The code depends a bit on what async runtime you're using. Using the popular Tokio runtime you can do something like this (the code should be very similar for async-std):

#[tokio::main]
async fn main() {
    let textures = Piece::load_all_textures().await;
    let game = Arc::new(Game::new(Player::User, Player::User));

    let game_ref = game.clone();
    let play_handle = tokio::spawn(async move { game_ref.play().await });

    let render_handle = tokio::spawn(async move {
        loop {
            game.render(&textures).await;
            next_frame().await;

            // Naive fixed delay; in reality you'd want to delay based on how long rendering took.
            tokio::time::sleep(Duration::from_millis(10)).await;
        }
    });

    let winner = play_handle.await.unwrap();

    // Do something now that the game has ended

    render_handle.abort();
    println!("{winner:?}");
}

Playground link with full code

We use tokio::spawn() to run a play task and render task concurrently. It returns a handle that you can .await to get the task's return value. The handle also has an abort() method that we use to end the render task once the play task finishes.

The Game struct is stored in an Arc so we can share it between the play and render tasks. The tokio::spawn() function requires a 'static future, so we can't just use plain references to share Game.

  • Related