Relationships
Define how entities relate to each other with first-class relationships.
Overview
Relationships let you create associations between entities. Use them for parent-child hierarchies, inventory systems, targeting, and more. A relationship connects a subject entity to a target entity.
Data Storage
Auto Cleanup
Exclusive Mode
const ChildOf = createRelation()
const parent = addEntity(world)
const child = addEntity(world)
// child is the SUBJECT, parent is the TARGET
addComponent(world, child, ChildOf(parent))
// Query: find all children of parent
const children = query(world, [ChildOf(parent)]) // [child]Creating Relations
Create relations with createRelation(). You can use an options object or composable modifiers:
import { createRelation, withStore, makeExclusive, withAutoRemoveSubject } from 'bitecs'
// Simple relation with no data
const ChildOf = createRelation()
// Relation with data storage
const Contains = createRelation(
withStore(() => ({ amount: [] as number[] }))
)
// Using options object syntax
const Contains = createRelation({
store: () => ({ amount: [] as number[] })
})
// Exclusive relation (one target per subject)
const Targeting = createRelation(makeExclusive)
// Auto-remove subject when target is removed
const ChildOf = createRelation(withAutoRemoveSubject)Adding Relations
Add relations using addComponent() with the relation called as a function on the target:
import { addEntity, addComponent, getRelationTargets } from 'bitecs'
const inventory = addEntity(world)
const gold = addEntity(world)
const silver = addEntity(world)
// Add relations - inventory contains gold and silver
addComponent(world, inventory, Contains(gold))
addComponent(world, inventory, Contains(silver))
// Set relation data
Contains(gold).amount[inventory] = 100
Contains(silver).amount[inventory] = 50
// Get all targets for a subject
const items = getRelationTargets(world, inventory, Contains)
console.log(items) // [gold, silver]Think of Contains(sword) as a component type. Each unique target creates a unique component. The relation itself (Contains) is a factory that produces these target-specific components.
Relation Options
Relations can be customized with modifiers (composable functions) or an options object:
| Modifier | Option | Description |
|---|---|---|
withStore(fn) | store: fn | Attach data to relation instances |
makeExclusive | exclusive: true | One target per subject |
withAutoRemoveSubject | autoRemoveSubject: true | Remove subject when target removed |
withOnTargetRemoved(fn) | onTargetRemoved: fn | Callback when target removed |
withStore / store
Attach data to each relationship instance:
const Likes = createRelation(
withStore(() => ({
intensity: [] as number[],
since: [] as number[]
}))
)
addComponent(world, alice, Likes(bob))
Likes(bob).intensity[alice] = 10
Likes(bob).since[alice] = Date.now()makeExclusive / exclusive
Ensure each subject only relates to one target:
const Targeting = createRelation(makeExclusive)
// or
const Targeting = createRelation({ exclusive: true })
addComponent(world, hero, Targeting(rat))
addComponent(world, hero, Targeting(goblin)) // Removes Targeting(rat)
hasComponent(world, hero, Targeting(rat)) // false
hasComponent(world, hero, Targeting(goblin)) // truewithAutoRemoveSubject / autoRemoveSubject
Remove the subject when its target is removed:
const ChildOf = createRelation(withAutoRemoveSubject)
// or
const ChildOf = createRelation({ autoRemoveSubject: true })
const parent = addEntity(world)
const child = addEntity(world)
addComponent(world, child, ChildOf(parent))
removeEntity(world, parent)
entityExists(world, child) // false - child was auto-removedwithOnTargetRemoved / onTargetRemoved
Custom callback when a target is removed:
const Owns = createRelation(
withOnTargetRemoved((world, subject, target) => {
console.log(`Entity ${subject} lost ownership of ${target}`)
})
)
// or options syntax
const Owns = createRelation({
onTargetRemoved: (world, subject, target) => {
// Handle cleanup
}
})Combining Modifiers
Chain multiple modifiers together:
const Equipped = createRelation(
makeExclusive, // One item per slot
withStore(() => ({
slot: [] as string[] // Which slot: 'head', 'weapon', etc.
})),
withOnTargetRemoved((world, subject, target) => {
console.log(`Unequipped item ${target} from entity ${subject}`)
})
)Querying Relations
Query for entities with specific relationships:
// Find all entities that are children of 'parent'
const children = query(world, [ChildOf(parent)])
// Find all entities that contain 'gold'
const goldOwners = query(world, [Contains(gold)])
// Find all entities that have any Contains relation
const containers = query(world, [Contains('*')])Wildcards
Use wildcards to match any target or find entities by relation type:
import { Wildcard } from 'bitecs'
// Find all entities with ANY Contains relation
query(world, [Contains('*')])
query(world, [Contains(Wildcard)])
// Find all entities that are contained BY any entity
query(world, [Wildcard(Contains)])
// Inverted: find all entities related to 'earth' by any relation
const earth = addEntity(world)
addComponent(world, earth, OrbitedBy(moon))
addComponent(world, earth, IlluminatedBy(sun))
query(world, [Wildcard(earth)]) // Entities related to earth| Pattern | Matches |
|---|---|
Contains('*') | Entities with Contains(anything) |
Contains(Wildcard) | Same as above |
Wildcard(Contains) | Targets of any Contains relation |
Wildcard(entity) | Entities with any relation to entity |
Hierarchy Queries
Use Hierarchy() for ordered traversal of hierarchical relationships. It ensures parents are processed before children—essential for transform hierarchies where a child's world position depends on its parent's.
import { Hierarchy, getHierarchyDepth, getMaxHierarchyDepth } from 'bitecs'
const ChildOf = createRelation(withAutoRemoveSubject)
// Query entities in topological order (parents before children)
for (const eid of query(world, [Transform, Hierarchy(ChildOf)])) {
// Safe to read parent's world transform - it's already updated
updateWorldTransform(eid)
}
// Query at specific depth
query(world, [Hierarchy(ChildOf, 0)]) // Root entities (no parent)
query(world, [Hierarchy(ChildOf, 1)]) // Depth 1 (direct children of root)
query(world, [Hierarchy(ChildOf, 2)]) // Depth 2
// Get depth information
const depth = getHierarchyDepth(world, entity, ChildOf)
const maxDepth = getMaxHierarchyDepth(world, ChildOf)ChildOf with withAutoRemoveSubject. Query with Hierarchy(ChildOf) for topological ordering.Common Patterns
Parent-Child Hierarchy
const ChildOf = createRelation(withAutoRemoveSubject)
// Create hierarchy
const scene = addEntity(world)
const player = addEntity(world)
const weapon = addEntity(world)
addComponent(world, player, ChildOf(scene))
addComponent(world, weapon, ChildOf(player))
// Get parent
const [parent] = getRelationTargets(world, weapon, ChildOf)
// Get children
const children = query(world, [ChildOf(player)])
// Removing scene removes player and weapon too!
removeEntity(world, scene)Inventory System
const ContainedIn = createRelation(
withStore(() => ({
slot: [] as number[],
count: [] as number[]
}))
)
const bag = addEntity(world)
const potion = addEntity(world)
addComponent(world, potion, ContainedIn(bag))
ContainedIn(bag).slot[potion] = 0
ContainedIn(bag).count[potion] = 5
// Get all items in bag
const items = query(world, [ContainedIn(bag)])Team/Faction System
const MemberOf = createRelation(makeExclusive) // One team at a time
const redTeam = addEntity(world)
const blueTeam = addEntity(world)
const player = addEntity(world)
addComponent(world, player, MemberOf(redTeam))
// Switch teams (exclusive removes old relation)
addComponent(world, player, MemberOf(blueTeam))
// Get all team members
const blueMembers = query(world, [MemberOf(blueTeam)])Combat Targeting
const Targeting = createRelation(
makeExclusive, // Only one target at a time
withOnTargetRemoved((world, attacker, target) => {
// Target died, clear targeting state
removeComponent(world, attacker, InCombat)
})
)
// Start targeting
addComponent(world, hero, Targeting(enemy))
// Switch target (auto-removes old)
addComponent(world, hero, Targeting(newEnemy))
// Get current target
const [target] = getRelationTargets(world, hero, Targeting)