How to build the simplest event loop for this Tetris ? (beginner)

Hello everyone.
My Tetris works ! It’s here. It is ugly as hell, but my goal is more about improving what’s under the hood, refactoring the data flow, clearing up the functions’ purpose, etc.

Now there is this thing I can’t wrap my head around. My run() function looks like this:

pub fn run(&mut self) {
        loop {
            // Termion's async stdin takes the user inputs and this function
            // calls the moves like push_right(), turn(), etc.
            self.take_directions();
            self.display_the_board();

            // Those functions manage game-induced events by keeping watch
            self.clear_full_rows();
            self.game_over();

            // The thread sleeps for 700 ms (for a start) and then the tick()
            // function brings the shape one row lower, and if it can't, freezes
            // it and call the next shape
            thread::sleep(time::Duration::from_millis(self.speed));
            self.tick();
        }
    }

The problem is that the player has to wait for 700 ms until his moves are displayed on the screen.

What I would like :

  • tick() called every 700 ms
  • a loop that lets take_directions() be used as much as the user wants

I’ve heard of event loops but the crates I’ve been looking in look very impressive for what I just want to do.
What should be the better option / crate in your opinion ? Is there anything in the standard library for me ? Something like the Threadpool the Book talks about (I’ve not followed the book so far)

I’d like to know what I need to learn. If it’s still too ambitious for me now, I’ll come back to it later, when I’m wiser and older :wink:

edit: I forgot to mark this with the help flap.

Simplest option is to not use a thread sleep (don’t block your loop), but instead track the last time you did a clear_full_rows and game_over.

pseudocode:

pub fn run(&mut self) {
  let mut lastUpdate = SystemTime::now();
  loop {
    self.take_directions();
    self.display_the_board();

    let elapsedSinceLastUpdate = lastUpdate.elapsed().unwrap().as_secs();
    let shouldUpdate = elapsedSinceLastUpdate >= time::Duration::from_millis(self.speed);
    if (shouldUpdate)
      self.clear_full_rows();
      self.game_over();
      lastUpdate = SystemTime::now();
    }
  }
}
3 Likes

Another option (a little harder) is to use two threads -

  1. take_directions and display_the_board
  2. clear_full_rows and game_over

Use messages passing to co-ordinate things: https://doc.rust-lang.org/book/ch16-02-message-passing.html

That’s roughly the advice in here: http://gameprogrammingpatterns.com/game-loop.html#play-catch-up

@Keksoj, I’d recommend reading just the sample code in the page I’ve linked at the section of that page that I linked to. Then, if you’re interested in way more information than you asked for, there’s a lot of information on game loops on that page. You don’t need all of it for Tetris. Most of it’s in case you’re making a game that has physics.

The big point of it is you have a tight loop which basically amounts to your own sleep function. In there, collect user input, but don’t advance the actual game other than that.

1 Like

That’s perfect I’ll dive into it, thanks for both your answers !

@ricjac Holy cow that works ! I had to change bits to your code but it worked perfectly:

pub fn run(&mut self) {
        let mut last_tick = std::time::SystemTime::now();

        loop {
            self.take_directions(); // moves
            self.display_the_board();

            let elapsed_since_last_tick = last_tick.elapsed().unwrap().as_millis();
            let should_update = elapsed_since_last_tick >= self.speed;
            if should_update {
                self.clear_full_rows(); 
                self.game_over(); 
                self.tick();
                last_tick = std::time::SystemTime::now();
            }
        }
    }

The only thing was that the display_the_board function was called either at each tick (which doesn’t allow the player some real_time feedback about the state of the game) or at each iteration of the loop (which is waaaaaaaay to fast), so I gave it its own time loop:

    pub fn run(&mut self) {
        let mut last_tick = std::time::SystemTime::now();
        let mut last_display = std::time::SystemTime::now();
        loop {
            self.take_directions(); // moves
            let elapsed_since_last_display = last_display.elapsed().unwrap().as_millis();
            let should_display = elapsed_since_last_display >= 50;
            if should_display {

                self.display_the_board();
                last_display = std::time::SystemTime::now();
            }
            let elapsed_since_last_tick = last_tick.elapsed().unwrap().as_millis();
            let should_update = elapsed_since_last_tick >= self.speed;
            if should_update {
                self.clear_full_rows(); //
                self.game_over(); // checks if game is over
                self.tick();
                last_tick = std::time::SystemTime::now();
            }
        }
    }

And now I’ve got a 50 ms fast display of the moves. I won’t let the code stay like that, I’ll call the display function at each move, that should do the trick. It just goes to show how simple and straigth-forward your solution is. Thank you very much.

One thing yet : the rust book usually writes variable like this: my_variable. Why do you spell it myVariable ? Is there some kind of convention I’m not aware of ?

Now I know what the multi-thread architecture is made for, I’ll have some purpose while learning about it.

@ajnye: Thanks for the link, it does answer exactly the questions I had while struggling.

1 Like

The standard Rust formatting guidelines say you should use snake_case (as you have in your code), not camelCase - I believe the compiler will throw a warning if you use the latter.

1 Like

Oh yeah, and crates must have a snake_case name as well. I remember implementing the snake game in Rust and I started the project by doing

cargo new Snake

and everytime I compiled it rustc told me :

warning: crate `Snake` should have a snake case name

that was fun.

3 Likes

@Keksoj that is awesome! great work, that’s really exciting!

Apologies about the Pascal casing, I spend 6+ hours a day on languages that use Pascal case, and Rust is just a hobby for me, so when replying on online forums, the rust compiler doesn’t warn me and I’m still in the old Pascal case mindset.

1 Like