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();
}
}