There aren't many different events to handle:
- Left and right arrow keys to move the tetrimino to the right or the left
- Up arrow key to make the tetrimino rotate
- Down arrow key to make the tetrimino descend one block
- Spacebar to make the tetrimino descend to the bottom instantly
- Escape to quit the game
It's still possible to add some later on (such as pausing the game with the return key, for example) but for now, let's focus on these ones. For this, go back inside the main loop of the game (inside the main function) and replace the current event handling with the following function:
fn handle_events(tetris: &mut Tetris, quit: &mut bool, timer: &mut SystemTime, event_pump: &mut sdl2::EventPump) -> bool { let mut make_permanent = false; if let Some(ref mut piece) = tetris.current_piece { let mut tmp_x = piece.x; let mut tmp_y = piece.y; for event in event_pump.poll_iter() { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } =>
{ *quit = true; break } Event::KeyDown { keycode: Some(Keycode::Down), .. } =>
{ *timer = SystemTime::now(); tmp_y += 1; } Event::KeyDown { keycode: Some(Keycode::Right), .. } =>
{ tmp_x += 1; } Event::KeyDown { keycode: Some(Keycode::Left), .. } =>
{ tmp_x -= 1; } Event::KeyDown { keycode: Some(Keycode::Up), .. } =>
{ piece.rotate(&tetris.game_map); } Event::KeyDown { keycode: Some(Keycode::Space), .. } =>
{ let x = piece.x; let mut y = piece.y; while piece.change_position(&tetris.game_map, x, y + 1)
== true { y += 1; } make_permanent = true; } _ => {} } } if !make_permanent { if piece.change_position(&tetris.game_map, tmp_x, tmp_y)
==
false && tmp_y != piece.y { make_permanent = true; } } } if make_permanent { tetris.make_permanent(); *timer = SystemTime::now(); } make_permanent }
Quite a big one:
let mut make_permanent = false;
This variable will tell us whether the current tetrimino is still falling. If not, then it becomes true, the tetrimino is then put into the game map and we generate a new one. Luckily for us, we already wrote all the needed functions to perform these operations:
if let Some(ref mut piece) = tetris.current_piece {
This is simple pattern binding. If our game doesn't have a current piece (for some reason), then we don't do anything and just leave:
let mut tmp_x = piece.x; let mut tmp_y = piece.y;
If there is a move on the x or on the y axis, we'll write it into these variables and then we'll test whether the tetrimino can actually go there:
for event in event_pump.poll_iter() {
As there can be multiple events that happened since the last time we came into this function, we need to loop over all of them.
Now we're arriving at the interesting part:
match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { *quit = true; break } Event::KeyDown { keycode: Some(Keycode::Down), .. } => { *timer = SystemTime::now(); tmp_y += 1; } Event::KeyDown { keycode: Some(Keycode::Right), .. } => { tmp_x += 1; } Event::KeyDown { keycode: Some(Keycode::Left), .. } => { tmp_x -= 1; } Event::KeyDown { keycode: Some(Keycode::Up), .. } => { piece.rotate(&tetris.game_map); } Event::KeyDown { keycode: Some(Keycode::Space), .. } => { let x = piece.x; let mut y = piece.y; while piece.change_position(&tetris.game_map, x, y + 1) ==
true { y += 1; } make_permanent = true; } _ => {} }
We can almost consider this small code as the core of our application, without it, no interaction with the program is possible. If you want more interactions, this is where you'll add them:
Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { *quit = true; break }
If we receive a quit event from sdl or if we receive an Escape, KeyDown event, we set the quit variable to true. It'll be used outside of this function to then leave the main loop--and therefore leave the program itself. Then we break; no need to go further since we know that we're leaving the game:
Event::KeyDown { keycode: Some(Keycode::Down), .. } => { *timer = SystemTime::now(); tmp_y += 1; }
If the down arrow is pressed, we need to make our tetrimino descend by one block and also put the timer value to now. timer is used to know at what speed the tetrimino blocks are falling. The shorter the time, the faster they'll descend.
For now, it isn't used in this function, so we'll see how to handle it outside of it:
Event::KeyDown { keycode: Some(Keycode::Right), .. } => { tmp_x += 1; } Event::KeyDown { keycode: Some(Keycode::Left), .. } => { tmp_x -= 1; }
In here, we handle the right and left arrow keys. It's just like the down arrow key, except we don't need to change the timer variable:
Event::KeyDown { keycode: Some(Keycode::Up), .. } => { piece.rotate(&tetris.game_map); }
If we receive an up arrow key pressed event, we rotate the tetrimino:
Event::KeyDown { keycode: Some(Keycode::Space), .. } => { let x = piece.x; let mut y = piece.y; while piece.change_position(&tetris.game_map, x, y + 1) == true { y += 1; } make_permanent = true; }
And finally the last of our events: the spacebar key pressed event. Here, we move the tetrimino down as much as we can and then set the make_permanent variable to true.
With this, that's it for our events. However, like we said before if you want to add more events, this is where you should put them.
Time to put all this into our main loop:
fn print_game_information(tetris: &Tetris) { println!("Game over..."); println!("Score: {}", tetris.score); // println!("Number of lines: {}", tetris.nb_lines); println!("Current level: {}", tetris.current_level); // Check highscores here and update if needed } let mut tetris = Tetris::new(); let mut timer = SystemTime::now(); loop { if match timer.elapsed() { Ok(elapsed) => elapsed.as_secs() >= 1, Err(_) => false, } { let mut make_permanent = false; if let Some(ref mut piece) = tetris.current_piece { let x = piece.x; let y = piece.y + 1; make_permanent =
!piece.change_position(&tetris.game_map,
x, y); } if make_permanent { tetris.make_permanent(); } timer = SystemTime::now(); } // We need to draw the tetris "grid" in here. if tetris.current_piece.is_none() { let current_piece = tetris.create_new_tetrimino(); if !current_piece.test_current_position(&tetris.game_map) { print_game_information(&tetris); break } tetris.current_piece = Some(current_piece); } let mut quit = false; if !handle_events(&mut tetris, &mut quit, &mut timer, &mut
event_pump) { if let Some(ref mut piece) = tetris.current_piece { // We need to draw our current tetrimino in here. } } if quit { print_game_information(&tetris); break } // We need to draw the game map in here. sleep(Duration::new(0, 1_000_000_000u32 / 60)); }
Doesn't seem that long, right? Just a few comments where we're supposed to draw our Tetris, but otherwise everything is in there, which means that our Tetris is now fully functional (even though it isn't displayed).
Let's explain what's happening in there:
let mut tetris = Tetris::new(); let mut timer = SystemTime::now();
In here, we initialize both our Tetris object and the timer. The timer will be used to let us know when the tetrimino is supposed to descend by one block:
if match timer.elapsed() { Ok(elapsed) => elapsed.as_secs() >= 1, Err(_) => false, } { let mut make_permanent = false; if let Some(ref mut piece) = tetris.current_piece { let x = piece.x; let y = piece.y + 1; make_permanent = !piece.change_position(&tetris.game_map,
x, y); } if make_permanent { tetris.make_permanent(); } timer = SystemTime::now(); }
This code checks whether it's been one second or more since the last time the tetrimino descended by one block. If we want to handle levels, we'll need to replace the following line:
Ok(elapsed) => elapsed.as_secs() >= 1,
Its replacement will need to be something more generic and we'll add an array to store the different levels' speed of descent.
So coming back to the code, if it's been one second or more then we try to make the tetrimino descend by one block. If it cannot, then we put it into the game map and re-initialize the timer variable.
Once again, you might wonder why we had to create the make_permanent variable instead of directly checking the output of:
!piece.change_position(&tetris.game_map, x, y)
It has an if condition, right? Well, just like the previous times, it's because of the borrow checker. We borrow tetris here:
if let Some(ref mut piece) = tetris.current_piece {
So as long as we're in this condition, we can't use tetris mutably, which is why we store the result of the condition in make_permanent so we can use the make_permanent method after:
if tetris.current_piece.is_none() { let current_piece = tetris.create_new_tetrimino(); if !current_piece.test_current_position(&tetris.game_map) { print_game_information(&tetris); return } tetris.current_piece = Some(current_piece); }
If there is no current tetrimino, we need to generate a new one, which we do by calling the create_new_tetrimino method. Then we check whether it can be put into the game on the top line by calling the test_current_position method. If not, then it means the game is over and we quit. Otherwise, we store the newly-generated tetrimino in tetris.current_piece and we move on.
Two things are missing here:
- Since we don't handle the increase of lines sent, nor the score, nor the level, there's no need to print them
- We didn't add yet the highscores loading/overwrite
Of course, we'll add all this later on:
let mut quit = false; if !handle_events(&mut tetris, &mut quit, &mut timer, &mut event_pump) { if let Some(ref mut piece) = tetris.current_piece { // We need to draw our current tetrimino in here. } } if quit { print_game_information(&tetris); break }
This code calls the handle_events function and acts according to its output. It returns whether the current tetrimino has been put into the game map or not. If it is the case, then there is no need to draw it.
We now need to do the following remaining things:
- Add the score, levels, and number of lines sent
- Load/overwrite the highscores if needed
- Actually draw the Tetris
Seems like we're getting very close to the end! Let's start by adding the score, number of lines sent, and levels!