A set of utilities for Jecs
Find a file
marked 3f8904a496
All checks were successful
Continous Integration / Build (push) Successful in 10s
Continous Integration / Lint (push) Successful in 10s
Continous Integration / Styling (push) Successful in 10s
Continous Integration / Unit Testing (push) Successful in 14s
Release / Build (push) Successful in 10s
Release / Release (push) Successful in 7s
Release / Publish (push) Successful in 19s
Fix release workflow
2025-05-07 01:13:28 +02:00
.forgejo/workflows Fix release workflow 2025-05-07 01:13:28 +02:00
.lune Cleanup & refactor 2025-05-07 00:37:24 +02:00
.zed Cleanup & refactor 2025-05-07 00:37:24 +02:00
assets Cleanup & refactor 2025-05-07 00:37:24 +02:00
lib Cleanup & refactor 2025-05-07 00:37:24 +02:00
test Cleanup & refactor 2025-05-07 00:37:24 +02:00
.gitignore packaging(pesde), tooling(rokit), deps: Fix pesde support, bump tools, bump jecs 2024-12-03 20:17:06 +01:00
.luaurc Cleanup & refactor 2025-05-07 00:37:24 +02:00
default.project.json Cleanup & refactor 2025-05-07 00:37:24 +02:00
jecs.luau Cleanup & refactor 2025-05-07 00:37:24 +02:00
LICENSE Cleanup & refactor 2025-05-07 00:37:24 +02:00
pesde.toml Cleanup & refactor 2025-05-07 00:37:24 +02:00
README.md Cleanup & refactor 2025-05-07 00:37:24 +02:00
rokit.toml Cleanup & refactor 2025-05-07 00:37:24 +02:00
selene.toml Cleanup & refactor 2025-05-07 00:37:24 +02:00
selene_definitions.yaml Initial push 2024-09-21 19:12:15 +02:00
stylua.toml Cleanup & refactor 2025-05-07 00:37:24 +02:00
wally.toml Cleanup & refactor 2025-05-07 00:37:24 +02:00

CI CD License: MIT Wally Pesde

A set of utilities for Jecs

Installation

Hammer is available on pesde @ marked/hammer and Wally @ mark-marks/hammer.

Usage

All utilities that require a Jecs world to function are exposed via a constructor pattern.
For instance, to build a ref:

local ref = hammer.ref(world)

This is the easiest solution for passing a world that doesn't sacrifice readability internally and externally or bind the developer to a Jecs version that hammer is currently using.

collect

A collect collects all arguments fired through the given signal, and exposes an iterator for them.
Its purpose is to interface with signals in ECS code, which ideally should run every frame in a loop.

For instance, take Roblox's RemoteEvents:

local pings = hammer.collect(events.ping)
local function system()
    for _, player, ping in pings do
        events.ping:FireClient(player, "pong!")
    end
end

command_buffer

A command_buffer lets you buffer world commands in order to prevent iterator invalidation.
Iterator invalidation refers to an iterator (e.g. world:query(Component)) becoming unusable due to changes in the underlying data.

To prevent this, command buffers can be used to delay world operations to the end of the current frame:

local command_buffer = hammer.command_buffer(world)

while true do
    step_systems()
    command_buffer.flush()
end

-- Inside a system:
command_buffer.add(entity, component) -- This runs after all of the systems run; no data changes while things are running

ref

A ref allows for storing and getting entities via some form of reference.
This is particularly useful for situations where you reconcile entities into your world from a foreign place, e.g. from across a networking boundary.

local ref = hammer.ref(world)

for id in net.new_entities.iter() do
    local entity = ref(`foreign-{id}`) -- A new entity that can be tracked via a foreign id
end

Refs by default create a new entity if the given value doesn't reference any stored one. In case you want to see if a reference exists, you can find one:

local entity[: Entity?] = ref.find(`my-key`)

Refs can also be deleted. All functions used to a fetch a reference also return a cleanup function:

local entity, destroy_reference = ref(`my-key`)
destroy_reference() -- `entity` still persists in the world, but `my-key` doesn't refer to it anymore.

Refs are automatically cached by world. ref(world) will have the same underlying references as ref(world).
In case you need an unique reference store, you can omit the cache via ref(world, true).

tracker

A tracker keeps a history of all components passed to it, and how to get to their current state in the least amount of commands.
They're great for replicating world state across a networking barrier, as you're able to easily get diffed snapshots and apply them.

local tracker = hammer.tracker(world, ComponentA, ComponentB)

world:set(entity_a, ComponentA, 50)
world:add(entity_b, ComponentB)

-- Says how to give `entity_a` `ComponentA` with the value of `50` and give `entity_b` `ComponentB`.
-- `state()` always tracks from when the tracker was first created.
local state = tracker.state()

-- Same as the above, but now this sets the origin for the next taken snapshot!
local snapshot = tracker.snapshot()

world:remove(entity_b, ComponentB)

-- This now only says to remove `ComponentB` from `entity_b`.
local snapshot_b = tracker.snapshot()

Trackers simplify the state internally. Removals remove all prior commands pertaining to the entity and component pair, adds remove all prior removals, etc.

Trackers are optimized under the hood with lookup tables for arrays, to allow for a constant time operation to check for whether it has a member or not. It can lead to worse memory usage, but makes it faster overall.