Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Drawing the Game Pieces

Recommended resources

Hello! Welcome back to our chess project.

In the last lesson, we successfully loaded all 12 of our chess piece images into a PieceTextures struct, preparing them for action. Our board is drawn, and our sprites are ready in memory.

Today, we'll finally bring the two together. Our goal is to render the piece sprites on the board according to the current game state. This means we'll read our game's data model—the 8x8 array representing the board—and draw the correct piece image on the correct square. By the end of this lesson, you'll see a fully set-up chessboard on your screen, ready for the first move.

1. Re-establishing the Game State

To draw the pieces, we first need to know where they are. In previous modules, we designed the data structures for our game state. Now it's time to integrate them into our graphical application.

Let's add the necessary structs and enums to main.rs. We'll also include the initialize_board function you designed earlier, which sets up the pieces in their standard starting positions.

Place the following code in your src/main.rs file, outside the main function. This provides the data model our drawing logic will depend on.

// --- Data model from previous modules ---

// Using some traits we will cover in more detail later.
// For now, they let us print and compare these structures easily.



#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceColor {
    White,
    Black,
}




#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceKind {
    Pawn,
    Rook,
    Knight,
    Bishop,
    Queen,
    King,
}




#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Piece {
    pub kind: PieceKind,
    pub color: PieceColor,
}

pub struct GameState {
    // An 8x8 array of optional pieces. None means the square is empty.
    pub board: [[Option<Piece>; 8]; 8],
}

impl GameState {
    // Function to initialize the board to the standard chess starting position
    pub fn new() -> Self {
        let mut board = [[None; 8]; 8];

        // Helper macro to create a piece
        macro_rules! piece {
            ($kind:ident, $color:ident) => {
                Some(Piece {
                    kind: PieceKind::$kind,
                    color: PieceColor::$color,
                })
            };
        }

        // Pawns
        for file in 0..8 {
            board[1][file] = piece!(Pawn, Black);
            board[6][file] = piece!(Pawn, White);
        }

        // Rooks
        board[0][0] = piece!(Rook, Black);
        board[0][7] = piece!(Rook, Black);
        board[7][0] = piece!(Rook, White);
        board[7][7] = piece!(Rook, White);

        // Knights
        board[0][1] = piece!(Knight, Black);
        board[0][6] = piece!(Knight, Black);
        board[7][1] = piece!(Knight, White);
        board[7][6] = piece!(Knight, White);

        // Bishops
        board[0][2] = piece!(Bishop, Black);
        board[0][5] = piece!(Bishop, Black);
        board[7][2] = piece!(Bishop, White);
        board[7][5] = piece!(Bishop, White);

        // Queens
        board[0][3] = piece!(Queen, Black);
        board[7][3] = piece!(Queen, White);

        // Kings
        board[0][4] = piece!(King, Black);
        board[7][4] = piece!(King, White);

        GameState { board }
    }
}

With these definitions in place, you can now create an instance of your game state in your main function, right after loading the textures:

// In main(), after build_textures_atlas();
let game_state = GameState::new();

2. The Drawing Loop: From Data to Pixels

Our core task is to loop through every square of the game_state.board array. If a square contains a Piece, we'll draw the corresponding texture at that location.

This involves two main challenges:

  1. Coordinate Translation: Converting board indices (rank, file) into screen pixels (x, y).
  2. Texture Selection: Choosing the correct Texture2D from our PieceTextures struct based on the piece's kind and color.

Coordinate Translation

This is straightforward. For a square at (rank, file), the top-left corner's screen position is:

  • x = file * SQUARE_SIZE
  • y = rank * SQUARE_SIZE

Texture Selection

This is a perfect use case for a match expression. We can write a helper function that takes a &Piece and our &PieceTextures and returns the correct texture. This keeps our main drawing logic clean.

Add this helper function to your main.rs file:

// Helper function to get the correct texture for a given piece
fn get_texture_for_piece<'a>(piece: &Piece, textures: &'a PieceTextures) -> &'a Texture2D {
    match piece.kind {
        PieceKind::Pawn => match piece.color {
            PieceColor::White => &textures.white_pawn,
            PieceColor::Black => &textures.black_pawn,
        },
        PieceKind::Rook => match piece.color {
            PieceColor::White => &textures.white_rook,
            PieceColor::Black => &textures.black_rook,
        },
        PieceKind::Knight => match piece.color {
            PieceColor::White => &textures.white_knight,
            PieceColor::Black => &textures.black_knight,
        },
        PieceKind::Bishop => match piece.color {
            PieceColor::White => &textures.white_bishop,
            PieceColor::Black => &textures.black_bishop,
        },
        PieceKind::Queen => match piece.color {
            PieceColor::White => &textures.white_queen,
            PieceColor::Black => &textures.black_queen,
        },
        PieceKind::King => match piece.color {
            PieceColor::White => &textures.white_king,
            PieceColor::Black => &textures.black_king,
        },
    }
}

3. Rendering with draw_texture_ex

In the last lesson, we used draw_texture for testing. However, our piece images might not be the exact size of our board squares. To ensure they fit perfectly, we should use draw_texture_ex. This function allows us to specify drawing parameters, including the destination size.

[PDF] PDF book - Game development in Rust with Macroquad - Agical.se

The game development guide we've been using has a great, simple example of draw_texture_ex. Let's look at it to see how to control the size of a rendered sprite.

Read the subsection named 'Draw the spaceship frames'. It shows how to use draw_texture_ex with a DrawTextureParams struct. Pay close attention to the dest_size field, which is exactly what we need to make our piece sprites fit our board squares perfectly.

As the resource demonstrates, we can pass a DrawTextureParams struct to draw_texture_ex. By setting the dest_size field, we tell macroquad to scale the image to our desired dimensions, which in our case is SQUARE_SIZE for both width and height.


4. Putting It All Together

Now we can write the final drawing logic. In your main function's loop, replace the test drawing code from the last lesson with a new draw_pieces function call.

First, here's the function that will contain our drawing loop. Add it to your main.rs file.

// New function to draw all the pieces on the board
fn draw_pieces(game_state: &GameState, textures: &PieceTextures, square_size: f32) {
    for rank in 0..8 {
        for file in 0..8 {
            // Check if there is a piece on this square
            if let Some(piece) = game_state.board[rank][file] {
                // Select the correct texture
                let texture = get_texture_for_piece(&piece, textures);

                // Convert board coordinates to screen coordinates
                let x = file as f32 * square_size;
                let y = rank as f32 * square_size;

                // Draw the texture, scaled to fit the square
                draw_texture_ex(
                    texture,
                    x,
                    y,
                    WHITE,
                    DrawTextureParams {
                        dest_size: Some(vec2(square_size, square_size)),
                        ..Default::default()
                    },
                );
            }
        }
    }
}

Finally, update your main function's loop to call this new function. Your loop should now look like this:

// In main()



#[macroquad::main("Chess")]
async fn main() {
    // ... (constants like SQUARE_SIZE, RANKS, FILES)
    // ... (asset folder setup and texture loading)
    
    let game_state = GameState::new();

    loop {
        clear_background(BLACK);

        // 1. Draw the board background (from the previous graphics lesson)
        draw_board(RANKS, FILES, SQUARE_SIZE); // Assuming you have this function

        // 2. Draw the pieces on top
        draw_pieces(&game_state, &textures, SQUARE_SIZE);

        next_frame().await;
    }
}

Make sure to remove any old draw_texture test calls. When you cargo run now, you should see your complete chessboard with all pieces in their starting positions!

A full chessboard with all 32 pieces rendered as sprites in their starting positions.

Your game should now look like this: a complete, visually rendered starting position.


Conclusion

Excellent work! You have now connected the game's abstract data model to the visual representation. This is a huge milestone in any game development project. The board is no longer an empty grid; it's a living game state rendered on screen.

Key Takeaways:

  • A dedicated drawing function (like draw_pieces) helps keep your main loop clean and organized.
  • You must translate from game logic coordinates (like a grid index) to rendering coordinates (pixels).
  • match statements are a powerful and idiomatic way in Rust to handle logic based on enum variants, like selecting the right texture for a piece.
  • draw_texture_ex gives you fine-grained control over rendering, such as scaling a sprite to a specific dest_size.

Preview of the Next Lesson

Our board looks great, but it's just a static image. The next step is to make it interactive. In our next lesson, we will begin building the user interface by learning how to detect mouse clicks and map the screen coordinates to board squares. This will allow us to know which square the player is clicking on, paving the way for piece selection and movement.