Skip to content

r6915ee/exile

Repository files navigation

exile

exile is an Entity Component System framework for the Lua scripting language. It primarily focuses on using Lua 5.1 syntax, safe manipulation functions, and leveraging the use of more native design concepts with managing objects.

Glossary

  • Entities: Entities, at their core, are simply integer keys that point torwards a set of information describing a set of components. In the case of exile, entities are used as keys to a set of tables describing associated components for that entity.
  • Components: Components are simply sets of data that can be used and manipulated by systems. An entity may refer to multiple different components at once.
  • Systems: Systems are simply functions that operate on entities with certain components, typically through archetypes. They are invoked by schedules.
  • Schedules: A concept unique to only some ECS frameworks, including exile, schedules categorize various systems together to be run at certain intervals specified by the base program.
  • Archetypes: Archetypes, a feature popularized by Unity, categorize entities based on their associated components. They are used for querying operations.

Installation

For installing the master branch, it is safe to use LuaRocks for installing like so:

luarocks install exile-master-1.rockspec

This will install the rock to your configured rocks directory.

Additionally, the library itself only consists of a module file, and it contains some information of the project itself as well. It is safe to place this file directly in a repository, or use a Git submodule.

cp ./exile/src/exile.lua ./project/src/exile.lua

Usage

Require exile as you would with any other third-party Lua module, assuming you either have the LuaRocks rocks directory in the Lua search path or in the working directory of the Lua executor:

exile = require("exile")

Components

Components can be constructed using the component method.

local position = exile:component({ x = 0, y = 0 })
local shouldDisplay = exile:component()

The component method can either accept a table with a certain set of default data, or nil for an empty table, which can act as a simple flag.

Printing position will output 1, and shouldDisplay will output 2:

print(position .. " " .. shouldDisplay)

This is because all objects in exile, besides archetypes, are organized using a typical table array internally. As such, components, entities, and systems (which are grouped in schedules) are simply integer keys to tables that use array indexing, starting from 1. For this reason, creating a component, entity, or schedule will return its associated index, which is useful for many functions in exile.

It is possible to provide a mutation when referring to a component index in some functions, particularly entity-related ones:

exile:mutate(position, { x = 10 })

This will attach a metatable to the mutation with the metamethod __index pointing to the original component and allow the mutation's index to be known, and then return the modified mutation.

Entities

Entities are constructed using the entity method. Just like the component method above, they can have no argument passed at all, although the arguments passed are instead variadic arguments that are either a component index or mutation each:

local displayObject = exile:entity(exile:mutate(1, { x = 10 }), 2)

Entities can have one or more components added or removed using the push and pull methods, respectively:

local velocity = exile:component({ x = 0, y = 0 })
exile:push(displayObject, velocity)
exile:pull(displayObject, shouldDisplay)

An entity's archetype name may be found using the getArchetype method that only accepts the entity index, which is useful for certain queries:

print(exile:getArchetype(displayObject)) -- 1,3

A simple way to verify whether or not one or more components are used by an entity is to use the entityHas method:

print(exile:entityHas(displayObject, 1)) -- true
print(exile:entityHas(displayObject, 2)) -- false, since we pulled it earlier!

Removing an entity is as simple as using removeEntity:

local dummyObject = exile:entity()
exile:removeEntity(dummyObject)

Systems

Systems are partially unconventional in exile for the preferred usage of a schedules system, particularly inspired from Bevy.

A schedule may be initiated with an initial set of systems using the following schedule method, which accepts a set of variadic arguments consisting only of functions, which are systems themselves:

local hello = exile:schedule(function(name)
   print("Hello, " .. tostring(name) .. "!")
end)

This schedule may then be invoked by using the respective invoke method, where each variadic argument will be passed to the systems in the associated schedule:

exile:invoke(hello, "exile") -- Hello, exile!

A schedule may have one or more systems assigned at runtime using the assign method; however, there is no method for removing a system.

exile:assign(hello, function(name)
   print("And goodbye, " .. tostring(name) .. "!")
end)

Queries

Queries are used to find entities associated with a certain archetype. To perform a query, there are several methods available. All of them return a table, where each key is the entity's index, and each value is that entity's component data that can be mutated for further use. It is also possible to fetch and mutate the component data of an entity.

Searching through an archetype by its associated components in a variadic fashion can be performed using the query method:

for k, v in pairs(exile:query(position, velocity)) do
   print(k .. ": " .. tostring(v))
end

However, this method of entity manipulation from a query is not efficient. The reason is because of the direct usage of pairs, which is often considered slow. Most people may want to use the specialized operate method instead, which avoids the usage of pairs and keeps the actual declaration simple. The same example with operate would look like:

exile:operate(function(index, entity) print(index .. ": " .. tostring(entity)) end, position, velocity)

queryString is a slightly faster alternative to query that queries directly using the archetype name provided, instead of converting the variadic arguments to a valid archetype name. However, it requires knowing the archetype name beforehand, which is in the form of raw component indexes separated by commas in numerical order:

for k, v in pairs(exile:queryString("1,3")) do
   print(k .. ": " .. tostring(v))
end

Due to its performance benefit, queryString has an associated method similar to operate called operateString, which uses queryString instead of query. The same example above, but with this method, would be:

exile:operateString(function(index, entity) print(index .. ": " .. tostring(entity)) end, "1,3")

Finally, operateQuery provides the same functionality as operate, but instead operates on a raw query - that is, the query has already been collected for further use. The two prior operation methods use this method internally.

exile:operateQuery(function(index, entity) print(index .. ": " .. tostring(entity)) end, exile:query(1, 3))
exile:operateQuery(function(index, entity) print(index .. ": " .. tostring(entity)) end, exile:queryString("1,3"))

allEntities is a simpler query method that queries all entities:

local extraEntity = exile:entity()
for k, v in pairs(exile:allEntities()) do
   print(k .. ": " .. tostring(v))
end

The entityHas method mentioned above can be used in conjunction with allEntities for basic filtering if so desired.

Demos

Demos are present in the demos folder of this repository.

License

exile, like Lua itself, is licensed under the MIT license.

About

Simple, Lua-adjacent Entity Component System framework (GitHub mirror)

Topics

Resources

License

Stars

Watchers

Forks