Nata is a Lua library for managing entities in games. It allows you to create entity pools, which are containers that hold all the objects in a game, like geometry, characters, and collectibles. At its simplest, pools hold entities and allow you to call functions on them, but they also provide a minimal structure for an Entity Component System organization.
To see Nata in action, open the demo with LÖVE from the base directory:
love demo
To use Nata, place nata.lua
in your project, and then add this code to the files where you use Nata:
local nata = require 'nata' -- if your nata.lua is in the root directory
local nata = require 'path.to.nata' -- if it's in subfolders
local pool = nata.new()
When called without any arguments, nata.new
creates a pool with default settings that are suitable for an object-oriented architecture.
local entity = pool:queue(entity)
pool:flush()
When an entity is added to the world, its add
function is called, if it has one.
pool:remove(f)
This removes every entity for which f(entity)
returns true, where f
is a user-provided function that takes the entity
as the first argument. For example, this code will remove any entities that have a field called dead
.
pool:remove(function(entity) return entity.dead end)
The remove
function will also be called on those entities, if they have one.
pool:emit(event, ...)
Calls the function named event
on each entity that has one, and passes the additional arguments ...
to that function.
for _, entity in ipairs(pool.groups.all.entities) do
-- operate on entities...
end
print(pool.groups.all.hasEntity[entity]) -- check if a group has an entity
Entities are stored in groups, which you can find in pool.groups
. Each group has entities
, an array of all the entities in the group, and hasEntity
, a table which has each entity in the world as a key (with a dummy value of true
).
Feel free to read from these tables and sort them. It's not recommended to add or remove entities from these tables manually though; use queue
/flush
/remove
for that.
You can set up additional groups to organize entities into. Each group can have its own filter, which determines which entities will be added to that group.
You can define groups by passing an options table into nata.new
:
local pool = nata.new {
groups = {
all = {},
physical = {filter = {'x', 'y', 'w', 'h'}},
large = {filter = function(entity)
return entity.w > 100 or entity.h > 100
end},
}
}
Filters can be either a table of required keys or a function. You can also leave out the filter, which allows all entities to be added to that group.
A system, generally speaking, defines a behavior that affects entities in certain groups. In Nata, a system is just an instance of a class that receives events from the pool and can call functions on the pool.
A system is defined like this:
local GravitySystem = {}
function GravitySystem:init()
self.baseGravity = 100
end
function GravitySystem:update(dt)
for _, e in ipairs(self.pool.groups.gravity.entities) do
e.vy = e.vy + self.baseGravity * e.gravity * dt
end
end
You can add systems to a pool by including a systems
table in the options table passed to nata.new
:
local pool = nata.new {
groups = {
gravity = {filter = {'gravity'}},
},
systems = {
GravitySystem,
},
}
Now, when pool:emit('update', dt)
is called, GravitySystem:update(dt)
will be called as well.
Also note that the system functions are all self functions - each pool creates "instances" of each system "class", so systems can hold their own internal state. Each system also has self.pool
, which allows access to all pool functions and properties.
Note that when the systems
table is not defined, the pool defaults to having one system: the nata.oop
system. This system is responsible for calling functions on entities when an event is emitted. If you're defining a list of systems and you want to retain this behavior, you should add nata.oop(group)
to your systems list, where group
is the name of the group whose entities you want to call functions on:
local pool = nata.new {
groups = {
everything = {},
gravity = {filter = {'gravity'}},
},
systems = {
nata.oop 'everything',
GravitySystem,
},
}
local pool = nata.new(options, ...)
Creates a new entity pool.
options
(optional) - a table of options to set up the pool with. The table should have the following contents:groups
(optional) - a table of groups the sort entities into. Defaults to{all = {}}
. Each key is the name of the group, and the value is a table with the following contents:filter
(optional) - the requirement for entities to be added to this group. It can either be a list of required keys or a function that takes an entity as the first argument and returns if the entity should be added to the group. If no filter is specified, all entities will be added to the group.sort
(optional) - a function that defines how entities will be sorted. The function works the same way as the as the function argument fortable.sort
.
systems
(optional) - a table of systems that should operate on the pool. Defaults tonata.oop('all')
.
...
- additional arguments that will be used when theinit
event is emitted.
pool:queue(entity)
Queues an entity to be added to the pool.
entity
- the entity to queue
pool:flush()
Adds all of the queued entities to the pool (in the order they were queued). pool:emit('add', entity)
will be called for each entity that's added.
pool:remove(f)
Removes all entities that meet the specified condition. pool:emit('remove', entity)
will be called for each entity that's removed.
f
- a function that takes an entity as an argument and returnstrue
if the entity should be removed.
pool:emit(event, ...)
Calls the function named event
on each system that has it.
event
- the name of the function to call...
- additional arguments to pass to the system's functions
pool:getSystem(systemDefinition)
Gets the pool's instance of a certain system.
systemDefinition
- the table that was used to add the system to the pool
local oopSystem = nata.oop(groupName)
Creates a system that receives events and calls the function of the same name on the entities themselves.
groupName
- the group of entities to call functions on
Nata is still in early development, so feel free to make suggestions about the design! Issues and pull requests are always welcome.