Color sorting puzzle games look almost too simple to write about. Tap a colored piece, move it to a matching stack, repeat until every group is sorted. No physics, no combat, no complex AI. But that simplicity is exactly why the genre is worth studying — it forces you to get the fundamentals of game architecture right, because there's nowhere to hide a sloppy implementation behind flashy visuals.
This article walks through the actual engineering behind a "sort by color" puzzle mechanic in Unity — using a bird-and-branch sorting concept as the running example — and covers the architecture decisions, data structures, and systems you need to build one properly. Whether you're implementing this from scratch or adapting an existing codebase, the same core problems show up every time.
The Core Mechanic, Defined Precisely
Before writing any code, it helps to state the rules unambiguously, because "sort by color" hides a surprising number of edge cases once you start implementing it.
The board consists of a set of containers (branches, tubes, stacks — the visual metaphor doesn't matter). Each container holds a stack of colored items, with strict LIFO (last-in-first-out) access — you can only interact with the item on top of each stack.
A move is valid when:
- The source container has at least one item on top.
- The destination container is either completely empty, or its top item matches the color of the item being moved.
- The destination container has remaining capacity.
The puzzle is solved when every non-empty container holds items of a single color only.
That's the entire rule set. But notice what it implies: you need capacity tracking per container, color comparison on every move attempt, and a win-condition check that runs after every single move. None of this is complicated in isolation, but it needs to be structured cleanly or it turns into spaghetti fast.
Data Structure: Why ScriptableObjects Matter Here
The single most important architectural decision in this genre is how you represent level data. A common beginner mistake is hardcoding level layouts directly in scene objects or in code — which means every new level requires touching a scene file or recompiling scripts.
The better approach is to define level data as a serializable asset, independent of any scene:
[CreateAssetMenu(fileName = "Level_", menuName = "Puzzle/LevelData")]
public class LevelData : ScriptableObject
{
public int levelNumber;
public int branchCount;
public int colorCount;
public int emptyBranchCount;
public List<BranchConfig> branches;
}
[System.Serializable]
public class BranchConfig
{
public int capacity;
public List<ColorType> initialItems; // bottom to top
}
With this structure, level design becomes a data-entry task performed in the Unity Inspector, not a coding task. A designer — or you, wearing a different hat — can create, test, and balance dozens of levels without touching a single gameplay script. This separation between level content and game logic is what allows a puzzle game to scale to hundreds of levels without the codebase growing more complex alongside it.
The Board Controller: Where the Actual Logic Lives
The board controller is the single source of truth for game state. Everything else — animation, UI, audio — reacts to changes this component makes; it never mutates state directly.
public class BoardController : MonoBehaviour
{
private List<Stack<ColorType>> branches;
private List<int> branchCapacities;
public bool TryMove(int fromIndex, int toIndex)
{
if (!IsValidMove(fromIndex, toIndex)) return false;
var item = branches[fromIndex].Pop();
branches[toIndex].Push(item);
OnMoveExecuted?.Invoke(fromIndex, toIndex, item);
if (CheckWinCondition())
OnLevelComplete?.Invoke();
return true;
}
private bool IsValidMove(int from, int to)
{
if (branches[from].Count == 0) return false;
if (branches[to].Count >= branchCapacities[to]) return false;
var movingItem = branches[from].Peek();
return branches[to].Count == 0 || branches[to].Peek() == movingItem;
}
private bool CheckWinCondition()
{
foreach (var branch in branches)
{
if (branch.Count == 0) continue;
var firstColor = branch.Peek();
if (branch.Any(item => item != firstColor)) return false;
}
return true;
}
}
A few things worth calling out here:
-
Validation happens before mutation.
IsValidMoveis a pure check with no side effects, which makes it trivial to unit test and reuse for things like hint generation (more on that below). -
Events, not direct calls, drive presentation.
OnMoveExecutedandOnLevelCompletedecouple game logic from animation and UI. The board controller doesn't know or care how a move looks visually — that's a separation you want in any game genre, but it's especially clean to enforce in a mechanic this contained. -
Stack is the correct native structure. Since containers are strictly LIFO, C#'s built-in
Stack<T>maps directly onto the game rule without needing a custom implementation.
Input Handling: Raycasting Against a Grid of Interactive Zones
Since this is a touch-first mobile mechanic, input handling needs to be fast and unambiguous. The standard approach uses Unity's physics raycasting against 2D colliders attached to each container:
void HandleTap(Vector2 screenPosition)
{
Ray ray = Camera.main.ScreenPointToRay(screenPosition);
RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
if (hit.collider == null) return;
int tappedBranch = hit.collider.GetComponent<BranchView>().BranchIndex;
if (selectedBranch == -1)
{
selectedBranch = tappedBranch;
HighlightBranch(tappedBranch);
}
else
{
boardController.TryMove(selectedBranch, tappedBranch);
ClearSelection();
}
}
The two-tap selection model (tap source, tap destination) is generally more reliable on mobile than drag-and-drop, since drag gestures are more prone to accidental triggers and require more precise hit detection during motion. Feedback needs to be immediate regardless of which input model you choose — a shake animation and short audio cue on an invalid move communicates the rule without requiring any text explanation.
Implementing a Hint System Without Solving the Puzzle For the Player
A hint system is one of the more interesting engineering problems in this genre, because a naive implementation either does nothing useful or accidentally gives away the full solution.
The goal is to find one legal move that makes meaningful progress, not to run a full solver. A reasonably effective heuristic:
public (int from, int to)? GetHint()
{
for (int from = 0; from < branches.Count; from++)
{
if (branches[from].Count == 0) continue;
var topColor = branches[from].Peek();
for (int to = 0; to < branches.Count; to++)
{
if (from == to) continue;
if (!IsValidMove(from, to)) continue;
// Prioritize moves that consolidate an existing color group
if (branches[to].Count > 0 && branches[to].Peek() == topColor)
return (from, to);
}
}
// Fall back to any valid move if no consolidating move exists
for (int from = 0; from < branches.Count; from++)
for (int to = 0; to < branches.Count; to++)
if (from != to && IsValidMove(from, to))
return (from, to);
return null;
}
This reuses the exact same IsValidMove check from the board controller — another payoff of keeping that method pure and side-effect free. The heuristic prioritizes moves that consolidate matching colors over arbitrary legal moves, which tends to nudge the player toward genuine progress rather than a move that's technically legal but strategically pointless.
Undo Without Storing Full Board Snapshots
A naive undo implementation stores a full copy of the board state after every move. This works, but it's wasteful — for this mechanic, you only ever need to reverse the last operation, which is trivially cheap:
private Stack<(int from, int to)> moveHistory = new Stack<(int, int)>();
public bool TryMove(int from, int to)
{
if (!IsValidMove(from, to)) return false;
var item = branches[from].Pop();
branches[to].Push(item);
moveHistory.Push((from, to));
return true;
}
public bool Undo()
{
if (moveHistory.Count == 0) return false;
var (from, to) = moveHistory.Pop();
var item = branches[to].Pop();
branches[from].Push(item);
return true;
}
Since every move is a simple pop-then-push between two containers, reversing it is just the same operation performed backward. This scales cleanly to supporting multiple sequential undos without any additional memory overhead per move.
Star Rating: Turning a Binary Win State Into a Skill Metric
A puzzle either gets solved or it doesn't — but that binary outcome alone doesn't give players a reason to replay a completed level. A star rating system based on move efficiency solves this cheaply:
public int CalculateStars(int movesUsed, int optimalMoves)
{
if (movesUsed <= optimalMoves) return 3;
if (movesUsed <= optimalMoves * 1.5f) return 2;
return 1;
}
The optimalMoves value should be precomputed and stored per level (ideally solved offline with a BFS/DFS solver during level design, not calculated at runtime) rather than derived on the fly. This turns a simple pass/fail game into one with a genuine mastery curve, which is a meaningful retention lever without adding any new core mechanics.
Structuring the Codebase for Reskinning
If there's one architectural habit worth adopting from this genre, it's designing explicitly for reskinning from day one — even if you have no immediate plan to reskin the game. It costs almost nothing to do upfront and saves significant rework later:
- Keep all color-to-sprite mappings in a single
ColorPaletteScriptableObject rather than hardcoding sprite references in prefabs. - Reference fonts and UI theme colors from one central config asset.
- Store all character/item art in a single labeled sprite atlas with a consistent naming convention.
- Never let gameplay logic (
BoardController,LevelData) reference visual assets directly — visual representation should be a pure function of game state, driven entirely through events.
Done properly, changing the entire visual theme of the game — different characters, different palette, different UI skin — becomes an asset-swapping exercise rather than a code-editing one.
Where AdMob Fits Into This Architecture Without Coupling to Gameplay
Monetization logic should never live inside your gameplay classes. The cleanest pattern is an AdManager that gameplay code talks to through simple method calls, with no gameplay-side knowledge of ad state:
public class AdManager : MonoBehaviour
{
public void ShowRewardedAd(System.Action onRewardGranted)
{
// Load and show rewarded ad, invoke callback on success
}
public void ShowInterstitial() { /* ... */ }
}
The hint and undo systems are natural candidates for rewarded-ad gating — offer a limited number of free uses per level, then route additional uses through AdManager.ShowRewardedAd(). Because the player is already invested in solving the specific puzzle in front of them at the moment they hit the limit, this placement tends to see meaningfully higher engagement than ads shown at arbitrary points in the session.
Performance Considerations for Mobile
This genre is 2D and mechanically lightweight, but that doesn't mean performance is a non-issue — it means the performance bar is simply higher, since there's no excuse for a simple puzzle game to run poorly on budget hardware.
Practical steps worth taking:
- Object pool the item GameObjects and particle effects rather than instantiating and destroying them on every move.
- Use a single sprite atlas per visual theme to minimize draw calls, since even simple puzzle scenes can accumulate a surprising number of individual sprite renderers.
- Avoid runtime parsing for level data — ScriptableObjects load natively without any deserialization step.
- Keep particle budgets modest. Win celebrations and move feedback should be visually satisfying without spawning hundreds of simultaneous particles on low-end GPUs.
Where This Fits Into the Broader Mobile Genre Landscape
Color sorting puzzles are one entry in a much larger set of casual mechanics currently performing well on mobile, and the architectural principles here — clean separation between game state and presentation, data-driven level design, and monetization decoupled from gameplay logic — apply broadly across the genre. For a wider look at which mobile game categories are gaining traction and why, this technical breakdown of the top trending mobile game genres in 2026 is a useful reference for developers deciding what to build next.
Building From Scratch vs. Starting From an Existing Codebase
Everything covered above — the board controller, hint heuristic, undo stack, star rating, and reskin-friendly asset organization — represents a non-trivial amount of engineering time to get right, even though none of it is individually difficult. Getting the validation logic bug-free, tuning the hint heuristic to feel helpful rather than intrusive, and structuring the reskin architecture properly typically takes longer than developers expect going in.
If you'd rather study a complete, working implementation of these systems instead of building each one from zero, the Bird Sort Puzzle Unity source code implements the full architecture described in this article — board logic, undo, hints, star ratings, AdMob hooks, and a reskin-ready asset structure — as a working Unity project you can read through, modify, and learn from directly.
Applying the Same Architecture Principles to Other Genres
It's worth noting that the core lesson here — keep game state logic pure and decoupled from presentation, drive levels from data rather than code, and design for reskinning from the start — isn't specific to sorting puzzles. The same principles apply directly to farming and simulation games, where inventory systems, crop-growth timers, and upgrade trees follow an almost identical data-driven pattern. If you're interested in seeing these same architectural ideas applied to a simulation genre instead of a puzzle one, the Farm Village Unity source code is a useful project to study side by side with this one.
Final Thoughts
Color sorting puzzle games are a great case study precisely because their simplicity exposes bad architecture immediately. There's no complex physics system or elaborate AI to distract from a poorly validated move function or a tangled dependency between gameplay logic and UI code.
If you take away one principle from this breakdown, make it this: keep your board/game-state logic completely independent of how it's rendered, animated, or monetized. Every system covered here — hints, undo, star ratings, AdMob integration — worked cleanly because it built on top of a board controller that did one job and did it correctly. That discipline is what actually separates a game that's easy to expand, reskin, and maintain from one that becomes harder to touch with every new feature you add.

Top comments (0)