Day 5 - 2D Game Renderer in Rust - Let's Make a Mini RPG!

Build your mini RPG with basic player movement, map rendering, and enemies using the ggez library. Learn game development fundamentals in Rust!


Introduction

Welcome to the world of game development in Rust! Today, we'll build a simple 2D RPG game renderer. We'll use ggez for our graphics needs, focusing on creating a game where players can move a character around a tile-based map, interact with objects, and perhaps even battle some basic enemies.

Difficulty

🌳 Intermediate-Advanced

GitHub Repository

πŸ”— Explore the 7Days7RustProjects Repository

Prerequisites

  • Intermediate Rust knowledge
  • Basic understanding of 2D graphics
  • Familiarity with game loop concepts

Project Structure

Let's organize our project:

mkdir rust-2d-rpg
cd rust-2d-rpg
cargo init --bin

Here’s our folder structure:

rust-2d-rpg/
β”‚
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.rs
β”‚   β”œβ”€β”€ game.rs
β”‚   β”œβ”€β”€ map.rs
β”‚   β”œβ”€β”€ player.rs
β”‚   β”œβ”€β”€ enemy.rs
β”‚   └── assets/
β”‚       β”œβ”€β”€ player.png
β”‚       β”œβ”€β”€ enemy.png
β”‚       └── tile.png
β”‚
β”œβ”€β”€ Cargo.toml
└── README.md

Step 1: Setting Up Cargo.toml

Add the following dependencies to your Cargo.toml:

[package]
name = "rust-2d-rpg"
version = "0.1.0"
edition = "2018"
 
[dependencies]
ggez = "0.6.1"
rand = "0.8.4"

Step 2: main.rs - The Entry Point

mod game;
mod map;
mod player;
mod enemy;
 
use ggez::{Context, ContextBuilder, GameResult};
use ggez::event::{self, EventHandler};
 
fn main() -> GameResult {
    let cb = ContextBuilder::new("rust_2d_rpg", "YourName")
        .window_setup(ggez::conf::WindowSetup::default().title("2D RPG in Rust"));
    let (mut ctx, event_loop) = cb.build()?;
    let game = game::Game::new(&mut ctx)?;
    event::run(ctx, event_loop, game)
}

Step 3: game.rs - Game Logic

use ggez::{Context, GameResult};
use ggez::event::{self, EventHandler};
use ggez::graphics;
 
pub struct Game {
    player: player::Player,
    map: map::Map,
    enemies: Vec<enemy::Enemy>,
}
 
impl Game {
    pub fn new(ctx: &mut Context) -> GameResult<Game> {
        let player = player::Player::new(ctx)?;
        let map = map::Map::new();
        let enemies = vec![enemy::Enemy::new(ctx)?];
        Ok(Game { player, map, enemies })
    }
}
 
impl EventHandler for Game {
    fn update(&mut self, _ctx: &mut Context) -> GameResult {
        // Update logic for player movement, enemy behavior, etc.
        Ok(())
    }
 
    fn draw(&mut self, ctx: &mut Context) -> GameResult {
        graphics::clear(ctx, [0.1, 0.2, 0.3, 1.0].into());
        self.map.draw(ctx)?;
        self.player.draw(ctx)?;
        for enemy in &self.enemies {
            enemy.draw(ctx)?;
        }
        graphics::present(ctx)?;
        Ok(())
    }
}

Step 4: player.rs - Player Management

use ggez::{Context, GameResult};
use ggez::graphics::{self, Rect};
use ggez::nalgebra as na;
 
pub struct Player {
    position: na::Point2<f32>,
}
 
impl Player {
    pub fn new(_ctx: &mut Context) -> GameResult<Player> {
        Ok(Player { position: na::Point2::new(100.0, 100.0) })
    }
 
    pub fn draw(&self, ctx: &mut Context) -> GameResult {
        let rectangle = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            Rect::new(self.position.x, self.position.y, 32.0, 32.0),
            graphics::Color::new(0.0, 1.0, 0.0, 1.0),
        )?;
        graphics::draw(ctx, &rectangle, (na::Point2::new(0.0, 0.0),))
    }
 
    // Add methods for movement, collision detection, etc.
}

Step 5: map.rs - World Generation

use ggez::{Context, GameResult};
use ggez::graphics;
 
pub struct Map {
    // Simplified map representation
    tiles: Vec<Vec<bool>>,
}
 
impl Map {
    pub fn new() -> Map {
        let tiles = vec![vec![true; 20]; 15];
        Map { tiles }
    }
 
    pub fn draw(&self, ctx: &mut Context) -> GameResult {
        let tile_size = 32.0;
        for (y, row) in self.tiles.iter().enumerate() {
            for (x, &tile) in row.iter().enumerate() {
                if tile {
                    let rectangle = graphics::Mesh::new_rectangle(
                        ctx,
                        graphics::DrawMode::fill(),
                        graphics::Rect::new((x as f32) * tile_size, (y as f32) * tile_size, tile_size, tile_size),
                        [0.5, 0.5, 0.5, 1.0].into(),
                    )?;
                    graphics::draw(ctx, &rectangle, (na::Point2::new(0.0, 0.0),))?;
                }
            }
        }
        Ok(())
    }
}

Step 6: enemy.rs - Enemies Management

use ggez::{Context, GameResult};
use ggez::graphics;
use ggez::nalgebra as na;
 
pub struct Enemy {
    position: na::Point2<f32>,
}
 
impl Enemy {
    pub fn new(ctx: &mut Context) -> GameResult<Enemy> {
        Ok(Enemy { position: na::Point2::new(200.0, 200.0) })
    }
 
    pub fn draw(&self, ctx: &mut Context) -> GameResult {
        let rectangle = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            graphics::Rect::new(self.position.x, self.position.y, 32.0, 32.0),
            graphics::Color::new(1.0, 0.0, 0.0, 1.0),
        )?;
        graphics::draw(ctx, &rectangle, (na::Point2::new(0.0, 0.0),))
    }
 
    // Add methods for enemy movement, AI, etc.
}

Step 7: Running the Game

To run the game:

cargo run

Explanation

  • Main Loop: In main.rs, we set up the ggez context and our game loop.
  • Game State: game.rs holds the overall game state, including the player, map, and enemies.
  • Graphics: Each component (Player, Map, Enemy) has its own draw method to handle rendering.
  • Expansion: This basic setup can be expanded with:
    • Player movement using keyboard input.
    • Collision detection between the player and map/enemies.
    • Basic AI for enemy movement or behavior.
    • Inventory system for items.
    • Combat mechanics or interaction with the environment.

Conclusion

You've now built a foundational 2D RPG game renderer in Rust! This project introduces you to game development concepts, graphics handling with ggez, and modular code organization.

Feel free to expand upon this base by:

  • Implementing player input for movement.
  • Adding different types of tiles or interactive objects.
  • Creating a combat system or quests.
  • Enhancing the game with sound or more complex graphics.

This project serves as an excellent platform for learning advanced Rust programming and game development principles. Keep exploring, and have fun creating your own game worlds!