Skip to content

Commit a945401

Browse files
authored
Merge pull request #37 from BlackMATov/dev
Dev
2 parents a1423ea + d9c9b4c commit a945401

9 files changed

Lines changed: 790 additions & 113 deletions

File tree

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (C) 2024-2025, by Matvey Cherevko (blackmatov@gmail.com)
3+
Copyright (C) 2024-2026, by Matvey Cherevko (blackmatov@gmail.com)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
- [Deferred Operations](#deferred-operations)
4444
- [Batch Operations](#batch-operations)
4545
- [Systems](#systems)
46+
- [Processing Payloads](#processing-payloads)
4647
- [Predefined Traits](#predefined-traits)
4748
- [Fragment Tags](#fragment-tags)
4849
- [Fragment Hooks](#fragment-hooks)
@@ -60,6 +61,7 @@
6061
- [Chunk](#chunk)
6162
- [Builder](#builder)
6263
- [Changelog](#changelog)
64+
- [v1.7.0](#v170)
6365
- [v1.6.0](#v160)
6466
- [v1.5.0](#v150)
6567
- [v1.4.0](#v140)
@@ -586,16 +588,22 @@ evolved.set(entity, fragment, 42)
586588

587589
One of the most important features of any ECS library is the ability to process entities by filters or queries. `evolved.lua` provides a simple and efficient way to do this.
588590

589-
First, you need to create a query that describes which entities you want to process. You can specify fragments you want to include, and fragments you want to exclude. Queries are just identifiers with a special predefined fragments: [`evolved.INCLUDES`](#evolvedincludes) and [`evolved.EXCLUDES`](#evolvedexcludes). These fragments expect a list of fragments as their components.
591+
First, you need to create a query that describes which entities you want to process. You can specify fragments you want to include, and fragments you want to exclude. Queries are just identifiers with a special predefined fragments: [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), and [`evolved.VARIANTS`](#evolvedvariants). These fragments expect a list of fragments as their components.
592+
593+
- [`evolved.INCLUDES`](#evolvedincludes) is used to specify fragments that must be present in the entity;
594+
- [`evolved.EXCLUDES`](#evolvedexcludes) is used to specify fragments that must not be present in the entity;
595+
- [`evolved.VARIANTS`](#evolvedvariants) is used to specify fragments where at least one must be present in the entity.
590596

591597
```lua
592598
local evolved = require 'evolved'
593599

594600
local health, poisoned, resistant = evolved.id(3)
601+
local alive, undead = evolved.id(2)
595602

596603
local query = evolved.id()
597604
evolved.set(query, evolved.INCLUDES, { health, poisoned })
598605
evolved.set(query, evolved.EXCLUDES, { resistant })
606+
evolved.set(query, evolved.VARIANTS, { alive, undead })
599607
```
600608

601609
The builder interface can be used to create queries too. It is more convenient to use, because the builder has special methods for including and excluding fragments. Here is a simple example of this:
@@ -604,10 +612,11 @@ The builder interface can be used to create queries too. It is more convenient t
604612
local query = evolved.builder()
605613
:include(health, poisoned)
606614
:exclude(resistant)
615+
:variant(alive, undead)
607616
:build()
608617
```
609618

610-
We don't have to set both [`evolved.INCLUDES`](#evolvedincludes) and [`evolved.EXCLUDES`](#evolvedexcludes) fragments, we can even do it without filters at all, then the query will match all chunks in the world.
619+
We don't have to set all of [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), and [`evolved.VARIANTS`](#evolvedvariants) fragments, we can even do it without filters at all, then the query will match all chunks in the world.
611620

612621
After the query is created, we are ready to process our filtered by this query entities. You can do this by using the [`evolved.execute`](#evolvedexecute) function. This function takes a query as an argument and returns an iterator that can be used to iterate over all matching with the query chunks.
613622

@@ -786,7 +795,7 @@ The [`evolved.process`](#evolvedprocess) function is used to process systems. It
786795
function evolved.process(...) end
787796
```
788797

789-
If you don't specify a query for the system, the system itself will be treated as a query. This means the system can contain `evolved.INCLUDES` and `evolved.EXCLUDES` fragments, and it will be processed according to them. This is useful for creating systems with unique queries that don't need to be reused in other systems.
798+
If you don't specify a query for the system, the system itself will be treated as a query. This means the system can contain `evolved.INCLUDES`, `evolved.EXCLUDES`, and `evolved.VARIANTS` fragments, and it will be processed according to them. This is useful for creating systems with unique queries that don't need to be reused in other systems.
790799

791800
```lua
792801
local evolved = require 'evolved'
@@ -880,6 +889,43 @@ The prologue and epilogue fragments do not require an explicit query. They will
880889
> [!NOTE]
881890
> And one more thing about systems. Execution callbacks are called in the [deferred scope](#deferred-operations), which means that all modifying operations inside the callback will be queued and applied after the system has processed all chunks. But prologue and epilogue callbacks are not called in the deferred scope, so all modifying operations inside them will be applied immediately. This is done to avoid confusion and to make it clear that prologue and epilogue callbacks are not part of the chunk processing.
882891
892+
#### Processing Payloads
893+
894+
Additionally, systems can have a payload that will be passed to the execution, prologue, and epilogue callbacks. This is useful for passing additional data to the system without using global variables or closures.
895+
896+
```lua
897+
---@param system evolved.system
898+
---@param ... any processing payload
899+
function evolved.process_with(system, ...) end
900+
```
901+
902+
The [`evolved.process_with`](#evolvedprocess_with) function is similar to the [`evolved.process`](#evolvedprocess) function, but it takes a processing payload as additional arguments. These arguments will be passed to the system's callbacks.
903+
904+
```lua
905+
local evolved = require 'evolved'
906+
907+
local position_x, position_y = evolved.id(2)
908+
local velocity_x, velocity_y = evolved.id(2)
909+
910+
local physics_system = evolved.builder()
911+
:include(position_x, position_y)
912+
:include(velocity_x, velocity_y)
913+
:execute(function(chunk, entity_list, entity_count, delta_time)
914+
local px, py = chunk:components(position_x, position_y)
915+
local vx, vy = chunk:components(velocity_x, velocity_y)
916+
917+
for i = 1, entity_count do
918+
px[i] = px[i] + vx[i] * delta_time
919+
py[i] = py[i] + vy[i] * delta_time
920+
end
921+
end):build()
922+
923+
local delta_time = 0.016
924+
evolved.process_with(physics_system, delta_time)
925+
```
926+
927+
`delta_time` in this example is passed as a processing payload to the system's execution callback. Payloads can be of any type and can be multiple values. Also, payloads are passed to prologue and epilogue callbacks if they are defined. Every subsystem in a group will receive the same payload when the group is processed with [`evolved.process_with`](#evolvedprocess_with).
928+
883929
### Predefined Traits
884930

885931
#### Fragment Tags
@@ -1125,9 +1171,9 @@ storage :: component[]
11251171
default :: component
11261172
duplicate :: {component -> component}
11271173
1128-
execute :: {chunk, entity[], integer}
1129-
prologue :: {}
1130-
epilogue :: {}
1174+
execute :: {chunk, entity[], integer, any...}
1175+
prologue :: {any...}
1176+
epilogue :: {any...}
11311177
11321178
set_hook :: {entity, fragment, component, component}
11331179
assign_hook :: {entity, fragment, component, component}
@@ -1159,6 +1205,7 @@ DISABLED :: fragment
11591205
11601206
INCLUDES :: fragment
11611207
EXCLUDES :: fragment
1208+
VARIANTS :: fragment
11621209
REQUIRES :: fragment
11631210
11641211
ON_SET :: fragment
@@ -1229,6 +1276,7 @@ execute :: query -> {execute_state? -> chunk?, entity[]?, integer?}, execute_sta
12291276
locate :: entity -> chunk?, integer
12301277
12311278
process :: system... -> ()
1279+
process_with :: system, ... -> ()
12321280
12331281
debug_mode :: boolean -> ()
12341282
collect_garbage :: ()
@@ -1292,6 +1340,7 @@ builder_mt:disabled :: builder
12921340
12931341
builder_mt:include :: fragment... -> builder
12941342
builder_mt:exclude :: fragment... -> builder
1343+
builder_mt:variant :: fragment... -> builder
12951344
builder_mt:require :: fragment... -> builder
12961345
12971346
builder_mt:on_set :: {entity, fragment, component, component} -> builder
@@ -1302,16 +1351,21 @@ builder_mt:on_remove :: {entity, fragment} -> builder
13021351
builder_mt:group :: system -> builder
13031352
13041353
builder_mt:query :: query -> builder
1305-
builder_mt:execute :: {chunk, entity[], integer} -> builder
1354+
builder_mt:execute :: {chunk, entity[], integer, any...} -> builder
13061355
1307-
builder_mt:prologue :: {} -> builder
1308-
builder_mt:epilogue :: {} -> builder
1356+
builder_mt:prologue :: {any...} -> builder
1357+
builder_mt:epilogue :: {any...} -> builder
13091358
13101359
builder_mt:destruction_policy :: id -> builder
13111360
```
13121361

13131362
## Changelog
13141363

1364+
### v1.7.0
1365+
1366+
- Added the new [`evolved.VARIANTS`](#evolvedvariants) query fragment that allows specifying any of multiple fragments in queries
1367+
- Added the new [`evolved.process_with`](#evolvedprocess_with) function that allows passing payloads to processing systems
1368+
13151369
### v1.6.0
13161370

13171371
- Significant performance improvements of the [`evolved.REQUIRES`](#evolvedrequires) fragment trait
@@ -1384,6 +1438,8 @@ builder_mt:destruction_policy :: id -> builder
13841438

13851439
### `evolved.EXCLUDES`
13861440

1441+
### `evolved.VARIANTS`
1442+
13871443
### `evolved.REQUIRES`
13881444

13891445
### `evolved.ON_SET`
@@ -1710,6 +1766,14 @@ function evolved.locate(entity) end
17101766
function evolved.process(...) end
17111767
```
17121768

1769+
### `evolved.process_with`
1770+
1771+
```lua
1772+
---@param system evolved.system
1773+
---@param ... any processing payload
1774+
function evolved.process_with(system, ...) end
1775+
```
1776+
17131777
### `evolved.debug_mode`
17141778

17151779
```lua
@@ -2013,6 +2077,14 @@ function evolved.builder_mt:include(...) end
20132077
function evolved.builder_mt:exclude(...) end
20142078
```
20152079

2080+
#### `evolved.builder_mt:variant`
2081+
2082+
```lua
2083+
---@param ... evolved.fragment fragments
2084+
---@return evolved.builder builder
2085+
function evolved.builder_mt:variant(...) end
2086+
```
2087+
20162088
### `evolved.builder_mt:require`
20172089

20182090
```lua

develop/all.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require 'develop.testing.locate_tests'
77
require 'develop.testing.main_tests'
88
require 'develop.testing.multi_spawn_tests'
99
require 'develop.testing.name_tests'
10+
require 'develop.testing.process_with_tests'
1011
require 'develop.testing.requires_fragment_tests'
1112
require 'develop.testing.spawn_tests'
1213
require 'develop.testing.system_as_query_tests'

develop/fuzzing/execute_fuzz.lua

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,31 @@ local function generate_query(query)
5656
end
5757
end
5858

59+
local variant_set = {}
60+
local variant_list = {}
61+
local variant_count = 0
62+
63+
for _ = 1, math.random(0, #all_fragment_list) do
64+
local variant = all_fragment_list[math.random(1, #all_fragment_list)]
65+
66+
if not variant_set[variant] then
67+
variant_count = variant_count + 1
68+
variant_set[variant] = variant_count
69+
variant_list[variant_count] = variant
70+
end
71+
end
72+
5973
if include_count > 0 then
6074
evo.set(query, evo.INCLUDES, include_list)
6175
end
6276

6377
if exclude_count > 0 then
6478
evo.set(query, evo.EXCLUDES, exclude_list)
6579
end
80+
81+
if variant_count > 0 then
82+
evo.set(query, evo.VARIANTS, variant_list)
83+
end
6684
end
6785

6886
---@param query_count integer
@@ -173,12 +191,22 @@ local function execute_query(query)
173191

174192
local query_include_list = evo.get(query, evo.INCLUDES) or {}
175193
local query_exclude_list = evo.get(query, evo.EXCLUDES) or {}
194+
local query_variant_list = evo.get(query, evo.VARIANTS) or {}
195+
196+
local query_include_count = #query_include_list
197+
local query_exclude_count = #query_exclude_list
198+
local query_variant_count = #query_variant_list
176199

177200
local query_include_set = {}
178201
for _, include in ipairs(query_include_list) do
179202
query_include_set[include] = true
180203
end
181204

205+
local query_variant_set = {}
206+
for _, variant in ipairs(query_variant_list) do
207+
query_variant_set[variant] = true
208+
end
209+
182210
for chunk, entity_list, entity_count in evo.execute(query) do
183211
assert(not query_chunk_set[chunk])
184212
query_chunk_set[chunk] = true
@@ -189,19 +217,29 @@ local function execute_query(query)
189217
query_entity_set[entity] = true
190218
end
191219

192-
assert(chunk:has_all(__table_unpack(query_include_list)))
193-
assert(not chunk:has_any(__table_unpack(query_exclude_list)))
220+
if query_include_count > 0 then
221+
assert(chunk:has_all(__table_unpack(query_include_list)))
222+
end
223+
224+
if query_exclude_count > 0 then
225+
assert(not chunk:has_any(__table_unpack(query_exclude_list)))
226+
end
227+
228+
if query_variant_count > 0 then
229+
assert(chunk:has_any(__table_unpack(query_variant_list)))
230+
end
194231
end
195232

196233
for i = 1, all_entity_count do
197234
local entity = all_entity_list[i]
198235

199236
local is_entity_matched =
200-
evo.has_all(entity, __table_unpack(query_include_list))
201-
and not evo.has_any(entity, __table_unpack(query_exclude_list))
237+
(query_variant_count == 0 or evo.has_any(entity, __table_unpack(query_variant_list))) and
238+
(query_include_count == 0 or evo.has_all(entity, __table_unpack(query_include_list))) and
239+
(query_exclude_count == 0 or not evo.has_any(entity, __table_unpack(query_exclude_list)))
202240

203241
for fragment in evo.each(entity) do
204-
if evo.has(fragment, evo.EXPLICIT) and not query_include_set[fragment] then
242+
if evo.has(fragment, evo.EXPLICIT) and not query_variant_set[fragment] and not query_include_set[fragment] then
205243
is_entity_matched = false
206244
end
207245
end
@@ -236,10 +274,13 @@ for _ = 1, math.random(1, 5) do
236274
if math.random(1, 2) == 1 then
237275
generate_query(query)
238276
else
239-
if math.random(1, 2) == 1 then
277+
local r = math.random(1, 3)
278+
if r == 1 then
240279
evo.remove(query, evo.INCLUDES)
241-
else
280+
elseif r == 2 then
242281
evo.remove(query, evo.EXCLUDES)
282+
else
283+
evo.remove(query, evo.VARIANTS)
243284
end
244285
end
245286
end

0 commit comments

Comments
 (0)