Files
2026-05-17 18:49:35 +02:00
..
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00
2026-05-17 18:49:35 +02:00

Jovian Tag System

A hierarchical, strongly-typed tag system for Unity. Define tags as dot-delimited hierarchies (Damage.Fire.AOE), auto-generate type-safe C# constants, and query relationships like ancestor/descendant/sibling at runtime with zero allocations.

Table of Contents

Quick Start

1. Create a Tag Settings asset

Right-click in the Project window > Create > Jovian > Tag System > Tag Settings

2. Add tags

Open the Tag Editor via Jovian > Tag System > Tag Editor...

Add tags like:

Character
Character.Player
Character.Enemy
Character.Enemy.Boss
Damage
Damage.Fire
Damage.Ice
Status
Status.Burning
Status.Frozen

3. Click Save & Generate

This creates a GameTags.cs file with strongly-typed constants:

// Auto-generated
public static partial class GameTags {
    public static class Character {
        public static JovianTag Character_Tag;
        public static class Enemy {
            public static JovianTag Enemy_Tag;
            public static JovianTag Boss_Tag;
        }
        public static JovianTag Player_Tag;
    }
    public static class Damage {
        public static JovianTag Damage_Tag;
        public static JovianTag Fire_Tag;
        public static JovianTag Ice_Tag;
    }
}

4. Use in code

if (projectile.IsDescendantOf(GameTags.Damage.Damage_Tag)) {
    // This is any damage type (Fire, Ice, etc.)
}

Important: Tags Are Not Inherited

TL;DR — Tagging an object with Enemy.Name.Butcher.Size.Large does NOT automatically assign Enemy.Name.Butcher or Enemy to that object.

Each dot-delimited path is a distinct, independent tag. The hierarchy is structural — it enables tree queries — but it does not imply membership.

Concrete Example

Register this tag in the Tag Editor:

Enemy.Name.Butcher.Size.Large

The system creates 5 separate tag definitions (one per segment), parented in the tree:

  • Enemy
  • Enemy.Name
  • Enemy.Name.Butcher
  • Enemy.Name.Butcher.Size
  • Enemy.Name.Butcher.Size.Large

Now tag a game object with Enemy.Name.Butcher.Size.Large. On that object, only that one tag is set. The ancestor tags exist as definitions, but they are not active on the object.

What This Means in Practice

You want to match... Do this
Only Large Butcher Tag object with Enemy.Name.Butcher.Size.Large, query with Contains
Any Butcher (any size) via separate tag Add a second tag Enemy.Name.Butcher to the object
Any Butcher (any size) via hierarchy Tag with Enemy.Name.Butcher.Size.Large, query with ContainsDescendantOf(GameTags.Enemy.Name.Butcher_Tag)

Exact vs Hierarchy Queries

Query Behavior
Contains(tag) Exact match only — matches the literal tag you assigned
ContainsDescendantOf(tag) Matches any descendant of tag, including tag itself
ContainsAncestorOf(tag) Matches any ancestor of tag

The tag itself does not decide matching behavior — the query does. Choose the query type based on your intent: Contains for "this exact thing," ContainsDescendantOf for "anything in this category."


Hierarchical Tags

Tags are organized in a tree using dot-delimited names. Each segment creates a level in the hierarchy.

Damage              ← root tag (depth 0)
├── Damage.Fire     ← child of Damage (depth 1)
│   └── Damage.Fire.AOE  ← child of Fire (depth 2)
├── Damage.Ice      ← child of Damage (depth 1)
└── Damage.Lightning ← child of Damage (depth 1)

Hierarchy Queries

Every tag knows its parent. This enables three types of queries:

IsDescendantOf — "Is this tag a child/grandchild/etc. of another?"

var fire = GameTags.Damage.Fire_Tag;
var damage = GameTags.Damage.Damage_Tag;
var aoe = GameTags.Damage.Fire.AOE_Tag;

fire.IsDescendantOf(damage);    // true — Fire is under Damage
aoe.IsDescendantOf(damage);     // true — AOE is under Damage (via Fire)
aoe.IsDescendantOf(fire);       // true — AOE is directly under Fire
damage.IsDescendantOf(fire);    // false — Damage is above Fire
fire.IsDescendantOf(fire);      // true — a tag is a descendant of itself

IsAncestorOf — "Is this tag a parent/grandparent/etc. of another?"

damage.IsAncestorOf(fire);      // true
damage.IsAncestorOf(aoe);       // true
fire.IsAncestorOf(damage);      // false

IsSiblingTo — "Do these tags share the same parent?"

var fire = GameTags.Damage.Fire_Tag;
var ice = GameTags.Damage.Ice_Tag;
var player = GameTags.Character.Player_Tag;

fire.IsSiblingTo(ice);           // true — both under Damage
fire.IsSiblingTo(player);        // false — different parents

Practical Use Cases

Category matching — check if something belongs to a broad category:

// Does this entity have ANY damage tag?
if (entity.tags.ContainsDescendantOf(GameTags.Damage.Damage_Tag)) {
    ApplyDamageEffect();
}

Specific matching — check for an exact tag:

if (entity.tags.Contains(GameTags.Damage.Fire_Tag)) {
    ApplyBurnEffect();
}

Resistance system — use hierarchy for type matching:

// Entity is immune to all fire damage (Fire, Fire.AOE, etc.)
if (incomingDamage.IsDescendantOf(GameTags.Damage.Fire_Tag) && entity.HasResistance(GameTags.Damage.Fire_Tag)) {
    return 0;
}

Tag Editor Window

Open via Jovian > Tag System > Tag Editor...

Features

  • Hierarchical tree view — tags displayed as an expandable/collapsible tree
  • Add root tags — type a name in the "New Tag" field at the bottom and click "+ Add"
  • Add child tags — click the green + button on any tag to add a child under it
  • Delete tags — click the red button. If the tag has children, you'll be asked to confirm deletion of the entire branch
  • Search — filter tags by name
  • Save & Generate — saves all changes and regenerates the C# constants file
  • Vertical indent guides — visual lines showing parent-child relationships

Adding hierarchical tags

You can type full paths in the "New Tag" field:

Damage.Fire.AOE

This automatically creates Damage and Damage.Fire as parents if they don't exist.

Or use the + button on an existing tag to add a child — a popup asks for just the child name.

Validation

Tags must:

  • Start with a letter
  • Contain only letters, digits, or underscores
  • Not be a C# reserved keyword (class, int, static, etc.)

Invalid names are rejected with an error message. Duplicates are detected and shown as warnings.

Using Tags in Code

Generated Constants

After clicking Save & Generate, use the generated constants:

using Jovian.TagSystem;

public class Projectile : MonoBehaviour {
    public void OnHit(JovianTag targetTag) {
        if (targetTag.IsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) {
            DealDamage();
        }
    }
}

JovianTag Struct

The core type — 8 bytes, zero heap allocation:

JovianTag tag = GameTags.Damage.Fire_Tag;

tag.IsValid();                    // true (not the empty/None tag)
tag.IsNone();                     // false
tag.Id;                           // unique int identifier
tag.ParentId;                     // parent's int identifier (0 = root)
tag.ToString();                   // "Damage.Fire" (when manager is initialized)
tag.IsDescendantOf(otherTag);     // hierarchy query
tag.IsAncestorOf(otherTag);       // hierarchy query
tag.IsSiblingTo(otherTag);        // same parent check
tag == otherTag;                  // equality by ID

Inspector Integration

JovianTagsGroup

Use JovianTagsGroup on any MonoBehaviour or ScriptableObject to select tags in the inspector:

public class Enemy : MonoBehaviour {
    public JovianTagsGroup tags;

    private void Start() {
        // Check tags at runtime
        if (tags.ContainsDescendantOf(GameTags.Character.Enemy.Enemy_Tag)) {
            Debug.Log("This is an enemy!");
        }

        // Convert to a runtime container for efficient repeated queries
        var container = tags.ToContainer();
    }
}

Inspector Display

  • Collapsible — shows Tags (3) when collapsed, full list when expanded
  • Tag list — each tag shown with a red ✕ remove button
  • + Add Tags button — opens the tag picker popup
  • Edit Tags button — opens the Tag Editor window

Tag Picker Popup

The picker popup opens when you click + Add Tags in the inspector. Features:

  • Hierarchical tree with checkboxes — check/uncheck any tag at any depth
  • Search — filter by name
  • +all / -all buttons — bulk select/deselect all children of a parent tag
  • Already-selected tags are pre-checked when the popup opens
  • Confirm / Cancel — apply or discard the selection
  • Edit Tags button — jump to the Tag Editor window

Code Generation

How It Works

The Tag Editor generates a C# file with:

  1. Nested static classes mirroring the tag hierarchy
  2. Static JovianTag fields for each tag (postfixed with _Tag)
  3. [RuntimeInitializeOnLoadMethod] that registers all tags and resolves the fields at startup
  4. [InitializeOnLoadMethod] (editor-only) for editor play mode

Manual Edits

You can add fields by hand to the generated file. They will be preserved on regeneration as long as the file compiles.

Regenerate Without the Window

Use Jovian > Tag System > Regenerate Tag Constants to regenerate from the menu without opening the Tag Editor.

Settings Inspector

The JovianTagsSettings ScriptableObject inspector has:

  • Default array editor for direct tag editing
  • Save & Generate Tags button
  • Open Tag Editor button
  • Validation errors shown inline (invalid names, duplicates)

Tag Containers

JovianTagsContainer (tags only)

A runtime collection for holding and querying multiple tags:

var container = new JovianTagsContainer(4);
container.Add(GameTags.Damage.Fire_Tag);
container.Add(GameTags.Status.Burning_Tag);

container.Contains(GameTags.Damage.Fire_Tag);                  // true
container.ContainsDescendantOf(GameTags.Damage.Damage_Tag);    // true
container.ContainsAncestorOf(GameTags.Damage.Fire.AOE_Tag);    // true
container.ContainsSibling(GameTags.Damage.Ice_Tag);            // true (Fire and Ice are siblings)
container.Count;                                                // 2

container.Remove(GameTags.Damage.Fire_Tag);
container.Clear();

From JovianTagsGroup

Convert a serialized selection to a runtime container:

public JovianTagsGroup selectedTags;

void Start() {
    var container = selectedTags.ToContainer();
    // Use container for efficient queries
}

Tag Containers with Data

JovianTagsContainer<T> (tags + values)

Pair tags with typed data — like a dictionary with hierarchy-aware queries:

// Damage resistances
var resistances = new JovianTagsContainer<float>(4);
resistances.Add(GameTags.Damage.Fire_Tag, 0.5f);    // 50% fire resistance
resistances.Add(GameTags.Damage.Ice_Tag, 0.75f);     // 75% ice resistance

// Exact lookup
if (resistances.TryGetValue(GameTags.Damage.Fire_Tag, out float fireRes)) {
    Debug.Log($"Fire resistance: {fireRes}");  // 0.5
}

// Hierarchy query — do I resist ANY damage type?
resistances.ContainsDescendantOf(GameTags.Damage.Damage_Tag);  // true

// Get all resistances under a parent
var results = new List<TagEntry<float>>();
resistances.GetByAncestor(GameTags.Damage.Damage_Tag, results);
// results contains Fire(0.5) and Ice(0.75)

TagEntry<T>

Each entry in a generic container:

TagEntry<float> entry = ...;
entry.Tag;                              // the JovianTag
entry.Value;                            // the float value
entry.IsDescendantOf(someAncestor);     // hierarchy query on the tag
entry.Is(someTag);                      // exact match

Runtime API Reference

JovianTagsHandler (static)

The central tag registry. Initialized automatically by the generated code.

Method Description
Initialize() Reset and initialize the registry
RegisterTag(string) Register a dot-delimited tag and all parents
GetTag(string) Get tag by full name
GetTag(int) Get tag by ID
TryGetGameTag(string, out JovianTag) Safe lookup by name
TagToString(JovianTag) Get the full name of a tag
DisplayName(string) Get the last segment ("Damage.Fire""Fire")
IsInitialized Whether the registry is ready

JovianTag (struct, 8 bytes)

Member Description
Id Unique integer identifier
ParentId Parent's ID (0 = root)
IsValid() Not the empty tag
IsNone() Is the empty tag
IsDescendantOf(tag) Hierarchy: child/grandchild check
IsAncestorOf(tag) Hierarchy: parent/grandparent check
IsSiblingTo(tag) Same parent check
==, !=, Equals() Equality by ID

JovianTagsGroup (serializable struct)

Member Description
tags string[] of tag names (serialized)
ToContainer() Resolve to JovianTagsContainer
Contains(tag) Check if any tag matches
ContainsDescendantOf(tag) Hierarchy query
ContainsAncestorOf(tag) Hierarchy query
HasAny() True if any tags selected

Performance

  • JovianTag — 8 bytes (int id + int parentId), no heap allocation
  • Hierarchy queries — O(depth) parent chain walk via JovianTagsHandler.GetTag(int) dictionary lookup. Typical depth is 1-4 hops.
  • Equality — O(1) integer comparison
  • Container queries — O(n) linear scan, optimal for typical small tag counts (<10 per entity)
  • ToString — uses cached static StringBuilder to avoid per-call allocation
  • Registration — uses ReadOnlySpan<char> for segment parsing to minimize string allocations