Snake Game

Team Size: 1 | Duration: 4 weeks

Solo game project to learn Unity

Unity
C#
Git

Things I learned

  • How to create a object and use script to move objects
  • Random wall spawns
  • A* path finding algorithm

View full project code


A* path finding alogorithm

  • Every 3 power-ups obtained, the game spawns an AI snake that moves to the furthest power-up from the spawn point. The shortest path is calculated using the A* pathfinding algorithm.

Random wall spawns

  • Walls and power-ups are spawned randomly. I created a spawn manager class that ensures the spawning locations are safe.
  • The SpawnManager is called at the beginning and during the game, handling object spawning.
public class SpawnManager : MonoBehaviour
{
    public static SpawnManager Instance { get; private set; }
    Pathfinding pathfinding;
    private int cellSize = 1;
    [Header("SpawnSettings")]
    private float minBound = 0.5f;
    private float maxBound = 39.5f;
    private float spawnDelay = 2.0f;
    private float maxPowerups = 5;
    private int maxWalls = 10;
    private float repeat = 1.0f;
    private float[] yAngles = { 0, 180 };
    private float[] zAngles = { 0, 90, 270 };
    private float spawnProbablility = 0.25f;
    private int powerupCount = 0;
    private int maxSpawnAttemptsPerObstacle = 20;
    private List<Vector3> enemySpawnPos;

    [Header("Prefabs")]
    public GameObject powerupPrefab;
    public GameObject defaultWallPrefab;
    public GameObject[] wallPrefabs;
    public GameObject SnakePrefab;
    public GameObject EnemyPrefab;

    // Start is called before the first frame update
    void Awake()
    {
        Instance = this;
        pathfinding = new Pathfinding(40, 40, cellSize);
        InitiateWalls();
        enemySpawnPos = new List<Vector3>()
        {
            new Vector3(36, 3, 0),
            new Vector3(36, 36, 0),
            new Vector3(3, 3, 0),
            new Vector3(3, 36, 0),
        };
    }

    public void Start()
    {
        //CountForThreeSeconds();
        SpawnSnake();
        InvokeRepeating("SpawnPowerups", spawnDelay, repeat);
    }

    IEnumerator CountForThreeSeconds()
    {
        yield return new WaitForSeconds(3);
    }


    // Update is called once per frame
    void Update()
    {
        CheckWallPositionInGrid();
        powerupCount = CheckPowerupCount();
    }


    /// <summary>
    /// Check wall position and set corresponding PathNode to false
    /// so the wall is excluded from path
    /// </summary>
    private void CheckWallPositionInGrid()
    {
        GameObject[] walls = GameObject.FindGameObjectsWithTag(Tags.WALL);
        if (walls.Length > 0)
        {
            foreach (GameObject wall in walls)
            {
                pathfinding.GetNode(wall.transform.position).SetIsWalkable(false);
            }
        }
    }

    /// <summary>
    /// Check how many Powerups in the world
    /// </summary>
    /// <returns></returns>
    private int CheckPowerupCount()
    {
        GameObject[] powerups = GameObject.FindGameObjectsWithTag(Tags.POWERUP);
        return powerups.Length;
    }


    /// <summary>
    /// Spawn powerup in the location doesn't overlap with other objects
    /// when powerups is less than Max Powerups 
    /// </summary>
    void SpawnPowerups()
    {
        Vector3 spawnPos = FindValidSpawnPosition();

        if (spawnPos != Vector3.zero && powerupCount < maxPowerups)
        {
            Instantiate(powerupPrefab, RoundToGrid(spawnPos), Quaternion.identity);
        }
    }

    /// <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 + 5.0f, maxBound - 5.0f);
            float spawnPosY = Random.Range(minBound + 5.0f, maxBound - 5.0f);
            spawnPos = new Vector3(spawnPosX, spawnPosY, 0);
            isValid = PreventSpawnOverLap(spawnPos, 3.0f);
        }
        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;
    }

    /// <summary>
    /// Convert WorldPosition to GridPosition
    /// </summary>
    /// <param name="position"></param>
    /// <returns></returns>
    private Vector3 RoundToGrid(Vector3 position)
    {
        float gridSize = pathfinding.GetGrid().GetCellSize();
        float x = Mathf.Round(position.x / gridSize) * gridSize;
        float y = Mathf.Round(position.y / gridSize) * gridSize;
        return new Vector3(x, y, position.z) + Vector3.one * 0.5f;
    }

    /// <summary>
    /// Initiate Outside walls and random walls
    /// </summary>
    public void InitiateWalls()
    {
        //Building side walls
        for (int i = 0; i < maxWalls; i++)
        {
            float randomValue = Random.Range(0f, 1f);
            if (randomValue >= spawnProbablility)
            {
                Instantiate(defaultWallPrefab, new Vector3(maxBound, maxBound - (4 * i), 0), Quaternion.identity);
                Instantiate(defaultWallPrefab, new Vector3(-minBound + 1.0f, maxBound - (4 * i), 0), Quaternion.identity);
            }
        }
        //Building Top and Bottom Walls
        for (int i = 0; i < maxWalls; i++)
        {
            float randomValue = Random.Range(0f, 1f);
            if (randomValue >= spawnProbablility)
            {
                Instantiate(defaultWallPrefab, new Vector3(minBound + (4 * i), minBound, 0), Quaternion.Euler(0, 0, 90));
                Instantiate(defaultWallPrefab, new Vector3(minBound + (4 * i), maxBound, 0), Quaternion.Euler(0, 0, 90));
            }
        }
        //Generating Random Walls
        for (int i = 0; i < maxWalls; i++)
        {
            Vector3 spawnPos = FindValidSpawnPosition();

            if (spawnPos != Vector3.zero)
            {
                int wallIndex = Random.Range(0, wallPrefabs.Length);
                int yAngleIndex = Random.Range(0, yAngles.Length);
                int zAngleIndex = Random.Range(0, zAngles.Length);
                Instantiate(wallPrefabs[wallIndex], RoundToGrid(spawnPos), Quaternion.Euler(0, yAngles[yAngleIndex], zAngles[zAngleIndex]));
            }
        }
    }

    /// <summary>
    /// Spawn Snake in location doesn't overlap with other objects
    /// </summary>
    public void SpawnSnake()
    {
        Vector3 spawnPos = FindValidSpawnPosition();

        if (spawnPos != Vector3.zero)
        {
            Instantiate(SnakePrefab,(spawnPos), Quaternion.identity);
        }
        else
        {
            //Just in case, set default location
            Instantiate(SnakePrefab, new Vector3 (3, 3, 0), Quaternion.identity);
        }
    }

    /// <summary>
    /// Spwn a wall at random location
    /// </summary>
    public void SpawnAWall()
    {
        Vector3 spawnPos = FindValidSpawnPosition();

        if (spawnPos != Vector3.zero)
        {
            int wallIndex = Random.Range(0, wallPrefabs.Length);
            int yAngleIndex = Random.Range(0, yAngles.Length);
            int zAngleIndex = Random.Range(0, zAngles.Length);
            Instantiate(wallPrefabs[wallIndex], RoundToGrid(spawnPos), Quaternion.Euler(0, yAngles[yAngleIndex], zAngles[zAngleIndex]));
        }
    }

    /// <summary>
    /// Spawn EnemySnake that will move towards furthest powerups positions
    /// EnemySnake will spawn will be randomly selected from 4 different locations in enemySpawnPos
    /// </summary>
    public void SpawnEnemySnake()
    {
        int randomDir = Random.Range(0, enemySpawnPos.Count);
        GameObject enemySnake = Instantiate(EnemyPrefab, Vector3.zero, Quaternion.identity);
        if (randomDir == 2 || randomDir == 3)
        {
            enemySnake.transform.rotation = Quaternion.Euler(0, 180, 0);
        }
        Vector3 gridPosition = RoundToGrid(enemySpawnPos[randomDir]);
        enemySnake.transform.position = gridPosition;
        EnemySnake enemyScript =  enemySnake.GetComponent<EnemySnake>();
        Vector3 targetPos = FurthestPowerupPosition(gridPosition);
        enemyScript.Path = pathfinding.FindPath(gridPosition, targetPos);
    }

    /// <summary>
    /// Returns the Position of furthest powerups at the moment
    /// </summary>
    /// <param name="pos"></param>
    /// <returns></returns>
    Vector3 FurthestPowerupPosition(Vector3 pos)
    {
        GameObject[] powerups = GameObject.FindGameObjectsWithTag("Powerup");
        float maxDistance = Mathf.NegativeInfinity;
        Vector3 targetPos = Vector3.zero;
        foreach(GameObject powerup in powerups)
        {
            float distance = (Vector3.Distance(pos, powerup.transform.position));
            if(distance > maxDistance)
            {
                maxDistance = distance;
                targetPos = powerup.transform.position;
            }
         }
        return targetPos;
    }
}

Reference

A* pathfinding in Unity by Code Monkey