Skip to content

Implement world:targets() as a valid method#311

Merged
Ukendio merged 17 commits into
Ukendio:mainfrom
kurokuukyo:main
Apr 21, 2026
Merged

Implement world:targets() as a valid method#311
Ukendio merged 17 commits into
Ukendio:mainfrom
kurokuukyo:main

Conversation

@kurokuukyo

@kurokuukyo kurokuukyo commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Brief Description of your Changes.

Closes #309.
This PR introduces the world:targets() syntax described in that issue.

An an example, assuming the following scenario:

local world = jecs.world()

local Alice = world:entity()
local Bob = world:entity()

local Likes = world:entity()

world:set(Alice, jecs.Name, "Alice")
world:set(Bob, jecs.Name, "Bob")

world:add(Alice, jecs.pair(Likes, Bob))

The question of "Who does Alice like?" could be many people.

The following is valid syntax under this PR:

for target in world:targets(Alice, Likes) do
	print(world:get(target, jecs.Name)) -- Bob
end

The motivation for this change is to reduce the boilerplate for doing the equivalent action with current methods (and hopefully make it a little faster):

local nth = 0
while true do
	local target = world:target(Alice, Likes, nth)
	if not target then
		break
	end
	print(world:get(target, jecs.Name)) -- Bob
end

A real world example of where this can be applied is timers. See this discord message for where I got this.

for e in world:query(pair(Timer, jecs.Wildcard)) do
	local nth = 0
	while true do 
		local t = world:target(e, Timer, nth)
		if not t then 
			break
		end
		local timer = world:get(e, pair(Timer, t))
		timer.time_left -= dt
		nth += 1
	end
end

This can be converted to:

for e in world:query(pair(Timer, jecs.Wildcard)) do
	for t in world:targets(e, Timer) do
		local timer = world:get(e, pair(Timer, t))
		timer.time_left -= dt
	end
end

Impact of your Changes

Initially this PR was meant to reduce the boilerplate required to do this type of operation. On further investigation after implementing unit tests, performance for the new syntax, upon initial benchmarks, shows promising improvements to performance in most cases. The code for this benchmark can be found here.

Since the start of this PR the benchmark has changed in a good way. We were at 115 μs as a 50% and have dropped down to 100μs!

Screenshot 2026-04-20 211729
Old Benchmark Screenshot 2026-04-20 183548

Tests Performed

Unit tests have been added and are passing as of submission of the PR (see changes to tests.luau). Previously mentioned in the motivation section, the benchmarks also show this can be more performant than the current method. Additional unit tests can be added as requested and necessary.

Additional Comments

See: alternative syntax proposal
A comment in the initial issue suggested the following syntax be valid (introduce nth in the iterator):

for target, nth in world:targets(Alice, Likes) do
	print(world:get(target, jecs.Name)) -- Bob
end

I do not see a reason why this should be included. It is fairly trivial to include if code review later on reveals a reason to include it.

@Ukendio Ukendio left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was pretty good! I appreciate that you took the time and also wrote some good unit tests for this.

Comment thread src/jecs.luau Outdated
Comment thread src/jecs.luau Outdated
Comment thread src/jecs.luau Outdated
@kurokuukyo kurokuukyo requested a review from Ukendio April 21, 2026 00:22
@kurokuukyo

Copy link
Copy Markdown
Contributor Author

I believe I got everything you wanted changed. If you see anything else give me a shout and I'll take another look.

@kurokuukyo

kurokuukyo commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

I have since taken a look and realized... you can optimize this a lot further than I thought...

return function(): i53?
	if nth == end_count then
		return nil
	end
	local target = dense_array[sparse_array[ECS_PAIR_SECOND(archetype_types[nth])].dense]
	nth += 1
	return target
end

I got curious and tried getting rid of most of the function calls I could. It turns out this approach still passes all the tests I wrote. If needed I can revert this, but I believe the idea is optimization at all costs, and thus I will leave it here for you to decide if this black magic can stay.

Yes further optimizations by setting nth - 1 and incrementing it then returning the whole expression I imagine is possible (eliminating the target variable altogether), but with the library being under --optimize 2 I doubt it has a meaningful performance gain.

Comment thread src/jecs.luau
@kurokuukyo kurokuukyo requested a review from Ukendio April 21, 2026 11:13
@Ukendio

Ukendio commented Apr 21, 2026

Copy link
Copy Markdown
Owner

LGTM!

@Ukendio Ukendio closed this Apr 21, 2026
@Ukendio Ukendio merged commit e2c56f5 into Ukendio:main Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add world:targets(entity: Id, relation: Id) -> () -> Id

2 participants