Project 8: Bare Metal Game
Classic early video games such as Space Invaders,
Asteroids, and
Pac-man were implemented without an operating system.
The games mediated all hardware interaction by themselves.
In this project, you will use the Pluggable Interrupt OS
to develop a bare-metal video game. The graphical aspect of the game will be rendered in the VGA buffer.
You are encouraged to be creative in using VGA characters to represent your game graphics.
You are welcome to develop an original game or to create your own interpretation of a classic game.
As an example, feel free to reference Ghost Hunter,
my own interpretation of a well-known classic.
Unlike our other projects, for this project you are welcome to work in a team of up to three students.
Interrupts
The primary purpose of this project is for you to gain experience writing software that handles
hardware interrupts. When an event pertinent to the hardware
occurs, it notifies the CPU by signaling an interrupt. The CPU responds to the interrupt by suspending execution
of whatever code it is running and running the code designated for handling the interrupt. Naturally,
interrupt-handling code should complete as quickly as possible so that the CPU can resume normal execution.
Setup
The Pluggable Interrupt OS provides a convenient framework for
specifying to the CPU the code to be executed to handle each interrupt.
I have created a template
for you to use as a starting point for your projects. To start your project, clone
the Pluggable Interrupt Template
project. In order to build the project, you’ll also need to install:
- Qemu
- Nightly Rust:
rustup override set nightly
llvm-tools-preview
:
rustup component add llvm-tools-preview
- The bootimage tool:
- On Windows:
rustup component add rust-src --toolchain nightly-x86_64-pc-windows-msvc
Once the template is up and running, you will be ready to implement your own interrupt handlers! Of course,
you’ll want to change the project name and authors in
Cargo.toml, and you’ll also
want to set up your own GitHub repository for it.
The template project demonstrates a simple interactive program that uses both keyboard and timer interrupts.
When the user types a viewable key, it is added to a string in the middle of the screen.
When the user types an arrow key, the string begins moving in the indicated direction.
Here is its main.rs
:
#![no_std]
#![no_main]
use pc_keyboard::DecodedKey;
use pluggable_interrupt_os::HandlerTable;
use pluggable_interrupt_os::vga_buffer::clear_screen;
use pluggable_interrupt_template::LetterMover;
use crossbeam::atomic::AtomicCell;
#[no_mangle]
pub extern "C" fn _start() -> ! {
HandlerTable::new()
.keyboard(key)
.timer(tick)
.startup(startup)
.cpu_loop(cpu_loop)
.start()
}
static LAST_KEY: AtomicCell<Option<DecodedKey>> = AtomicCell::new(None);
static TICKS: AtomicCell<usize> = AtomicCell::new(0);
fn cpu_loop() -> ! {
let mut kernel = LetterMover::new();
let mut last_tick = 0;
loop {
if let Some(key) = LAST_KEY.load() {
LAST_KEY.store(None);
kernel.key(key);
}
let current_tick = TICKS.load();
if current_tick > last_tick {
last_tick = current_tick;
kernel.tick();
}
}
}
fn tick() {
TICKS.fetch_add(1);
}
fn key(key: DecodedKey) {
LAST_KEY.store(Some(key));
}
fn startup() {
clear_screen();
}
The _start() function kicks everything off by placing references to our interrupt handling functions
in a HandlerTable object. Invoking .start() on the HandlerTable
sets up the interrupt handlers, then starts execution of the cpu_loop()
function.
I created the LetterMover
struct
to represent the application state. It is maintained inside of
cpu_loop()
. It gets updated in response to messages from the interrupt
handlers. Those messages are sent by updating shared AtomicCell
objects.
Whenever a timer interrupt occurs, tick()
increases TICKS
by one. Whenever a
keyboard interrupt occurs, key()
updates LAST_KEY
to contain the most
recent keypress.
The cpu_loop()
checks TICKS
constantly. Whenever a new tick occurs,
it instructs the LetterMover
to update its state accordingly. Similarly,
whenever a new keypress appears, cpu_loop()
relays that keystroke
to LetterMover
.
This shows the basic design that all of these projects should employ:
- Create a
main.rs
that sets up the interrupt handlers.
- Write a
cpu_loop()
function that contains the main data structures
and monitors concurrent state for updates from interrupts.
- Write one-line handlers for the timer and keyboard that reference shared
state that
cpu_loop()
can employ.
- Place all of the game functionality within the game-state object, defined in lib.rs.
Here is the rest of its code, found in its lib.rs
file:
#![cfg_attr(not(test), no_std)]
use bare_metal_modulo::{ModNumC, MNum, ModNumIterator};
use pluggable_interrupt_os::vga_buffer::{BUFFER_WIDTH, BUFFER_HEIGHT, plot, ColorCode, Color, is_drawable};
use pc_keyboard::{DecodedKey, KeyCode};
use num::traits::SaturatingAdd;
#[derive(Copy,Debug,Clone,Eq,PartialEq)]
pub struct LetterMover {
letters: [char; BUFFER_WIDTH],
num_letters: ModNumC<usize, BUFFER_WIDTH>,
next_letter: ModNumC<usize, BUFFER_WIDTH>,
col: ModNumC<usize, BUFFER_WIDTH>,
row: ModNumC<usize, BUFFER_HEIGHT>,
dx: ModNumC<usize, BUFFER_WIDTH>,
dy: ModNumC<usize, BUFFER_HEIGHT>
}
impl LetterMover {
pub fn new() -> Self {
LetterMover {
letters: ['A'; BUFFER_WIDTH],
num_letters: ModNumC::new(1),
next_letter: ModNumC::new(1),
col: ModNumC::new(BUFFER_WIDTH / 2),
row: ModNumC::new(BUFFER_HEIGHT / 2),
dx: ModNumC::new(0),
dy: ModNumC::new(0)
}
}
fn letter_columns(&self) -> impl Iterator<Item=usize> {
ModNumIterator::new(self.col)
.take(self.num_letters.a())
.map(|m| m.a())
}
pub fn tick(&mut self) {
self.clear_current();
self.update_location();
self.draw_current();
}
fn clear_current(&self) {
for x in self.letter_columns() {
plot(' ', x, self.row.a(), ColorCode::new(Color::Black, Color::Black));
}
}
fn update_location(&mut self) {
self.col += self.dx;
self.row += self.dy;
}
fn draw_current(&self) {
for (i, x) in self.letter_columns().enumerate() {
plot(self.letters[i], x, self.row.a(), ColorCode::new(Color::Cyan, Color::Black));
}
}
pub fn key(&mut self, key: DecodedKey) {
match key {
DecodedKey::RawKey(code) => self.handle_raw(code),
DecodedKey::Unicode(c) => self.handle_unicode(c)
}
}
fn handle_raw(&mut self, key: KeyCode) {
match key {
KeyCode::ArrowLeft => {
self.dx -= 1;
}
KeyCode::ArrowRight => {
self.dx += 1;
}
KeyCode::ArrowUp => {
self.dy -= 1;
}
KeyCode::ArrowDown => {
self.dy += 1;
}
_ => {}
}
}
fn handle_unicode(&mut self, key: char) {
if is_drawable(key) {
self.letters[self.next_letter.a()] = key;
self.next_letter += 1;
self.num_letters = self.num_letters.saturating_add(&ModNumC::new(1));
}
}
}
The keyboard handler receives each character as it is typed. Keys representable as a char
are added to the moving string. The arrow keys change how the string is moving.
Requirements
This project is very flexible in its requirements. A successful submission will:
- Run in the Qemu x86 virtual machine without any supporting operating system.
- Be built upon the Pluggable Interrupt OS.
- Only use
#[no_std]
Rust features and crates.
- Enable the player to control a graphically depicted avatar using the keyboard.
- Include gameplay elements that advance based on clock ticks and cause the game to eventually end if the player
does not react to them appropriately.
- Run indefinitely without any panics.
- Allow the user to restart the game when it ends.
- Display the user’s current score.
- If multiple students collaborate, the features of the game should be proportionate
to the number of contributors.
Submissions
- Create a public GitHub repository for your bare metal game.
- Post the repository URL to the General channel in the CSCI 320 Teams page by 5pm on Thursday, March 16.
- Pick four games. Download and play them. Then post reviews of each game in your private Teams channel by
Friday, March 17 at 5 pm.
Assessment
- Partial: Meets requirements 1, 2, and 3, and either requirement 4 or requirement 5.
- Complete: Meets all requirements given above, including the submitted reviews of other games.
- Bonus Token: If the program meets all requirements, and additionally
demonstrates creativity or innovation in a striking way, each contributor will earn
a bonus token.
Acknowledgement
This assignment was adapted from materials developed by
Philipp Oppermann.