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

// GDnD wiki example
// Demonstrates: discrete grid hopping with boundary clamping, plus per-frame lane evaluation (squash, drown, ride)
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-input]], [[unity-collider2d-and-triggers]]
 
using UnityEngine;
 
// The frog hops one cell per key press (grid movement) but the world around it
// moves continuously. Each frame it asks the game what kind of lane it is on and
// resolves the consequences: a road lane can squash it, a river lane drowns it
// unless it is standing on a log — in which case the log carries it sideways.
public class FrogController : MonoBehaviour
{
    [SerializeField] private float columnStep = 0.8f;
    [SerializeField] private Vector2 hitBox = new Vector2(0.4f, 0.4f);
    [SerializeField] private float horizontalMargin = 0.4f;
 
    [Header("Layers")]
    [SerializeField] private LayerMask vehicleMask;
    [SerializeField] private LayerMask logMask;
 
    private FroggerGame game;
    private Camera cachedCamera;
 
    public void Bind(FroggerGame owner)
    {
        game = owner;
        cachedCamera = Camera.main;
    }
 
    public void ResetToStart()
    {
        transform.position = new Vector3(0f, game.StartY, 0f);
    }
 
    private void Update()
    {
        HandleHop();
        EvaluateLane();
    }
 
    private void HandleHop()
    {
        Vector3 position = transform.position;
 
        // GetKeyDown gives one discrete hop per press — the grid-movement feel.
        if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
        {
            position.y = Mathf.Min(position.y + game.RowHeight, game.GoalY);
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
        {
            position.y = Mathf.Max(position.y - game.RowHeight, game.StartY);
        }
        else if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
        {
            position.x -= columnStep;
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
        {
            position.x += columnStep;
        }
 
        // Level boundary: clamp the frog inside the screen on the x axis.
        float halfWidth = cachedCamera.orthographicSize * cachedCamera.aspect - horizontalMargin;
        position.x = Mathf.Clamp(position.x, -halfWidth, halfWidth);
        transform.position = position;
    }
 
    private void EvaluateLane()
    {
        // Reaching the top row is a win.
        if (transform.position.y >= game.GoalY - 0.01f)
        {
            game.OnFrogReachedGoal();
            return;
        }
 
        switch (game.GetLaneType(transform.position.y))
        {
            case FroggerGame.LaneType.Road:
                if (Physics2D.OverlapBox(transform.position, hitBox, 0f, vehicleMask) != null)
                {
                    game.OnFrogDied();
                }
                break;
 
            case FroggerGame.LaneType.River:
                RideOrDrown();
                break;
        }
    }
 
    private void RideOrDrown()
    {
        Collider2D log = Physics2D.OverlapBox(transform.position, hitBox, 0f, logMask);
        if (log == null)
        {
            game.OnFrogDied(); // in the water with nothing under us
            return;
        }
 
        // Carried by the log. Drifting off the screen edge is fatal.
        float carry = log.GetComponent<Hazard>().Velocity.x;
        transform.position += Vector3.right * (carry * Time.deltaTime);
 
        float halfWidth = cachedCamera.orthographicSize * cachedCamera.aspect;
        if (Mathf.Abs(transform.position.x) > halfWidth)
        {
            game.OnFrogDied();
        }
    }
}