first comit
This commit is contained in:
424
README.md
424
README.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user