diff --git a/how_to/004_tags.luau b/how_to/004_tags.luau index a50a987..53133fa 100755 --- a/how_to/004_tags.luau +++ b/how_to/004_tags.luau @@ -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 diff --git a/how_to/022_query_caching.luau b/how_to/022_query_caching.luau index 428f959..79d32a2 100755 --- a/how_to/022_query_caching.luau +++ b/how_to/022_query_caching.luau @@ -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 diff --git a/how_to/110_hooks.luau b/how_to/110_hooks.luau index fc75dd6..f6ccc5b 100755 --- a/how_to/110_hooks.luau +++ b/how_to/110_hooks.luau @@ -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. ]] diff --git a/src/jecs.luau b/src/jecs.luau index 7fb45fc..713ee6d 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -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