Demilade Sonuga's blog
All postsThe Game Scene
It's finally time to draw up our game on the screen.
First off, delete that block.bmp
file from the project. Get the assets folder from the
repo
At this point, your blasterball
directory should be looking like this:
blasterball/
| src/
| | assets/
| | | ball.bmp
| | | blue_block.bmp
| | | canon-in-d-major.wav
| | | cyan_block.bmp
| | | drum.wav
| | | green_block.bmp
| | | paddle.bmp
| | | pink_block.bmp
| | | yellow_block.bmp
| | bitmap.rs
| | font.rs
| | main.rs
| | uefi.rs
| Cargo.lock
| Cargo.toml
Before we proceed we, first of all, have to ask the question: how exactly is the game scene supposed to look and how do I translate that to code?
To answer the first question, the game scene is supposed to look something like this:
The second question of how to translate to code is not as straightforward as the first. We combine what we already have to produce our desired outcome.
Let's start this translation process by getting rid of our experimental animation in efi_main
:
#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
let sys_table = unsafe { &*sys_table };
let boot_services = sys_table.boot_services();
let init_graphics_result = init_graphics(boot_services);
if let Err(msg) = init_graphics_result {
let simple_text_output = sys_table.simple_text_output();
write!(simple_text_output, "{}", msg);
loop {}
}
let screen = init_graphics_result.unwrap();
init_screen(screen);
let screen = get_screen().unwrap();
/* DELETED:
let block_bytes = include_bytes!("./block.bmp");
let block = Bitmap::new(block_bytes);
let mut block_position = (0, 0);
while block_position.1 < NO_OF_PIXELS_IN_A_ROW {
draw_bitmap(screen, &block, block_position);
erase_bitmap(screen, &block, block_position);
block_position.1 += 1;
}
*/
0
}
Next, we create a new module game.rs
in the blasterball src directory. All the game code will go in here.
This makes sense because it seems main.rs
's function is to set the stage for the game and not to run it.
In game.rs
, we create a new function blasterball
:
pub fn blasterball(screen: &mut Screen) -> ! {
}
It takes a Screen
reference because it needs to draw to the screen. The return value of !
tells the compiler
that this function is never meant to return. This makes sense because the game is going to keep running until
we explicitly exit it.
Now, to begin the translation process, we start with the blocks. Each block in the asset folder has a width of 80 pixels and a height of 36 pixels. So, we can have a total of 640 / 80 == 8 blocks in a row. The number of rows of blocks to use is a matter of personal taste. The total number of rows of the screen the blocks can occupy is 480 / 36 == 13.333... So, if we decide to fill in the screen with blocks completely, we'll have approximately 13 rows of blocks. Drawing 4 rows will use up about a third of the screen for the blocks. A third of the screen for the blocks seems like a reasonable amount, so we'll go with 4 rows.
We need an algorithm that will draw the blocks on the screen, reasonably distributing the block colors. Come up with one now.
- Load the images.
- Cycle through them, drawing them on screen until 4 full rows of blocks have been drawn.
In pseudocode, the algorithm will look something like this:
fn blasterball(screen)
blocks <- load all blocks into an array
for each block in cycle(blocks)
if all 4 rows have been filled
break
else
draw_bitmap(screen, block)
The block position is omitted to make the algorithm clear. The cycle
function there cycles endlessly through
an array. So if blocks = [red, blue, green], then cycle(blocks) = [red, blue, green, red, blue, green, red, ...].
This way, the colors of the blocks will be reasonably distributed.
The only thing here that seems not so straightforward to translate to Rust is the cycle
function. To do this
we're going to make use of the Iterator
trait.
We've encountered Iterator
before in one of those early posts but we didn't go into what exactly they were
and why they're needed. Remember that traits define common behavior of different things. The Write
trait
defines behavior for anything that characters can be written into. The Index
and IndexMut
traits define behavior for anything that can be indexed. Likewise, the Iterator
trait defines the behavior of anything
that can be iterated over.
Once this Iterator
trait has been defined on a thing, there are automatically a lot of default implementations
of useful functions. You can find them in the docs
One of those useful functions automatically defined on an Iterator is cycle
which does exactly what we
described.
Our translation to Rust now seems straightforward:
// NEW:
use crate::uefi::{Screen, NO_OF_PIXELS_IN_A_ROW};
use crate::bitmap::{draw_bitmap, Bitmap};
// The number of rows of blocks that are going to be drawn on screen
const NO_OF_BLOCK_ROWS: usize = 4;
pub fn blasterball(screen: &mut Screen) -> ! {
// NEW:
// Loading the into an array
let blue_block_bytes = include_bytes!("./assets/blue_block.bmp");
let blue_block = Bitmap::new(blue_block_bytes);
let cyan_block_bytes = include_bytes!("./assets/cyan_block.bmp");
let cyan_block = Bitmap::new(cyan_block_bytes);
let green_block_bytes = include_bytes!("./assets/green_block.bmp");
let green_block = Bitmap::new(green_block_bytes);
let pink_block_bytes = include_bytes!("./assets/pink_block.bmp");
let pink_block = Bitmap::new(pink_block_bytes);
let yellow_block_bytes = include_bytes!("./assets/yellow_block.bmp");
let yellow_block = Bitmap::new(yellow_block_bytes);
let paddle_bytes = include_bytes!("./assets/paddle.bmp");
let paddle = Bitmap::new(paddle_bytes);
let ball_bytes = include_bytes!("./assets/ball.bmp");
let ball = Bitmap::new(ball_bytes);
let blocks = [blue_block, cyan_block, green_block, pink_block, yellow_block];
// The initial block position is at the top left corner
// of the screen
let mut block_position = (0, 0); // (row, column)
// Cycle through the blocks until 4 rows have been filled
for (i, block) in blocks.iter().cycle().enumerate() {
let no_of_blocks_in_a_row = NO_OF_PIXELS_IN_A_ROW / block.width();
if i >= no_of_blocks_in_a_row * NO_OF_BLOCK_ROWS {
break;
}
draw_bitmap(screen, block, block_position);
block_position.1 += block.width();
if block_position.1 >= NO_OF_PIXELS_IN_A_ROW {
block_position.0 += block.height();
block_position.1 = 0;
}
}
loop {}
}
The array's iter
method returns an Iterator over the references to the block's items. Calling cycle
returns
a new Iterator that cycles through those references endlessly. Calling enumerate
on the resulting Iterator
returns a new Iterator that yields tuples (index, block reference) where index is a usize
that starts
from 0.
The algorithm says to keep drawing until 4 rows have been filled. But how will we know when exactly four rows have been filled? If we fill four rows, then the total number of blocks will be the number of blocks in a row * 4.
The number of blocks in a row: number of pixels in one row of the screen/block width in pixels.
Hence, the condition: i >= no_of_blocks_in_a_row * NO_OF_BLOCK_ROWS
because if this condition becomes
true, then we've already drawn four rows of blocks.
As for this:
block_position.1 += block.width();
if block_position.1 >= NO_OF_PIXELS_IN_A_ROW {
block_position.0 += block.height();
block_position.1 = 0;
}
This just ensures that when one row is filled, the drawing continues on the next row.
To run the game now, we invoke this game
function in main.rs
:
#![no_std]
#![no_main]
#![feature(abi_efiapi)]
#![feature(panic_info_message)]
// ... Others
// NEW:
mod game;
// ... Others
#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ... Others
init_screen(screen);
let screen = get_screen().unwrap();
// NEW:
game::blasterball(screen);
// Returning 0 because the function expects it
0
}
Running the game now will draw the blocks on the screen. The paddle and the ball are all that's left. To determine the position of the ball and paddle, we'll do some simple trial and error.
Let's start with the paddle since the ball is on top of the paddle.
The paddle is at the center of the screen and a little above the bottom. The width of the screen is 640, so let's try placing it in column 640 / 2. Since the paddle is just a little above the bottom, it will be on one of the last rows. The height of the paddle is 16, so let's try placing it on row 480 - 16 - 10. The -10 there is just for space between the paddle and the bottom of the screen.
// DELETED: use crate::uefi::{Screen, NO_OF_PIXELS_IN_A_ROW};
use crate::uefi::{Screen, NO_OF_PIXELS_IN_A_ROW, NO_OF_PIXELS_IN_A_COLUMN}; // NEW
// ... Others
pub fn blasterball(screen: &mut Screen) -> ! {
// ... Others
let initial_paddle_position = (NO_OF_PIXELS_IN_A_COLUMN - paddle.height() - 10, NO_OF_PIXELS_IN_A_ROW / 2);
draw_bitmap(screen, &paddle, initial_paddle_position);
loop {}
}
If you run the game now, you'll see that the paddle is not at the center of the screen, but rather, it's shifted to the right. To push it to the center, let's reduce that column by, say 45.
pub fn blasterball(screen: &mut Screen) -> ! {
// ... Others
let initial_paddle_position = (
NO_OF_PIXELS_IN_A_COLUMN - paddle.height() - 10,
// DELETED: NO_OF_PIXELS_IN_A_ROW / 2,
NO_OF_PIXELS_IN_A_ROW / 2 - 45 // NEW
);
draw_bitmap(screen, &paddle, initial_paddle_position);
loop {}
}
If you run it now, the paddle's position should seem reasonable. It appears centered on the screen and it's slightly above the screen bottom.
The ball will be above the paddle, so a reasonable way to determine the row position will be to set it to the paddle's row position - the ball's height (so the ball will be on top) - a small number (for space). The ball is meant to be centered above the paddle. So a reasonable way to determine the column position will set it to the paddle's column position increased by half the paddle's width (to put it at the center).
pub fn blasterball(screen: &mut Screen) -> ! {
// ... Others
draw_bitmap(screen, &paddle, initial_paddle_position);
let initial_ball_position = (
initial_paddle_position.0 - ball.height() - 5,
initial_paddle_position.1 + paddle.width() / 2
);
draw_bitmap(screen, &ball, initial_ball_position);
loop {}
}
The ball looks a bit right-shifted from the paddle's center. To fix this, just reduce the column by a small amount so the ball can look reasonably centered.
pub fn blasterball(screen: &mut Screen) -> ! {
// ... Others
draw_bitmap(screen, &paddle, initial_paddle_position);
let initial_ball_position = (
initial_paddle_position.0 - ball.height() - 5,
initial_paddle_position.1 + paddle.width() / 2 - 10
);
draw_bitmap(screen, &ball, initial_ball_position);
loop {}
}
And the game scene has been drawn.
Take Away
For the full code up to this point, go to the repo
In The Next Post
We'll be getting ready for event handling