Snake Game

Team Size: 1 | Duration: 4 weeks

Classic Snake game with AI enemy snakes using A* pathfinding - Unity learning project

Unity
C#
git

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

  1. Grid Alignment - Converting world positions to grid coordinates for pathfinding
  2. Dynamic Pathfinding - Updating walkable nodes as walls spawn during gameplay
  3. Enemy Spawn Logic - Selecting spawn points that don’t immediately collide with player

Reference

A* pathfinding in Unity by Code Monkey