first comit

This commit is contained in:
Sebastian Bularca
2026-04-21 00:33:52 +02:00
parent 505b2ca161
commit 0331d0ede9
53 changed files with 3250 additions and 15 deletions

424
README.md
View File

@@ -1,3 +1,423 @@
# unity-tag-system
# Jovian Tag System
A strong-typed tag system for various unity systems
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
- [Installation](#installation)
- [Quick Start](#quick-start)
- [**Important: Tags Are Not Inherited**](#important-tags-are-not-inherited)
- [Hierarchical Tags](#hierarchical-tags)
- [Tag Editor Window](#tag-editor-window)
- [Using Tags in Code](#using-tags-in-code)
- [Inspector Integration](#inspector-integration)
- [Tag Picker Popup](#tag-picker-popup)
- [Code Generation](#code-generation)
- [Tag Containers](#tag-containers)
- [Tag Containers with Data](#tag-containers-with-data)
- [Runtime API Reference](#runtime-api-reference)
- [Performance](#performance)
## 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:
```csharp
// 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
```csharp
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?"
```csharp
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?"
```csharp
damage.IsAncestorOf(fire); // true
damage.IsAncestorOf(aoe); // true
fire.IsAncestorOf(damage); // false
```
**IsSiblingTo** — "Do these tags share the same parent?"
```csharp
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:
```csharp
// Does this entity have ANY damage tag?
if (entity.tags.ContainsDescendantOf(GameTags.Damage.Damage_Tag)) {
ApplyDamageEffect();
}
```
**Specific matching** — check for an exact tag:
```csharp
if (entity.tags.Contains(GameTags.Damage.Fire_Tag)) {
ApplyBurnEffect();
}
```
**Resistance system** — use hierarchy for type matching:
```csharp
// 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:
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
// 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:
```csharp
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