String Tags
String tags provide runtime-flexible entity tagging that complements the type-safe tag components. Unlike ITagComponent types which require compile-time definitions, string tags can be assigned dynamically at runtime.
When to Use String Tags
Use string tags for scenarios requiring runtime flexibility:
- Designer-driven content tagging - Tags defined in data files or editors
- Serialization - Human-readable tags that persist naturally to JSON
- Dynamic categorization - Tags determined by game logic at runtime
- Editor tooling - Filtering and grouping entities in development tools
- Debugging - Quick categorization without creating new types
Use type-safe tag components (ITagComponent) when:
- Tags are known at compile time
- You want compile-time type checking
- Tags participate in archetype-based queries for maximum performance
Basic Usage
Adding Tags
var enemy = world.Spawn()
.With(new Position { X = 0, Y = 0 })
.Build();
// Add tags after creation
world.AddTag(enemy, "Enemy");
world.AddTag(enemy, "Hostile");
world.AddTag(enemy, "Boss");
Adding Tags During Entity Creation
var enemy = world.Spawn()
.With(new Position { X = 0, Y = 0 })
.WithTag("Enemy") // String tag
.WithTag("Hostile")
.WithTag<EnemyTag>() // Type-safe tag (different overload)
.Build();
Checking Tags
if (world.HasTag(entity, "Boss"))
{
// Special boss handling
}
// Safe with stale entities - returns false instead of throwing
if (world.HasTag(possiblyDeadEntity, "Player"))
{
// Only executed if entity is alive AND has the tag
}
Removing Tags
// Remove a single tag
world.RemoveTag(entity, "Hostile");
// Tags are automatically removed when entity is despawned
world.Despawn(entity);
Getting All Tags
var tags = world.GetTags(entity);
Console.WriteLine($"Entity has {tags.Count} tags:");
foreach (var tag in tags)
{
Console.WriteLine($" - {tag}");
}
Querying by Tag
Simple Tag Query
// Get all entities with a specific tag
foreach (var entity in world.QueryByTag("Enemy"))
{
ref var pos = ref world.Get<Position>(entity);
// Process enemy
}
// Count entities with a tag
int playerCount = world.QueryByTag("Player").Count();
Combining with Component Queries
String tags integrate with the fluent query API:
// Entities with Position AND "Enemy" tag
foreach (var entity in world.Query<Position>().WithTag("Enemy"))
{
ref var pos = ref world.Get<Position>(entity);
// Process positioned enemies
}
// Entities with Position, WITHOUT "Frozen" tag
foreach (var entity in world.Query<Position, Velocity>().WithoutTag("Frozen"))
{
// Process moving entities that aren't frozen
}
// Multiple tag filters
foreach (var entity in world.Query<Position>()
.WithTag("Enemy")
.WithTag("Active")
.WithoutTag("Dead"))
{
// Process active, living enemies
}
Use Cases
Dynamic State Tagging
public class StatusSystem : SystemBase
{
public override void Update(float deltaTime)
{
foreach (var entity in World.Query<Health>())
{
ref var health = ref World.Get<Health>(entity);
// Add/remove tags based on state
if (health.Current <= health.Max * 0.25f)
{
World.AddTag(entity, "LowHealth");
}
else
{
World.RemoveTag(entity, "LowHealth");
}
}
}
}
// Other systems can react to the tag
public class AISystem : SystemBase
{
public override void Update(float deltaTime)
{
// Enemies flee when low on health
foreach (var entity in World.Query<Position, Velocity>()
.WithTag("Enemy")
.WithTag("LowHealth"))
{
// Flee behavior
}
}
}
Data-Driven Tagging
// Load tags from configuration
var entityConfig = JsonSerializer.Deserialize<EntityConfig>(json);
var entity = world.Spawn()
.With(new Position { X = entityConfig.X, Y = entityConfig.Y })
.Build();
// Apply tags from data
foreach (var tag in entityConfig.Tags)
{
world.AddTag(entity, tag);
}
Editor Integration
// In an editor, allow designers to add arbitrary tags
public void OnTagAdded(Entity entity, string newTag)
{
world.AddTag(entity, newTag);
RefreshEntityInspector(entity);
}
// Filter entities in editor by tag
public IEnumerable<Entity> GetEntitiesByEditorFilter(string tagFilter)
{
return world.QueryByTag(tagFilter);
}
Debugging and Diagnostics
// Tag entities during debugging
world.AddTag(suspiciousEntity, "DEBUG_Investigate");
// Find tagged entities later
foreach (var entity in world.QueryByTag("DEBUG_Investigate"))
{
LogEntityState(entity);
}
Tag Validation
Tags must be non-empty strings:
// These throw ArgumentNullException
world.AddTag(entity, null!);
// These throw ArgumentException
world.AddTag(entity, "");
world.AddTag(entity, " ");
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
AddTag |
O(1) | Hash set insertion |
RemoveTag |
O(1) | Hash set removal |
HasTag |
O(1) | Hash set lookup |
GetTags |
O(1) | Returns existing collection |
QueryByTag |
O(N) | N = entities with that tag |
String tags use dual indexing:
- Entity → Tags: O(1) lookup of tags for an entity
- Tag → Entities: O(1) lookup of entities with a tag
Performance Tips
- Prefer type-safe tags for hot paths: Component-based tags benefit from archetype-based iteration
- Reuse tag strings: Consider using constants for frequently-used tags
- Avoid excessive tag changes: Each add/remove updates two indexes
// Good - use constants for common tags
public static class Tags
{
public const string Enemy = "Enemy";
public const string Player = "Player";
public const string Active = "Active";
}
world.AddTag(entity, Tags.Enemy);
Comparison: String Tags vs Component Tags
| Aspect | String Tags | Component Tags |
|---|---|---|
| Definition | Runtime strings | Compile-time types |
| Type safety | None | Full |
| Performance | Good (hash-based) | Best (archetype-based) |
| Serialization | Natural | Requires type resolution |
| Dynamic creation | Yes | No (requires code) |
| Query integration | Yes | Yes |
| IDE support | None | Autocomplete, refactoring |
Example: Hybrid Approach
Combine both tagging systems for flexibility:
// Type-safe tags for core game mechanics
[TagComponent]
public partial struct EnemyTag { }
[TagComponent]
public partial struct PlayerTag { }
// String tags for dynamic/editor-driven features
var boss = world.Spawn()
.With(new Position())
.With(new Health { Current = 500, Max = 500 })
.WithTag<EnemyTag>() // Core gameplay tag
.WithTag("Boss") // Difficulty tier
.WithTag("FireElemental") // Specific type
.WithTag("DropsTreasure") // Loot table flag
.Build();
// Query using component tag (most efficient)
foreach (var entity in world.Query<Position>().With<EnemyTag>())
{
// All enemies
}
// Filter further with string tags
foreach (var entity in world.Query<Position>().With<EnemyTag>().WithTag("Boss"))
{
// Just bosses
}