<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: unity source code</title>
    <description>The latest articles on DEV Community by unity source code (@unitysourcecode).</description>
    <link>https://dev.to/unitysourcecode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3985071%2F714492cf-7104-4c12-98fc-38203adfed9c.png</url>
      <title>DEV Community: unity source code</title>
      <link>https://dev.to/unitysourcecode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unitysourcecode"/>
    <language>en</language>
    <item>
      <title>Building a Crowd Runner Combat System in Unity: Army Growth, Formations, and Battle Phases</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:25:50 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-crowd-runner-combat-system-in-unity-army-growth-formations-and-battle-phases-nhh</link>
      <guid>https://dev.to/unitysourcecode/building-a-crowd-runner-combat-system-in-unity-army-growth-formations-and-battle-phases-nhh</guid>
      <description>&lt;p&gt;Crowd runner games — the genre where you sprint down a lane collecting followers, then throw that crowd into a battle sequence — have quietly become one of the most consistent hits in mobile gaming. Think &lt;em&gt;Last War: Survival&lt;/em&gt;, &lt;em&gt;Zombie Run&lt;/em&gt;, &lt;em&gt;Count Masters&lt;/em&gt;. The appeal is simple to describe and surprisingly tricky to implement well: a runner section that feeds directly into a combat section, where every decision in phase one visibly changes your odds in phase two.&lt;/p&gt;

&lt;p&gt;I want to walk through the actual systems that make this genre work, with real C# code you can drop into a Unity project. We'll cover three core pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A performant crowd/army system (using object pooling, not &lt;code&gt;Instantiate&lt;/code&gt; spam)&lt;/li&gt;
&lt;li&gt;A formation-following movement system for your crowd&lt;/li&gt;
&lt;li&gt;A phase-transition state machine that connects the runner section to the combat section&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end you'll have a working skeleton you can extend into a full game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Crowd Runners Are Deceptively Hard to Build
&lt;/h2&gt;

&lt;p&gt;The naive approach — spawn a &lt;code&gt;GameObject&lt;/code&gt; for every unit, parent it to the player, and call it a day — falls apart fast. If your player is meant to command 50, 100, or even 300 units on screen simultaneously (which is normal for this genre), naive instantiation and per-frame &lt;code&gt;Transform&lt;/code&gt; updates will tank your frame rate on mobile hardware within seconds.&lt;/p&gt;

&lt;p&gt;The three problems you need to solve up front are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spawning and despawning hundreds of units without garbage collection spikes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Making a crowd move together in a way that looks organic, not like a single rigid block&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleanly transitioning game state from "running" to "combat" without hacky flags scattered across your codebase&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's tackle each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Object Pooling for Crowd Units
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;Instantiate()&lt;/code&gt; and &lt;code&gt;Destroy()&lt;/code&gt; call allocates and frees memory. Do that 50 times a frame as your crowd grows and shrinks, and you'll see GC spikes causing visible stutter. The fix is a pool: pre-allocate your unit objects once, and recycle them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CrowdUnitPool&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;unitPrefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;initialPoolSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;initialPoolSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unitPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="nf"&gt;GetUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unitPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fallback if pool runs dry&lt;/span&gt;

        &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPositionAndRotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ReturnUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision here is the fallback &lt;code&gt;Instantiate&lt;/code&gt; inside &lt;code&gt;GetUnit&lt;/code&gt;. Your army size is variable — a player might recruit far more units than your &lt;code&gt;initialPoolSize&lt;/code&gt; accounts for. Rather than hard-capping crowd size (which breaks the power fantasy of the genre), let the pool grow on demand, but always return objects to the pool instead of destroying them once they've been created.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Formation-Based Crowd Movement
&lt;/h2&gt;

&lt;p&gt;A crowd that moves as a single rigid block reads as fake immediately. What actually sells the "army" feeling is loose formation logic: each unit has a target offset from the player, with some smoothing so units drift into position rather than snapping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Generic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CrowdFormationController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;followSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;spacing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;unitsPerRow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RemoveUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;targetOffset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetFormationOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;targetPosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;targetOffset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;targetPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;followSpeed&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deltaTime&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="nf"&gt;GetFormationOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;unitsPerRow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="n"&gt;unitsPerRow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Center the row so units fan out symmetrically behind the player&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;centeredCol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unitsPerRow&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;centeredCol&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;-(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;spacing&lt;/span&gt; &lt;span class="c1"&gt;// stack rows behind the player&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Vector3.Lerp&lt;/code&gt; with &lt;code&gt;Time.deltaTime&lt;/code&gt;&lt;/strong&gt; gives you smooth, organic-looking movement instead of units snapping to grid positions. This alone makes a huge visual difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row/column offset math&lt;/strong&gt; keeps your formation readable even at large crowd sizes, rather than units overlapping or clipping into each other.&lt;/li&gt;
&lt;li&gt;Consider adding a small random jitter (&lt;code&gt;Random.insideUnitCircle * 0.1f&lt;/code&gt;) to each unit's offset if you want an even more organic, less "grid-like" look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For performance at very high unit counts (200+), you'd eventually want to move this off individual &lt;code&gt;Transform&lt;/code&gt; updates and into Unity's Job System with Burst compilation, or use &lt;code&gt;GPU instancing&lt;/code&gt; for rendering. But this CPU-based version comfortably handles a few hundred units on mid-range mobile hardware, which covers most crowd runner use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Run → Combat State Machine
&lt;/h2&gt;

&lt;p&gt;This is the part that actually defines the genre. The running phase and the combat phase need to feel like two different games, but they need to share data cleanly — specifically, the size and strength of the army you built during the run.&lt;/p&gt;

&lt;p&gt;A simple, explicit state machine keeps this manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Transitioning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Combat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResultsScreen&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GamePhaseController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;GamePhaseController&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt; &lt;span class="n"&gt;CurrentPhase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;RunnerController&lt;/span&gt; &lt;span class="n"&gt;runnerController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CombatController&lt;/span&gt; &lt;span class="n"&gt;combatController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ArmyData&lt;/span&gt; &lt;span class="n"&gt;armyData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnterCombatPhase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CurrentPhase&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;CurrentPhase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transitioning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;runnerController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopRunning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Hand off the army built during the run to the combat system&lt;/span&gt;
        &lt;span class="n"&gt;combatController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeBattle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;armyData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;armyData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AverageUnitPower&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;CurrentPhase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Combat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EndCombatPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;playerWon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentPhase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GamePhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResultsScreen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// Trigger UI, rewards, and progression updates here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArmyData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;AverageUnitPower&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;unitPower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;totalPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AverageUnitPower&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;AverageUnitPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalPower&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;unitPower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RemoveUnits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CurrentUnitCount&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason to keep &lt;code&gt;ArmyData&lt;/code&gt; as a plain serializable class rather than baking the numbers directly into your runner or combat controllers is reusability — you can persist it, feed it into a save system, or use it to drive a pre-battle "army preview" screen without your combat logic knowing anything about how the army was built.&lt;/p&gt;

&lt;p&gt;Notice the explicit &lt;code&gt;Transitioning&lt;/code&gt; state between &lt;code&gt;Running&lt;/code&gt; and &lt;code&gt;Combat&lt;/code&gt;. It's tempting to skip this and just flip straight from one to the other, but having a dedicated transition state gives you a clean hook for things like a camera pan, a "Battle Start" animation, or a short loading pause — all without cluttering your &lt;code&gt;Running&lt;/code&gt; or &lt;code&gt;Combat&lt;/code&gt; state logic with one-off timing hacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the Systems
&lt;/h2&gt;

&lt;p&gt;Putting it together, your recruitment logic (picking up allies during the run) should call both &lt;code&gt;CrowdFormationController.AddUnit()&lt;/code&gt; and &lt;code&gt;ArmyData.AddUnit()&lt;/code&gt; at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecruitGate&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CrowdUnitPool&lt;/span&gt; &lt;span class="n"&gt;unitPool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CrowdFormationController&lt;/span&gt; &lt;span class="n"&gt;formation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ArmyData&lt;/span&gt; &lt;span class="n"&gt;armyData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;unitBasePower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnTriggerEnter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Collider&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompareTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Player"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;newUnit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unitPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;formation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;armyData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unitBasePower&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps a clean separation of concerns: the pool handles memory, the formation controller handles visual positioning, and &lt;code&gt;ArmyData&lt;/code&gt; handles the numbers that actually matter for combat balance. When your combat phase starts, it only needs to read from &lt;code&gt;ArmyData&lt;/code&gt; — it doesn't care how the army was visually assembled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Checklist Before You Ship
&lt;/h2&gt;

&lt;p&gt;A few things worth double-checking once your core loop is working, especially if you're targeting lower-end Android devices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile with the actual target device&lt;/strong&gt;, not just the Unity Editor. Crowd rendering performance varies wildly between a flagship phone and a budget Android device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch your unit materials.&lt;/strong&gt; If every unit shares the same material and mesh, Unity can batch draw calls automatically (static or dynamic batching / GPU instancing). Mixing materials per-unit kills this optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap active combat units on screen&lt;/strong&gt;, even if your "army size" number is higher. Many shipped crowd runner games visually cap on-screen combatants at 40–60 and represent the rest abstractly in UI, because rendering hundreds of animated characters simultaneously in the combat phase is a much heavier cost than in the running phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LOD (Level of Detail) your unit models&lt;/strong&gt; if your crowd count regularly exceeds 100 — distant or background units don't need full-resolution meshes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The core technical challenge of a crowd runner combat game isn't any single system — it's making the pooling, formation, and phase-transition logic talk to each other cleanly without your codebase turning into a tangle of flags and special cases. Get those three systems solid, and the rest of the genre — upgrades, cosmetics, monetization hooks, level design — builds on top of a foundation that won't fight you later.&lt;/p&gt;

&lt;p&gt;If you'd rather not build this stack from scratch, it's worth knowing that fully implemented versions of this exact system — pooling, formation movement, run/combat state handling, and progression — exist as ready-made Unity templates. I found a solid library of them, including crowd-combat and runner-genre projects specifically, while researching this piece: &lt;a href="https://unitysourcecode.net/products" rel="noopener noreferrer"&gt;unitysourcecode.net/products&lt;/a&gt;. Even if you don't buy one, reading through a shipped implementation is a good way to sanity-check your own architecture against something that's already handled the edge cases.&lt;/p&gt;

&lt;p&gt;If you build your own version of this system, I'd genuinely like to hear what approach you took for large-crowd performance — Job System, GPU instancing, or something else entirely. Drop it in the comments.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>tutorial</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Unity Game Reskinning Explained: The Indie Developer's Shortcut to Shipping Faster in 2026</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:45:17 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/unity-game-reskinning-explained-the-indie-developers-shortcut-to-shipping-faster-in-2026-43d0</link>
      <guid>https://dev.to/unitysourcecode/unity-game-reskinning-explained-the-indie-developers-shortcut-to-shipping-faster-in-2026-43d0</guid>
      <description>&lt;p&gt;If you spend any time in indie game Discord servers or subreddits, you've probably noticed the same question popping up over and over: "How do these solo devs keep shipping games so fast?" More often than not, the answer isn't some secret productivity hack. It's reskinning.&lt;/p&gt;

&lt;p&gt;Reskinning has quietly become one of the most common ways solo developers and small studios get playable, monetizable games into app stores without spending a year (or a small fortune) building everything from the ground up. In this post, I want to walk through what reskinning actually is, why it's gained so much traction among indie devs, the real workflow behind it, and the mistakes that tend to sink an otherwise promising reskin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Reskinning Actually Means
&lt;/h2&gt;

&lt;p&gt;At its core, reskinning is the practice of taking an existing, working Unity project — one that already has its gameplay loop, physics, UI systems, and core logic in place — and replacing the surface layer: art, audio, branding, and sometimes a few gameplay parameters. The code that makes the game &lt;em&gt;function&lt;/em&gt; stays largely untouched. What changes is everything the player sees and hears.&lt;/p&gt;

&lt;p&gt;A useful mental model: imagine buying a fully renovated house instead of building one from an empty lot. The plumbing, wiring, and foundation are done. You're choosing paint colors, furniture, and fixtures. You still get a home that feels like yours, but you skip months of structural work.&lt;/p&gt;

&lt;p&gt;In practice, a typical reskin touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sprites, 3D models, and animations&lt;/li&gt;
&lt;li&gt;UI theme, fonts, and color palette&lt;/li&gt;
&lt;li&gt;Character and environment art&lt;/li&gt;
&lt;li&gt;Sound effects and music&lt;/li&gt;
&lt;li&gt;App icon, splash screen, and store listing assets&lt;/li&gt;
&lt;li&gt;Occasionally, difficulty curves or in-game economy tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying scripts, gameplay mechanics, and architecture generally stay as-is, which is exactly what makes the whole process so much quicker than a from-scratch build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Approach Has Taken Off Among Indie Developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It compresses timelines dramatically
&lt;/h3&gt;

&lt;p&gt;Building a mobile game from zero routinely takes anywhere from a few months to well over a year, depending on scope. Reskinning an already-functional template can shrink that to a matter of days or weeks. For developers who want to test several game ideas quickly rather than betting everything on one long build, that speed difference is huge.&lt;/p&gt;

&lt;h3&gt;
  
  
  It lowers the cost of entry
&lt;/h3&gt;

&lt;p&gt;You're not paying for a gameplay programmer, a systems architect, or months of QA on core mechanics — that work has already been done and (ideally) tested. Your budget shifts toward art, polish, and user acquisition, which are the levers that actually move downloads and revenue.&lt;/p&gt;

&lt;h3&gt;
  
  
  It reduces technical risk
&lt;/h3&gt;

&lt;p&gt;Bugs and crashes are one of the biggest threats to a new release. Starting from a template that has already shipped once, or has at least been through some level of QA, means you're not debugging core systems from scratch — you're mostly validating your own additions.&lt;/p&gt;

&lt;h3&gt;
  
  
  It opens the door for non-programmers
&lt;/h3&gt;

&lt;p&gt;Not everyone chasing a game release is a confident C# developer. Reskinning lets designers, marketers, and generally non-technical founders participate in publishing, since most of the hands-on work is asset replacement and configuration rather than systems programming.&lt;/p&gt;

&lt;h3&gt;
  
  
  It supports a portfolio strategy
&lt;/h3&gt;

&lt;p&gt;A lot of successful indie publishers don't bet on a single title. They release several small games, watch the data, and double down on whatever gets traction. That kind of iterative, portfolio-based approach is only realistic if each release doesn't require a full development cycle — which is precisely what reskinning enables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Workflow
&lt;/h2&gt;

&lt;p&gt;Here's roughly what the process looks like end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a genre and template&lt;/strong&gt; that fits the audience you're targeting (hyper-casual, puzzle, runner, survival, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit the code&lt;/strong&gt; for cleanliness, documentation, and Unity version compatibility before you commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the art assets&lt;/strong&gt; — this is where most of your creative time goes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebrand everything&lt;/strong&gt; — app icon, splash screen, store metadata, in-game logos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune gameplay parameters&lt;/strong&gt; where it makes sense (difficulty, rewards, progression pacing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire up monetization&lt;/strong&gt; — ads, IAP, or a hybrid model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test across devices&lt;/strong&gt;, not just your dev machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish and iterate&lt;/strong&gt; based on real player data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The genuinely useful part of this workflow, if you already have a solid engineering foundation, is that steps 3, 4, and 6 are where you spend almost all your time. That's a very different time allocation than a from-scratch build, where steps 1 and most of what would be "step 2" (building the actual systems) dominate the schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reskinning vs. Building From Scratch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;From Scratch&lt;/th&gt;
&lt;th&gt;Reskinning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to launch&lt;/td&gt;
&lt;td&gt;Months to a year+&lt;/td&gt;
&lt;td&gt;Days to a few weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding skill needed&lt;/td&gt;
&lt;td&gt;Advanced&lt;/td&gt;
&lt;td&gt;Basic to intermediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upfront cost&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low to moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug risk&lt;/td&gt;
&lt;td&gt;Higher (untested systems)&lt;/td&gt;
&lt;td&gt;Lower (pre-tested base)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creative control&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;High at the visual/branding layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Original, mechanically unique games&lt;/td&gt;
&lt;td&gt;Rapid launches, portfolio building, learning the publishing pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither approach is objectively "better." A fully original game can build a stronger long-term brand and defensible IP. But if your priority right now is getting real market feedback quickly, or you're still learning the mechanics of shipping and publishing, reskinning is a far more forgiving entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a Template Worth Reskinning
&lt;/h2&gt;

&lt;p&gt;This is where a lot of first attempts go sideways. A messy, undocumented, or outdated codebase can end up costing you more time than building from scratch would have. Before committing to a template, it's worth checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code readability&lt;/strong&gt; — are scripts organized and reasonably commented, or is it a single 2,000-line &lt;code&gt;GameManager.cs&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unity version compatibility&lt;/strong&gt; — is it built against a version you can actually maintain going forward?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset licensing&lt;/strong&gt; — do you have clear rights to reuse and republish the assets commercially?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modularity&lt;/strong&gt; — can you swap systems (like ads or leaderboards) without rewriting core logic?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Genres with simple, modular structures tend to reskin the most cleanly. Hyper-casual, puzzle, endless runner, and crowd/combat-style games are common choices precisely because their systems are simple enough to adapt without deep architectural changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features That Make a Reskin Feel Less Like a Reskin
&lt;/h2&gt;

&lt;p&gt;The biggest risk with reskinning isn't legal or technical — it's that your game ends up feeling indistinguishable from every other reskin of the same base template. Differentiation matters, and a lot of it comes down to which extra systems you bother to add.&lt;/p&gt;

&lt;p&gt;Retention-focused features are usually the highest-leverage additions. Two that consistently pay off:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leaderboards and achievements.&lt;/strong&gt; Competitive and completionist mechanics are proven retention drivers, and they're often missing from bare-bones templates. If your base project doesn't already have this wired up, this walkthrough on &lt;a href="https://unitysourcecode.net/blog/how-to-add-leaderboards-and-achievements" rel="noopener noreferrer"&gt;adding leaderboards and achievements to a Unity game&lt;/a&gt; covers the implementation in a genre-agnostic way, which is useful whether you're reskinning a runner, a puzzle game, or something more combat-focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notifications.&lt;/strong&gt; Bringing lapsed players back into the app is often cheaper than acquiring new ones, and a well-timed notification system can meaningfully move your day-7 and day-30 retention numbers.&lt;/p&gt;

&lt;p&gt;Small additions like these are usually a few hours of work on top of a reskin, but they're often the difference between a forgettable clone and something with actual staying power.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Sleep on Platform-Specific Requirements
&lt;/h2&gt;

&lt;p&gt;One area indie developers consistently underestimate is how different shipping to iOS is from shipping to Android — both in terms of Apple's review process and the technical checklist required before you can even submit a build. Provisioning profiles, App Store Connect setup, IDFA/tracking prompts, and Apple's stricter review guidelines all trip up first-time publishers, reskin or not.&lt;/p&gt;

&lt;p&gt;If iOS is part of your release plan, it's worth reading through this iOS shipping checklist before you get anywhere near a submission: &lt;a href="https://dev.to/unitysourcecode/shipping-your-unity-game-to-ios-the-technical-checklist-nobody-gives-you-upfront-k2h"&gt;Shipping Your Unity Game to iOS: The Technical Checklist Nobody Gives You Upfront&lt;/a&gt;. It covers a lot of the small, easy-to-miss steps that otherwise turn into rejected builds or last-minute scrambling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization Once You've Shipped
&lt;/h2&gt;

&lt;p&gt;Getting to market fast is only half the job — you still need a plan for turning downloads into revenue. Most reskinned games lean on some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rewarded video ads&lt;/strong&gt; for optional bonuses (extra lives, in-game currency, power-ups)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interstitial ads&lt;/strong&gt; at natural breakpoints like level completions or game-overs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-app purchases&lt;/strong&gt; for cosmetics, currency, or ad removal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriptions&lt;/strong&gt;, for games with an ongoing content cadence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hyper-casual titles typically skew heavily toward ad revenue given short session lengths and high volume. More session-based, engagement-heavy games often do better with a hybrid ad-and-IAP model. There's no universal answer here — it depends on genre, audience, and how much friction your players will tolerate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Ways Reskins Go Wrong
&lt;/h2&gt;

&lt;p&gt;A few mistakes show up again and again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incomplete asset replacement.&lt;/strong&gt; Leftover placeholder art, original branding, or unlicensed assets slipping through review is one of the most common (and avoidable) causes of store rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping device testing.&lt;/strong&gt; A build that runs fine on your dev phone can chug or crash on a mid-range Android device. Test broadly before you launch, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring App Store Optimization.&lt;/strong&gt; Even a solid reskin underperforms if your title, keywords, icon, and screenshots aren't optimized for discoverability. Development is only step one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Picking an oversaturated template with no differentiation.&lt;/strong&gt; If dozens of other developers are reskinning the exact same base project with minimal changes, yours will get lost in the noise. Invest in art direction or bonus features that set it apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating launch as the finish line.&lt;/strong&gt; Games that get regular updates — new levels, bug fixes, seasonal content — tend to hold onto their store rankings and reviews far better than games that ship and get abandoned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Reskinning the Right Call for You?
&lt;/h2&gt;

&lt;p&gt;Reskinning isn't a guarantee of success, and it's not meant to be a permanent substitute for original development. But it's a genuinely useful tool if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're new to publishing and want to learn the pipeline without betting a year on one idea&lt;/li&gt;
&lt;li&gt;You're a solo dev or small team without deep engineering resources&lt;/li&gt;
&lt;li&gt;You want to test multiple concepts and let player data decide what to scale&lt;/li&gt;
&lt;li&gt;You're trying to build a portfolio of live, revenue-generating apps rather than one big swing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your goal is a single, mechanically original game that carves out its own identity long-term, a custom build is probably the better investment. But if speed, lower risk, and real market feedback are what you're optimizing for, reskinning remains one of the most practical paths into mobile game publishing right now.&lt;/p&gt;

&lt;p&gt;For a deeper breakdown of the reskinning workflow, including how to choose a base template and where the process tends to go wrong, the original write-up this post is based on is worth a read: &lt;a href="https://unitysourcecode.net/blog/unity-game-reskinning-and-why-indie-developers" rel="noopener noreferrer"&gt;What Is Unity Game Reskinning and Why Indie Developers Are Using It to Launch Faster&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>reskinning</category>
      <category>beginners</category>
      <category>developer</category>
    </item>
    <item>
      <title>Shipping Your Unity Game to iOS: The Technical Checklist Nobody Gives You Upfront</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sun, 02 Aug 2026 18:26:04 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/shipping-your-unity-game-to-ios-the-technical-checklist-nobody-gives-you-upfront-k2h</link>
      <guid>https://dev.to/unitysourcecode/shipping-your-unity-game-to-ios-the-technical-checklist-nobody-gives-you-upfront-k2h</guid>
      <description>&lt;p&gt;If you've built mobile games before, you already know that finishing your game is only step one. Getting it actually approved and live on the App Store is a completely different challenge — one that trips up even experienced developers, because Apple's requirements shift subtly year over year, and the gap between "it works on my device" and "it passes App Store review" is wider than most of us expect.&lt;/p&gt;

&lt;p&gt;I want to walk through the technical realities of shipping a Unity game to iOS — the stuff that usually isn't obvious until you've either hit it yourself or had a submission bounced back with a rejection email that gives you just enough detail to be confused, but not enough to immediately know how to fix it.&lt;/p&gt;

&lt;p&gt;This isn't a beginner "click these buttons in Xcode" tutorial. This is the checklist I wish someone had handed me the first time I tried to ship a Unity project to iOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why iOS Deployment Feels Harder Than Android
&lt;/h2&gt;

&lt;p&gt;If your first mobile launch was on Android, the jump to iOS can feel jarring. Android's build and distribution process is comparatively forgiving — you can side-load APKs, testing is flexible, and Google Play's review process, while not trivial, tends to be faster and less strict on subjective design and content quality.&lt;/p&gt;

&lt;p&gt;iOS is a different beast entirely. You're dealing with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A build pipeline that requires macOS and Xcode, even if you're developing on Windows&lt;/li&gt;
&lt;li&gt;Strict code signing and provisioning profile requirements&lt;/li&gt;
&lt;li&gt;A review process that evaluates not just functionality, but design quality, metadata accuracy, and privacy compliance&lt;/li&gt;
&lt;li&gt;Platform-specific APIs for things like notifications, in-app purchases, and tracking permissions that behave differently than their Android counterparts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is meant to scare you off — plenty of indie developers ship successfully to iOS regularly. But going in with realistic expectations about the technical and procedural hurdles will save you from a lot of frustration mid-process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your Build Pipeline Correctly From the Start
&lt;/h2&gt;

&lt;p&gt;The first technical decision that trips people up is build pipeline setup. If you're developing on Windows or Linux, you'll need access to a Mac (physical, virtual, or a cloud-based Mac build service) at some point in your pipeline, since Xcode — Apple's required build and signing tool — only runs on macOS.&lt;/p&gt;

&lt;p&gt;Within Unity, make sure your &lt;strong&gt;Player Settings&lt;/strong&gt; for iOS are configured correctly before you ever attempt your first build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bundle Identifier matching your App Store Connect app record exactly&lt;/li&gt;
&lt;li&gt;Target minimum iOS version (consider your player base — supporting very old iOS versions can add unnecessary testing burden for a shrinking user segment)&lt;/li&gt;
&lt;li&gt;Correct architecture settings (ARM64 is standard for modern devices)&lt;/li&gt;
&lt;li&gt;Appropriate scripting backend (IL2CPP is required for App Store submissions — Mono builds won't pass review)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mistake I see repeatedly: developers build and test extensively in the Unity Editor and on Android, then treat the iOS build as an afterthought right before submission. iOS-specific bugs — particularly around performance, memory management, and platform-specific API behavior — have a way of surfacing only once you're testing on an actual physical iOS device, so build for iOS early and often throughout your development cycle, not just at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Certificates, Provisioning Profiles, and Code Signing
&lt;/h2&gt;

&lt;p&gt;This is the part of iOS deployment that causes the most confusion for developers new to Apple's ecosystem, so let's break it down clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certificates&lt;/strong&gt; identify you (or your organization) as a developer to Apple's systems. You'll need a Development certificate for testing and a Distribution certificate for App Store submissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning profiles&lt;/strong&gt; link your app's Bundle ID, your certificate, and (for development builds) the specific devices allowed to run your test builds. Distribution profiles for App Store submission don't require device UUIDs, but they do need to match your app's exact capabilities configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code signing&lt;/strong&gt; is the process of actually applying these certificates and profiles to your build. Xcode handles much of this automatically if you enable automatic signing, which is generally the right call for solo developers and small teams — manual signing gives you more control but introduces more opportunities for configuration errors, and it's rarely necessary unless you're managing a complex team structure with specific compliance requirements.&lt;/p&gt;

&lt;p&gt;If you hit a signing error during your build or submission process, it's almost always one of these three things: an expired certificate, a provisioning profile that doesn't match your current Bundle ID or capabilities, or a mismatch between your Xcode project's signing configuration and your actual Apple Developer account team settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Store Review: What Actually Gets Games Rejected
&lt;/h2&gt;

&lt;p&gt;Apple's App Store review process evaluates far more than "does the app crash." Based on common rejection patterns, here are the areas that trip up mobile game developers most often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incomplete or inaccurate metadata.&lt;/strong&gt; Your app's description, screenshots, and age rating need to accurately reflect what's actually in the game. Screenshots showing features that don't exist in the current build, or descriptions that oversell functionality, are common and easily avoidable rejection triggers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing privacy disclosures.&lt;/strong&gt; If your game uses any third-party SDKs — ad networks, analytics tools, crash reporting — you're required to accurately disclose data collection practices in your App Store Connect privacy details. This has become significantly stricter in recent years, and inaccurate or incomplete privacy labels are a fast track to rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-app purchase and ad implementation issues.&lt;/strong&gt; If your game includes IAP, every purchasable item needs to be properly configured in App Store Connect and clearly represented in your game's UI. Ads need to comply with Apple's guidelines around frequency, placement, and appropriateness for your app's age rating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broken or incomplete core functionality.&lt;/strong&gt; This sounds obvious, but it's worth stating directly: reviewers actually play your game. Broken tutorial flows, crashes on specific actions, or features that clearly don't work as described will get flagged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Tracking Transparency (ATT) compliance.&lt;/strong&gt; If your game requests tracking permissions (commonly required by certain ad SDKs for personalized advertising), you need to properly implement Apple's ATT prompt and handle both the granted and denied permission states gracefully.&lt;/p&gt;

&lt;p&gt;The practical takeaway: budget real time for a genuine pre-submission QA pass that specifically checks these areas, not just general gameplay testing. A significant percentage of rejections come from these process and compliance issues rather than actual bugs in the game itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention Systems Matter for Your App Store Success Too
&lt;/h2&gt;

&lt;p&gt;Here's something that's easy to overlook when you're heads-down on getting through the technical submission process: your post-launch performance — driven heavily by retention — directly affects your visibility in Apple's App Store search and ranking algorithms. Apps with strong retention and engagement metrics tend to get more organic discovery over time, which compounds your growth without requiring additional ad spend.&lt;/p&gt;

&lt;p&gt;This means your retention systems deserve real technical attention before you submit, not as a "we'll add it in an update later" afterthought. One of the most effective and commonly under-implemented systems here is push notifications — properly timed, well-targeted notifications that bring lapsed players back into your game meaningfully improve your day-7 and day-30 retention numbers.&lt;/p&gt;

&lt;p&gt;If you haven't implemented this system yet (or if you're not confident your current implementation is doing its job well), it's worth reading through this deep dive on &lt;a href="https://dev.to/unitysourcecode/push-notifications-in-unity-the-retention-system-most-devs-ship-too-late-or-not-at-all-2okf"&gt;push notifications in Unity as a retention system most developers ship too late or not at all&lt;/a&gt;. It covers exactly the kind of implementation and strategic timing considerations that separate games with healthy retention curves from games that see a spike of downloads followed by a steep, quiet drop-off.&lt;/p&gt;

&lt;p&gt;Building this system in before your initial submission, rather than bolting it on in a rushed post-launch update, gives you a meaningfully stronger foundation for your first few weeks of live performance — which matters enormously for how Apple's discovery algorithms perceive your app early on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Submission: What Happens After You Hit Submit
&lt;/h2&gt;

&lt;p&gt;Once you've submitted your build, Apple's review process typically takes anywhere from 24 hours to a few days, though this can vary based on submission volume and the complexity of your app. A few practical notes for this stage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor your App Store Connect dashboard closely.&lt;/strong&gt; Status changes — "In Review," "Rejected," "Pending Developer Release," "Ready for Sale" — happen without much advance notice, and rejection messages often require prompt action to keep your submission moving through the queue efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read rejection messages carefully and respond directly to the specific issue cited.&lt;/strong&gt; Resubmitting without actually addressing the flagged concern (or addressing a different concern than what was actually flagged) tends to result in repeated rejection cycles that waste valuable time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have your launch marketing prepared in advance,&lt;/strong&gt; but don't fully commit to a specific launch date publicly until you've actually received approval. Review timelines, while generally predictable, aren't perfectly guaranteed, and building anticipation around a hard date you can't fully control can create unnecessary pressure if review takes longer than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead: Staying Current With Platform Changes
&lt;/h2&gt;

&lt;p&gt;One thing that's genuinely important to internalize about iOS development: Apple's requirements evolve, sometimes significantly, from year to year. Privacy requirements, SDK compliance rules, minimum supported iOS versions, and even Xcode's own build requirements shift regularly, which means guidance that was accurate a year or two ago can be meaningfully outdated by the time you're actually submitting your game.&lt;/p&gt;

&lt;p&gt;For a more comprehensive, up-to-date walkthrough of the full submission process — covering everything from initial Apple Developer account setup through App Store Connect configuration and submission — this guide on &lt;a href="https://unitysourcecode.net/blog/how-to-launch-a-unity-game-on-ios-in-2026" rel="noopener noreferrer"&gt;how to launch a Unity game on iOS in 2026&lt;/a&gt; is a solid, current resource that goes deeper into the specific configuration steps than I have room for here. It's worth reading in full if you're preparing for your first (or even your fifth) iOS submission, since it reflects the current state of Apple's requirements rather than older guidance that may no longer fully apply.&lt;/p&gt;

&lt;p&gt;If you're actively building your game right now and want to explore additional source code, templates, or reference projects while you work through your development pipeline, it's also worth browsing through &lt;a href="https://unitysourcecode.net/newest-items" rel="noopener noreferrer"&gt;the newest items available&lt;/a&gt; for fresh, up-to-date Unity resources that reflect current best practices and platform requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Shipping a Unity game to iOS is genuinely more involved than most developers expect going in, but it's absolutely manageable with the right preparation. Set up your build pipeline correctly from the start, understand your certificates and provisioning profiles before you're troubleshooting them under submission deadline pressure, take App Store review's compliance requirements seriously, and don't neglect your retention systems just because they're not required for technical approval.&lt;/p&gt;

&lt;p&gt;The developers who have the smoothest iOS launches aren't the ones who got lucky — they're the ones who treated the submission process as a real technical and procedural workflow deserving genuine planning, rather than an afterthought tacked onto the end of development. Give it that respect, and your first (or next) iOS launch will go a lot more smoothly than you might be expecting.&lt;/p&gt;

</description>
      <category>unitygame</category>
      <category>gamechallenge</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Push Notifications in Unity: The Retention System Most Devs Ship Too Late (Or Not At All)</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Fri, 31 Jul 2026 17:38:27 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/push-notifications-in-unity-the-retention-system-most-devs-ship-too-late-or-not-at-all-2okf</link>
      <guid>https://dev.to/unitysourcecode/push-notifications-in-unity-the-retention-system-most-devs-ship-too-late-or-not-at-all-2okf</guid>
      <description>&lt;p&gt;If you've shipped a Unity mobile game before, you've probably lived through this exact sequence: install numbers look decent, first-session engagement looks fine, and then you open your Day 7 retention cohort and quietly die inside.&lt;/p&gt;

&lt;p&gt;It happens to almost everyone. And the frustrating part is that it's rarely because the game is bad — it's because nothing in the app is actively reminding players it exists. Phones are crowded, competitive environments. Unlike a messaging app or email client, there's no built-in reason for someone to reopen your game unprompted. That gap is exactly what push notifications are for, and it's one of the highest-leverage, lowest-cost systems you can add to a mobile title — yet a surprising number of solo devs and small studios either skip it entirely or bolt it on so late (and so poorly) that it barely moves the needle.&lt;/p&gt;

&lt;p&gt;This post walks through why notifications matter, the difference between local and remote notifications, the actual Unity implementation using Firebase, and the practical mistakes that quietly wreck open rates and drive uninstalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Actually Matters for Retention
&lt;/h2&gt;

&lt;p&gt;Every dev obsesses over acquisition — ad creatives, ASO, influencer deals, UA spend. Comparatively little energy goes into what happens after the first session ends, even though that's usually where the real losses happen.&lt;/p&gt;

&lt;p&gt;Push notifications are effective specifically because they don't require additional acquisition spend. You already have the player. You already (assuming they opted in) have permission to reach them again. The only question is whether you're using that channel well.&lt;/p&gt;

&lt;p&gt;Games with a deliberate notification strategy consistently show better Day 7 and Day 30 retention than games without one. And retention compounds directly into revenue — a player who comes back regularly has more chances to spend currency, watch a rewarded ad, or engage with a live event than someone who opened your app once and forgot it existed.&lt;/p&gt;

&lt;p&gt;If you're deep into building a specific genre and want to see how retention mechanics get baked directly into game design rather than bolted on afterward, it's worth checking out this breakdown of building progression and matching systems into a fashion-style Unity game — a genre where daily-return hooks are basically part of the core loop: &lt;a href="https://dev.to/unitysourcecode/building-a-fashion-game-in-unity-outfit-matching-progression-3832"&gt;Building a Fashion Game in Unity: Outfit Matching &amp;amp; Progression&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local vs. Remote Notifications: Know Which One You Need
&lt;/h2&gt;

&lt;p&gt;A lot of confusion here comes from developers treating "push notifications" as one monolithic feature. There are actually two distinct systems, and picking the right one for a given use case matters both architecturally and strategically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local notifications&lt;/strong&gt; are scheduled entirely on-device — no server call, no network dependency at delivery time. The OS fires the notification based on instructions the app gave it earlier. Perfect for anything timer-based: energy regeneration, harvest-ready alerts, streak reminders, "your build finished" messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote notifications&lt;/strong&gt; (push notifications in the strict sense) come from a server, typically Firebase Cloud Messaging, and travel over the network. These are for anything dynamic or centrally controlled: sale announcements, live-ops events, new content drops, or personalized win-back campaigns for players who've gone quiet.&lt;/p&gt;

&lt;p&gt;These aren't competing systems — a solid retention stack runs both. Local for predictable, mechanical triggers. Remote for anything that needs server-side targeting or dynamic copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Touch Any Code
&lt;/h2&gt;

&lt;p&gt;Make sure you've got:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unity 2021 LTS or newer (2022 LTS is the safer bet for package compatibility)&lt;/li&gt;
&lt;li&gt;A Firebase project (free tier is enough)&lt;/li&gt;
&lt;li&gt;Android Studio installed for SDK/build tooling&lt;/li&gt;
&lt;li&gt;An Apple Developer account if you're targeting iOS (required for APNs credentials)&lt;/li&gt;
&lt;li&gt;A physical Android and/or iOS device — notifications frequently misbehave on emulators/simulators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one wastes more dev hours than it should. If your notifications "aren't firing," check whether you're testing on a simulator before you start tearing apart your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking Your Stack
&lt;/h2&gt;

&lt;p&gt;Three realistic options:&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase Cloud Messaging (FCM)
&lt;/h3&gt;

&lt;p&gt;Free, cross-platform, full control over server-side logic. This is the default choice for most teams since there's no subscription cost and it integrates cleanly with Unity's official SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  OneSignal
&lt;/h3&gt;

&lt;p&gt;A solid third-party option if you'd rather not run your own backend messaging logic. Generous free tier, built-in analytics, A/B testing, and a dashboard usable by non-technical teammates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unity Mobile Notifications Package (local only)
&lt;/h3&gt;

&lt;p&gt;Unity's first-party package for local, device-scheduled notifications. No server, lightweight, fine if your retention strategy is purely timer-driven.&lt;/p&gt;

&lt;p&gt;For most commercial titles, the practical answer is a hybrid: Unity Mobile Notifications for local reminders, FCM for remote campaigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation, Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Set Up Firebase
&lt;/h3&gt;

&lt;p&gt;Create a Firebase project, register your app with a package name matching your Unity Player Settings exactly, and download the platform config files (&lt;code&gt;google-services.json&lt;/code&gt; for Android, &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; for iOS).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install the SDK
&lt;/h3&gt;

&lt;p&gt;Import the Firebase Messaging Unity package via &lt;code&gt;Assets &amp;gt; Import Package &amp;gt; Custom Package&lt;/code&gt;, let the External Dependency Manager resolve native Android/iOS libraries, and drop the config files into your &lt;code&gt;Assets&lt;/code&gt; folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Android Config
&lt;/h3&gt;

&lt;p&gt;Confirm your package name matches Firebase's registration, verify your keystore under Publishing Settings, and enable Custom Main Manifest / Custom Gradle Template if you need custom notification icons or channels. Rebuild once so dependencies settle.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. iOS Config (APNs)
&lt;/h3&gt;

&lt;p&gt;Generate an APNs Authentication Key from your Apple Developer account (preferred over a certificate — it doesn't expire), upload it under Firebase's Cloud Messaging settings, and enable Push Notifications + Background Modes → Remote Notifications in Xcode after building out from Unity.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Unity Code
&lt;/h3&gt;

&lt;p&gt;Minimal setup for initializing Firebase Messaging and capturing device tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Firebase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Firebase.Messaging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PushNotificationManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FirebaseApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckAndFixDependenciesAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ContinueWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DependencyStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Available&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;FirebaseMessaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenReceived&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnTokenReceived&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;FirebaseMessaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageReceived&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;OnMessageReceived&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Firebase dependency error: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnTokenReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TokenReceivedEventArgs&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Send this token to your backend to target this device&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Token: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnMessageReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MessageReceivedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle in-app display when the app is in the foreground&lt;/span&gt;
        &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Message: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach this to a persistent &lt;code&gt;GameObject&lt;/code&gt; with &lt;code&gt;DontDestroyOnLoad&lt;/code&gt; so it initializes once per session and survives scene loads.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Foreground vs. Background
&lt;/h3&gt;

&lt;p&gt;Behavior differs by app state. Foreground: &lt;code&gt;MessageReceived&lt;/code&gt; fires and you decide how to display it — most devs swap in a soft in-app banner instead of a jarring system popup. Background/closed: the OS handles display automatically from your server payload. Make sure that payload includes both a &lt;code&gt;notification&lt;/code&gt; block (for OS-level display) and a &lt;code&gt;data&lt;/code&gt; block (for custom in-app handling).&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Local Notifications
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Unity.Notifications.Android&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AndroidNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"game_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game Notifications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Importance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Reminders and rewards"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;AndroidNotificationCenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AndroidNotification&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Your reward is ready!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Come back and claim your daily bonus."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FireTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;AndroidNotificationCenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"game_channel"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Test on Real Devices
&lt;/h3&gt;

&lt;p&gt;Send a test campaign from the Firebase Console under Engage → Messaging, and verify all three app states — foreground, background, fully closed. Confirm tapping a notification deep-links correctly to the intended screen. A notification that doesn't route the player anywhere useful wastes the exact moment of intent you worked to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices That Actually Matter
&lt;/h2&gt;

&lt;p&gt;Getting the plumbing right is half the job. The other half is not turning your notification channel into the reason someone uninstalls.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap frequency.&lt;/strong&gt; One to two per day max for most casual games. Cross that consistently and opt-out rates climb fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time around actual activity&lt;/strong&gt;, not a fixed global schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write benefit-driven copy.&lt;/strong&gt; "Your energy is full — play now!" beats vague copy like "come back and play."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment your audience.&lt;/strong&gt; Whales, lapsed players, and new users need different messaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect opt-outs completely.&lt;/strong&gt; If someone disables notifications at the OS level, that's final.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B test copy and timing.&lt;/strong&gt; Small wording or scheduling tweaks often move open rates more than intuition suggests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistakes That Keep Showing Up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting to request runtime permission on Android 13+ via &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; — skip this and delivery silently fails on newer devices.&lt;/li&gt;
&lt;li&gt;Testing on only one platform. Android and iOS handle background delivery differently enough that something working on one can completely fail on the other.&lt;/li&gt;
&lt;li&gt;Hardcoding notification copy directly into a build instead of pulling from remote config — every wording tweak then needs a full release.&lt;/li&gt;
&lt;li&gt;Ignoring time zones on server-side campaigns for a global player base.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Genre Shapes Strategy
&lt;/h2&gt;

&lt;p&gt;Notification cadence isn't one-size-fits-all. Puzzle and merge games benefit from daily challenge reminders and streak nudges. Skill/arcade titles often use notifications for new levels, leaderboard resets, or tournament results. Action and combat games lean on remote, server-driven campaigns for limited-time events or PvP tournaments where urgency matters more than routine reminders.&lt;/p&gt;

&lt;p&gt;If you're exploring which base project or genre fits a notification-driven retention loop best, browsing a range of ready-built Unity projects is a fast way to see how timers, rewards, and notification hooks are typically structured in practice: &lt;a href="https://unitysourcecode.net/products" rel="noopener noreferrer"&gt;Unity Source Code — All Products&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring Whether It's Working
&lt;/h2&gt;

&lt;p&gt;Shipping the pipeline isn't the finish line — it's the start of a feedback loop. Track open rate (is your copy/timing landing at all?), opt-out rate over time (are you sending too often?), and session return rate specifically attributable to a notification, not just organic reopens. Firebase's console gives basic delivery data out of the box, but piping notification events into your analytics stack lets you slice performance by cohort and campaign type — a "daily reward ready" local ping and a "flash sale ending" remote push are solving different problems and shouldn't be judged against the same baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Note on Permission Prompts
&lt;/h2&gt;

&lt;p&gt;When you ask for permission matters almost as much as how you ask. Requesting it the instant someone opens the app — before any positive experience — tends to produce lower opt-in than waiting until after a meaningful first-session moment, like finishing the tutorial. A lot of successful games use a soft, in-game prompt first ("get notified when your energy refills") with a clear opt-in button, and only trigger the native OS permission dialog after the player agrees. That two-step approach usually beats a cold native prompt because the player has context before the system interrupts them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. Buy
&lt;/h2&gt;

&lt;p&gt;Here's the honest math: a correct, cross-platform notification system — Firebase setup, platform-specific config, Android permission handling, APNs for iOS, and real-device testing across every app state — is not a one-afternoon task, especially the first time. Even for an experienced dev, it can eat several days of focused engineering time.&lt;/p&gt;

&lt;p&gt;That's exactly why plenty of developers start from an existing, structured Unity codebase instead of building every system from a blank project. A solid foundation with monetization, UI, and notification infrastructure already wired in frees up your time for what actually makes your game different, instead of re-solving problems the industry has already solved many times over.&lt;/p&gt;

&lt;p&gt;For the full step-by-step technical breakdown — complete C# implementation for both Firebase Cloud Messaging and Unity's local notification package, platform-specific config for Android and iOS, and a detailed FAQ covering edge cases — check out the original in-depth guide: &lt;a href="https://unitysourcecode.net/blog/add-push-notifications-to-a-unity-mobile-game" rel="noopener noreferrer"&gt;How to Add Push Notifications to a Unity Mobile Game (Step-by-Step)&lt;/a&gt;. It goes deeper than what's practical to cover in a single post, with code you can drop directly into your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Push notifications are a small technical investment with a disproportionately large payoff in retention and revenue. The mechanics — Firebase for remote, native scheduling for local — aren't especially exotic once you've built the system once. What separates a good strategy from a bad one is restraint: fewer, better-timed, more personalized messages instead of maximum volume.&lt;/p&gt;

&lt;p&gt;If you're still deciding on your game's technical foundation, starting from an existing, production-ready Unity project can shortcut a meaningful chunk of this infrastructure work. Either way, the goal doesn't change — give players a genuine, well-timed reason to open your game again, and then get out of their way.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Building a Fashion Game in Unity: Outfit Matching &amp; Progression</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:18:14 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-fashion-game-in-unity-outfit-matching-progression-3832</link>
      <guid>https://dev.to/unitysourcecode/building-a-fashion-game-in-unity-outfit-matching-progression-3832</guid>
      <description>&lt;p&gt;&lt;em&gt;A technical look at what actually goes into dress-up and styling games — the genre that looks simple on the surface and turns out to be a genuinely interesting systems design problem underneath.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Fashion, dress-up, and "styling blogger" style games are deceptively complex under the hood. They combine inventory systems, weighted scoring algorithms, progression economies, and social/sharing mechanics into a loop that &lt;em&gt;feels&lt;/em&gt; like simple dress-up but is closer, architecturally, to a lightweight simulation game. This post breaks down the core systems, the common implementation pitfalls, and how these mechanics compare to other popular casual sub-genres.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Fashion/Styling Games Are More Interesting Than They Look
&lt;/h2&gt;

&lt;p&gt;If you've never built one, it's easy to assume a "dress-up" or "fashion blogger" style game is mostly an art and UI problem — swap some sprites, let the player click through outfit pieces, done. Once you actually start implementing one, though, a different picture emerges. These games sit at the intersection of a few genuinely non-trivial systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Combinatorial inventory management&lt;/strong&gt; — dozens (sometimes hundreds) of clothing items across multiple slots, with valid and invalid combinations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring/matching algorithms&lt;/strong&gt; — determining whether an outfit "works" based on style tags, color theory, or theme matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progression economies&lt;/strong&gt; — unlocking new items, currencies, and content at a pace that keeps players engaged without feeling grindy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social and sharing loops&lt;/strong&gt; — letting players show off outfits, vote on others' choices, or compete in styling challenges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are unique to fashion games individually, but the way they interact — an outfit choice affecting a score, which affects currency earned, which affects what unlocks next, which affects what the player can show off — creates a surprisingly dense dependency graph for what looks like a casual, low-stakes game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 1: The Inventory and Slot Architecture
&lt;/h2&gt;

&lt;p&gt;The foundation of any styling game is how you represent "an outfit." The naive approach — a flat list of currently equipped items — breaks down fast once you need to support outfit saving, sharing, or comparison.&lt;/p&gt;

&lt;p&gt;A more scalable pattern is to treat each clothing category as a distinct &lt;strong&gt;slot&lt;/strong&gt;, backed by a ScriptableObject-driven item database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ClothingItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Fashion/ClothingItem"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClothingItemData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ClothingSlot&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Top, Bottom, Shoes, Accessory, etc.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Sprite&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;StyleTag&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;styleTags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Casual, Formal, Edgy, Pastel, etc.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ColorPalette&lt;/span&gt; &lt;span class="n"&gt;dominantColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;unlockCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isPremium&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ClothingSlot&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Hair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Shoes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Accessory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Outerwear&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using ScriptableObjects here (rather than hardcoded item lists) gives designers the ability to add new clothing items without touching code — which matters a lot in a genre that lives and dies on regular content updates. New seasonal collections, event-limited items, and collaboration outfits are basically expected content cadence for this genre, so your data architecture needs to support fast iteration from day one.&lt;/p&gt;

&lt;p&gt;The outfit itself becomes a simple dictionary keyed by slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutfitState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ClothingSlot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClothingItemData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;equippedItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Equip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClothingItemData&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;equippedItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ClothingItemData&lt;/span&gt; &lt;span class="nf"&gt;GetEquipped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClothingSlot&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;equippedItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes it trivial to serialize an outfit for saving, sharing, or comparing against a "target look" the player is trying to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 2: Outfit Scoring and Matching Logic
&lt;/h2&gt;

&lt;p&gt;This is where things get genuinely interesting from a design and implementation standpoint. Most styling games need some way to evaluate "how good" an outfit is — whether that's matching a target theme, satisfying a client's request, or just generating a numeric score for a leaderboard.&lt;/p&gt;

&lt;p&gt;There are a few common approaches, in increasing order of complexity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag-overlap scoring&lt;/strong&gt; — the simplest approach. Each item carries style tags (Casual, Formal, Edgy, Pastel), and the outfit's score is based on how many tags across the equipped items match the level's target tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;CalculateTagScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutfitState&lt;/span&gt; &lt;span class="n"&gt;outfit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StyleTag&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;targetTags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;outfit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllEquipped&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;styleTags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Color harmony scoring&lt;/strong&gt; — a step up in complexity, this evaluates whether the dominant colors across equipped items form a visually coherent palette (complementary, analogous, monochrome), rather than just clashing randomly. This usually requires converting sprite dominant colors to HSV and comparing hue relationships rather than doing naive RGB distance checks, which tends to produce much more visually sensible "does this look good together" results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weighted multi-factor scoring&lt;/strong&gt; — the most sophisticated (and most common in successful commercial titles), combining tag matching, color harmony, item rarity/tier, and sometimes a randomized "critic reaction" element to keep results from feeling too deterministic.&lt;/p&gt;

&lt;p&gt;Getting this scoring system to feel &lt;em&gt;fair&lt;/em&gt; to the player is one of the trickier design/tuning problems in the genre — score it too strictly and players feel punished for creative choices; score it too loosely and the mechanic stops feeling meaningful at all. Expect to spend real playtesting time tuning weight values here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 3: Progression and Unlock Economy
&lt;/h2&gt;

&lt;p&gt;Once the core outfit-scoring loop works, the next system is what keeps players coming back: progression. This typically layers a few interlocking mechanics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Soft currency&lt;/strong&gt; earned through gameplay (completing looks, winning styling challenges)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard/premium currency&lt;/strong&gt; available through purchase or rare rewards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlockable item tiers&lt;/strong&gt;, often gated by both currency and player level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seasonal/limited-time content&lt;/strong&gt; to create urgency and recurring engagement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A clean way to structure this in Unity is to keep the economy logic entirely separate from the UI and gameplay logic, using an event-driven approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrencyManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnCurrencyChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;softCurrency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TrySpend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;softCurrency&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;softCurrency&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;OnCurrencyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;softCurrency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;softCurrency&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;OnCurrencyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;softCurrency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping currency and unlock logic decoupled from any specific UI screen means you can reuse the same economy backbone across a shop screen, a reward popup, and a progression map without duplicating logic — a pattern that pays off significantly as the game's content grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 4: Social and Sharing Mechanics
&lt;/h2&gt;

&lt;p&gt;The "blogger" framing in this genre usually implies some kind of social loop layered on top of the core styling gameplay — sharing an outfit for likes/votes, competing in styling challenges against other players, or building a follower count that gates new content.&lt;/p&gt;

&lt;p&gt;Technically, this typically requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend (Firebase, PlayFab, or custom) to store and retrieve shared outfit data&lt;/li&gt;
&lt;li&gt;A voting/rating system with basic anti-abuse protections (rate limiting, duplicate-vote prevention)&lt;/li&gt;
&lt;li&gt;A feed or gallery UI capable of loading and displaying a paginated list of community content efficiently, without loading everything into memory at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is genuinely one of the more backend-heavy corners of casual mobile game development, and it's worth scoping carefully — a full social sharing system is a meaningfully larger engineering investment than the core dress-up loop itself, and it's worth prototyping the core styling mechanic first before committing to the social layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing This to Other Systems-Heavy Casual Genres
&lt;/h2&gt;

&lt;p&gt;It's worth noting that fashion/styling games aren't the only casual genre where the "looks simple, is actually a systems design problem" pattern shows up. Parking and spatial-puzzle games are another good example — mechanically about as far from fashion games as you can get, but architecturally similar in that a simple-looking core interaction hides a fair amount of underlying complexity.&lt;/p&gt;

&lt;p&gt;There's a solid technical breakdown of exactly this in a systems-level write-up of a park-and-match mechanic: &lt;a href="https://dev.to/unitysourcecode/building-a-parking-puzzle-in-unity-a-systems-breakdown-of-the-park-match-mechanic-2jpj"&gt;Building a Parking Puzzle in Unity: A Systems Breakdown of the Park &amp;amp; Match Mechanic&lt;/a&gt;. It's a genuinely different genre, but the underlying lesson is the same one that applies here — "simple to play" and "simple to build" are two very different claims, and a lot of the genre's real engineering complexity lives in state management and scoring logic that never shows up on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Implementation Pitfalls
&lt;/h2&gt;

&lt;p&gt;A few mistakes tend to show up repeatedly in styling/dress-up game codebases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coupling scoring logic directly to UI code&lt;/strong&gt;, making it impossible to unit test or reuse the scoring algorithm elsewhere (like an AI-suggested outfit feature)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding item data&lt;/strong&gt; instead of using a data-driven asset pipeline, which turns every content update into a code change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pooling for gallery/feed UI elements&lt;/strong&gt;, causing frame drops when scrolling through community outfit feeds with dozens of entries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overly strict scoring thresholds&lt;/strong&gt; discovered only after launch, frustrating players who feel like their creative outfit choices are being "wrong" answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underestimating backend scope&lt;/strong&gt; for the social/sharing layer, treating it as an afterthought rather than a real infrastructure investment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of these are avoidable with early architectural decisions — particularly keeping data, logic, and presentation cleanly separated from the start, which matters more in this genre than it might initially seem given how content-heavy and frequently updated these games tend to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Studying a Working Implementation
&lt;/h2&gt;

&lt;p&gt;Reading about these systems in the abstract only gets you so far — at some point, it's worth looking at a fully built, working implementation to see how the inventory, scoring, and progression systems actually fit together in a shipped project. There's a working example of exactly this kind of styling/dress-up mechanic available here: &lt;a href="https://unitysourcecode.net/product/fashion-blogger-game-in-unity" rel="noopener noreferrer"&gt;Fashion Blogger Game in Unity&lt;/a&gt;, built out with the slot-based inventory, outfit scoring, and progression systems described above already implemented and functioning end to end. It's a useful reference point if you're trying to see how these pieces connect in practice rather than just in isolated code snippets.&lt;/p&gt;

&lt;p&gt;If you're exploring this genre more broadly, or comparing it against other casual mechanics before deciding what to build, it's also worth browsing a wider set of ready-made Android-focused casual titles for a sense of how different genres solve similar retention and progression problems: &lt;a href="https://unitysourcecode.net/blog/best-ready-made-unity-games-for-android" rel="noopener noreferrer"&gt;Best Ready-Made Unity Games for Android&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Fashion and styling games are a great example of a genre where the player-facing simplicity is doing a lot of work to hide real systems complexity underneath. Slot-based inventory, weighted scoring algorithms, layered progression economies, and social sharing mechanics all need to work together cleanly — and the genre's expectation of frequent content updates means your architecture needs to be data-driven from day one, not bolted on after the fact.&lt;/p&gt;

&lt;p&gt;If you're building something in this space, the advice that seems to hold up consistently is: get your data layer (ScriptableObject-driven items, decoupled scoring logic, event-driven currency systems) right early. Content will keep growing for as long as the game is live, and a clean architecture is what determines whether that growth stays manageable or turns into a maintenance burden six months in.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Parking Puzzle in Unity: A Systems Breakdown of the Park Match Mechanic</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:35:22 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-parking-puzzle-in-unity-a-systems-breakdown-of-the-park-match-mechanic-2jpj</link>
      <guid>https://dev.to/unitysourcecode/building-a-parking-puzzle-in-unity-a-systems-breakdown-of-the-park-match-mechanic-2jpj</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you want to see the finished product this breakdown is loosely based on, there's a working template here: &lt;a href="https://unitysourcecode.net/product/park-match-unity-game-template" rel="noopener noreferrer"&gt;Park Match Unity Game Template&lt;/a&gt;. Everything below applies whether you're building something similar from scratch or extending an existing base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Parking Puzzles Are Harder Than They Look
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Valid-move detection&lt;/strong&gt;: how do you know, at any given moment, which vehicles can actually move given their orientation and the current board state?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path resolution&lt;/strong&gt;: once a vehicle starts moving, how does it navigate around other vehicles and obstacles without clipping through them or getting stuck mid-animation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match/clear logic&lt;/strong&gt;: 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?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level authoring at scale&lt;/strong&gt;: 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?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 1: Valid-Move Detection
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ParkingGrid&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[,]&lt;/span&gt; &lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1 = empty, otherwise vehicleId&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ParkingGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;occupancy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
                &lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsPathClear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nf"&gt;InBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;occupancy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;InBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 2: Path Resolution Without Fighting Physics
&lt;/h2&gt;

&lt;p&gt;A tempting shortcut is to drive vehicle movement through Unity's physics engine directly — &lt;code&gt;Rigidbody2D&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;A more predictable approach separates &lt;strong&gt;grid-state movement&lt;/strong&gt; (instant, logical, resolved before any animation plays) from &lt;strong&gt;presentation movement&lt;/strong&gt; (purely visual, tweened, guaranteed not to fight with physics):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VehicleController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;gridPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;facingDirection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ParkingGrid&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TryMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;onArrived&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ComputeExitDistance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsPathClear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gridPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facingDirection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;onArrived&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// move rejected, no animation plays&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gridPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;targetWorldPos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GridToWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gridPosition&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;facingDirection&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;StartCoroutine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;AnimateMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetWorldPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onArrived&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;AnimateMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.35f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deltaTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key decision here is that the grid state resolves &lt;strong&gt;before&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 3: Match and Clear Logic Without Double-Triggers
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The fix is to route every possible clear condition through a single authority rather than letting each system independently decide a vehicle is "done":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClearAuthority&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clearedThisFrame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RequestClear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;vehicleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClearReason&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clearedThisFrame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vehicleId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already handled&lt;/span&gt;
        &lt;span class="n"&gt;clearedThisFrame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vehicleId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;BoardStateManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveVehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vehicleId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ScoreManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterClear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vehicleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;LevelWinChecker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckWinCondition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LateUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clearedThisFrame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ClearReason&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ReachedExit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MatchedPair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the exit-detection script and the match-detection script call &lt;code&gt;ClearAuthority.RequestClear&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 4: Level Data as Content, Not Code
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;ScriptableObject&lt;/code&gt; assets that the runtime scene reads, rather than baking layouts directly into hand-built scenes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"LevelLayout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Level Layout"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LevelLayout&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VehiclePlacement&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;gridPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;facingDirection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;VehicleType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;colorIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;gridWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;gridHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VehiclePlacement&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;placements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;moveLimit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single generic level-loader scene reads whichever &lt;code&gt;LevelLayout&lt;/code&gt; asset is active, spawns vehicles at the specified grid positions, and wires them into the same &lt;code&gt;ParkingGrid&lt;/code&gt; and &lt;code&gt;ClearAuthority&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 5: Monetization Hooks at Natural Break Points
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRewardService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OfferExtraMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;onGranted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;onDeclinedOrFailed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LevelStallHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IRewardService&lt;/span&gt; &lt;span class="n"&gt;rewardService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnNoValidMovesRemaining&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rewardService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OfferExtraMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;onGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MoveLimitManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GrantExtraMove&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;onDeclinedOrFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LevelWinChecker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TriggerLevelFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LevelStallHandler&lt;/code&gt; doesn't know or care whether &lt;code&gt;IRewardService&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the Core Loop
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://unitysourcecode.net/products" rel="noopener noreferrer"&gt;Unity Source Code's full product catalog&lt;/a&gt; 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 &lt;a href="https://dev.to/unitysourcecode/building-a-relaxing-coloring-game-in-unity-a-systems-breakdown-of-mandala-fill-mechanics-26ad"&gt;earlier piece on building a relaxing coloring game in Unity&lt;/a&gt;, and a lot of the same underlying principles show up there too: decoupled input handling, &lt;code&gt;ScriptableObject&lt;/code&gt;-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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Building a Relaxing Coloring Game in Unity: A Systems Breakdown of Mandala Fill Mechanics</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Mon, 27 Jul 2026 17:48:39 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-relaxing-coloring-game-in-unity-a-systems-breakdown-of-mandala-fill-mechanics-26ad</link>
      <guid>https://dev.to/unitysourcecode/building-a-relaxing-coloring-game-in-unity-a-systems-breakdown-of-mandala-fill-mechanics-26ad</guid>
      <description>&lt;p&gt;Coloring games look simple from the outside. Tap a shape, it fills with a color, move to the next shape. But if you've ever tried to build one that feels &lt;em&gt;good&lt;/em&gt; — smooth fills, no flood-fill lag, palettes that update instantly, and progress that survives an app restart — you know there's a surprising amount of systems design hiding behind that simplicity.&lt;/p&gt;

&lt;p&gt;I recently went through the architecture of a mandala-style coloring 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 region detection works, how the palette and fill pipeline talk to each other, how zoom/pan is handled without killing frame rate on low-end Android devices, and how the save system persists a half-finished mandala across sessions.&lt;/p&gt;

&lt;p&gt;If you want to see the finished product this breakdown is based on, there's a working template here: &lt;a href="https://unitysourcecode.net/product/mandala-coloring-unity-template" rel="noopener noreferrer"&gt;Mandala Coloring Unity Template&lt;/a&gt;. Everything below applies whether you're building from scratch or extending an existing base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Coloring Games Are Harder Than They Look
&lt;/h2&gt;

&lt;p&gt;The core loop of a coloring game — tap, fill, repeat — is trivial to describe and non-trivial to implement well. Three problems show up almost immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Region detection&lt;/strong&gt;: how do you know which "cell" of the mandala the player just tapped?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fill rendering&lt;/strong&gt;: how do you change that region's color without repainting the whole texture or triggering a garbage collection spike?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence&lt;/strong&gt;: how do you remember which regions are colored, in what color, across app restarts, without bloating save file size?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get any of these wrong and the game either lags on mid-range phones, looks visually broken (color bleeding past region borders), or loses player progress — all of which tank retention numbers fast in casual mobile games.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 1: Region Detection (Sprite Segmentation vs. Mesh Segmentation)
&lt;/h2&gt;

&lt;p&gt;There are two common approaches to segmenting a mandala into fillable regions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sprite-based segmentation&lt;/strong&gt; treats each closed shape in the mandala as its own separate sprite object, layered with a shared outline sprite on top. Each sprite has its own &lt;code&gt;SpriteRenderer&lt;/code&gt; and a &lt;code&gt;PolygonCollider2D&lt;/code&gt; (or &lt;code&gt;PolygonCollider2D&lt;/code&gt; auto-generated from the sprite's physics shape) so taps can be captured via standard &lt;code&gt;OnMouseDown&lt;/code&gt; or a raycast against 2D colliders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MandalaRegion&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SpriteRenderer&lt;/span&gt; &lt;span class="n"&gt;regionRenderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;regionRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;SaveSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnMouseDown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaletteManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the approach most reskinnable coloring templates use, because it keeps each region as a discrete, inspectable GameObject — artists can drop in a new mandala design just by swapping sprites and re-generating colliders, without touching code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mesh/texture-based segmentation&lt;/strong&gt; instead bakes the entire mandala into a single texture with a region ID encoded per pixel (often in an unused color channel or a separate lookup texture). A tap does a pixel read at the touch coordinate to determine the region ID, then a shader recolors just that region using a color-replace pass. This scales better for extremely dense mandalas (hundreds of tiny regions) since you avoid the overhead of hundreds of individual GameObjects and colliders, but it's harder for non-programmers to reskin since new art requires regenerating the ID lookup texture.&lt;/p&gt;

&lt;p&gt;For most mobile coloring games — including mandala templates where region counts are usually in the 40–150 range — sprite-based segmentation is the more sustainable choice. It trades a small amount of raw performance for a huge gain in reskin speed, which matters more when the business model is "sell the template and let studios re-skin it fast."&lt;/p&gt;

&lt;h2&gt;
  
  
  System 2: The Palette-to-Fill Pipeline
&lt;/h2&gt;

&lt;p&gt;The palette bar is deceptively important. Players expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instant visual feedback when they select a color (no delay before the next tap fills correctly)&lt;/li&gt;
&lt;li&gt;The currently selected color to be visually indicated (usually a border/highlight state)&lt;/li&gt;
&lt;li&gt;Colors to persist correctly if the player switches palettes mid-design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A clean way to handle this is a singleton &lt;code&gt;PaletteManager&lt;/code&gt; that other systems subscribe to via events rather than polling every frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaletteManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;PaletteManager&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;CurrentColor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnColorChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SelectColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;OnColorChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Region components then just read &lt;code&gt;PaletteManager.Instance.CurrentColor&lt;/code&gt; at the moment of the tap, rather than caching a stale value. This avoids a subtle bug that shows up in a lot of hobby coloring-game code: the player selects a new color, taps a region, but it fills with the &lt;em&gt;previous&lt;/em&gt; color because the region component cached the color reference on &lt;code&gt;Start()&lt;/code&gt; instead of reading it live.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 3: Zoom and Pan Without Killing Frame Rate
&lt;/h2&gt;

&lt;p&gt;Mandalas have fine detail, so zoom is not optional — it's core to the game feel. The naive implementation (scaling a &lt;code&gt;Canvas&lt;/code&gt; or parent &lt;code&gt;Transform&lt;/code&gt; directly in response to pinch input) works but tends to produce jittery, low-frame-rate zooming on budget Android devices because every frame recalculates layout for potentially dozens of UI-driven region objects.&lt;/p&gt;

&lt;p&gt;A more robust approach separates the &lt;strong&gt;camera&lt;/strong&gt; from the &lt;strong&gt;canvas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the mandala on a &lt;code&gt;World Space&lt;/code&gt; canvas (or plain sprites, not UI at all)&lt;/li&gt;
&lt;li&gt;Drive zoom via &lt;code&gt;Camera.main.orthographicSize&lt;/code&gt;, clamped to a min/max range&lt;/li&gt;
&lt;li&gt;Drive pan via camera position, clamped to keep the mandala roughly centered in view&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Cinemachine&lt;/code&gt; (or a lightweight custom camera rig) so zoom/pan momentum feels natural instead of snapping
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MandalaCameraRig&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;minZoom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;maxZoom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Camera&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Zoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orthographicSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orthographicSize&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;minZoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;maxZoom&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Driving zoom through the camera instead of scaling individual objects means the fill/tap logic never has to know or care about the current zoom level — a tap is still a tap, region colliders don't need re-scaling, and performance stays flat regardless of zoom depth. This is the same principle that shows up in a lot of casual mobile games: keep gameplay logic decoupled from camera/viewport state so one system's changes don't cascade into another.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 4: Save System — Persisting Partial Progress
&lt;/h2&gt;

&lt;p&gt;Coloring games are rarely finished in one sitting. A player might color 12 of 60 regions, close the app, and come back three days later expecting exactly where they left off. That means the save system needs to track, per mandala design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which regions have been filled&lt;/li&gt;
&lt;li&gt;What color each filled region currently holds&lt;/li&gt;
&lt;li&gt;Which mandala/level the player was last working on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lightweight approach avoids saving a full serialized scene and instead stores a compact dictionary keyed by region ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MandalaSaveData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;mandalaId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filledRegionIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hexColors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;// parallel array&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveSystem&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SaveSystem&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MandalaSaveData&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RecordFill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filledRegionIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ColorUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHtmlStringRGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexColors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filledRegionIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexColors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mandalaId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PlayerPrefs&lt;/code&gt; is fine for a coloring game since save payloads are small (a mandala with 150 regions is still a tiny JSON blob), but if you're extending this into something with dozens of unlockable designs, cloud sync, or account-based progress, it's worth migrating to a proper local file store or a lightweight backend fairly early — retrofitting a save system after players already have progress on-device is a much more painful migration than building it in from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  System 5: Reskin Architecture — Why This Matters for the Business Model
&lt;/h2&gt;

&lt;p&gt;A lot of coloring, puzzle, and casual mobile templates are sold specifically because they're &lt;em&gt;fast to reskin&lt;/em&gt; — a studio buys the base mechanic once and reskins it repeatedly for different markets or ad campaigns. That only works if the codebase separates &lt;strong&gt;content&lt;/strong&gt; from &lt;strong&gt;logic&lt;/strong&gt; cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All mandala designs live as prefab variants referencing the same base &lt;code&gt;MandalaController&lt;/code&gt; script&lt;/li&gt;
&lt;li&gt;Palette color sets are &lt;code&gt;ScriptableObject&lt;/code&gt; assets, not hardcoded arrays, so a new palette is a new asset, not a new build&lt;/li&gt;
&lt;li&gt;UI theme (background, buttons, fonts) is decoupled from gameplay scripts entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern isn't unique to coloring games — it's the same architectural discipline you'd want in any casual game meant to support multiple SKUs from one codebase. I covered a related angle of this — building systems that are meant to be extended and reskinned rather than rebuilt from scratch — in a previous breakdown of &lt;a href="https://dev.to/unitysourcecode/adding-leaderboards-and-achievements-to-a-unity-puzzle-game-a-systems-breakdown-21c1"&gt;adding leaderboards and achievements to a Unity puzzle game&lt;/a&gt;, which walks through wiring meta-progression systems into an existing puzzle loop without touching core gameplay code. The same separation-of-concerns logic applies whether you're adding a leaderboard to a match-3 game or adding a new mandala pack to a coloring game — the content layer and the systems layer should never know too much about each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the Core Loop
&lt;/h2&gt;

&lt;p&gt;Once the fill, palette, zoom, and save systems are solid, most of the "make it feel like a full game" work is additive rather than architectural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily challenges&lt;/strong&gt;: a &lt;code&gt;ScriptableObject&lt;/code&gt;-driven rotation that swaps which mandala is "featured" based on the system date&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradient/texture fills&lt;/strong&gt;: swap the flat &lt;code&gt;SpriteRenderer.color&lt;/code&gt; assignment for a material property block driving a gradient shader&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambient audio&lt;/strong&gt;: a simple &lt;code&gt;AudioSource&lt;/code&gt; crossfade system tied to scene load, decoupled from gameplay entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization hooks&lt;/strong&gt;: reward-based unlocks (watch an ad to unlock a premium palette or design) plumbed through the same &lt;code&gt;PaletteManager&lt;/code&gt;/save system rather than as a bolted-on separate system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these require touching the region-fill or save-system core — which is exactly the point of building the foundation this way. A coloring game with a well-separated fill/palette/save architecture can absorb months of content updates (new mandala packs, new palettes, seasonal designs) without ever needing a rewrite of the underlying mechanics.&lt;/p&gt;

&lt;p&gt;If you're building something adjacent — say, a physics-based puzzle rather than a fill-based one — a lot of the same principles (decoupled input handling, ScriptableObject-driven content, lightweight per-object save state) carry over directly. There's a similar mechanically-distinct but architecturally-related template worth looking at if nuts-and-bolts style puzzle mechanics are more your speed: &lt;a href="https://unitysourcecode.net/product/wood-nuts-bolts-screw-unity-template" rel="noopener noreferrer"&gt;Wood Nuts &amp;amp; Bolts Screw Unity Template&lt;/a&gt;, which deals with a different core loop (screw/unscrew physics puzzles) but faces a lot of the same reskin and save-state design questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Coloring games earn their "casual" label by hiding complexity, not by lacking it. The systems that make a mandala coloring game feel smooth — clean region segmentation, an event-driven palette pipeline, camera-decoupled zoom, compact save state, and a content/logic split that supports fast reskinning — are the same categories of systems you'll run into building almost any casual mobile game. Get those five things right and the rest (new designs, new palettes, new monetization hooks) is just content work layered on top of a foundation that doesn't need to change.&lt;/p&gt;

&lt;p&gt;If you're prototyping something similar, start with the region-detection decision first — sprite-based vs. texture-based segmentation shapes almost every downstream decision about performance, reskin speed, and how your save system needs to be structured. Everything else in this breakdown follows from getting that one choice right for your specific mandala density and target device range.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Adding Leaderboards and Achievements to a Unity Puzzle Game: A Systems Breakdown</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:42:42 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/adding-leaderboards-and-achievements-to-a-unity-puzzle-game-a-systems-breakdown-21c1</link>
      <guid>https://dev.to/unitysourcecode/adding-leaderboards-and-achievements-to-a-unity-puzzle-game-a-systems-breakdown-21c1</guid>
      <description>&lt;p&gt;In my last post, I walked through the tile-collection match mechanic slot-based matching, win/loss detection, level data structures, the works. A few people asked a reasonable follow-up question: okay, the core loop works, now what? What actually turns a finished prototype into something players open more than once?&lt;/p&gt;

&lt;p&gt;The honest answer is retention systems, and the two cheapest, most reliable ones you can add are leaderboards and achievements. Neither is glamorous. Neither will fix a weak core loop. But bolted onto a mechanic that already works, they're some of the highest return-on-effort systems you can build, and they're a genuinely good exercise in state management, event-driven architecture, and separating your game logic from your presentation layer — skills that transfer to basically every other system you'll build afterward.&lt;/p&gt;

&lt;p&gt;This post walks through the actual architecture: how I'd structure the data models, where the hooks into your core loop should live, and the mistakes that turn a simple feature into a tangled mess if you don't think about them up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More Than It Sounds Like It Should
&lt;/h2&gt;

&lt;p&gt;If you've shipped a game before, you already know that getting someone to open your app a second time is often harder than getting the first download. A tile-matching puzzle with no reason to return is a game people finish (or abandon) once and delete. The same game with a persistent leaderboard and a handful of well-designed achievements gives players an external reason to come back — competing with a friend's score, chasing the next unlock, closing out a completion percentage.&lt;/p&gt;

&lt;p&gt;None of that changes your core mechanic. It changes the meta-layer wrapped around it, and that meta-layer is disproportionately responsible for Day 7 and Day 30 retention numbers compared to how much development time it actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the Data Model First
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of UI code, decide what you're actually tracking. For a tile-matching puzzle like the one from my last post, a reasonable starting model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerStats&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;highScore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;totalMatchesCleared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;totalLevelsCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentWinStreak&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bestWinStreak&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;unlockedAchievementIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;Keep this separate from your gameplay state entirely. &lt;code&gt;PlayerStats&lt;/code&gt; shouldn't know anything about tiles, slots, or match logic — it just accumulates numbers over time. This separation is what saves you from a tangled mess later: your gameplay code fires events, and something else entirely is responsible for listening to those events and updating stats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven Hooks Instead of Direct Calls
&lt;/h2&gt;

&lt;p&gt;The mistake I see most often — and one I made myself the first time I built this — is calling stat-tracking code directly from inside gameplay logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ClearMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;matchedTiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... match clearing logic ...&lt;/span&gt;
    &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalMatchesCleared&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
    &lt;span class="nf"&gt;CheckAchievements&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// gameplay code shouldn't know achievements exist&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;This works fine for a five-minute prototype and becomes a nightmare the moment you want to add a new achievement, change how streaks are calculated, or reuse the same gameplay code in a different game mode. Instead, fire an event and let a dedicated manager listen for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameEvents&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnMatchCleared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnLevelCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;OnLevelFailed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MatchCleared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tileCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnMatchCleared&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tileCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LevelCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnLevelCompleted&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LevelFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnLevelFailed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;Your gameplay code now just fires the event and moves on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ClearMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;matchedTiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... match clearing logic ...&lt;/span&gt;
    &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MatchCleared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matchedTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;And a separate &lt;code&gt;StatsManager&lt;/code&gt; subscribes and owns all the bookkeeping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatsManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnMatchCleared&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;HandleMatchCleared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnLevelCompleted&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;HandleLevelCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnLevelFailed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;HandleLevelFailed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDisable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnMatchCleared&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;HandleMatchCleared&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnLevelCompleted&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;HandleLevelCompleted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnLevelFailed&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;HandleLevelFailed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleMatchCleared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tileCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalMatchesCleared&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;AchievementManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckMatchAchievements&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleLevelCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalLevelsCompleted&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentWinStreak&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bestWinStreak&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bestWinStreak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentWinStreak&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highScore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;LeaderboardManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubmitScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;AchievementManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckLevelAchievements&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleLevelFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentWinStreak&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;Now your gameplay code has zero knowledge of achievements, leaderboards, or streak logic. You can gut and rebuild the entire retention system without touching a single line of match-clearing logic, and vice versa — you can rework your tile-matching mechanic without worrying about breaking stat tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring Achievements So They Scale
&lt;/h2&gt;

&lt;p&gt;The other common mistake is hardcoding achievement conditions directly in code — an &lt;code&gt;if&lt;/code&gt; statement per achievement, scattered across your codebase. It works for three achievements. It falls apart at twenty. Define achievements as data instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Achievement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Game/Achievement"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AchievementData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;achievementId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AchievementType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;targetValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AchievementType&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TotalMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TotalLevelsCompleted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WinStreak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HighScore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;Then your &lt;code&gt;AchievementManager&lt;/code&gt; just iterates over a list of these ScriptableObjects and checks the relevant stat against each one's &lt;code&gt;targetValue&lt;/code&gt;, instead of hardcoding a check per achievement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AchievementManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;AchievementManager&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AchievementData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;allAchievements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CheckMatchAchievements&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EvaluateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalMatchesCleared&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CheckLevelAchievements&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;EvaluateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalLevelsCompleted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalLevelsCompleted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;EvaluateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WinStreak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentWinStreak&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;EvaluateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HighScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EvaluateType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allAchievements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlockedAchievementIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;achievementId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentValue&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;targetValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;UnlockAchievement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UnlockAchievement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementData&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PlayerStats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlockedAchievementIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;achievementId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AchievementUnlocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fire an event for UI to react to&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;Designing a new achievement is now a data task, not a code task — create a new &lt;code&gt;AchievementData&lt;/code&gt; asset in the editor, set its type and target value, done. No recompiling, no new &lt;code&gt;if&lt;/code&gt; statement buried somewhere in your stats manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Leaderboards vs. Platform Leaderboards
&lt;/h2&gt;

&lt;p&gt;There are really two tiers here, and it's worth building them as separate concerns rather than assuming you'll only ever need one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local leaderboards&lt;/strong&gt; are just a sorted list of scores stored on-device — useful for single-player high score tracking even before you've integrated any platform services. A simple approach using &lt;code&gt;PlayerPrefs&lt;/code&gt; for a lightweight top-10 list works fine early on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaderboardManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;LeaderboardManager&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LeaderboardKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"local_leaderboard"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SubmitScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;LoadScores&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MaxEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxEntries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;SaveScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;LoadScores&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LeaderboardKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveScores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LeaderboardKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform leaderboards&lt;/strong&gt; — Google Play Games Services on Android, Game Center on iOS — are what actually give you cross-device, social, competitive leaderboards, and they're what most players mean when they think "leaderboard." These require SDK integration rather than a homegrown system, and the implementation details (authentication flow, submitting scores through the platform API, showing the native UI) differ meaningfully between platforms and are honestly enough to warrant their own dedicated walkthrough rather than trying to cram it into this post.&lt;/p&gt;

&lt;p&gt;If you're at the point of wiring in the real platform SDKs — Play Games Services setup, Game Center authentication, achievement syncing across both platforms — I'd point you to &lt;a href="https://unitysourcecode.net/blog/how-to-add-leaderboards-and-achievements" rel="noopener noreferrer"&gt;this full walkthrough on adding leaderboards and achievements to a Unity game&lt;/a&gt;, which goes through the platform-specific setup in more depth than makes sense to duplicate here. The architecture in this post (event-driven stats, data-driven achievements) is exactly what you'd plug that platform integration into — the &lt;code&gt;LeaderboardManager.SubmitScore()&lt;/code&gt; method above is where you'd eventually call &lt;code&gt;PlayGamesPlatform.Instance.SubmitScore()&lt;/code&gt; or the iOS equivalent instead of (or alongside) the local implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI Layer: React to Events, Don't Poll
&lt;/h2&gt;

&lt;p&gt;The last piece worth getting right is how your UI finds out an achievement unlocked. Polling &lt;code&gt;PlayerStats&lt;/code&gt; every frame to check for changes is wasteful and awkward. Instead, subscribe to the event you already defined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AchievementToastUI&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;toastPrefab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;toastParent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnAchievementUnlocked&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ShowToast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnDisable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GameEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnAchievementUnlocked&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;ShowToast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ShowToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AchievementData&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;toast&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toastPrefab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toastParent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToastDisplay&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;achievement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter fullscreen mode&lt;/p&gt;

&lt;p&gt;Exit fullscreen mode&lt;/p&gt;

&lt;p&gt;This is the same pattern as your &lt;code&gt;StatsManager&lt;/code&gt; — the UI layer knows nothing about how achievements are evaluated, it just reacts to an event firing. If you later change how achievements are unlocked (server-side validation, for instance), the UI code doesn't need to change at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits Into the Bigger Picture
&lt;/h2&gt;

&lt;p&gt;If you worked through the tile-collection matcher from my previous post, everything above bolts directly onto that project without touching the core match-detection logic — that's the whole point of keeping gameplay and meta-systems decoupled through events. &lt;code&gt;GameEvents.MatchCleared()&lt;/code&gt; and &lt;code&gt;GameEvents.LevelCompleted()&lt;/code&gt; just need to be called from the same places your slot-matching and win/loss logic already lives.&lt;/p&gt;

&lt;p&gt;The broader lesson generalizes past this one genre, too. Any Unity project — puzzle, runner, idle, arcade — benefits from the same separation: gameplay systems fire events, a stats layer listens and accumulates, an achievement layer evaluates data-driven conditions, and a UI layer reacts to whatever the stats and achievement layers decide happened. Once you've built this pattern once, adding it to your next project is mostly copy-and-adapt rather than starting from zero.&lt;/p&gt;

&lt;p&gt;If you're building this into an existing project and want the deeper platform-integration details — actual Play Games Services and Game Center setup, achievement syncing, and the edge cases around offline score submission — the &lt;a href="https://unitysourcecode.net/blog/how-to-add-leaderboards-and-achievements" rel="noopener noreferrer"&gt;full leaderboards and achievements guide&lt;/a&gt; is the better resource for that layer specifically. Everything in this post is the architecture you'd want in place before you get there.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
      <category>systems</category>
    </item>
    <item>
      <title>Breaking Down the Tile-Collection Match Mechanic (and Why It's a Great Second Project for New Unity Devs)</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:42:33 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/breaking-down-the-tile-collection-match-mechanic-and-why-its-a-great-second-project-for-new-unity-9k</link>
      <guid>https://dev.to/unitysourcecode/breaking-down-the-tile-collection-match-mechanic-and-why-its-a-great-second-project-for-new-unity-9k</guid>
      <description>&lt;p&gt;f you've built a Unity project or two already and you're looking for the next thing to tackle, tile-matching puzzle games are one of the most underrated genres to study. They look simple on the surface — tap a tile, match three, clear the board — but the systems underneath (state management, win/loss detection, level data structures, UI feedback) hit almost every fundamental a new Unity developer needs to practice before jumping into something more ambitious.&lt;/p&gt;

&lt;p&gt;I want to walk through the core mechanics of a &lt;strong&gt;tile-collection match puzzle&lt;/strong&gt; — the "tap to collect, manage limited slots" variant rather than the classic swap-three-in-a-row format — and talk about why this genre is such a good learning ground, what systems you'd need to build one from scratch, and where a ready-made source code base fits into that picture if you'd rather study a working implementation than start from a blank scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Different Match-3 Philosophies
&lt;/h2&gt;

&lt;p&gt;Most people picture "match 3" as the classic swap mechanic — think Candy Crush, where you swap two adjacent tiles to form a line of three or more. That's one valid design, but it's not the only one, and honestly it's not always the friendliest one to prototype as a beginner because the swap-and-cascade logic (checking for matches after every swap, handling chain reactions, animating multiple simultaneous clears) gets complicated fast.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;tile-collection&lt;/strong&gt; variant works differently, and it's a genuinely good place to start if you're newer to gameplay systems programming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tiles sit on the board in a static grid (or scattered layout)&lt;/li&gt;
&lt;li&gt;Tapping a tile moves it into a limited-capacity "collection slot" row&lt;/li&gt;
&lt;li&gt;When three identical tiles land in the slot row, they auto-match and clear&lt;/li&gt;
&lt;li&gt;If your slots fill up before you can complete a match, it's game over&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what's different here: there's no swap logic, no gravity/cascade system, and no complex line-detection algorithm. The state machine is much simpler — you're tracking a slot array, checking for three-of-a-kind whenever a new tile enters it, and handling an overflow condition. That's a far more approachable systems-programming exercise than a cascading swap-3 clone, while still teaching you the same underlying skills: grid-based data structures, win/loss state detection, and UI that reflects game state in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Systems You'd Actually Need to Build
&lt;/h2&gt;

&lt;p&gt;If you set out to build something like this from scratch, here's roughly what you're looking at, broken into the pieces that actually matter for learning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Board and tile data model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You need a clean separation between the visual tile GameObjects and the underlying data representing what's on the board. A common approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TileData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tileTypeId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt; &lt;span class="n"&gt;gridPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isCollected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping your data model separate from your visuals (rather than reading tile type directly off a sprite or GameObject name) is one of the biggest habits worth building early — it pays off the moment you want to add save/load, level editors, or analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Slot management and match detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your collection row is really just a fixed-size array (commonly six or seven slots) that you push tiles into. Every time a tile enters, you check whether three of the same type are now present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CheckForMatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slotTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tileTypeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;ClearTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a great exercise in LINQ and collection manipulation if you're still getting comfortable with C#, and it maps directly onto win/loss detection: if the slot array is full and no group has reached the match threshold, the run ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Level data and progression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than hardcoding tile layouts in the scene, most production-quality implementations load level data from ScriptableObjects or JSON, which lets designers (or you, later) iterate on difficulty without touching code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CreateAssetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;menuName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Puzzle/LevelData"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LevelData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptableObject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;levelNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TileLayoutEntry&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;tileLayout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;slotCapacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;targetMoves&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also where you'd start thinking about difficulty curves — slot capacity, tile variety count, and layout density are your main levers for tuning how forgiving or punishing a given level feels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. UI feedback loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tile games live or die on juiciness — the little animations and feedback cues that make a match feel satisfying. Scale punches on match, a brief particle burst, a slot-row shake on overflow warning. None of this is complicated individually, but it's often the part beginners skip, and it's the part that actually determines whether a prototype feels like a real game or a spreadsheet with sprites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monetization and retention hooks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the core loop works, the systems that actually make a mobile puzzle game commercially viable are the ones layered on top: booster items, daily challenges, ad-based continues, and basic progression tracking (stars, level unlocks). These aren't complicated individually, but wiring all of them together cleanly — without turning your codebase into spaghetti — is genuinely one of the harder parts of finishing a mobile game, as opposed to just prototyping one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Genre Is a Smart Second (or Third) Project
&lt;/h2&gt;

&lt;p&gt;If you're a newer Unity developer trying to figure out what to build next, tile-matching puzzles hit a sweet spot: complex enough to teach real systems-design lessons, simple enough that you can actually finish it instead of abandoning it three weeks in. That "actually finish it" part matters more than people give it credit for — a completed simple project teaches you more than an abandoned ambitious one.&lt;/p&gt;

&lt;p&gt;If you're looking for a broader list of project ideas at a similar difficulty level — genres and mechanics that are approachable enough to complete but substantial enough to actually teach you something — this roundup of &lt;a href="https://unitysourcecode.net/blog/best-unity-projects-for-new-developers" rel="noopener noreferrer"&gt;best Unity projects for new developers&lt;/a&gt; is worth a look. It's a good way to figure out what to build next once you've got the fundamentals from a project like this one down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Studying a Finished Implementation vs. Building From Zero
&lt;/h2&gt;

&lt;p&gt;There's real value in building this kind of system from scratch once, purely for the learning experience. But there's also real value in reading a finished, production-tested implementation to see how the pieces actually fit together in a shipped project — how the slot logic handles edge cases you might not think of, how the level data is structured for a real content pipeline, how the monetization and progression systems are wired in without becoming a tangled mess.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://unitysourcecode.net/product/tile-world-match-3-unity-game" rel="noopener noreferrer"&gt;Tile World Match 3 Unity source code&lt;/a&gt; is a working example of exactly this genre — tap-to-collect tile matching with limited slot management, a modular C# structure, and hooks already in place for boosters, daily challenges, leaderboards, and AdMob integration. Whether you're using it as a reference to compare against your own from-scratch build, or as a foundation to reskin and extend into your own release, it's a useful case study in how a genuinely simple core mechanic gets turned into a complete, monetizable mobile product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;If tile-matching isn't quite the genre you want to specialize in, it's worth browsing more broadly to see what other mechanics and systems are out there. Puzzle, arcade, idle, and casual mobile genres all share a lot of the same underlying skills — grid systems, state machines, UI feedback, monetization hooks — even when the surface-level gameplay looks completely different. The &lt;a href="https://unitysourcecode.net/category/games" rel="noopener noreferrer"&gt;Games category on Unity Source Code&lt;/a&gt; is a good place to browse a wide range of genres and mechanics side by side if you want to compare a few different approaches before committing to your next project.&lt;/p&gt;

&lt;p&gt;Whichever direction you go, the underlying lesson holds: the genres that look "too simple to be interesting" are usually the ones with the best learning-to-effort ratio. Tile matching is a perfect example — a mechanic simple enough to build in a weekend, but deep enough in its data structures and state management to teach you patterns you'll reuse in every mobile game you build after it.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Monetization Almost Killed My Unity Puzzle Game (Here's What I'd Do Differently)</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Fri, 24 Jul 2026 17:58:35 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/monetization-almost-killed-my-unity-puzzle-game-heres-what-id-do-differently-3oe5</link>
      <guid>https://dev.to/unitysourcecode/monetization-almost-killed-my-unity-puzzle-game-heres-what-id-do-differently-3oe5</guid>
      <description>&lt;p&gt;I shipped my first puzzle game with a monetization plan that was basically "add AdMob, figure it out later." That decision cost me about three weeks of post-launch scrambling, a retention curve that looked like a cliff, and a very humbling lesson about how much design work "add ads" actually hides.&lt;/p&gt;

&lt;p&gt;This post is the practical, slightly embarrassing version of what I learned — not the polished "10 monetization tips" listicle version. If you're earlier in your Unity mobile journey, hopefully this saves you the detour.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Monetization isn't a checkbox you tick at the end — it's a design decision that changes your retry flow, your failure states, and your session pacing.&lt;/li&gt;
&lt;li&gt;Ads-first, IAP-first, and hybrid aren't interchangeable defaults; they map to specific genres and session lengths, and picking the wrong one for your game shape will hurt retention.&lt;/li&gt;
&lt;li&gt;Reusing systems (yours or someone else's) that already have monetization scaffolding wired in correctly saves you from re-learning these lessons the hard way.&lt;/li&gt;
&lt;li&gt;None of this is about clever code — it's about testing your assumptions against real player behavior early.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I went wrong
&lt;/h2&gt;

&lt;p&gt;My prototype was a straightforward match-based puzzle game. Core loop felt good in about two weeks. I spent the next six weeks on levels, polish, and juice — tweening, particles, the fun stuff — and left monetization for "later," treating it as a plumbing task I'd bolt on right before submission.&lt;/p&gt;

&lt;p&gt;Later arrived. I dropped in an interstitial after every level completion because that was the fastest thing to implement, and I didn't think hard about it beyond "does the SDK call fire." First week of soft launch, Day 1 retention was noticeably worse than my internal playtests had suggested it should be. Session length was short. Players were bailing right around level 4 or 5 — right where the ad frequency started to feel punishing rather than incidental.&lt;/p&gt;

&lt;p&gt;The code was fine. The ad SDK integration worked exactly as intended. The problem was that I'd treated a design decision as an implementation detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson: ad placement is part of game feel
&lt;/h2&gt;

&lt;p&gt;This sounds obvious written down, but it wasn't obvious to me mid-project: for a puzzle game, session feel &lt;em&gt;is&lt;/em&gt; the product. A rewarded ad a player opts into for an extra move feels like generosity. An interstitial that yanks them out of flow right after a level they were enjoying feels like punishment — even though, from a code perspective, both are just an SDK call with a callback.&lt;/p&gt;

&lt;p&gt;Once I rebuilt my ad placement around this — rewarded ads for hints and continues, interstitials capped and only shown at natural break points rather than every single level — retention recovered noticeably. Nothing about the underlying tech changed. Only the design thinking behind where and when those calls fired.&lt;/p&gt;

&lt;p&gt;If you want the deeper technical and business breakdown of this tradeoff — IAP vs. ad revenue, hybrid models, genre-specific benchmarks, and how session length should actually inform which model you lean into — there's a genuinely thorough guide on this here: &lt;a href="https://unitysourcecode.net/blog/in-app-purchases-vs-ad-revenue-for-unity-games" rel="noopener noreferrer"&gt;In-App Purchases vs. Ad Revenue for Unity Games&lt;/a&gt;. It covers the numbers side of this decision in more depth than I'm going to here, including conversion benchmarks and how to read your own retention curve to decide which model fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently: design monetization alongside the core loop
&lt;/h2&gt;

&lt;p&gt;If I were starting over, I'd sketch out ad and IAP touchpoints at the same time I'm sketching the core loop and failure states — not after. Concretely, that means answering these questions before writing a single line of monetization code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where does a player naturally want a boost?&lt;/strong&gt; (a failed level, a near-miss, low currency) — that's your rewarded ad opportunity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where would an interruption feel least disruptive?&lt;/strong&gt; (between distinct sessions, not mid-flow) — that's where interstitials belong, if at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's actually worth paying for in this game?&lt;/strong&gt; (remove ads, hint packs, cosmetic skins, level skips) — this shapes your IAP catalog, and it should map to what players are actually frustrated by or excited about, not a generic shop template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's my expected session length, and does that favor ads-first, IAP-first, or hybrid?&lt;/strong&gt; Short, snackable sessions lean ads. Deeper, progression-heavy sessions have more room for IAP. Most casual/puzzle games land somewhere in a hybrid model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires exotic engineering. It requires treating monetization as a design surface instead of a checklist item you rush through at the end, which is exactly the mistake I made the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger pattern: I kept re-learning the same lessons
&lt;/h2&gt;

&lt;p&gt;This wasn't actually the first time I'd relearned a version of this lesson. Save system robustness, cascade edge cases, deadlock detection — I've written before about how much of my early development time went into rediscovering the same set of problems on every new project, instead of building on what I'd already solved. I went into more of that in &lt;a href="https://dev.to/unitysourcecode/what-shipping-a-puzzle-game-in-unity-actually-taught-me-about-speed-vs-perfectionism-5234"&gt;What Shipping a Puzzle Game in Unity Actually Taught Me About Speed vs. Perfectionism&lt;/a&gt;, which covers the architecture and workflow side of this same underlying problem — building the dumbest working version first, and why perfect architecture on day one is usually wasted effort.&lt;/p&gt;

&lt;p&gt;Monetization turned out to be the same story with a different subsystem. The fix wasn't a smarter SDK integration. It was recognizing, earlier, that ad placement and IAP design are core-loop decisions, not afterthoughts — and that starting from a codebase or template where that thinking is already baked in saves you from learning it the expensive way, in production, against real retention numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're evaluating starting points
&lt;/h2&gt;

&lt;p&gt;If you're weighing whether to build your monetization layer completely from scratch or start from something with ad and IAP scaffolding already wired in sensibly, it's worth spending a little time browsing what's actually out there before committing to a from-scratch build. I've spent a fair amount of time going through the &lt;a href="https://unitysourcecode.net/category/games" rel="noopener noreferrer"&gt;Unity games catalog on Unity Source Code&lt;/a&gt; just to calibrate what reasonable monetization integration looks like across different genres — even when I ended up building pieces myself, having that reference point was useful for knowing what "done" should look like before I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If there's one thing I'd want a newer Unity dev to take from this: monetization is not the boring part you save for last. It's as much a design decision as your level difficulty curve or your retry flow, and getting it wrong doesn't show up as a bug — it shows up as a retention curve that quietly tells you players are leaving right where you made them feel punished instead of rewarded.&lt;/p&gt;

&lt;p&gt;Happy to talk through specifics in the comments if you're mid-build on your own monetization layer — curious what ad placement decisions other folks have landed on for puzzle or casual genres specifically.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What Shipping a Puzzle Game in Unity Actually Taught Me About Speed vs. Perfectionism</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:53:37 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/what-shipping-a-puzzle-game-in-unity-actually-taught-me-about-speed-vs-perfectionism-5234</link>
      <guid>https://dev.to/unitysourcecode/what-shipping-a-puzzle-game-in-unity-actually-taught-me-about-speed-vs-perfectionism-5234</guid>
      <description>&lt;p&gt;&lt;em&gt;A dev-to-dev breakdown of what actually slows puzzle game development down, and what I'd do differently if I started over.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I used to be the developer who wanted every system "done right" before moving to the next feature. Clean architecture first, then features. Sounds responsible, right? It also meant my first puzzle game prototype sat half-finished for four months while I refactored a grid system that honestly worked fine the first time.&lt;/p&gt;

&lt;p&gt;That experience — and a few shipped titles since — changed how I think about building puzzle games in Unity. This post is a collection of the actual lessons, not the polished "10 tips" version. Some of this might be obvious to folks who've shipped a dozen titles. If you're earlier in your mobile dev journey, hopefully it saves you a few of the detours I took.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Puzzle game mechanics are simple, but the &lt;em&gt;edge cases&lt;/em&gt; around them are where all your time actually goes.&lt;/li&gt;
&lt;li&gt;Perfect architecture on day one is usually wasted effort — build for the game you have, not the game you imagine you'll have in six months.&lt;/li&gt;
&lt;li&gt;Reusing proven systems (yours or someone else's) isn't cutting corners, it's just good time allocation.&lt;/li&gt;
&lt;li&gt;Shipping fast and iterating on real player data beats theorizing about what players "probably" want.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Grid System Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;Every puzzle game tutorial starts the same way: build a grid, populate it with tiles, detect matches. Easy enough that you can have something moving on screen within a day.&lt;/p&gt;

&lt;p&gt;Here's where I went wrong the first time. I got the basic match detection working, and instead of moving forward, I started "future-proofing" it — building support for hexagonal grids I didn't need, an abstract tile-type system for mechanics I hadn't designed yet, and a generic event bus for interactions that didn't exist in my actual game.&lt;/p&gt;

&lt;p&gt;None of that complexity was wrong in principle. It was wrong in timing. I was solving problems I didn't have yet, at the expense of problems I did have — like the fact that my match detection broke the moment two chain reactions happened in the same frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; build the dumbest version that solves your actual current problem. Generalize later, once you know what you're actually generalizing for. Abstracting early, before you have two or three real use cases to compare, almost always guesses wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cascades Will Humble You
&lt;/h2&gt;

&lt;p&gt;If you've never built cascade logic (the chain-reaction effect where a match causes tiles to fall, which causes new matches, which causes more falling), it sounds like a simple loop. It is not.&lt;/p&gt;

&lt;p&gt;The failure mode that got me was assuming cascades resolve sequentially and predictably. They don't, especially once you add special tiles, obstacles, or multiple simultaneous match groups. I had a bug where two match groups resolving in the same update cycle would sometimes double-count score, and it took an embarrassingly long time to track down because it only happened under specific board configurations that my manual testing rarely hit.&lt;/p&gt;

&lt;p&gt;What actually fixed it wasn't cleverer code — it was better testing discipline. I built a simple in-editor tool to load specific board states directly (bypassing random generation) so I could reproduce edge cases on demand instead of playing the game repeatedly hoping to get lucky. If you're building match-based mechanics, build that tool early. It'll save you hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Save System Nobody Thinks About Until It Breaks
&lt;/h2&gt;

&lt;p&gt;This one's less glamorous but genuinely important: your save system needs to survive the app being killed mid-write. Players background apps constantly, phones sleep, OS updates interrupt things — and if your save file gets corrupted because you were mid-write when the process died, you get a player who loses hours of progress and immediately uninstalls.&lt;/p&gt;

&lt;p&gt;The fix is straightforward once you know to look for it: write to a temp file, then atomically rename/replace the actual save file only after the write completes successfully. It's not hard. It's just easy to skip if you're focused on gameplay and treating "save the game" as an afterthought feature you'll polish later. Don't. Build it defensively from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ads Are Part of Your Game Feel, Not a Bolt-On
&lt;/h2&gt;

&lt;p&gt;I used to treat ad integration as a checkbox task at the very end of development — plug in the SDK, place a few interstitial calls, done. That's a mistake for puzzle games specifically, because ad placement directly affects session feel in a genre where session feel &lt;em&gt;is&lt;/em&gt; the product.&lt;/p&gt;

&lt;p&gt;Rewarded ads that players opt into (for hints, extra moves, or a second chance) tend to feel like part of the game's generosity. Interstitials that interrupt a player mid-flow tend to feel like punishment. The difference in perceived quality between those two approaches is enormous, even though the underlying SDK integration is nearly identical from a code perspective.&lt;/p&gt;

&lt;p&gt;If you're building a puzzle game, I'd genuinely recommend thinking about ad placement during design, not after. It changes how you structure failure states, retry flows, and reward pacing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Stopped Rebuilding the Same Systems From Scratch
&lt;/h2&gt;

&lt;p&gt;After my second puzzle title, I had a realization that felt obvious in hindsight: the systems I kept rebuilding — grid management, cascade resolution, save robustness, ad mediation — were mostly the same problems with different skins on top. I was spending 60-70% of my development time re-solving problems I'd already solved before, just because each project started from an empty Unity scene.&lt;/p&gt;

&lt;p&gt;This is roughly the same realization a lot of solo devs and small teams eventually land on, and it's why starting from a proven, already-tested codebase (whether your own internal template or a well-built third-party source code) makes so much sense once you've been burned by rebuilding core systems a few times. I wrote a longer breakdown of the different puzzle mechanic categories worth considering — match-3, merge, sorting, physics-based — along with a realistic launch timeline, in &lt;a href="https://unitysourcecode.net/blog/best-puzzle-game-source-codes-in-unity" rel="noopener noreferrer"&gt;Best Puzzle Game Source Codes in Unity&lt;/a&gt;, if you want a more structured comparison.&lt;/p&gt;

&lt;p&gt;The point isn't that you shouldn't understand how these systems work under the hood — you absolutely should, because you'll need to debug and extend them. The point is that re-implementing solved problems from zero, every single project, is a poor use of limited development time when your actual competitive advantage is in content, polish, and player experience, not in reinventing grid logic for the fifth time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Actually Prioritize If Starting Over
&lt;/h2&gt;

&lt;p&gt;If I were starting a new puzzle game project today, here's the order I'd tackle things in, based on what actually mattered versus what I wasted time on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Playable core loop first&lt;/strong&gt; — the dumbest possible version, ugly art, no polish. Get it in your hands and see if it's fun before investing further.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deadlock detection&lt;/strong&gt; — before you build a single extra level, make sure your board can never reach an unsolvable state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A board-state debugging tool&lt;/strong&gt; — the ability to load specific configurations on demand, not just random generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save system robustness&lt;/strong&gt; — atomic writes, tested against forced app kills, from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Juice and feel&lt;/strong&gt; — tweening, particles, sound feedback. This is where "functional" becomes "fun," but it's step five, not step one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization design&lt;/strong&gt; — thought through as part of the core loop, not bolted on afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content pipeline&lt;/strong&gt; — a way to add/edit levels without recompiling, so your last few weeks before launch are about content and balance, not code changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; early on this list: generic architecture, abstract systems for features you don't have yet, and premature optimization. Those come later, once real constraints tell you what actually needs generalizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed Isn't the Enemy of Quality
&lt;/h2&gt;

&lt;p&gt;There's a mindset in some corners of gamedev that shipping fast means cutting corners, and that "real" developers take their time to build things properly. I used to believe that too. What I've actually found is closer to the opposite: teams and solo devs who ship faster tend to get &lt;em&gt;more&lt;/em&gt; real player feedback per unit of time, which means their second, third, and fourth iterations are informed by actual data instead of guesswork.&lt;/p&gt;

&lt;p&gt;The developer who spends eight months building the "perfect" architecture before ever getting a build in front of real players is optimizing for a version of quality that doesn't actually matter yet. The developer who ships a rough-but-functional version in three weeks, watches real retention and funnel data, and iterates based on that — that's the version of "doing it properly" that actually correlates with games people want to keep playing.&lt;/p&gt;

&lt;p&gt;If you're weighing genre options or want to see what a broader, already-built catalog of puzzle and casual mechanics looks like — useful context even if you end up building your own from scratch, just to calibrate what "done" looks like — it's worth browsing through the &lt;a href="https://unitysourcecode.net/products/games" rel="noopener noreferrer"&gt;games catalog on Unity Source Code&lt;/a&gt; to get a sense of feature completeness and scope for this genre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;None of this is revolutionary advice. It's the kind of thing that's obvious in retrospect and completely invisible when you're heads-down building your first puzzle game. If there's one thing I'd want a newer dev reading this to take away, it's: the mechanic is not where your time goes. The edge cases, the robustness, the "boring" infrastructure around the fun part — that's where every puzzle game project actually lives or dies. Budget your time (and your patience) accordingly.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments if you're mid-build on something similar — always curious to hear what edge cases other folks have run into with cascade logic or save systems specifically.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>beginners</category>
      <category>mobile</category>
    </item>
    <item>
      <title>What a Maze-Logic Puzzle Game Taught Me About Shipping Mobile Titles Fast</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:01:54 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/what-a-maze-logic-puzzle-game-taught-me-about-shipping-mobile-titles-fast-fil</link>
      <guid>https://dev.to/unitysourcecode/what-a-maze-logic-puzzle-game-taught-me-about-shipping-mobile-titles-fast-fil</guid>
      <description>&lt;p&gt;I've spent a fair amount of time lately thinking about a question that doesn't get discussed enough in dev communities: why do some small, mechanically simple mobile puzzle games quietly outperform far more ambitious projects in terms of actual revenue per hour of development time invested? The answer, once you dig into it, has less to do with luck and more to do with a set of decisions around genre selection, technical scope, and monetization strategy that a lot of solo and indie developers get wrong on their first few attempts.&lt;/p&gt;

&lt;p&gt;I want to walk through this using a maze-navigation puzzle format as the anchor example — the kind of game where a player guides a character through an increasingly complex path structure, trying to reach an exit without getting boxed in. It's a deceptively simple mechanic, and that simplicity is exactly what makes it worth examining closely as a case study in efficient mobile game development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Underrated Strength of "Simple to Learn, Hard to Master" Mechanics
&lt;/h2&gt;

&lt;p&gt;There's a reason path-navigation puzzles have persisted across decades of gaming, from the original arcade-era maze games through to modern mobile brain-teaser apps. The core interaction — get from point A to point B without hitting an obstacle — requires zero explanation. A player understands the objective within the first two seconds of seeing the screen, with no tutorial overlay needed, no lengthy onboarding flow, nothing standing between install and engagement.&lt;/p&gt;

&lt;p&gt;That matters enormously for mobile retention metrics specifically. Every additional second a player spends figuring out what they're supposed to do before their first meaningful action is a second where churn risk increases. Genres that front-load complexity — deep strategy games, anything requiring resource management literacy, games with unfamiliar control schemes — pay a real cost in day-one retention that's easy to underestimate when you're deep in development and have long since forgotten what it's like to see the game for the first time.&lt;/p&gt;

&lt;p&gt;Maze and path-logic puzzles sidestep this problem almost entirely. The learning curve is close to zero, but the skill ceiling isn't, because level design can introduce genuinely challenging spatial reasoning problems without ever needing to add a new control or a new rule. That's an efficient use of design effort: nearly all of the "content" comes from level layout rather than from new systems, which means a solo developer can generate weeks of engaging gameplay by iterating on level design rather than writing new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Scope: Why This Genre Is Achievable for Small Teams
&lt;/h2&gt;

&lt;p&gt;From an engineering standpoint, a maze-navigation puzzle game sits in a genuinely favorable spot. The core systems involved — grid or path-based movement, collision detection against maze boundaries, win-condition checking when a player reaches an exit — are well within reach of a single developer without requiring deep expertise in physics simulation, complex AI, or advanced rendering techniques.&lt;/p&gt;

&lt;p&gt;That doesn't mean there's no craft involved in getting it right. Movement needs to feel responsive rather than sluggish, which usually means tuning input handling carefully rather than relying on default physics behavior. Maze generation and level design need enough variety that levels don't start feeling repetitive by level twenty, which is a design problem more than a technical one. And difficulty progression has to be tuned so that the jump from "easy enough to feel confident" to "hard enough to feel rewarding" happens gradually rather than in a jarring spike that causes players to quit.&lt;/p&gt;

&lt;p&gt;But compared to genres that require solving genuinely hard technical problems — real-time multiplayer synchronization, complex physics-based combat, procedural 3D world generation — the technical risk here is low. That's a meaningful consideration for anyone deciding what to build next, particularly if the goal is validating a monetization strategy or building a portfolio of shipped titles rather than pursuing one large, technically ambitious project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Most Developers Get Wrong: Monetization Placement
&lt;/h2&gt;

&lt;p&gt;Here's where I think a lot of first-time puzzle game developers leave real money on the table, and it's not because the mechanics are wrong — it's because the ad placement strategy is an afterthought bolted on right before submission rather than something designed into the game from the start.&lt;/p&gt;

&lt;p&gt;Puzzle games are unusually well-suited to rewarded video ads specifically, more so than most other genres, because the player's motivation to watch an ad is intrinsic rather than manufactured. A player who's stuck on a maze level and offered a hint in exchange for watching a short ad is making a genuinely voluntary trade that solves an actual problem they have. Compare that to games that force interstitial ads at arbitrary intervals regardless of what the player is doing — that's friction with no corresponding value delivered, and it shows up in both review scores and uninstall rates.&lt;/p&gt;

&lt;p&gt;The placement pattern that tends to perform best in this genre looks something like: rewarded ads offered as an optional hint or retry mechanism when a player is stuck, interstitial ads placed at natural breakpoints between levels rather than mid-puzzle, and occasional optional rewarded placements for bonus content or cosmetic unlocks. None of this is exotic advice, but the difference between implementing it thoughtfully versus implementing it as an afterthought is often the difference between a game that earns modestly and one that earns well, holding gameplay quality constant.&lt;/p&gt;

&lt;p&gt;There's a broader point buried in here too: the revenue difference between two functionally similar puzzle games is frequently not about which one has better core mechanics. It's about which developer thought more carefully about where and how monetization touches the player experience. That's a solvable problem, but only if you treat it as a design question rather than a technical checkbox to tick off right before launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reskinning as a Legitimate Content Strategy, Not a Shortcut
&lt;/h2&gt;

&lt;p&gt;One thing I've come to appreciate about genres like maze-navigation puzzles is how naturally they support a reskinning-based content strategy. Because the underlying mechanic — navigate a path, avoid obstacles, reach an exit — is largely independent of visual theme, the same core system can support wildly different presentations: a neon-lit cyberpunk maze, a jungle-themed escape, a minimalist geometric puzzle aesthetic. Each of these can appeal to a meaningfully different audience segment while sharing nearly all of the same underlying code.&lt;/p&gt;

&lt;p&gt;This is worth taking seriously as a strategy rather than dismissing it as a lesser form of development. A developer who validates a maze-puzzle mechanic once and then produces two or three thematically distinct variations is, in effect, testing multiple audience hypotheses at a fraction of the cost of building each one from scratch. If one theme resonates more strongly with a particular market or age group, that's genuinely useful signal — and it's signal you can only get by actually shipping multiple variations, which reskinning makes economically realistic in a way that full rebuilds don't.&lt;/p&gt;

&lt;p&gt;It also compounds over time in a way that's easy to underestimate. Each subsequent reskin benefits from lessons learned on the previous one — which difficulty curve worked, which monetization placement performed best, which visual style got better store page conversion. That accumulated knowledge is worth more, in practice, than the marginal creative satisfaction of building every project's core systems from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations That Are Easy to Overlook
&lt;/h2&gt;

&lt;p&gt;Because maze-puzzle games look mechanically simple, it's tempting to assume performance optimization barely matters. In my experience that assumption causes more problems than it prevents. Puzzle games are frequently played in short bursts — a level or two while waiting in line, a few minutes before bed — and in that context, any friction between opening the app and actually playing disproportionately hurts the experience compared to genres where players are settling in for a longer session anyway.&lt;/p&gt;

&lt;p&gt;Load times matter more than they seem like they should. Input responsiveness matters more than it seems like it should. A maze game where movement has even a slight, barely perceptible delay between input and response ends up feeling worse to play than the mechanical simplicity would suggest, precisely because there's so little else going on to distract from that friction. Getting this right requires actual device testing across a reasonable spread of hardware, not just testing in the Unity editor and assuming it'll translate cleanly to real devices — a mistake I've made myself more than once early on.&lt;/p&gt;

&lt;p&gt;This connects to a broader point about shipping mobile games generally: the gap between "works in the editor" and "performs well on a mid-range Android device in the field" is bigger than it looks from inside a development environment, and profiling on real hardware before you consider a build ready is not optional busywork — it's where a meaningful share of post-launch review complaints get prevented before they ever happen. I've written up a more detailed, checklist-style breakdown of how I approach this step specifically, which is worth a look if you're getting close to a submission and haven't done a dedicated profiling pass yet: &lt;a href="https://dev.to/unitysourcecode/profiling-your-unity-mobile-build-a-practical-checklist-before-you-shippublished-13pc"&gt;Profiling Your Unity Mobile Build: A Practical Checklist Before You Ship&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Pieces Together: A Realistic Case Study
&lt;/h2&gt;

&lt;p&gt;To make this concrete rather than abstract, it's worth looking at how these principles show up in an actual shipped template. The &lt;a href="https://unitysourcecode.net/product/snake-escape-puzzle-game" rel="noopener noreferrer"&gt;Snake Escape Puzzle Unity source code&lt;/a&gt; is a reasonable example to point to, since it's built specifically around this maze-navigation format — guiding a snake through progressively complex paths without getting trapped, with swipe-based controls, a difficulty curve that escalates gradually across levels, and AdMob integration structured around the rewarded-hint and between-level-interstitial pattern described above.&lt;/p&gt;

&lt;p&gt;What makes it a useful reference point isn't that it's doing anything mechanically novel — it isn't, and that's sort of the point. It's a clean execution of a genre that's proven itself repeatedly, built with the kind of modular structure that supports exactly the reskinning strategy discussed earlier: swappable maze themes, adjustable difficulty curves, and a codebase organized well enough that a developer can focus their effort on level design and visual identity rather than fighting the underlying architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Financial Question Worth Asking
&lt;/h2&gt;

&lt;p&gt;All of this eventually circles back to a question that's worth being honest with yourself about before investing weeks or months into any project: what's the realistic revenue ceiling for a game in this category, and does the development effort required actually make sense against that ceiling? This is a question I see a lot of developers skip entirely, either because it feels uncomfortably commercial to ask, or because it's genuinely hard to estimate without real data.&lt;/p&gt;

&lt;p&gt;I'd encourage anyone thinking seriously about building — or reskinning — a puzzle title like this to look at real numbers rather than guessing. There's a detailed breakdown worth reading on exactly this topic that covers what reskinned Unity games realistically earn across different genres and monetization setups: &lt;a href="https://unitysourcecode.net/blog/how-much-can-you-earn-selling-a-reskinned-unity-game" rel="noopener noreferrer"&gt;How Much Can You Earn Selling a Reskinned Unity Game&lt;/a&gt;. It's the kind of grounded, numbers-first perspective that's genuinely useful before committing real development time to any specific genre or strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;None of what I've described here is complicated in isolation. Simple, learnable mechanics. Monetization placement designed around genuine player value rather than bolted on as an afterthought. Reskinning treated as a legitimate content and audience-testing strategy rather than a lesser form of development. Real device profiling before considering a build ship-ready. Individually these are all fairly obvious pieces of advice. Where I think a lot of developers — myself included, in earlier projects — go wrong is in treating them as optional polish rather than as the actual substance of what separates a mobile puzzle game that earns modestly from one that earns well on a comparable amount of development effort.&lt;/p&gt;

&lt;p&gt;Maze-logic puzzle games happen to be a particularly clean genre for illustrating all of this precisely because the mechanical complexity is low enough that these other factors — monetization design, reskinning strategy, performance discipline — end up mattering more, proportionally, than they would in a more technically demanding genre where raw engineering effort dominates the outcome.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>puzzel</category>
      <category>mobile</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
