Skip to content
Merged

Dev #37

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

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

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
90 changes: 81 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- [Deferred Operations](#deferred-operations)
- [Batch Operations](#batch-operations)
- [Systems](#systems)
- [Processing Payloads](#processing-payloads)
- [Predefined Traits](#predefined-traits)
- [Fragment Tags](#fragment-tags)
- [Fragment Hooks](#fragment-hooks)
Expand All @@ -60,6 +61,7 @@
- [Chunk](#chunk)
- [Builder](#builder)
- [Changelog](#changelog)
- [v1.7.0](#v170)
- [v1.6.0](#v160)
- [v1.5.0](#v150)
- [v1.4.0](#v140)
Expand Down Expand Up @@ -586,16 +588,22 @@ evolved.set(entity, fragment, 42)

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.

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.
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.

- [`evolved.INCLUDES`](#evolvedincludes) is used to specify fragments that must be present in the entity;
- [`evolved.EXCLUDES`](#evolvedexcludes) is used to specify fragments that must not be present in the entity;
- [`evolved.VARIANTS`](#evolvedvariants) is used to specify fragments where at least one must be present in the entity.

```lua
local evolved = require 'evolved'

local health, poisoned, resistant = evolved.id(3)
local alive, undead = evolved.id(2)

local query = evolved.id()
evolved.set(query, evolved.INCLUDES, { health, poisoned })
evolved.set(query, evolved.EXCLUDES, { resistant })
evolved.set(query, evolved.VARIANTS, { alive, undead })
```

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:
Expand All @@ -604,10 +612,11 @@ The builder interface can be used to create queries too. It is more convenient t
local query = evolved.builder()
:include(health, poisoned)
:exclude(resistant)
:variant(alive, undead)
:build()
```

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.
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.

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.

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

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.
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.

```lua
local evolved = require 'evolved'
Expand Down Expand Up @@ -880,6 +889,43 @@ The prologue and epilogue fragments do not require an explicit query. They will
> [!NOTE]
> 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.

#### Processing Payloads

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.

```lua
---@param system evolved.system
---@param ... any processing payload
function evolved.process_with(system, ...) end
```

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.

```lua
local evolved = require 'evolved'

local position_x, position_y = evolved.id(2)
local velocity_x, velocity_y = evolved.id(2)

local physics_system = evolved.builder()
:include(position_x, position_y)
:include(velocity_x, velocity_y)
:execute(function(chunk, entity_list, entity_count, delta_time)
local px, py = chunk:components(position_x, position_y)
local vx, vy = chunk:components(velocity_x, velocity_y)

for i = 1, entity_count do
px[i] = px[i] + vx[i] * delta_time
py[i] = py[i] + vy[i] * delta_time
end
end):build()

local delta_time = 0.016
evolved.process_with(physics_system, delta_time)
```

`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).

### Predefined Traits

#### Fragment Tags
Expand Down Expand Up @@ -1125,9 +1171,9 @@ storage :: component[]
default :: component
duplicate :: {component -> component}

execute :: {chunk, entity[], integer}
prologue :: {}
epilogue :: {}
execute :: {chunk, entity[], integer, any...}
prologue :: {any...}
epilogue :: {any...}

set_hook :: {entity, fragment, component, component}
assign_hook :: {entity, fragment, component, component}
Expand Down Expand Up @@ -1159,6 +1205,7 @@ DISABLED :: fragment

INCLUDES :: fragment
EXCLUDES :: fragment
VARIANTS :: fragment
REQUIRES :: fragment

ON_SET :: fragment
Expand Down Expand Up @@ -1229,6 +1276,7 @@ execute :: query -> {execute_state? -> chunk?, entity[]?, integer?}, execute_sta
locate :: entity -> chunk?, integer

process :: system... -> ()
process_with :: system, ... -> ()

debug_mode :: boolean -> ()
collect_garbage :: ()
Expand Down Expand Up @@ -1292,6 +1340,7 @@ builder_mt:disabled :: builder

builder_mt:include :: fragment... -> builder
builder_mt:exclude :: fragment... -> builder
builder_mt:variant :: fragment... -> builder
builder_mt:require :: fragment... -> builder

builder_mt:on_set :: {entity, fragment, component, component} -> builder
Expand All @@ -1302,16 +1351,21 @@ builder_mt:on_remove :: {entity, fragment} -> builder
builder_mt:group :: system -> builder

builder_mt:query :: query -> builder
builder_mt:execute :: {chunk, entity[], integer} -> builder
builder_mt:execute :: {chunk, entity[], integer, any...} -> builder

builder_mt:prologue :: {} -> builder
builder_mt:epilogue :: {} -> builder
builder_mt:prologue :: {any...} -> builder
builder_mt:epilogue :: {any...} -> builder

builder_mt:destruction_policy :: id -> builder
```

## Changelog

### v1.7.0

- Added the new [`evolved.VARIANTS`](#evolvedvariants) query fragment that allows specifying any of multiple fragments in queries
- Added the new [`evolved.process_with`](#evolvedprocess_with) function that allows passing payloads to processing systems

### v1.6.0

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

### `evolved.EXCLUDES`

### `evolved.VARIANTS`

### `evolved.REQUIRES`

### `evolved.ON_SET`
Expand Down Expand Up @@ -1710,6 +1766,14 @@ function evolved.locate(entity) end
function evolved.process(...) end
```

### `evolved.process_with`

```lua
---@param system evolved.system
---@param ... any processing payload
function evolved.process_with(system, ...) end
```

### `evolved.debug_mode`

```lua
Expand Down Expand Up @@ -2013,6 +2077,14 @@ function evolved.builder_mt:include(...) end
function evolved.builder_mt:exclude(...) end
```

#### `evolved.builder_mt:variant`

```lua
---@param ... evolved.fragment fragments
---@return evolved.builder builder
function evolved.builder_mt:variant(...) end
```

### `evolved.builder_mt:require`

```lua
Expand Down
1 change: 1 addition & 0 deletions develop/all.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require 'develop.testing.locate_tests'
require 'develop.testing.main_tests'
require 'develop.testing.multi_spawn_tests'
require 'develop.testing.name_tests'
require 'develop.testing.process_with_tests'
require 'develop.testing.requires_fragment_tests'
require 'develop.testing.spawn_tests'
require 'develop.testing.system_as_query_tests'
Expand Down
55 changes: 48 additions & 7 deletions develop/fuzzing/execute_fuzz.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,31 @@ local function generate_query(query)
end
end

local variant_set = {}
local variant_list = {}
local variant_count = 0

for _ = 1, math.random(0, #all_fragment_list) do
local variant = all_fragment_list[math.random(1, #all_fragment_list)]

if not variant_set[variant] then
variant_count = variant_count + 1
variant_set[variant] = variant_count
variant_list[variant_count] = variant
end
end

if include_count > 0 then
evo.set(query, evo.INCLUDES, include_list)
end

if exclude_count > 0 then
evo.set(query, evo.EXCLUDES, exclude_list)
end

if variant_count > 0 then
evo.set(query, evo.VARIANTS, variant_list)
end
end

---@param query_count integer
Expand Down Expand Up @@ -173,12 +191,22 @@ local function execute_query(query)

local query_include_list = evo.get(query, evo.INCLUDES) or {}
local query_exclude_list = evo.get(query, evo.EXCLUDES) or {}
local query_variant_list = evo.get(query, evo.VARIANTS) or {}

local query_include_count = #query_include_list
local query_exclude_count = #query_exclude_list
local query_variant_count = #query_variant_list

local query_include_set = {}
for _, include in ipairs(query_include_list) do
query_include_set[include] = true
end

local query_variant_set = {}
for _, variant in ipairs(query_variant_list) do
query_variant_set[variant] = true
end

for chunk, entity_list, entity_count in evo.execute(query) do
assert(not query_chunk_set[chunk])
query_chunk_set[chunk] = true
Expand All @@ -189,19 +217,29 @@ local function execute_query(query)
query_entity_set[entity] = true
end

assert(chunk:has_all(__table_unpack(query_include_list)))
assert(not chunk:has_any(__table_unpack(query_exclude_list)))
if query_include_count > 0 then
assert(chunk:has_all(__table_unpack(query_include_list)))
end

if query_exclude_count > 0 then
assert(not chunk:has_any(__table_unpack(query_exclude_list)))
end

if query_variant_count > 0 then
assert(chunk:has_any(__table_unpack(query_variant_list)))
end
end

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

local is_entity_matched =
evo.has_all(entity, __table_unpack(query_include_list))
and not evo.has_any(entity, __table_unpack(query_exclude_list))
(query_variant_count == 0 or evo.has_any(entity, __table_unpack(query_variant_list))) and
(query_include_count == 0 or evo.has_all(entity, __table_unpack(query_include_list))) and
(query_exclude_count == 0 or not evo.has_any(entity, __table_unpack(query_exclude_list)))

for fragment in evo.each(entity) do
if evo.has(fragment, evo.EXPLICIT) and not query_include_set[fragment] then
if evo.has(fragment, evo.EXPLICIT) and not query_variant_set[fragment] and not query_include_set[fragment] then
is_entity_matched = false
end
end
Expand Down Expand Up @@ -236,10 +274,13 @@ for _ = 1, math.random(1, 5) do
if math.random(1, 2) == 1 then
generate_query(query)
else
if math.random(1, 2) == 1 then
local r = math.random(1, 3)
if r == 1 then
evo.remove(query, evo.INCLUDES)
else
elseif r == 2 then
evo.remove(query, evo.EXCLUDES)
else
evo.remove(query, evo.VARIANTS)
end
end
end
Expand Down
Loading
Loading