DEV Community

unity source code
unity source code

Posted on

Building a Parking Puzzle in Unity: A Systems Breakdown of the Park Match Mechanic

Parking and matching puzzles look almost insultingly simple from the outside. A few cars, a cramped lot, tap or drag to move them out. But if you've actually tried to build one that feels tight — no janky collision resolution, no ambiguous "why didn't that move register" moments, no lag once the board gets crowded — you know there's a real systems design problem hiding underneath what looks like a weekend project.

I recently went through the architecture of a parking/matching hybrid template built in Unity and wanted to break down the core systems the way I'd want them explained if I were reskinning or extending one myself. This isn't a marketing post — it's a walkthrough of the actual mechanics: how vehicle movement and collision resolution work, how the match/clear pipeline is structured, how level data is separated from movement logic so hundreds of levels don't require touching code, and how monetization hooks slot into natural break points without polluting gameplay scripts.

If you want to see the finished product this breakdown is loosely based on, there's a working template here: Park Match Unity Game Template. Everything below applies whether you're building something similar from scratch or extending an existing base.

Why Parking Puzzles Are Harder Than They Look

The core loop — tap or drag a vehicle, it exits along a valid path, the lot clears one piece at a time — is trivial to describe and genuinely fiddly to implement well. Four problems show up almost immediately once you move past a static mockup:

  1. Valid-move detection: how do you know, at any given moment, which vehicles can actually move given their orientation and the current board state?
  2. Path resolution: once a vehicle starts moving, how does it navigate around other vehicles and obstacles without clipping through them or getting stuck mid-animation?
  3. Match/clear logic: when does a vehicle actually "clear" the board — on reaching an exit, on matching color/type with another vehicle, or both — and how do you avoid double-triggering a clear when two systems could plausibly fire at once?
  4. Level authoring at scale: since this genre lives or dies on having dozens or hundreds of well-tuned layouts, how do you avoid a level being a hardcoded scene that takes as long to build as a whole new feature?

Get any of these wrong and the game either feels unresponsive (taps that don't register because the valid-move check was too conservative), visually broken (vehicles overlapping mid-animation), or becomes a nightmare to scale past a dozen hand-built levels — all of which show up fast in churn and review scores for casual mobile puzzles.

System 1: Valid-Move Detection

Before a vehicle can move, the game needs to know whether a clear path exists in the direction that vehicle is oriented. The cleanest way to handle this is to keep the board as a simple 2D grid of cell occupancy, independent of the visual transform positions of the vehicles themselves.

public class ParkingGrid
{
    private readonly int[,] occupancy; // -1 = empty, otherwise vehicleId
    private readonly int width, height;

    public ParkingGrid(int w, int h)
    {
        width = w;
        height = h;
        occupancy = new int[w, h];
        for (int x = 0; x < w; x++)
            for (int y = 0; y < h; y++)
                occupancy[x, y] = -1;
    }

    public bool IsPathClear(Vector2Int start, Vector2Int direction, int length)
    {
        for (int i = 1; i <= length; i++)
        {
            var check = start + direction * i;
            if (!InBounds(check) || occupancy[check.x, check.y] != -1)
                return false;
        }
        return true;
    }

    private bool InBounds(Vector2Int p) =>
        p.x >= 0 && p.x < width && p.y >= 0 && p.y < height;
}
Enter fullscreen mode Exit fullscreen mode

Notice the grid knows nothing about sprites, animation curves, or input — it's pure occupancy state. That separation matters enormously once you get to system 2, because path resolution and visual movement are genuinely different concerns that get tangled constantly in naive implementations.

System 2: Path Resolution Without Fighting Physics

A tempting shortcut is to drive vehicle movement through Unity's physics engine directly — Rigidbody2D with collisions, let the physics solver figure out contact resolution. This works for about the first five vehicles you test and then falls apart the moment two vehicles are released to move in the same frame and their colliders briefly overlap during the transition, producing a visible stutter or an unintended nudge.

A more predictable approach separates grid-state movement (instant, logical, resolved before any animation plays) from presentation movement (purely visual, tweened, guaranteed not to fight with physics):

public class VehicleController : MonoBehaviour
{
    public Vector2Int gridPosition;
    public Vector2Int facingDirection;
    private ParkingGrid grid;

    public void TryMove(Action onArrived)
    {
        int length = ComputeExitDistance();
        if (!grid.IsPathClear(gridPosition, facingDirection, length))
        {
            onArrived?.Invoke(); // move rejected, no animation plays
            return;
        }

        grid.ClearCell(gridPosition);
        var targetWorldPos = GridToWorld(gridPosition + facingDirection * length);
        StartCoroutine(AnimateMove(targetWorldPos, onArrived));
    }

    private IEnumerator AnimateMove(Vector3 target, Action onComplete)
    {
        float duration = 0.35f;
        float t = 0f;
        Vector3 start = transform.position;
        while (t < duration)
        {
            t += Time.deltaTime;
            transform.position = Vector3.Lerp(start, target, t / duration);
            yield return null;
        }
        transform.position = target;
        onComplete?.Invoke();
    }
}
Enter fullscreen mode Exit fullscreen mode

The key decision here is that the grid state resolves before any animation starts, not during it. By the time the player sees a vehicle sliding out, the logical outcome of that move has already been decided and committed. This eliminates an entire category of race-condition bugs where two vehicles both start animating toward positions that briefly conflict, because the grid never allows that state to exist in the first place — a move that isn't valid never starts animating at all.

System 3: Match and Clear Logic Without Double-Triggers

Depending on the specific variant, a vehicle might clear the board by reaching a designated exit, by matching color or type with another vehicle in an adjacent clear zone, or both. The failure mode to watch for here is a clear event firing twice — once from an exit-reached check and once from a match check that both happened to resolve on the same frame.

The fix is to route every possible clear condition through a single authority rather than letting each system independently decide a vehicle is "done":

public class ClearAuthority : MonoBehaviour
{
    private readonly HashSet<int> clearedThisFrame = new HashSet<int>();

    public void RequestClear(int vehicleId, ClearReason reason)
    {
        if (clearedThisFrame.Contains(vehicleId)) return; // already handled
        clearedThisFrame.Add(vehicleId);

        BoardStateManager.Instance.RemoveVehicle(vehicleId);
        ScoreManager.Instance.RegisterClear(vehicleId, reason);
        LevelWinChecker.Instance.CheckWinCondition();
    }

    private void LateUpdate() => clearedThisFrame.Clear();
}

public enum ClearReason { ReachedExit, MatchedPair }
Enter fullscreen mode Exit fullscreen mode

Both the exit-detection script and the match-detection script call ClearAuthority.RequestClear, and neither one needs to know or care whether the other system might also be trying to clear the same vehicle on the same frame. This is the same defensive pattern that shows up anywhere multiple systems can plausibly trigger the same terminal state — better to have one gatekeeper than to have every caller individually try to guard against a race condition it can only partially see.

System 4: Level Data as Content, Not Code

This is the part that determines whether a parking puzzle game can scale to a real level count or stalls out around level fifteen because every new layout means opening a scene and hand-placing vehicles. The fix is the same one that shows up in basically every casual-genre systems breakdown worth reading: pull layout data out into ScriptableObject assets that the runtime scene reads, rather than baking layouts directly into hand-built scenes.

[CreateAssetMenu(fileName = "LevelLayout", menuName = "Game/Level Layout")]
public class LevelLayout : ScriptableObject
{
    [Serializable]
    public class VehiclePlacement
    {
        public Vector2Int gridPosition;
        public Vector2Int facingDirection;
        public VehicleType type;
        public int colorIndex;
    }

    public int gridWidth;
    public int gridHeight;
    public List<VehiclePlacement> placements;
    public int moveLimit;
}
Enter fullscreen mode Exit fullscreen mode

A single generic level-loader scene reads whichever LevelLayout asset is active, spawns vehicles at the specified grid positions, and wires them into the same ParkingGrid and ClearAuthority systems described above — no per-level code, no per-level scene. Building a new level becomes a data-authoring task (arranging placements in an Inspector or an external level editor tool that exports to this same format), not an engineering task. This is what actually lets a template like this scale to hundreds of levels without a proportional increase in developer time per level.

System 5: Monetization Hooks at Natural Break Points

Parking and matching puzzles have unusually clean break points for monetization compared to genres with more continuous action — a level either ends in a win or a stall (no more valid moves, move limit reached), and both of those moments are natural, non-disruptive places to offer a rewarded video for an extra move or a hint, rather than interrupting the player mid-action.

The mistake to avoid is wiring ad calls directly into the win/loss detection logic itself. Keeping monetization behind the same kind of interface-driven boundary used for the clear authority avoids polluting the core game logic with third-party SDK concerns:

public interface IRewardService
{
    void OfferExtraMove(Action onGranted, Action onDeclinedOrFailed);
}

public class LevelStallHandler : MonoBehaviour
{
    private IRewardService rewardService;

    public void OnNoValidMovesRemaining()
    {
        rewardService.OfferExtraMove(
            onGranted: () => MoveLimitManager.Instance.GrantExtraMove(),
            onDeclinedOrFailed: () => LevelWinChecker.Instance.TriggerLevelFailed()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

LevelStallHandler doesn't know or care whether IRewardService is backed by AdMob, Unity LevelPlay, or a mock implementation used in editor testing. That separation is exactly what let earlier systems in this breakdown — grid state, path resolution, clear authority — stay entirely free of any monetization-specific logic, which matters a lot the first time you need to swap ad networks or A/B test reward placement without touching gameplay code at all.

Extending the Core Loop

Once movement, matching, level-data separation, and monetization hooks are solid, most of the "make it feel like a full game" work is additive rather than architectural:

  • New vehicle types: trucks that need two grid cells instead of one, or vehicles that can only exit in specific directions, both slot into the existing ParkingGrid/VehicleController split without new core systems
  • Hint systems: a lightweight solver that scans current grid occupancy for any valid move and highlights it, built entirely on top of the same IsPathClear check the core loop already uses
  • Daily challenges: a rotating LevelLayout reference swapped based on system date, using the exact same data-driven loader described in System 4
  • Leaderboards and star ratings: meta-progression layered on top of ScoreManager, decoupled from the movement and clear systems entirely

None of these require touching the grid, movement, or clear-authority core — which is really the entire point of building the foundation this way. A parking puzzle with this kind of separation can absorb months of new level content and feature additions without ever needing a rewrite of the underlying mechanics.

If you're evaluating whether to build this kind of system from scratch or start from an existing, already-architected base, it's worth browsing what's already out there before committing to either path — a broader look at Unity Source Code's full product catalog is a reasonable place to compare feature sets and architecture quality across templates before deciding where to spend your own engineering time. I went through a similarly detailed systems breakdown for a completely different casual mechanic — fill-based coloring gameplay rather than movement-based puzzle gameplay — in an earlier piece on building a relaxing coloring game in Unity, and a lot of the same underlying principles show up there too: decoupled input handling, ScriptableObject-driven content, and a save/state layer that doesn't know or care about presentation. The specific mechanic changes; the architectural discipline that makes a casual game scale past a dozen hand-built levels doesn't.

Wrapping Up

Parking and matching puzzles earn their "casual" label by hiding complexity, not by lacking it. The systems that make this genre feel tight — grid-based occupancy state decoupled from visual animation, a single clear authority that prevents double-trigger bugs, level layouts as data rather than hardcoded scenes, and monetization wired behind an interface instead of tangled into win/loss logic — are the same categories of systems you'll run into building almost any casual mobile puzzle game. Get those right and the rest — new vehicle types, hint systems, daily challenges, leaderboards — is just content and feature work layered on top of a foundation that doesn't need to change.

If you're prototyping something in this space, start with the grid-state/animation separation first. Every other system in this breakdown — path resolution, clear detection, even how cleanly monetization hooks slot in — depends on getting that one decision right before anything else gets built on top of it.

Top comments (0)