<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: unity source code</title>
    <description>The latest articles on DEV Community by unity source code (@unitysourcecode).</description>
    <link>https://dev.to/unitysourcecode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3985071%2F714492cf-7104-4c12-98fc-38203adfed9c.png</url>
      <title>DEV Community: unity source code</title>
      <link>https://dev.to/unitysourcecode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unitysourcecode"/>
    <language>en</language>
    <item>
      <title>Building an Endless Runner in Unity: A Developer's Technical Breakdown</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Thu, 20 Aug 2026 17:35:13 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-an-endless-runner-in-unity-a-developers-technical-breakdown-2epo</link>
      <guid>https://dev.to/unitysourcecode/building-an-endless-runner-in-unity-a-developers-technical-breakdown-2epo</guid>
      <description>&lt;p&gt;Endless runners look deceptively simple from the outside. Swipe, jump, slide, collect coins, repeat. But if you've ever tried to actually implement one — procedural track generation, object pooling, collision handling, input responsiveness, and performance tuning for low-end Android devices all at once — you know the genre has more engineering depth than it lets on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k5itdtzzr6710kea0se.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k5itdtzzr6710kea0se.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article is written for developers, not marketers. We're going to walk through the actual systems that make an endless runner work in Unity: the architecture decisions, the common pitfalls, and the performance considerations that separate a smooth 60fps runner from a janky prototype that drops frames the moment three obstacles spawn at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Endless Runners Are a Good Engineering Exercise
&lt;/h2&gt;

&lt;p&gt;Before diving into implementation, it's worth understanding why this genre is such a common starting point for Unity developers — and why so many teams choose to build on top of an existing source code rather than starting from a blank scene.&lt;/p&gt;

&lt;p&gt;The core systems in an endless runner are reusable across dozens of other genres: object pooling, procedural spawning, state machines for player movement, and performance-conscious rendering. Once you understand how to build these systems well in a runner, you can carry that architecture into platformers, obstacle-course games, and even some puzzle formats.&lt;/p&gt;

&lt;p&gt;A well-built reference implementation is genuinely useful here. The roundup of runner templates on the &lt;a href="https://unitysourcecode.net/blog/top-endless-runner-unity-source-codes" rel="noopener noreferrer"&gt;Unity Source Code blog&lt;/a&gt; is a good place to see how different teams have approached the same core loop — comparing tile-based level design against fully procedural generation, for example, or seeing how a chase-and-combat hybrid like &lt;a href="https://unitysourcecode.net/product/last-war-survival-battle-crowd-runner-combat" rel="noopener noreferrer"&gt;Last War: Survival Battle&lt;/a&gt; layers combat systems on top of the standard runner architecture. Studying multiple implementations side by side is one of the fastest ways to understand which architectural decisions actually matter versus which ones are just stylistic preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 1: Procedural Track Generation
&lt;/h2&gt;

&lt;p&gt;The heart of any endless runner is the system that generates the world in front of the player indefinitely without eating memory or tanking performance. There are two common approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Segment-based generation.&lt;/strong&gt; The world is broken into fixed-length "chunks" (say, 20 units long) that are pre-authored or randomly assembled from a pool of prefab segments. As the player moves forward, new segments spawn ahead and old ones get recycled behind.&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;TrackSpawner&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;GameObject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;trackSegments&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;Transform&lt;/span&gt; &lt;span class="n"&gt;player&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;segmentLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20f&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;segmentsAhead&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="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;activeSegments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;nextSpawnZ&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="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="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;segmentsAhead&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="nf"&gt;SpawnSegment&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;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nextSpawnZ&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segmentsAhead&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;segmentLength&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;SpawnSegment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;RecycleOldSegment&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;SpawnSegment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;prefab&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trackSegments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&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;trackSegments&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="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;segment&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;prefab&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;Vector3&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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextSpawnZ&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;activeSegments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;nextSpawnZ&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;segmentLength&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;RecycleOldSegment&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;activeSegments&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;segmentsAhead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeSegments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&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 approach is easier to control from a design perspective — you can hand-author interesting segment layouts and control difficulty precisely by weighting which segments are more likely to spawn as the game progresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fully procedural obstacle placement.&lt;/strong&gt; Instead of pre-built segments, obstacles and collectibles are spawned individually based on rules (minimum spacing, lane distribution, difficulty curve). This gives more variety but requires more careful tuning to avoid impossible or unfair obstacle combinations — like two full-width obstacles spawning too close together for the player to react.&lt;/p&gt;

&lt;p&gt;Most production runners use a hybrid: segment-based track geometry (ground, ramps, turns) combined with procedural obstacle and collectible placement within each segment. This gives you the best of both — predictable level structure with unpredictable moment-to-moment gameplay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 2: Object Pooling (Non-Negotiable)
&lt;/h2&gt;

&lt;p&gt;If there's one performance mistake that kills more endless runner prototypes than anything else, it's calling &lt;code&gt;Instantiate()&lt;/code&gt; and &lt;code&gt;Destroy()&lt;/code&gt; repeatedly during gameplay. Garbage collection spikes from constant allocation/deallocation are one of the leading causes of frame stutter in this genre, especially on mid- and low-tier Android devices.&lt;/p&gt;

&lt;p&gt;Object pooling solves this by pre-allocating a pool of reusable objects and simply enabling/disabling them instead of creating and destroying them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ObjectPool&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;GameObject&lt;/span&gt; &lt;span class="n"&gt;prefab&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;poolSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&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;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;poolSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;obj&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;prefab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="nf"&gt;GetObject&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;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&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="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;extra&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;prefab&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;extra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&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;ReturnObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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;Every obstacle, coin, power-up, and particle effect in your runner should go through a pooling system like this. It's the single highest-leverage optimization you can make early in development, and retrofitting it into a codebase that wasn't built with pooling in mind is significantly more painful than building it in from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System 3: Input Handling and Responsiveness
&lt;/h2&gt;

&lt;p&gt;Endless runners live or die on input responsiveness. A 100ms delay between a swipe and the character's lane change is enough to feel "off" to players, even if they can't articulate why.&lt;/p&gt;

&lt;p&gt;A few practical guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detect swipes on touch-start delta, not touch-end.&lt;/strong&gt; Waiting for a full swipe gesture to complete before registering input adds unnecessary latency. Many production runners start evaluating swipe direction as soon as the touch has moved a minimum threshold distance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a dedicated input buffer for jump/slide actions.&lt;/strong&gt; Registering input a few frames before the player is technically able to act on it (and queuing that action) makes controls feel more forgiving without actually changing the underlying timing window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple input polling from physics updates.&lt;/strong&gt; Poll input in &lt;code&gt;Update()&lt;/code&gt;, but apply movement in &lt;code&gt;FixedUpdate()&lt;/code&gt; using cached input state, so your control responsiveness isn't tied to physics tick rate.
&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;private&lt;/span&gt; &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="n"&gt;touchStartPos&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="n"&gt;swipeDetected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchCount&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Touch&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTouch&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TouchPhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Began&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;touchStartPos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;touch&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;swipeDetected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TouchPhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Moved&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;swipeDetected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Vector2&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;touch&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;touchStartPos&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;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magnitude&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;50f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// minimum swipe threshold&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;HandleSwipe&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;swipeDetected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="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;h2&gt;
  
  
  Core System 4: Difficulty Scaling
&lt;/h2&gt;

&lt;p&gt;A static difficulty curve is one of the fastest ways to make a runner feel stale. Most production-grade runners scale difficulty dynamically based on distance traveled or elapsed time, adjusting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forward movement speed&lt;/li&gt;
&lt;li&gt;Obstacle spawn frequency and density&lt;/li&gt;
&lt;li&gt;Minimum gap between obstacles (which should never shrink below what's physically possible to react to)&lt;/li&gt;
&lt;li&gt;Power-up spawn rate (often inversely related to difficulty, to give players relief valves during harder sections)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple linear or logarithmic curve tied to distance traveled works well as a baseline:&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="kt"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentSpeed&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;distanceTraveled&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;baseSpeed&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;maxSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20f&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;rampDistance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2000f&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="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distanceTraveled&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rampDistance&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;Mathf&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;baseSpeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxSpeed&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important engineering detail here is capping the maximum speed. Uncapped scaling eventually produces obstacle configurations that are mathematically impossible to react to at the player's input speed, which is a common (and easily avoidable) bug in amateur runner implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Optimization for Low-End Android
&lt;/h2&gt;

&lt;p&gt;This is where a lot of otherwise well-built runners fall apart in production. Endless runners are particularly punishing on frame rate because of constant camera movement, particle effects, and procedural spawning — all happening simultaneously, every frame, for as long as the player keeps the app open.&lt;/p&gt;

&lt;p&gt;Key optimization targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draw call reduction.&lt;/strong&gt; Batch static geometry where possible, and use texture atlases for obstacle and environment sprites to minimize material swaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Particle system budgets.&lt;/strong&gt; Cap simultaneous particle emitters and use simplified particle effects on detected low-end devices via &lt;code&gt;SystemInfo.systemMemorySize&lt;/code&gt; or a device-tier detection library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LOD and culling.&lt;/strong&gt; Even in a runner where the camera mostly faces forward, aggressive frustum culling on track segments behind the player prevents wasted rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physics simplification.&lt;/strong&gt; Use simple colliders (box/capsule) over mesh colliders wherever possible, and avoid running full physics simulation on objects that only need trigger-based collision detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this genre is so sensitive to frame rate consistency across the fragmented Android hardware landscape, it's worth budgeting real testing time on actual low-end devices rather than relying on editor performance or high-end test phones, which will mask problems that only show up on budget hardware in the field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing Considerations: Platform-Specific Build Configuration
&lt;/h2&gt;

&lt;p&gt;Once the core systems are built and optimized, the next engineering hurdle is platform-specific build configuration — and this is a step where a lot of developers underestimate the differences between Android and iOS.&lt;/p&gt;

&lt;p&gt;Build settings, signing certificates, IL2CPP vs Mono scripting backends, texture compression formats (ASTC vs ETC2 vs PVRTC), and store-specific review requirements around ads and permissions all differ meaningfully between the two platforms. Getting these wrong doesn't just cause build errors — it can lead to store rejections that cost a full review cycle to fix.&lt;/p&gt;

&lt;p&gt;For a detailed technical breakdown of these platform differences — including build configuration specifics and submission requirements — see &lt;a href="https://dev.to/unitysourcecode/unity-android-vs-ios-publishing-a-developers-technical-breakdown-1k00"&gt;this developer-focused walkthrough of Unity Android vs iOS publishing&lt;/a&gt;. It's worth reading before you lock in your build pipeline, since some of these settings (like scripting backend and API compatibility level) are much easier to configure correctly up front than to retrofit after a submission gets flagged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and Iteration Workflow
&lt;/h2&gt;

&lt;p&gt;A practical testing workflow for a runner in active development typically includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Editor playtesting&lt;/strong&gt; for rapid iteration on core mechanics and feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device farm testing&lt;/strong&gt; across a range of Android hardware tiers to catch performance regressions early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated frame-rate logging&lt;/strong&gt; during build QA, comparing average and minimum FPS across representative test devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playtester feedback sessions&lt;/strong&gt; focused specifically on difficulty pacing — this is subjective enough that it benefits from real human input rather than pure metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft launch analytics&lt;/strong&gt; once the build is live, tracking session length, retry rate, and drop-off points along the difficulty curve.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treating difficulty tuning as an ongoing, data-informed process rather than a one-time decision before launch tends to produce noticeably better retention outcomes over the game's lifecycle.&lt;/p&gt;

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

&lt;p&gt;Endless runners reward strong engineering fundamentals more than most casual mobile genres. The mechanics are simple to describe but genuinely demanding to implement well — procedural generation that stays fair, object pooling that eliminates GC spikes, input handling tight enough to feel instant, and difficulty scaling that stays challenging without becoming impossible.&lt;/p&gt;

&lt;p&gt;If you're starting a runner project, it's worth studying existing production implementations before writing your own systems from scratch. Comparing different architectural approaches — like the range of templates covered in the &lt;a href="https://unitysourcecode.net/blog/top-endless-runner-unity-source-codes" rel="noopener noreferrer"&gt;endless runner source code roundup&lt;/a&gt; — can save significant development time by showing you which patterns are proven versus which are still experimental.&lt;/p&gt;

&lt;p&gt;The genre isn't going anywhere. Its technical demands are well understood, its player expectations are well established, and its engineering challenges — performance, responsiveness, and fair difficulty scaling — are exactly the kind of problems that make you a better Unity developer once you've solved them properly.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>android</category>
    </item>
    <item>
      <title>Unity Android vs iOS Publishing: A Developer's Technical Breakdown</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:45:02 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/unity-android-vs-ios-publishing-a-developers-technical-breakdown-1k00</link>
      <guid>https://dev.to/unitysourcecode/unity-android-vs-ios-publishing-a-developers-technical-breakdown-1k00</guid>
      <description>&lt;p&gt;Cross-platform is one of Unity's biggest promises: write your gameplay once, deploy it to Android and iOS from the same project. That promise holds up reasonably well at the code layer. Where it breaks down is everywhere else — build tooling, signing, store review, monetization APIs, and performance behavior across wildly different hardware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz2hnuhav4xaiyanv9l0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz2hnuhav4xaiyanv9l0.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're a developer preparing your first dual-platform release, this is the technical rundown you need before you hit "Build." We'll go through the environment setup, store submission mechanics, IAP wiring, performance planning, and signing workflow for both platforms, along with the mistakes that trip up most first-time submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "One Build, Two Platforms" Isn't Really True
&lt;/h2&gt;

&lt;p&gt;Unity abstracts a huge amount of platform complexity for you. Input handling, rendering, physics, and most gameplay systems can stay platform-agnostic if you write them that way. But the moment you move past gameplay code into "how does this thing actually get onto a phone," Android and iOS stop looking like variations of the same process and start looking like two separate pipelines that happen to share a game engine.&lt;/p&gt;

&lt;p&gt;Treating them identically — same build flags, same assumptions about testing, same submission timeline — is the single most common reason first submissions get delayed or rejected. Let's break down where the real divergence happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Build Environment and Tooling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;

&lt;p&gt;Unity Hub can install everything required for Android builds directly: the Android SDK, NDK, and OpenJDK. You can build and test an APK on Windows, macOS, or Linux without any special hardware, which makes Android the lower-friction platform to get your first build running on.&lt;/p&gt;

&lt;p&gt;The Player Settings you'll actually care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum API Level&lt;/strong&gt; — the floor for how old a device can be and still install your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target API Level&lt;/strong&gt; — Google raises the required minimum yearly, and enforces it strictly at submission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripting Backend&lt;/strong&gt; — IL2CPP is effectively mandatory now for Play Store submissions due to 64-bit requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Architectures&lt;/strong&gt; — ARM64 is required; ARMv7 is optional but still useful for older device coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common pattern for separating Android-specific code from shared 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="cp"&gt;#if UNITY_ANDROID &amp;amp;&amp;amp; !UNITY_EDITOR
&lt;/span&gt;    &lt;span class="c1"&gt;// Android-specific initialization, e.g. Play Billing setup&lt;/span&gt;
    &lt;span class="nf"&gt;InitializeAndroidBilling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;There's no way around this one: you need a Mac running Xcode to finish an iOS build. Unity generates an Xcode project as its iOS build output, and compiling, signing, and archiving all happen inside Xcode before anything reaches App Store Connect. Windows and Linux can get you 90% of the way there, but the final 10% requires macOS.&lt;/p&gt;

&lt;p&gt;Key settings on the iOS side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target minimum iOS version&lt;/li&gt;
&lt;li&gt;Signing &amp;amp; Provisioning Profiles, which require a paid Apple Developer Program membership&lt;/li&gt;
&lt;li&gt;Architecture, which on current devices means ARM64 only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't own a Mac, this becomes a real planning constraint — you're looking at either borrowing one, renting time on a cloud Mac build service, or budgeting to buy one before your iOS release date is realistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Store Review: Automated vs. Manual
&lt;/h2&gt;

&lt;p&gt;This is where the day-to-day developer experience diverges most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play&lt;/strong&gt; review is largely automated. Google is mainly checking for policy violations: malware, deceptive functionality, disallowed content, and privacy issues — particularly whether your Data Safety form actually matches what your app collects. Turnaround is typically a few hours to a couple of days, and results are fairly predictable if you've followed policy.&lt;/p&gt;

&lt;p&gt;A few things have tightened recently: new developer accounts frequently need to run a closed test with a minimum tester count before they're allowed to go public, target API level requirements are enforced hard at submission, and the Data Safety section needs to be accurate, not just filled in generically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple's App Store&lt;/strong&gt; review is manual and human-reviewed, typically completed in 24–48 hours, but rejections on first submissions are common. Frequent causes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incomplete or misleading metadata and screenshots&lt;/li&gt;
&lt;li&gt;Crashes or obvious bugs found during the reviewer's pass&lt;/li&gt;
&lt;li&gt;Violations of the Human Interface Guidelines&lt;/li&gt;
&lt;li&gt;Missing or incomplete privacy "nutrition label"&lt;/li&gt;
&lt;li&gt;In-app purchase implementation issues — Apple is particularly strict here&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your game has login functionality, expect to provide a working demo account. If you use ads or IAP, App Tracking Transparency has to be implemented correctly or you risk rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; build extra time into your schedule for iOS, especially for your first submission. Don't plan a launch marketing push around your very first App Store submission date — plan it around your first &lt;em&gt;approved&lt;/em&gt; build instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. In-App Purchases and Billing APIs
&lt;/h2&gt;

&lt;p&gt;Monetization is one of the more technically fiddly parts of dual-platform publishing, mostly because Android and iOS use completely separate billing systems under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt; requires Google Play Billing for any in-app purchase. Unity IAP wraps this, but you still need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create matching in-app products inside the Google Play Console&lt;/li&gt;
&lt;li&gt;Keep product IDs identical between your Unity project and the Play Console&lt;/li&gt;
&lt;li&gt;Track Billing Library version deprecations — Google periodically forces migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt; uses Apple's StoreKit. App Store Connect follows a similar product-ID model, but with its own requirements: tax and banking details must be fully completed before IAP products can go live, and subscription products carry stricter rules than one-time purchases.&lt;/p&gt;

&lt;p&gt;A typical Unity IAP initialization pattern that works across both stores:&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;InitializePurchasing&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="nf"&gt;IsInitialized&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StandardPurchasingModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"coins_pack_small"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProductType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Consumable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remove_ads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProductType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonConsumable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;UnityPurchasing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&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="n"&gt;builder&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 product IDs (&lt;code&gt;coins_pack_small&lt;/code&gt;, &lt;code&gt;remove_ads&lt;/code&gt;) have to match exactly on both the Play Console and App Store Connect side — a mismatch here is one of the most common causes of "purchases silently failing" bug reports.&lt;/p&gt;

&lt;p&gt;Ad SDKs add another layer of platform difference. iOS requires the App Tracking Transparency prompt, and how users respond to it directly affects ad fill rate and eCPM. Android is more lenient on tracking permissions but strictly enforces its own content rating and ad placement policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Performance: Fragmentation vs. Consistency
&lt;/h2&gt;

&lt;p&gt;If there's one technical factor that separates Android and iOS optimization work, it's hardware fragmentation.&lt;/p&gt;

&lt;p&gt;Android spans everything from high-end flagship GPUs down to budget devices with limited RAM and older graphics hardware still in heavy use across many regions. A build running a smooth 60 FPS on a flagship can drop into single digits on a low-end device if you haven't planned for it.&lt;/p&gt;

&lt;p&gt;Practical mitigation steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use appropriate texture compression (ASTC, ETC2) and keep texture sizes sane&lt;/li&gt;
&lt;li&gt;Batch aggressively and watch draw call counts&lt;/li&gt;
&lt;li&gt;Scale particle effects and shadow quality based on a detected device tier&lt;/li&gt;
&lt;li&gt;Test on real physical devices — emulators don't reliably reflect thermal and GPU behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;iOS hardware, by contrast, is far more consistent. Apple controls both hardware and software, and at any given time there's a relatively small set of active device models. If your build runs well on a couple of representative iPhone models, you can be reasonably confident about the broader install base.&lt;/p&gt;

&lt;p&gt;That doesn't mean iOS is performance-worry-free, though. iOS devices tend to have less thermal headroom for sustained heavy workloads, so long sessions with demanding shaders can trigger thermal throttling — worth explicit testing if your game leans on visual effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Signing and Certificates
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt; apps are signed with a keystore file. Since 2021, Google Play requires Play App Signing, where Google holds your actual signing key and you upload builds signed with a separate upload key. This mostly eliminated the old failure mode where losing your keystore meant permanently losing the ability to update your app — but backing up your credentials is still essential practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt; signing involves more moving parts: developer certificates, App IDs, and provisioning profiles for development, ad hoc, and distribution builds. Xcode's Automatic Signing handles most of this for solo developers, but CI/CD pipelines and larger teams often end up managing profiles manually, adding complexity that simply doesn't exist on Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Testing Pipelines
&lt;/h2&gt;

&lt;p&gt;Google Play offers Internal, Closed, and Open testing tracks, letting you roll builds out to specific tester groups before a public release. As mentioned, many newer developer accounts are required to run a closed test with a minimum number of testers for a set period before production publishing unlocks.&lt;/p&gt;

&lt;p&gt;Apple's equivalent is TestFlight, supporting up to 10,000 external testers per app. TestFlight builds go through a lighter version of Apple's review before external testers can install them, generally faster than a full App Store submission.&lt;/p&gt;

&lt;p&gt;Use both pipelines seriously — not just to catch crashes, but to get early signal on your store listing itself (icon, screenshots, description), since first impressions have an outsized effect on install conversion on both stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Store Economics and Listing Requirements
&lt;/h2&gt;

&lt;p&gt;Both stores take a standard 30% commission on paid apps and IAP, reduced to 15% for developers under certain annual revenue thresholds via Google's Play Media Experience Program and Apple's Small Business Program — eligibility criteria are worth verifying since they can change.&lt;/p&gt;

&lt;p&gt;Listing requirements differ too: Google Play gives more flexibility around feature graphics, promotional video, and short/long descriptions, and supports staged update rollouts. Apple enforces stricter per-device screenshot sizing and tighter rules on app preview video content.&lt;/p&gt;

&lt;p&gt;Pricing structure also differs — Apple uses fixed pricing tiers applied globally, while Google Play allows granular per-country pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Post-Launch Updates
&lt;/h2&gt;

&lt;p&gt;Android updates are typically reviewed within hours, and Google's staged rollout feature lets you release to a percentage of users first to catch issues before a full rollout. iOS updates go through the same full manual review as your original submission — typically 24–48 hours — though Apple offers an expedited review request for critical fixes (not guaranteed, and not something to rely on as standard process).&lt;/p&gt;

&lt;p&gt;This matters most if you're running a live-ops style game with frequent balance or event changes — Android lets you iterate meaningfully faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treating the store listing as an afterthought instead of a conversion-critical asset&lt;/li&gt;
&lt;li&gt;Skipping tests on real low-end Android hardware and getting hit with bad reviews post-launch&lt;/li&gt;
&lt;li&gt;Overlooking Apple's privacy nutrition label, causing avoidable review delays&lt;/li&gt;
&lt;li&gt;Scattering platform-specific logic instead of cleanly isolating it with &lt;code&gt;#if UNITY_ANDROID&lt;/code&gt; / &lt;code&gt;#if UNITY_IOS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Skipping IAP sandbox testing on both platforms before going live&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Starting From a Working Project Instead of Zero
&lt;/h2&gt;

&lt;p&gt;Configuring build settings, signing, IAP, and performance scaling correctly across both platforms is a real time investment, even for experienced developers. This is a big part of why many solo developers and small studios start from an already-built Unity project rather than assembling every system from scratch — the core mechanics, UI, and platform build configuration are already in place and tested, so your time goes into reskinning, tuning, and marketing.&lt;/p&gt;

&lt;p&gt;For a concrete example of a casual, low-complexity game already structured for a dual-platform release, this &lt;a href="https://unitysourcecode.net/product/house-cleaning-unity-game" rel="noopener noreferrer"&gt;House Cleaning Unity game source code&lt;/a&gt; is a useful reference point — a simulation-style casual title with the kind of lightweight performance profile that maps well to the fragmentation concerns covered above.&lt;/p&gt;

&lt;p&gt;If you'd rather look at a more technically involved genre, this &lt;a href="https://dev.to/unitysourcecode/build-a-crowd-runner-combat-game-in-unity-a-technical-guide-1pke"&gt;technical guide to building a crowd runner combat game in Unity&lt;/a&gt; walks through the architecture decisions behind a more systems-heavy project, which is worth reading if you're weighing how much gameplay complexity to take on before you start thinking about platform-specific build configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Android (Google Play)&lt;/th&gt;
&lt;th&gt;iOS (App Store)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build machine required&lt;/td&gt;
&lt;td&gt;Windows, macOS, or Linux&lt;/td&gt;
&lt;td&gt;macOS with Xcode only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review process&lt;/td&gt;
&lt;td&gt;Mostly automated, hours to ~2 days&lt;/td&gt;
&lt;td&gt;Manual human review, ~1–2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware fragmentation&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing&lt;/td&gt;
&lt;td&gt;Keystore + Play App Signing&lt;/td&gt;
&lt;td&gt;Certificates + provisioning profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAP system&lt;/td&gt;
&lt;td&gt;Google Play Billing&lt;/td&gt;
&lt;td&gt;Apple StoreKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Internal/Closed/Open tracks&lt;/td&gt;
&lt;td&gt;TestFlight (up to 10,000 testers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update rollout&lt;/td&gt;
&lt;td&gt;Staged rollout supported&lt;/td&gt;
&lt;td&gt;Full re-review each time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commission&lt;/td&gt;
&lt;td&gt;30% (15% under threshold)&lt;/td&gt;
&lt;td&gt;30% (15% under threshold)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Publishing a Unity game across Android and iOS isn't a single checklist — it's two separate operational pipelines that happen to share a codebase. Android rewards developers who can handle hardware fragmentation and want fast iteration cycles. iOS rewards polish, consistency, and strict adherence to review guidelines.&lt;/p&gt;

&lt;p&gt;If you're new to dual-platform publishing, the fastest way to internalize these differences is to study a project that's already correctly configured for both — build settings, signing, and IAP hooks included — rather than debugging every platform quirk on a brand-new game built entirely from scratch.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>android</category>
      <category>gamedev</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Build a Crowd Runner Combat Game in Unity: A Technical Guide</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sun, 16 Aug 2026 18:03:23 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/build-a-crowd-runner-combat-game-in-unity-a-technical-guide-1pke</link>
      <guid>https://dev.to/unitysourcecode/build-a-crowd-runner-combat-game-in-unity-a-technical-guide-1pke</guid>
      <description>&lt;p&gt;Crowd runner games have quietly become one of the most consistently profitable genres in mobile gaming. If you've spent any time browsing top charts on the App Store or Google Play, you've likely seen the pattern: players guide a growing crowd of characters through an obstacle course, gates multiply or divide their numbers, and the run culminates in some kind of climactic showdown. What started as a niche hyper-casual mechanic has evolved into a genre with real depth, especially when combat systems are layered on top of the core running loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij1w2f313wi8azhne0i8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij1w2f313wi8azhne0i8.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we're going to dig into the technical and design fundamentals behind crowd runner combat games, using the mechanics found in survival battle-style Unity projects as our reference point. This isn't a marketing pitch. It's a breakdown intended for developers who want to understand how these systems actually work under the hood, why certain design decisions matter, and what technical challenges you'll run into if you try to build one yourself.&lt;/p&gt;

&lt;p&gt;By the end of this post, you should have a solid mental model of the core systems involved: crowd management, gate logic, combat resolution, and the progression systems that tie it all together.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Crowd Runner Combat Games Are Worth Studying
&lt;/h3&gt;

&lt;p&gt;Before getting into architecture, it's worth understanding why this genre has proven so durable. Crowd runner games succeed because they combine two very different psychological hooks into a single experience.&lt;/p&gt;

&lt;p&gt;The running phase taps into the same "flow state" mechanics that make endless runners and rhythm games compelling: constant forward momentum, quick decisions, and immediate visual feedback. Every gate you pass either rewards or punishes you instantly, which keeps players locked into a tight feedback loop.&lt;/p&gt;

&lt;p&gt;The combat phase, on the other hand, taps into something different entirely: payoff and consequence. All the decisions made during the running section, how many units were recruited, which upgrades were collected, which gates were avoided, culminate in a single dramatic confrontation. This creates a structure similar to a resource-management game compressed into sixty seconds, where preparation directly determines outcome.&lt;/p&gt;

&lt;p&gt;Games like &lt;strong&gt;&lt;a href="https://unitysourcecode.net/product/last-war-survival-battle-crowd-runner-combat" rel="noopener noreferrer"&gt;Last War Survival Battle&lt;/a&gt;&lt;/strong&gt; are a good example of this hybrid structure in practice. Players build an army by making path decisions during the run, then watch that army clash with enemy forces in a battlefield sequence at the end of the level. Studying how a template like this structures its systems is genuinely useful for understanding the genre, regardless of whether you use it as a direct starting point or simply as a reference for your own architecture.&lt;/p&gt;




&lt;h3&gt;
  
  
  Core System 1: Crowd/Unit Management
&lt;/h3&gt;

&lt;p&gt;The foundation of any crowd runner game is a system that tracks and visually represents a dynamically changing group of units. This sounds simple in concept, but it has real performance implications once your unit count climbs into the hundreds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The naive approach&lt;/strong&gt; — instantiating and destroying individual GameObjects every time the crowd size changes — works fine for prototypes but breaks down quickly at scale. Instantiate/Destroy calls are expensive, and doing them every frame as units are gained or lost during a fast-paced run will tank your frame rate on mid-range Android devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A better approach&lt;/strong&gt; relies on object pooling. Pre-instantiate a pool of unit GameObjects at scene load, then activate/deactivate them as the crowd count changes rather than creating and destroying instances. Combine this with a formation system, typically a grid or radial layout algorithm, that recalculates unit positions relative to the player's transform as the crowd grows or shrinks. Units should lerp or spring toward their target formation position rather than snapping instantly, which produces the fluid, organic crowd movement players expect from this genre.&lt;/p&gt;

&lt;p&gt;For very large crowds (several hundred units), some developers go a step further and use GPU instancing or Unity's DOTS/ECS stack to render units, since traditional GameObject-per-unit approaches become a bottleneck well before you hit visually impressive crowd sizes. Whether this level of optimization is necessary depends heavily on your target unit count and device tier.&lt;/p&gt;




&lt;h3&gt;
  
  
  Core System 2: Gate and Multiplier Logic
&lt;/h3&gt;

&lt;p&gt;Gates are the primary interactive element during the running phase, and they need to be built as a modular, data-driven system rather than hardcoded level-by-level logic.&lt;/p&gt;

&lt;p&gt;A clean implementation typically defines a &lt;code&gt;GateEffect&lt;/code&gt; as a ScriptableObject or simple data class containing an operation type (add, subtract, multiply, divide) and a value. When the player's crowd collider intersects a gate's trigger volume, the gate applies its effect to the current crowd count and then deactivates or destroys itself.&lt;/p&gt;

&lt;p&gt;The trickier design challenge isn't the collision logic, it's balancing gate placement and values so that risk and reward feel meaningful without becoming punishing. If a "divide by 2" gate appears too frequently relative to "multiply by 2" gates, players will feel like progress is arbitrary rather than skill-based. This is less a coding problem and more a data-tuning problem, and it's worth building a level editor tool (Unity's custom editor windows are great for this) that lets designers preview cumulative crowd growth across a full gate sequence before shipping a level.&lt;/p&gt;




&lt;h3&gt;
  
  
  Core System 3: Combat Resolution
&lt;/h3&gt;

&lt;p&gt;The combat phase is where crowd runner games diverge most from simple endless runners, and it's also where the most interesting architectural decisions happen.&lt;/p&gt;

&lt;p&gt;There are generally two approaches to resolving battlefield combat:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulated combat&lt;/strong&gt; treats the encounter as a numbers-driven calculation. Your army size and stats are compared against the enemy's, a resolution formula determines the outcome, and the visual battlefield sequence is essentially a scripted animation representing that pre-calculated result. This approach is computationally cheap and easy to balance, but risks feeling disconnected from player skill if not designed carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live combat&lt;/strong&gt; actually simulates individual unit-vs-unit encounters in real time, with units from both sides colliding, attacking, and being eliminated based on per-unit stats. This produces a more visually dynamic and satisfying battle sequence, but it's significantly more expensive computationally and harder to balance predictably, since emergent unit-level behavior can produce unexpected outcomes.&lt;/p&gt;

&lt;p&gt;Most successful mobile crowd runner combat games use a hybrid: a lightweight, deterministic simulation runs behind the scenes to guarantee a fair and balanced outcome based on player stats, while a separate visual layer plays out an approximate, non-authoritative animation of units fighting on screen. This gives you the best of both worlds — predictable, testable game balance, plus a satisfying visual spectacle — without needing full physics-driven combat simulation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Core System 4: Progression and Upgrade Architecture
&lt;/h3&gt;

&lt;p&gt;Long-term retention in this genre depends heavily on a well-structured progression system. From an engineering standpoint, this typically means building a persistent player data layer that tracks unlocked upgrades, currency balances, and unit stat multipliers, separate from any individual level's runtime state.&lt;/p&gt;

&lt;p&gt;A clean pattern here is to define upgrades as data assets (again, ScriptableObjects work well) with an ID, a cost curve, and a stat modifier. Your runtime unit stats are then calculated as a base value multiplied by whatever upgrades the player has unlocked, rather than hardcoding stat values per unit type. This makes balancing dramatically easier, since you can adjust a single upgrade's modifier value without touching unit prefabs or combat code.&lt;/p&gt;

&lt;p&gt;It's also worth designing your progression system with monetization hooks in mind from the start. Rewarded ads that grant temporary stat boosts, currency multipliers, or extra army slots are common in this genre, and they work best when your upgrade system already has clean, event-driven hooks (&lt;code&gt;OnUpgradePurchased&lt;/code&gt;, &lt;code&gt;OnRunCompleted&lt;/code&gt;, etc.) that a separate monetization manager can subscribe to.&lt;/p&gt;




&lt;h3&gt;
  
  
  Performance Considerations Specific to This Genre
&lt;/h3&gt;

&lt;p&gt;Crowd runner combat games have some performance characteristics that are worth calling out specifically, since they differ from more typical mobile game profiles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw calls scale with crowd size.&lt;/strong&gt; If every unit uses its own material instance, you'll hit draw call limits well before you hit interesting crowd sizes. Use material batching, GPU instancing, or a shared material with per-instance property blocks to keep draw calls manageable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physics can become a bottleneck fast.&lt;/strong&gt; If every unit has its own Rigidbody and collider for gate detection, physics calculations can spike during large crowd scenes. Many implementations use simplified trigger checks based on distance calculations rather than full physics collision for gate interactions, reserving actual physics simulation for the combat phase where visual impact matters more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animation cost adds up.&lt;/strong&gt; Hundreds of animated units on screen simultaneously can be expensive if each is running an independent Animator component. Techniques like animation texture baking or GPU-driven animation systems become worth considering once your crowd sizes get large enough.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Note on Vehicle and Physics-Based Genres
&lt;/h3&gt;

&lt;p&gt;It's worth noting that crowd runner combat games are just one example of a broader category of mobile games where core mechanics need to be layered with a secondary system, in this case, running plus combat. Other genres follow a similar "two systems working together" pattern, just with different underlying physics.&lt;/p&gt;

&lt;p&gt;If you're interested in a deeper technical dive into a genre that leans much more heavily on physics simulation specifically, this breakdown on &lt;strong&gt;&lt;a href="https://dev.to/unitysourcecode/building-realistic-flight-physics-in-unity-a-technical-breakdown-of-aircraft-simulation-mechanics-45m8"&gt;building realistic flight physics in Unity, covering aircraft simulation mechanics&lt;/a&gt;&lt;/strong&gt; is a great companion read. It covers a very different technical domain, lift, drag, thrust calculations, and control surface simulation, but the underlying engineering discipline of balancing simulation accuracy against mobile performance constraints is a theme that shows up across almost every genre, including the crowd combat systems discussed here.&lt;/p&gt;




&lt;h3&gt;
  
  
  Practical Advice If You're Building This Genre
&lt;/h3&gt;

&lt;p&gt;If you're a developer considering building a crowd runner combat game, here's a practical starting checklist based on the systems covered above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build your crowd management system with pooling from day one.&lt;/strong&gt; Retrofitting object pooling into a project that already instantiates/destroys units directly is painful. Start with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate your combat resolution logic from your combat visuals.&lt;/strong&gt; A deterministic, testable combat calculation layer will save you countless balance headaches compared to relying purely on emergent unit-vs-unit physics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make gates and upgrades data-driven.&lt;/strong&gt; ScriptableObject-based configuration lets you (or a designer on your team) iterate on balance without touching code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile early, especially draw calls and physics.&lt;/strong&gt; Crowd-based genres hit performance walls in different places than typical 3D mobile games, so don't assume standard optimization advice will catch everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Study existing implementations before starting from scratch.&lt;/strong&gt; Reviewing how an existing, working project like a crowd runner combat template structures its systems can save significant development time and help you avoid common architectural mistakes. For developers newer to Unity overall, it's also worth reviewing a broader list of &lt;strong&gt;&lt;a href="https://unitysourcecode.net/blog/best-unity-projects-for-new-developers" rel="noopener noreferrer"&gt;recommended Unity projects for new developers&lt;/a&gt;&lt;/strong&gt;, which covers genres and project types that are well-suited for building foundational Unity skills before tackling something as system-heavy as a crowd combat game.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Crowd runner combat games look simple from a player's perspective, tap gates, grow your army, watch a battle play out, but the systems underneath are genuinely interesting from an engineering standpoint. Crowd management demands careful attention to object pooling and formation logic. Gate systems benefit enormously from data-driven design. Combat resolution requires balancing deterministic fairness against visual spectacle. And progression systems need clean architecture to support both game balance and monetization.&lt;/p&gt;

&lt;p&gt;If you're a Unity developer looking to build in this genre, the best approach is to treat each of these systems as a separate, testable module rather than tangling them together in a handful of monolithic scripts. Not only will this make your project easier to maintain and extend, it'll make it dramatically easier to balance and tune once real players start generating data.&lt;/p&gt;

&lt;p&gt;Whether you're building from scratch or starting from an existing template, understanding these underlying systems will make you a more effective developer in this genre, and in mobile game development more broadly.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>c</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building Realistic Flight Physics in Unity: A Technical Breakdown of Aircraft Simulation Mechanics</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:45:37 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-realistic-flight-physics-in-unity-a-technical-breakdown-of-aircraft-simulation-mechanics-45m8</link>
      <guid>https://dev.to/unitysourcecode/building-realistic-flight-physics-in-unity-a-technical-breakdown-of-aircraft-simulation-mechanics-45m8</guid>
      <description>&lt;p&gt;Flight simulation is one of the most technically demanding genres you can build in Unity. Unlike platformers or top-down games where physics can be simplified or faked, a flight sim lives or dies by how believable its aircraft &lt;em&gt;feels&lt;/em&gt; to control — and that believability comes from getting a surprisingly deep stack of physics, input handling, and environment systems working together correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4176b5fj7ffza86mwqc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4176b5fj7ffza86mwqc.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I want to walk through the core engineering problems behind building a mobile flight simulator in Unity: how aircraft physics actually works under the hood, how to implement pitch/roll/yaw controls that feel responsive on a touchscreen, how mission systems are structured, and the performance considerations unique to rendering open-world environments on mobile hardware. I'll also point to a working reference implementation along the way for anyone who wants to see these systems in a completed project rather than just pseudocode.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Flight Physics Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;Most gameplay physics in Unity relies on &lt;code&gt;Rigidbody&lt;/code&gt; and built-in collision handling, and for many genres that's enough. Flight simulation breaks that assumption almost immediately, because an aircraft in flight is being acted on by multiple competing forces simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lift&lt;/strong&gt; — generated by airspeed and wing surface, pushing the aircraft upward&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag&lt;/strong&gt; — resistance from air, scaling with speed and surface area&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thrust&lt;/strong&gt; — forward force from the engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gravity&lt;/strong&gt; — constant downward force&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Torque from control surfaces&lt;/strong&gt; — pitch (elevators), roll (ailerons), and yaw (rudder)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these forces exist in isolation. Increase thrust, and airspeed increases, which increases lift, which changes how the aircraft responds to control input. This interdependency is what makes flight feel "real" — and it's also what makes naive implementations feel floaty, unresponsive, or physically absurd.&lt;/p&gt;

&lt;p&gt;A common beginner mistake is to directly manipulate the aircraft's &lt;code&gt;transform.rotation&lt;/code&gt; based on input, without ever touching physics forces. This produces something that &lt;em&gt;looks&lt;/em&gt; like flying but has none of the momentum, stall behavior, or inertia that makes flight simulation satisfying. The aircraft snaps to new orientations instantly instead of gradually rotating under torque, and there's no sense of speed affecting maneuverability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Physics Setup: Forces Over Transforms
&lt;/h2&gt;

&lt;p&gt;The right approach is to drive the aircraft through Unity's &lt;code&gt;Rigidbody&lt;/code&gt; using &lt;code&gt;AddForce&lt;/code&gt; and &lt;code&gt;AddTorque&lt;/code&gt;, letting the physics engine handle the actual motion integration. Here's a simplified version of what that looks like:&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;AircraftController&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;Rigidbody&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;thrustPower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;liftCoefficient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.05f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dragCoefficient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.02f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;throttleInput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;controlInput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// x = pitch, y = yaw, z = roll&lt;/span&gt;

    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FixedUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ApplyThrust&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;ApplyLift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;ApplyDrag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;ApplyControlTorque&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;ApplyThrust&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddForce&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;forward&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;throttleInput&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;thrustPower&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;ApplyLift&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;speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magnitude&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;liftForce&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;liftCoefficient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddForce&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;up&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;liftForce&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;ApplyDrag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddForce&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrMagnitude&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dragCoefficient&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;ApplyControlTorque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRelativeTorque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;controlInput&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;pitchSensitivity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;controlInput&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;yawSensitivity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;controlInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rollSensitivity&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision here is that &lt;strong&gt;lift scales with the square of speed&lt;/strong&gt;. This is what produces the emergent behavior you actually want from a flight sim: a slow-moving aircraft generates almost no lift and will stall or fall out of the sky, while a fast-moving aircraft generates strong lift and climbs easily. You don't need to hand-script stall behavior — it emerges naturally from the force relationships if the coefficients are tuned correctly.&lt;/p&gt;

&lt;p&gt;Drag scaling with the square of velocity is equally important. Without it, aircraft accelerate indefinitely under constant thrust, which breaks the sense of a realistic top speed and makes throttle management meaningless to the player.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tuning Control Surfaces for Mobile Touch Input
&lt;/h2&gt;

&lt;p&gt;Desktop flight sims typically use a joystick or keyboard combination for pitch, roll, and yaw. Mobile devices need touch-based equivalents that are precise enough to feel controllable but forgiving enough for casual play on a small screen. There are two common approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual joystick overlay&lt;/strong&gt; — a fixed or floating on-screen joystick that maps drag distance and direction to pitch and roll input. This is the most common approach for arcade-leaning flight games because it gives players a consistent, learnable control scheme.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tilt-based input&lt;/strong&gt; — using the device's accelerometer or gyroscope to map physical device tilt to aircraft orientation. This can feel more immersive but requires careful dead-zone tuning, since raw accelerometer data is noisy and needs smoothing to avoid jittery control response.&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;HandleTouchInput&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;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchCount&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Touch&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTouch&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;Vector2&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;touch&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;joystickCenter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="n"&gt;normalizedDelta&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClampMagnitude&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;joystickRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;controlInput&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="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;normalizedDelta&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="c1"&gt;// pitch&lt;/span&gt;
        &lt;span class="n"&gt;controlInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;normalizedDelta&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="c1"&gt;// roll&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="c1"&gt;// Auto-recenter controls when not touching&lt;/span&gt;
        &lt;span class="n"&gt;controlInput&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;controlInput&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="n"&gt;zero&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;recenterSpeed&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;That auto-recenter behavior matters more than it might seem — without it, releasing the touch leaves the aircraft locked into its last control input, which feels broken to players used to a joystick returning to neutral.&lt;/p&gt;




&lt;h2&gt;
  
  
  Structuring Mission Systems Without Hardcoding Every Level
&lt;/h2&gt;

&lt;p&gt;A flight simulator's mission system needs to support varied objective types — reach a checkpoint, land within a target zone, deliver cargo, survive an emergency scenario — without requiring a new script for every mission. A clean approach is to define missions as data rather than code, using a base &lt;code&gt;MissionObjective&lt;/code&gt; class with polymorphic subtypes:&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;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MissionObjective&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;objectiveDescription&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;abstract&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CheckCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AircraftController&lt;/span&gt; &lt;span class="n"&gt;aircraft&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;CheckpointObjective&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MissionObjective&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;Transform&lt;/span&gt; &lt;span class="n"&gt;checkpointTransform&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;triggerRadius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20f&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;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CheckCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AircraftController&lt;/span&gt; &lt;span class="n"&gt;aircraft&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;distance&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;Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aircraft&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;checkpointTransform&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;return&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;triggerRadius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LandingObjective&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MissionObjective&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;Transform&lt;/span&gt; &lt;span class="n"&gt;runwayZone&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;maxLandingSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;15f&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;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;CheckCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AircraftController&lt;/span&gt; &lt;span class="n"&gt;aircraft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;inZone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runwayZone&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;Collider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;bounds&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;aircraft&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;safeSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aircraft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentSpeed&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxLandingSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;grounded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aircraft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsGrounded&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;inZone&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;safeSpeed&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;grounded&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;Using &lt;code&gt;ScriptableObject&lt;/code&gt;-based mission definitions means level designers (or you, wearing a level design hat) can create new missions entirely in the Unity Editor — dragging in checkpoint transforms and tuning trigger radii — without touching code. This is the same data-driven pattern worth using for any content-heavy game system, since it decouples level content from engine logic and makes iteration dramatically faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Considerations for Open-World Mobile Rendering
&lt;/h2&gt;

&lt;p&gt;Flight simulators are unusual among mobile game genres because they demand large, continuous, explorable environments rather than the small, contained levels typical of most casual mobile games. That creates rendering challenges that don't come up in a puzzle game or a platformer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level of Detail (LOD) systems&lt;/strong&gt; are non-negotiable for open-world flight environments. Terrain, buildings, and distant objects need progressively simplified meshes as the aircraft's distance from them increases. Unity's built-in &lt;code&gt;LODGroup&lt;/code&gt; component handles this reasonably well for static geometry, but terrain specifically benefits from a dedicated LOD terrain system that adjusts mesh resolution based on camera distance in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frustum and occlusion culling&lt;/strong&gt; matter more in flight sims than almost any other mobile genre, because the camera is frequently pointed at wide-open sky with a large draw distance. Making sure Unity's culling systems are configured correctly — and that terrain chunks outside the view frustum aren't being processed unnecessarily — has an outsized impact on frame rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw call batching&lt;/strong&gt; for repeated environment assets (trees, buildings, terrain tiles) should use GPU instancing wherever the art style allows it. A flight sim scene can easily contain thousands of repeated small objects across a large terrain, and without batching, draw calls become the primary bottleneck well before you run out of raw polygon budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fog and atmospheric scattering&lt;/strong&gt; aren't just visual polish in a flight sim — they're a legitimate performance tool. Using distance fog to obscure the furthest-rendered terrain lets you reduce draw distance and LOD detail at the horizon without it being visually obvious, since the player's eye reads the fade as atmospheric haze rather than as a rendering cutoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling the Camera: Why Flight Sim Cameras Are Their Own Problem
&lt;/h2&gt;

&lt;p&gt;A chase camera that works fine for a car racing game usually breaks down in a flight sim, because aircraft rotate on all three axes simultaneously in a way ground vehicles never do. A naive camera that rigidly follows the aircraft's rotation will roll and pitch violently with every player input, which is disorienting rather than immersive.&lt;/p&gt;

&lt;p&gt;The fix most flight sims use is a camera that lags behind the aircraft's rotation with smoothed interpolation, particularly on the roll axis:&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;LateUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;targetPosition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aircraft&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;aircraft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forward&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;followDistance&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="n"&gt;up&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;followHeight&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;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;targetPosition&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;positionSmoothing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Quaternion&lt;/span&gt; &lt;span class="n"&gt;targetRotation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LookRotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aircraft&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;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="n"&gt;up&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;rotation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slerp&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;rotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetRotation&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;rotationSmoothing&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 that the camera's "up" vector is locked to world-up (&lt;code&gt;Vector3.up&lt;/code&gt;) rather than the aircraft's local up. This keeps the horizon roughly level from the player's perspective even as the aircraft rolls, which dramatically reduces motion sickness and disorientation compared to a camera that rigidly mirrors the aircraft's full rotation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where a Reference Implementation Helps
&lt;/h2&gt;

&lt;p&gt;Reading through physics formulas and code snippets is useful, but there's a real gap between understanding the theory and seeing how all of these systems — flight physics, touch controls, mission structures, LOD terrain, and camera smoothing — are integrated into a single working, published-ready project. If you want to study a complete implementation of these systems rather than assembling them piece by piece, the &lt;a href="https://unitysourcecode.net/product/flight-simulator-game-complete-source-code-with-unity-assets" rel="noopener noreferrer"&gt;Flight Simulator Game – Complete Source Code with Unity Assets&lt;/a&gt; is a full Unity project built around exactly this architecture — physics-driven flight controls, open-world exploration, mission-based objectives, and mobile-optimized rendering — with the complete codebase and asset structure available to inspect and extend.&lt;/p&gt;

&lt;p&gt;Working from a complete reference project like this is a genuinely useful way to learn systems-level Unity architecture, since you can trace how each subsystem connects to the others in a way that isolated tutorials rarely show.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applying These Same Physics Principles to Other Genres
&lt;/h2&gt;

&lt;p&gt;Interestingly, several of the engineering patterns covered here — data-driven objective systems, careful input smoothing, and force-based movement instead of transform manipulation — show up again in genres that look nothing like flight simulation on the surface. Precision-based physics games, where careful force application and momentum determine success rather than raw reflexes, rely on many of the same underlying principles: tuning coefficients so the physics &lt;em&gt;feels&lt;/em&gt; right rather than scripting outcomes directly.&lt;/p&gt;

&lt;p&gt;If you're interested in how force-driven physics and precision mechanics translate to a completely different genre and control scheme, it's worth reading &lt;a href="https://dev.to/unitysourcecode/building-a-3d-sliceprecision-style-mobile-game-in-unity-mechanics-physics-and-performance-14pg"&gt;Building a 3D Slice/Precision-Style Mobile Game in Unity: Mechanics, Physics, and Performance&lt;/a&gt;, which covers a similar physics-first approach applied to a very different gameplay context, along with performance optimization strategies that overlap significantly with the mobile rendering concerns discussed above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond Flight: Puzzle Mechanics and the Value of Studying Different Genres
&lt;/h2&gt;

&lt;p&gt;One habit worth building as a Unity developer is studying game systems outside your primary genre, since underlying architecture patterns — state machines, data-driven objectives, physics tuning — transfer across genres more than developers often expect. Sorting and logic-based puzzle mechanics, for example, involve their own interesting state management and win-condition validation problems that are worth understanding even if you primarily build simulation or action games.&lt;/p&gt;

&lt;p&gt;For a look at how those systems are structured in a completely different genre, the &lt;a href="https://unitysourcecode.net/product/bird-sort-puzzle-game" rel="noopener noreferrer"&gt;Bird Sort Puzzle Game&lt;/a&gt; source code is a useful contrast case — a logic-driven puzzle template built around sorting mechanics and win-state validation, which approaches game state management from a very different angle than the physics-driven systems covered in this article, but shares the same underlying discipline of building clean, modular, data-driven Unity architecture.&lt;/p&gt;




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

&lt;p&gt;Building a convincing flight simulator in Unity comes down to a handful of interconnected systems: physics driven by real force relationships rather than direct transform manipulation, control schemes tuned specifically for touch input, mission systems structured as data rather than hardcoded logic, and rendering optimizations suited to large open-world environments on mobile hardware.&lt;/p&gt;

&lt;p&gt;None of these systems are individually exotic — &lt;code&gt;AddForce&lt;/code&gt;, &lt;code&gt;AddTorque&lt;/code&gt;, &lt;code&gt;ScriptableObject&lt;/code&gt;-based data, and &lt;code&gt;LODGroup&lt;/code&gt; are all standard Unity tools. The engineering skill is in how they're tuned and integrated together, and that integration work is where most of the real development time goes on a project like this.&lt;/p&gt;

&lt;p&gt;If you're building a flight sim, a racing game, or any physics-heavy simulation title in Unity, I'd genuinely recommend starting by getting the force relationships right in isolation — lift, drag, and thrust in a bare test scene — before layering missions, UI, and rendering optimization on top. Get the physics feeling right first; everything else is easier to build once the core loop already feels good in your hands.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>physics</category>
    </item>
    <item>
      <title>Building a 3D "Slice/Precision" Style Mobile Game in Unity: Mechanics, Physics, and Performance</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:17:57 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-3d-sliceprecision-style-mobile-game-in-unity-mechanics-physics-and-performance-14pg</link>
      <guid>https://dev.to/unitysourcecode/building-a-3d-sliceprecision-style-mobile-game-in-unity-mechanics-physics-and-performance-14pg</guid>
      <description>&lt;h2&gt;
  
  
  What makes the slice-and-precision genre deceptively hard to build well
&lt;/h2&gt;

&lt;p&gt;If you've spent any time on the app stores in the last few years, you've run into the genre I'm going to call "slice/precision" games — the ones where the entire loop revolves around one clean physical action performed with split-second timing: slicing a rotating object cleanly in half, cutting through a stack of material at exactly the right depth, or splitting a moving shape into precise proportions. Titles built around this mechanic are everywhere on the charts because the core loop is instantly understandable and deeply satisfying to execute well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmxe3lq26hdtonvsg5ek.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmxe3lq26hdtonvsg5ek.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's less obvious from the player side is how much technical work goes into making that one action feel good. A slice that looks janky, a physics response that feels floaty, or a frame drop at the exact moment of the cut will kill the entire appeal of the genre, because the whole game lives or dies on that single moment of feedback. This article breaks down how these mechanics actually get built in Unity — the geometry approach, the physics considerations, the feedback systems that sell the moment, and the performance work that keeps it all running smoothly on the wide range of devices mobile players actually own.&lt;/p&gt;

&lt;p&gt;I'll be using a published example of this genre, &lt;a href="https://unitysourcecode.net/product/perfect-slice-3d-unity-game" rel="noopener noreferrer"&gt;Perfect Slice 3D&lt;/a&gt;, as a reference point throughout, since it's a clean, focused implementation of exactly the mechanics discussed here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem: real-time mesh slicing
&lt;/h2&gt;

&lt;p&gt;The technical heart of any slice-based game is runtime mesh manipulation — taking an existing 3D mesh and dividing it into two new, correctly formed meshes along a plane defined by the player's cut. This sounds simple conceptually and is genuinely fiddly in practice, because a naive implementation breaks in predictable ways: normals point the wrong direction after the cut, the interior "cut face" is missing entirely so the sliced object looks hollow, or the resulting mesh has degenerate triangles that cause visual artifacts.&lt;/p&gt;

&lt;p&gt;The general algorithm most implementations converge on works like this: define the slicing plane in world space based on the player's swipe or cut gesture, iterate through the original mesh's triangles and classify each vertex as being on the positive or negative side of the plane, split any triangle that straddles the plane by computing new intersection vertices, and then cap each resulting half with a new triangulated face along the cut line so both halves read as solid objects rather than open shells. Unity doesn't include this out of the box, so most teams either write a custom mesh-slicing utility or adapt one of the several well-documented open-source approaches built around this same triangle-classification technique.&lt;/p&gt;

&lt;p&gt;A simplified version of the classification step looks roughly like this in C#:&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="n"&gt;Plane&lt;/span&gt; &lt;span class="n"&gt;cutPlane&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;Plane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cutNormal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cutPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;triangles&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="n"&gt;i&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="kt"&gt;bool&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;cutPlane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;triangles&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cutPlane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;triangles&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cutPlane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;triangles&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="m"&gt;2&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;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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Triangle sits entirely on one side — assign to that half directly&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="c1"&gt;// Triangle straddles the plane — compute intersection points&lt;/span&gt;
        &lt;span class="c1"&gt;// and generate new triangles for both halves&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 real complexity lives in that "compute intersection points" step, plus correctly recalculating UVs and normals for the newly generated vertices so the cut surface shades and textures correctly instead of looking flat or broken. It's worth budgeting real development time for this system specifically, because it's the one piece of the game that can't be faked with animation tricks — it has to actually work correctly across an enormous variety of object shapes and cut angles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Physics: making the halves feel weighty, not floaty
&lt;/h2&gt;

&lt;p&gt;Once an object is split, what happens to the two halves matters as much as the split itself. This is where a lot of slice-genre prototypes feel unsatisfying even when the mesh-cutting math is technically correct — the resulting pieces either don't react to the cut at all, or react with generic physics that ignores the specifics of what just happened.&lt;/p&gt;

&lt;p&gt;A few details separate a satisfying slice from a flat one. Apply an impulse to each half along the slicing plane's normal, proportional to the force or speed of the cut gesture, so faster or cleaner cuts visibly send the pieces apart with more energy. Recalculate each half's center of mass and collider after the cut rather than relying on the original object's collider, since an asymmetric cut should make an object tumble differently than a clean center cut. And stagger the physics response slightly rather than applying it in the same frame as the visual cut, since a one or two frame delay between the visual separation and the physics kick actually reads as more natural to players, even though it's technically "wrong" relative to a real-world simultaneous event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selling the moment: feedback systems
&lt;/h2&gt;

&lt;p&gt;The slice-and-precision genre survives almost entirely on feedback quality, because the underlying action repeats constantly and needs to stay satisfying across hundreds of repetitions in a single session. The systems that do the heaviest lifting here are usually the least technically complex: a brief hitstop (freezing gameplay for a few milliseconds at the moment of a clean cut) sells impact far more effectively than any particle effect on its own; particle bursts along the cut plane, scaled to the precision of the cut, give the player instant visual confirmation of how well they performed; and audio layering — a distinct sound for a perfect cut versus a sloppy one — reinforces the skill feedback loop without requiring any UI at all.&lt;/p&gt;

&lt;p&gt;None of these systems are expensive to implement, but they're easy to skip during a prototyping phase and then never circle back to, which is a mistake — for this genre specifically, feedback polish isn't a "nice to have" layered on top of the mechanic, it effectively &lt;em&gt;is&lt;/em&gt; the mechanic from the player's perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where developers underestimate the genre: performance
&lt;/h2&gt;

&lt;p&gt;Real-time mesh slicing generates new geometry constantly during gameplay, which creates a performance profile that's genuinely different from most other mobile genres. Every slice means new mesh data being allocated, new colliders being generated, and new physics bodies being simulated — and if a level has multiple slice-able objects on screen simultaneously, that cost compounds quickly.&lt;/p&gt;

&lt;p&gt;The practical mitigations that matter most here are pooling and reusing GameObjects and mesh buffers for sliced pieces rather than allocating fresh ones every time, capping the number of active physics-simulated pieces on screen at once and transitioning older pieces to static or destroyed states once their visual moment has passed, and being deliberate about collision mesh complexity for the sliced pieces — a simplified convex collider is almost always sufficient and dramatically cheaper than a mesh collider matching the exact cut geometry.&lt;/p&gt;

&lt;p&gt;This genre also tends to get built by smaller teams iterating quickly, which means performance testing frequently happens only on the developer's own device — usually a mid-range or newer phone — right up until release, at which point real-world reviews reveal stutter and slowdown on the wider range of hardware actual players own. Given how central smooth, responsive feedback is to this genre specifically, a dropped frame at the moment of a slice is far more damaging to the player experience here than in a genre with slower-paced mechanics. I've written a detailed technical breakdown of exactly how to approach this kind of optimization work for Android specifically — covering texture and shader settings, batching, and profiling methodology — that's directly relevant if you're building anything in this physics-heavy, real-time-mesh-generation space: &lt;a href="https://unitysourcecode.net/blog/optimize-a-unity-mobile-game-for-low-end-android-devices" rel="noopener noreferrer"&gt;Optimize a Unity Mobile Game for Low-End Android Devices&lt;/a&gt;. Given how heavily this genre depends on physics and dynamic geometry generation, treating device-range testing as a late-stage checklist item rather than an ongoing practice is one of the more common and costly mistakes teams make.&lt;/p&gt;

&lt;h2&gt;
  
  
  A related genre worth studying: puzzle-based precision games
&lt;/h2&gt;

&lt;p&gt;Slice mechanics share a lot of DNA with another mobile genre that's currently having a strong moment — color-sorting and jam-style puzzle games, where the core skill is also about precise, satisfying manipulation of objects within tight spatial constraints, just without the destructive mesh-cutting element. If you're interested in how a related genre solves similar problems around satisfying interaction feedback, level design pacing, and mobile-appropriate physics and UI, I put together a full developer deep-dive into building a Color Block Jam–style 3D puzzle game in Unity, covering the grid logic, block movement systems, and level generation approach that genre depends on: &lt;a href="https://dev.to/unitysourcecode/building-a-color-block-jam-style-3d-puzzle-game-in-unity-a-developers-deep-dive-4id9"&gt;Building a Color Block Jam-Style 3D Puzzle Game in Unity: A Developer's Deep Dive&lt;/a&gt;. It's a useful comparison piece if you're deciding between genres or want to see how similar design principles — clear feedback, tight controls, escalating difficulty — get implemented differently when the core verb is "sort" instead of "cut."&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it into practice
&lt;/h2&gt;

&lt;p&gt;If you're prototyping a slice-based mechanic, the order of operations that tends to work best is: get a basic, even if crude, mesh-slicing implementation working first so you can actually play with the core loop early; layer in physics response and feedback systems as soon as the slicing works, since this genre lives or dies on feel and you want to be evaluating that feel as early as possible; and only after the core loop is genuinely fun should you invest heavily in level content and polish, since no amount of level design saves a slice mechanic that doesn't feel satisfying at the most basic level.&lt;/p&gt;

&lt;p&gt;The technical challenge of runtime mesh slicing is real, but it's also well-trodden ground with solid reference implementations to learn from. The bigger risk in this genre isn't usually the geometry math — it's underinvesting in the feedback and performance work that determines whether the hundredth slice in a session feels as satisfying as the first one, and whether that satisfaction survives contact with the actual range of devices your players are using.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Color Block Jam Style 3D Puzzle Game in Unity: A Developer's Deep Dive</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:30:32 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-color-block-jam-style-3d-puzzle-game-in-unity-a-developers-deep-dive-4id9</link>
      <guid>https://dev.to/unitysourcecode/building-a-color-block-jam-style-3d-puzzle-game-in-unity-a-developers-deep-dive-4id9</guid>
      <description>&lt;p&gt;Block-jam puzzle games have quietly become one of the most reliable genres in the hyper-casual and casual mobile space. If you've spent any time browsing the top charts, you've almost certainly seen a title where the goal is simple: slide colored blocks out of a confined space before you run out of moves. The mechanic is easy to learn, satisfying to master, and — from a developer's perspective — genuinely interesting to build well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqln9157x6b1rz9zsqas.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqln9157x6b1rz9zsqas.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article I want to walk through what it actually takes to build a &lt;strong&gt;3D block-jam puzzle game in Unity&lt;/strong&gt;, using a real, shipped-style template — &lt;a href="https://unitysourcecode.net/product/color-block-jam-3d-puzzle-game" rel="noopener noreferrer"&gt;Color Block Jam 3D Puzzle&lt;/a&gt; — as the reference point. I'll cover the core gameplay loop, the systems you need to get right (grid logic, movement, level design, monetization), the performance traps that catch mobile devs off guard, and a practical checklist for taking a template like this from "it compiles" to "it's on the store."&lt;/p&gt;

&lt;p&gt;Whether you're building from scratch, evaluating a Unity asset to reskin, or just trying to understand why this genre performs so well, this should give you a solid technical foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Block-Jam Puzzles Work So Well on Mobile
&lt;/h2&gt;

&lt;p&gt;Before touching any code, it's worth understanding &lt;em&gt;why&lt;/em&gt; this genre is so dominant in casual mobile gaming, because the design goals should drive your architecture.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instant comprehension.&lt;/strong&gt; A player understands the objective within two seconds of seeing the board — no tutorial wall of text required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short session length.&lt;/strong&gt; Levels typically resolve in 15–60 seconds, which fits perfectly into ad-supported, session-based monetization loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High replay value through procedural difficulty.&lt;/strong&gt; Because the core mechanic is spatial reasoning rather than reflexes, you can generate near-infinite levels algorithmically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural ad placement points.&lt;/strong&gt; Level completion and failure states are clean, non-intrusive moments to show interstitials or offer rewarded continues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These four properties dictate almost everything about how you should structure your Unity project. Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Gameplay Loop: Grid, Blocks, and Exit Logic
&lt;/h2&gt;

&lt;p&gt;At its heart, a 3D block-jam game is a constraint-satisfaction puzzle rendered on a grid. The fundamental building blocks (pun intended) are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;grid/board representation&lt;/strong&gt; — usually a 2D or 3D array tracking occupied cells&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block entities&lt;/strong&gt; — each with a color, a shape footprint, and a fixed movement axis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit/collection points&lt;/strong&gt; — where blocks of a matching color need to reach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move validation&lt;/strong&gt; — checking whether a block can legally slide given current occupancy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal grid representation might look 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GridCell&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;Position&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;IsOccupied&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;BlockController&lt;/span&gt; &lt;span class="n"&gt;OccupyingBlock&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;GridManager&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;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&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;height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&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;GridCell&lt;/span&gt;&lt;span class="p"&gt;[,]&lt;/span&gt; &lt;span class="n"&gt;cells&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;{&lt;/span&gt;
        &lt;span class="n"&gt;cells&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;GridCell&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;height&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;width&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="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;height&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="n"&gt;cells&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GridCell&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;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2Int&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="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;CanMoveTo&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;target&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;target&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="m"&gt;0&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;x&lt;/span&gt; &lt;span class="p"&gt;&amp;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;target&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="m"&gt;0&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;y&lt;/span&gt; &lt;span class="p"&gt;&amp;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;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cells&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;x&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;y&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;IsOccupied&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 deliberately simple, but it's the backbone everything else hangs off. The template referenced above extends this pattern into a full 3D coordinate system, since blocks in Color Block Jam move along a constrained axis in 3D space rather than a flat 2D board — which changes your collision and raycasting approach meaningfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling 3D Movement Constraints
&lt;/h3&gt;

&lt;p&gt;The "3D" part of these puzzle games isn't just a visual upgrade over 2D match-style games — it changes the interaction model. Each block typically has a &lt;strong&gt;locked movement axis&lt;/strong&gt; (it can only slide forward/backward along one direction), which means your input handling needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Raycast from touch/mouse input to detect which block was selected&lt;/li&gt;
&lt;li&gt;Determine that block's allowed axis&lt;/li&gt;
&lt;li&gt;Project drag input onto that axis&lt;/li&gt;
&lt;li&gt;Validate the path is clear before committing the move&lt;/li&gt;
&lt;li&gt;Animate the slide, then re-evaluate the board state (did the block reach an exit? Did it unblock another block?)
&lt;/li&gt;
&lt;/ol&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;OnDrag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector2&lt;/span&gt; &lt;span class="n"&gt;screenDelta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectedBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MovementAxis&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;projectedDistance&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;Dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="n"&gt;projectedDistance&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;selectedBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositiveDirection&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selectedBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NegativeDirection&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;gridManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CanMoveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedBlock&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;direction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;selectedBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slide&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="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 important detail here is &lt;strong&gt;path validation, not just destination validation&lt;/strong&gt; — a 3-cell-long block needs every cell along its path checked, not just the final resting cell, or you'll get blocks visually clipping through each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level Design: Data-Driven, Not Hardcoded
&lt;/h2&gt;

&lt;p&gt;If you're building (or extending) a puzzle game with any real content pipeline, hardcoding levels in the scene is a dead end after level 10. Every serious block-jam implementation uses a &lt;strong&gt;data-driven level format&lt;/strong&gt;, typically JSON or ScriptableObjects, so that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Level designers can iterate without touching code&lt;/li&gt;
&lt;li&gt;You can procedurally generate levels and validate solvability offline&lt;/li&gt;
&lt;li&gt;Live-ops teams can push new level packs post-launch without a full app update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ScriptableObject-based level definition is usually the cleanest approach in Unity:&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;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;BlockDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;blocks&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;parTime&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;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;BlockDefinition&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;startPosition&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;size&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;BlockColor&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;public&lt;/span&gt; &lt;span class="n"&gt;MovementAxis&lt;/span&gt; &lt;span class="n"&gt;axis&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 also makes it trivial to build a &lt;strong&gt;level editor tool inside Unity's Editor window&lt;/strong&gt;, which is genuinely worth the half-day investment if you plan to ship more than a handful of levels. Designers dragging blocks onto a grid and hitting "Save" is dramatically faster than editing raw JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solvability: The Problem Nobody Talks About Enough
&lt;/h2&gt;

&lt;p&gt;Here's something a lot of tutorials skip entirely: &lt;strong&gt;not every configuration of blocks on a grid is solvable.&lt;/strong&gt; If you're procedurally generating levels, or even hand-placing them quickly, you need a solver pass that verifies a level can actually be completed before it ships.&lt;/p&gt;

&lt;p&gt;The simplest reliable approach is a breadth-first search over the state space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each unique board configuration is a node&lt;/li&gt;
&lt;li&gt;Each legal move is an edge&lt;/li&gt;
&lt;li&gt;The goal state is "all target-colored blocks have exited"&lt;/li&gt;
&lt;li&gt;If BFS from the initial state can reach a goal state within your move limit, the level is solvable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small grids (8x8 or smaller with a handful of blocks) this is computationally cheap enough to run at build time or even at runtime for procedural generation. Skipping this step is the single most common reason indie block-jam clones ship with unsolvable levels and get torched in reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: Where 3D Puzzle Games Quietly Fall Apart
&lt;/h2&gt;

&lt;p&gt;3D puzzle games look deceptively simple, but there are a few specific performance traps that hit this genre harder than others:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Draw calls from unique block materials.&lt;/strong&gt; If every block color uses a separate material and shader variant, you'll rack up draw calls fast on a crowded board. Use a single shader with a color property (or a texture atlas) and batch via GPU instancing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Physics-based sliding.&lt;/strong&gt; It's tempting to use Rigidbody + physics collisions for block movement because it "just works" visually. Don't. Use kinematic, grid-based movement with tweened animation (DOTween or a simple coroutine lerp) instead — physics-driven puzzle logic is a constant source of edge-case bugs (blocks nudging each other out of grid alignment, floating point drift, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Overdraw from transparent UI and particle effects.&lt;/strong&gt; Block-jam games love juicy VFX on level completion — confetti, particle bursts, glow trails. On mid-range Android devices, unoptimized overdraw from these effects is a common cause of frame drops right at the moment you most want the game to feel polished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Garbage collection spikes from per-move allocations.&lt;/strong&gt; If your move validation or pathing logic allocates new lists/arrays every single drag event, you'll get GC stutter during exactly the interaction that needs to feel buttery smooth. Pool your temporary collections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization Architecture
&lt;/h2&gt;

&lt;p&gt;Puzzle games with clear level boundaries are genuinely well-suited to ad monetization, but the implementation details matter for retention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interstitials&lt;/strong&gt; should fire on level completion, not level start — showing an ad before a player even sees the puzzle is a fast way to inflate churn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewarded video&lt;/strong&gt; works best as an &lt;em&gt;opt-in unstuck mechanic&lt;/em&gt; — offering extra moves or a hint after a failed attempt, rather than being forced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Banner ads&lt;/strong&gt;, if used at all, belong on menu/level-select screens, never during active gameplay where they compete for touch input space.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Structuring your monetization calls behind an interface rather than calling AdMob/Unity Ads SDKs directly throughout your codebase will save you significant pain later:&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;IAdService&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;ShowInterstitial&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;Action&lt;/span&gt; &lt;span class="n"&gt;onComplete&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;ShowRewarded&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;Action&lt;/span&gt; &lt;span class="n"&gt;onReward&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;Action&lt;/span&gt; &lt;span class="n"&gt;onFailed&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 lets you swap mediation providers, A/B test ad frequency, or add a mediation layer without touching gameplay code — something that matters a lot once you're running live-ops experiments on ad placement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reskinning: What Actually Needs to Change
&lt;/h2&gt;

&lt;p&gt;If you're starting from an existing template rather than building from zero — which is a completely reasonable choice given how solved the core mechanics are for this genre — the real differentiation work happens in a fairly narrow set of places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual theme:&lt;/strong&gt; block materials, environment lighting, particle effects, UI skin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta progression:&lt;/strong&gt; whether you add a level map, currency system, or daily rewards layer on top of the core loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficulty curve:&lt;/strong&gt; move limits, grid sizes, and the pacing of new mechanics (locked blocks, obstacles, special colors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization tuning:&lt;/strong&gt; ad frequency and rewarded-ad placement, calibrated against your own retention data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you start layering changes onto any existing Unity codebase — whether it's your own six-month-old project or a template you've picked up — it's worth doing a proper audit first so you know what you're actually working with. I wrote a full checklist for this exact situation here: &lt;a href="https://dev.to/unitysourcecode/how-to-audit-a-unity-codebase-before-you-reskin-it-a-developers-checklist-28cp"&gt;How to Audit a Unity Codebase Before You Reskin It&lt;/a&gt;. It covers dependency checks, asset bloat, deprecated API usage, and the kind of technical debt that turns a "quick reskin" into a multi-week rewrite if you skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing Checklist
&lt;/h2&gt;

&lt;p&gt;A few things worth confirming before you push a build to the stores, specific to this genre:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Grid and block colliders don't leak physics jitter into visual position&lt;/li&gt;
&lt;li&gt;[ ] Level solver pass has validated every level in your content set&lt;/li&gt;
&lt;li&gt;[ ] Ad SDK calls are behind an interface and tested in both online/offline states&lt;/li&gt;
&lt;li&gt;[ ] Draw calls per typical board state are profiled (Unity Frame Debugger) on a mid-tier device, not just your dev machine&lt;/li&gt;
&lt;li&gt;[ ] Save/load system handles interrupted sessions (app killed mid-level) gracefully&lt;/li&gt;
&lt;li&gt;[ ] Android back-button and iOS gesture navigation don't break mid-drag input&lt;/li&gt;
&lt;li&gt;[ ] Localization strings are externalized, not hardcoded in UI prefabs&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Block-jam puzzle games are a great genre to build in Unity precisely because the mechanics are well-understood but the execution quality still varies enormously between shipped titles. The difference between a forgettable clone and a chart-performing puzzle game usually comes down to the details covered above: clean grid logic, verified solvability, disciplined performance profiling, and monetization that respects the player's session.&lt;/p&gt;

&lt;p&gt;If you want to see a complete, working implementation of these systems rather than building the grid logic, level format, and movement handling from scratch, the Color Block Jam 3D Puzzle Unity template is a solid reference point — it's AdMob-ready, optimized for both Android and iOS, and structured with clean, documented C# scripts that are straightforward to extend. It's also a good case study to pair with the audit checklist above if you're evaluating whether to build on top of it or use it purely as a learning reference.&lt;/p&gt;

&lt;p&gt;For more Unity source code and game templates across other genres, the full catalog is browsable here: &lt;a href="https://unitysourcecode.net/category/games" rel="noopener noreferrer"&gt;Unity Games Category&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mobiledev</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>How to Audit a Unity Codebase Before You Reskin It (A Developer's Checklist)</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:04:57 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/how-to-audit-a-unity-codebase-before-you-reskin-it-a-developers-checklist-28cp</link>
      <guid>https://dev.to/unitysourcecode/how-to-audit-a-unity-codebase-before-you-reskin-it-a-developers-checklist-28cp</guid>
      <description>&lt;p&gt;Reskinning a Unity project sounds simple on paper: take an existing game, swap the art, change the theme, ship it. In practice, whether that process takes three days or three weeks almost entirely comes down to one thing — how the original codebase was architected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbyuzpct7o2dr2i2lueba.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbyuzpct7o2dr2i2lueba.webp" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most articles about Unity reskinning talk about this at a high level: "look for clean code," "check the folder structure," "make sure it's modular." That advice is true, but it's also vague enough to be nearly useless when you're actually staring at a &lt;code&gt;.unitypackage&lt;/code&gt; you just imported and trying to decide whether it's worth your time.&lt;/p&gt;

&lt;p&gt;This article takes a more concrete approach. Instead of general principles, we'll walk through a practical, code-level audit you can run on any Unity project in under thirty minutes — the specific files to open, the specific patterns to look for, and the specific red flags that predict a painful reskin before you've written a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Quick Audit Saves You Weeks
&lt;/h2&gt;

&lt;p&gt;Every Unity developer has a story about a project that looked great in the asset store preview and turned into a nightmare once they opened it. Usually, the problem isn't the game design — it's structural decisions made early in development that make even simple changes difficult. A hard-coded path here, a tangled singleton there, and suddenly changing a sprite requires touching four different scripts.&lt;/p&gt;

&lt;p&gt;The good news is that these problems are almost always visible within the first few files you open, if you know what to look for. A structured audit turns "does this project feel reskinnable?" — a vague, unreliable gut check — into a checklist you can run consistently across any project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check How Assets Are Referenced
&lt;/h2&gt;

&lt;p&gt;Open two or three of the core gameplay scripts and search for how they access sprites, prefabs, and audio clips. This is the single most important thing to check, because it determines how much of your reskin will require touching code versus simply dragging in new assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag pattern:&lt;/strong&gt;&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;BubbleSpawner&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;SpawnBubble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Sprite&lt;/span&gt; &lt;span class="n"&gt;bubbleSprite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sprites/bubble_red"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;bubble&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;Resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Prefabs/Bubble"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;bubble&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;SpriteRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;sprite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bubbleSprite&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 pattern hard-codes a specific file path directly into the script. To reskin this, you'd need to either rename your new assets to match these exact paths (fragile and confusing) or edit the script directly (extra work, and risky if you're not fully comfortable with the codebase).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you want to see instead:&lt;/strong&gt;&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;BubbleSpawner&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;Sprite&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;bubbleSprites&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;bubblePrefab&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;SpawnBubble&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;colorIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt; &lt;span class="n"&gt;bubble&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;bubblePrefab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;bubble&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;SpriteRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;sprite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bubbleSprites&lt;/span&gt;&lt;span class="p"&gt;[&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, every visual reference is exposed through the Inspector. Reskinning this component means dragging new sprites into a list — no code changes required. When auditing a project, open the Inspector for a handful of key GameObjects and count how many fields are actually exposed versus how much is buried in &lt;code&gt;Resources.Load&lt;/code&gt; calls or hard-coded strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Look for Data-Driven Configuration
&lt;/h2&gt;

&lt;p&gt;The next thing to check is how game content — levels, item stats, difficulty curves — is defined. Projects that hard-code this information directly into scripts are far more painful to expand or rebalance than ones using external data structures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flag pattern:&lt;/strong&gt;&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;SetLevelDifficulty&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;level&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;level&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;spawnRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.5f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;enemyCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;spawnRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.2f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;enemyCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&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="n"&gt;spawnRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.9f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;enemyCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...and so on, for every level&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new level requires a new code branch. This doesn't just make reskinning harder — it makes any future content updates harder too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you want to see instead:&lt;/strong&gt;&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;"LevelData"&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/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;float&lt;/span&gt; &lt;span class="n"&gt;spawnRate&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;enemyCount&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;levelName&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;With this pattern, level data lives as an asset that can be created, duplicated, and edited entirely inside the Unity Editor. A designer — or a developer doing a quick reskin — can create ten new level variants without opening a script editor at all. When auditing a project, search the codebase for &lt;code&gt;ScriptableObject&lt;/code&gt; and see how much of the game's content is defined this way versus hard-coded in &lt;code&gt;if&lt;/code&gt; statements or switch cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Trace One Full Interaction End to End
&lt;/h2&gt;

&lt;p&gt;Pick the single most important interaction in the game — the shot in a physics game, the swipe in a puzzle game, the tap in an arcade game — and trace it from input to outcome. This tells you more about the overall code quality than almost anything else you can check.&lt;/p&gt;

&lt;p&gt;This step matters even more in physics-heavy games, where an interaction chain often involves input handling, force application, collision detection, and scoring logic all working together. A good example of what a clean version of this chain looks like is covered in this technical breakdown of &lt;a href="https://dev.to/unitysourcecode/building-draw-to-solve-puzzle-mechanics-in-unity-line-rendering-physics-barriers-and-level-design-121e"&gt;building draw-to-solve puzzle mechanics in Unity, including line rendering, physics barriers, and level design&lt;/a&gt;. It walks through how a &lt;code&gt;LineRenderer&lt;/code&gt; component, physics-based barrier detection, and level-loading logic are wired together in a way that keeps each responsibility isolated — which is exactly the kind of separation you want to look for when auditing an unfamiliar project. If you can read through an interaction like this and understand what each component is responsible for without cross-referencing five other scripts, that's a strong signal the project is well-structured.&lt;/p&gt;

&lt;p&gt;If, instead, tracing a single interaction sends you jumping between a dozen loosely related scripts with unclear responsibilities, that's a signal the project may require significantly more time to safely modify, even if the end result would eventually look polished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Check for Singleton Overuse and Tight Coupling
&lt;/h2&gt;

&lt;p&gt;Singletons are common in Unity projects — they're a convenient way to manage game state, audio, or UI — but overused or poorly implemented singletons can make a codebase surprisingly fragile when you start modifying it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch for this pattern spreading everywhere:&lt;/strong&gt;&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;GameManager&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;GameManager&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="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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentLevel&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;AudioSource&lt;/span&gt; &lt;span class="n"&gt;musicSource&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;UIManager&lt;/span&gt; &lt;span class="n"&gt;ui&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;PlayerController&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// dozens of unrelated fields and methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a single "God object" like this ends up referenced throughout the entire codebase, even small changes can have unpredictable ripple effects, since so many systems depend on its internal state directly. This isn't necessarily a dealbreaker for a reskin — most reskins don't require deep refactoring — but it's a signal to be more cautious when adding new mechanics or making structural changes, since the blast radius of any given edit is harder to predict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Confirm UI Is Built to Scale
&lt;/h2&gt;

&lt;p&gt;UI issues are one of the most common sources of last-minute bugs during a reskin, especially when new art assets have different proportions than the originals. Open the Canvas settings and check the Canvas Scaler component.&lt;/p&gt;

&lt;p&gt;Look for a &lt;strong&gt;UI Scale Mode&lt;/strong&gt; set to &lt;code&gt;Scale With Screen Size&lt;/code&gt;, with a sensible reference resolution (commonly 1080×1920 for portrait mobile games) and a Match value that blends width and height scaling rather than relying on just one axis. Projects that leave the Canvas Scaler on &lt;code&gt;Constant Pixel Size&lt;/code&gt; often look correct on the developer's test device but break on other aspect ratios — something that becomes especially noticeable once you start swapping in UI art with different dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Skim the Monetization Integration
&lt;/h2&gt;

&lt;p&gt;Finally, search the project for ad network SDK calls (&lt;code&gt;AdMob&lt;/code&gt;, &lt;code&gt;IronSource&lt;/code&gt;, &lt;code&gt;Unity Ads&lt;/code&gt;, etc.) and in-app purchase logic. You don't need to fully understand the integration during an audit — you just need to confirm it exists, roughly where it sits in the code, and whether it's cleanly separated from gameplay logic or tangled directly into UI button handlers. Monetization code that's scattered across multiple unrelated scripts is a sign that UI reskinning could accidentally break ad triggers or purchase flows if you're not careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together: A Thirty-Minute Audit Checklist
&lt;/h2&gt;

&lt;p&gt;Here's the condensed version you can run on any new Unity project before committing to a reskin:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open two or three core gameplay scripts — are assets referenced through Inspector fields, or hard-coded paths?&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;ScriptableObject&lt;/code&gt; usage — is content data-driven, or buried in conditional logic?&lt;/li&gt;
&lt;li&gt;Trace one core interaction from input to outcome — is the responsibility chain clear and isolated?&lt;/li&gt;
&lt;li&gt;Search for singleton patterns — is state management centralized in a way that's manageable, or sprawling?&lt;/li&gt;
&lt;li&gt;Check the Canvas Scaler settings — is UI built to scale across devices?&lt;/li&gt;
&lt;li&gt;Locate the monetization integration — is it cleanly separated from gameplay and UI code?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these checks require deep familiarity with the specific game. They're structural questions that apply to almost any Unity project, regardless of genre, and running through them consistently makes it far easier to compare multiple candidate projects objectively instead of relying on how polished a demo video looks.&lt;/p&gt;

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

&lt;p&gt;This kind of code-level audit complements the higher-level evaluation criteria — genre fit, art requirements, monetization strategy — that usually come first when deciding what to reskin. A more general breakdown of what to look for across several template genres, including puzzle, physics-based, and simulation projects, is covered in this guide to the &lt;a href="https://unitysourcecode.net/blog/top-5-unity-game-templates-reskin-and-publish" rel="noopener noreferrer"&gt;top Unity game templates that are easiest to reskin and publish&lt;/a&gt;, which is a useful starting point before you get into the kind of technical audit described here.&lt;/p&gt;

&lt;p&gt;Once you've narrowed down a genre and a rough shortlist of candidates, running this checklist against each one — ideally by browsing a broader &lt;a href="https://unitysourcecode.net/products" rel="noopener noreferrer"&gt;catalog of available Unity source code projects&lt;/a&gt; so you have more than one option to compare — helps you make a more informed, evidence-based decision instead of picking based on demo footage alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Reading Unfamiliar Codebases Efficiently
&lt;/h2&gt;

&lt;p&gt;One skill that makes this whole audit process faster over time is getting comfortable reading unfamiliar code without needing to understand every line. When you open a script for the first time, resist the urge to read it top to bottom like a book. Instead, scan for three things first: the public and serialized fields (these tell you what's configurable from the Inspector), the method names (these tell you what the class actually does), and any calls to other manager or singleton classes (these tell you how coupled this script is to the rest of the project).&lt;/p&gt;

&lt;p&gt;This scanning approach is faster than a full read-through and, in practice, tells you almost everything you need for an audit. If a script's fields are mostly private with hard-coded values inside method bodies, that's already a signal before you've read a single line of logic. If the method names are vague (&lt;code&gt;DoStuff()&lt;/code&gt;, &lt;code&gt;Handle()&lt;/code&gt;, &lt;code&gt;Update2()&lt;/code&gt;), that's a signal too — poorly named methods often correlate with poorly organized logic elsewhere in the same script.&lt;/p&gt;

&lt;p&gt;Over time, this kind of fast scanning becomes intuitive. You'll start recognizing patterns — a well-structured spawner script, a tangled UI controller, a clean ScriptableObject-driven economy system — within seconds of opening a file, which makes the thirty-minute audit described above get faster with every project you run it against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Objections to This Process
&lt;/h2&gt;

&lt;p&gt;It's worth addressing a couple of objections that come up when developers hear about running a structured audit before starting a reskin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This feels like overkill for a small project."&lt;/strong&gt; For very simple genres — a single-scene puzzle game, for example — a lighter version of this audit is fine. You might only need steps 1 and 3 (asset references and interaction tracing) to get a confident read on the project. The full checklist matters most for genres with more interconnected systems, like simulation, RPG, or physics-heavy games, where a structural problem discovered late is much more expensive to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I can just start and figure it out as I go."&lt;/strong&gt; This works for developers with enough experience to recognize structural problems the moment they encounter them and adapt on the fly. For developers earlier in their reskinning journey, though, discovering a hard-coded path or a tangled singleton chain three days into a project — after you've already invested time in art direction based on assumptions about how easy certain changes would be — is a far more expensive way to learn the same lesson a thirty-minute audit would have taught you upfront.&lt;/p&gt;

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

&lt;p&gt;A thirty-minute audit won't tell you everything about a project, but it will surface the structural issues that matter most for a smooth reskin: how assets are referenced, whether content is data-driven, how tightly coupled the core systems are, and whether UI and monetization are built to handle change gracefully.&lt;/p&gt;

&lt;p&gt;Developers who run this kind of check consistently — rather than relying on gut feeling after watching a trailer — end up with far fewer surprises once they're a week into a project. It's a small upfront investment that pays for itself almost every time, and over multiple projects, it becomes second nature: open a handful of scripts, ask the right questions, and you'll know within minutes whether a codebase is going to work with you or against you.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Building Draw-to-Solve Puzzle Mechanics in Unity: Line Rendering, Physics Barriers, and Level Design</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Fri, 07 Aug 2026 18:00:35 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-draw-to-solve-puzzle-mechanics-in-unity-line-rendering-physics-barriers-and-level-design-121e</link>
      <guid>https://dev.to/unitysourcecode/building-draw-to-solve-puzzle-mechanics-in-unity-line-rendering-physics-barriers-and-level-design-121e</guid>
      <description>&lt;p&gt;Draw-to-solve puzzles are one of the most reliably engaging formats in mobile gaming. The concept is simple to explain in one sentence — draw a line to protect or guide something — yet the implementation touches almost every system in Unity: input handling, 2D physics, procedural mesh generation, and level design tooling. Games in this genre (protecting a character from hazards by sketching barriers, guiding a ball through a maze you draw yourself, or fencing off danger zones) share a common technical backbone, and that backbone is worth understanding in depth if you're building anything in this space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz99x00ckjjlk2oaqgsxc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz99x00ckjjlk2oaqgsxc.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article breaks down how draw-to-solve mechanics actually work under the hood in Unity: converting a finger swipe into a physical object, managing limited "ink" as a resource, structuring levels so difficulty scales predictably, and avoiding the performance traps that show up once you have dozens of hand-drawn colliders on screen at once.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. From Touch Input to a Physical Line
&lt;/h3&gt;

&lt;p&gt;The first technical challenge is converting a raw touch gesture into something the physics engine can collide with. This happens in three stages: capturing input points, rendering the line, and generating a collider that matches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capturing input points&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't want to record every single touch event — on a modern phone that can be hundreds of points per second, most of which are redundant. Instead, sample points at a minimum distance threshold:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastPoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentPoint&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;minPointDistance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;linePoints&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;currentPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;lastPoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentPoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps your line smooth without flooding your point list with near-duplicate positions, which matters both for rendering performance and for the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering the line&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unity's &lt;code&gt;LineRenderer&lt;/code&gt; is the natural fit here. It takes your sampled points directly and handles width, corner smoothing, and material rendering without you needing to build custom meshes for the visual layer. The main tuning knobs are &lt;code&gt;numCornerVertices&lt;/code&gt; and &lt;code&gt;numCapVertices&lt;/code&gt;, which control how smooth curves and line ends look — worth bumping up slightly for thin, fast-drawn lines that would otherwise look jagged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a matching collider&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that trips people up. &lt;code&gt;LineRenderer&lt;/code&gt; is purely visual — it doesn't collide with anything. You need to generate an &lt;code&gt;EdgeCollider2D&lt;/code&gt; (or a chain of small &lt;code&gt;BoxCollider2D&lt;/code&gt; segments) that follows the same points:&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="n"&gt;EdgeCollider2D&lt;/span&gt; &lt;span class="n"&gt;edgeCollider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drawnObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EdgeCollider2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;edgeCollider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linePoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;EdgeCollider2D&lt;/code&gt; is generally the better choice over stitching together box colliders — it's lighter weight and handles arbitrary curved paths natively. The one caveat is that edge colliders are one-sided in terms of physics behavior in some configurations, so test collision response from both approach directions during development, not just the one you happen to test with first.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Treating Ink as a Resource, Not Just a Cosmetic Meter
&lt;/h3&gt;

&lt;p&gt;Most games in this genre limit how much a player can draw, and that constraint is what turns "draw a line" into an actual puzzle rather than a trivial task. The naive approach is to track ink as "number of points drawn," but that ties your resource economy directly to your sampling rate, which is a mistake — change your point-distance threshold later and you silently rebalance every level.&lt;/p&gt;

&lt;p&gt;The more robust approach is to track ink by cumulative line length:&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;segmentLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previousPoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;currentInkUsed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;segmentLength&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;currentInkUsed&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxInkAllowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;StopDrawing&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 decouples your resource budget from implementation details like sampling rate and screen resolution, which matters a lot once you're designing dozens of levels and need ink budgets to feel consistent across different device DPIs.&lt;/p&gt;

&lt;p&gt;It's also worth exposing ink remaining as a normalized value (0 to 1) rather than a raw float, since your UI, difficulty curve, and any "undo" or "erase" mechanics will all want to reference it that way.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Structuring Hazards and Win Conditions Independently of Drawing
&lt;/h3&gt;

&lt;p&gt;A recurring architectural mistake — similar to what shows up in physics-based sports and board games — is coupling win/loss detection directly to the drawing system. It's tempting to check "did the hazard's path get blocked" inside the same script that handles touch input. This works until you add a second hazard type, at which point the drawing script becomes a dumping ground for unrelated game logic.&lt;/p&gt;

&lt;p&gt;A cleaner separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drawing system&lt;/strong&gt; — only responsible for capturing input, rendering the line, generating the collider, and tracking ink. It doesn't know or care what the line is protecting against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hazard system&lt;/strong&gt; — each hazard (a bee, a falling object, a rolling ball) is its own component with its own movement and damage logic. It reacts to whatever colliders happen to be in its way, drawn or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objective system&lt;/strong&gt; — a separate controller watching win/loss state (e.g., "target survived for N seconds," "target reached the goal zone"). It listens to events from the hazard system rather than polling collision state directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation pays off the moment you want to introduce a new hazard type or a new win condition — you're adding a new component, not modifying a monolithic controller that already handles five unrelated responsibilities.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Designing a Difficulty Curve That Actually Escalates
&lt;/h3&gt;

&lt;p&gt;Puzzle games live or die on their difficulty curve, and "escalating" doesn't just mean adding more hazards. There are several independent difficulty levers, and mixing them deliberately produces a much more interesting curve than cranking all of them up together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ink scarcity&lt;/strong&gt; — reducing the ink budget relative to the space that needs covering forces more efficient, minimal solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hazard timing&lt;/strong&gt; — a hazard that appears immediately is easier to plan around than one that spawns mid-level, forcing players to react rather than pre-plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spatial complexity&lt;/strong&gt; — more open space with multiple valid solution paths early on; tighter, more constrained geometry later that narrows the solution space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time pressure&lt;/strong&gt; — a soft timer or a hazard that accelerates over time adds pressure without changing the core puzzle logic at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical technique is to build a simple internal scoring system per level — something like &lt;code&gt;(hazard count × timing complexity) + (spatial complexity / ink budget)&lt;/code&gt; — and use it to sanity-check that your level order actually escalates in the way you intend, rather than relying purely on gut feel. It's easy to accidentally place a genuinely harder level before an easier one just because it "felt" like the next step during design.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Level Data as Configuration, Not Code
&lt;/h3&gt;

&lt;p&gt;Because this genre lives or dies on having a large number of levels, hardcoding hazard positions and ink budgets in scene files scales badly. A &lt;code&gt;ScriptableObject&lt;/code&gt;-based level definition is a far better fit:&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;"LevelData"&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;float&lt;/span&gt; &lt;span class="n"&gt;maxInk&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;surviveDuration&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;HazardConfig&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;hazards&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;Vector2&lt;/span&gt; &lt;span class="n"&gt;targetStartPosition&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 has a few concrete benefits beyond just tidiness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designers (or you, wearing a designer hat) can tune values without touching scene hierarchies.&lt;/li&gt;
&lt;li&gt;Levels can be loaded dynamically by index, which makes A/B testing difficulty curves trivial.&lt;/li&gt;
&lt;li&gt;It's straightforward to build a lightweight in-editor level browser that instantiates any &lt;code&gt;LevelData&lt;/code&gt; asset into a test scene, dramatically speeding up iteration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you eventually want server-driven levels (for live-ops style content updates without an app store release), the &lt;code&gt;ScriptableObject&lt;/code&gt; structure also maps cleanly onto a JSON schema, since the fields are simple serializable types.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Performance Considerations Specific to This Genre
&lt;/h3&gt;

&lt;p&gt;Draw-to-solve games look simple, but a few performance issues show up reliably once you're testing on real mid-range Android hardware rather than an editor or flagship device:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collider count creep.&lt;/strong&gt; If a player draws several long, winding lines across a level, and each becomes its own &lt;code&gt;EdgeCollider2D&lt;/code&gt;, you can end up with more colliders active simultaneously than you'd expect. Since these are typically static once drawn, marking them appropriately and avoiding unnecessary &lt;code&gt;Rigidbody2D&lt;/code&gt; components on drawn objects (a kinematic or static collider is usually sufficient) keeps physics step costs down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Line renderer vertex count.&lt;/strong&gt; Long, detailed lines with high corner vertex counts add up in render cost, especially with multiple lines active at once. A simple mitigation is to apply light point simplification (removing points that don't meaningfully change the line's direction) before finalizing the drawn line, rather than keeping every raw sampled point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Garbage collection from per-frame allocations.&lt;/strong&gt; It's common to see &lt;code&gt;List&amp;lt;Vector2&amp;gt;&lt;/code&gt; allocations happening every frame during active drawing if you're not careful about reusing collections. Pre-allocating your point list with a reasonable capacity and clearing rather than reallocating between draws avoids unnecessary GC pressure, which matters more on this genre's target audience of budget and mid-range devices than it might for a higher-spec game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch input smoothing cost.&lt;/strong&gt; If you're applying smoothing algorithms (like Catmull-Rom interpolation) to raw touch points for a nicer-looking line, do it once when finalizing the line rather than recalculating the entire smoothed path on every new point added — that's an easy accidental O(n²) cost as lines get longer.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Testing Solvability
&lt;/h3&gt;

&lt;p&gt;One issue specific to physics-based puzzle design that's easy to overlook: because your solution space is player-drawn geometry rather than a fixed set of moves, it's possible to accidentally design a level that's unsolvable with the ink budget you've assigned, especially after later tweaking hazard speed or spawn timing. A lightweight internal tool — even something as simple as an automated "bot" that tries a handful of heuristic drawing strategies and reports whether any of them succeed — can catch these regressions before they ship, rather than relying entirely on manual playtesting to catch a broken level.&lt;/p&gt;




&lt;h3&gt;
  
  
  8. Handling Undo, Erase, and Multi-Touch Edge Cases
&lt;/h3&gt;

&lt;p&gt;Once a drawing mechanic ships, players immediately start testing its edges — drawing with two fingers at once, starting a new line before finishing the last one, or expecting to erase a mistake without losing their entire ink budget. A few patterns handle these cleanly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single active stroke enforcement.&lt;/strong&gt; Most implementations restrict input to one active touch for drawing, ignoring secondary touch IDs rather than trying to support simultaneous multi-line drawing. This avoids a whole class of state-management bugs where two &lt;code&gt;EdgeCollider2D&lt;/code&gt; objects are being built concurrently from interleaved touch events.&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchCount&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;activeTouchId&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="n"&gt;Touch&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTouch&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;activeTouchId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerId&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;Undo as object removal, not ink refund by formula.&lt;/strong&gt; Rather than recalculating ink mathematically when a line is erased, it's simpler and less bug-prone to store the ink cost alongside each drawn line object when it's created, then simply refund that stored value and destroy the object on undo. This avoids drift between your ink tracking and what's actually on screen, which is a common source of "ink went negative" bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debounced erase gestures.&lt;/strong&gt; If erasing is triggered by a gesture like a tap-and-hold on an existing line, add a short delay before triggering removal so that a quick accidental tap during normal drawing doesn't delete a line the player wanted to keep.&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Cross-Device Input Consistency
&lt;/h3&gt;

&lt;p&gt;Draw-to-solve games are unusually sensitive to input differences between devices, more so than many other mobile genres, because the entire mechanic is built on precise gesture capture. A few things worth testing explicitly rather than assuming they'll "just work":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DPI and screen size variance.&lt;/strong&gt; A &lt;code&gt;minPointDistance&lt;/code&gt; threshold tuned in editor on a desktop-simulated screen often feels too sparse or too dense on an actual device. Testing on both a small-screen budget phone and a larger flagship tends to surface this quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch pressure and palm rejection.&lt;/strong&gt; Some devices report phantom touch points from a resting palm during drawing, particularly on larger tablets. Filtering touches by a minimum distance from the primary touch, or simply capping to a single tracked touch ID, avoids most of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor mouse input vs. device touch input.&lt;/strong&gt; It's easy to build and test an entire drawing system using &lt;code&gt;Input.mousePosition&lt;/code&gt; in editor and only discover touch-specific quirks (multi-touch noise, different pressure curves) once you build to an actual device. Testing on hardware early, even with placeholder art, avoids late-stage surprises.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Closing Thoughts
&lt;/h3&gt;

&lt;p&gt;Draw-to-solve puzzle mechanics look deceptively simple from a player's perspective, but a solid implementation touches input sampling, physics collider generation, resource management, and level configuration — each of which benefits from being kept as its own clearly bounded system rather than tangled together. Getting the ink-as-resource model right, decoupling hazards from the drawing system itself, and treating levels as data rather than hardcoded scenes are the three decisions that will save the most rework as your level count grows from a handful of prototypes to a full game.&lt;/p&gt;

&lt;p&gt;These same principles — separating input capture from physics response, and treating game content as configurable data rather than code — apply broadly across mobile genres, including runner and arcade games where core loop tuning and difficulty pacing follow a similar pattern. This breakdown of building an endless roller game in Unity covers that side of the equation, including core loop structure and monetization pacing: &lt;a href="https://dev.to/unitysourcecode/building-an-endless-roller-game-in-unity-core-loop-difficulty-curve-and-monetization-fab"&gt;Building an Endless Roller Game in Unity&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a working reference implementation of the draw-to-protect mechanic covered in this article, a complete Unity puzzle rescue project is available here: &lt;a href="https://unitysourcecode.net/product/save-the-dogs-unity-game" rel="noopener noreferrer"&gt;Save The Dogs Unity Game Source Code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>puzzle</category>
      <category>gamemechanics</category>
      <category>unity3d</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building an Endless Roller Game in Unity: Core Loop, Difficulty Curve, and Monetization</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Thu, 06 Aug 2026 17:56:40 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-an-endless-roller-game-in-unity-core-loop-difficulty-curve-and-monetization-fab</link>
      <guid>https://dev.to/unitysourcecode/building-an-endless-roller-game-in-unity-core-loop-difficulty-curve-and-monetization-fab</guid>
      <description>&lt;p&gt;Hyper-casual games live or die in the first 15 seconds. If a player doesn't understand the controls immediately and feel a pull to try "just one more run," they're gone. Few genres make this rule as obvious as the endless roller — the "control a ball as it falls through a shifting maze of platforms" format popularized by games like the original Rapid Roll on Java phones, and still alive and well on mobile app stores today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7j3vl0sqdzbl9zgh2iu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7j3vl0sqdzbl9zgh2iu.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've spent the last few weeks digging into how these games are actually structured under the hood — not just playing them, but pulling apart the core loop, the difficulty curve, and the monetization layer that makes them commercially viable. This post is a breakdown of what I learned, aimed at anyone building (or reskinning) a hyper-casual roller game in Unity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Roller Sub-Genre Still Works
&lt;/h2&gt;

&lt;p&gt;The endless roller format has three properties that make it a near-ideal hyper-casual template:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One input axis.&lt;/strong&gt; You're either moving left/right (swipe or tilt) or you're not. There's no button mapping to learn, no tutorial screen required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single failure state.&lt;/strong&gt; You fall off, you die, you restart. No health bars, no combo systems, no ambiguity about what just happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A score that only goes up during a run.&lt;/strong&gt; Distance fallen or platforms cleared gives players an immediate, legible measure of "did I do better than last time."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These three properties combine into what game designers call a "tight core loop" — the shortest possible path from &lt;em&gt;start&lt;/em&gt; to &lt;em&gt;fail&lt;/em&gt; to &lt;em&gt;retry&lt;/em&gt;, with nothing extraneous in between. When you're evaluating (or building) a roller game, almost every design decision should be judged against whether it protects or dilutes that loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Loop, Broken Into Systems
&lt;/h2&gt;

&lt;p&gt;If you strip a roller game down to its systems, you get roughly this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input system&lt;/strong&gt; — reads swipe or tilt input and converts it into lateral ball velocity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descent system&lt;/strong&gt; — the ball (or camera) moves downward at a speed that increases over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform generator&lt;/strong&gt; — spawns platforms ahead of the ball and despawns them behind it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision/fail system&lt;/strong&gt; — detects when the ball misses a platform or hits an obstacle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score system&lt;/strong&gt; — tracks distance or platforms cleared, persists a high score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization system&lt;/strong&gt; — shows interstitials on death, rewarded ads for revives or bonuses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go through the ones that actually determine whether the game feels good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input: Swipe vs. Tilt
&lt;/h3&gt;

&lt;p&gt;Most implementations offer swipe-to-move as the primary control, with tilt as an alternative for players who prefer motion controls. A minimal swipe handler in Unity looks something 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SwipeInput&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;swipeThreshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50f&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;Vector2&lt;/span&gt; &lt;span class="n"&gt;startTouch&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;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchCount&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Touch&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTouch&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TouchPhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Began&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;startTouch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;touch&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="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&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;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TouchPhase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Moved&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;deltaX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;touch&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;x&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;startTouch&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;if&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;Abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltaX&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;swipeThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;MoveBall&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;Sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltaX&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="n"&gt;startTouch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;touch&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="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;MoveBall&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;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// apply lateral velocity to the ball's Rigidbody&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 important detail isn't the code itself — it's the threshold tuning. Too sensitive and players overshoot platforms on accident; too sluggish and the game feels unresponsive. This value should almost always be exposed as a serialized field so it can be tuned by feel during playtesting, not hardcoded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Procedural Platform Generation
&lt;/h3&gt;

&lt;p&gt;The platform layout is where most of the "feel" of a roller game actually comes from. A naive approach spawns platforms at fixed intervals with a random x-offset. That's fine as a first pass, but it produces layouts that feel random rather than intentional — sometimes trivially easy, sometimes unfairly narrow.&lt;/p&gt;

&lt;p&gt;A better approach uses a small set of hand-authored "chunk" prefabs (straight gaps, staggered platforms, narrow bridges, moving platforms) that get selected and stitched together procedurally, weighted by the current difficulty tier. This gives you the variety of procedural generation with the readability of hand-designed level segments — you avoid the "impossible gap" problem that pure randomness tends to produce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difficulty Curve
&lt;/h3&gt;

&lt;p&gt;The descent speed increase should not be linear. A curve that increases quickly at first and then flattens tends to feel better than a constant acceleration, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New players get a fair first 10–15 seconds to learn the controls&lt;/li&gt;
&lt;li&gt;The mid-game ramp creates the tension spike that makes near-misses feel earned&lt;/li&gt;
&lt;li&gt;The late-game plateau prevents the game from becoming physically unplayable (which just frustrates rather than challenges)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple approach is an exponential decay toward a max speed:&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;currentSpeed&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;Lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseSpeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxSpeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1f&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;Exp&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="n"&gt;difficultyRamp&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;elapsedTime&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tune &lt;code&gt;difficultyRamp&lt;/code&gt; by feel, not by formula — this is one of those numbers you adjust after watching five real people play, not something you can derive theoretically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization Without Breaking the Loop
&lt;/h2&gt;

&lt;p&gt;Hyper-casual games almost universally monetize through ads rather than IAP-heavy economies, and AdMob is the default choice for most Unity teams shipping to both Android and iOS. The two placements that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interstitial on death&lt;/strong&gt; — shown after the score screen, not instead of it. Never interrupt the moment of failure itself; let the player see their score first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewarded video for a revive&lt;/strong&gt; — offering "watch an ad to continue from where you fell" is one of the highest-performing rewarded placements in the genre, because it's opt-in and clearly valuable to the player.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see most often in reskinned templates is over-frequent interstitials — showing one after every single run regardless of run length. A better heuristic is to gate interstitials behind either a minimum elapsed time or a run counter (e.g., every 2nd or 3rd death), which noticeably improves retention without sacrificing much revenue.&lt;/p&gt;

&lt;p&gt;If you're planning to layer in-app purchases on top of ad monetization — cosmetic ball skins, an ad-removal purchase, or a starter bundle — it's worth understanding Unity's IAP receipt validation flow before you wire anything up, since getting it wrong is a common source of App Store rejections. I found this guide useful for getting the setup right the first time: &lt;a href="https://github.com/unitysourcecode2026/unity-in-app-purchase-guide" rel="noopener noreferrer"&gt;Unity In-App Purchase Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Roller Games Fit Among Other Hyper-Casual Loops
&lt;/h2&gt;

&lt;p&gt;It's worth zooming out for a second. Endless rollers are one point on a spectrum of hyper-casual and casual core loops, and comparing them against other genres clarifies why certain design choices matter. I recently wrote about the opposite end of that spectrum — the core loop of an idle RPG clicker, where the challenge isn't split-second reflexes but pacing long-term progression, offline earnings, and boss-fight loot cadence over days rather than seconds. If you're deciding what genre to build next, or just want to see how differently "core loop" design plays out when the session length changes from 30 seconds to 30 minutes, it's a useful comparison: &lt;a href="https://dev.to/unitysourcecode/designing-the-core-loop-for-an-idle-rpg-clicker-in-unity-boss-fights-loot-and-offline-progression-32o7"&gt;Designing the Core Loop for an Idle RPG Clicker in Unity&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations for Mobile
&lt;/h2&gt;

&lt;p&gt;A few things matter disproportionately for a fast-paced mobile roller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object pooling for platforms.&lt;/strong&gt; Instantiating and destroying platform prefabs every few seconds will cause GC spikes that are very noticeable in a game running at high speed. Pool and reuse instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple collision shapes.&lt;/strong&gt; Box or sphere colliders only — mesh colliders on fast-moving objects are a common source of tunneling bugs at high descent speeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed timestep tuning.&lt;/strong&gt; If the ball uses Rigidbody physics, make sure &lt;code&gt;Fixed Timestep&lt;/code&gt; in Project Settings is tight enough to avoid physics inconsistency at high speeds, but not so tight that it tanks frame rate on low-end Android devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture atlasing.&lt;/strong&gt; Roller games typically use flat, minimal art — atlas everything into as few draw calls as possible, since this is one of the few genres where the frame rate ceiling actually affects playability, not just visual polish.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you're evaluating whether to build a roller game from scratch or start from an existing template, the honest answer depends on your timeline. Building the systems above from zero — input handling, chunk-based procedural generation, a tuned difficulty curve, AdMob integration, and mobile-optimized rendering — is a solid one-to-two week project for a solo developer who's done it before, longer if it's your first time wiring up ad mediation.&lt;/p&gt;

&lt;p&gt;That's the gap that pre-built templates are meant to fill. I looked at &lt;a href="https://unitysourcecode.net/product/rapid-roll-unity-game-template" rel="noopener noreferrer"&gt;Rapid Roll&lt;/a&gt;, a Unity source template built around exactly this loop — swipe/tilt-controlled ball descent, chunk-based platform generation, a progressive difficulty ramp, and AdMob interstitial/rewarded placements already wired in. For a reskin project or a fast prototype to validate a theme before investing in original assets, starting from a working, tuned implementation like this is usually a faster path to a Play Store build than reimplementing all of the above from a blank Unity project — especially the ad mediation and difficulty tuning, which tend to eat the most iteration time.&lt;/p&gt;

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

&lt;p&gt;The endless roller genre looks simple on the surface, and that's exactly the point — every system underneath is in service of keeping the core loop as short and legible as possible. If you're building one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep input responsive and tune the swipe/tilt threshold by feel&lt;/li&gt;
&lt;li&gt;Use chunk-based generation over pure randomness to avoid unfair layouts&lt;/li&gt;
&lt;li&gt;Curve your difficulty ramp instead of scaling linearly&lt;/li&gt;
&lt;li&gt;Gate interstitials by run count or time, not every single death&lt;/li&gt;
&lt;li&gt;Pool your platforms and keep collision shapes simple for consistent frame rate on low-end devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you build the loop from scratch or start from a template, the systems above are what separate a roller game that feels good from one that just technically works. Happy building.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdubl6udeb80vfvdnnf7d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdubl6udeb80vfvdnnf7d.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>mobile</category>
      <category>csharp</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Designing the Core Loop for an Idle RPG Clicker in Unity: Boss Fights, Loot, and Offline Progression</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:43:53 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/designing-the-core-loop-for-an-idle-rpg-clicker-in-unity-boss-fights-loot-and-offline-progression-32o7</link>
      <guid>https://dev.to/unitysourcecode/designing-the-core-loop-for-an-idle-rpg-clicker-in-unity-boss-fights-loot-and-offline-progression-32o7</guid>
      <description>&lt;p&gt;Idle RPG clickers look deceptively simple from the outside. Tap the screen, watch a number go up, defeat a boss, repeat. But if you've ever tried to actually build one, you know the real complexity isn't the tapping — it's the systems underneath it. Offline progression math, loot balancing, boss difficulty curves, and save-state integrity all have to work together, or the whole game falls apart the moment a player closes the app and comes back six hours later.&lt;/p&gt;

&lt;p&gt;In this article I want to break down the core architecture behind a typical idle RPG clicker — boss battles, loot systems, idle/offline earnings, and progression scaling — the way you'd actually structure it in Unity with C#. I'll use patterns pulled from a shipped mobile template as a reference point throughout, because it's easier to reason about real systems than hypothetical ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Idle + Clicker Hybrids Are Harder Than They Look
&lt;/h2&gt;

&lt;p&gt;A pure clicker game only has to solve one problem: make tapping feel good. A pure idle game only has to solve one problem: make numbers grow in a satisfying curve while the player is away. An idle RPG clicker has to solve &lt;em&gt;both&lt;/em&gt; problems simultaneously, and then reconcile them so neither system undermines the other.&lt;/p&gt;

&lt;p&gt;If your idle income scales too aggressively, active tapping becomes pointless and players stop engaging. If tapping scales too aggressively, idle income feels like a rounding error and your retention hook (the thing that brings players back after being offline) stops working. Getting this balance right is 80% of what makes these games either addictive or forgettable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System #1: The Tap-Damage Loop
&lt;/h2&gt;

&lt;p&gt;At the most basic level, you need a damage-per-tap system that feeds into a boss health pool. This sounds trivial, but the way you structure it determines how easy the rest of the game is to build on top of.&lt;/p&gt;

&lt;p&gt;Here's a minimal but extensible version:&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;BossCombatController&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;BossData&lt;/span&gt; &lt;span class="n"&gt;currentBoss&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;currentBossHealth&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="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnBossHealthChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// current, max&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;BossData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OnBossDefeated&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;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;currentBossHealth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentBoss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxHealth&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;ApplyTapDamage&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;baseDamage&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;critMultiplier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1f&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;finalDamage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseDamage&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;critMultiplier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;currentBossHealth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentBossHealth&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;finalDamage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;OnBossHealthChanged&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;currentBossHealth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentBoss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxHealth&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;currentBossHealth&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&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="nf"&gt;HandleBossDefeat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;HandleBossDefeat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;OnBossDefeated&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;currentBoss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Loot roll, next boss load, and reward calculation happen&lt;/span&gt;
        &lt;span class="c1"&gt;// in listeners subscribed to OnBossDefeated, not here.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision here is keeping &lt;code&gt;BossCombatController&lt;/code&gt; dumb on purpose. It doesn't know about loot tables, currency, or UI. It just tracks health and fires events. This separation matters more in idle games than most genres, because you'll eventually need the exact same "boss defeated" event to trigger loot drops, idle-rate recalculation, achievement checks, and save-state writes — all independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System #2: Loot and Reward Loops
&lt;/h2&gt;

&lt;p&gt;The loot loop is what keeps the tap-damage loop from feeling repetitive. Every boss kill needs to produce &lt;em&gt;some&lt;/em&gt; combination of currency, item drops, and upgrade materials, ideally with enough randomness that players don't feel like they're grinding a fixed script.&lt;/p&gt;

&lt;p&gt;A simple weighted loot table 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;LootEntry&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;float&lt;/span&gt; &lt;span class="n"&gt;dropWeight&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;minQuantity&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;maxQuantity&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;LootTable&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LootEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entries&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="n"&gt;itemId&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;quantity&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RollLoot&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;rolls&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&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;,&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;float&lt;/span&gt; &lt;span class="n"&gt;totalWeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&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;=&amp;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;dropWeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rolls&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;float&lt;/span&gt; &lt;span class="n"&gt;roll&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&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;totalWeight&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;cumulative&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="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;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;cumulative&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropWeight&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;roll&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cumulative&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;qty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minQuantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxQuantity&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;results&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                    &lt;span class="k"&gt;break&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;return&lt;/span&gt; &lt;span class="n"&gt;results&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;Two things matter here that are easy to overlook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Weighted, not fixed-probability, rolls.&lt;/strong&gt; Fixed drop chances (e.g., "10% chance for X") get harder to balance as your loot table grows, because every new item changes the effective probability of every other item. A weighted system self-normalizes as you add or remove entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loot rolling is decoupled from combat.&lt;/strong&gt; &lt;code&gt;LootTable&lt;/code&gt; has no idea a boss even exists. It just responds to "roll N times" and returns results. That decoupling is what lets you reuse the exact same loot system for chests, daily rewards, or event drops later without duplicating logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I found this same separation pattern in &lt;a href="https://unitysourcecode.net/product/lootscape-boss-mania" rel="noopener noreferrer"&gt;Lootscape Boss Mania&lt;/a&gt;, a Unity idle RPG clicker template that combines tap-based boss combat with an offline idle-earning system. Looking at how a shipped project structures the boundary between combat, loot, and progression is a good sanity check against over-engineering — or under-engineering — your own version of the same systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core System #3: Idle and Offline Progression
&lt;/h2&gt;

&lt;p&gt;This is the system that actually differentiates an "idle RPG" from a plain clicker, and it's also the one most beginners get wrong. The naive approach — just multiply idle-rate by elapsed real-world time — works, but it opens the door to two problems: clock manipulation exploits, and reward curves that don't feel intentional.&lt;/p&gt;

&lt;p&gt;Here's a more defensible pattern:&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;IdleProgressionManager&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;const&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;MAX_OFFLINE_HOURS&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;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;goldPerSecond&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;SaveExitTimestamp&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="s"&gt;"last_exit_utc"&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;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o"&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;Save&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;float&lt;/span&gt; &lt;span class="nf"&gt;CalculateOfflineEarnings&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;PlayerPrefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"last_exit_utc"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&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;DateTime&lt;/span&gt; &lt;span class="n"&gt;lastExit&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="nf"&gt;Parse&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="s"&gt;"last_exit_utc"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;null&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;Globalization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DateTimeStyles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoundtripKind&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;elapsed&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;UtcNow&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastExit&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;elapsedSeconds&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="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalSeconds&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;MAX_OFFLINE_HOURS&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;3600f&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;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;goldPerSecond&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always store timestamps in UTC.&lt;/strong&gt; If you store local time, you'll get silently wrong results the moment a player crosses a daylight-saving boundary or travels between timezones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap offline earnings.&lt;/strong&gt; An uncapped idle system either becomes exploitable (leave the game running for a week, come back absurdly overpowered) or forces you into diminishing-returns math that's harder to communicate to players. A hard cap with a clear UI message ("earning gold for up to 8 hours while away") is usually the simplest honest design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recalculate &lt;code&gt;goldPerSecond&lt;/code&gt; from upgrades, not from a cached value.&lt;/strong&gt; If you cache the idle rate at the moment the player exits, you lose the ability to retroactively apply upgrades or events that should affect offline earnings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Core System #4: Progression and Difficulty Scaling
&lt;/h2&gt;

&lt;p&gt;The last piece is tying boss difficulty, loot value, and idle rate together into a single progression curve. This is where a lot of idle games either flatten out (boring) or spike unpredictably (frustrating). A common, safe approach is exponential scaling with a tunable growth rate:&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;ProgressionCurve&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="kt"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;BossHealthForStage&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;stage&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;baseHealth&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;growthRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.15f&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;baseHealth&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;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;growthRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&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="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;LootValueForStage&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;stage&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;baseValue&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;growthRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.08f&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;baseValue&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;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;growthRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that boss health and loot value scale at &lt;em&gt;different&lt;/em&gt; rates (1.15 vs 1.08 in this example). That gap is intentional — it's what creates the sense that later stages require real strategic investment (upgrades, better gear) rather than just more tapping. If both curves scale at the same rate, the game plateaus into a flat grind with no perceived progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Systems Together
&lt;/h2&gt;

&lt;p&gt;None of these four systems — combat, loot, idle progression, and difficulty scaling — are complicated in isolation. The actual engineering challenge is keeping them loosely coupled enough that you can tune, replace, or extend any one of them without breaking the others. In practice that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Combat fires events; it doesn't call loot or save logic directly&lt;/li&gt;
&lt;li&gt;Loot tables are content-driven (ScriptableObjects or JSON), not hardcoded&lt;/li&gt;
&lt;li&gt;Idle earnings are recalculated from current stats, never cached&lt;/li&gt;
&lt;li&gt;Progression curves are pure functions you can unit test independently of Unity's scene graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This event-driven separation is a pattern that shows up constantly outside of idle clickers too. I covered a very similar architecture recently while building out a &lt;a href="https://dev.to/unitysourcecode/building-a-crowd-runner-combat-system-in-unity-army-growth-formations-and-battle-phases-nhh"&gt;crowd runner combat system with army growth and battle phases&lt;/a&gt; — different genre entirely, but the same underlying principle: keep your state-changing systems dumb and event-driven, and let independent listeners handle the consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Content-Driven Design
&lt;/h2&gt;

&lt;p&gt;One thing that separates a shippable idle RPG from a tech demo is how much of the game lives in data rather than code. Boss stats, loot tables, upgrade costs, and stage requirements should all be defined in ScriptableObjects or external config files, not hardcoded into MonoBehaviours. This matters for two practical reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Balancing becomes a spreadsheet problem, not a recompile problem.&lt;/strong&gt; You want a designer (even if that's just you, wearing a different hat) to be able to tune a boss's health or a loot drop rate without touching C#.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reskinning and content expansion become trivial.&lt;/strong&gt; If your combat, loot, and progression logic never reference specific boss names or item IDs directly, adding a new boss or a new loot tier is a data change, not a code change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This same content-driven philosophy extends naturally into adjacent genres. Idle mechanics aren't exclusive to RPG clickers — resource-accumulation and management games lean on nearly identical systems: production rates instead of gold-per-second, harvest cycles instead of boss timers, and upgrade trees that scale the same way. If you're exploring how these systems adapt to a slower-paced, management-style game, it's worth looking at how a title like &lt;a href="https://unitysourcecode.net/product/farm-village-unity-source-code" rel="noopener noreferrer"&gt;Farm Village&lt;/a&gt; structures its resource and progression loop — the underlying math (rate-based accumulation, offline calculation, tiered upgrades) is strikingly similar to what an idle RPG clicker needs, just wrapped in a different theme and pacing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls to Avoid
&lt;/h2&gt;

&lt;p&gt;A few mistakes I see repeatedly in idle/clicker prototypes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing floats for currency past a certain scale.&lt;/strong&gt; Idle games generate very large numbers quickly. Standard &lt;code&gt;float&lt;/code&gt; precision starts breaking down well before you hit typical late-game currency values. Switch to &lt;code&gt;double&lt;/code&gt;, or better, a custom big-number type (many idle games implement a simple mantissa/exponent pair) once your numbers regularly exceed a few million.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recalculating idle rate inside &lt;code&gt;Update()&lt;/code&gt;.&lt;/strong&gt; Idle-rate math should only run when something actually changes it — an upgrade purchase, a stage completion, a boss defeat. Recomputing it every frame is wasted work and makes the system harder to reason about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tight coupling between UI and game state.&lt;/strong&gt; If your boss health bar directly polls &lt;code&gt;BossCombatController.currentBossHealth&lt;/code&gt; every frame instead of subscribing to &lt;code&gt;OnBossHealthChanged&lt;/code&gt;, you'll eventually hit desync bugs the moment you add pause states, background processing, or save/load transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No offline cap communication.&lt;/strong&gt; If you cap offline earnings at 8 hours but never tell the player, you'll get support messages and negative reviews from people who feel cheated. Surface the cap in the UI explicitly.&lt;/p&gt;

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

&lt;p&gt;Idle RPG clickers are a genuinely good genre to study if you're trying to get better at systems-level game architecture, because the constraints are unusually tight: everything has to work correctly even when the player isn't actively playing, every number has to scale predictably over potentially hundreds of stages, and every system has to stay loosely coupled enough to support ongoing content additions without a rewrite.&lt;/p&gt;

&lt;p&gt;If you're building your own version of this loop, start with the four systems above — tap combat, loot rolls, offline progression, and difficulty scaling — get each one working in isolation with unit tests where possible, and only then wire them together through events. It's a slower start than jamming everything into one big &lt;code&gt;GameManager&lt;/code&gt; class, but it pays off the moment you need to add your fifth boss, your tenth loot tier, or your first live event.&lt;/p&gt;

&lt;p&gt;What patterns have you used for offline progression or loot balancing in your own projects? I'd be curious to hear how other people have handled the big-number precision problem in particular — that one tends to bite everyone eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: A Simple Big-Number Type
&lt;/h2&gt;

&lt;p&gt;Since it came up above, here's a minimal mantissa/exponent implementation you can drop into a project once your currency values start exceeding what &lt;code&gt;double&lt;/code&gt; can represent cleanly (roughly 1e15 before precision loss becomes visible, and 1e308 before you hit a hard ceiling):&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;struct&lt;/span&gt; &lt;span class="nc"&gt;BigNumber&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;double&lt;/span&gt; &lt;span class="n"&gt;mantissa&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;exponent&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;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;mantissa&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;exponent&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mantissa&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mantissa&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="n"&gt;exponent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Normalize&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;Normalize&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;mantissa&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="n"&gt;exponent&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;return&lt;/span&gt;&lt;span class="p"&gt;;&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;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mantissa&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;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;mantissa&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="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;++;&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;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;mantissa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mantissa&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="n"&gt;mantissa&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="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;--;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;BigNumber&lt;/span&gt; &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="p"&gt;+(&lt;/span&gt;&lt;span class="n"&gt;BigNumber&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;BigNumber&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;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expDiff&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;exponent&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="n"&gt;exponent&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;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expDiff&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;15&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;expDiff&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;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="c1"&gt;// difference too large to matter&lt;/span&gt;

        &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;aligned&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expDiff&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mantissa&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="n"&gt;mantissa&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&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="n"&gt;expDiff&lt;/span&gt;&lt;span class="p"&gt;)&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;mantissa&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&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="n"&gt;expDiff&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;mantissa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aligned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Math&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exponent&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="n"&gt;exponent&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;override&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ToString&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;exponent&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mantissa&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&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="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&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;mantissa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;e&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exponent&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="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 isn't a full replacement for something like a proper arbitrary-precision library, but for the vast majority of idle games it's more than enough headroom, and it keeps your UI formatting (&lt;code&gt;1.25e47&lt;/code&gt; instead of an unreadable string of digits) clean without extra dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Loop Before You Build Content
&lt;/h2&gt;

&lt;p&gt;One more practical tip: before you build out ten bosses and thirty loot items, get the four core systems running with placeholder data — a single boss, three loot entries, one upgrade — and play through several "sessions" manually, closing and reopening the game to simulate offline time. It's much cheaper to catch a broken progression curve or a save-state bug when you have one boss to debug than when you have thirty. Content is easy to add once the systems underneath it are solid; it's expensive to redo once you've built fifty stages on top of a shaky foundation.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjofx386z8dunxl5aibz7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjofx386z8dunxl5aibz7.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>gamechallenge</category>
      <category>developers</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Crowd Runner Combat System in Unity: Army Growth, Formations, and Battle Phases</title>
      <dc:creator>unity source code</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:25:50 +0000</pubDate>
      <link>https://dev.to/unitysourcecode/building-a-crowd-runner-combat-system-in-unity-army-growth-formations-and-battle-phases-nhh</link>
      <guid>https://dev.to/unitysourcecode/building-a-crowd-runner-combat-system-in-unity-army-growth-formations-and-battle-phases-nhh</guid>
      <description>&lt;p&gt;Crowd runner games — the genre where you sprint down a lane collecting followers, then throw that crowd into a battle sequence — have quietly become one of the most consistent hits in mobile gaming. Think &lt;em&gt;Last War: Survival&lt;/em&gt;, &lt;em&gt;Zombie Run&lt;/em&gt;, &lt;em&gt;Count Masters&lt;/em&gt;. The appeal is simple to describe and surprisingly tricky to implement well: a runner section that feeds directly into a combat section, where every decision in phase one visibly changes your odds in phase two.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;





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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</description>
      <category>unity3d</category>
      <category>reskinning</category>
      <category>beginners</category>
      <category>developer</category>
    </item>
  </channel>
</rss>
