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

// GDnD wiki example
// Demonstrates: building a row layout from data, mapping a y position to a lane type, and owning score/lives
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[csharp-enums]], [[unity-gamemanager-pattern]]
 
using UnityEngine;
 
// Builds the board from a list of lane definitions and answers the one question
// the frog keeps asking: "what kind of lane is at this height?". Row 0 (bottom)
// and the top row are always safe; the lanes in between are road or river. Also
// owns score and lives.
public class FroggerGame : MonoBehaviour
{
    public enum LaneType { Safe, Road, River }
 
    [System.Serializable]
    public struct LaneDef
    {
        public LaneType type;
        [Tooltip("Units per second. Sign sets direction; ignored for Safe lanes.")]
        public float speed;
        [Tooltip("Seconds between spawns; ignored for Safe lanes.")]
        public float spawnInterval;
    }
 
    [Header("Layout (bottom row up, excluding the safe start and goal rows)")]
    [SerializeField] private LaneDef[] lanes;
    [SerializeField] private float rowHeight = 0.9f;
 
    [Header("Prefabs")]
    [Tooltip("Hazard on the Vehicle layer.")]
    [SerializeField] private Hazard carPrefab;
    [Tooltip("Wide hazard on the Log layer.")]
    [SerializeField] private Hazard logPrefab;
 
    [SerializeField] private FrogController frog;
    [SerializeField] private int startingLives = 3;
 
    public float RowHeight => rowHeight;
    public float StartY { get; private set; }
    public float GoalY { get; private set; }
    public int Score { get; private set; }
    public int Lives { get; private set; }
 
    private int totalRows;
 
    private void Start()
    {
        Build();
    }
 
    private void Build()
    {
        // Safe start row + lanes + safe goal row, centred vertically on the camera.
        totalRows = lanes.Length + 2;
        StartY = -((totalRows - 1) * rowHeight) / 2f;
        GoalY = ((totalRows - 1) * rowHeight) / 2f;
 
        for (int i = 0; i < lanes.Length; i++)
        {
            int rowIndex = i + 1; // row 0 is the safe start
            float y = StartY + rowIndex * rowHeight;
 
            GameObject laneObject = new GameObject($"Lane {rowIndex} ({lanes[i].type})");
            laneObject.transform.SetParent(transform);
 
            Hazard prefab = lanes[i].type switch
            {
                LaneType.Road => carPrefab,
                LaneType.River => logPrefab,
                _ => null,
            };
 
            laneObject.AddComponent<Lane>().Configure(lanes[i].type, lanes[i].speed, lanes[i].spawnInterval, prefab, y);
        }
 
        Score = 0;
        Lives = startingLives;
        frog.Bind(this);
        frog.ResetToStart();
        Debug.Log($"Frogger ready. Lives {Lives}.");
    }
 
    // Round the y position to the nearest row and report that row's type.
    public LaneType GetLaneType(float y)
    {
        int index = Mathf.RoundToInt((y - StartY) / rowHeight);
        if (index <= 0 || index >= totalRows - 1)
        {
            return LaneType.Safe;
        }
 
        return lanes[index - 1].type;
    }
 
    public void OnFrogReachedGoal()
    {
        Score += 100;
        Debug.Log($"Home! Score {Score}");
        frog.ResetToStart();
    }
 
    public void OnFrogDied()
    {
        Lives--;
        Debug.Log($"Frog lost. Lives remaining: {Lives}");
 
        if (Lives <= 0)
        {
            Debug.Log($"Game over. Final score: {Score}");
            Score = 0;
            Lives = startingLives;
        }
 
        frog.ResetToStart();
    }
}