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

Attach data to relationship instances with stores.

Auto Cleanup

Automatically remove entities when targets are removed.

Exclusive Mode

Limit subjects to one target at a time.
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:

creating.ts
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:

adding.ts
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]
Relation vs Component

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:

ModifierOptionDescription
withStore(fn)store: fnAttach data to relation instances
makeExclusiveexclusive: trueOne target per subject
withAutoRemoveSubjectautoRemoveSubject: trueRemove subject when target removed
withOnTargetRemoved(fn)onTargetRemoved: fnCallback 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)) // true

withAutoRemoveSubject / 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-removed

withOnTargetRemoved / 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:

querying.ts
// 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:

wildcards.ts
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
PatternMatches
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.

hierarchy.ts
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)
!
Parent-Child Pattern
For hierarchies, use 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)

Next Steps

Continue Learning

  • Observers - React to relationship changes with onAdd/onRemove
  • Prefabs - Use IsA relation for entity inheritance
  • Serialization - Save and load relationship data
  • Examples - See relationships in game patterns