jecs/docs/learn/observers.md
2025-02-25 05:04:54 +03:00

7.6 KiB

Observer APIs

jecs provides observer hooks that allow you to respond to component lifecycle events and implement cleanup policies.

Component Lifecycle Hooks

Component lifecycle hooks let you execute code when components are added, removed, or modified.

OnAdd

The OnAdd hook is called when a component is added to an entity.

-- Define a component
local Transform = world:component()

-- Set an OnAdd hook for the Transform component
world:set(Transform, jecs.OnAdd, function(entity)
  print("Transform component added to entity", entity)
end)

-- The hook will be called when Transform is added to any entity
local entity = world:entity()
world:add(entity, Transform) -- OnAdd hook is triggered

TypeScript signature:

type OnAddHook = (entity: Entity) => void;

OnRemove

The OnRemove hook is called when a component is removed from an entity.

-- Set an OnRemove hook for the Transform component
world:set(Transform, jecs.OnRemove, function(entity)
  print("Transform component removed from entity", entity)
end)

-- The hook will be called when Transform is removed from any entity
world:remove(entity, Transform) -- OnRemove hook is triggered

TypeScript signature:

type OnRemoveHook = (entity: Entity) => void;

OnSet

The OnSet hook is called when a component's value is set or changed on an entity.

-- Set an OnSet hook for the Transform component
world:set(Transform, jecs.OnSet, function(entity, value)
  print("Transform component set on entity", entity, "with value", value)
end)

-- The hook will be called when Transform's value is set on any entity
world:set(entity, Transform, { position = {x = 10, y = 20} }) -- OnSet hook is triggered

TypeScript signature:

type OnSetHook<T> = (entity: Entity, value: T) => void;

Automatic Cleanup Policies

jecs provides automatic cleanup policies through the OnDelete and OnDeleteTarget hooks paired with action components.

OnDelete

The OnDelete trait specifies what happens when a component or entity is deleted. It must be paired with an action component like Delete or Remove.

(OnDelete, Delete)

When paired with Delete, this policy deletes entities that have the component when the component itself is deleted.

-- Define a component
local Health = world:component()

-- Add the (OnDelete, Delete) cleanup policy to Health
world:add(Health, jecs.pair(jecs.OnDelete, jecs.Delete))

-- Create entities
local entity1 = world:entity()
local entity2 = world:entity()

-- Add Health component to entities
world:add(entity1, Health)
world:add(entity2, Health)

-- When Health component is deleted, all entities with Health will be deleted
world:delete(Health) -- Deletes entity1 and entity2

(OnDelete, Remove)

When paired with Remove, this policy removes a component from entities when the component itself is deleted.

-- Define components
local Poison = world:component()
local PoisonEffect = world:component()

-- Add the (OnDelete, Remove) cleanup policy to Poison
world:add(Poison, jecs.pair(jecs.OnDelete, jecs.Remove))
world:add(PoisonEffect, jecs.pair(jecs.OnDelete, jecs.Remove))

-- Create entity
local entity = world:entity()
world:add(entity, Poison)
world:add(entity, PoisonEffect)

-- When Poison component is deleted, PoisonEffect will be removed from all entities
world:delete(Poison) -- Removes PoisonEffect from entity

OnDeleteTarget

The OnDeleteTarget trait specifies what happens when a relationship target is deleted. It must be paired with an action component like Delete or Remove.

(OnDeleteTarget, Delete)

When paired with Delete, this policy deletes entities that have a relationship with the deleted target.

-- Define a ChildOf component for parent-child relationships
local ChildOf = world:component()

-- Add the (OnDeleteTarget, Delete) cleanup policy
world:add(ChildOf, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))

-- Create parent and child entities
local parent = world:entity()
local child1 = world:entity()
local child2 = world:entity()

-- Establish parent-child relationships
world:add(child1, jecs.pair(ChildOf, parent))
world:add(child2, jecs.pair(ChildOf, parent))

-- When the parent is deleted, all its children will be deleted too
world:delete(parent) -- Deletes child1 and child2

(OnDeleteTarget, Remove)

When paired with Remove, this policy removes a relationship component when its target is deleted.

-- Define a Likes component for relationships
local Likes = world:component()

-- Add the (OnDeleteTarget, Remove) cleanup policy
world:add(Likes, jecs.pair(jecs.OnDeleteTarget, jecs.Remove))

-- Create entities
local bob = world:entity()
local alice = world:entity()
local charlie = world:entity()

-- Establish relationships
world:add(bob, jecs.pair(Likes, alice))
world:add(charlie, jecs.pair(Likes, alice))

-- When alice is deleted, all Likes relationships targeting her will be removed
world:delete(alice) -- Removes Likes relationship from bob and charlie

Best Practices

  1. Use hooks for reactive logic: Component hooks are perfect for synchronizing game state with visual representations or external systems.

  2. Keep hook logic simple: Hooks should be lightweight and focused on a single concern.

  3. Consider cleanup policies carefully: The right cleanup policies can prevent memory leaks and simplify your codebase by automating entity management.

  4. Avoid infinite loops: Be careful not to create circular dependencies with your hooks and cleanup policies.

  5. Document your policies: When using cleanup policies, document them clearly so other developers understand the entity lifecycle in your application.

  6. Use OnAdd for initialization: The OnAdd hook is ideal for initializing component-specific resources.

  7. Use OnRemove for cleanup: The OnRemove hook ensures resources are properly released when components are removed.

  8. Use OnSet for synchronization: The OnSet hook helps keep different aspects of your game in sync when component values change.

Example: Complete Entity Lifecycle

This example shows how to use all observer hooks together to manage an entity's lifecycle:

local world = jecs.World.new()
local pair = jecs.pair

-- Define components
local Model = world:component()
local Transform = world:component()
local ChildOf = world:component()

-- Set up component hooks
world:set(Model, jecs.OnAdd, function(entity)
    print("Model added to entity", entity)
    -- Create visual representation
end)

world:set(Model, jecs.OnRemove, function(entity)
    print("Model removed from entity", entity)
    -- Destroy visual representation
end)

world:set(Model, jecs.OnSet, function(entity, model)
    print("Model set on entity", entity, "with value", model)
    -- Update visual representation
end)

-- Set up cleanup policies
world:add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete))
world:add(Transform, pair(jecs.OnDelete, jecs.Remove))

-- Create entities
local parent = world:entity()
local child = world:entity()

-- Set up relationships and components
world:add(child, pair(ChildOf, parent))
world:set(child, Model, "cube")
world:set(child, Transform, {position = {x = 0, y = 0, z = 0}})

-- Updating a component triggers the OnSet hook
world:set(child, Model, "sphere")

-- Removing a component triggers the OnRemove hook
world:remove(child, Model)

-- Deleting the parent triggers the OnDeleteTarget cleanup policy
-- which automatically deletes the child
world:delete(parent)

By effectively using observer hooks and cleanup policies, you can create more maintainable and robust ECS-based applications with less manual resource management.