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