// 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 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 body = new List(); private readonly List segments = new List(); 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(); } }