DEV Community

Trish
Trish

Posted on

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

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

πŸ”— Keep the conversation going on Twitter(X): @trish_07

πŸ”— 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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(())
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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(())
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Running the Game

To run the game:

cargo run
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)