-
Notifications
You must be signed in to change notification settings - Fork 2
/
nata.lua
546 lines (486 loc) · 16.7 KB
/
nata.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
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