mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
Document delete flag in OnRemove hooks
This commit is contained in:
parent
e4b12f4a28
commit
d5c9abc57f
4 changed files with 69 additions and 9 deletions
|
|
@ -16,8 +16,8 @@ local jecs = require("@jecs")
|
|||
--[[
|
||||
There are two ways to create tags:
|
||||
|
||||
1. Using jecs.tag() - preregister a tag entity which will be allocated when you create the world.
|
||||
2. Using world:entity() - creates a regular entity id
|
||||
1. Using jecs.tag() to preregister a tag entity which will be allocated when you create the world.
|
||||
2. Using world:entity() to create a regular entity id
|
||||
|
||||
The first method is the "proper" way to create tags but it hinges upon that
|
||||
you do remember to create the world after declaring all of your
|
||||
|
|
|
|||
|
|
@ -79,6 +79,6 @@ end
|
|||
-- Cached query (faster for repeated use)
|
||||
local cached_query = world:query(Position, Velocity):cached()
|
||||
for entity, pos, vel in cached_query do
|
||||
-- Process entities - this is faster for repeated iterations
|
||||
-- Process entities. This is faster for repeated iterations.
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ world:set(Transform, jecs.OnAdd, function(entity, id, data)
|
|||
print(`Transform added to entity {entity}`)
|
||||
end)
|
||||
|
||||
world:set(Transform, jecs.OnRemove, function(entity, id)
|
||||
world:set(Transform, jecs.OnRemove, function(entity, id, delete)
|
||||
-- A transform component id has been removed from entity
|
||||
-- delete is true if the entity is being deleted, false/nil otherwise
|
||||
print(`Transform removed from entity {entity}`)
|
||||
end)
|
||||
|
||||
|
|
@ -37,9 +38,67 @@ end)
|
|||
|
||||
When an entity graph contains cycles, order is undefined. This includes cycles
|
||||
that can be formed using different relationships.
|
||||
|
||||
However an important note to make is that structural changes are not
|
||||
necessarily always safe in OnRemove hooks. For instance, when an entity is
|
||||
being deleted and invokes all of the OnRemove hooks on its components. It
|
||||
can cause a lot of issues with moving entities
|
||||
]]
|
||||
|
||||
--[[
|
||||
Structural changes in OnRemove hooks
|
||||
|
||||
You can call world:add, world:remove, and world:set inside OnRemove hooks.
|
||||
That's fine. But there's a catch.
|
||||
|
||||
When an entity is being deleted, all of its components get removed. Each
|
||||
removal triggers the OnRemove hook. If you try to make structural changes
|
||||
to the entity during deletion, like removing more components or adding new
|
||||
ones, you're fighting against the deletion process itself. The entity is
|
||||
going to lose all its components anyway, so what's the point?
|
||||
|
||||
This creates a conflict. On one hand, you might want to clean up related
|
||||
components when a specific component is removed. On the other hand, during
|
||||
deletion, you don't want to do that because the entity is already being
|
||||
torn down. So you need a way to tell the difference.
|
||||
|
||||
The solution is the delete boolean. Every OnRemove hook receives it as the
|
||||
third parameter. It's true when the entity is being deleted, and false
|
||||
(or nil) when you're just removing a single component normally.
|
||||
|
||||
So you check it. If delete is true, you bail out early. If it's false,
|
||||
you do your cleanup. Simple.
|
||||
|
||||
Here's what it looks like in practice:
|
||||
]]
|
||||
|
||||
local Health = world:component()
|
||||
local Dead = world:component()
|
||||
|
||||
world:set(Health, jecs.OnRemove, function(entity, id, delete)
|
||||
if delete then
|
||||
-- Entity is being deleted, don't try to clean up
|
||||
return
|
||||
end
|
||||
|
||||
-- Normal removal, do cleanup
|
||||
world:remove(entity, Dead)
|
||||
end)
|
||||
|
||||
--[[
|
||||
The ob.luau module uses this pattern extensively. When you're building
|
||||
observers or monitors that track component removals, you need to distinguish
|
||||
between "component removed" and "entity deleted" because they mean different
|
||||
things for your tracking logic.
|
||||
|
||||
Now, about the DEBUG flag. If you create a world with DEBUG enabled:
|
||||
|
||||
local world = jecs.world(true)
|
||||
|
||||
Then the world will actively prevent you from calling world:add, world:remove,
|
||||
or world:set inside OnRemove hooks when delete is true. It throws an error
|
||||
that tells you exactly what went wrong. This is useful during development
|
||||
to catch cases where you forgot to check the delete flag.
|
||||
|
||||
But here's the important part: even with DEBUG enabled, you're still allowed
|
||||
to call these functions when delete is false. The DEBUG mode only prevents
|
||||
structural changes during deletion, not during normal component removal.
|
||||
|
||||
So the pattern is always the same: check delete, bail if true, proceed if false.
|
||||
The DEBUG flag just makes sure you don't forget to do the check.
|
||||
]]
|
||||
|
|
|
|||
|
|
@ -3748,6 +3748,7 @@ local function world_new(DEBUG: boolean?)
|
|||
DEBUG_DELETING_ENTITY = entity
|
||||
DEBUG_IS_INVALID_ENTITY(entity)
|
||||
canonical_world_delete(world, entity, id)
|
||||
DEBUG_DELETING_ENTITY = nil
|
||||
end
|
||||
world_delete = world_delete_checked
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue