Beyond Static Tags: Building Your Unity Runtime Tagging System

Here's the thing—I spent a good afternoon once trying to figure out why my game's enemy AI couldn't find allies to call for backup. I was using Unity's built-in tag system, and the problem hit me hard: each GameObject can only have one tag. My enemy was tagged as "Enemy," but when I needed to also mark them as "Alerted" to signal nearby allies, I was stuck. That's when I discovered Unity runtime tagging systems, and honestly, it revolutionized how I architect my games.

A Unity runtime tagging system solves the problem of dynamically categorizing and finding GameObjects while the game is running. Unlike Unity's built-in tags, which are static and set in the editor, a runtime system allows you to add or remove descriptive tags on the fly, enabling you to create complex, emergent gameplay behaviors. You can ask questions like "find all enemies that are currently on fire" or "find all allies within this healing aura." Think of it like using hashtags on social media—you can apply multiple hashtags (#enemy, #flying, #onFire) to a post (a GameObject), and later, anyone can instantly find all posts that match a specific hashtag, creating powerful, flexible groupings that can change at any moment.

First Off, What's the Difference Between Unity's Tags and a Real Tagging System?

Before we dive into code, let me explain the key difference that most beginner developers miss. When I first learned about Unity component tagging at CMU, I was confused about why anyone would build a custom system when Unity already has tags. Let me break down the terminology in plain terms.

Static Tagging (Unity's Tag System): This refers to assigning a single, predefined tag to a GameObject in the Unity Editor. This tag cannot be changed at runtime, and an object can only have one such tag at a time. It's like giving someone a permanent job title—once they're labeled "Manager," they can't also be "Developer" at the same time.

Runtime Tagging: This is the process of programmatically adding or removing one or more tags from a GameObject while the game is actively running, providing a much more flexible way to manage object states. It's like giving someone multiple roles that can change throughout the day—"Manager" in the morning, "Developer" in the afternoon, and "Mentor" in the evening.

Component-Based Tagging: An implementation approach where tags are managed by a custom C# component attached to a GameObject, which typically holds a list of string-based tags. This component sits on your GameObject just like any other component, but its job is solely to manage what categories or states that object belongs to.

Query System: A centralized manager or service that keeps track of all tagged objects in the scene, providing efficient methods to find and retrieve lists of objects based on the tags they currently possess. Think of it as a search engine for your game—you type in what you're looking for, and it instantly returns all matching GameObjects.

HashSet<T>: A C# collection that stores unique elements and provides highly optimized methods for adding, removing, and checking for the existence of an item, making it ideal for managing lists of tagged objects. The "T" is a placeholder for whatever type you're storing—in our case, it'll be strings for tag names or GameObjects for object lists. HashSet automatically prevents duplicates, which is perfect for tags.

Building Blocks You Need to Know

Let me show you how I approach building a Unity runtime tagging system from the ground up. We'll start with the two core components that make the entire system work: the Taggable component and the Tag Manager.

The Taggable Component: Your Object's Tag Container

The core of the system is a simple component that you attach to any GameObject you want to be taggable. This component holds a collection of its current tags. I use a HashSet to prevent duplicate tags—this is crucial because you don't want an object accidentally tagged as "Enemy" five times.

Here's the exact code I use:

csharp
using System.Collections.Generic;
using UnityEngine;

public class Taggable : MonoBehaviour
{
    // Using a HashSet ensures that each tag is unique and provides fast lookups.
    public HashSet<string> Tags = new HashSet<string>();
}

This is beautifully simple. The HashSet<string> stores all the tags for this particular GameObject. The HashSet data structure is perfect here because checking if a tag exists (using Contains()) is extremely fast—it's an O(1) operation, meaning it takes the same amount of time whether you have 1 tag or 100 tags.

The Tag Manager: Your Central Query Database

A global manager is needed to keep track of all tagged objects. After working on multiple Unity projects, I've found this singleton pattern is the cleanest approach. The manager uses a dictionary where the key is the tag (e.g., "Enemy") and the value is a HashSet of all GameObjects that currently have that tag.

Here's my singleton Tag Manager implementation:

csharp
using System.Collections.Generic;
using UnityEngine;

public class TagManager : MonoBehaviour
{
    public static TagManager Instance { get; private set; }

    // The central database mapping tags to all GameObjects that have them.
    private Dictionary<string, HashSet<GameObject>> tagDatabase = new Dictionary<string, HashSet<GameObject>>();

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

The DontDestroyOnLoad call is critical—it ensures the TagManager persists across scene transitions. Without it, all your tag data would be lost every time you load a new scene, which would be a disaster.

Creating Your Taggable Component and Tag Manager

Now let's connect these pieces together. When a tag is added to or removed from a Taggable component, the TagManager must be updated. This is the logic that keeps the Unity GameObject query system in sync with the game state.

Registering and Unregistering Tags

Here's how I implement adding and removing tags. These methods ensure that both the local component and the central manager stay synchronized:

csharp
// Add these methods to the Taggable component
public void AddTag(string tag)
{
    Tags.Add(tag);
    TagManager.Instance.RegisterTag(tag, this.gameObject);
}

public void RemoveTag(string tag)
{
    Tags.Remove(tag);
    TagManager.Instance.UnregisterTag(tag, this.gameObject);
}

// Add these methods to the TagManager
public void RegisterTag(string tag, GameObject obj)
{
    if (!tagDatabase.ContainsKey(tag))
    {
        tagDatabase[tag] = new HashSet<GameObject>();
    }
    tagDatabase[tag].Add(obj);
}

public void UnregisterTag(string tag, GameObject obj)
{
    if (tagDatabase.ContainsKey(tag))
    {
        tagDatabase[tag].Remove(obj);
    }
}

What's happening here? When you call AddTag("Enemy") on a Taggable component, two things happen: first, the tag is added to the local HashSet, and second, the TagManager is notified to add this GameObject to the "Enemy" list in its central database. When you remove a tag, the reverse happens. This bidirectional update ensures your data is always consistent.

Querying for Objects: The Whole Point

The primary purpose of the manager is to provide an efficient way to find all objects with a given tag. This is where the real power of the system shines. Here's the query method I use:

csharp
// Add this method to the TagManager
public HashSet<GameObject> GetObjectsWithTag(string tag)
{
    if (tagDatabase.ContainsKey(tag))
    {
        return tagDatabase[tag];
    }
    // Return an empty set to avoid null reference exceptions
    return new HashSet<GameObject>();
}

This method returns the HashSet of objects with the specified tag, or an empty HashSet if the tag doesn't exist. Returning an empty HashSet instead of null is a best practice—it prevents null reference exceptions when you iterate over the results. Your code can safely do foreach (GameObject obj in results) without checking for null first.

For more details on HashSet performance and usage, see the Microsoft Docs - HashSet<T> Class.

The Critical Decision: Built-In Tags vs. Runtime System

Been there—staring at two approaches wondering which one to use. Here's the comparison table I reference whenever I'm architecting a new Unity tag system:

Criteria Approach A: Unity's Built-in Tags Approach B: Runtime Component System
Best For Identifying static, unchanging types of objects where an object only ever belongs to one category (e.g., "Player", "MainCamera"). Dynamic gameplay systems where objects can change roles or states, such as Unity status effects, factions, or interactive states.
Performance GameObject.CompareTag() is very fast for checking a single object's tag. GameObject.FindGameObjectsWithTag() is slow and should be avoided in Update loops as it searches the entire scene hierarchy. A custom query system is extremely fast for lookups (O(1) complexity) because it uses a dictionary. The main cost is the minor overhead of registering/unregistering objects when tags change.
Complexity Very simple to use. You define tags in the editor's Tag Manager and access them with built-in functions. No custom code is required for the basic system. Requires setting up the Taggable component and TagManager singleton. This is more initial work but provides a vastly more powerful and scalable foundation.
Code Example if (other.CompareTag("Enemy")) { ... }
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
taggable.AddTag("OnFire");
HashSet<GameObject> burningThings = TagManager.Instance.GetObjectsWithTag("OnFire");

What I find fascinating about this comparison is that Unity's built-in system is perfect for permanent identifiers like "Player" or "MainCamera," but it completely breaks down when you need dynamic, multi-category systems. From my time at CMU and later at KIXEYE, I learned that almost every non-trivial game needs the flexibility of runtime tagging.

Why This Changes Your Entire Unity Game Architecture

Let me tell you why implementing a Unity HashSet collection-based tagging system isn't just a technical improvement—it's a fundamental shift in how you architect your entire game. These benefits compound as your project grows.

Enables Emergent Gameplay: This allows you to create systems that react to changing game states, such as a water spell that only extinguishes entities with an "OnFire" tag, without needing to know about them in advance. I've used this pattern to create spells that interact with status effects in ways I never explicitly programmed—the systems just work together naturally because they communicate through tags.

Decouples Systems: A Unity AI system doesn't need a direct reference to the player. It can simply query the TagManager for any object with the "Player" tag, making your code more modular and easier to maintain. This is huge for team projects—different programmers can work on different systems without stepping on each other's toes, as long as they agree on tag names.

High Performance Queries: A centralized dictionary provides near-instant lookups for groups of objects, which is far more performant than repeatedly calling GameObject.FindObjectsOfType or similar scene-searching functions. I've seen games go from 30 FPS to 60 FPS just by replacing FindObjectsOfType calls with a proper tagging system. The performance difference is that dramatic.

Support for Multiple Categories: Unlike Unity's built-in system, an object can have many tags at once (e.g., "Enemy", "Flying", "Poisoned"), allowing for much more detailed and nuanced object classification. This multi-category support is what enables complex gameplay interactions. A spell that targets "Flying" and "Poisoned" enemies specifically? Trivial with runtime tags, impossible with Unity's built-in system.

The Pro Techniques That Make It Production-Ready

Alright, here are the techniques that took me a bit to figure out. These are the differences between a prototype that works in your dev environment and a production system that won't break under stress.

Automate Unregistering in OnDestroy

To prevent memory leaks and errors from objects that have been destroyed but are still in the database, the Taggable component should automatically unregister all its tags when it is destroyed. Trust me, you'll thank me later for this tip—I ran into this issue early on when destroyed enemies were still appearing in query results, causing null reference exceptions everywhere.

Here's the exact cleanup code I use:

csharp
// Add this method to the Taggable component
void OnDestroy()
{
    // Make a copy of the tags to avoid modifying the collection while iterating
    List<string> tagsToRemove = new List<string>(Tags);
    foreach (string tag in tagsToRemove)
    {
        RemoveTag(tag);
    }
}

The crucial detail here is creating a copy of the Tags collection before iterating. If you tried to call RemoveTag while directly iterating over Tags, you'd get a "Collection was modified" exception because you're changing the HashSet while looping through it. Making a copy solves this elegantly.

Use ScriptableObjects for Tag Definitions

To avoid typos when using string-based tags, you can define your tags as ScriptableObject assets. This provides a dropdown menu in the Inspector and compile-time checking. After analyzing dozens of professional Unity projects, I've found this pattern significantly reduces bugs caused by misspelled tag names.

Here's how I set up ScriptableObject tags:

csharp
// Create a new C# script file for this class
using UnityEngine;

[CreateAssetMenu(fileName = "NewTag", menuName = "Tag System/Tag")]
public class GameplayTag : ScriptableObject { }

// In your Taggable component, you could then use:
// public List<GameplayTag> tags;

With this setup, you create tag assets in your project (right-click → Create → Tag System → Tag), and then you drag these assets into lists in the Inspector. No more wondering if you typed "Enmy" instead of "Enemy."

Provide an Initial Tags List

For convenience, allow designers to set a list of initial tags in the Inspector, which the Taggable component will automatically register at the start of the game. This is the approach I use in all my projects because it lets level designers set up tagged objects without writing any code.

Here's the implementation:

csharp
// Add this to the Taggable component
public List<string> initialTags = new List<string>();

void Start()
{
    foreach (string tag in initialTags)
    {
        AddTag(tag);
    }
}

Now when you attach the Taggable component to an object, you'll see an Initial Tags list in the Inspector. You can add "Enemy", "Flying", "Boss" directly in the editor, and they'll be automatically registered when the game starts.

For more details on OnDestroy and its usage, see the Unity Docs - OnDestroy.

How Your Favorite Games Use Dynamic Tagging

Let me share some examples of how runtime tagging appears in games you've probably played. Understanding these real-world applications helps you see when and why to use these techniques in your own projects.

The Last of Us: Stealth and AI States

The Mechanic: Enemies dynamically change their state based on what they see and hear, transitioning between "patrolling," "suspicious," and "hostile." The AI needs to communicate state changes to other nearby enemies to create coordinated group behaviors.

The Implementation: I've seen this technique used brilliantly in The Last of Us. Each enemy likely has a runtime tagging component. When a player makes a sound, nearby enemies might be tagged as "Suspicious." If the player is seen, the tag changes to "Hostile," which is then queried by other nearby AI to trigger group attack behaviors. What makes this brilliant is the emergent coordination—no enemy explicitly tells another enemy to attack; they just query "who's hostile?" and join in.

The Player Experience: This creates a tense and reactive stealth experience where the player's actions have a tangible and cascading impact on the AI's behavior. From a developer's perspective, this tagging approach makes the AI feel intelligent without requiring complex state machines for group coordination.

Diablo III: Area of Effect (AoE) Spells

The Mechanic: A wizard casts a Blizzard spell, which creates a circular area on the ground. Any enemy that enters this area is chilled and takes damage over time. Multiple AoE effects can stack, creating complex combat scenarios.

The Implementation: One of my favorite implementations of dynamic tagging is in Diablo III's spell system. When an enemy enters the Blizzard's trigger volume, a "Chilled" tag is added to it. A separate damage system can then query the TagManager every second for all objects with the "Chilled" tag and apply damage to them. When an enemy leaves the area, the tag is removed. Here's how you can adapt this for your own game: use trigger colliders for AoE zones, and add/remove tags in OnTriggerEnter/OnTriggerExit.

The Player Experience: This allows for complex and visually satisfying interactions between different game systems, where players can strategically layer multiple effects on enemies. I always tell my students to look at how Diablo III handles status effects—it's a masterclass in decoupled system design.

Control: Interactive Environments

The Mechanic: The player can use their telekinetic "Launch" ability to pick up and throw specific objects in the environment, like fire extinguishers and chunks of concrete. Not every object is launchable—the game needs to identify valid targets dynamically.

The Implementation: Let me tell you about how Control solved this exact problem. Objects that can be manipulated are likely given a runtime tag of "Launchable." When the player aims the ability, the system performs a query to find the nearest object with the "Launchable" tag to be targeted. This is why I always recommend studying Control's approach—it creates environmental interactivity withouthardcoding relationships between the player and every throwable object.

The Player Experience: This creates a dynamic and empowering combat sandbox where the environment itself becomes a weapon, rewarding player observation and creativity. After analyzing dozens of games with environmental interaction, Control stands out because its tagging system makes every level feel like a playground.

Alright, Let's Build This Thing: Three Systems You Can Code Now

Let's get hands-on together. I'm going to walk you through building three practical systems that demonstrate the power of runtime tagging. These are the exact implementations I use in my projects.

Blueprint 1: Dynamic Status Effect System

Scenario Goal: Create a system where a player's weapon can apply a "Burning" tag to an enemy, and a separate damage-over-time system will deal damage to all burning enemies.

Unity Editor Setup:

Step 1: Create the FireSword Script

This script will detect when it hits an object and, if that object is taggable, apply the "Burning" tag for a few seconds. In my projects, I use this exact pattern for all temporary status effects:

csharp
using UnityEngine;
using System.Collections;

public class FireSword : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        // Try to get the Taggable component on the object we hit
        Taggable taggable = collision.gameObject.GetComponent<Taggable>();
        if (taggable != null)
        {
            StartCoroutine(ApplyBurning(taggable));
        }
    }

    private IEnumerator ApplyBurning(Taggable target)
    {
        target.AddTag("Burning");
        Debug.Log(target.name + " is now on fire!");

        yield return new WaitForSeconds(5f); // Effect lasts for 5 seconds

        target.RemoveTag("Burning");
        Debug.Log(target.name + " is no longer on fire.");
    }
}

The coroutine pattern here is perfect for temporary effects. The tag is added immediately on collision, waits 5 seconds, then automatically removes itself. No complex state management required.

Step 2: Create the DamageOverTimeSystem

This system will run independently. Every second, it will query the TagManager to find all objects with the "Burning" tag and apply damage to them. This is the decoupled system design I mentioned earlier—the damage system doesn't know about the FireSword, and vice versa:

csharp
using System.Collections.Generic;
using UnityEngine;

public class DamageOverTimeSystem : MonoBehaviour
{
    private float tickRate = 1.0f;
    private float timer;

    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= tickRate)
        {
            timer -= tickRate;
            ApplyBurningDamage();
        }
    }

    void ApplyBurningDamage()
    {
        // Find all objects currently tagged as "Burning"
        HashSet<GameObject> burningObjects = TagManager.Instance.GetObjectsWithTag("Burning");

        foreach (GameObject obj in burningObjects)
        {
            // In a real game, you'd get a Health component and deal damage
            Debug.Log("Dealing 5 burning damage to " + obj.name);
        }
    }
}

Notice how the damage system has zero knowledge of what caused the burning status. It just queries "who's burning?" and deals damage. This is beautiful decoupling.

Blueprint 2: Simple Faction-Based AI Turret

Scenario Goal: Create an AI turret that automatically finds and fires at the nearest target belonging to the "Enemy" faction.

Unity Editor Setup:

Step-by-Step Implementation:

Implement the TurretAI Logic

The turret will use the TagManager to get a list of all enemies, find the closest one, and aim at it. These are the exact settings I use when creating turret AI:

csharp
using System.Collections.Generic;
using UnityEngine;

public class TurretAI : MonoBehaviour
{
    public float range = 15f;
    private Transform currentTarget;

    void Start()
    {
        // Periodically search for new targets
        InvokeRepeating("FindTarget", 0f, 0.5f);
    }

    void Update()
    {
        if (currentTarget != null)
        {
            // Aim at the current target
            transform.LookAt(currentTarget);
            // In a real game, you would add firing logic here
        }
    }

    void FindTarget()
    {
        HashSet<GameObject> enemies = TagManager.Instance.GetObjectsWithTag("Enemy");
        float shortestDistance = Mathf.Infinity;
        GameObject nearestEnemy = null;

        foreach (GameObject enemy in enemies)
        {
            float distanceToEnemy = Vector3.Distance(transform.position, enemy.transform.position);
            if (distanceToEnemy < shortestDistance)
            {
                shortestDistance = distanceToEnemy;
                nearestEnemy = enemy;
            }
        }

        if (nearestEnemy != null && shortestDistance <= range)
        {
            currentTarget = nearestEnemy.transform;
        }
        else
        {
            currentTarget = null;
        }
    }
}

The InvokeRepeating call is crucial here—we search for targets every 0.5 seconds rather than every frame. This is a performance optimization that I learned from my time at KIXEYE. Searching every frame would be overkill and waste CPU cycles.

Blueprint 3: An "Interactable" Object System

Scenario Goal: Create a system where the player can look at objects tagged as "Interactable" and a UI prompt appears.

Unity Editor Setup:

Step-by-Step Implementation:

Create the PlayerInteraction Script

This script will cast a ray from the camera. If it hits an object with the "Interactable" tag, it will show the UI prompt. Here's my go-to implementation for interaction systems:

csharp
using UnityEngine;
using UnityEngine.UI;

public class PlayerInteraction : MonoBehaviour
{
    public Camera playerCamera;
    public float interactionDistance = 3f;
    public Text interactionPrompt; // Assign your UI Text element here

    void Update()
    {
        Ray ray = playerCamera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));
        RaycastHit hit;

        bool successfulHit = false;

        if (Physics.Raycast(ray, out hit, interactionDistance))
        {
            Taggable taggable = hit.collider.GetComponent<Taggable>();
            if (taggable != null && taggable.Tags.Contains("Interactable"))
            {
                interactionPrompt.text = "[E] Interact with " + hit.collider.name;
                interactionPrompt.gameObject.SetActive(true);
                successfulHit = true;
            }
        }

        if (!successfulHit)
        {
            interactionPrompt.gameObject.SetActive(false);
        }
    }
}

The successfulHit flag is a pattern I use to clean up the UI. If the raycast doesn't hit anything interactable, we hide the prompt. This prevents the prompt from lingering after you look away from an object. A quick note on a best practice here: since this raycast is for player input and doesn't directly manipulate Rigidbody physics, we use Update() to ensure maximum responsiveness to player actions, which are checked every frame. The taggable != null check is also a good defensive habit to prevent errors if the ray hits an object without a Taggable component.

For more details on raycasting, see the Unity Docs - Physics.Raycast.


Ready to Start Building Your First Game?

If you've made it this far, you now have the foundational skills to implement a robust runtime tagging system in Unity. But tagging is just one piece of creating a complete, polished game experience. From core gameplay mechanics to AI systems, there's a complete journey from these basics to shipping a professional game.

That's exactly why I created the Mr. Blocks Unity Course at Outscal. This course takes you from your very first Unity project to building a complete, polished game with professional-quality features. You'll learn how to implement core systems like the tagging and query systems we covered today, create intuitive UI, optimize performance, and publish your game—all while building a portfolio-worthy project.

Whether you're a college student exploring game development for the first time or a Class 12 student considering a career in gaming, this course gives you hands-on, project-based learning that prepares you for the real challenges of game development. Let's take your Unity skills from these foundational systems to shipping complete games.


The Core Lessons to Lock In

Here are the essential takeaways about Unity runtime tagging system implementation that I want you to remember:

Common Questions About Unity Runtime Tagging Systems

What is a runtime tagging system in Unity?+

A runtime tagging system is a custom solution that allows you to dynamically add, remove, and query multiple tags on GameObjects while the game is running. Unlike Unity's built-in tag system where each object has one fixed tag set in the editor, runtime tagging lets you programmatically change an object's categories on the fly. For example, an enemy could start with just "Enemy" but gain "Burning" and "Slowed" tags during gameplay based on what abilities hit it.

Why can't I just use Unity's built-in tag system?+

Unity's built-in tag system has two critical limitations: each GameObject can only have one tag, and tags are static (set in the editor, not changeable at runtime). This makes it impossible to create dynamic gameplay systems where objects need multiple categories or need to change states. For example, you can't have an object be both "Enemy" and "Poisoned" using built-in tags. Runtime tagging solves both problems.

What is a HashSet and why do you use it for tags?+

A HashSet is a C# collection that stores unique elements with extremely fast lookup, addition, and removal operations (O(1) complexity). We use HashSet<string> to store tags because it automatically prevents duplicates (you can't accidentally add "Enemy" five times) and checking if a tag exists is nearly instant. This performance characteristic is crucial when you're querying tags hundreds or thousands of times per second in gameplay code.

How does the TagManager singleton pattern work?+

The singleton pattern ensures only one instance of TagManager exists in your game at any time. When the first TagManager awakens, it sets itself as the Instance and calls DontDestroyOnLoad to persist across scenes. If another TagManager tries to awaken, it sees Instance is already set and destroys itself. This gives you a globally accessible point (TagManager.Instance) to register, unregister, and query tags from any script in your game.

What's the performance difference between runtime tagging and FindGameObjectsWithTag?+

Runtime tagging with a dictionary-based query system is dramatically faster. FindGameObjectsWithTag() searches through every GameObject in your scene hierarchy every time you call it, which is slow (O(n) where n is the number of objects). A dictionary-based TagManager query is O(1)—it returns results in constant time regardless of scene size. I've seen projects gain 30+ FPS just by replacing FindGameObjectsWithTag calls in Update loops with a proper tagging system.

Why do you need to unregister tags in OnDestroy?+

When a GameObject is destroyed, its Taggable component is destroyed too. But the TagManager's central database still has references to that GameObject in its HashSets. If you don't unregister these tags, your query results will include destroyed objects, causing null reference exceptions when you try to access them. Automatic cleanup in OnDestroy prevents these bugs by removing the object from all tag lists before it's destroyed.

Can I use this system with Unity's built-in tags at the same time?+

Absolutely. Runtime tagging is completely independent of Unity's built-in tag system—they can coexist peacefully. I often use Unity's built-in tags for permanent identifiers like "Player", "MainCamera", or "Ground" that never change, and use runtime tagging for dynamic gameplay states like "Burning", "Stunned", or "InCover" that change during play. This hybrid approach gives you the simplicity of built-in tags for static cases and the flexibility of runtime tags for dynamic systems.

What are ScriptableObjects and why would I use them for tags?+

ScriptableObjects are Unity assets that hold data but aren't attached to GameObjects. When you define tags as ScriptableObject assets, you create actual files in your project (like "EnemyTag.asset" or "BurningTag.asset"). Then in your scripts, instead of typing the string "Enemy" (which could have typos), you drag the EnemyTag asset into an Inspector field. This gives you compile-time safety, prevents spelling mistakes, and makes refactoring tags across your project much easier.

How do I handle tag changes when objects are disabled?+

When a GameObject is disabled, its components remain attached but their Update/FixedUpdate methods stop being called. However, the Taggable component and its registered tags are still valid—they don't automatically unregister. This is usually what you want (a disabled enemy is still an enemy). If you need tags to be removed when disabled, add an OnDisable method to your Taggable component that unregisters specific tags, then re-add them in OnEnable.

Can multiple systems query the same tag simultaneously?+

Yes, and this is one of the major advantages of the centralized TagManager approach. Multiple completely independent systems can all query "Burning" tags without knowing about each other—one system might apply damage, another might create particle effects, and a third might affect AI behavior. They all call GetObjectsWithTag("Burning") and get the same list of objects. This enables the emergent gameplay and system decoupling I mentioned throughout the article.

What happens if I query a tag that doesn't exist?+

The GetObjectsWithTag method I showed returns an empty HashSet if the tag doesn't exist in the database. This is a defensive programming practice—it means your code won't crash with a null reference exception if you query a tag that no object currently has. You can safely iterate over the results with foreach, and it simply won't execute if the HashSet is empty. Always design query systems to fail gracefully like this.

How do I debug which objects have which tags?+

Add a custom Inspector editor for the Taggable component that displays the current tags in real-time during play mode. You can also add a Debug.Log statement inside AddTag and RemoveTag to print whenever tags change. For more advanced debugging, create a debug UI panel that shows all active tags in the TagManager's database and how many objects have each tag. I always build these debugging tools early in development—they save hours of troubleshooting later.