Source file unity-csharp/arcade-classics/snake/SnakeGame.cs from the GDnD code examples. Download raw file

// GDnD wiki example
// Demonstrates: grid logic, a list-based snake body, timed stepping, growth, and self/wall collision — the step-3 Snake skills
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[csharp-collections]], [[monobehaviour-lifecycle]]
 
using System.Collections.Generic;
using UnityEngine;
 
// The whole game runs on a discrete grid, advanced on a fixed timer rather than
// every frame. This is the key shift from the Pong/Breakout examples: motion is
// now "one cell per step", which makes collision a list lookup instead of
// geometry. The body is a List<Vector2Int> with the head at index 0.
public class SnakeGame : MonoBehaviour
{
    [Header("Board (in cells)")]
    [SerializeField] private int gridWidth = 20;
    [SerializeField] private int gridHeight = 15;
    [SerializeField] private float cellSize = 0.5f;
 
    [Header("Prefabs")]
    [Tooltip("A square sprite for each body cell.")]
    [SerializeField] private Transform segmentPrefab;
    [Tooltip("A differently coloured square sprite for the food.")]
    [SerializeField] private Transform foodPrefab;
 
    [Header("Speed")]
    [SerializeField] private float stepInterval = 0.18f;
    [SerializeField] private float minStepInterval = 0.06f;
    [SerializeField] private float speedUpPerFood = 0.005f;
 
    [SerializeField] private SnakeInput input;
 
    public int Score { get; private set; }
 
    private readonly List<Vector2Int> body = new List<Vector2Int>();
    private readonly List<Transform> segments = new List<Transform>();
    private Vector2Int food;
    private Transform foodVisual;
    private float stepTimer;
    private float currentInterval;
    private bool isAlive;
 
    private void Start()
    {
        NewGame();
    }
 
    private void NewGame()
    {
        foreach (Transform segment in segments)
        {
            if (segment != null)
            {
                Destroy(segment.gameObject);
            }
        }
 
        segments.Clear();
        body.Clear();
 
        Score = 0;
        currentInterval = stepInterval;
        stepTimer = 0f;
        input.ResetDirection();
 
        // Start length 3, heading right (head first, tail to the left).
        Vector2Int start = new Vector2Int(gridWidth / 2, gridHeight / 2);
        body.Add(start);
        body.Add(start + Vector2Int.left);
        body.Add(start + Vector2Int.left * 2);
 
        if (foodVisual == null && foodPrefab != null)
        {
            foodVisual = Instantiate(foodPrefab, transform);
            foodVisual.localScale = Vector3.one * (cellSize * 0.9f);
        }
 
        SpawnFood();
        SyncVisuals();
        isAlive = true;
    }
 
    private void Update()
    {
        if (!isAlive)
        {
            return;
        }
 
        // Fixed-timestep stepping: accumulate real time, advance one cell per interval.
        stepTimer += Time.deltaTime;
        if (stepTimer >= currentInterval)
        {
            stepTimer -= currentInterval;
            Step();
        }
    }
 
    private void Step()
    {
        Vector2Int direction = input.ConsumeDirection();
        Vector2Int newHead = body[0] + direction;
 
        if (IsWall(newHead) || IsBody(newHead))
        {
            GameOver();
            return;
        }
 
        body.Insert(0, newHead);
 
        if (newHead == food)
        {
            Score++;
            currentInterval = Mathf.Max(minStepInterval, currentInterval - speedUpPerFood);
            SpawnFood();
            // Growth = keep the tail this step (do not remove the last cell).
        }
        else
        {
            body.RemoveAt(body.Count - 1);
        }
 
        SyncVisuals();
    }
 
    private bool IsWall(Vector2Int cell)
    {
        return cell.x < 0 || cell.y < 0 || cell.x >= gridWidth || cell.y >= gridHeight;
    }
 
    private bool IsBody(Vector2Int cell)
    {
        // Skip the last cell: the tail will vacate it this step (except when growing,
        // and then the head is on the food cell, never the tail).
        for (int i = 0; i < body.Count - 1; i++)
        {
            if (body[i] == cell)
            {
                return true;
            }
        }
 
        return false;
    }
 
    private void SpawnFood()
    {
        // Pick a random unoccupied cell. The guard avoids an infinite loop on a
        // nearly full board; fine for a learning-scale grid.
        Vector2Int cell;
        int guard = 0;
        do
        {
            cell = new Vector2Int(Random.Range(0, gridWidth), Random.Range(0, gridHeight));
            guard++;
        }
        while (body.Contains(cell) && guard < 1000);
 
        food = cell;
        if (foodVisual != null)
        {
            foodVisual.position = CellToWorld(cell);
        }
    }
 
    // Keep the pool of segment visuals the same size as the body, then place each.
    private void SyncVisuals()
    {
        while (segments.Count < body.Count)
        {
            Transform segment = Instantiate(segmentPrefab, transform);
            segment.localScale = Vector3.one * (cellSize * 0.9f);
            segments.Add(segment);
        }
 
        while (segments.Count > body.Count)
        {
            int last = segments.Count - 1;
            if (segments[last] != null)
            {
                Destroy(segments[last].gameObject);
            }
 
            segments.RemoveAt(last);
        }
 
        for (int i = 0; i < body.Count; i++)
        {
            segments[i].position = CellToWorld(body[i]);
        }
    }
 
    // Convert a grid cell to a world position, centred on this object.
    private Vector3 CellToWorld(Vector2Int cell)
    {
        float originX = -((gridWidth - 1) * cellSize) / 2f;
        float originY = -((gridHeight - 1) * cellSize) / 2f;
        return transform.position + new Vector3(originX + cell.x * cellSize, originY + cell.y * cellSize, 0f);
    }
 
    private void GameOver()
    {
        Debug.Log($"Game over. Length {body.Count}, score {Score}.");
        isAlive = false;
        NewGame();
    }
}