Game Development with Blazor

This outline explains how to create a simple game using Blazor. The game chosen for this example is a Pac-Man clone. However, the techniques and concepts discussed are applicable to a wide range of games. This outline explains the key features of the game, specifically focusing on how to handle user input, draw graphics, and implement game logic within a Blazor framework.

Game Loop

The game logic is driven by a game loop. The loop is responsible for updating the game state and redrawing the game screen at regular intervals. This is handled by Game.cs.

  • Updating the Game State: This involves updating the position of the Pac-Man, the ghosts, and the pellets based on their current state and user input.

  • Redrawing the Game Screen: This involves rendering the game elements on the screen, based on the updated game state.

// From Game.cs: https://github.com/stevedunn/pacmanblazor/blob/main/PacMan.Client/Pages/Game.razor.cs
          
          public class Game : ComponentBase
          {
              // ...
          
              protected override async Task OnAfterRenderAsync(bool firstRender)
              {
                  // ...
          
                  // Add a delay to let the browser catch up before starting the game loop.
                  await Task.Delay(100);
                  await StartGameLoop();
              }
          
              private async Task StartGameLoop()
              {
                  // ...
          
                  // Start the main game loop
                  while (!_isGameFinished)
                  {
                      // Update the game state
                      GameLogic.UpdateGame();
                      // Redraw the screen
                      StateHasChanged();
                      // Wait for a short period to control the game speed.
                      await Task.Delay(GameLogic.Speed);
                  }
              }
          
              // ...
          }
          

User Input

User input is handled through keyboard events. The Pac-Man’s movement is dictated by the user pressing the arrow keys. Game.razor handles the keyboard events.

// From Game.razor: https://github.com/stevedunn/pacmanblazor/blob/main/PacMan.Client/Pages/Game.razor
          
          <div class="container" @onkeydown="@HandleKeyDown" @onkeyup="@HandleKeyUp">
              <!--  ... -->
          </div>
          
          @code {
              // ...
              private void HandleKeyDown(KeyboardEventArgs e)
              {
                  // ...
                  if (e.Key == "ArrowUp" && !isGameFinished)
                  {
                      GameLogic.PacManDirection = Direction.Up;
                      // ...
                  }
                  // ...
              }
              private void HandleKeyUp(KeyboardEventArgs e)
              {
                  // ...
                  if (e.Key == "ArrowUp" && !isGameFinished)
                  {
                      // ...
                  }
                  // ...
              }
              // ...
          }
          

Graphics

Graphics are drawn using HTML Canvas. CanvasComponent.razor displays the game graphics.

  • Drawing Pac-Man: The Pac-Man is rendered as a circle, with its mouth opening and closing based on its direction of movement. This is accomplished through DrawPacMan.

  • Drawing the Ghosts: The ghosts are rendered as squares, with their colors representing their individual types. This is accomplished through DrawGhost.

  • Drawing the Maze: The maze is a set of lines and walls, forming a maze that Pac-Man and the ghosts must navigate. The maze is drawn based on the game level’s design. This is accomplished through DrawMaze.

  • Drawing the Pellets: The pellets are rendered as small circles, which Pac-Man must consume.

// From CanvasComponent.razor: https://github.com/stevedunn/pacmanblazor/blob/main/PacMan.Client/Pages/CanvasComponent.razor
          
          @page "/canvascomponent"
          
          <div class="container">
              <canvas id="canvas" height="400" width="400"></canvas>
          </div>
          
          @code {
              // ...
          
              protected override async Task OnAfterRenderAsync(bool firstRender)
              {
                  if (firstRender)
                  {
                      canvas = document.getElementById("canvas") as HTMLCanvasElement;
                      canvasContext = canvas.getContext("2d");
                      // ...
                      DrawGame();
                      // ...
                  }
                  // ...
              }
          
              private void DrawGame()
              {
                  if (canvasContext != null)
                  {
                      // ...
                      DrawMaze();
                      DrawPacMan();
                      DrawGhosts();
                      DrawPellets();
                      // ...
                  }
              }
              // ...
              private void DrawPacMan()
              {
                  // ...
                  // Draw Pac-Man's body as a circle
                  canvasContext.beginPath();
                  canvasContext.arc(GameLogic.PacMan.X, GameLogic.PacMan.Y, PacManRadius, 0, 2 * Math.PI);
                  // ...
              }
          
              private void DrawGhosts()
              {
                  // ...
                  foreach (var ghost in GameLogic.Ghosts)
                  {
                      // ...
                      canvasContext.fillStyle = ghost.Color;
                      canvasContext.fillRect(ghost.X, ghost.Y, GhostSize, GhostSize);
                      // ...
                  }
              }
          
              private void DrawMaze()
              {
                  // ...
                  // Draw each line/wall of the maze
                  foreach (var line in GameLogic.Maze.Lines)
                  {
                      // ...
                      canvasContext.beginPath();
                      canvasContext.moveTo(line.X1, line.Y1);
                      canvasContext.lineTo(line.X2, line.Y2);
                      // ...
                  }
              }
          
              private void DrawPellets()
              {
                  // ...
                  foreach (var pellet in GameLogic.Pellets)
                  {
                      // ...
                      canvasContext.beginPath();
                      canvasContext.arc(pellet.X, pellet.Y, PelletRadius, 0, 2 * Math.PI);
                      // ...
                  }
              }
              // ...
          }
          

Game Logic

GameLogic.cs handles the core game logic:

  • Pac-Man Movement: This includes moving Pac-Man in the direction specified by user input, preventing him from moving through walls and handling pellet consumption.

  • Ghost Movement: This includes controlling the ghosts’ movement patterns, making them chase Pac-Man, and handling their behavior when they are eaten.

  • Collision Detection: This determines when Pac-Man or the ghosts collide with walls, pellets, or each other.

  • Game State Management: This involves tracking the score, the number of lives, and the game level.

  • Game Over: This function ends the game when Pac-Man is caught by the ghosts.

// From GameLogic.cs: https://github.com/stevedunn/pacmanblazor/blob/main/PacMan.Client/Pages/GameLogic.cs
          
          public static class GameLogic
          {
              // ...
          
              public static void UpdateGame()
              {
                  // ...
                  UpdatePacMan();
                  UpdateGhosts();
                  CheckCollisions();
                  // ...
              }
          
              private static void UpdatePacMan()
              {
                  // ...
                  // Move Pac-Man based on his current direction
                  switch (PacManDirection)
                  {
                      case Direction.Up:
                          PacMan.Y -= Speed;
                          break;
                      case Direction.Down:
                          PacMan.Y += Speed;
                          break;
                      case Direction.Left:
                          PacMan.X -= Speed;
                          break;
                      case Direction.Right:
                          PacMan.X += Speed;
                          break;
                      // ...
                  }
                  // ...
              }
              
              private static void UpdateGhosts()
              {
                  // ...
                  // Move ghosts based on their individual AI.
                  foreach (var ghost in Ghosts)
                  {
                      // ...
                      switch (ghost.GhostType)
                      {
                          case GhostType.Blinky:
                              // ...
                              // Implementation for Blinky's movement
                              break;
                          case GhostType.Pinky:
                              // ...
                              // Implementation for Pinky's movement
                              break;
                          case GhostType.Inky:
                              // ...
                              // Implementation for Inky's movement
                              break;
                          case GhostType.Clyde:
                              // ...
                              // Implementation for Clyde's movement
                              break;
                      }
                      // ...
                  }
                  // ...
              }
          
              private static void CheckCollisions()
              {
                  // ...
                  // Check if Pac-Man collides with pellets
                  foreach (var pellet in Pellets)
                  {
                      // ...
                  }
                  // ...
                  // Check if Pac-Man collides with ghosts
                  foreach (var ghost in Ghosts)
                  {
                      // ...
                  }
                  // ...
                  // Check if ghosts collide with walls
                  foreach (var ghost in Ghosts)
                  {
                      // ...
                  }
                  // ...
              }
              // ...
          }
          

Game State

GameState.cs holds the game’s current state.

  • Pac-Man Position: The current position of Pac-Man on the game board.
  • Ghost Positions: The current positions of the ghosts on the game board.
  • Score: The player’s current score.
  • Lives: The number of lives remaining.
  • Level: The current game level.
  • IsGameFinished: A flag that indicates if the game has ended.
// From GameState.cs: https://github.com/stevedunn/pacmanblazor/blob/main/PacMan.Client/Pages/GameState.cs
          
          public static class GameState
          {
              public static PacMan PacMan { get; set; } = new PacMan();
          
              public static List<Ghost> Ghosts { get; set; } = new List<Ghost>()
              {
                  new Ghost { GhostType = GhostType.Blinky, X = 13.5 * TileSize, Y = 14.5 * TileSize, Color = "red" },
                  new Ghost { GhostType = GhostType.Pinky, X = 12.5 * TileSize, Y = 14.5 * TileSize, Color = "pink" },
                  new Ghost { GhostType = GhostType.Inky, X = 13.5 * TileSize, Y = 15.5 * TileSize, Color = "cyan" },
                  new Ghost { GhostType = GhostType.Clyde, X = 14.5 * TileSize, Y = 14.5 * TileSize, Color = "orange" }
              };
              // ...
              public static int Score { get; set; } = 0;
          
              public static int Lives { get; set; } = 3;
          
              public static int Level { get; set; } = 1;
          
              public static bool IsGameFinished { get; set; } = false;
          }