Custom Components
Introduction
Common issues and Best Practices to follow when creating custom Components.Stale References
When your component is passed (world, component)
to add, tick, or remove callbacks, it is not always safe to reference component
in a nested function and use it after the callback has returned.
Incorrect Example
ecs.registerComponent({
name: 'age-counter',
data: {
age: ecs.i32,
interval: ecs.i32,
},
add: (world, component) => {
const interval = world.time.setInterval(() => {
// This is not safe because we're accessing data after some amount of time
// has passed, it's not guaranteed to still be valid.
component.data.age += 1
}, 1000)
// This is safe because we're assigning to data within the add function
component.data.interval = interval
},
tick: (world, component) => {
console.log('I am', component.data.age, 'seconds old')
},
remove: (world, component) => {
world.time.clearTimeout(component.data.interval)
}
})
Correct Example
ecs.registerComponent({
name: 'age-counter',
data: {
age: ecs.i32,
interval: ecs.i32,
},
add: (world, component) => {
const {eid, dataAttribute} = component
const interval = world.time.setInterval(() => {
// This is safe because we're re-acquiring a cursor at the time we need it,
// instead of using a stale cursor from before.
const data = dataAttribute.cursor(eid)
data.age += 1
}, 1000)
component.data.interval = interval
},
tick: (world, component) => {
console.log('I am', component.data.age, 'seconds old')
},
remove: (world, component) => {
// world.time.clearTimeout(component.data.interval)
}
})
In the example above, dataAttribute is used as a stable method to access the component’s data within a nested function. This ensures that data remains valid and up-to-date even when the function is called asynchronously.
Additionally, the eid variable is destructured rather than accessing component.eid directly because component.eid can change depending on which entity is receiving the callback. Using a destructured variable avoids potential stale references.
Of the arguments passed to component callbacks, here is the validity of each:
Always destructure eid before use instead of accessing component.eid, as directly accessing component.eid can lead to stale references.
context | Changes after callback exits? | Can be used in a nested function? | Lifetime |
---|---|---|---|
world | ❌ No | ✅ Yes | Experience Lifetime |
eid | ✅ Yes | ✅ Yes | Entity Lifetime |
schema & data | ✅ Yes | ❌ No | Top level of Callback |
schemaAttribute & dataAttribute | ❌ No | ✅ Yes | Entity Lifetime |
Invalidated Cursors
Cursor objects act as interfaces for reading and writing data in the ECS state. Each time a cursor is requested for a component, the same cursor instance is reused, but it points to a different location in memory. As a result, a cursor reference can become invalid, meaning it may no longer point to the expected data.