Component Bundles Guide
Component bundles group commonly-used components together, reducing boilerplate and improving entity creation ergonomics. This guide covers when and how to use bundles.
What Are Bundles?
Bundles are compile-time constructs that bundle multiple components into a single reusable group. They're similar to prefabs but operate at the component level rather than the entity level.
// Without bundles (verbose)
var enemy = world.Spawn()
.With(new Position { X = 0, Y = 0 })
.With(new Rotation { Angle = 0 })
.With(new Scale { X = 1, Y = 1 })
.With(new Health { Current = 100, Max = 100 })
.With(new Damage { Amount = 10 })
.WithTag<EnemyTag>()
.Build();
// With bundles (concise)
var enemy = world.Spawn()
.With(new TransformBundle(x: 0, y: 0))
.With(new ActorBundle(health: 100, damage: 10))
.WithTag<EnemyTag>()
.Build();
Defining Bundles
Use the [Bundle] attribute on a partial struct:
using KeenEyes.Generators.Attributes;
[Bundle]
public partial struct TransformBundle
{
public Position Position;
public Rotation Rotation;
public Scale Scale;
}
[Bundle]
public partial struct ActorBundle
{
public Health Health;
public Damage Damage;
public Named Name;
}
Bundle Requirements
- Struct type - Bundles must be structs (value types)
- Partial - Must be declared
partialfor source generation - Component fields - Fields must be components (types implementing
IComponent) - At least one field - Empty bundles are not allowed
Generated Code
The source generator creates several helpers for each bundle:
1. Constructor
// Generated for TransformBundle
public TransformBundle(Position position, Rotation rotation, Scale scale)
{
Position = position;
Rotation = rotation;
Scale = scale;
}
2. EntityBuilder Extension
// Generated: With(bundle) extension method
public static EntityBuilder With(this EntityBuilder builder, TransformBundle bundle)
{
return builder
.With(bundle.Position)
.With(bundle.Rotation)
.With(bundle.Scale);
}
3. World Add/Remove Extensions
// Generated: AddTransformBundle extension method
public static void AddTransformBundle(this World world, Entity entity,
Position position, Rotation rotation, Scale scale)
{
world.Add(entity, position);
world.Add(entity, rotation);
world.Add(entity, scale);
}
// Generated: RemoveTransformBundle extension method
public static void RemoveTransformBundle(this World world, Entity entity)
{
world.Remove<Position>(entity);
world.Remove<Rotation>(entity);
world.Remove<Scale>(entity);
}
4. Bundle Queries
// Query all entities with all bundle components
foreach (var entity in world.Query<TransformBundle>())
{
ref var transform = ref world.GetBundle(entity, default(TransformBundle));
// transform.Position, transform.Rotation, transform.Scale are all refs
}
5. GetBundle Method
// Generated: Returns a ref struct with refs to all components
public ref struct TransformBundleRef
{
public ref Position Position;
public ref Rotation Rotation;
public ref Scale Scale;
}
// Zero-copy access to all bundle components
var transform = world.GetBundle(entity, default(TransformBundle));
transform.Position.X += 10; // Direct modification
Common Bundle Patterns
Transform Bundle
The most common bundle for spatial data:
[Component]
public partial struct Position { public float X, Y; }
[Component]
public partial struct Rotation { public float Angle; }
[Component]
public partial struct Scale { public float X, Y; }
[Bundle]
public partial struct TransformBundle
{
public Position Position;
public Rotation Rotation;
public Scale Scale;
}
// Usage
var entity = world.Spawn()
.With(new TransformBundle
{
Position = new() { X = 100, Y = 50 },
Rotation = new() { Angle = 0 },
Scale = new() { X = 1, Y = 1 }
})
.Build();
Physics Bundle
Group physics-related components:
[Bundle]
public partial struct PhysicsBundle
{
public Position Position;
public Velocity Velocity;
public Collider Collider;
public RigidBody RigidBody;
}
// Usage
var dynamicObject = world.Spawn()
.With(new PhysicsBundle
{
Position = new() { X = 0, Y = 0 },
Velocity = new() { X = 10, Y = 0 },
Collider = new() { Radius = 5 },
RigidBody = new() { Mass = 1.0f }
})
.Build();
Character Bundle
Common components for game characters:
[Bundle]
public partial struct CharacterBundle
{
public Position Position;
public Health Health;
public Mana Mana;
public Inventory Inventory;
}
// Usage
var player = world.Spawn()
.With(new CharacterBundle
{
Position = new() { X = 0, Y = 0 },
Health = new() { Current = 100, Max = 100 },
Mana = new() { Current = 50, Max = 50 },
Inventory = new() { Capacity = 20 }
})
.WithTag<PlayerTag>()
.Build();
Nested Bundles
Bundles can contain other bundles (up to 5 levels deep):
[Bundle]
public partial struct EntityBundle
{
public TransformBundle Transform; // Nested bundle
public PhysicsBundle Physics; // Nested bundle
public SpriteRenderer Sprite;
}
// Usage
var entity = world.Spawn()
.With(new EntityBundle
{
Transform = new()
{
Position = new() { X = 0, Y = 0 },
Rotation = new() { Angle = 0 },
Scale = new() { X = 1, Y = 1 }
},
Physics = new()
{
Position = new() { X = 0, Y = 0 },
Velocity = new() { X = 5, Y = 0 },
Collider = new() { Radius = 10 },
RigidBody = new() { Mass = 2.0f }
},
Sprite = new() { TextureId = "hero.png" }
})
.Build();
Note: Nested bundles are flattened into individual components at compile time. There's no runtime overhead.
Optional Bundle Fields
Mark fields as optional using the [Optional] attribute:
[Bundle]
public partial struct EnemyBundle
{
public Position Position;
public Health Health;
public Damage Damage;
[Optional]
public AI? AI; // Optional - must be nullable
[Optional]
public Loot? Loot; // Optional - must be nullable
}
// Usage - optional fields can be omitted
var basicEnemy = world.Spawn()
.With(new EnemyBundle
{
Position = new() { X = 50, Y = 0 },
Health = new() { Current = 50, Max = 50 },
Damage = new() { Amount = 5 }
// AI and Loot omitted
})
.Build();
var smartEnemy = world.Spawn()
.With(new EnemyBundle
{
Position = new() { X = 100, Y = 0 },
Health = new() { Current = 100, Max = 100 },
Damage = new() { Amount = 10 },
AI = new AI() { BehaviorTree = "smart_enemy" } // Include AI
})
.Build();
Important: Optional fields must be nullable types (T? where T : struct). The generator validates this at compile time.
Working with Bundles
Adding Bundles to Existing Entities
var entity = world.Spawn().Build();
// Add all components from a bundle
world.AddTransformBundle(entity,
position: new() { X = 0, Y = 0 },
rotation: new() { Angle = 0 },
scale: new() { X = 1, Y = 1 }
);
Removing Bundles
// Remove all components from a bundle
world.RemoveTransformBundle(entity);
// Entity no longer has Position, Rotation, or Scale
Querying with Bundles
// Query all entities with all bundle components
foreach (var entity in world.Query<TransformBundle>())
{
// This entity has Position, Rotation, and Scale
var transform = world.GetBundle(entity, default(TransformBundle));
// Zero-copy access to all components
transform.Position.X += 10;
transform.Rotation.Angle += 0.1f;
}
// Mix bundle queries with component queries
foreach (var entity in world.Query<TransformBundle, Velocity>())
{
var transform = world.GetBundle(entity, default(TransformBundle));
ref readonly var velocity = ref world.Get<Velocity>(entity);
transform.Position.X += velocity.X;
transform.Position.Y += velocity.Y;
}
Bundle-Aware Queries with Filters
// Query with bundle + filters
foreach (var entity in world.Query<TransformBundle>()
.With<EnemyTag>()
.Without<DisabledTag>())
{
var transform = world.GetBundle(entity, default(TransformBundle));
// Process active enemies
}
Component Mixins
Mixins allow component composition at compile time using the [Mixin] attribute:
[Component]
public partial struct HealthComponent
{
public int Current;
public int Max;
}
[Component]
[Mixin(typeof(HealthComponent))] // Include all fields from HealthComponent
public partial struct RegeneratingHealth
{
public float RegenRate;
}
// Generated code includes both mixin fields and original fields:
// public partial struct RegeneratingHealth
// {
// // From HealthComponent mixin
// public int Current;
// public int Max;
//
// // Original field
// public float RegenRate;
// }
// Usage
var entity = world.Spawn()
.WithRegeneratingHealth(current: 100, max: 100, regenRate: 5.0f)
.Build();
Multiple Mixins
[Component]
[Mixin(typeof(Position))]
[Mixin(typeof(Rotation))]
public partial struct Transform2D
{
public float Z; // Add Z coordinate to Position + Rotation
}
// Generated struct has X, Y (from Position), Angle (from Rotation), and Z
Mixin Validation
The generator validates mixins at compile time:
- Circular mixin references are detected and rejected
- Maximum nesting depth is 5 levels
- Mixins must be valid component types
- Field name conflicts are reported as errors
Performance Considerations
Zero Runtime Overhead
Bundles are compile-time constructs. At runtime:
- No bundle objects are created
- No reflection is used
- Components are stored individually in archetypes
- Bundle operations compile to the same code as manual component operations
// These two are equivalent at runtime:
world.Spawn().With(new TransformBundle { ... }).Build();
world.Spawn().With(position).With(rotation).With(scale).Build();
Archetype Pre-allocation
Bundles automatically register their component combinations for archetype pre-allocation:
// During World initialization, bundles hint the archetype manager
// to pre-allocate storage for common component combinations
// This reduces archetype migrations when spawning entities
Pre-allocation happens automatically - no manual configuration needed.
When to Use Bundles
| Use Bundles When | Use Manual Components When |
|---|---|
| Spawning many entities with the same components | Adding components one-at-a-time based on conditions |
| Common component groups (Transform, Physics) | Rare or unique component combinations |
| You want convenient spawning syntax | Maximum flexibility is needed |
| Building entity templates | Components are added dynamically |
Best Practices
Keep Bundles Focused
// ✅ Good: Small, focused bundles
[Bundle]
public partial struct TransformBundle
{
public Position Position;
public Rotation Rotation;
public Scale Scale;
}
// ❌ Bad: Bundles with unrelated components
[Bundle]
public partial struct EverythingBundle
{
public Position Position;
public Health Health;
public SpriteRenderer Sprite;
public InputState Input;
public NetworkId NetworkId;
// Too many responsibilities!
}
Use Nested Bundles for Composition
// ✅ Good: Compose larger bundles from smaller ones
[Bundle]
public partial struct CharacterBundle
{
public TransformBundle Transform; // Reuse existing bundle
public CombatBundle Combat; // Reuse existing bundle
public SpriteRenderer Sprite;
}
Name Bundles Descriptively
// ✅ Good: Clear, descriptive names
TransformBundle
PhysicsBundle
CharacterBundle
EnemyBundle
// ❌ Bad: Vague names
DataBundle
EntityStuff
ComponentGroup
Don't Over-Bundle
Not every component combination needs a bundle. Only create bundles for frequently-used patterns.
// If you only spawn one or two entities with these components,
// a bundle might be overkill - just use .With() directly
Bundles vs Prefabs
Bundles and prefabs serve different purposes:
| Bundles | Prefabs |
|---|---|
| Compile-time component grouping | Runtime entity templates |
| Type-safe component combinations | Data-driven entity spawning |
| No default values | Can have default component values |
| Generates code | Registered at runtime |
| Use for common patterns | Use for content creation |
You can combine both:
[Prefab("Characters/Goblin")]
[Bundle]
public partial struct GoblinPrefab
{
public TransformBundle Transform;
public EnemyBundle Enemy;
public AI AI;
}
Common Patterns
Factory Methods with Bundles
public static class EntityFactory
{
public static Entity SpawnEnemy(World world, float x, float y, int health)
{
return world.Spawn()
.With(new TransformBundle
{
Position = new() { X = x, Y = y },
Rotation = new() { Angle = 0 },
Scale = new() { X = 1, Y = 1 }
})
.With(new ActorBundle
{
Health = new() { Current = health, Max = health },
Damage = new() { Amount = 10 },
Name = new() { Value = "Enemy" }
})
.WithTag<EnemyTag>()
.Build();
}
}
Bundle-Based Systems
public class TransformSystem : SystemBase
{
public override void Update(float deltaTime)
{
// Process all entities with TransformBundle
foreach (var entity in World.Query<TransformBundle>())
{
var transform = World.GetBundle(entity, default(TransformBundle));
// Apply transformations
transform.Position.X += transform.Rotation.Angle * deltaTime;
}
}
}
Conditional Bundle Fields
[Bundle]
public partial struct SpawnableBundle
{
public Position Position;
[Optional]
public Velocity? Velocity; // Only if entity should move
[Optional]
public Health? Health; // Only if entity is damageable
}
// Spawn static decoration (no velocity or health)
world.Spawn().With(new SpawnableBundle
{
Position = new() { X = 0, Y = 0 }
}).Build();
// Spawn moving, damageable entity
world.Spawn().With(new SpawnableBundle
{
Position = new() { X = 0, Y = 0 },
Velocity = new Velocity() { X = 5, Y = 0 },
Health = new Health() { Current = 100, Max = 100 }
}).Build();
Troubleshooting
Compiler Error: "Bundle must be a struct"
// ❌ Error
[Bundle]
public partial class TransformBundle { } // Classes not allowed
// ✅ Fix
[Bundle]
public partial struct TransformBundle { }
Compiler Error: "Bundle field must be a component type"
// ❌ Error
[Bundle]
public partial struct MyBundle
{
public string Name; // string is not IComponent
}
// ✅ Fix: Use a component wrapper
[Component]
public partial struct Named { public string Value; }
[Bundle]
public partial struct MyBundle
{
public Named Name; // Now it's a component
}
Compiler Error: "Circular bundle reference detected"
// ❌ Error
[Bundle]
public partial struct BundleA
{
public BundleB B;
}
[Bundle]
public partial struct BundleB
{
public BundleA A; // Circular reference!
}
// ✅ Fix: Remove circular dependency
[Bundle]
public partial struct BundleA
{
public Position Position;
}
[Bundle]
public partial struct BundleB
{
public BundleA A; // One-way reference is fine
public Velocity Velocity;
}
Compiler Error: "Optional field must be nullable"
// ❌ Error
[Bundle]
public partial struct MyBundle
{
[Optional]
public Position Position; // Not nullable
}
// ✅ Fix
[Bundle]
public partial struct MyBundle
{
[Optional]
public Position? Position; // Nullable struct
}
Next Steps
- Components Guide - Component design patterns
- Prefabs Guide - Entity templates
- Queries Guide - Querying entities
- Getting Started - Basic ECS usage