// 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().Velocity.x; transform.position += Vector3.right * (carry * Time.deltaTime); float halfWidth = cachedCamera.orthographicSize * cachedCamera.aspect; if (Mathf.Abs(transform.position.x) > halfWidth) { game.OnFrogDied(); } } }