switch to nata
This commit is contained in:
parent
e7197f93fc
commit
0a1d655267
4 changed files with 588 additions and 51 deletions
546
two_player_cleaning_game/libs/nata.lua
Normal file
546
two_player_cleaning_game/libs/nata.lua
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
--- Entity management for Lua.
|
||||
-- @module nata
|
||||
|
||||
local nata = {
|
||||
_VERSION = 'Nata v0.3.3',
|
||||
_DESCRIPTION = 'Entity management for Lua.',
|
||||
_URL = 'https://github.com/tesselode/nata',
|
||||
_LICENSE = [[
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Andrew Minnich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
-- gets the error level needed to make an error appear
|
||||
-- in the user's code, not the library code
|
||||
local function getUserErrorLevel()
|
||||
local source = debug.getinfo(1).source
|
||||
local level = 1
|
||||
while debug.getinfo(level).source == source do
|
||||
level = level + 1
|
||||
end
|
||||
--[[
|
||||
we return level - 1 here and not just level
|
||||
because the level was calculated one function
|
||||
deeper than the function that will actually
|
||||
use this value. if we produced an error *inside*
|
||||
this function, level would be correct, but
|
||||
for the function calling this function, level - 1
|
||||
is correct.
|
||||
]]
|
||||
return level - 1
|
||||
end
|
||||
|
||||
-- gets the name of the function that the user called
|
||||
-- that eventually caused an error
|
||||
local function getUserCalledFunctionName()
|
||||
return debug.getinfo(getUserErrorLevel() - 1).name
|
||||
end
|
||||
|
||||
local function checkCondition(condition, message)
|
||||
if condition then return end
|
||||
error(message, getUserErrorLevel())
|
||||
end
|
||||
|
||||
-- changes a list of types into a human-readable phrase
|
||||
-- i.e. string, table, number -> "string, table, or number"
|
||||
local function getAllowedTypesText(...)
|
||||
local numberOfArguments = select('#', ...)
|
||||
if numberOfArguments >= 3 then
|
||||
local text = ''
|
||||
for i = 1, numberOfArguments - 1 do
|
||||
text = text .. string.format('%s, ', select(i, ...))
|
||||
end
|
||||
text = text .. string.format('or %s', select(numberOfArguments, ...))
|
||||
return text
|
||||
elseif numberOfArguments == 2 then
|
||||
return string.format('%s or %s', select(1, ...), select(2, ...))
|
||||
end
|
||||
return select(1, ...)
|
||||
end
|
||||
|
||||
-- checks if an argument is of the correct type, and if not,
|
||||
-- throws a "bad argument" error consistent with the ones
|
||||
-- lua and love produce
|
||||
local function checkArgument(argumentIndex, argument, ...)
|
||||
for i = 1, select('#', ...) do
|
||||
-- allow tables with the __call metamethod to be treated like functions
|
||||
if select(i, ...) == 'function' then
|
||||
if type(argument) == 'table' and getmetatable(argument).__call then
|
||||
return
|
||||
end
|
||||
end
|
||||
if type(argument) == select(i, ...) then return end
|
||||
end
|
||||
error(
|
||||
string.format(
|
||||
"bad argument #%i to '%s' (expected %s, got %s)",
|
||||
argumentIndex,
|
||||
getUserCalledFunctionName(),
|
||||
getAllowedTypesText(...),
|
||||
type(argument)
|
||||
),
|
||||
getUserErrorLevel()
|
||||
)
|
||||
end
|
||||
|
||||
local function checkOptionalArgument(argumentIndex, argument, ...)
|
||||
if argument == nil then return end
|
||||
checkArgument(argumentIndex, argument, ...)
|
||||
end
|
||||
|
||||
local function removeByValue(t, v)
|
||||
for i = #t, 1, -1 do
|
||||
if t[i] == v then table.remove(t, i) end
|
||||
end
|
||||
end
|
||||
|
||||
local function entityHasKeys(entity, keys)
|
||||
for _, key in ipairs(keys) do
|
||||
if not entity[key] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function filterEntity(entity, filter)
|
||||
if type(filter) == 'table' then
|
||||
return entityHasKeys(entity, filter)
|
||||
elseif type(filter) == 'function' then
|
||||
return filter(entity)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Defines the behaviors of a system.
|
||||
--
|
||||
-- There's no constructor for SystemDefinitions. Rather, you simply
|
||||
-- define a table with functions that correspond to events. These
|
||||
-- events can be named anything you like. Below are the built-in events
|
||||
-- that the pool will automatically call.
|
||||
-- @type SystemDefinition
|
||||
|
||||
--- Called when the pool is first created.
|
||||
-- @function SystemDefinition:init
|
||||
-- @param ... additional arguments that were passed to `nata.new`.
|
||||
|
||||
--- Called when an entity is added to the pool.
|
||||
-- @function SystemDefinition:add
|
||||
-- @tparam table e the entity that was added
|
||||
|
||||
--- Called when an entity is removed from the pool.
|
||||
-- @function SystemDefinition:remove
|
||||
-- @tparam table e the entity that was removed
|
||||
|
||||
--- Called when an entity is added to a group.
|
||||
-- @function SystemDefinition:addToGroup
|
||||
-- @string groupName the name of the group that the entity was added to
|
||||
-- @tparam table e the entity that was added
|
||||
|
||||
--- Called when an entity is removed from a group.
|
||||
-- @function SystemDefinition:removeFromGroup
|
||||
-- @string groupName the name of the group that the entity was removed from
|
||||
-- @tparam table e the entity that was removed
|
||||
|
||||
--- Responds to events in a pool.
|
||||
--
|
||||
-- Systems are not created directly. They're created by the @{Pool}
|
||||
-- according to the @{SystemDefinition}s passed to `nata.new`.
|
||||
-- Each system instance inherits all of the functions of its
|
||||
-- @{SystemDefinition}.
|
||||
-- @type System
|
||||
|
||||
--- The @{Pool} that this system is running on.
|
||||
-- @tfield Pool pool
|
||||
|
||||
--- Manages a subset of entities.
|
||||
-- @type Group
|
||||
|
||||
--- The filter that defines which entities are added to this group.
|
||||
-- Can be either:
|
||||
--
|
||||
-- - A list of required keys
|
||||
-- - A function that takes the entity as the first argument
|
||||
-- and returns true if the entity should be added to the group
|
||||
-- @tfield[opt] table|function filter
|
||||
|
||||
--- A function that specifies how the entities in this group should be sorted.
|
||||
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||
-- @tfield[opt] function sort
|
||||
|
||||
--- A list of all the entities in the group.
|
||||
-- @tfield table entities
|
||||
|
||||
--- A set of all the entities in the group.
|
||||
-- @tfield table hasEntity
|
||||
-- @usage
|
||||
-- print(pool.groups.physical.hasEntity[e]) -- prints "true" if the entity is in the "physical" group, or "nil" if not
|
||||
|
||||
--- Manages entities in a game world.
|
||||
-- @type Pool
|
||||
local Pool = {}
|
||||
Pool.__index = Pool
|
||||
|
||||
--- A list of all the entities in the pool.
|
||||
-- @tfield table entities
|
||||
|
||||
--- A set of all the entities in the pool.
|
||||
-- @tfield table hasEntity
|
||||
-- @usage
|
||||
-- print(pool.hasEntity[e]) -- prints "true" if the entity is in the pool, or "nil" if not
|
||||
|
||||
--- A dictionary of the @{Group}s in the pool.
|
||||
-- @tfield table groups
|
||||
|
||||
--- A field containing any data you want.
|
||||
-- @field data
|
||||
|
||||
---
|
||||
|
||||
function Pool:_validateOptions(options)
|
||||
checkOptionalArgument(1, options, 'table')
|
||||
if not options then return end
|
||||
if options.groups then
|
||||
checkCondition(type(options.groups) == 'table', "groups must be a table")
|
||||
for groupName, groupOptions in pairs(options.groups) do
|
||||
checkCondition(type(groupOptions) == 'table',
|
||||
string.format("options for group '$s' must be a table", groupName))
|
||||
local filter = groupOptions.filter
|
||||
if filter ~= nil then
|
||||
checkCondition(type(filter) == 'table' or type(filter) == 'function',
|
||||
string.format("filter for group '%s' must be a table or function", groupName))
|
||||
end
|
||||
local sort = groupOptions.sort
|
||||
if sort ~= nil then
|
||||
checkCondition(type(sort) == 'function',
|
||||
string.format("sort for group '%s' must be a function", groupName))
|
||||
end
|
||||
end
|
||||
end
|
||||
if options.systems then
|
||||
checkCondition(type(options.systems) == 'table', "systems must be a table")
|
||||
for _, system in ipairs(options.systems) do
|
||||
checkCondition(type(system) == 'table', "all systems must be tables")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Pool:_init(options, ...)
|
||||
self:_validateOptions(options)
|
||||
options = options or {}
|
||||
-- entities that will be added to the pool on the next flush
|
||||
self._queue = {}
|
||||
-- a temporary table for entities that will be added to the pool
|
||||
-- on the current flush (see Pool.flush for more details)
|
||||
self._entitiesToFlush = {}
|
||||
self.entities = {}
|
||||
self.hasEntity = {}
|
||||
self.groups = {}
|
||||
self._systems = {}
|
||||
self._events = {}
|
||||
self.data = options.data or {}
|
||||
local groups = options.groups or {}
|
||||
local systems = options.systems or { nata.oop() }
|
||||
for groupName, groupOptions in pairs(groups) do
|
||||
self.groups[groupName] = {
|
||||
filter = groupOptions.filter,
|
||||
sort = groupOptions.sort,
|
||||
entities = {},
|
||||
hasEntity = {},
|
||||
}
|
||||
end
|
||||
for _, systemDefinition in ipairs(systems) do
|
||||
local system = setmetatable({
|
||||
pool = self,
|
||||
}, { __index = systemDefinition })
|
||||
table.insert(self._systems, system)
|
||||
end
|
||||
self:emit('init', ...)
|
||||
end
|
||||
|
||||
--- Queues an entity to be added to the pool.
|
||||
-- @tparam table entity the entity to add
|
||||
-- @treturn table the queued entity
|
||||
function Pool:queue(entity)
|
||||
table.insert(self._queue, entity)
|
||||
return entity
|
||||
end
|
||||
|
||||
--- Adds the queued entities to the pool. Entities are added
|
||||
-- in the order they were queued.
|
||||
function Pool:flush()
|
||||
--[[
|
||||
Move the currently queued entities to a temporary
|
||||
table. This way, if an add/addToGroup/removeToGroup
|
||||
event emission leads to another entity being queued,
|
||||
it will be saved for the next flush, rather than
|
||||
adding entities to the table we're in the middle
|
||||
of iterating over, which would lead to an array with
|
||||
holes and screw everything up.
|
||||
]]
|
||||
for i = 1, #self._queue do
|
||||
local entity = self._queue[i]
|
||||
self._entitiesToFlush[i] = entity
|
||||
self._queue[i] = nil
|
||||
end
|
||||
for i = 1, #self._entitiesToFlush do
|
||||
local entity = self._entitiesToFlush[i]
|
||||
-- check if the entity belongs in each group and
|
||||
-- add it to/remove it from the group as needed
|
||||
for groupName, group in pairs(self.groups) do
|
||||
if filterEntity(entity, group.filter) then
|
||||
if not group.hasEntity[entity] then
|
||||
table.insert(group.entities, entity)
|
||||
group.hasEntity[entity] = true
|
||||
self:emit('addToGroup', groupName, entity)
|
||||
end
|
||||
if group.sort then group._needsResort = true end
|
||||
elseif group.hasEntity[entity] then
|
||||
removeByValue(group.entities, entity)
|
||||
group.hasEntity[entity] = nil
|
||||
self:emit('removeFromGroup', groupName, entity)
|
||||
end
|
||||
end
|
||||
-- add the entity to the pool if it hasn't been added already
|
||||
if not self.hasEntity[entity] then
|
||||
table.insert(self.entities, entity)
|
||||
self.hasEntity[entity] = true
|
||||
self:emit('add', entity)
|
||||
end
|
||||
self._entitiesToFlush[i] = nil
|
||||
end
|
||||
-- re-sort groups
|
||||
for _, group in pairs(self.groups) do
|
||||
if group._needsResort then
|
||||
table.sort(group.entities, group.sort)
|
||||
group._needsResort = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes entities from the pool.
|
||||
-- @tparam function f the condition upon which an entity should be
|
||||
-- removed. The function should take an entity as the first argument
|
||||
-- and return `true` if the entity should be removed.
|
||||
function Pool:remove(f)
|
||||
checkArgument(1, f, 'function')
|
||||
for groupName, group in pairs(self.groups) do
|
||||
for i = #group.entities, 1, -1 do
|
||||
local entity = group.entities[i]
|
||||
if f(entity) then
|
||||
self:emit('removeFromGroup', groupName, entity)
|
||||
table.remove(group.entities, i)
|
||||
group.hasEntity[entity] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = #self.entities, 1, -1 do
|
||||
local entity = self.entities[i]
|
||||
if f(entity) then
|
||||
self:emit('remove', entity)
|
||||
table.remove(self.entities, i)
|
||||
self.hasEntity[entity] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Registers a function to be called when an event is emitted.
|
||||
-- @string event the event to listen for
|
||||
-- @tparam function f the function to call
|
||||
-- @treturn function the function that was registered
|
||||
function Pool:on(event, f)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
checkArgument(2, f, 'function')
|
||||
self._events[event] = self._events[event] or {}
|
||||
table.insert(self._events[event], f)
|
||||
return f
|
||||
end
|
||||
|
||||
--- Unregisters a function from an event.
|
||||
-- @string event the event to unregister the function from
|
||||
-- @tparam function f the function to unregister
|
||||
function Pool:off(event, f)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
checkArgument(2, f, 'function')
|
||||
if self._events[event] then
|
||||
removeByValue(self._events[event], f)
|
||||
end
|
||||
end
|
||||
|
||||
--- Emits an event. The `system[event]` function will be called
|
||||
-- for each system that has it, and functions registered
|
||||
-- to the event will be called as well.
|
||||
-- @string event the event to emit
|
||||
-- @param ... additional arguments to pass to the functions that are called
|
||||
function Pool:emit(event, ...)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
for _, system in ipairs(self._systems) do
|
||||
if type(system[event]) == 'function' then
|
||||
system[event](system, ...)
|
||||
end
|
||||
end
|
||||
if self._events[event] then
|
||||
for _, f in ipairs(self._events[event]) do
|
||||
f(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets this pool's instance of a system.
|
||||
-- @tparam SystemDefinition systemDefinition the system class to get the instance of
|
||||
-- @treturn System the instance of the system running in this pool
|
||||
function Pool:getSystem(systemDefinition)
|
||||
checkArgument(1, systemDefinition, 'table')
|
||||
for _, system in ipairs(self._systems) do
|
||||
if getmetatable(system).__index == systemDefinition then
|
||||
return system
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
local function validateOopOptions(options)
|
||||
checkOptionalArgument(1, options, 'table')
|
||||
if not options then return end
|
||||
if options.include then
|
||||
checkCondition(type(options.include) == 'table', "include must be a table")
|
||||
end
|
||||
if options.exclude then
|
||||
checkCondition(type(options.exclude) == 'table', "exclude must be a table")
|
||||
end
|
||||
end
|
||||
|
||||
--- Defines the behavior of an OOP system.
|
||||
-- @type OopOptions
|
||||
-- @see nata.oop
|
||||
|
||||
--- A list of events to forward to entities. If not defined,
|
||||
-- the system will forward all events to entities (except
|
||||
-- for the ones in the exclude list).
|
||||
-- @tfield table include
|
||||
|
||||
--- A list of events *not* to forward to entities.
|
||||
-- @tfield table exclude
|
||||
|
||||
--- The name of the group of entities to forward events to.
|
||||
-- If not defined, the system will forward events to all entities.
|
||||
-- @tfield string group
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
--- Creates a new OOP system definition.
|
||||
-- An OOP system, upon receiving an event, will call
|
||||
-- the function of the same name on each entity it monitors
|
||||
-- (if it exists). This facilitates a more traditional, OOP-style
|
||||
-- entity management, where you loop over a table of entities and
|
||||
-- call update and draw functions on them.
|
||||
-- @tparam[opt] OopOptions options how to set up the OOP system
|
||||
-- @treturn SystemDefinition the new OOP system definition
|
||||
function nata.oop(options)
|
||||
validateOopOptions(options)
|
||||
local group = options and options.group
|
||||
local include, exclude
|
||||
if options and options.include then
|
||||
include = {}
|
||||
for _, event in ipairs(options.include) do
|
||||
include[event] = true
|
||||
end
|
||||
end
|
||||
if options and options.exclude then
|
||||
exclude = {}
|
||||
for _, event in ipairs(options.exclude) do
|
||||
exclude[event] = true
|
||||
end
|
||||
end
|
||||
return setmetatable({ _cache = {} }, {
|
||||
__index = function(t, event)
|
||||
t._cache[event] = t._cache[event] or function(self, ...)
|
||||
local shouldCallEvent = true
|
||||
if include and not include[event] then shouldCallEvent = false end
|
||||
if exclude and exclude[event] then shouldCallEvent = false end
|
||||
if shouldCallEvent then
|
||||
local entities
|
||||
-- not using ternary here because if the group doesn't exist,
|
||||
-- i'd rather it cause an error than just silently falling back
|
||||
-- to the main entity pool
|
||||
if group then
|
||||
entities = self.pool.groups[group].entities
|
||||
else
|
||||
entities = self.pool.entities
|
||||
end
|
||||
for _, entity in ipairs(entities) do
|
||||
if type(entity[event]) == 'function' then
|
||||
entity[event](entity, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return t._cache[event]
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
--- Defines the filter and sort function for a @{Group}.
|
||||
-- @type GroupOptions
|
||||
|
||||
--- The filter that defines which entities are added to this group.
|
||||
-- Can be either:
|
||||
--
|
||||
-- - A list of required keys
|
||||
-- - A function that takes the entity as the first argument
|
||||
-- and returns true if the entity should be added to the group
|
||||
-- @tfield[opt] table|function filter
|
||||
|
||||
--- A function that specifies how the entities in this group should be sorted.
|
||||
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||
-- @tfield[opt] function sort
|
||||
|
||||
--- Defines the groups and systems for a @{Pool}.
|
||||
-- @type PoolOptions
|
||||
|
||||
--- A dictionary of groups for the pool to have.
|
||||
-- Each key is the name of the group, and each value
|
||||
-- should be a @{GroupOptions} table.
|
||||
-- @tfield[opt={}] table groups
|
||||
|
||||
--- A list of @{SystemDefinition}s for the pool to use.
|
||||
-- @tfield[opt={nata.oop()}] table systems
|
||||
|
||||
--- An initial value to set @{Pool.data} to.
|
||||
-- @field[opt={}] data
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
--- Creates a new @{Pool}.
|
||||
-- @tparam[opt] PoolOptions options how to set up the pool
|
||||
-- @param[opt] ... additional arguments to pass to the pool's init event
|
||||
-- @treturn Pool the new pool
|
||||
function nata.new(options, ...)
|
||||
local pool = setmetatable({}, Pool)
|
||||
pool:_init(options, ...)
|
||||
return pool
|
||||
end
|
||||
|
||||
return nata
|
||||
Loading…
Add table
Add a link
Reference in a new issue