This project is a maze labyrinth game implemented in Unity. Players can generate a random maze and visualize the solution path using an A * pathfinding algorithm. The game showcases clean architecture with:
- Event-driven design via a custom Event Bus framework (
SimpleBus). - Manual dependency injection from a single entry point.
- Configurable path visualization with step-trail rendering.
- Random Maze Generation: Procedurally generates a new maze layout every time the player clicks the "Generate Map" button.
- A Pathfinding: Calculates the shortest path from the player to the treasure, using a configurable node grid.
- Path Visualization: Draws the path using a step-based trail for easy readability, independent of the maze node precision.
- Event Bus System: Clean decoupled architecture using a custom event bus framework (
SimpleBus) to manage game events. - Manual Dependency Injection: All components are injected from a single entry point (
MazeMatchStart), keeping the system modular and testable. - The project leverages interface segregation to decouple different responsibilities, making it easy to swap implementations for different parts of the system:
InitializeGridAsync()sets up the pathfinding grid.FindSolutionAsync()finds the path from start to goal.IsGeneratingPathindicates if a path is currently being computed.
- Provides start and goal positions via:
GetStartPosition()GetEndGoalPosition()
- Exposes:
PlayerPositionTreasurePosition
SpawnEntities(IMazeGenerator mazeGenerator)places the player and treasure in the maze.
- Sets up all core systems:
GridPathBuilder,PathDrawer,EntitySpawner,GridPathFinder, andMazeGenerator. - Registers UI buttons to raise events (
MapGeneratedEvent,ShowMazePathEvent). - Uses the Event Bus to notify subscribers when to generate a new maze or show the solution path.
/// <summary>
/// Single Entry Point layer of the maze test, Manual injector.
/// </summary>
public class MazeMatchStart : MonoBehaviour
{
[Header("References")]
[SerializeField] private PathDrawerConfig pathDrawerConfig;
[SerializeField] private GameObject playerPrefab;
[SerializeField] private GameObject treasurePrefab;
[Header("Maze Data")]
[SerializeField] private MazeNode _mazeNodePrefab;
[SerializeField] private LayerMask _wallMask;
[Header("Grid Data")]
[SerializeField] private Vector2Int _gridWorldSize;
[Header("GUI")]
[SerializeField] private Button generateButton;
[SerializeField] private Button showPathButton;
private const float NODE_RADIUS = 0.1f;
private IPathFinder _pathFinder;
private IEntitySpawner _entitySpawner;
private IMazeGenerator _mazeGenerator;
private PathDrawer _pathDrawer;
private readonly MapGeneratedEvent _mapGenerated = new();
private readonly ShowMazePathEvent _showMazePath = new();
void Start()
{
SetBehavioralComponents();
InjectListeners();
}
void SetBehavioralComponents()
{
var grid = new GridPathBuilder(_wallMask, _gridWorldSize, NODE_RADIUS, transform);
_pathDrawer = new PathDrawer(pathDrawerConfig);
_entitySpawner = new EntitySpawner(playerPrefab, treasurePrefab);
_pathFinder = new GridPathFinder(grid, _pathDrawer, _entitySpawner);
_mazeGenerator = new MazeGenerator(_pathFinder, _entitySpawner, _gridWorldSize, _mazeNodePrefab);
}
void InjectListeners()
{
generateButton.onClick.AddListener(GenerateMap);
showPathButton.onClick.AddListener(ShowPath);
}
void GenerateMap()
{
EventBus<MapGeneratedEvent>.Raise(_mapGenerated);
}
void ShowPath()
{
EventBus<ShowMazePathEvent>.Raise(_showMazePath);
}
}- The
PathDrawertakes the A* path and draws it in the scene using cubes. - Supports configurable visual scale and node skipping to render a clear step-trail.
- Uses
ObjectPool<GameObject>for performance, avoiding frequent instantiation/destruction.
public class GridPathFinder : IPathFinder
{
private AStar _aStar = new();
private PathDrawer _pathDrawer;
private GridPathBuilder _gridBuilder;
private IEntitySpawner _spawner;
private EventListener<ShowMazePathEvent> _showMazePathListener;
public bool IsGeneratingPath { get; private set; }
public GridPathFinder(GridPathBuilder gridBuilder, PathDrawer pathDrawer, IEntitySpawner spawner)
{
_gridBuilder = gridBuilder;
_pathDrawer = pathDrawer;
_spawner = spawner;
RegisterEvents();
}
public async Task InitializeGridAsync()
{
IsGeneratingPath = true;
_pathDrawer.ClearPath();
// Simulate async grid creation without blocking
await Task.Yield();
_gridBuilder.CreateGrid();
IsGeneratingPath = false;
}
public async Task<List<Node>> FindSolutionAsync()
{
Vector3 start = _spawner.PlayerPosition;
Vector3 end = _spawner.TreasurePosition;
var finalPath = _aStar.FindPathSolution(start, end, _gridBuilder);
_pathDrawer.DrawPath(finalPath);
await Task.Yield(); // simulate async
return finalPath;
}
void RegisterEvents()
{
_showMazePathListener = new EventListener<ShowMazePathEvent>(() =>
{
_ = FindSolutionAsync().ContinueWith(t =>
{
if (t.Exception != null)
Debug.LogException(t.Exception);
});
});
EventBus<ShowMazePathEvent>.Register(_showMazePathListener);
}
}- The game uses a generic event listener (
EventBinder<T>/EventListener<T>) to handle events. - Components subscribe to events without tight coupling, allowing modular updates and easy testing.
_mapGeneratedBinder = new EventListener<MapGeneratedEvent>(Generate);
EventBus<MapGeneratedEvent>.Register(_mapGeneratedBinder);- Click Generate Map to create a new maze.
- Click Show Path Solution to visualize the A* path from the player to the treasure.
- Observe the path trail as a sequence of steps.
- Unity 2021+
- No external packages; all pathfinding and event system implemented in project.
- Possibly convert it into a full game with procedural levels
- Add better backgrounds and gimmmicks if I get to do it a mobile title.
