I saw a post on reddit asking for a simple game dev tutorial with Rust. Generally people encourage using ECS with any Rust game, so I guess there's no tutorials for anything without it. However, for simple games like Pong or Flappy Bird, it really is easier to forgo the ECS.

While Amethyst, the biggest Rust game engine, is completely built on ECS and you can't make a game without it, there's still plenty of small game engines that don't make any assumptions. My favorite of these is ggez.

With this tutorial I won't try to explain very much syntax. You should probably read at least a few chapter of the book to understand everything.

I'd recommend also opening the ggez documentation while following this tutorial.


Project setup

To get started, make a new folder and initialize a Rust project with cargo init. If you don't have rust installed, install it with rustup. If you're on Linux rustup is probably already in your distro's repositories.

After running cargo init, you should have a folder structure like this:

.
├── Cargo.lock
├── Cargo.toml
└── src
  └── main.rs

Cargo.toml contains the manifest info for your Rust crate (crate is Rustacean for package). Mine looks like this:

[package]
name = "pong_tutorial"
version = "0.1.0"
authors = ["Mikail Khan <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

The src/ directory is where all of your Rust code goes. main.rs comes with Hello World already written:

fn main() {
  println!("Hello, world!");
}

Run cargo build to build your project, and cargo run to run it. If you just want to check for project for syntax and type errors, run cargo check. It's a lot faster than cargo build.


ggez basics

Adding ggez as a dependency is pretty simple. Just go to the dependencies section of your Cargo.toml and add ggez = "0.5.1"

[package]
name = "pong_tutorial"
version = "0.1.0"
authors = ["Mikail Khan <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
ggez = "0.5.1"

Run cargo build now to download and build ggez and all of its dependencies. It might take a few minutes.

The behavior of the program hasn't actually changed at all yet. cargo run will still just output "Hello, World!".

ggez works by creating a graphics context and an event loop and then using a user defined struct with specific methods implemented to run the game. ggez comes with a specific function to make the actual game run. It has this signature:

pub fn run<S>(
  ctx: &mut Context, 
  events_loop: &mut EventsLoop, 
  state: &mut S
) -> GameResult where
  S: EventHandler,

What this tells us is that to get started we need:

  • [ ] A ggez Context
  • [ ] A ggez EventsLoop
  • [ ] A struct that implements EventHandler

We can actually make a Context and EventsLoop with a single step, so let's get that out of the way. Clear your main method and add this:

fn main() {
   let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Mikail Khan").build();
}

ggez nicely comes with a ContextBuilder to make a Context and EventsLoop. This is an example of the builder pattern in Rust.

ggez::ContextBuilder::new("Pong", "Mikail Khan") creates a ContextBuilder with title "Pong" and author "Mikail Khan". Running .build() on it creates a Context and EventsLoop. The &mut at the front makes sure that we get a mutable reference to the created values.

Next, we need a struct that implements ggez::event::EventHandler. For convenience, add use ggez::event::EventHandler to the start of the file. Next, make a struct MainState. It doesn't need to have anything for now.

struct MainState {
}

Next, we need to implement EventHandler for MainState. EventHandler is the trait that lets ggez know what to do with your struct to get the game going. It sets up the main loop and the draw loop for your game.

impl EventHandler for MainState {
}

If you run cargo check (or cargo build) now, you'll get an error:

error[E0046]: not all trait items implemented, missing: `update`, `draw`
  --> src/main.rs:20:1
   |
20 | impl EventHandler for MainState {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `update`, `draw` in implementation
   |
   = help: implement the missing item: `fn update(&mut self, _: &mut ggez::context::Context) -> std::result::Result<(), ggez::error::GameError> {
 todo!() }`
   = help: implement the missing item: `fn draw(&mut self, _: &mut ggez::context::Context) -> std::result::Result<(), ggez::error::GameError> { t
odo!() }`

This error tells us that we're missing two functions and gives us their signatures. If we follow its advice, we get:

impl EventHandler for MainState {
  fn update(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
    todo!()
  }

  fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
    todo!()
  }
}

todo!() is a macro which just exits the program with an error.

Now that we have the skeleton of a struct that implements EventHandler, we can complete our main method for now:

fn main() {
  // create a mutable reference to a `Context` and `EventsLoop`
  let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Fish").build().unwrap();

  // Make a mutable reference to `MainState`
  let main_state = &mut MainState {};

  // Start the game
  ggez::event::run(ctx, event_loop, main_state);
}

The program is now runnable! It doesn't do anything yet though. We get this error:

thread 'main' panicked at 'not yet implemented', src/main.rs:22:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

That's because ggez tried to run our update() method, which just has a todo!().

There's also one warning:

warning: unused `std::result::Result` that must be used
  --> src/main.rs:38:5
   |
38 |   ggez::event::run(ctx, event_loop, main_state);
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

This error tels us that ggez::event::run returns a Result, which might be an error. If it's an error, we should handle it. However, if this function fails, the program can't really recover. Also, it's going to be the last line in main(). To fix this, we can make main() return a Result. This is useful because if main() fails the error will just be printed when the program crashes. ggez::event::run() specifically returns a ggez::GameResult, so that's what we should make main() return.

Change the function signature to

fn main() -> ggez::GameResult

If we try to compile this (or cargo check it), we'll get another error:

error[E0308]: mismatched types
  --> src/main.rs:30:14
   |
30 | fn main() -> ggez::GameResult {
   |  ----    ^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found `()`
   |  |
   |  implicitly returns `()` as its body has no tail or `return` expression
...
38 |   ggez::event::run(ctx, event_loop, main_state);
   |                          - help: consider removing this semicolon
   |
   = note:   expected enum `std::result::Result<(), ggez::error::GameError>`
       found unit type `()`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

The Rust compiler helpfully tells us exactly what to do. If we remove the last semicolon on our ggez::event::run() call, main() will return it. Now, main() looks like this:

fn main() -> ggez::GameResult {
  // create a mutable reference to a `Context` and `EventsLoop`
  let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Fish").build().unwrap();

  // Make a mutable reference to `MainState`
  let main_state = &mut MainState {};

  // Start the game
  ggez::event::run(ctx, event_loop, main_state)
}

To stop the game from crashing immediately, we should replace the bodies of our update() and draw() functions with something other than todo!(). Since both of them also return a Result, we can make both of them just return Ok(()), which just means that there's been no errors.

impl EventHandler for MainState {
  fn update(&mut self, _: &mut ggez::Context) -> ggez::GameResult {
    Ok(())
  }

  fn draw(&mut self, _: &mut ggez::Context) -> ggez::GameResult {
    Ok(())
  }
}

Running the program now will create a blank window and do nothing. That's the start of the game!

You can find the full code so far on Github here.

If you have any feedback, please email me at [email protected]

I'll write Part 2 soon and link it here.

Part 2: https://mkhan45.github.io/2020/05/20/Pong-tutorial-with-ggez-Part-2.html