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;
}