Demilade Sonuga's blog

All posts

To Create A Game

2023-06-26

We're almost there. According to the list laid down in the prologue, we've completed nine out of twelve items in the creation of this project. And while that's all wonderful, we still do not have a game. All our work did in the preceding posts was to set up the infrastructure that we'll use to make the game, not the game itself. So, that's what we'll dive into in this post.

What Exactly Are We Doing?

Before we can proceed to make the game, we first have to confirm that we truly know the answer to these questions: What is Blasterball? What is this game exactly? When a typical player sits to play the game, how will this player play it? What is expected to be the player's aim? And how is the player supposed to achieve this aim?

Well, Blasterball is actually a clone of a much older game: Breakout which was developed in 1976 and supposedly designed by Steve Wozniak and a bunch of other computer-happy troopers.

What we're developing here is a simple variant of this Breakout.

For more info about Breakout, check the references.

Gameplay

The gameplay is elementary: Blocks are arranged at the top of the screen. Using a single ball, the player must blast all the blocks. If the player manages to do this without the ball ever going offscreen, the player wins. If the ball goes offscreen, the player loses.

The Physics

But from this description, some more questions still arise. What exactly is meant by "blast all the blocks"? What exactly is meant by "ball goes offscreen"?

Take a look at our game scene:

The Game Scene

Consider our cast of characters: the ball, the paddle, and the blocks. This cast does not exist in a vacuum. They're in a 2D world with their own physics, their own laws of motion and interaction.

Motion

The blocks are stationary. When the game starts, they're in a single position and they remain in that position until the ball hits them, or until the game is over.

The paddle, which initially carries the ball, can only move in straight horizontal lines from the position it started in.

The ball can move anywhere, but only in a straight line in the direction it bounces into.

Interaction

When the game starts, the ball is catapulted from the paddle in a random direction upwards.

When the ball hits (or shall we say "blasts") a block, the block disintegrates and the ball bounces away from it in a different direction.

When the ball hits the paddle, it bounces away from the paddle in a direction different from that in which it was moving when it made contact with the paddle.

When the ball hits the screen boundary at the top or on the right or on the left, it bounces off in a different direction. You can assume the top of the screen is some kind of ceiling and the left and right are walls.

But when the ball moves downward, if the player is unable to move the paddle into a position where it can reflect the ball's direction quickly enough, the ball will pass the bottom boundary of the screen. It will "go offscreen".

You can view the bottom boundary of the screen as a sink waiting to suck the ball in and scream "GAME OVER!!!".

Altogether

After the implementation of our game's physical rules of motion and interaction, typical gameplay will look something like this:

GamePlay

Getting On With The Game

How do we approach this? Let's start from where are now. Right now, we have the initial game scene. Now, when the game starts the ball ought to move in a random direction.

So, that's what we'll do now. If you need a refresher on animation, you should check out the animating bitmaps post.

To make the ball move, we redraw it over and over in different positions, giving the illusion of motion.

For now and for simplicity, the ball will only move upwards. Let's just get something moving.

First, let's get rid of the hook_event call in efi_main, so it won't disturb us later.

In main.rs

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ...Others

    interrupts::enable_interrupts();

    /* DELETED:
    event_hook::hook_event(EventKind::Keyboard, boxed_fn::BoxedFn::new(|event_info| {
        if let EventInfo::Keyboard(key_event) = event_info {
            if key_event.direction == keyboard::KeyDirection::Down {
                write!(SCREEN.lock().as_mut().unwrap(), "{:?}", key_event.keycode);
            }
        }
    }, allocator::get_allocator()));
    */

    game::blasterball(&SCREEN);
    // ...Others
}

This task is rather simple. To make the ball move upwards, all we have to do is periodically erase the ball and then redraw it in a slightly higher position. More precisely:

fn move_ball_upwards()
    initial_ball_pos = (y, x)
    draw game scene
    periodically:
        erase ball
        initial_ball_pos.y -= a small amount
        draw ball

If you take a good look at the algorithm given, you'll see that there are two things that are rather strange about it. For one, the coordinate of the ball is specified as (y, x) instead of the usual (x, y). And to move the ball upwards, we subtract from the y position instead of adding to it. The reason for this will soon become clear (if it isn't already). For now, let's just say that because of the way we've modeled the screen, we have to specify coordinates as (y, x) subtracting from the y coordinate takes an object upwards.

Open up game.rs and throw this in:

// ...Others
// DELETED: use crate::display::bitmap::{draw_bitmap, Bitmap};
use crate::display::bitmap::{draw_bitmap, Bitmap, erase_bitmap}; // NEW
use crate::sync::{mutex::Mutex};
// NEW:
use crate::alloc::allocator;
use crate::alloc::boxed_fn::BoxedFn;
use crate::event_hook::{self, EventKind};
pub fn blasterball(screen: &Mutex<Option<&mut Screen>>) -> ! {
    // ...Others
    draw_bitmap(screen, &paddle, initial_paddle_position);

    /* DELETED:
    let initial_ball_position = (
        initial_paddle_position.0 - ball.height() - 5,
        initial_paddle_position.1 + paddle.width() / 2 - 10
    );
    */
    // NEW:
    let mut ball_pos = (
        initial_paddle_position.0 - ball.height() - 5,
        initial_paddle_position.1 + paddle.width() / 2 - 10
    );
    // DELETED: draw_bitmap(screen, &ball, initial_ball_position);
    draw_bitmap(screen, &ball, ball_pos); // NEW
    
    // NEW:
    event_hook::hook_event(EventKind::Timer, BoxedFn::new(|_| {
        erase_bitmap(screen, &ball, ball_pos);
        ball_pos.0 -= 10;
        draw_bitmap(screen, &ball, ball_pos);
    }, allocator::get_allocator()));
}

The initial_ball_position name is changed to ball_pos because it no longer only holds the ball's initial position.

The timer event is used to redraw the ball periodically. The timer interrupt fires every 100 milliseconds, so the redrawing should be frequent enough to give the illusion of motion.

For this to work, the definition of erase_bitmap has to be modified:

In display/bitmap.rs

// DELETED: pub fn erase_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) {
pub fn erase_bitmap(screen: &Mutex<Option<&mut Screen>>, bitmap: &Bitmap, pos: (usize, usize)) { // NEW
    for row in 0..bitmap.width() {
        for col in 0..bitmap.height() {
            if row + pos.0 < NO_OF_PIXELS_IN_A_COLUMN && col + pos.1 < NO_OF_PIXELS_IN_A_ROW {
                // DELETED: screen.pixels[row + pos.0][col + pos.1] = Pixel {
                screen.lock().as_mut().unwrap().pixels[row + pos.0][col + pos.1] = Pixel { // NEW
                    red: 0,
                    green: 0,
                    blue: 0,
                    reserved: 0
                };
            }
        }
    }
}

Upon running, you should get something like this:

Moving Up

The ball moves up and passes right through the blocks, erasing part of them. When it reaches the top, its y position becomes 0, but our redraw function doesn't recognize that, so it subtracts from the position (which is a usize) again, resulting in an overflow panic because a usize cannot hold a value lesser than 0.

Right now, it is clear what is missing: the physics. How can we know when the ball has collided with a block or a wall or the ceiling? How do we create a deflection of the ball's direction? How can we make the ball move in a direction that is not completely right or left or up or down?

This all boils down to the mathematics of the matter and that is what we're going to get into next.

Take Away

For the full code up to this point, go to the repo

In The Next Post

We'll start getting into the mathematics of the game world.

References

  • Breakout: https://en.wikipedia.org/wiki/Breakout_(video_game)
  • The number of times the timer generates an interrupt per second: https://pdos.csail.mit.edu/6.828/2008/lec/l-interrupt.html under the heading Device Interrupts.