Snake Game
Team Size: 1 | Duration: 4 weeks
Classic Snake game with AI enemy snakes using A* pathfinding - Unity learning project
Project Overview
After learning C# development in CS3500, I decided to create a Snake game using Unity with a twist—every 3 powerups collected spawns an AI enemy snake that hunts the furthest powerup using A* pathfinding. Built as a Unity learning project to understand game loops, pathfinding algorithms, and dynamic object spawning.
Play it here: Unity WebGL Build
Features
- Player Snake Controls - Classic grid-based movement with tail growth
- AI Enemy Snakes - Spawn every 3 powerups and navigate using A* pathfinding
- Dynamic Obstacles - Random wall generation with collision-free spawn validation
- Progressive Difficulty - More walls spawn as score increases
- Smart Spawning - SpawnManager ensures powerups/walls don’t overlap with existing objects
A* Pathfinding Implementation
The AI enemy snakes use A* pathfinding to navigate to the furthest powerup from their spawn point. This creates interesting gameplay where the player must compete with intelligent enemies.
Why A*?
- Guarantees shortest path
- Efficient for grid-based navigation (O(N log N))
- Handles dynamic obstacles (walls marked as non-walkable)
/// <summary>
/// Find shortest path in grids from starting points to end points
/// path are calculated by following A* pathfinding algorithm
/// </summary>
/// <param name="startX"></param>
/// <param name="startY"></param>
/// <param name="endX"></param>
/// <param name="endY"></param>
/// <returns></returns>
public List<PathNode> FindPath(int startX, int startY, int endX, int endY)
{
PathNode startNode = grid.GetGridObject(startX, startY);
PathNode endNode = grid.GetGridObject(endX, endY);
openList = new List<PathNode>() { startNode };
closedList = new List<PathNode>();
for(int x = 0; x < grid.GetWidth(); x++)
{
for(int y = 0; y < grid.GetHeight(); y++)
{
PathNode pathNode = grid.GetGridObject(x, y);
pathNode.gCost = int.MaxValue;
pathNode.CalculateFCost();
pathNode.cameFromNode = null;
}
}
startNode.gCost = 0;
startNode.hCost = CalculateDistanceCost(startNode, endNode);
startNode.CalculateFCost();
while (openList.Count > 0)
{
PathNode currentNode = GetLowestFCostNode(openList);
if(currentNode == endNode)
{
return CalculatePath(endNode);
}
openList.Remove(currentNode);
closedList.Add(currentNode);
foreach(PathNode neighborNode in GetNeighborList(currentNode))
{
if (closedList.Contains(neighborNode)) continue;
if (!neighborNode.isWalkable)
{
closedList.Add(neighborNode);
continue;
}
int tentativeGCost = currentNode.gCost + CalculateDistanceCost(currentNode, neighborNode);
if(tentativeGCost < neighborNode.gCost)
{
neighborNode.cameFromNode = currentNode;
neighborNode.gCost = tentativeGCost;
neighborNode.hCost = CalculateDistanceCost(neighborNode, endNode);
neighborNode.CalculateFCost();
if (!openList.Contains(neighborNode))
{
openList.Add(neighborNode);
}
}
}
}
//No nodes left in openList
return null;
}
Collision-Free Spawn System
One key challenge was ensuring powerups and walls spawn in valid locations without overlapping. The SpawnManager uses sphere overlap detection with retry logic.
/// <summary>
/// Returns valid position doesn't overlap with other objects
/// if no valid location is found method will return Vector3.zero
/// </summary>
/// <returns></returns>
Vector3 FindValidSpawnPosition()
{
Vector3 spawnPos = Vector3.zero;
bool isValid = false;
int spawnAttempts = 0;
while (!isValid && spawnAttempts < maxSpawnAttemptsPerObstacle)
{
spawnAttempts++;
float spawnPosX = Random.Range(minBound + xOffset, maxBound - xOffset);
float spawnPosY = Random.Range(minBound + yOffset, maxBound - yOffset);
spawnPos = new Vector3(spawnPosX, spawnPosY, 0);
isValid = PreventSpawnOverLap(spawnPos, checkRadius);
}
return isValid ? spawnPos : Vector3.zero;
}
/// <summary>
/// Check if Desired Position will collides with other objects in the world
/// within given radius
/// </summary>
/// <param name="spawnPos">Target Position</param>
/// <param name="checkRadius">Radius to check for other objects</param>
/// <returns></returns>
bool PreventSpawnOverLap(Vector3 spawnPos, float checkRadius)
{
Collider[] colliders = Physics.OverlapSphere(spawnPos, checkRadius);
foreach (Collider collider in colliders)
{
if (collider.tag == Tags.WALL || collider.tag == Tags.POWERUP
|| collider.tag == Tags.SNAKE || collider.tag == Tags.TAIL)
{
return false;
}
}
return true;
}
Key Features:
- Maximum spawn attempts to prevent infinite loops
- Radius-based collision checking
- Grid-aligned positioning
What I Learned
Unity Fundamentals
- Game loop architecture and Update() / FixedUpdate() timing
- Prefab instantiation and GameObject management
- Collision detection using Physics.OverlapSphere
Design Patterns
- Singleton Pattern for GameManager and SpawnManager
- Component-based architecture separating logic (Snake, EnemySnake)
- Event-driven spawning using InvokeRepeating for timed powerup generation
Algorithms & Data Structures
- A* pathfinding with heuristic cost estimation
- Generic
Grid<T>system for reusable grid logic - PathNode class with f/g/h cost calculations
C# Programming
- Coroutines for countdown timers
- List and Queue data structures for pathfinding
- Lambda expressions in grid initialization
Technical Challenges
- Grid Alignment - Converting world positions to grid coordinates for pathfinding
- Dynamic Pathfinding - Updating walkable nodes as walls spawn during gameplay
- Enemy Spawn Logic - Selecting spawn points that don’t immediately collide with player