We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
3. Build Your First Game with Rust
Now we get to start in with our first game Flappy Dragon. This will involve some loops and the following:
state machine
gravity
add player
flap the dragons wings
Understanding the Game Loop
The game loop will run through these steps as we update state and screen.
1 configure app (windows and graphics)
2 Poll OS for input state
3 call tick function
4 update screen
5 quit?
back to 2 if no quit
. Configure App, Window, and Graphics: Configuring an application to work
with your Operating System is no small undertaking and varies by plat-
form. Displaying graphics can be an enormous endeavor—Vulkan or
OpenGL can require hundreds of lines of code before the system is ready
to draw a single triangle.
2. Poll OS for Input State: Polling for input state is also platform specific.
Most Operating Systems provide a series of events representing what the
user did with the mouse, keyboard, and window elements. The game
engine translates these events into a standard format so that you don’t
need to worry about what type of computer you’re targeting.
3. Call Tick Function: The tick() function provides your gameplay. bracket-lib,
the game engine you’ll use in this chapter, calls the tick() function on each
pass through the main loop (“frame” and “tick” both represent this). Most
game engines provide tick functionality in some form. Unreal Engine and
Unity can attach tick functionality to objects—leading to hundreds of tick
functions. Other engines provide only one and leave it to the developer to
separate functionality. You’ll implement your tick() function in Storing
State, on page 49.
4. Update Screen: Once the game has updated, the game engine updates
the screen. Again, the details vary by platform.
5. Quit (Yes/No): Finally, the loop checks to see if the program should exit.
If so, execution terminates and exits the game—again, the mechanism
for this is Operating System specific. If the game should continue, the
loop ensures that other programs continue to run smoothly by yielding
control back to the OS. The game loop then returns to step 2, displaying
another frame.
We don’t want to get to bogged down on the specifics we want to just build a game that works and with that said we will use the Bracket-Lib library.
What are Bracket-Lib and Bracket-Terminal?
bracket-lib is a Rust game programming library. Its designed to simplify game development.
bracket-terminal is the display portion of the bracket-lib. It provides an emulated console, and can work with rendering platforms. Some of those are: OpenGL, Vulkan, Web Assembly, and Metal. It also supports sprites.
Create a New Project that Uses Bracket-Lib
When you create a game that uses a game loop, the first step is to create the program’s basic structure. That means you:
1. Create a project.
2. Connect the project to the game engine.
3. Display “Hello, World” text.
First let’s create a new project with cargo new flappy.
Next we need to add in the dependencies for the bracket-lib to the Cargo.toml file
[package]
name = "flappy"
version = "0.1.0"
edition = "2024"
[dependencies]
bracket-lib = "~0.8.1"
Hello, Bracket Terminal
Okay so now we want to include the bracket-lib within our main.src. Add this line to the top of the file.
use bracket_lib::prelude::*;
The * is a wildcard and it says to include everything that is inside the bracket-lib library. It is exported via the prelude. Now that we have it in the program we will need to store what it is doing. Everything that we want it to have and show will be in the game state.
Storing State
The game loop will run by calling your applications tick() function with every frame. It will update the state of the application. We will need a way to store the state so let’s create a struct for it.
struct State {}
Implementing Traits_
Traits are a way to define shared functionality for objects. They are similar to interfaces in other languages. They are implemented very similar to how we define a struct.
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print(1, 1, "Hello, Bracket Terminal!");
}
}
The for here says what we are doing for the State. For the tick we need to be able to &mut self to keep the state up-to-date, ctx provides the window currently running the bracket-terminal (this will provide access to the mouse position and keyboard input). ctx.cls() clears the screen. ctx.print() will put something on the screen for us.
Okay so we sent the parameters of 1,1 to the print. These are screen-space coordinates. Bracket-lib describes the top left as 0,0 and the bottom right of a 80x50 would be 79,49.
Handling Errors in the Main Function
Results are Rust’s standard method for handling errors. Results are an enumeration-just like Option. Bracket-lib provides a Result type named BError. Let’s set up the main to return that type.
fn main() -> BError {}
There is a standard builder pattern for constructing complicated objects.
Using the Builder Pattern
Bracket-terminal provides a simple80x50() as as starting point as this is a normal screen size for many small projects. You can then add more and more to it to get more starting state set. with_title() or with_font() can be added after. Add the following to your main().
fn main() -> BError {
let context = BTermBuilder::simple80x50()
.with_title("Flappy Dragon")
.build()?;
}
We built a 80x50 terminal. Made the window called “Flappy Dragon”. Then we can use the ? because we had main return a Result.
Now we can set up the main_loop so that the game will have a loop to follow add the following after the context.
main_loop(context, State {})
Let’s run the project cargo run and lets see the output.
Ran Into Some Issues
Here I want into some issues with the code not running. Check the end of the Chapter for all the work around that I did.
Codepage 437: The IBM Extended ASCII Character Set
Codepage 437 is the standard font from DOS-based PCs and that is what bracket-lib uses.
Creating Different Game Modes
Games will typically run in Modes. This will setup what the tick() will do. You don’t want the same thing to happen in a menu as you might in the middle of a battle. Flappy Dragon will have 3 modes:
1. Menu: The player is waiting at the main menu.
2. Playing: Game play is in progress.
3. End: The game is over.
With this said we can set up a GameMode enumeration.
enum GameMode {
Menu,
Playing,
End,
}
We have seen this before we set up and Enum. Then we set up the different states that we can be in. We now need to update some code to work with this.
struct State {
mode: GameMode,
}
impl State {
fn new() -> Self {
State {
mode: GameMode::Menu
}
}
}
/// Now we need to set up the main loop to work with this
main_loop(context, State::new())
We now have the ability to set the initial state of the game. Now we need to start to do things differently when we are in different states. Let’s change up the tick() so we can match to the mode of GameMode.
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
match self.mode {
GameMode::Menu => self.main_menu(ctx),
GameMode::End => self.dead(ctx),
GameMode::Playing => self.play(ctx),
}
}
}
WWe now route tick() to different helper functions depending on the current GameMode.
Play Function Stub
For now we just want the play to set the GameMode to End.
impl State {
...
fn play(&mut self, ctx: &mut BTerm) {
self.mode = GameMode::End;
}
}
Main Menu
Now we simply want the Menu to set the GameMode to Playing. Keep in mind that the menu will need to do a lot more. Like reset a game, take user input, etc. We will implement the restart that will start the game, and the main_menu that will take the user input.
impl State {
...
fn restart(&mut self) {
self.mode = GameMode::Playing;
}
}
fn main_menu(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "Welcome to Flappy Dragon");
ctx.print_centered(8, "(P) Play Game");
ctx.print_centered(9, "(Q) Quit Game");
}
Okay so we have some display and a way to change the GameMode. We will need to start to work with the user input. Thankfully there is a way to do that built into the BTerm object. We will use the if let to capture the value and again we will use the Some() or nothing to match values. Finish off the main_menu with this.
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
Game Over Menu
Now we can quickly work on the dead function of the state that will show up when a player is dead. Add this to the State.
fn dead(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "You are dead!");
ctx.print_centered(8, "(P) Play Again");
ctx.print_centered(9, "(Q) Quit Game");
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
}
Completed Game Control Flow
Let’s run the Program cargo run.
(P) Play Game
(Q) Quit Game
p
You are dead!❮
(P) Play Again
(Q) Quit Game
q
Adding the Player
For the game to work we need to add in the player, in this case the Dragon. You will press space in order to gain altitude. So we should add in a new struct for the player.
struct Player {
x: i32,
y: i32,
velocity: f32,
}
The x is the world space where they are in the level. The y is the screen space where they are on the screen. The velocity of the dragon. It might change or it might not. Next we can build the constructor.
impl Player {
fn new(x: i32, y: i32) -> Self {
Player {
x,
y,
velocity: 0.0,
}
}
}
Rendering the Player
Okay so for right now we will render the player as a yellow @ symbol towards the left of the screen. Let’s add in a render to the Player implementation.
fn render(&mut self, ctx: &mut BTerm) {
ctx.set(
0,
self.y,
YELLOW,
BLACK,
to_cp437('@')
);
}
set is responsible for setting a single character on screen. x is the x on the screen. y is the y on the screen. YELLOW and BLACK are the colors we will use. to_cp437() is the character we will display.
Falling to Your Inevitable Death
We will first set up the gravity and the terminal velocity for the Dragon.
fn gravity_and_move(&mut self) {
if self.velocity < 2.0 {
self.velocity += 0.2;
}
self.y += self.velocity as i32;
self.x += 1;
if self.y < 0 {
self.y = 0;
}
}
If the velocity is <2 we increase it by .2. We then add the current velocity to the player’s y position (remember that the lower on the screen the higher the value of the y). We then move the player forward on the global position. Lastly we can’t go higher than the screen.
Flapping Your Wings
When the dragon flaps its wings we want to increase the velocity of the dragon in the negative direction.
fn flap(&mut self) {
self.velocity = -2.0;
}
We now have a way to increase the velocity of the dragon.
Activating the Player
We now need to add a Player to the GameState and have something to keep track of the frame_time so we can regulate the game speed.
struct State {
player: Player,
frame_time: f32,
mode: GameMode,
}
impl State {
fn new() -> Self {
State {
player: Player::new(5,25),
frame_time: 0.0,
mode: GameMode::Menu,
}
}
Now we can set the restart to better reflect what we want to have for a new game.
fn restart(&mut self) {
self.mode = GameMode::Playing;
self.player = Player::new(5, 25);
self.frame_time = 0.0;
}
Constants
We can now work with some const that are meant to be global variables that we can use within the program. They call them constants for our program we will set the: SCREEN_WIDTH, SCREEN_HEIGHT, and FRAME_DURATION
const SCREEN_WIDTH: i32 = 80;
const SCREEN_HEIGHT: i32 = 50;
const FRAME_DURATION: f32 = 75.0;
Playing the Game
Now we can take care of the play of the State to represent the current state of play.
fn play(&mut self, ctx: &mut BTerm) {
ctx.cls_bg(NAVY);
self.frame_time += ctx.frame_time_ms;
if self.frame_time > FRAME_DURATION {
self.frame_time = 0.0;
self.player.gravity_and_move();
}
if let Some(VirtualKeyCode::Space) = ctx.key {
self.player.flap();
}
self.player.render(ctx);
ctx.print(0, 0, "Press SPACE to flap.");
if self.player.y > SCREEN_HEIGHT {
self.mode = GameMode::End;
}
}
cls_bg() sets the background color. The _tick() will run as fast as we let it, this check is to make sure that enough time has past before we add more gravity to the player. We then have the check for the player hitting space. One of the last checks is to see if the player has hit the bottom.
Flapping Your Wings
Now you can run the game cargo run. Check out how it looks and try to figure out any of the issues that you might have.
Creating Obstacles and Keeping Score
Now we can work on creating obstacles that we will have to avoid. Let’s create an new struct and its constructor.
struct Obstacle {
x: i32,
gap_y: i32,
size: i32,
}
impl Obstacle {
fn new(x: i32, score: i32) -> Self {
let mut random = RandomNumberGenerator::new();
Obstacle {
x,
gap_y: random.range(10, 40),
size: i32::max(2, 20 - score),
}
}
}
We have built the objects to work with what we have on the screen. There is some randomness in regards to the size of the gap based off the current score.
Rendering Obstacles
We now want to start to worry about how we will render the objects on the screen. They will use the “|” character to make them visible. We will have a gap that will be determined by the random number and the score. In the end we will have 2 things to render for the obstacle. The top that will go from 0 - half the screen - half the size of the gap. The other way will go from half the screen size plus have the size of the gap - the top of the y value.
fn render(&mut self, ctx: &mut BTerm, player_x: i32) {
let screen_x = self.x - player_x;
let half_size = self.size / 2;
// Draw the top half of the obstacle
for y in 0..self.gap_y - half_size {
ctx.set(screen_x, y, RED, BLACK, to_cp437('|'));
}
// Draw the bottom half of the obstacle
for y in self.gap_y + half_size..SCREEN_HEIGHT {
ctx.set(screen_x, y, RED, BLACK, to_cp437('|'));
}
}
Crashing into Walls
Now we can work on the check for if the dragon has hit a wall.
fn hit_obstacle(&self, player: &Player) -> bool {
let half_size = self.size / 2;
let does_x_match = player.x == self.x;
let player_above_gap = player.y < self.gap_y - half_size;
let player_below_gap = player.y > self.gap_y + half_size;
does_x_match && (player_above_gap || player_below_gap)
}
First we check to see if the player and the object have the same x. Then we calculate if the player is above the gap or below the gap. Then we check if both those things are true.
Keeping Score and Obstacle State
Now we can add in the obstacle and the score into the state.
struct State {
player: Player,
frame_time: f32,
mode: GameMode,
obstacle: Obstacle,
score: i32,
}
impl State {
fn new() -> Self {
State {
player: Player::new(5, 25),
frame_time: 0.0,
mode: GameMode::Menu,
obstacle: Obstacle::new(SCREEN_WIDTH, 0),
score: 0,
}
}
Including the Obstacle and Score in the Play Function
Let’s include the score and the obstacle into the play.
ctx.print(0, 0, "Press SPACE to flap.");
ctx.print(0, 1, &format!("Score: {}", self.score));
self.obstacle.render(ctx, self.player.x);
if self.player.x > self.obstacle.x {
self.score += 1;
self.obstacle = Obstacle::new(self.player.x + SCREEN_WIDTH, self.score);
}
if self.player.y > SCREEN_HEIGHT || self.obstacle.hit_obstacle(&self.player) {
self.mode = GameMode::End;
}
We print the score on the screen. We call the render of the obstacle to render it on screen. If we pass the obstacle then we add a point to the score and render a new one.
Adding the Score to the Game Over Screen
We are almost done so let’s fix the end screen dead.
fn dead(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "You are dead!");
ctx.print_centered(6, &format!("You earned {} points", self.score));
ctx.print_centered(8, "(P) Play Again");
ctx.print_centered(9, "(Q) Quit Game");
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
}
Resetting the Score and Obstacles When Restarting
We will fix the last bit of the restart as it will reset a game and start a new one.
fn restart(&mut self) {
self.mode = GameMode::Playing;
self.player = Player::new(5, 25);
self.frame_time = 0.0;
self.score = 0;
self.obstacle = Obstacle::new(SCREEN_WIDTH, 0);
}
Flappy Dragon
Run your completed game cargo run.
Wrap-Up
You have plenty of ways to improve Flappy Dragon. Here are some exercises to try:
• Play with the gravity level, velocity changes, and game speed. Notice how
different values can radically change the feel of the game.
• See if you can add graphics for the walls and dragon.
• Consider making the graphics bigger and the overall play area smaller to
better match Flappy Bird.
• Investigate bracket-lib’s “flexible console” and change the player coordinates
to floating-point numbers for smoother movement.
• Add color and visual flair to the menus.
• Try animating the dragon.
Chapter 3 fixes
This is the work to get the first Flappy Dragon running.
Fontconfig Missing
Error
The system library `fontconfig` required by crate `servo-fontconfig-sys` was not found.
Cause
bracket-lib depends on native Linux graphics/font libraries that Cargo cannot install automatically.
Fix
Install the required Linux packages:
sudo apt update
sudo apt install pkg-config libfontconfig1-dev
Runtime Crash in freetype-rs
Error
unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be aligned and non-null
Cause
bracket-lib 0.8.x uses older graphics/font dependencies that are not fully compatible with very new Rust versions (rustc 1.95.0).
The project compiled successfully, but crashed at runtime inside:
freetype-rs
Switched to an Older Rust Toolchain
Install older Rust version
rustup install 1.74.1
Set project-local Rust version
Run inside the project folder:
rustup override set 1.74.1
Verify
rustc --version
cargo --version
Cargo Lockfile Incompatibility
Error
lock file version `4` was found, but this version of Cargo does not understand this lock file
Cause
The newer Rust toolchain created a newer Cargo.lock format that older Cargo could not read.
Fix
Delete the lockfile and regenerate it with the older toolchain:
rm Cargo.lock
cargo run
or
cargo generate-lockfile
Updated Cargo.toml
Changed Rust edition
From:
edition = "2024"
To:
edition = "2021"
Final setup
[package]
name = "flappy"
version = "0.1.0"
authors = ["Adam Vietro <adam.vietro@gmail.com>"]
edition = "2021"
[dependencies]
bracket-lib = "~0.8.1"
Useful Commands
Clean project
cargo clean
Run project
cargo run
Check active Rust version
rustc --version
cargo --version
Remove project-local override later
rustup override unset