<?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>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>
    <item>
      <title>Profiling Your Unity Mobile Build: A Practical Checklist Before You Ship published</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:51:00 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/profiling-your-unity-mobile-build-a-practical-checklist-before-you-shippublished-13pc</link>
      <guid>https://dev.to/unitysourcecode/profiling-your-unity-mobile-build-a-practical-checklist-before-you-shippublished-13pc</guid>
      <description>&lt;h2&gt;
  
  
  Why Performance Debugging Gets Skipped
&lt;/h2&gt;

&lt;p&gt;Most Unity mobile devs test on one device — usually a mid-to-high-end phone — and call it done. Then the build ships, reviews start mentioning stutter and battery drain, and retention quietly tanks on lower-end Android devices that make up a huge chunk of the actual install base.&lt;/p&gt;

&lt;p&gt;This isn't a lecture about "always profile your game." You already know that. This is a practical checklist of the specific things that go wrong in Unity mobile projects most often, especially in casual and hyper-casual titles where a single dropped frame during an ad transition can tank a session.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Draw Calls and Batching
&lt;/h2&gt;

&lt;p&gt;The most common performance killer in casual mobile games isn't complex shaders — it's unbatched draw calls from sprite-heavy UI and scene objects.&lt;/p&gt;

&lt;p&gt;Quick checks:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
// In the Profiler, check SetPass calls and Draw Calls under Rendering&lt;br&gt;
// If either spikes above ~100-150 on mid-tier Android, investigate batching&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure sprites share the same atlas and material where possible.&lt;/li&gt;
&lt;li&gt;Static batch anything that doesn't move (background props, campsite decorations, static UI panels).&lt;/li&gt;
&lt;li&gt;Dynamic batching only helps for very low vertex-count meshes — don't rely on it for anything complex.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Garbage Collection Spikes
&lt;/h2&gt;

&lt;p&gt;GC spikes are the silent killer of "smooth on my device, laggy in the wild" bug reports. They're especially brutal on Android, where GC pauses are more expensive than on iOS.&lt;/p&gt;

&lt;p&gt;Common offenders in casual/kids' games specifically:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
// BAD: allocates every frame&lt;br&gt;
void Update() {&lt;br&gt;
    string status = "Score: " + score.ToString();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// BETTER: cache and reuse&lt;br&gt;
StringBuilder _sb = new StringBuilder();&lt;br&gt;
void UpdateScoreText() {&lt;br&gt;
    _sb.Clear();&lt;br&gt;
    _sb.Append("Score: ").Append(score);&lt;br&gt;
    scoreText.text = _sb.ToString();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Also watch for:&lt;/p&gt;

&lt;p&gt;GetComponent() calls inside Update() instead of cached in Awake()/Start()&lt;br&gt;
LINQ usage in per-frame code paths&lt;br&gt;
foreach over List in hot loops in older Unity/IL2CPP configurations (boxing overhead)&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ad SDK Integration Is a Common, Under-Diagnosed Culprit
&lt;/h2&gt;

&lt;p&gt;This one deserves special attention because it's rarely where developers look first. Monetization SDKs — AdMob mediation stacks in particular — can introduce frame drops, memory spikes, and even battery drain that have nothing to do with your actual gameplay code. Ad network SDKs run their own background threads, prefetch creatives, and sometimes hold onto references longer than they should.&lt;/p&gt;

&lt;p&gt;Symptoms that point to SDK overhead rather than your own code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frame drops specifically around interstitial/rewarded ad load calls, not during gameplay&lt;/li&gt;
&lt;li&gt;Memory that climbs steadily over a session and doesn't return to baseline after ads close&lt;/li&gt;
&lt;li&gt;Battery drain reports concentrated in sessions with heavy ad frequency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote a full breakdown of exactly how monetization SDKs impact performance and the concrete steps to mitigate it — preloading strategy, callback threading, and how to isolate SDK-caused frame drops from your own code in the Profiler: &lt;a href="https://dev.to/unitysourcecode/monetization-sdks-are-probably-hurting-your-unity-games-performance-heres-how-to-fix-it-29of"&gt;Monetization SDKs Are Probably Hurting Your Unity Game's Performance — Here's How to Fix It&lt;/a&gt;. Worth a read before you assume a performance issue is in your own gameplay code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Texture Memory and Compression Settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Especially relevant for colorful, asset-heavy casual games:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check your texture import settings per-platform. A texture compressed correctly for iOS (ASTC) but left at default for Android can bloat memory significantly.&lt;/li&gt;
&lt;li&gt;Audit texture sizes against actual on-screen size. A 2048x2048 background asset displayed at 400x400 on screen is wasted memory and bandwidth.&lt;/li&gt;
&lt;li&gt;Use the Memory Profiler package, not just the basic Profiler window, to see actual texture memory footprint per asset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Physics and Update Loop Overhead
&lt;/h2&gt;

&lt;p&gt;Casual games often use far more physics than they need to — colliders on decorative objects, unnecessary Rigidbody components, or Update() logic running on objects that are off-screen or inactive.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
// Disable physics/logic on objects outside the camera view&lt;br&gt;
void OnBecameInvisible() {&lt;br&gt;
    enabled = false;&lt;br&gt;
}&lt;br&gt;
void OnBecameVisible() {&lt;br&gt;
    enabled = true;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Small optimization, but it compounds fast in scenes with dozens of interactive props — which is common in exploration-style casual games.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Pre-Ship Checklist
&lt;/h2&gt;

&lt;p&gt;Before submitting a build, run through this on at least one low-tier and one mid-tier physical device (not just the Editor or a simulator):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Profiler: no GC spikes above ~1-2ms during normal gameplay&lt;/li&gt;
&lt;li&gt; Draw calls stay reasonable across all scenes, not just the main menu&lt;/li&gt;
&lt;li&gt; Ad load/show calls don't cause visible frame drops&lt;/li&gt;
&lt;li&gt; Texture memory checked against platform-specific compression settings&lt;/li&gt;
&lt;li&gt; Session tested with ads at production frequency, not disabled for testing&lt;/li&gt;
&lt;li&gt; Battery drain tested over a realistic 15-20 minute session&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you're building on top of a pre-existing Unity template rather than a fully custom project, these same profiling steps still apply — arguably more so, since you didn't write every system yourself and may not know where the SDK integration points or asset pipelines have hidden costs. Reviewing the technical scope and included systems before you start customizing helps avoid surprises later. You can browse structured, feature-documented Unity game projects in the &lt;a href="https://unitysourcecode.net/products/games" rel="noopener noreferrer"&gt;Games category on Unity Source Code&lt;/a&gt; if you're comparing templates and want to check what's already integrated versus what you'll need to build or optimize yourself.&lt;/p&gt;

&lt;p&gt;Performance work isn't glamorous, but it's the difference between a game that quietly loses players to stutter and one that actually holds retention long enough for your monetization to matter.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>performance</category>
    </item>
    <item>
      <title>Monetization SDKs Are Probably Hurting Your Unity Game's Performance — Here's How to Fix It</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:15:35 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/monetization-sdks-are-probably-hurting-your-unity-games-performance-heres-how-to-fix-it-29of</link>
      <guid>https://dev.to/unitysourcecode/monetization-sdks-are-probably-hurting-your-unity-games-performance-heres-how-to-fix-it-29of</guid>
      <description>&lt;p&gt;If you've ever profiled a Unity mobile game and found unexplained frame drops right after a level transition, an ad close event, or a store popup opening — you're not imagining it. Ad mediation SDKs and IAP frameworks are some of the most common (and most overlooked) sources of runtime performance issues in mobile games, and almost nobody blames them first because they "just work" most of the time.&lt;/p&gt;

&lt;p&gt;This article covers the specific ways monetization SDKs cause performance problems in Unity, and practical fixes you can apply without ripping out your ad/IAP integration entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Monetization SDKs Are a Performance Blind Spot
&lt;/h2&gt;

&lt;p&gt;When developers profile their Unity game, they instinctively look at their own code first — physics calculations, particle systems, UI canvas rebuilds, texture memory. Third-party SDKs get treated as a black box that's "not really part of the game," so they rarely show up on the initial suspect list.&lt;/p&gt;

&lt;p&gt;But ad mediation SDKs do a surprising amount of background work: network calls for ad fill, waterfall bidding across multiple networks, asset preloading for rendered ad creatives, and view hierarchy injection when an ad actually displays. IAP frameworks similarly run background receipt validation, store connection handshakes, and UI overlay rendering that competes with your game's own render pipeline.&lt;/p&gt;

&lt;p&gt;None of this is a bug — it's just cost that's easy to miss until you specifically profile for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Performance Symptoms and Their Root Causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Frame Hitches Right Before a Rewarded Ad Shows
&lt;/h3&gt;

&lt;p&gt;This is almost always caused by ad asset loading happening synchronously on the main thread at the moment you call &lt;code&gt;ShowRewardedAd()&lt;/code&gt;, instead of being preloaded in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always preload the next ad immediately after the current one finishes, not right before you need to show 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;void&lt;/span&gt; &lt;span class="nf"&gt;OnRewardedAdCompleted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;GrantReward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Preload the next one immediately, don't wait until it's needed&lt;/span&gt;
    &lt;span class="n"&gt;adService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PreloadRewardedAd&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;h3&gt;
  
  
  2. GC Spikes After Ad Close
&lt;/h3&gt;

&lt;p&gt;Many ad SDKs allocate a meaningful amount of managed memory when rendering ad creatives (especially video/interactive HTML5 ads), and that memory gets released all at once when the ad closes — triggering a garbage collection spike that shows up as a visible stutter right when gameplay resumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Avoid triggering your own heavy allocations (level loading, UI rebuilds, particle bursts) in the same frame the ad closes. Add a short buffer:&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;private&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;ResumeGameplayAfterAd&lt;/span&gt;&lt;span class="p"&gt;()&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;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.15f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// let GC settle before heavy work&lt;/span&gt;
    &lt;span class="nf"&gt;LoadNextLevel&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;It's a small hack, but it reliably smooths out the worst of the post-ad stutter on lower-end Android devices where GC pauses are more pronounced.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Store UI Causing Canvas Rebuild Storms
&lt;/h3&gt;

&lt;p&gt;If your IAP store screen uses Unity's UGUI and shares a Canvas with active gameplay UI, opening the store can trigger a full canvas rebuild that's expensive on lower-end devices — especially if the store has scrollable product lists with a lot of nested layout groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Put your store UI on its own dedicated Canvas, separate from gameplay HUD elements, so rebuilds are scoped to just the store hierarchy instead of cascading through your entire UI tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ad Mediation Initialization Blocking App Startup
&lt;/h3&gt;

&lt;p&gt;A frequent mistake is initializing ad mediation SDKs synchronously during your splash/loading screen, which can add real, measurable seconds to cold start time — directly hurting your Day 1 retention before the player has even seen gameplay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Initialize monetization SDKs asynchronously in parallel with your game's own asset loading, not as a blocking sequential step:&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;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InitializeApp&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;monetizationInit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MonetizationManager&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;InitializeAsync&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;gameAssetsLoad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GameAssetLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadCoreAssetsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monetizationInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gameAssetsLoad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;SceneManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MainMenu"&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 alone can shave meaningful time off cold start on devices where ad network initialization is slow due to network conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Profiling Monetization SDK Impact Specifically
&lt;/h2&gt;

&lt;p&gt;Unity's Profiler will show SDK activity, but it's easy to miss if you're not specifically isolating it. A few practical tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Deep Profile&lt;/strong&gt; mode temporarily around ad show/close events to see exactly what's firing on the main thread during those windows&lt;/li&gt;
&lt;li&gt;Watch the &lt;strong&gt;GC Alloc&lt;/strong&gt; column specifically in the frames immediately after an ad closes — this is where most SDK-related spikes hide&lt;/li&gt;
&lt;li&gt;On Android, check &lt;strong&gt;Logcat&lt;/strong&gt; during ad load/show events — many mediation SDKs log verbose network and rendering activity that reveals what's actually happening under the hood&lt;/li&gt;
&lt;li&gt;Test on a genuinely low-end device, not just your dev phone — SDK overhead that's invisible on a flagship device can be very visible on budget Android hardware, which is often a large chunk of your actual install base&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  This Ties Directly Into Broader Optimization Work
&lt;/h2&gt;

&lt;p&gt;Monetization SDK overhead is just one piece of the broader performance picture. If you've already gone through a full optimization pass on your Unity mobile game — cutting draw calls, batching sprites, trimming texture memory — and you're &lt;em&gt;still&lt;/em&gt; seeing stutter, monetization SDKs are one of the most likely remaining culprits, precisely because they're outside your own codebase and easy to overlook during a standard optimization sweep.&lt;/p&gt;

&lt;p&gt;I covered the broader set of reasons a Unity mobile game keeps stuttering even after "optimization" in more depth here, including rendering pipeline issues, garbage collection patterns, and asset streaming problems that go beyond just SDK overhead: &lt;a href="https://dev.to/unitysourcecode/why-your-unity-mobile-game-still-stutters-even-after-you-optimized-it-2e0"&gt;Why Your Unity Mobile Game Still Stutters Even After You Optimized It&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Balancing Performance Against Monetization Needs
&lt;/h2&gt;

&lt;p&gt;It's worth being honest about the tradeoff here: you can't eliminate monetization SDK overhead entirely, because the SDK needs to do real work to serve ads and process purchases. The goal isn't zero overhead — it's making sure that overhead doesn't collide with your game's own performance-critical moments (level transitions, boss fights, physics-heavy sequences).&lt;/p&gt;

&lt;p&gt;A few principles that consistently help:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trigger ad show/close logic during your game's most performance-sensitive frames.&lt;/strong&gt; Time ad placements around natural pause points, not mid-action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload aggressively, show lazily.&lt;/strong&gt; Ad assets should already be loaded well before you actually need to display them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate your store UI architecturally&lt;/strong&gt;, both in canvas structure and in code, so it doesn't compete with gameplay rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile monetization SDK behavior on real low-end devices&lt;/strong&gt;, not just your dev environment — this is where the gap between "fine on my phone" and "unplayable for a meaningful chunk of your install base" actually shows up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building on top of a pre-integrated Unity template rather than wiring up SDKs from scratch, it's worth checking how the monetization layer is structured before you assume performance issues are coming from your own code — a poorly integrated ad SDK in a rushed template can absolutely be the source of stutter that looks like it's coming from gameplay systems. You can browse a range of performance-tested, monetization-ready Unity game templates &lt;a href="https://unitysourcecode.net/products/games" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want a reference implementation that's already been optimized for this exact issue.&lt;/p&gt;




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

&lt;p&gt;Monetization SDKs are essential — they're how most Unity mobile games actually make money — but they're not free, and treating them as an invisible black box during optimization work leaves real performance on the table. Preload aggressively, isolate your store UI, initialize asynchronously, and profile specifically around ad/IAP events rather than assuming your own gameplay code is always the bottleneck.&lt;/p&gt;

&lt;p&gt;If you haven't nailed down your monetization strategy yet and you're weighing ads against IAP for your specific genre, it's worth reading through a full comparison of both models — including benchmark numbers and a decision framework — before you lock in the SDK integrations this article covers: &lt;a href="https://unitysourcecode.net/blog/in-app-purchases-vs-ad-revenue-unity-mobile-games" rel="noopener noreferrer"&gt;In-App Purchases vs. Ad Revenue: Which Monetization Model Works Best for Unity Mobile Games?&lt;/a&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Your Unity Mobile Game Still Stutters (Even After You "Optimized" It)</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:07:49 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/why-your-unity-mobile-game-still-stutters-even-after-you-optimized-it-2e0</link>
      <guid>https://dev.to/unitysourcecode/why-your-unity-mobile-game-still-stutters-even-after-you-optimized-it-2e0</guid>
      <description>&lt;p&gt;If you've shipped a Unity mobile game, you've probably had this exact experience: the game runs beautifully in the Editor, looks fine on your $1,200 test device, and then someone with a three-year-old mid-range Android phone opens it and reports it "freezes constantly." You profile it, fix the obvious thing, ship an update, and the complaints don't fully go away.&lt;/p&gt;

&lt;p&gt;This is one of the most common failure modes in mobile Unity development, and it's rarely caused by one big mistake. It's usually five or six smaller inefficiencies compounding on each other — garbage collection spikes stacking on top of overdraw, physics running more often than it needs to, UI canvases rebuilding when nothing visually changed. Individually, none of these would tank your frame rate. Together, on a mid-tier device with a fraction of your flagship's thermal headroom, they add up to a genuinely bad experience.&lt;/p&gt;

&lt;p&gt;I want to walk through where these problems actually come from, because "optimize your game" is not useful advice on its own — the fixes only make sense once you understand &lt;em&gt;why&lt;/em&gt; the underlying systems behave the way they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Profiler, Not With Guesses
&lt;/h2&gt;

&lt;p&gt;This sounds obvious, but it's worth stating plainly because it's the single most common mistake I see: developers guess at what's slow instead of measuring it. Unity's Profiler (and Profile Analyzer for comparing runs) will tell you, frame by frame, whether your bottleneck is CPU-side script execution, rendering, physics, UI, or garbage collection. Guessing wastes time optimizing systems that were never the actual problem, and — worse — it can lead to premature micro-optimizations that make the codebase harder to maintain without moving the needle on frame time at all.&lt;/p&gt;

&lt;p&gt;The practical workflow that actually works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Profile on a representative low-to-mid-tier device, not your dev machine or a flagship test phone.&lt;/li&gt;
&lt;li&gt;Identify whether the bottleneck is CPU, GPU, or memory/GC related — these require completely different fixes.&lt;/li&gt;
&lt;li&gt;Fix the single biggest offender, re-profile, repeat.&lt;/li&gt;
&lt;li&gt;Resist the urge to "optimize everything" — target the frames that are actually dropping.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Garbage Collection: The Silent Frame Killer
&lt;/h2&gt;

&lt;p&gt;If your game has occasional, unpredictable stutters rather than a consistently low frame rate, garbage collection is very often the culprit. C#'s garbage collector runs periodically to reclaim memory from objects you're no longer using, and when it runs, it can pause your game for anywhere from a few milliseconds to well over 16ms — enough to drop a frame outright on a 60fps target.&lt;/p&gt;

&lt;p&gt;The fix isn't "call GC.Collect() less" — it's generating less garbage in the first place. A few of the most common sources of unnecessary allocation in Unity projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;String concatenation in Update loops.&lt;/strong&gt; Every &lt;code&gt;"Score: " + score&lt;/code&gt; call allocates a new string. If this runs every frame, you're generating garbage constantly for something that could be cached or use &lt;code&gt;StringBuilder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LINQ in hot paths.&lt;/strong&gt; LINQ is expressive and easy to read, but most LINQ operations allocate. Fine in a one-time setup call, expensive if it's running every frame inside a loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boxing value types.&lt;/strong&gt; Passing a struct where an object is expected (common with older &lt;code&gt;ArrayList&lt;/code&gt;-style patterns, or careless generic usage) silently boxes it onto the heap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New list/array allocations inside loops.&lt;/strong&gt; Creating a fresh &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; every frame instead of reusing and clearing an existing one is one of the most common patterns I see in Update methods that shouldn't be there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these individually will crater your frame rate. But mobile devices have far less memory headroom than desktop, and their GC pauses are proportionally more expensive relative to your frame budget — a 60fps target only gives you a 16.6ms window per frame, and a GC spike alone can eat that entire budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overdraw: The GPU Cost Nobody Notices Until It's Too Late
&lt;/h2&gt;

&lt;p&gt;Overdraw happens when your GPU renders the same pixel multiple times in a single frame — usually from stacked transparent UI elements, particle effects, or overlapping sprites. On desktop GPUs this is often invisible because there's so much raw fill-rate headroom. On mobile GPUs, which are far more fill-rate constrained, overdraw is one of the most common causes of frame drops in UI-heavy or particle-heavy mobile games.&lt;/p&gt;

&lt;p&gt;Practical ways to reduce it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimize the number of overlapping transparent UI panels rendering simultaneously, especially full-screen ones.&lt;/li&gt;
&lt;li&gt;Use the Scene view's overdraw visualization mode during development to actually see where you're stacking draw calls unnecessarily.&lt;/li&gt;
&lt;li&gt;Be deliberate with particle systems — a burst of fifty overlapping semi-transparent particles is a much heavier GPU cost than it looks like in the Scene view.&lt;/li&gt;
&lt;li&gt;Disable or cull off-screen UI canvases rather than just hiding them with alpha, since a hidden-but-still-rendering canvas can still contribute to overdraw depending on setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Canvas Rebuilds: The UI Performance Trap
&lt;/h2&gt;

&lt;p&gt;Unity's UI system batches and rebuilds canvases to minimize draw calls, but any change to a UI element — even something as small as updating a single text field — can trigger a full canvas rebuild if that element shares a canvas with a lot of other UI. On a canvas with dozens of elements, a single frequently-updating health bar or score counter can force the entire canvas to rebuild every frame it changes.&lt;/p&gt;

&lt;p&gt;The standard fix is to split your UI into multiple canvases based on update frequency — static UI (menus, borders, backgrounds) on one canvas, and frequently updating elements (health bars, timers, score counters) on a separate canvas so their rebuilds don't drag the static elements along with them. This is a small structural change that consistently produces some of the largest, cheapest performance wins available in UI-heavy mobile games.&lt;/p&gt;

&lt;h2&gt;
  
  
  Physics: Running More Often Than It Needs To
&lt;/h2&gt;

&lt;p&gt;Unity's default fixed timestep runs physics calculations at a set interval regardless of your actual frame rate needs, and a lot of mobile games ship with physics settings tuned for desktop defaults rather than mobile constraints. A few targeted checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm your Fixed Timestep setting is appropriate for your game's actual physics precision needs — many mobile games run physics far more frequently than their gameplay actually requires.&lt;/li&gt;
&lt;li&gt;Reduce the number of active colliders doing continuous collision checks; use simpler collider shapes (box, sphere) over mesh colliders wherever the visual fidelity doesn't require them.&lt;/li&gt;
&lt;li&gt;Disable Rigidbody components on objects that aren't actively moving rather than leaving them active and idle.&lt;/li&gt;
&lt;li&gt;Use layer-based collision matrices to prevent physics from checking collisions between objects that should never interact in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Texture and Asset Memory: The Problem You Don't See Until It Crashes
&lt;/h2&gt;

&lt;p&gt;Mobile devices have dramatically less GPU memory than the desktop hardware most of us develop on, and texture memory is one of the most common causes of both performance degradation and outright crashes on lower-end devices. Uncompressed or oversized textures don't just slow rendering down — they can push total memory usage past what a mid-range device can allocate, leading to crashes that never show up in Editor testing at all.&lt;/p&gt;

&lt;p&gt;The fixes here are largely about being deliberate rather than clever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use appropriate compression formats per platform (ASTC for modern Android/iOS rather than defaulting to uncompressed RGBA) — this is worth double-checking in Unity's Player Settings per platform rather than trusting import defaults.&lt;/li&gt;
&lt;li&gt;Cap texture resolution based on how large the asset actually appears on screen, not the resolution the source art was delivered at.&lt;/li&gt;
&lt;li&gt;Use texture atlasing to reduce both draw calls and total texture memory overhead for UI and sprite-heavy games.&lt;/li&gt;
&lt;li&gt;Audit your build's memory profile with the Memory Profiler package rather than assuming asset sizes are fine because the build compiled successfully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Batching: Reducing Draw Calls Without Reducing Visual Quality
&lt;/h2&gt;

&lt;p&gt;Every draw call has CPU overhead, and mobile CPUs generally have far less headroom for a high draw call count than desktop CPUs do. Unity's static and dynamic batching, along with GPU instancing for repeated objects, can meaningfully reduce draw call count without requiring you to strip visual content from the game. The most common oversight here isn't a lack of batching support — it's assets that are structured in a way that accidentally breaks batching, like unnecessary material variations on objects that could easily share a single material and texture atlas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting This Together: It's a System, Not a Checklist
&lt;/h2&gt;

&lt;p&gt;The reason mobile performance problems are so persistent is that none of the individual issues above are dramatic on their own. A little GC pressure here, a bit of overdraw there, a canvas rebuilding more often than it needs to — none of it alone will tank your frame rate on a flagship device. But mobile hardware, especially the mid-range devices that make up the bulk of most games' actual install base, has dramatically less headroom across CPU, GPU, memory, and thermal budget than the devices most of us develop and test on. Small inefficiencies that are invisible on your dev machine compound into very visible stutter on the hardware your actual players are using.&lt;/p&gt;

&lt;p&gt;I went into more specific, actionable detail on this exact topic — with concrete before/after profiling comparisons — in a separate piece: &lt;a href="https://dev.to/unitysourcecode/unity-mobile-performance-7-optimizations-that-actually-move-the-needle-3llk"&gt;7 Unity mobile performance optimizations that actually move the needle&lt;/a&gt;. If you're actively chasing frame drops on a real project, it's worth reading alongside this one, since it goes deeper into the specific profiler workflows and before/after numbers behind several of the fixes described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You're Starting a New Project
&lt;/h2&gt;

&lt;p&gt;One practical note for anyone starting a new mobile project rather than optimizing an existing one: a lot of these performance problems are far easier to avoid from the start than to retrofit later, especially canvas structure and physics configuration, both of which get progressively harder to restructure as a project grows. If you're evaluating pre-built Unity templates or source code as a starting point for a new game, it's worth checking how the UI canvases, physics setup, and asset pipeline are structured before committing to a codebase — a template built with these considerations in mind from the start will save you a significant amount of the optimization work described here. Browsing through a &lt;a href="https://unitysourcecode.net/products" rel="noopener noreferrer"&gt;catalog of existing Unity game templates&lt;/a&gt; can also be a useful way to see how different genres and project structures handle these tradeoffs in practice, since UI-heavy puzzle games, physics-heavy arcade games, and asset-heavy simulation games each tend to hit different corners of this performance picture first.&lt;/p&gt;

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

&lt;p&gt;Mobile performance work rarely rewards a single big fix. It rewards profiling honestly, understanding which system is actually responsible for a given frame drop, and fixing the structural habits — garbage generation patterns, canvas organization, physics configuration, texture budgets — that compound quietly over the course of a project. None of the individual techniques here are exotic or hard to implement. The hard part is building the discipline to profile on real target hardware early and often, rather than discovering these problems for the first time in a one-star review after launch.&lt;/p&gt;

&lt;p&gt;If there's one habit worth taking away from all of this, it's simply this: test on the hardware your players actually own, not the hardware you happen to have on your desk. Almost every performance problem described above is invisible on a flagship device and painfully obvious on a three-year-old mid-range phone — and that gap is exactly where most shipped Unity mobile games quietly lose their retention numbers.&lt;/p&gt;

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