<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Talha Cagatay ISIK - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Talha Cagatay ISIK - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 16:26:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/talhaisik/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Reusable Modular Unity Packages to Speed Up Development ]]>
                </title>
                <description>
                    <![CDATA[ How many times have you rewritten the same systems across different Unity projects? Or copied entire folders from an old project, only to spend hours fixing references, renaming namespaces, and adapti ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-reusable-modular-unity-packages-to-speed-up-development/</link>
                <guid isPermaLink="false">6998ee96a20b74e093d7e671</guid>
                
                    <category>
                        <![CDATA[ unity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C# ]]>
                    </category>
                
                    <category>
                        <![CDATA[ modularity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Game Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Talha Cagatay ISIK ]]>
                </dc:creator>
                <pubDate>Fri, 20 Feb 2026 23:30:30 +0000</pubDate>
                <media:content url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7a3eeaef-4bab-403c-b9ef-3ebc785efb2f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>How many times have you rewritten the same systems across different Unity projects? Or copied entire folders from an old project, only to spend hours fixing references, renaming namespaces, and adapting the code to fit a slightly different architecture?</p>
<p>That repetition doesn’t just waste time – it slows down your development and creates maintenance headaches across projects.</p>
<p>This guide walks you through a set of reusable, modular Unity packages you can drop into any project to speed up development. You’ll build them once as packages and install them via Git wherever you need them, instead of reimplementing the same systems every time.</p>
<p>The article covers four core packages:</p>
<ol>
<li><p><strong>com.core.initializer</strong> – Finds and initializes your game controllers at runtime (MVC-style) so startup and dependencies centralized.</p>
</li>
<li><p><strong>com.core.data</strong> – Handles local (and optionally cloud) save data using MemoryPack binary serialization and a provider abstraction so you can switch or extend storage backends.</p>
</li>
<li><p><strong>com.core.ui</strong> – Manages popups and full-screen UIs in a consistent way so you can show dialogs, panels, and screens without duplicating logic.</p>
</li>
<li><p><strong>com.core.dotween</strong> – Wraps the DoTween tween engine as a Unity package so <strong>com.core.ui</strong> (and other packages) can reference it for animations.</p>
</li>
</ol>
<p>In this tutorial, we’ll build all four packages step by step. The Initializer, Data, UI, and DoTween packages are all documented so you can easily follow along. All packages are also upload to github which you can find the links at the end.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-set-up-your-development-project">Set Up Your Development Project</a></p>
</li>
<li><p><a href="#heading-package-1-the-initializer">Package 1: The Initializer Package</a></p>
</li>
<li><p><a href="#heading-package-2-the-data-package">Package 2: The Data Package</a></p>
</li>
<li><p><a href="#heading-package-3-comcoredotween">Package 3: The DoTween Package</a></p>
</li>
<li><p><a href="#heading-package-4-the-ui-package">Package 4: The UI Package</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-learn"><strong>What You Will Learn</strong></h2>
<ul>
<li><p>How to create a Unity package with the Package Manager</p>
</li>
<li><p>How to set up the package structure</p>
</li>
<li><p>How to built a centralized initialization flow for your packages</p>
</li>
<li><p>How to use UniTask for async initialization on Unity's main thread</p>
</li>
<li><p>How the Data package uses <code>IDataProvider</code> and <strong>MemoryPack</strong> for local save/load</p>
</li>
<li><p>How the UI package uses popups (stack) and screens (single active) with DoTween animations</p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>Unity installed (any Long Term Support version compatible with the packages. The referenced packages target Unity 6000.3)</p>
</li>
<li><p>Git installed</p>
</li>
<li><p>Jetbrains Rider or Visual Studio Community installed</p>
</li>
<li><p>Know how to use Unity game engine</p>
</li>
<li><p>Know C# and async/await structures</p>
</li>
</ul>
<p>You will use the Unity Package Manager to create packages, then upload and install them via Git in other projects.</p>
<h2 id="heading-set-up-your-development-project"><strong>Set Up Your Development Project</strong></h2>
<p>Create a new Unity project if you don’t already have one. We’ll use it as a playground to build and test your packages. This project (and not the packages themselves) is your development environment.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770938731026/c2afbae6-69a7-4cd2-a2c3-2d43b0c68fe6.png" alt="Creating a new unity project via Unity Hub" width="600" height="400" loading="lazy">

<h2 id="heading-package-1-the-initializer"><strong>Package 1: The Initializer</strong></h2>
<p>The first package is <strong>com.core.initializer</strong>. It finds all your controllers, runs their async initialization before the first scene loads, and lets the rest of the game access them via a single handler.</p>
<h3 id="heading-what-comcoreinitializer-does">What com.core.initializer Does</h3>
<ul>
<li><p><strong>Early initialization</strong>: Runs at <code>RuntimeInitializeLoadType.BeforeSceneLoad</code>, so all controllers exist and are initialized before the first scene loads.</p>
</li>
<li><p><strong>Central access</strong>: Stores each controller by its type and exposes them through a type-safe getter.</p>
</li>
<li><p><strong>Completion signaling</strong>: Exposes a static event (<code>ControllersInitialized</code>) and a <code>UniTaskCompletionSource</code> (<code>InitializationCompleted</code>) so you can run code only after all controllers have finished initializing.</p>
</li>
</ul>
<h3 id="heading-create-the-initializer-package">Create the Initializer Package</h3>
<p>To start, open Window → Package Manager (under Package Management). Then click the + button and choose Create package.</p>
<p>Name the package <code>com.core.initializer</code> (or your preferred name).</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770939190680/3d9d3c0e-94f7-40bf-b5b7-d3d05ff90301.png" alt="Opening Unity Package Manager" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771030017622/bc551fb9-69d5-4fa6-8309-293fd34d6cc5.png" alt="Creating a new package" width="600" height="400" loading="lazy">

<p>Unity creates the essential package files for you.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770939568125/86637464-89c7-4dc6-861b-cb071cc2d942.png" alt="Example package folders and files" width="600" height="400" loading="lazy">

<p>You can edit <code>package.json</code> and the assembly definition (asmdef) names to match your naming. The full set of changes is not listed here. The final package is available on GitHub (linked at the end).</p>
<h3 id="heading-add-the-unitask-dependency">Add the UniTask Dependency</h3>
<p>Unity runs on the main thread, and C# Tasks can be problematic in that context. They don’t know about the Unity Editor's play state and keep running after exiting play mode for example. So you’ll need to handle these cases manually. For these reasons, this package uses <a href="https://github.com/Cysharp/UniTask">UniTask</a> instead of C# Tasks for async operations.</p>
<p>Git-based dependencies are supported at the project level but not at the package level. Add UniTask via OpenUPM by following the <a href="https://openupm.com/packages/com.cysharp.unitask/#modal-manualinstallation">manual installation steps</a>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770988159264/9d2db898-9e1f-4514-aeda-53fb4a190045.png" alt="Adding OpenUPM and UniTask to scoped registries" width="600" height="400" loading="lazy">

<p>Add UniTask as a dependency in package.json of initializer package:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/698d2f3c217bebae73c3f5e8/035b9242-f111-4c96-8483-4b02452a13bc.png" alt="package.json file of initializer package" width="600" height="400" loading="lazy">

<pre><code class="language-json">"dependencies": {
  "com.cysharp.unitask": "2.5.10"
}
</code></pre>
<p>Also reference UniTask in the asmdef file.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771255181446/9812cea2-d278-4920-88e5-43e89cd41a91.png" alt="Referencing UniTask within Initializer package's asmdef file" width="600" height="400" loading="lazy">

<p>You’ll need to follow this dependency flow for the rest of the packages you create.</p>
<h3 id="heading-implement-the-package-structure">Implement the Package Structure</h3>
<p>The initializer uses an MVC-like architecture. Controllers handle both initialization and game logic. You could later split initialization into services and keep business logic in controllers (for example, an MVCS-style setup). This tutorial keeps things simple and uses only controllers.</p>
<p>Target layout:</p>
<ul>
<li><p><strong>Runtime/Interface/</strong> – <code>IController</code></p>
</li>
<li><p><strong>Runtime/Helper/</strong> – <code>Creator</code> (reflection-based instance creation)</p>
</li>
<li><p><strong>Runtime/</strong> – <code>ControllerHandler</code> (orchestrates initialization)</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771255216106/90741270-2166-4694-874f-fcc7aec539fe.png" alt="Initializer package final folders and files" width="600" height="400" loading="lazy">

<p>All controllers implement the <code>IController</code> interface. The <code>Initialize</code> method returns a <code>UniTask</code> so initialization can be async on the main thread.</p>
<pre><code class="language-csharp">using Cysharp.Threading.Tasks;

namespace com.core.initializer
{
    public interface IController
    {
        bool IsInitialized { get; }

        UniTask Initialize();
    }
}
</code></pre>
<h3 id="heading-create-the-creator-helper">Create the Creator Helper</h3>
<p>You’ll need to find all types that implement <code>IController</code>, create instances of them at runtime, and also find MonoBehaviours that implement <code>IController</code> (since those cannot be created via reflection). The <code>Creator</code> class does both.</p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace com.core.initializer
{
    public static class Creator
    {
        /// &lt;summary&gt;
        /// Creates instances of every type which inherits from T interface.
        /// &lt;/summary&gt;
        public static IEnumerable&lt;T&gt; CreateInstancesOfType&lt;T&gt;(params Type[] exceptTypes)
        {
            var interfaceType = typeof(T);

            var result = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x =&gt; x.GetTypes()).Where
                (
                 x =&gt; interfaceType.IsAssignableFrom(x) &amp;&amp;
                      !x.IsInterface                    &amp;&amp;
                      !x.IsAbstract                     &amp;&amp;
                      exceptTypes.All(type =&gt; !x.IsSubclassOf(type) &amp;&amp; x != type)
                ).Select(Activator.CreateInstance);

            return result.Cast&lt;T&gt;();
        }

        public static IEnumerable&lt;T&gt; GetMonoControllers&lt;T&gt;() =&gt; UnityEngine.Object.FindObjectsByType&lt;MonoBehaviour&gt;(FindObjectsInactive.Include, FindObjectsSortMode.None).OfType&lt;T&gt;();
    }
}
</code></pre>
<p>Using reflection at runtime has some overhead. You can optimize this later with baking the reflection in editor and using it at runtime. For MonoBehaviours, <code>FindObjectsByType</code> is used here for simplicity. In a larger project, you might use a dependency injection solution such as <a href="https://github.com/hadashiA/VContainer">VContainer</a> or <a href="https://github.com/gustavopsantos/Reflex">Reflex</a>.</p>
<h3 id="heading-implement-the-controllerhandler">Implement the ControllerHandler</h3>
<p>The <code>ControllerHandler</code> uses <code>Creator</code> to gather all <code>IController</code> instances (both reflection-created and MonoBehaviours), initializes them in order, and exposes an event and a <strong>UniTaskCompletionSource</strong> so the rest of the game can wait for startup to finish.</p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace com.core.initializer
{
    public class ControllerHandler
    {
        public static readonly UniTaskCompletionSource InitializationCompleted = new();

        public static event Action ControllersInitialized;

        private static Dictionary&lt;Type, IController&gt; _controllers = new();

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        public static async void Initialize()
        {
            var controllers = Creator.CreateInstancesOfType&lt;IController&gt;(typeof(MonoBehaviour)).ToList();
            controllers.AddRange(Creator.GetMonoControllers&lt;IController&gt;());

            Debug.Log("Initializing controller");

            foreach (var controller in controllers)
            {
                Debug.Log($"&lt;color=yellow&gt;Initializing {controller.GetType().Name}&lt;/color&gt;");
                await controller.Initialize();
                _controllers.Add(controller.GetType(), controller);
                Debug.Log($"&lt;color=green&gt;Initialized {controller.GetType().Name}&lt;/color&gt;");
            }

            Debug.Log("&lt;color=green&gt;All controllers are initialized&lt;/color&gt;");
            ControllersInitialized?.Invoke();
            InitializationCompleted?.TrySetResult();
        }

        public static T GetController&lt;T&gt;() where T : class, IController =&gt; _controllers[typeof(T)] as T;
    }
}
</code></pre>
<h3 id="heading-how-to-test-the-initializer">How to Test the Initializer</h3>
<ol>
<li><p>Create a class that implements <code>IController</code> (or a MonoBehaviour that implements it).</p>
</li>
<li><p>Implement <code>Initialize()</code> with your setup logic (you can use <code>await</code> and UniTask).</p>
</li>
<li><p>After initialization, access the controller with:<br><code>var myController = ControllerHandler.GetController&lt;MyController&gt;();</code></p>
</li>
</ol>
<p>You can also subscribe to <code>ControllerHandler.ControllersInitialized</code> or await <code>ControllerHandler.InitializationCompleted.Task</code> to run code after all controllers are ready.</p>
<p>There are some limitations here. First, it can be hard to handle dependencies between different controllers. Second, it’s easy to run into circular dependencies.</p>
<p>When you check the <a href="https://github.com/TalhaCagatay/com.core.initializer">GitHub repo</a>, a DI framework may already be implemented to address these limitations. Make sure to open an issue if you want this feature.</p>
<h2 id="heading-package-2-the-data-package">Package 2: The Data Package</h2>
<p>com.core.data handles local saving with a binary serializer. The package uses <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a> for fast, binary serialization and defines an <code>IDataProvider</code> interface so you can plug in different providers (local, cloud, or hybrid).</p>
<p><strong>Package dependency:</strong> <code>com.cysharp.memorypack</code> (version 1.10.0). Add it to your project via OpenUPM and add it to package.json and the asmdef for com.core.data.</p>
<p>Create a new package and name it com.core.data. Use a structure such as:</p>
<ul>
<li><p><strong>Runtime/Interface/</strong> – <code>IDataProvider</code></p>
</li>
<li><p><strong>Runtime/Providers/</strong> – <code>LocalDataProvider</code></p>
</li>
<li><p><strong>Runtime/</strong> – optional sample <code>DataController</code> (in your game assembly) that implements <code>IController</code> and uses an <code>IDataProvider</code></p>
</li>
</ul>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/698d2f3c217bebae73c3f5e8/b6192b39-04d1-4c5a-90e6-b0ed9f1a4ce4.png" alt="Adding MemoryPack to scoped registries" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771266043719/9cd79a74-4a72-4882-b803-8c53ca95eaa0.png" alt="Data package final folders and files" width="600" height="400" loading="lazy">

<p>All data providers use the IDataProvider interface.</p>
<pre><code class="language-csharp">using Cysharp.Threading.Tasks;

namespace com.core.data
{
    public interface IDataProvider
    {
        bool     IsInitialized { get; }
        void     Save&lt;T&gt;(string key, T data);
        T        Load&lt;T&gt;(string key, T defaultValue = default);
        bool     HasKey(string  key);
        string[] GetKeys();
        UniTask  Delete(string key);
        UniTask  DeleteAll();
    }
}
</code></pre>
<p>You will use UniTask in the data package as well. Add com.cysharp.unitask to package.json since both IController and IDataProvider uses it.</p>
<p>Here’s the DataController:</p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Text;
using com.core.data;
using com.core.initializer;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace com.core.data
{
    public class DataController : IController
    {
        private const string PLAYER_VERSION_KEY = "player-version-key";

        private IDataProvider _provider;

        public bool IsInitialized { get; private set; }

        public UniTask Initialize()
        {
            _provider = new LocalDataProvider();
            SaveUserVersion();
            IsInitialized = true;
            return UniTask.CompletedTask;
        }

        private void SaveUserVersion()
        {
            var versionHistory = Load(PLAYER_VERSION_KEY, new List&lt;string&gt;());
            var sb             = new StringBuilder();
            versionHistory.ForEach(vh =&gt; sb.Append(vh + Environment.NewLine));
            Debug.Log($"[DataController] Player version history: {sb}");

            if (!versionHistory.Contains(Application.version))
            {
                Debug.Log($"[DataController] Player's current version: {Application.version}");
                versionHistory.Add(Application.version);
                Save(PLAYER_VERSION_KEY, versionHistory);
            }
        }

        public void Save&lt;T&gt;(string key, T data) =&gt; _provider.Save(key, data);

        public T Load&lt;T&gt;(string key, T defaultValue = default) =&gt; _provider.Load(key, defaultValue);

        public bool         HasKey(string key)  =&gt; _provider.HasKey(key);
        public string[]     GetKeys()           =&gt; _provider.GetKeys();
        public UniTask      Delete(string key)  =&gt; _provider.Delete(key);
        public UniTask      DeleteAll()         =&gt; _provider.DeleteAll();
        public List&lt;string&gt; GetVersionHistory() =&gt; Load(PLAYER_VERSION_KEY, new List&lt;string&gt;());
    }
}
</code></pre>
<p>The controller delegates all logic to LocalDataProvider, which uses MemoryPack to serialize and write bytes to a "Data" folder under <code>Application.persistentDataPath</code>. The controller also keeps the user's app version history, which is useful for upgrade prompts or blocking old versions.</p>
<p>Here’s the full implementation:</p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;

namespace com.core.data
{
    public class LocalDataProvider : IDataProvider
    {
        private const    string StorageFolderName = "Data";
        private readonly string _storagePath;

        private static readonly UTF8Encoding KeyEncoding = new(false);

        public bool IsInitialized =&gt; true;

        public LocalDataProvider()
        {
            _storagePath = Path.Combine(Application.persistentDataPath, StorageFolderName);
            EnsureStorageDirectoryExists();
        }

        public void Save&lt;T&gt;(string key, T data)
        {
            if (string.IsNullOrEmpty(key)) { Debug.LogWarning("[LocalDataProvider] Save called with null or empty key."); return; }
            if (data == null) { Debug.LogWarning("[LocalDataProvider] Save called with null data."); Delete(key).Forget(); return; }
            try
            {
                byte[] bin = MemoryPackSerializer.Serialize(data);
                WriteBytes(key, bin);
            }
            catch (Exception ex) { Debug.LogError($"[LocalDataProvider] Save failed for key '{key}': {ex.Message}"); throw; }
        }

        public T Load&lt;T&gt;(string key, T defaultValue = default)
        {
            if (string.IsNullOrEmpty(key)) { Debug.LogWarning("[LocalDataProvider] Load called with null or empty key."); return defaultValue; }
            if (!ReadBytes(key, out byte[] bin)) return defaultValue;
            try { return MemoryPackSerializer.Deserialize&lt;T&gt;(bin) ?? defaultValue; }
            catch (Exception ex) { Debug.LogError($"[LocalDataProvider] Load failed for key '{key}': {ex.Message}"); return defaultValue; }
        }

        public bool HasKey(string key) =&gt; !string.IsNullOrEmpty(key) &amp;&amp; File.Exists(GetFilePath(key));

        public UniTask Delete(string key)
        {
            if (string.IsNullOrEmpty(key)) return UniTask.CompletedTask;
            try { var filePath = GetFilePath(key); if (File.Exists(filePath)) File.Delete(filePath); }
            catch (Exception ex) { Debug.LogError($"[LocalDataProvider] Delete failed for key '{key}': {ex.Message}"); }
            return UniTask.CompletedTask;
        }

        public UniTask DeleteAll()
        {
            try
            {
                if (!Directory.Exists(_storagePath)) return UniTask.CompletedTask;
                foreach (string file in Directory.GetFiles(_storagePath))
                {
                    try { File.Delete(file); }
                    catch (Exception ex) { Debug.LogWarning($"[LocalDataProvider] Could not delete file '{file}': {ex.Message}"); }
                }
            }
            catch (Exception ex) { Debug.LogError($"[LocalDataProvider] DeleteAll failed: {ex.Message}"); }
            return UniTask.CompletedTask;
        }

        public string[] GetKeys()
        {
            if (!Directory.Exists(_storagePath)) return Array.Empty&lt;string&gt;();
            var keys = new List&lt;string&gt;();
            foreach (string file in Directory.GetFiles(_storagePath))
            {
                try { string key = DecodeKeyFromFileName(Path.GetFileName(file)); if (key != null) keys.Add(key); }
                catch { Debug.LogWarning($"[LocalDataProvider] Could not read file '{file}'."); }
            }
            return keys.ToArray();
        }

        private void EnsureStorageDirectoryExists() { if (!Directory.Exists(_storagePath)) Directory.CreateDirectory(_storagePath); }
        private void WriteBytes(string key, byte[] bin) { EnsureStorageDirectoryExists(); File.WriteAllBytes(GetFilePath(key), bin); }
        private bool ReadBytes(string key, out byte[] bin) { string filePath = GetFilePath(key); if (!File.Exists(filePath)) { bin = null; return false; } bin = File.ReadAllBytes(filePath); return true; }
        private string GetFilePath(string key) =&gt; Path.Combine(_storagePath, EncodeKeyToFileName(key));
        private static string EncodeKeyToFileName(string key) { byte[] bytes = KeyEncoding.GetBytes(key); string base64 = Convert.ToBase64String(bytes); return base64.Replace('+', '-').Replace('/', '_'); }
        private static string DecodeKeyFromFileName(string fileName) { try { string base64 = fileName.Replace('-', '+').Replace('_', '/'); return KeyEncoding.GetString(Convert.FromBase64String(base64)); } catch { return null; } }
    }
}
</code></pre>
<p>You can implement another IDataProvider (for example JSON via <a href="https://openupm.com/packages/com.newtonsoft.json/">Newtonsoft</a> or <a href="https://docs.unity3d.com/6000.3/Documentation/PlayerPrefs.html">PlayerPrefs</a>) and swap it in your DataController.</p>
<p>In production, you might use a local and a cloud provider and sync data based on the player's online status. The com.core.data <a href="https://github.com/TalhaCagatay/com.core.data">GitHub repo</a> may be updated with cloud saving and syncing. You can open an issue if you need that feature.</p>
<h2 id="heading-package-3-comcoredotween"><strong>Package 3: com.core.dotween</strong></h2>
<p>com.core.dotween is a Unity package wrapper around <a href="https://dotween.demigiant.com/">DOTween</a> (Demigiant). DoTween is a widely used, production-ready tween engine. The UI package uses it for popup show/hide animations.</p>
<p>DOTween is not available on OpenUPM, so you’ll typically need to download it from the <a href="https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676">Unity Asset Store</a> and import it into your project (for example, under Assets/Plugins).</p>
<p>Then you’ll run the DOTween setup window(it should pop automatically after importing) and click Create ASMDEF so assembly definition files are generated and you can reference them from other packages.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/698d2f3c217bebae73c3f5e8/1d2a1ea1-1cc4-4bc0-95b8-5e23d22d3f17.png" alt="1d2a1ea1-1cc4-4bc0-95b8-5e23d22d3f17" width="600" height="400" loading="lazy">

<p>Next, create a new package (for example, com.core.dotween) and move the Demigiant folder from Assets/Plugins into the package folder so that com.core.ui (or any other package) can depend on com.core.dotween via Package Manager or Git.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771252964839/36d9e2f0-31a1-445f-b536-8560d0987131.png" alt="Dotween package folders and files" width="600" height="400" loading="lazy">

<p>After that, reference com.core.dotween in com.core.ui's asmdef(You will create the UI package right after this) so you can use <code>DG.Tweening</code> namespace in BasePopup and other UI scripts.</p>
<p>com.core.dotween has no external dependencies – it only wraps the DOTween asset.</p>
<h2 id="heading-package-4-the-ui-package">Package 4: The UI Package</h2>
<p>com.core.ui centralizes how you show popups and full-screen UIs. It contains:</p>
<ul>
<li><p><strong>Popups</strong> – Modal or non-modal dialogs (confirm/cancel, alerts, forms) with a consistent API so you don’t duplicate panel logic in every screen. Popups are stacked – the top one is closed first.</p>
</li>
<li><p><strong>Screens</strong> – Full-screen panels (for example, loading, main menu). Only one screen is active at a time, and switching a screen hides the current one and shows the new one.</p>
</li>
</ul>
<p>Create a new package named com.core.ui with a structure such as:</p>
<ul>
<li><p><strong>Runtime/Interface/</strong> – <code>IUI</code></p>
</li>
<li><p><strong>Runtime/UIs/</strong> – <code>BasePopup</code>, <code>BaseScreen</code></p>
</li>
<li><p><strong>Runtime/</strong> – <code>UIController</code>, <code>UIParent</code></p>
</li>
<li><p><strong>Runtime/Resources/UIPrefabs/</strong> – <code>UIParent</code> prefab, and subfolders Popups and Screens</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771254115546/a71fd415-901c-472c-b46b-f919b5e6ec82.png" alt="UI package folders and files" width="600" height="400" loading="lazy">

<p>Add com.core.dotween, com.core.initializer, and com.cysharp.unitask as dependencies in package.json and asmdef.</p>
<p>Popups work like a stack: each popup is shown on top of the previous one and closed from top to bottom. Screens are single-active: only one screen is visible at a time. UIController hides the current one when you show another. UIController is the single point of contact for both screens and popups and handles stacking, caching, and background placement.</p>
<h3 id="heading-iui-interface">IUI Interface</h3>
<p>Both BasePopup and BaseScreen implement IUI:</p>
<pre><code class="language-csharp">using System;
using Cysharp.Threading.Tasks;

namespace com.core.ui
{
    public interface IUI
    {
        event Action&lt;IUI&gt; Showed;
        event Action&lt;IUI&gt; Hidden;

        UniTask ShowAsync();
        UniTask HideAsync();
    }
}
</code></pre>
<p>IUI is a very basic interface with 2 async Show/Hide methods and 2 events to be fired upon completion of those methods.</p>
<h3 id="heading-basepopup">BasePopup</h3>
<p>BasePopup uses DoTween to animate scale on show/hide and exposes events and overridable animation methods:</p>
<pre><code class="language-csharp">using System;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using UnityEngine;

namespace com.core.ui
{
    public abstract class BasePopup : MonoBehaviour, IUI
    {
        public event Action&lt;IUI&gt; StartedShowing;
        public event Action&lt;IUI&gt; Showed;
        public event Action&lt;IUI&gt; Hidden;

        [SerializeField] private float   openingTime  = 0.35f;
        [SerializeField] private float   closingTime  = 0.25f;
        [SerializeField] private Vector3 initialScale = new(0.75f, 0.75f, 0.75f);
        [SerializeField] private Ease   openingEase  = Ease.OutBack;
        [SerializeField] private Ease   closingEase  = Ease.InBack;

        protected virtual Tweener PlayShowAnimation(Action completedCallback) =&gt; transform.DOScale(Vector3.one, openingTime).SetEase(openingEase).SetUpdate(true).OnComplete(() =&gt; completedCallback?.Invoke());
        protected virtual Tweener PlayHideAnimation(Action completedCallback) =&gt; transform.DOScale(initialScale, closingTime).SetEase(closingEase).SetUpdate(true).OnComplete(() =&gt; completedCallback?.Invoke());

        public UniTask ShowAsync()
        {
            StartedShowing?.Invoke(this);
            transform.SetAsLastSibling();
            var utcs = new UniTaskCompletionSource();
            transform.localScale = initialScale;
            gameObject.SetActive(true);

            PlayShowAnimation(() =&gt; OnAnimationShowed(utcs));
            return utcs.Task;
        }

        public void Show() =&gt; ShowAsync().Forget();

        public UniTask HideAsync()
        {
            var utcs = new UniTaskCompletionSource();
            PlayHideAnimation(() =&gt; OnAnimationCompleted(utcs));
            return utcs.Task;
        }

        private void OnAnimationShowed(UniTaskCompletionSource utcs)
        {
            Showed?.Invoke(this);
            utcs.TrySetResult();
        }

        private void OnAnimationCompleted(UniTaskCompletionSource utcs)
        {
            transform.SetAsFirstSibling();
            gameObject.SetActive(false);
            Hidden?.Invoke(this);
            utcs.TrySetResult();
        }

        public void Hide() =&gt; HideAsync().Forget();
    }
}
</code></pre>
<p>You can see that DoTween is the core component in this class and a lot of parameters are configurable like opening/closing times and easings. Notice how DoTween completion callbacks are utilized to fire Showed and Hidden events.</p>
<p>Another important thing here is the usage of SetUpdate(true) for animations. This makes animations to ignore timescale. I found that most of the time it's best to ignore the timescale for animations but if you don't want this then feel free to change it.</p>
<h3 id="heading-basescreen"><strong>BaseScreen</strong></h3>
<p>BaseScreen only enables/disables the GameObject and raises events. There’s no animation:</p>
<pre><code class="language-csharp">using System;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace com.core.ui
{
    public abstract class BaseScreen : MonoBehaviour, IUI
    {
        public event Action&lt;IUI&gt; Showed;
        public event Action&lt;IUI&gt; Hidden;

        public virtual UniTask ShowAsync()
        {
            gameObject.SetActive(true);
            Showed?.Invoke(this);
            return UniTask.CompletedTask;
        }

        public UniTask HideAsync()
        {
            gameObject.SetActive(false);
            Hidden?.Invoke(this);
            return UniTask.CompletedTask;
        }
    }
}
</code></pre>
<p>You will notice that BaseScreen doesn't need UniTask since it is not performing any async operations but it is still beneficial to have this since it doesn't cause any overheads and if we ever need an animation for any of our screens like popups, we have it ready.</p>
<h3 id="heading-uicontroller"><strong>UIController</strong></h3>
<p>UIController implements <code>IController</code>, instantiates UIParent from Resources, and provides APIs to push/pop popups and show/hide screens. It caches popup and screen instances by type and loads prefabs from Resources:</p>
<ul>
<li><p><strong>UIParent</strong>: <code>Resources/UIPrefabs/UIParent</code></p>
</li>
<li><p><strong>Popups</strong>: <code>Resources/UIPrefabs/Popups/&lt;TypeName&gt;.prefab</code></p>
</li>
<li><p><strong>Screens</strong>: <code>Resources/UIPrefabs/Screens/&lt;TypeName&gt;.prefab</code></p>
</li>
</ul>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using com.core.initializer;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Object = UnityEngine.Object;

namespace com.core.ui
{
    public class UIController : IController
    {
        private const string UI_PARENT_PATH    = "UIPrefabs/UIParent";
        private const string POPUPS_RESOURCES  = "UIPrefabs/Popups";
        private const string SCREENS_RESOURCES = "UIPrefabs/Screens";

        private readonly Stack&lt;BasePopup&gt;             _popupStack  = new();
        private readonly Dictionary&lt;Type, BasePopup&gt;  _popupCache  = new();
        private readonly Dictionary&lt;Type, BaseScreen&gt; _ScreenCache = new();

        private BaseScreen _currentScreen;
        private UIParent   _uiParent;

        public bool IsInitialized { get; private set; }

        public UniTask Initialize()
        {
            var uiParentPrefab = Resources.Load&lt;UIParent&gt;(UI_PARENT_PATH);
            if (uiParentPrefab == null)
            {
                Debug.LogError($"[UIController] UIParent prefab not found at {UI_PARENT_PATH}. Ensure it exists in a Resources folder.");
                return UniTask.CompletedTask;
            }

            _uiParent      = Object.Instantiate(uiParentPrefab);
            _uiParent.name = "UIParent";

            Object.DontDestroyOnLoad(_uiParent.gameObject);

            IsInitialized = true;
            return UniTask.CompletedTask;
        }

        public async UniTask&lt;TPopup&gt; PushPopupAsync&lt;TPopup&gt;() where TPopup : BasePopup
        {
            var popup = GetOrCreatePopup&lt;TPopup&gt;();
            _popupStack.Push(popup);
            var popupTask = popup.ShowAsync();
            UpdateBackgroundForTopPopup();
            await popupTask;
            return popup;
        }

        public void PushPopup&lt;TPopup&gt;() where TPopup : BasePopup =&gt; PushPopupAsync&lt;TPopup&gt;().Forget();

        public async UniTask PopPopupAsync()
        {
            if (_popupStack.Count == 0)
            {
                Debug.LogWarning("[UIController] PopPopup called but popup stack is empty.");
                return;
            }

            var top = _popupStack.Pop();
            await top.HideAsync();
            UpdateBackgroundForTopPopup();
        }

        public void PopPopup() =&gt; PopPopupAsync().Forget();

        public BasePopup PeekPopup() =&gt; _popupStack.Count &gt; 0 ? _popupStack.Peek() : null;

        public async UniTask&lt;TScreen&gt; ShowScreenAsync&lt;TScreen&gt;() where TScreen : BaseScreen
        {
            var screen = GetOrCreateScreen&lt;TScreen&gt;();

            if (_currentScreen == screen &amp;&amp; screen.gameObject.activeSelf) return screen;
            if (_currentScreen != null   &amp;&amp; _currentScreen != screen &amp;&amp; _currentScreen.gameObject.activeSelf) await _currentScreen.HideAsync();

            _currentScreen = screen;
            await screen.ShowAsync();
            return screen;
        }

        public async UniTask HideScreenAsync&lt;TScreen&gt;() where TScreen : BaseScreen
        {
            var type = typeof(TScreen);
            if (!_ScreenCache.TryGetValue(type, out var screen))
            {
                Debug.LogWarning($"[UIController] HideScreenAsync&lt;{type.Name}&gt; called but Screen was never shown (not in cache).");
                return;
            }

            await screen.HideAsync();

            if (_currentScreen == screen) _currentScreen = null;
        }

        public void ShowScreen&lt;TScreen&gt;() where TScreen : BaseScreen =&gt; ShowScreenAsync&lt;TScreen&gt;().Forget();
        public void HideScreen&lt;TScreen&gt;() where TScreen : BaseScreen =&gt; HideScreenAsync&lt;TScreen&gt;().Forget();

        private TPopup GetOrCreatePopup&lt;TPopup&gt;() where TPopup : BasePopup
        {
            var type = typeof(TPopup);
            if (_popupCache.TryGetValue(type, out var cached) &amp;&amp; cached != null) return (TPopup)cached;

            var prefab = LoadPopupPrefab&lt;TPopup&gt;();
            if (prefab == null)
            {
                Debug.LogError($"[UIController] Popup prefab for {type.Name} not found at {POPUPS_RESOURCES}/{type.Name}. Ensure the prefab exists in a Resources folder.");
                return null;
            }

            var instance = UnityEngine.Object.Instantiate(prefab, _uiParent.PopupParent);
            instance.gameObject.SetActive(false);

            var popup = instance.GetComponent&lt;TPopup&gt;();
            _popupCache[type] = popup;

            return popup;
        }

        private TScreen GetOrCreateScreen&lt;TScreen&gt;() where TScreen : BaseScreen
        {
            var type = typeof(TScreen);
            if (_ScreenCache.TryGetValue(type, out var cached) &amp;&amp; cached != null) return (TScreen)cached;

            var prefab = LoadScreenPrefab&lt;TScreen&gt;();
            if (prefab == null)
            {
                Debug.LogError($"[UIController] Screen prefab for {type.Name} not found at {SCREENS_RESOURCES}/{type.Name}. Ensure the prefab exists in a Resources folder.");
                return null;
            }

            var instance = UnityEngine.Object.Instantiate(prefab, _uiParent.ScreenParent);
            instance.gameObject.SetActive(false);

            var screen = instance.GetComponent&lt;TScreen&gt;();
            _ScreenCache[type] = screen;

            return screen;
        }

        private TPopup  LoadPopupPrefab&lt;TPopup&gt;() where TPopup : BasePopup     =&gt; LoadViewPrefab&lt;TPopup&gt;(POPUPS_RESOURCES);
        private TScreen LoadScreenPrefab&lt;TScreen&gt;() where TScreen : BaseScreen =&gt; LoadViewPrefab&lt;TScreen&gt;(SCREENS_RESOURCES);

        private static T LoadViewPrefab&lt;T&gt;(string resourcesPath) where T : MonoBehaviour
        {
            var type = typeof(T);
            var path = $"{resourcesPath}/{type.Name}";
            var go   = Resources.Load&lt;GameObject&gt;(path);
            return go != null ? go.GetComponent&lt;T&gt;() : null;
        }

        private void UpdateBackgroundForTopPopup()
        {
            var backgroundGO = _uiParent.BackgroundGO;
            if (backgroundGO == null) return;

            if (_popupStack.Count &gt; 0)
            {
                var topPopup = _popupStack.Peek();
                if (topPopup != null &amp;&amp; topPopup.gameObject.activeSelf)
                {
                    // Position background one step behind the top popup
                    var topPopupIndex = topPopup.transform.GetSiblingIndex();
                    backgroundGO.SetActive(true);
                    backgroundGO.transform.SetSiblingIndex(Mathf.Max(0, topPopupIndex - 1));
                }
                else
                {
                    // Top popup is inactive, hide background
                    backgroundGO.SetActive(false);
                }
            }
            else
            {
                // No popups in stack, hide background
                backgroundGO.SetActive(false);
            }
        }
    }
}
</code></pre>
<p>Usage:</p>
<pre><code class="language-csharp">var ui = ControllerHandler.GetController&lt;UIController&gt;();
await ui.PushPopupAsync&lt;MyPopup&gt;();
await ui.PopPopupAsync();
await ui.ShowScreenAsync&lt;MainMenuScreen&gt;();
await ui.HideScreenAsync&lt;LoadingScreen&gt;();
</code></pre>
<p>UIController also updates the background GameObject so it indexes behind the top popup.</p>
<p>It uses <code>GetOrCreatePopup&lt;TPopup&gt;()</code> / <code>GetOrCreateScreen&lt;TScreen&gt;()</code> to load and cache prefabs from <code>Resources/UIPrefabs/Popups/&lt;TypeName&gt;</code> and <code>Resources/UIPrefabs/Screens/&lt;TypeName&gt;</code>, pushes popups onto a stack, and shows/hides screens with async methods.</p>
<p>UIParent is a MonoBehaviour that holds references to ScreenParent, PopupParent, and BackgroundGO:</p>
<pre><code class="language-csharp">using UnityEngine;

namespace com.core.ui
{
    public class UIParent : MonoBehaviour
    {
        [SerializeField] private Transform  screensParent;
        [SerializeField] private Transform  popupsParent;
        [SerializeField] private GameObject backgroundGO;

        public Transform  ScreenParent =&gt; screensParent;
        public Transform  PopupParent  =&gt; popupsParent;
        public GameObject BackgroundGO =&gt; backgroundGO;
    }
}
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771288099657/6104e31e-e8e0-489c-bca5-592d5daec174.png" alt="UIParent prefab hierarchy" width="600" height="400" loading="lazy">

<p>UIParent prefab has a canvas and UIParent script attached:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/698d2f3c217bebae73c3f5e8/cb79381a-35c3-49ac-af50-da33b0f7f988.png" alt="UIParent prefab components" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>There are a couple limitations here as well. First, popups and screens must live under Resources/UIPrefabs/Popups and Resources/UIPrefabs/Screens. Second, prefab name must match the script/type name (for example, TestPopup prefab name for a script named TestPopup).</p>
<h2 id="heading-summary"><strong>Summary</strong></h2>
<p>In this article, you set up a Unity project and walked through creating four reusable packages:</p>
<ul>
<li><p><strong>com.core.initializer</strong> – You created the package, added <strong>UniTask</strong>, defined <code>IController</code>, used the <code>Creator</code> helper to find and create controllers (including MonoBehaviours), and implemented <code>ControllerHandler</code> to run initialization at <code>BeforeSceneLoad</code> and expose controllers via <code>GetController&lt;T&gt;()</code>.</p>
</li>
<li><p><strong>com.core.data</strong> – You use <code>IDataProvider</code> and <code>LocalDataProvider</code> with MemoryPack for binary local save/load, and the <code>DataController</code> that implements <code>IController</code> and tracks version history.</p>
</li>
<li><p><strong>com.core.dotween</strong> – You wrap the <strong>DoTween</strong> asset as a package so other packages (like <strong>com.core.ui</strong>) can reference it for animations.</p>
</li>
<li><p><strong>com.core.ui</strong> – You created a modular UI package that you can use in your games to handle popups and full size screens. You can also extend this to add other UI types in the future.</p>
</li>
</ul>
<p>Building these as separate packages keeps your code modular and speeds up development across projects. You can install each package via Git into any Unity project.</p>
<p>Example manifest.json file to implement these packages:</p>
<pre><code class="language-json">{
  "dependencies"    : {
	"com.core.data"       : "https://github.com/TalhaCagatay/com.core.data.git#v0.1.0",
	"com.core.initializer": "https://github.com/TalhaCagatay/com.core.initializer.git#v0.1.0",
	"com.core.dotween"    : "https://github.com/TalhaCagatay/com.core.dotween.git#v0.1.0",
	"com.core.ui"         : "https://github.com/TalhaCagatay/com.core.ui.git#v0.1.0"
  },
  "scopedRegistries": [
	{
	  "name"  : "package.openupm.com",
	  "url"   : "https://package.openupm.com",
	  "scopes": [
		"com.cysharp.unitask",
		"com.cysharp.memorypack"
	  ]
	}
  ]
}
</code></pre>
<p><strong>Resources:</strong></p>
<ul>
<li><p>com.core.initializer on <a href="https://github.com/TalhaCagatay/com.core.initializer">GitHub</a></p>
</li>
<li><p>com.core.data on <a href="https://github.com/TalhaCagatay/com.core.data">GitHub</a></p>
</li>
<li><p>com.core.dotween on <a href="https://github.com/TalhaCagatay/com.core.dotween">Github</a></p>
</li>
<li><p>com.core.ui on <a href="https://github.com/TalhaCagatay/com.core.ui">Github</a></p>
</li>
<li><p><a href="https://play.google.com/store/apps/details?id=com.Focus.Matchingham">Example game</a> that I developed and used many modular systems like the ones you built in this article.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
