DEV Community

unity source code
unity source code

Posted on

Building a Color Sorting Puzzle Game in Unity: The Architecture Behind the Genre

Color sorting puzzle games look deceptively simple from the player's side — pour, sort, clear the level, repeat. But if you've ever tried to actually implement one, you know the "simple" gameplay hides a handful of interesting architectural decisions: how you represent color state, how you detect a "solved" container, how you structure levels so designers (or non-programmers) can add hundreds of them without touching code, and how you keep the whole thing performant on low-end Android devices.

In this post, I want to break down the core architecture behind a typical Unity color sorting puzzle — the kind of system you'd find in tube-sort, ball-sort, or hexa-sort style games — and talk through the design decisions that separate a clean implementation from a fragile one.

The Core Data Model

Before writing any gameplay code, it's worth deciding how you're going to represent color state. A common beginner mistake is hardcoding colors as raw Unity Color values scattered across scripts. Instead, treat color as an enum or an ID, and keep a single source of truth for how that ID maps to a visual (sprite, material, or particle color).

public enum PuzzleColor
{
    Red,
    Blue,
    Green,
    Yellow,
    Purple,
    Orange
}

[CreateAssetMenu(menuName = "ColorSort/ColorPalette")]
public class ColorPalette : ScriptableObject
{
    [System.Serializable]
    public struct ColorEntry
    {
        public PuzzleColor id;
        public Color displayColor;
        public Sprite icon;
    }

    public ColorEntry[] entries;

    public Color GetColor(PuzzleColor id)
    {
        foreach (var entry in entries)
            if (entry.id == id) return entry.displayColor;
        return Color.white;
    }
}
Enter fullscreen mode Exit fullscreen mode

This one decision pays off enormously later — if you ever want to reskin the game (a common practice in this genre), you swap the palette asset instead of hunting through gameplay scripts for hardcoded colors.

Modeling a Container as a Stack

Whether your game uses tubes, jars, or hex cells, the underlying data structure is almost always a stack (or a stack-like list) of color values. Sorting games are, at their core, a constraint-satisfaction problem dressed up with nice art.

public class ContainerModel
{
    private readonly List<PuzzleColor> contents = new List<PuzzleColor>();
    public int Capacity { get; }

    public ContainerModel(int capacity)
    {
        Capacity = capacity;
    }

    public bool IsFull => contents.Count >= Capacity;
    public bool IsEmpty => contents.Count == 0;
    public PuzzleColor Top => contents[contents.Count - 1];

    public bool IsSolved =>
        contents.Count == Capacity &&
        contents.TrueForAll(c => c == contents[0]);

    public bool CanAccept(PuzzleColor color)
    {
        if (IsFull) return false;
        if (IsEmpty) return true;
        return Top == color;
    }

    public void Push(PuzzleColor color) => contents.Add(color);
    public PuzzleColor Pop()
    {
        var c = Top;
        contents.RemoveAt(contents.Count - 1);
        return c;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that this class has zero dependency on Unity's MonoBehaviour, physics, or rendering. That's intentional. Keeping your puzzle logic as plain C# means you can unit test it without spinning up a scene, and it keeps your MonoBehaviour layer focused purely on presentation — animating the pour, playing sound effects, and updating visuals.

Separating Logic From Presentation

A pattern that pays off a lot in this genre is a strict separation between the model (the data above) and the view (the MonoBehaviour that renders it). Your ContainerView should never mutate game state directly — it should only ask the model whether a move is valid, then animate the result.

public class ContainerView : MonoBehaviour
{
    [SerializeField] private ContainerModel model;
    [SerializeField] private Transform[] slotPositions;

    public bool TryPourInto(ContainerView target)
    {
        if (!target.model.CanAccept(model.Top))
            return false;

        var color = model.Pop();
        target.model.Push(color);

        AnimatePour(target, color);
        return true;
    }

    private void AnimatePour(ContainerView target, PuzzleColor color)
    {
        // Trigger tween/animation here
    }
}
Enter fullscreen mode Exit fullscreen mode

This split matters more than it might seem at first glance. When your win-condition checking, undo system, and level validation all read from the same lightweight model, you avoid an entire category of bugs where the "visual" state and the "logical" state drift out of sync — which is a surprisingly common issue in puzzle games built without this separation.

Designing a Data-Driven Level System

If there's one architectural decision that determines whether a color sorting game can scale to hundreds of levels without becoming unmaintainable, it's this: levels should be data, not code.

[CreateAssetMenu(menuName = "ColorSort/LevelData")]
public class LevelData : ScriptableObject
{
    [System.Serializable]
    public struct ContainerConfig
    {
        public int capacity;
        public PuzzleColor[] initialColors;
    }

    public int moveLimit;
    public ContainerConfig[] containers;
}
Enter fullscreen mode Exit fullscreen mode

With this approach, a designer (or even a non-programmer) can create new levels entirely inside the Unity Editor by creating new LevelData assets, without ever touching a script. It also makes it trivial to build a simple level-select screen that just iterates over an array of LevelData assets, or to load level definitions from JSON if you want server-side level delivery later.

This same principle — keeping gameplay data separate from gameplay code — shows up across other puzzle mechanics too, not just color sorting. I covered a similar approach in the context of a physical extraction puzzle in Building a Screw Puzzle Game in Unity: The Design and Architecture Behind Wood Nuts & Bolts, which walks through how a very different core mechanic (screw extraction instead of color pouring) can still benefit from the same data-driven level design pattern.

Handling the Win Condition and Move Limits

Checking for a solved puzzle should be a pure function over your container models — no rendering, no coroutines, just data in, boolean out.

public class PuzzleManager : MonoBehaviour
{
    [SerializeField] private ContainerModel[] containers;
    [SerializeField] private int movesRemaining;

    public bool CheckWinCondition()
    {
        foreach (var container in containers)
        {
            if (!container.IsEmpty && !container.IsSolved)
                return false;
        }
        return true;
    }

    public void OnMoveMade()
    {
        movesRemaining--;
        if (CheckWinCondition())
            OnLevelComplete();
        else if (movesRemaining <= 0)
            OnOutOfMoves();
    }

    private void OnLevelComplete() { /* fire win UI, save progress */ }
    private void OnOutOfMoves() { /* fire lose/retry UI */ }
}
Enter fullscreen mode Exit fullscreen mode

Because CheckWinCondition only touches plain data, it's trivial to write an editor tool or unit test that generates random level configurations and verifies they're actually solvable before you ship them — something that's genuinely useful once you're generating levels procedurally instead of hand-placing every one.

Performance Considerations for Low-End Devices

A large share of the casual puzzle audience plays on budget Android devices, so a few performance habits matter more here than they might in a graphically heavier genre:

  • Pool your particle effects and UI popups. Sorting games trigger a lot of small visual feedback events (pour animations, "solved" bursts, combo text), and instantiating/destroying these repeatedly causes avoidable GC pressure.
  • Avoid per-frame allocations in your win-check loop. Iterating over containers every move is fine; doing it every Update() frame is not.
  • Batch UI updates. If your level has a lot of containers rendered as UI elements, avoid triggering a full canvas rebuild on every single small state change.
  • Keep container capacity reasonable. Larger containers (more colors per stack) increase both visual complexity and the branching factor of your win-check logic — test on mid-range hardware, not just your dev machine.

Where Color Sorting Fits in the Broader Casual Genre Landscape

It's worth zooming out for a second. Color sorting is one branch of a much larger tree of casual, mechanically simple genres that share this same architectural philosophy — small, well-defined state, data-driven levels, and a strict separation between logic and presentation. If you're deciding whether this genre is worth your development time compared to alternatives, I'd recommend reading Best Color Sorting Puzzle Unity Source Codes in 2026, which breaks down current market trends, monetization approaches, and a curated set of existing implementations worth studying before you start building your own from scratch.

And if you want to see how a completely different, timing-based mechanic handles its own architecture — state machines driven by input timing rather than stack-based color logic — it's worth looking at Knife Hit Unity Game Source Code. Comparing the two is a genuinely useful exercise: one genre is built entirely around discrete state transitions (sorting), the other around continuous timing and physics (throwing), and seeing both patterns side by side makes you a noticeably better systems thinker when you sit down to design your next mechanic.

Wrapping Up

Color sorting puzzle games are a great case study in how much architectural discipline can matter even in a "simple" genre. The gameplay looks trivial from the outside, but a clean implementation — plain C# models, ScriptableObject-driven levels, a hard separation between logic and view — is what makes the difference between a game you can scale to 500 levels without breaking a sweat, and one that turns into a tangle of special-case bugs by level 40.

If you're building your first puzzle game, start with the data model before you touch a single animation or particle effect. Get the sorting logic rock solid and fully testable in isolation, and the rest of the game — UI, juice, monetization hooks — will slot in far more smoothly than if you build it all together from day one.

Happy building, and if you end up implementing your own version of this system, I'd genuinely love to hear what data structure you landed on for your containers — stack, list, or something more exotic.

Top comments (0)