Improve Lua ECS #29

Closed
opened 2024-10-19 03:06:45 +00:00 by SeanOMik · 5 comments
Owner

Currently ECS in Lua scripts is a bit wonky. The engine's script api is very different than the rust api. To query for the Transform and WorldTransform of entities:

---@type number
local dt = world:resource(DeltaTime)

world:view(
    ---@param t Transform
    ---@param wt WorldTransform
    function (t, wt)
        print("Entity is at: " .. tostring(wt))
        t:translate(0, 0.15 * dt, 0)
        return t
    end, Transform, WorldTransform
)

This limits the api to not support filters, like Changed<T> and resources. It would be better if the API was more like the Rust's one, where you have to create a view and then use that view, or like nidorx/ecs-lua which has a similar method.

Currently ECS in Lua scripts is a bit wonky. The engine's script api is very different than the rust api. To query for the `Transform` and `WorldTransform` of entities: ```lua ---@type number local dt = world:resource(DeltaTime) world:view( ---@param t Transform ---@param wt WorldTransform function (t, wt) print("Entity is at: " .. tostring(wt)) t:translate(0, 0.15 * dt, 0) return t end, Transform, WorldTransform ) ``` This limits the api to not support filters, like `Changed<T>` and resources. It would be better if the API was more like the Rust's one, where you have to create a view and then use that view, or like [nidorx/ecs-lua](https://github.com/nidorx/ecs-lua) which has a similar method.
SeanOMik added the
Kind/Enhancement
label 2024-10-19 03:06:45 +00:00
Author
Owner

Figured out the majority of the new ecs api. Now I need to add support for querying resources from the world, alongside the components of an entity.

Here's a short code example that does the same thing as the code snippet above:

---@type number
local dt = world:resource(DeltaTime)

local view = View.new(Transform, WorldTransform)
local res = world:view_query(view)

for entity, transform, world_tran in res:iter() do
    print("Entity is at: " .. tostring(world_tran))

    transform:translate(0, 0.15 * dt, 0)
    entity:update(transform)
end
Figured out the majority of the new ecs api. Now I need to add support for querying resources from the world, alongside the components of an entity. Here's a short code example that does the same thing as the code snippet above: ```lua ---@type number local dt = world:resource(DeltaTime) local view = View.new(Transform, WorldTransform) local res = world:view_query(view) for entity, transform, world_tran in res:iter() do print("Entity is at: " .. tostring(world_tran)) transform:translate(0, 0.15 * dt, 0) entity:update(transform) end ```
Author
Owner

Querying resources from Views is now possible:

local view = View.new(Transform, WorldTransform, Res(DeltaTime))
local res = world:view_query(view)

for entity, transform, world_tran, dt in res:iter() do
    print("Entity is at: " .. tostring(world_tran))

    transform:translate(0, 0.15 * dt, 0)
    entity:update(transform)
end

The function Res, is a helper function written in Lua that just runs ResQuery.new(resource).

The ResQuery is implemented in Rust as UserData. The way the queries work is that when the ViewResult is created, it iterates through the args and checks if it finds an internal function on the Lua table or userdata. The function is __lyra_internal_ecs_query_result (or the const FN_NAME_INTERNAL_ECS_QUERY_RESULT).

The internal function must return a Lua value, and take in a single argument: ScriptWorldPtr. If nil is returned, the query in a View will not provide any results. If it returns a boolean, the result will act as a filter, and the value will not be in the result. Any other value will be included in the result.

Now I want to create some filters, like Changed<T>!

Querying resources from Views is now possible: ```lua local view = View.new(Transform, WorldTransform, Res(DeltaTime)) local res = world:view_query(view) for entity, transform, world_tran, dt in res:iter() do print("Entity is at: " .. tostring(world_tran)) transform:translate(0, 0.15 * dt, 0) entity:update(transform) end ``` The function `Res`, is a helper function written in Lua that just runs `ResQuery.new(resource)`. The `ResQuery` is implemented in Rust as UserData. The way the queries work is that when the `ViewResult` is created, it iterates through the args and checks if it finds an internal function on the Lua table or userdata. The function is `__lyra_internal_ecs_query_result` (or the const `FN_NAME_INTERNAL_ECS_QUERY_RESULT`). The internal function must return a Lua value, and take in a single argument: `ScriptWorldPtr`. If `nil` is returned, the query in a `View` will not provide any results. If it returns a boolean, the result will act as a filter, and the value will not be in the result. Any other value will be included in the result. Now I want to create some filters, like `Changed<T>`!
SeanOMik added reference feat/improve-lua-ecs-29 2024-10-20 00:42:55 +00:00
SeanOMik added spent time 2024-10-20 00:43:08 +00:00
6 hours
Author
Owner

I finished writing the ChangedQuery for Lua. In the process of writing it, I had a difficult to diagnose issue where the query wouldn't detect changes like 50% of the time. I eventually figured it out, it was because the next function from ViewResult would return if the query for ANY entity returned nothing, even if another entity would return something.

Fixing that also made it so I had to change the behavior of FN_NAME_INTERNAL_ECS_QUERY_RESULT a bit. First is that the signature of the function changed; its now: fn(ScriptWorldPtr, Entity) -> LuaValue. Now for the behavior changes:

  • When nil is returned, its considered that the query will not result in anything for this View, no matter the entity. When the query is used in a View and returns nil, it will NOT check for other entities. This is used in the ResQuery Lua query. If the
    resource is missing, it will ALWAYS be missing for the View, no matter the entity.
  • If it returns a boolean, the query will act as a filter. The boolean value will not be in the result. When the boolean is false, other entities will be checked by the View.
  • Any other value will be included in the result.
    I may create a QueryResult enum that could make this a bit simpler and less ambiguous.

Here's a Lua code example using the new query!

-- Although WorldTransform isn't used, I only want to
-- modify entities with that component.
local view = View.new(Transform, WorldTransform, Res(DeltaTime))
local res = world:view_query(view)
for entity, transform, _wt, dt in res:iter() do
    transform:translate(0, 0.15 * dt, 0)
    entity:update(transform)
end

local changed_view = View.new(Changed(Transform))
local changed_res = world:view_query(changed_view)
for _, transform in changed_res:iter() do
    print("Entity transform changed to: " .. tostring(transform))
end

Now I need to expose a couple of other ECS things to Lua:

  • ViewOne for querying from a specific entity.
  • Filters:
    • Has
    • Not
    • Or
  • Queries:
    • Optional
    • TickOf
  • Add a way to get world tick (i.e., world:current_tick())
  • Create a QueryResult enum for simplifying the implementation of queries
I finished writing the `ChangedQuery` for Lua. In the process of writing it, I had a difficult to diagnose issue where the query wouldn't detect changes like 50% of the time. I eventually figured it out, it was because the `next` function from `ViewResult` would return if the query for ANY entity returned nothing, even if another entity would return something. Fixing that also made it so I had to change the behavior of `FN_NAME_INTERNAL_ECS_QUERY_RESULT` a bit. First is that the signature of the function changed; its now: `fn(ScriptWorldPtr, Entity) -> LuaValue`. Now for the behavior changes: * When `nil` is returned, its considered that the query will not result in anything for this `View`, **no matter the entity**. When the query is used in a `View` and returns `nil`, it will **NOT** check for other entities. This is used in the `ResQuery` Lua query. If the resource is missing, it will **ALWAYS** be missing for the `View`, no matter the entity. * If it returns a boolean, the query will act as a filter. The boolean value will not be in the result. When the boolean is `false`, other entities will be checked by the `View`. * Any other value will be included in the result. I may create a `QueryResult` enum that could make this a bit simpler and less ambiguous. Here's a Lua code example using the new query! ```lua -- Although WorldTransform isn't used, I only want to -- modify entities with that component. local view = View.new(Transform, WorldTransform, Res(DeltaTime)) local res = world:view_query(view) for entity, transform, _wt, dt in res:iter() do transform:translate(0, 0.15 * dt, 0) entity:update(transform) end local changed_view = View.new(Changed(Transform)) local changed_res = world:view_query(changed_view) for _, transform in changed_res:iter() do print("Entity transform changed to: " .. tostring(transform)) end ``` --- Now I need to expose a couple of other ECS things to Lua: - [x] `ViewOne` for querying from a specific entity. - [x] Filters: - [x] `Has` - [x] `Not` - [x] `Or` - [x] Queries: - [x] `Optional` - [x] `TickOf` - [x] Add a way to get world tick (i.e., `world:current_tick()`) - [x] Create a `QueryResult` enum for simplifying the implementation of queries
SeanOMik added spent time 2024-10-21 01:32:19 +00:00
4 hours
SeanOMik deleted spent time 2024-10-21 01:32:28 +00:00
- 4 hours
SeanOMik added spent time 2024-10-21 01:32:44 +00:00
6 hours
SeanOMik added spent time 2024-10-22 01:58:45 +00:00
3 hours
SeanOMik added spent time 2024-10-23 20:48:01 +00:00
1 hour
SeanOMik added spent time 2024-10-30 01:56:40 +00:00
1 hour
SeanOMik added spent time 2024-10-30 03:05:03 +00:00
30 minutes
Author
Owner

All the todo's I left in the previous comment have been completed! I also wrote lua annotations for everything that was added. Here's some code using most of the new features:

-- Get entities without WorldTransform
local view = View.new(Transform, Not(Has(WorldTransform)), Res(DeltaTime))
local res = world:view_query(view)
---@param transform Transform
---@param dt DeltaTime
for entity, transform, dt in res:iter() do
    transform:translate(0, 0.15 * dt, 0)
    entity:update(transform)
end

-- Query for entities that were moved.
local changed_view = View.new(Changed(Transform))
local changed_res = world:view_query(changed_view)
---@param transform Transform
for _, transform in changed_res:iter() do
    print("Entity transform changed to: '" .. tostring(transform) .. "' on tick " .. tostring(world:get_tick()))
end

-- Get the tick of transforms for all entities.
local tick_view = View.new(TickOf(Transform))
local tick_res = world:view_query(tick_view)
---@param tick number
for _, tick in tick_res:iter() do
    print("Entity transform last changed on tick " .. tostring(tick))
end

-- Get the Transform of a single entity
local pos_view = View.new(Transform)
-- cube_entity is a global of a known entity
local vone = world:view_one(cube_entity, pos_view)
local result = vone() -- short hand for 'vone:get()'
if result then
    ---@type Transform
    local pos = result[1]
    print("Found cube entity at '" .. tostring(pos) .. "'")
end

I think this will mark the completion of this issue. A lot was done and this makes the ECS a lot nicer to use in Lua.

All the todo's I left in the previous comment have been completed! I also wrote lua annotations for everything that was added. Here's some code using most of the new features: ```lua -- Get entities without WorldTransform local view = View.new(Transform, Not(Has(WorldTransform)), Res(DeltaTime)) local res = world:view_query(view) ---@param transform Transform ---@param dt DeltaTime for entity, transform, dt in res:iter() do transform:translate(0, 0.15 * dt, 0) entity:update(transform) end -- Query for entities that were moved. local changed_view = View.new(Changed(Transform)) local changed_res = world:view_query(changed_view) ---@param transform Transform for _, transform in changed_res:iter() do print("Entity transform changed to: '" .. tostring(transform) .. "' on tick " .. tostring(world:get_tick())) end -- Get the tick of transforms for all entities. local tick_view = View.new(TickOf(Transform)) local tick_res = world:view_query(tick_view) ---@param tick number for _, tick in tick_res:iter() do print("Entity transform last changed on tick " .. tostring(tick)) end -- Get the Transform of a single entity local pos_view = View.new(Transform) -- cube_entity is a global of a known entity local vone = world:view_one(cube_entity, pos_view) local result = vone() -- short hand for 'vone:get()' if result then ---@type Transform local pos = result[1] print("Found cube entity at '" .. tostring(pos) .. "'") end ``` I think this will mark the completion of this issue. A lot was done and this makes the ECS a lot nicer to use in Lua.
Author
Owner

Merged with #30

Merged with #30
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Total Time Spent: 17 hours 30 minutes
SeanOMik
17 hours 30 minutes
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: SeanOMik/lyra-engine#29
No description provided.