Show HN: Evolved.lua – Lua 的进化版实体组件系统
Navigation Menu
BlackMATov / evolved.lua Public
- Notifications You must be signed in to change notification settings
- Fork 0
- Star 21
Evolved ECS (Entity-Component-System) for Lua github.com/BlackMATov/evolved.lua
License
MIT license 21 stars 0 forks Branches Tags Activity Star Notifications You must be signed in to change notification settings
Additional navigation options
BlackMATov/evolved.lua
Folders and files
Name| Name| Last commit message| Last commit date ---|---|---|---
Latest commit
History
517 Commits .github/workflows| .github/workflows .vscode| .vscode develop| develop rockspecs| rockspecs .gitignore| .gitignore .luarc.json| .luarc.json LICENSE.md| LICENSE.md README.md| README.md ROADMAP.md| ROADMAP.md evolved.lua| evolved.lua
Repository files navigation
evolved.lua (开发中)
Lua 的 Evolved ECS (Entity-Component-System)
Introduction
evolved.lua
是一个快速且灵活的 Lua ECS (Entity-Component-System) 库。它被设计得简单易用,同时提供了创建复杂系统所需的所有功能,并具有极高的性能。在开始探索该库之前,让我们先来看看使用 evolved.lua
的主要优势:
Performance
该库旨在实现快速。采用了多种技术来实现这一点。它使用基于 Archetype 的方法来存储实体及其组件。组件以 SoA(Structure of Arrays)方式存储在连续数组中,从而可以进行快速迭代和处理。Chunks 用于将具有相同组件集的实体组合在一起,从而可以通过查询有效地进行过滤。此外,所有操作都旨在最大程度地减少 GC(Garbage Collector)压力并避免不必要的分配。我已经尝试考虑到原生 Lua 和 LuaJIT 的所有性能陷阱。
并非所有我想实现的优化都已完成,但我将努力进行优化。但是,我已经可以说该库对于大多数用例来说足够快了。
Simplicity
我已经尝试使 API 尽可能简单直观。我还控制了函数的数量。所有函数都具有自我解释性且易于使用。阅读概述部分后,您应该可以毫无问题地使用该库。
是的,该库的核心有一些不寻常的概念,但是一旦您掌握了它,您会发现它非常易于使用。
Flexibility
evolved.lua
不仅仅是将组件保留在实体中。它是一个功能完善的 ECS 库,可让您创建复杂的系统和流程。您可以创建带有过滤器的查询,使用延迟操作和批量操作。您还可以创建以特定顺序处理实体的系统。该库被设计为灵活和可扩展的,因此您可以轻松添加自己的功能。诸如片段钩子之类的功能使您可以更灵活地管理组件,将它们与外部系统或库同步。该库还提供语法糖,例如实体构建器,用于创建实体、片段和系统,以使您的生活更轻松。
另一方面,evolved.lua
试图做到极简主义,并且不提供可以在库外部实现的功能。我试图在极简主义和可能性的数量之间找到平衡,这迫使我在库的设计中做出灵活的决策。我希望您会发现这种平衡是可以接受的。
Requirements
Installation
您可以使用 luarocks 通过以下命令安装 evolved.lua
:
luarocks install evolved.lua
或者只需克隆 repository 并将 evolved.lua 文件复制到您的项目中。
Quick Start
要开始使用 evolved.lua
,请首先阅读概述部分。它将使您对库的工作方式以及如何使用它有一个基本的了解。之后,请查看功能齐全的 Example,它演示了该库的复杂用法。最后,请参阅 Cheat Sheet,以快速参考该库提供的所有函数和类。
Enjoy!
Cheat Sheet
Aliases
id :: implementation-specific
entity :: id
fragment :: id
query :: id
system :: id
component :: any
storage :: component[]
default :: component
duplicate :: {component -> component}
execute :: {chunk, entity[], integer}
prologue :: {}
epilogue :: {}
set_hook :: {entity, fragment, component, component?}
assign_hook :: {entity, fragment, component, component}
insert_hook :: {entity, fragment, component}
remove_hook :: {entity, fragment, component}
each_state :: implementation-specific
execute_state :: implementation-specific
each_iterator :: {each_state? -> fragment?, component?}
execute_iterator :: {execute_state? -> chunk?, entity[]?, integer?}
Predefs
TAG :: fragment
NAME :: fragment
UNIQUE :: fragment
EXPLICIT :: fragment
DEFAULT :: fragment
DUPLICATE :: fragment
PREFAB :: fragment
DISABLED :: fragment
INCLUDES :: fragment
EXCLUDES :: fragment
ON_SET :: fragment
ON_ASSIGN :: fragment
ON_INSERT :: fragment
ON_REMOVE :: fragment
GROUP :: fragment
QUERY :: fragment
EXECUTE :: fragment
PROLOGUE :: fragment
EPILOGUE :: fragment
DESTRUCTION_POLICY :: fragment
DESTRUCTION_POLICY_DESTROY_ENTITY :: id
DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id
Functions
id :: integer? -> id...
pack :: integer, integer -> id
unpack :: id -> integer, integer
defer :: boolean
commit :: boolean
spawn :: <fragment, component>? -> entity
clone :: entity -> <fragment, component>? -> entity
alive :: entity -> boolean
alive_all :: entity... -> boolean
alive_any :: entity... -> boolean
empty :: entity -> boolean
empty_all :: entity... -> boolean
empty_any :: entity... -> boolean
has :: entity, fragment -> boolean
has_all :: entity, fragment... -> boolean
has_any :: entity, fragment... -> boolean
get :: entity, fragment... -> component...
set :: entity, fragment, component -> ()
remove :: entity, fragment... -> ()
clear :: entity... -> ()
destroy :: entity... -> ()
batch_set :: query, fragment, component -> ()
batch_remove :: query, fragment... -> ()
batch_clear :: query... -> ()
batch_destroy :: query... -> ()
each :: entity -> {each_state? -> fragment?, component?}, each_state?
execute :: query -> {execute_state? -> chunk?, entity[]?, integer?}, execute_state?
process :: system... -> ()
debug_mode :: boolean -> ()
collect_garbage :: ()
Classes
Chunk
chunk :: fragment, fragment... -> chunk, entity[], integer
chunk_mt:alive :: boolean
chunk_mt:empty :: boolean
chunk_mt:has :: fragment -> boolean
chunk_mt:has_all :: fragment... -> boolean
chunk_mt:has_any :: fragment... -> boolean
chunk_mt:entities :: entity[], integer
chunk_mt:fragments :: fragment[], integer
chunk_mt:components :: fragment... -> storage...
Builder
builder :: builder
builder_mt:spawn :: entity
builder_mt:clone :: entity -> entity
builder_mt:has :: fragment -> boolean
builder_mt:has_all :: fragment... -> boolean
builder_mt:has_any :: fragment... -> boolean
builder_mt:get :: fragment... -> component...
builder_mt:set :: fragment, component -> builder
builder_mt:remove :: fragment... -> builder
builder_mt:clear :: builder
builder_mt:tag :: builder
builder_mt:name :: string -> builder
builder_mt:unique :: builder
builder_mt:explicit :: builder
builder_mt:default :: component -> builder
builder_mt:duplicate :: {component -> component} -> builder
builder_mt:prefab :: builder
builder_mt:disabled :: builder
builder_mt:include :: fragment... -> builder
builder_mt:exclude :: fragment... -> builder
builder_mt:on_set :: {entity, fragment, component, component?} -> builder
builder_mt:on_assign :: {entity, fragment, component, component} -> builder
builder_mt:on_insert :: {entity, fragment, component} -> builder
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:prologue :: {} -> builder
builder_mt:epilogue :: {} -> builder
builder_mt:destruction_policy :: id -> builder
Overview
该库被设计为简单且高性能。它使用基于 Archetype 的方法来存储实体及其组件。这使您可以非常有效地过滤和处理您的实体,尤其是在您有很多实体的情况下。
如果您熟悉 ECS(Entity-Component-System)模式,您会感到宾至如归。如果不是,我强烈建议您先阅读有关它的内容。这是一个好的起点:Entity Component System FAQ。
让我们开始吧!
Identifiers
标识符是一个打包的 40 位整数。前 20 位表示索引,后 20 位表示版本。要创建新的标识符,请使用 evolved.id
函数。
---@param count? integer
---@return evolved.id ... ids
function evolved.id(count) end
count
参数是可选的,默认为 1
。该函数根据 count
参数返回一个或多个标识符。活动标识符的最大数量为 2^20-1
(1048575)。此后,该函数将引发错误:| evolved.lua | id index overflow
。
标识符可以回收利用。当不再需要标识符时,请使用 evolved.destroy
函数将其销毁。这将释放该标识符以供重复使用。
---@param ... evolved.id ids
function evolved.destroy(...) end
evolved.destroy
函数将一个或多个标识符作为参数。销毁的标识符将添加到回收站空闲列表中。在非活动标识符上调用 evolved.destroy
是安全的;该函数将简单地忽略它们。
销毁标识符后,可以通过再次调用 evolved.id
函数来重复使用它。新的标识符将具有与销毁的标识符相同的索引,但版本不同。每次销毁标识符时,版本都会递增。此机制使我们能够重用索引并知道标识符是否处于活动状态。
可以使用 evolved.alive
函数集来检查标识符是否处于活动状态。
---@param id evolved.id
---@return boolean
function evolved.alive(id) end
---@param ... evolved.id ids
---@return boolean
function evolved.alive_all(...) end
---@param ... evolved.id ids
---@return boolean
function evolved.alive_any(...) end
有时(例如,出于调试目的),需要从标识符中提取索引和版本,或者将它们重新打包到标识符中。可以使用 evolved.pack
和 evolved.unpack
函数来实现此目的。
---@param index integer
---@param version integer
---@return evolved.id id
function evolved.pack(index, version) end
---@param id evolved.id
---@return integer index
---@return integer version
function evolved.unpack(id) end
这是一个如何使用标识符的简短示例:
local evolved = require 'evolved'
local id = evolved.id() -- 创建一个新的标识符
assert(evolved.alive(id)) -- 检查标识符是否处于活动状态
local index, version = evolved.unpack(id) -- 解包标识符
assert(evolved.pack(index, version) == id) -- 重新打包它
evolved.destroy(id) -- 销毁标识符
assert(not evolved.alive(id)) -- 检查标识符现在是否未处于活动状态
Entities, Fragments, and Components
首先,您需要了解实体和片段只是标识符。它们之间的区别纯粹是语义上的。实体用于表示世界中的对象,而片段用于表示可以附加到实体的组件类型。另一方面,组件是通过片段附加到实体的任何数据。
---@alias evolved.entity evolved.id
---@alias evolved.fragment evolved.id
---@alias evolved.component any
这是一个如何将组件附加到实体的简单示例:
local evolved = require 'evolved'
local entity, fragment = evolved.id(2)
evolved.set(entity, fragment, 100)
assert(evolved.get(entity, fragment) == 100)
我知道它还不是很清楚,但请不要担心,我们会到达那里的。在下一个示例中,我将命名实体和片段,因此更容易理解这里发生了什么。
local evolved = require 'evolved'
local player = evolved.id()
local health = evolved.id()
local stamina = evolved.id()
evolved.set(player, health, 100)
evolved.set(player, stamina, 50)
assert(evolved.get(player, health) == 100)
assert(evolved.get(player, stamina) == 50)
我们创建了一个名为 player
的实体和两个名为 health
和 stamina
的片段。我们通过这些片段将组件 100
和 50
附加到实体。之后,我们可以使用 evolved.get
函数检索组件。
我们将在后面关于修改操作的部分中更详细地介绍 evolved.set
和 evolved.get
函数。现在,让我们说它们用于通过片段从实体设置和获取组件。
这里要理解的主要事情是,您可以使用其他标识符将任何数据附加到任何标识符。
Traits
由于片段只是标识符,因此您也可以将它们用作实体!片段的片段通常称为 “traits”。例如,这对于使用一些元数据标记片段非常有用。
local evolved = require 'evolved'
local serializable = evolved.id()
local position = evolved.id()
evolved.set(position, serializable, true)
local velocity = evolved.id()
evolved.set(velocity, serializable, true)
local player = evolved.id()
evolved.set(player, position, {x = 0, y = 0})
evolved.set(player, velocity, {x = 0, y = 0})
在此示例中,我们创建了一个名为 serializable
的 trait,并将片段 position
和 velocity
标记为可序列化。之后,您可以编写一个将序列化实体的函数,并且此函数将仅序列化标记为可序列化的片段。这是该库的一个非常强大的功能,它使您可以创建非常灵活的系统。
Singletons
片段甚至可以附加到自身;这称为单例。当您要存储一些数据而没有单独的实体时,请使用此方法。例如,您可以使用它来存储全局数据,例如游戏状态或当前级别。
local evolved = require 'evolved'
local gravity = evolved.id()
evolved.set(gravity, gravity, 10)
assert(evolved.get(gravity, gravity) == 10)
Chunks
接下来我们需要了解的是,所有非空实体都存储在 chunks 中。Chunks 只是将实体及其组件一起存储的表。片段的每个唯一组合都存储在一个单独的 chunk 中。这意味着,如果您有两个带有 health
和 stamina
片段的实体,它们将存储在 <health, stamina>
chunk 中。如果您有另一个带有 health
、stamina
和 mana
片段的实体,它将存储在 <health, stamina, mana>
chunk 中。出于性能原因,这非常有用,因为它允许我们将具有相同片段的实体存储在一起,从而更容易迭代、过滤和处理它们。
local evolved = require 'evolved'
local health, stamina, mana = evolved.id(3)
local entity1 = evolved.id()
evolved.set(entity1, health, 100)
evolved.set(entity1, stamina, 50)
local entity2 = evolved.id()
evolved.set(entity2, health, 75)
evolved.set(entity2, stamina, 40)
local entity3 = evolved.id()
evolved.set(entity3, health, 50)
evolved.set(entity3, stamina, 30)
evolved.set(entity3, mana, 20)
以下是执行上述代码后 chunks 的外观:
chunk | health | stamina ---|---|--- entity1 | 100 | 50 entity2 | 75 | 40 chunk | health | stamina | mana ---|---|---|--- entity3 | 50 | 30 | 20
通常,您不需要直接在 chunks 上进行操作,但是可以使用 evolved.chunk
函数来获取特定的 chunk。
---@param fragment evolved.fragment
---@param ... evolved.fragment fragments
---@return evolved.chunk chunk
function evolved.chunk(fragment, ...) end
evolved.chunk
函数将一个或多个片段作为参数,并返回此组合的 chunk。之后,您可以使用 chunk 的方法来检索它们的实体、片段和组件。
---@return evolved.entity[] entity_list
---@return integer entity_count
function chunk_mt:entities() end
---@return evolved.fragment[] fragment_list
---@return integer fragment_count
function chunk_mt:fragments() end
---@param ... evolved.fragment fragments
---@return evolved.storage ... storages
function chunk_mt:components(...)
完整示例:
local evolved = require 'evolved'
local health, stamina, mana = evolved.id(3)
local entity1 = evolved.id()
evolved.set(entity1, health, 100)
evolved.set(entity1, stamina, 50)
local entity2 = evolved.id()
evolved.set(entity2, health, 75)
evolved.set(entity2, stamina, 40)
local entity3 = evolved.id()
evolved.set(entity3, health, 50)
evolved.set(entity3, stamina, 30)
evolved.set(entity3, mana, 20)
-- 获取(或创建,如果不存在)chunk <health, stamina>
local chunk = evolved.chunk(health, stamina)
-- 获取 chunk 中实体的列表和它们的数量
local entity_list, entity_count = chunk:entities()
-- 获取 chunk 中组件的列
local health_components = chunk:components(health)
local stamina_components = chunk:components(stamina)
for i = 1, entity_count do
local entity = entity_list[i]
local entity_health = health_components[i]
local entity_stamina = stamina_components[i]
-- 使用实体及其组件做一些事情
print(string.format(
'Entity: %d, Health: %d, Stamina: %d',
entity, entity_health, entity_stamina))
end
-- 预期输出:
-- Entity: 1048602, Health: 100, Stamina: 50
-- Entity: 1048603, Health: 75, Stamina: 40
Structural Changes
每次我们从实体中插入或删除片段时,该实体都将迁移到新的 chunk 中。当然,这是由库自动完成的。但是,您应该意识到这一点,因为它会影响性能,尤其是在实体上有很多片段的情况下。这称为 “structural change”。
您应该尽量避免 structural changes,尤其是在对性能至关重要的代码中。例如,您可以生成具有他们永远需要的所有片段的实体,并避免在实体的生命周期中更改它们。覆盖现有组件不是 structural change,因此您可以自由地执行此操作。
Spawning Entities
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.spawn(components) end
---@param prefab evolved.entity
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.clone(prefab, components) end
evolved.spawn
函数允许您生成具有所有必要片段的实体。它将组件表作为参数,其中键是片段,值是组件。顺便说一句,您无需每次都创建此 components
表;考虑使用预定义的表以获得最大的性能。
您还可以使用 evolved.clone
函数来克隆现有实体。这对于创建与现有实体具有相同片段但具有不同组件的实体很有用。
local evolved = require 'evolved'
local health, stamina = evolved.id(2)
-- 生成具有所有必要片段的实体
local enemy1 = evolved.spawn {
[health] = 100,
[stamina] = 50,
}
-- 生成另一个具有相同片段的实体,
-- 但其中一些具有不同的组件
local enemy2 = evolved.clone(enemy1, {
[health] = 50,
})
-- 这里没有 structural changes,
-- 我们只是覆盖现有组件
evolved.set(enemy1, health, 75)
evolved.set(enemy1, stamina, 42)
Entity Builders
避免在生成实体时出现 structural changes 的另一种方法是使用 evolved.builder
流畅接口。evolved.builder
函数返回一个构建器对象,该对象允许您生成具有特定片段集和组件的实体,而无需通过每次更改的 structural changes 逐一设置它们。
local evolved = require 'evolved'
local health, stamina = evolved.id(2)
local enemy = evolved.builder()
:set(health, 100)
:set(stamina, 50)
:spawn()
构建器可以重复使用,因此您可以创建一个具有特定片段集和组件的构建器,然后