8. Let's play Pong

Used imports in this chapter

use cortex_m_rt::entry;
use defmt::{info, unwrap};
use embassy_101::game::{GameState, InputState};
use embassy_executor::{Executor, InterruptExecutor, Spawner};
use embassy_futures::select::select4;
use embassy_futures::yield_now;
use embassy_nrf::gpio::{AnyPin, Input, Pull};
use embassy_nrf::interrupt;
use embassy_nrf::interrupt::{InterruptExt, Priority};
use embassy_nrf::peripherals::SPI3;
use embassy_nrf::{
    bind_interrupts,
    gpio::{Level, Output, OutputDrive},
    peripherals, spim,
};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex};
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::geometry::{Dimensions, OriginDimensions};
use embedded_graphics::pixelcolor::raw::{RawData, RawU16};
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::Rectangle;
use embedded_graphics::Drawable;
use static_cell::StaticCell;
use {defmt_rtt as _, panic_probe as _};

Ok, we have already come a long way exploring many fundamental parts of the ecosystem. In this last chapter, we are going to build upon the foundations we built over the course of the last chapters and build a little game called Pong.

We rely on embedded_graphics for their abstractions to build more complex shapes. Additionally, we will use Embassy to asynchronously process the input events.

Getting access in the draw_task to the input state

To share the input state, we need a way to store the information coming from the input_handler. Look into the src/game.rs file, where there is already an InputState struct defined. Some traits already have been derived, such that we can more easily share the input state.

#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct InputState {
    pub btn1: bool,
    pub btn2: bool,
    pub btn3: bool,
    pub btn4: bool,
}

Just as before, let's add some helper type aliases.

// a shorthand, to not have to type this out every time we need it
pub type InputStateType = Mutex<CriticalSectionRawMutex, InputState>;
pub type InputStateRef = &'static InputStateType;

Then in our main function right before we spawn our input_handler, we initialize our struct, and retrieve a static reference to it:

// init input state, so we know that from now on input state will always be valid
static INPUT_STATE: StaticCell<InputStateType> = StaticCell::new();
let input_static_ref: InputStateRef = INPUT_STATE.init(Mutex::new(InputState::default()));

Something peculiar to the previous snippet is the use of CriticalSectionRawMutex instead of the usual NoopRawMutex. This is because we are sharing across 2 different executors, where one of them is an interrupt based executor. The CriticalSectionRawMutex ensures that no interrupts will be fired while it is locked, thereby sufficiently protecting our state.

The next step will now be to pass this freshly created reference to the corresponding tasks. This includes sharing the reference and populating it with correct data in input_handler, and reading the state in draw_task. At the end, you should have something like the following in draw_task:

// Later we will initialize the game state here
loop {
    // make sure that the mutex guard for input state is dropped
    // as soon as possible after we have copied the state.
    let input_state = { *input_state.lock().await };
    // Update here the game
    {
        let mut fb = back_buffer.lock().await;
        let fb = &mut *fb; // unpack guard

        // start drawing calls
        unwrap!(fb.fill_solid(&fb.bounding_box(), Rgb565::BLACK));
        // Draw here the game

        signal.signal(()); // signal to driver that draw has been called
    } // Guard is dropped here
    led.set_low();
    yield_now().await;
}

And the following input_handler:

#[embassy_executor::task]
async fn input_handler(
    btns: (AnyPin, AnyPin, AnyPin, AnyPin),
    input_state: InputStateRef,
    led: AnyPin, // We optionally add here a pin, such that we can more easily debug things
) {
    // Initialize all the buttons
    let mut btn1 = Input::new(btns.0, Pull::Up);
    let mut btn2 = Input::new(btns.1, Pull::Up);
    let mut btn3 = Input::new(btns.2, Pull::Up);
    let mut btn4 = Input::new(btns.3, Pull::Up);

    let mut led = Output::new(led, Level::Low, OutputDrive::Standard);

    loop {
        // Wait until we see a button change
        select4(
            btn1.wait_for_any_edge(),
            btn2.wait_for_any_edge(),
            btn3.wait_for_any_edge(),
            btn4.wait_for_any_edge(),
        )
        .await;

        // Create an updated state
        led.set_high();
        let state = InputState {
            btn1: btn1.is_low(),
            btn2: btn2.is_low(),
            btn3: btn3.is_low(),
            btn4: btn4.is_low(),
        };
        {
            // Send the update to the other tasks
            *input_state.lock().await = state; // keep lock as small as possible
        } // drop immediatly

        // Log something, such that we know what is happening
        info!(
            "Detected input change 1: {}, 2: {}, 3: {}, 4: {}",
            state.btn1, state.btn2, state.btn3, state.btn4,
        );
        led.set_low();
    }
}

Implementing the game logic

Next is actually implementing the game of Pong. We will start by creating a struct for the players:

pub struct Player {
    position: Point, // the current position of the player
}

impl Player {
    /// p is the initial point to spawn the player on
    pub fn new(p: Point) -> Self {
        Self { position: p }
    }

    // make sure that the player can only move on the screen
    // the max height is 128 pixels
    pub fn move_up(&mut self) {
        todo!()
    }

    // make sure that the player can only move on the screen
    // the min height is 0 pixels
    pub fn move_down(&mut self) {
        todo!()
    }
}
Solution: move_up
pub fn move_up(&mut self) {
    self.position.y -= 10;
    self.position.y = self.position.y.clamp(0, 128);
}
Solution: move_down
pub fn move_down(&mut self) {
    self.position.y += 10;
    self.position.y = self.position.y.clamp(0, 128);
}

To make it easier for us to later do the collision detection, while leveraging embedded_graphics, we will implement some interesting traits that keep track off how big the player is (already provided), and how to draw the player on screen (target):

// Drawable is something that can be drawn onto the screen
impl Drawable for Player {
    type Color = Rgb565; // needed to be compatible with our screen

    type Output = (); // we will not need to return anything after drawing

    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
    where
        D: DrawTarget<Color = Self::Color>,
    {
        // Make sure you use the same values as used in the Dimensions
        // trait implementation. Otherwise the collision detection will
        // be harder to correctly implement
        todo!("Look at how we have drawn a rectangle in the past")
    }
}

// Have a way to know what the bounding box of the player is
impl Dimensions for Player {
    fn bounding_box(&self) -> Rectangle {
        Rectangle {
            top_left: self.position,
            size: Size {
                width: 5, // these seem good values
                height: 30,
            },
        }
    }
}
Solution: Draw
/// Draw a player as a rectangle and use the values from Dimensions-trait.
/// This makes it easier in the GameState to do the collision detection calculations
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
    D: DrawTarget<Color = Self::Color>,
{
    let rectangle = self.bounding_box();
    let style = PrimitiveStyle::with_fill(Rgb565::WHITE);
    rectangle.draw_styled(&style, target)
}

Similarly for the ball, we are going to need to implement a few methods on the Pong struct.

pub struct Pong {
    pub position: Point, // current position
    pub velocity: Point, // to where we will update the position
}

impl Pong {
    pub fn new(p: Point) -> Self {
        Self {
            position: p,
            velocity: Point::new(3, 3),
        }
    }

    pub async fn update(&mut self) {
        todo!()
    }

    pub fn flip_horizontal(&mut self) {
        todo!()
    }

    pub fn flip_vertical(&mut self) {
        todo!()
    }
}

impl Drawable for Pong {
    type Color = Rgb565;

    type Output = ();

    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
    where
        D: DrawTarget<Color = Self::Color>,
    {
        todo!("This time draw me as a circle -> take a look at the primitives section in the embedded_graphics documentation")
    }
}

impl Dimensions for Pong {
    fn bounding_box(&self) -> Rectangle {
        Rectangle {
            top_left: self.position,
            size: Size {
                width: 10, // seem good values
                height: 10,
            },
        }
    }
}
Solution: Update
pub async fn update(&mut self) {
    self.position += self.velocity;
}
Solution: Flip horizontal/vertical
pub fn flip_horizontal(&mut self) {
    self.velocity.y = -self.velocity.y;
}

pub fn flip_vertical(&mut self) {
    self.velocity.x = -self.velocity.x;
}
Solution: Draw
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
    D: DrawTarget<Color = Self::Color>,
{
    let circle = Circle::new(self.position, 10);
    let style = PrimitiveStyle::with_fill(Rgb565::WHITE);
    circle.draw_styled(&style, target)
}

And now the game state itself. We will need a struct which can store both players, their scores and the ball. There is already a GameState defined in src/game.rs, but it needs some extra code to be completely functional.

The definition of the GameState is:

pub struct GameState {
    score1: u32, // The score of player 1
    player1: Player, // Player 1
    score2: u32, // The score of player 2
    player2: Player, // Player 2
    pong: Pong, // The ball
    screen_box: Rectangle, // The bounds of the current screen
}

We have already defined the following methods:

  • new: The constructor
  • update: Updates the game state and does the collision detection
  • draw: Draws a line in the middle, draws the scores, the 2 players and the ball

Now, you should complete the draw method as it does not yet draw the players and the ball as it should.

impl Drawable for GameState {
    type Color = Rgb565;

    type Output = ();

    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
    where
        D: DrawTarget<Color = Self::Color>,
    {
        // ... more drawing code here ...

        // power of the abstractions of embedded_graphics -> draw the players and ball
        todo!("Draw player1, player2 and pong"); // REPLACE THIS
    }
}
Solution
impl Drawable for GameState {
    type Color = Rgb565;

    type Output = ();

    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
    where
        D: DrawTarget<Color = Self::Color>,
    {
        let background_style = PrimitiveStyle::with_fill(Rgb565::CSS_GRAY);
        let screen_box = target.bounding_box();
        let split_line = Rectangle::with_center(
            screen_box.center(),
            Size {
                width: 2,
                height: screen_box.size.height,
            },
        );

        split_line.draw_styled(&background_style, target)?;

        // use the font system of embedded_graphics
        let score_style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_GRAY);

        // pprint score 1 with a stack allocated string
        let mut score1 = heapless::String::<4>::new();
        if { write!(score1, "{}", self.score1) }.is_err() {
            defmt::error!("Error while formatting score 1");
        }
        Text::with_alignment(
            &score1,
            Point::new(3 * screen_box.size.width as i32 / 4, 10),
            score_style,
            Alignment::Center,
        )
        .draw(target)?;

        // pprint score 2
        let mut score2 = heapless::String::<4>::new();
        if { write!(score2, "{}", self.score2) }.is_err() {
            defmt::error!("Error while formatting score 2");
        }
        Text::with_alignment(
            &score2,
            Point::new(screen_box.size.width as i32 / 4, 10),
            score_style,
            Alignment::Center,
        )
        .draw(target)?;

        // power of the abstractions of embedded_graphics -> draw the players and ball
        self.player1.draw(target)?; // NEW
        self.player2.draw(target)?; // NEW
        self.pong.draw(target) // NEW
    }
}

Now all we still have to do is initialize our game state in the draw_task, update it every frame, and draw it to the framebuffer.

// draw_task
let mut game_state = todo!(); // NEW
loop {
    let input_state = { *input_state.lock().await };
    game_state.update(input_state).await; // NEW
    {
        let mut fb = back_buffer.lock().await;
        let fb = &mut *fb; // unpack guard

        // start drawing calls
        unwrap!(fb.fill_solid(&fb.bounding_box(), Rgb565::BLACK));
        unwrap!(game_state.draw(fb)); // NEW

    } // Guard is dropped here
    signal.signal(()); // signal to driver that draw has been called
    yield_now().await;
}
Solution
let mut game_state = GameState::new(Rectangle {
    top_left: Point::new(0, 0),
    size: Size::new(160, 128),
});

🎉 Congratulations 🎉

This is the end of this book. You should now have a working Pong game. Try to play a bit and enjoy your async Pong implementation.

If you got here, but did not have a finished game, try to make sure that you have spawned all the necessary tasks.

The tasks that need to be spawned:

  • input_handler: Spawned in the high priority executor
  • main: Spawned as the first task in the low priority executor
  • draw_task: Called at the end of main
  • async_driver: Spawned at the end of main

If your input is not working as expected, make sure that:

  • You update the InputState in input_handler
  • The input is correctly shared between the input_handler and the draw_task
  • You drop the mutex guard quickly enough in the draw_task

If the game is not updating, see if you maybe forgot to call .await on a future. Futures in Rust do not call themselves, so if you forget to call .await, to progress will be made.

Ps. Is it getting a bit slow? Try to compile in release mode:

$ cargo run --release # and now wait until it compiles :p

Finished pong