mirror of
				https://github.com/Reuh/ubiquitousse.git
				synced 2025-10-27 17:19:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			909 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			909 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| --[[-- ECS ([entity compenent system](https://en.wikipedia.org/wiki/Entity_component_system)) library for Lua.
 | |
| 
 | |
| Entity Component System library, inspired by the excellent [tiny-ecs](https://github.com/bakpakin/tiny-ecs/tree/master) by bakpakin.
 | |
| Main differences include:
 | |
| 
 | |
| * ability to nest systems (more organisation potential);
 | |
| * instanciation of systems for each world (no shared state) (several worlds can coexist at the same time easily);
 | |
| * adding and removing entities is done instantaneously (no going isane over tiny-ecs cache issues);
 | |
| * ability to add and remove components from entities after they were added to the world (more dynamic entities).
 | |
| 
 | |
| And a fair amount of other quality-of-life features.
 | |
| 
 | |
| The goals of this library are similar in spirit to tiny-ecs: simple to use, flexible, and useful.
 | |
| The more advanced features it provides relative to tiny-ecs are made so that you can completely ignore them
 | |
| if you don't use them.
 | |
| 
 | |
| The module returns a table that contains several functions, `world` or `scene` are starting points
 | |
| to create your world.
 | |
| 
 | |
| No mandatory dependency.
 | |
| Optional dependency: `ubiquitousse.scene`, to allow quick creation of ECS-based scenes (`ecs.scene`).
 | |
| 
 | |
| @module ecs
 | |
| @usage
 | |
| -- Same example as tiny-ecs', for comparaison purposes
 | |
| 
 | |
| local ecs = require("ubiquitousse.ecs")
 | |
| 
 | |
| local talkingSystem = {
 | |
| 	filter = { "name", "mass", "phrase" },
 | |
| 	process = function(self, e, c, dt)
 | |
| 		e.mass = e.mass + dt * 3
 | |
| 		print(("%s who weighs %d pounds, says %q."):format(e.name, e.mass, e.phrase))
 | |
| 	end
 | |
| }
 | |
| 
 | |
| local joe = {
 | |
| 	name = "Joe",
 | |
| 	phrase = "I'm a plumber.",
 | |
| 	mass = 150,
 | |
| 	hairColor = "brown"
 | |
| }
 | |
| 
 | |
| local world = ecs.world(talkingSystem)
 | |
| world:add(joe)
 | |
| 
 | |
| for i = 1, 20 do
 | |
| 	world:update(1)
 | |
| end
 | |
| --]]
 | |
| local loaded, scene
 | |
| if ... then loaded, scene = pcall(require, (...):match("^(.-)ecs").."scene") end
 | |
| if not loaded then scene = nil end
 | |
| 
 | |
| let ecs
 | |
| 
 | |
| -- TODO: Implement a skip list for faster search.
 | |
| -- better control over system order: process, draw, methods? (for lag reasons and dependencies)
 | |
| 
 | |
| --- Entities are regular tables that get processed by `System`s.
 | |
| --
 | |
| -- The idea is that entities _should_ only contain data and no code; it's the systems that are responsible for the actual processing
 | |
| -- (but it's your game, do as you want).
 | |
| --
 | |
| -- This data is referred to, and organized in, "components".
 | |
| -- @type Entity
 | |
| 
 | |
| --[[-- Components.
 | |
| 
 | |
| Entities are Lua tables, and thus contain key-values pairs: each one of these pairs is called a "component". The data can be
 | |
| whatever you want, and ideally each component _should_ store the data for one singular aspect of the entity, for example its position, name, etc.
 | |
| 
 | |
| This library does not do any kind of special processing by itself on the entity tables and take them as is (no metatable, no methamethods, etc.),
 | |
| so you are free to handle them as you want in your systems or elsewhere.
 | |
| 
 | |
| Since it's relatively common for systems to only operate on a single component, as a shortcut the library often consider what it calls the "system component":
 | |
| that is, the component in the entity that is named like `System.component` (or `System.name` if it is not set). Though there's no problem if there's no system
 | |
| component or if it doesn't exist in the entity.
 | |
| 
 | |
| @doc Component
 | |
| @usage
 | |
| -- example entity
 | |
| local entity = {
 | |
| 	position = { x = 0, y = 52 }, -- a "position" component
 | |
| 	sprite = newSprite("awesomeguy.png") -- a "sprite" component
 | |
| }
 | |
| 
 | |
| -- example "sprite" system
 | |
| local sprite = {
 | |
| 	name = "sprite",
 | |
| 	filter = "sprite", -- process entities that have a "sprite" component
 | |
| 	-- systems callbacks that are called per-entity often give you the system component as an argument
 | |
| 	-- the system component is the component with the same name as the system, thus here the sprite component
 | |
| 	render = function(self, entity, component)
 | |
| 		-- component == entity.sprite
 | |
| 		draw(component)
 | |
| 	end
 | |
| }
 | |
| ]]--
 | |
| 
 | |
| --- Recursively remove subsystems from a system.
 | |
| let recDestroySystems = (system)
 | |
| 	for i=#system.systems, 1, -1 do
 | |
| 		let s = system.systems[i]
 | |
| 		recDestroySystems(s)
 | |
| 		s:onDestroy()
 | |
| 		system.systems[i] = nil
 | |
| 		if s.name then
 | |
| 			system.world.s[s.name] = nil
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| --- Recursively call :clear and :onRemoveFromWorld to a list of systems in a world.
 | |
| let recCallOnRemoveFromWorld = (world, systems)
 | |
| 	for _, s in ipairs(systems) do
 | |
| 		s:clear()
 | |
| 		recCallOnRemoveFromWorld(world, s.systems)
 | |
| 		s:onRemoveFromWorld(world)
 | |
| 	end
 | |
| end
 | |
| 
 | |
| --- Iterate through the next entity, based on state s: { previousLinkedListItem }
 | |
| let nextEntity = (s)
 | |
| 	if s[1] then
 | |
| 		let var = s[1][1]
 | |
| 		s[1] = s[1][2]
 | |
| 		return var
 | |
| 	else
 | |
| 		return nil
 | |
| 	end
 | |
| end
 | |
| 
 | |
| --- Recursively copy content of a into b if it isn't already present.
 | |
| -- Don't copy keys, will preserve metatable but not copy them.
 | |
| let copy = (a, b, cache={})
 | |
| 	for k, v in pairs(a) do
 | |
| 		if type(v) == "table" then
 | |
| 			if b[k] == nil then
 | |
| 				if cache[v] then
 | |
| 					b[k] = cache[v]
 | |
| 				else
 | |
| 					cache[v] = {}
 | |
| 					b[k] = cache[v]
 | |
| 					copy(v, b[k], cache)
 | |
| 					setmetatable(b[k], getmetatable(v))
 | |
| 				end
 | |
| 			elseif type(b[k]) == "table" then
 | |
| 				copy(v, b[k], cache)
 | |
| 			end
 | |
| 		elseif b[k] == nil then
 | |
| 			b[k] = v
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| --[[-- Systems and Worlds.
 | |
| Systems are what do the processing on your entities. A system contains a list of entities; the entities in this list are selected
 | |
| using a `filter`, and the system will only operate on those filtered entities.
 | |
| 
 | |
| A system can also be created that do not accept any entity (`filter = false`, this is the default): such a system can still be
 | |
| used to do processing that don't need to be done per-entity but still behave like other systems (e.g. to do some static calculation each update).
 | |
| 
 | |
| The system also contains `callbacks`, these define the actual processing done on the system and its entities and you will want to redefine
 | |
| at least one of them to make your system actually do something.
 | |
| 
 | |
| Then you can call `System:update`, `System:draw`, `System:emit` or `System:callback` at appropriate times and the system will call the
 | |
| associated callbacks on itself and its entities, and then pass it to its subsystems. In practise you would likely only call these on
 | |
| the world system, so the callbacks are correctly propagated to every single system in the world.
 | |
| 
 | |
| Systems are defined as regular tables with all the fields and methods you need in it. However, when a system is added to
 | |
| a world, the table you defined is not used directly, but we use what we call an "instancied system": think of it of an instance of your system
 | |
| like if it were a class.
 | |
| The instancied system will have a metatable set that gives it some methods and fields defined by the library on top of what you defined.
 | |
| Modifying the instancied system will only modify this instance and not the original system you defined, so several instances of your system
 | |
| can exist in different worlds (note that the original system is not copied on instancing; if you reference a table in the original system it will use the
 | |
| original table directly).
 | |
| 
 | |
| Systems can have subsystems; that is a system that behave as an extension of their parent system. They only operates on the entities already
 | |
| present in their parent subsystem, only update when their parent system updates, etc. You can thus organize your systems in a hierarchy to
 | |
| avoid repeating your filters or allow controlling several system from a single parent system.
 | |
| 
 | |
| The top-level system is called the "world"; it behaves in exactly the same way as other systems, and accept every entity by default.
 | |
| 
 | |
| @type System
 | |
| @usage
 | |
| local sprite = {
 | |
| 	filter = { "sprite", "position" }, -- only operate on entities with "sprite" and "position" components
 | |
| 	systems = { animated }, -- subsystems: they only operate on entities already filtered by this system (on top of their own filtering)
 | |
| 
 | |
| 	-- Called when an entity is added to this system.
 | |
| 	onAdd = function(self, entity, component)
 | |
| 		print("Added an entity, entity count in the system:", self.entityCount) -- self refer to the instancied system
 | |
| 	end,
 | |
| 
 | |
| 	-- Called when the system is updated, for every entity the system
 | |
| 	process = function(self, entity, component, dt)
 | |
| 		-- processing...
 | |
| 	end
 | |
| }
 | |
| 
 | |
| local world = ecs.world(system) -- instanciate a world with the sprite system (and all its subsystems)
 | |
| 
 | |
| -- Add an entity: doesn't pass the filtering, so nothing happens
 | |
| world:add {
 | |
| 	name = "John"
 | |
| }
 | |
| 
 | |
| -- Added to the sprite system! Call sprite:onAdd, and also try to add it to its subsystems
 | |
| world:add {
 | |
| 	sprite = newSprite("example.png"),
 | |
| 	position = { x=5, y=0 }
 | |
| }
 | |
| 
 | |
| -- Trigger sprite:onUpdate and sprite:process callbacks
 | |
| world:update(dt)
 | |
| --]]
 | |
| let system_mt = {
 | |
| 	--- Modifiable fields.
 | |
| 	--
 | |
| 	-- Every field defined below is optional and can be accessed or redefined at any time, unless written otherwise. Though you would typically set them
 | |
| 	-- before instanciating your systems.
 | |
| 	-- @doc modifiable
 | |
| 
 | |
| 	--- Name of the system.
 | |
| 	-- Used to create a field with the system's name in `world.s` and determine the associated system component if `System.component` is not set.
 | |
| 	-- If not set, the system will not appear in `world.s`.
 | |
| 	--
 | |
| 	-- Do not change after system instanciation.
 | |
| 	-- @ftype string
 | |
| 	-- @ftype nil if no name
 | |
| 	name = nil,
 | |
| 
 | |
| 	--- List of subsystems.
 | |
| 	-- On a instancied system, this is a list of the same subsystems, but instancied for this world.
 | |
| 	--
 | |
| 	-- Do not change after system instanciation.
 | |
| 	-- @ftype table
 | |
| 	-- @ftype nil if no subsystem
 | |
| 	systems = nil,
 | |
| 
 | |
| 	--- If not `false`, the system will only update every interval seconds.
 | |
| 	-- `false` by default.
 | |
| 	-- @ftype number interval of time between each update
 | |
| 	-- @ftype false to disable
 | |
| 	interval = false,
 | |
| 	--- The system and its susbsystems will only update if this is `true`.
 | |
| 	-- `true` by default.
 | |
| 	-- @ftype boolean
 | |
| 	active = true,
 | |
| 	--- The system and its subsystems will only draw if this is `true`.
 | |
| 	-- `true` by default.
 | |
| 	-- @ftype boolean
 | |
| 	visible = true,
 | |
| 
 | |
| 	--- Name of the system component.
 | |
| 	-- Used to determine the associated system component.
 | |
| 	-- If not set, this will fall back to `System.name`. If this is also not set, then we will give `nil` instead of the system component in callbacks.
 | |
| 	-- @ftype string
 | |
| 	-- @ftype nil if no name
 | |
| 	component = nil,
 | |
| 	--- Defaults value to put into the entities's system component when they are added.
 | |
| 	--
 | |
| 	-- If this is table, will recursively fill missing values.
 | |
| 	-- Metatables will be preserved during the copy but not copied themselves.
 | |
| 	--
 | |
| 	-- Changing this will not affect entities already in the system.
 | |
| 	-- Doesn't have any effect if the system doesn't have a component name.
 | |
| 	-- @ftype any
 | |
| 	-- @ftype nil if no default
 | |
| 	default = nil,
 | |
| 
 | |
| 	--- Callbacks.
 | |
| 	--
 | |
| 	-- Functions that are called when something happens in the system.
 | |
| 	-- Redefine them to change system behaviour.
 | |
| 	-- @doc callbacks
 | |
| 
 | |
| 	--- Called when checking if an entity should be added to this system.
 | |
| 	-- Returns `true` if the entity should be added to this system (and therefore its subsystems).
 | |
| 	--
 | |
| 	-- If this is a string or a table, it will be converted to a filter function on instanciation using `ecs.all`.
 | |
| 	--
 | |
| 	-- If this `true`, will accept every entity; if `false`, reject every entity.
 | |
| 	--
 | |
| 	-- Will only test entities when they are added; changing this after system creation will not affect entities already in the system.
 | |
| 	--
 | |
| 	-- By default, rejects everything.
 | |
| 	-- @callback
 | |
| 	-- @tparam table e entity table to check
 | |
| 	-- @treturn boolean `true` if entity should be added
 | |
| 	filter = :(e) return false end,
 | |
| 	--- Called when adding an entity to this system determining its order.
 | |
| 	-- Returns `true` if `e1 <=` e2 (i.e., if `e1` should be processed before `e2` in this system). e1 and e2 are two entities.
 | |
| 	--
 | |
| 	-- Used to place the entity in the sorted entity list when it is added; changing this after system creation
 | |
| 	-- will not change the order of entities already in the system.
 | |
| 	--
 | |
| 	-- By default, new entities are added at the start of the list.
 | |
| 	-- @callback
 | |
| 	-- @tparam Entity e1 entity table to check for inferiority
 | |
| 	-- @tparam Entity e2 entity table to check for superiority
 | |
| 	-- @treturn boolean `true` if e1 <= e2
 | |
| 	compare = :(e1, e2) return true end,
 | |
| 
 | |
| 	--- Called when adding an entity to the system.
 | |
| 	-- @callback
 | |
| 	-- @tparam Entity e the entity table
 | |
| 	-- @tparam Component c the entity's system component, if any
 | |
| 	onAdd = :(e, c) end,
 | |
| 	--- Called when removing an entity from the system.
 | |
| 	-- @callback
 | |
| 	-- @tparam Entity e the entity table
 | |
| 	-- @tparam Component c the entity's system component, if any
 | |
| 	onRemove = :(e, c) end,
 | |
| 	--- Called when the system is instancied, before any call to `System:onAddToWorld` (including other systems in the world).
 | |
| 	-- @callback
 | |
| 	onInstance = :() end,
 | |
| 	--- Called when the system is added to a world.
 | |
| 	-- @callback
 | |
| 	-- @tparam System world world system
 | |
| 	onAddToWorld = :(world) end,
 | |
| 	--- Called when the system is removed from a world (i.e., the world is destroyed).
 | |
| 	-- @callback
 | |
| 	-- @tparam System world world system
 | |
| 	onRemoveFromWorld = :(world) end,
 | |
| 	--- Called when the world is destroyed, after every call to `System:onRemoveFromWorld` (including other systems in the world).
 | |
| 	-- @callback
 | |
| 	onDestroy = :() end,
 | |
| 	--- Called when updating the system.
 | |
| 	-- Called before any call to `System:process` or call to subsystems.
 | |
| 	-- @callback
 | |
| 	-- @number dt delta-time since last update
 | |
| 	onUpdate = :(dt) end,
 | |
| 	--- Called when drawing the system.
 | |
| 	-- Called before any call to `System:draw` or call to subsystems.
 | |
| 	-- @callback
 | |
| 	onDraw = :() end,
 | |
| 	--- Called when updating the system, for every entity the system contains.
 | |
| 	-- Called after `System:onUpdate` was called on the system, and before any call to subsystems.
 | |
| 	-- @callback
 | |
| 	-- @tparam Entity e the entity table
 | |
| 	-- @tparam Component c the entity's system component, if any
 | |
| 	-- @number dt delta-time since last update
 | |
| 	process = :(e, c, dt) end,
 | |
| 	--- Called when drawing the system, for every entity the system contains.
 | |
| 	-- Called after `System:onDraw` was called on the system, and before any call to subsystems.
 | |
| 	-- @callback
 | |
| 	-- @tparam Entity e the entity table
 | |
| 	-- @tparam Component c the entity's system component, if any
 | |
| 	render = :(e, c) end,
 | |
| 	--- Called after updating the system.
 | |
| 	-- Called after `System:onDraw`, `System:process` and calls to subsystems.
 | |
| 	-- @callback
 | |
| 	-- @number dt delta-time since last update
 | |
| 	onUpdateEnd = :(dt) end,
 | |
| 	--- Called after drawing the system.
 | |
| 	-- Called after `System:onUpdate`, `System:render` and calls to subsystems.
 | |
| 	-- @callback
 | |
| 	onDrawEnd = :() end,
 | |
| 
 | |
| 	--- Read-only fields.
 | |
| 	--
 | |
| 	-- Fields available on instancied systems. Don't modify them unless you like broken things.
 | |
| 	-- @doc ro
 | |
| 
 | |
| 	--- The world the system belongs to.
 | |
| 	-- @ftype System world
 | |
| 	-- @ro
 | |
| 	world = nil,
 | |
| 	--- Shortcut to `System.world`.
 | |
| 	-- @ftype System world
 | |
| 	-- @ro
 | |
| 	w = nil,
 | |
| 	--- Number of entities in the system.
 | |
| 	-- @ftype integer
 | |
| 	-- @ro
 | |
| 	entityCount = 0,
 | |
| 	--- Map of all named systems in the world (not only subsystems). Same for every system from the same world.
 | |
| 	-- @ftype table {[system.name]=instanciedSystem, ...}
 | |
| 	-- @ro
 | |
| 	s = nil,
 | |
| 
 | |
| 	--- Private fields ---
 | |
| 
 | |
| 	--- First element of the linked list of entities: { entity, next_element }.
 | |
| 	-- @local
 | |
| 	_first = nil,
 | |
| 	--- Associative map of entities in the system and their previous linked list element (or `true` if first element).
 | |
| 	-- This make the list effectively a doubly linked list, but with easy access to the previous element using this map (and therefore O(1) deletion).
 | |
| 	-- @local
 | |
| 	_previous = nil,
 | |
| 	--- Amount of time waited since last update (if interval is set).
 | |
| 	-- @local
 | |
| 	_waited = 0,
 | |
| 
 | |
| 	--- Methods.
 | |
| 	--
 | |
| 	-- Methods available on instancied systems.
 | |
| 	-- @doc smethods
 | |
| 
 | |
| 	--- Add entities to the system and its subsystems.
 | |
| 	--
 | |
| 	-- Will skip entities that are already in the system.
 | |
| 	--
 | |
| 	-- Entities are added to subsystems after they were succesfully added to their parent system.
 | |
| 	--
 | |
| 	-- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters.
 | |
| 	-- If you do that, since `System:remove` will not search for entities in systems where they should have been filtered out, the added entities will not be removed
 | |
| 	-- when calling `System:remove` on a parent system or the world. The entity can be removed by calling `System:remove` on the system `System:add` was called on.
 | |
| 	--
 | |
| 	-- Complexity: O(1) per unordered system, O(entityCount) per ordered system.
 | |
| 	-- @tparam Entity e entity to add
 | |
| 	-- @tparam Entity... ... other entities to add
 | |
| 	-- @treturn Entity,... `e,...` the function arguments
 | |
| 	add = :(e, ...)
 | |
| 		if e ~= nil and not @_previous[e] and @filter(e) then
 | |
| 			-- copy default system component
 | |
| 			if @component and @default then
 | |
| 				copy({ [@component] = @default }, e)
 | |
| 			end
 | |
| 			-- add to linked list
 | |
| 			if @_first == nil then
 | |
| 				@_first = { e, nil }
 | |
| 				@_previous[e] = true
 | |
| 			elseif @compare(e, @_first[1]) then
 | |
| 				let nxt = @_first
 | |
| 				@_first = { e, nxt }
 | |
| 				@_previous[e] = true
 | |
| 				@_previous[nxt[1]] = @_first
 | |
| 			else
 | |
| 				let entity = @_first
 | |
| 				while entity[2] ~= nil do
 | |
| 					if @compare(e, entity[2][1]) then
 | |
| 						let nxt = entity[2]
 | |
| 						entity[2] = { e, nxt }
 | |
| 						@_previous[e] = entity
 | |
| 						@_previous[nxt[1]] = entity[2]
 | |
| 						break
 | |
| 					end
 | |
| 					entity = entity[2]
 | |
| 				end
 | |
| 				if entity[2] == nil then
 | |
| 					entity[2] = { e, nil }
 | |
| 					@_previous[e] = entity
 | |
| 				end
 | |
| 			end
 | |
| 			-- notify addition
 | |
| 			@entityCount += 1
 | |
| 			@onAdd(e, e[@component])
 | |
| 			-- add to subsystems (if it wasn't immediately removed in onAdd)
 | |
| 			if @_previous[e] then
 | |
| 				for _, s in ipairs(@systems) do
 | |
| 					s:add(e)
 | |
| 				end
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @add(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Remove entities from the system and its subsystems.
 | |
| 	--
 | |
| 	-- Will skip entities that are not in the system.
 | |
| 	--
 | |
| 	-- Entities are removed from subsystems before they are removed from their parent system.
 | |
| 	--
 | |
| 	-- If you intend to call this on a subsystem instead of the world, please read the warning in `System:add`.
 | |
| 	--
 | |
| 	-- Complexity: O(1) per system.
 | |
| 	-- @tparam Entity e entity to remove
 | |
| 	-- @tparam Entity... ... other entities to remove
 | |
| 	-- @treturn Entity,... `e,...` the function arguments
 | |
| 	remove = :(e, ...)
 | |
| 		if e ~= nil then
 | |
| 			if @_previous[e] then
 | |
| 				-- remove from subsystems
 | |
| 				for _, s in ipairs(@systems) do
 | |
| 					s:remove(e)
 | |
| 				end
 | |
| 			end
 | |
| 			if @_previous[e] then -- recheck in case it was removed already from a subsystem onRemove callback
 | |
| 				-- remove from linked list
 | |
| 				let prev = @_previous[e]
 | |
| 				if prev == true then
 | |
| 					@_first = @_first[2]
 | |
| 					if @_first then
 | |
| 						@_previous[@_first[1]] = true
 | |
| 					end
 | |
| 				else
 | |
| 					prev[2] = prev[2][2]
 | |
| 					if prev[2] then
 | |
| 						@_previous[prev[2][1]] = prev
 | |
| 					end
 | |
| 				end
 | |
| 				-- notify removal
 | |
| 				@_previous[e] = nil
 | |
| 				@entityCount -= 1
 | |
| 				@onRemove(e, e[@component])
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @remove(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Refresh an entity's systems.
 | |
| 	--
 | |
| 	-- Behave similarly to `System:add`, but if the entity is already in the system, instead of skipping it, it
 | |
| 	-- will check for new and removed components and add and remove from (sub)systems accordingly.
 | |
| 	--
 | |
| 	-- Complexity: O(1) per system + add/remove complexity.
 | |
| 	-- @tparam Entity e entity to refresh
 | |
| 	-- @tparam Entity... ... other entities to refresh
 | |
| 	-- @treturn Entity,... `e,...` the function arguments
 | |
| 	refresh = :(e, ...)
 | |
| 		if e ~= nil then
 | |
| 			if not @_previous[e] then
 | |
| 				@add(e)
 | |
| 			elseif @_previous[e] then
 | |
| 				if not @filter(e) then
 | |
| 					@remove(e)
 | |
| 				else
 | |
| 					for _, s in ipairs(@systems) do
 | |
| 						s:refresh(e)
 | |
| 					end
 | |
| 				end
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @refresh(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Reorder an entity.
 | |
| 	--
 | |
| 	-- Will recalculate the entity position in the entity list for this system and its subsystems.
 | |
| 	-- Will skip entities that are not in the system.
 | |
| 	--
 | |
| 	-- Complexity: O(entityCount) per system.
 | |
| 	-- @tparam Entity e entity to reorder
 | |
| 	-- @tparam Entity... ... other entities to reorder
 | |
| 	-- @treturn Entity,... `e,...` the function arguments
 | |
| 	reorder = :(e, ...)
 | |
| 		if e ~= nil then
 | |
| 			if @_previous[e] then
 | |
| 				let prev = @_previous[e] -- { prev, { e, next } }
 | |
| 				let next = prev == true and @_first[2] or prev[2][2]
 | |
| 				-- remove e from linked list
 | |
| 				if prev == true then
 | |
| 					@_first = @_first[2]
 | |
| 				else
 | |
| 					prev[2] = next
 | |
| 				end
 | |
| 				if next then
 | |
| 					@_previous[next[1]] = prev
 | |
| 				end
 | |
| 				-- find position so that prev < e <= next
 | |
| 				while prev ~= true and @compare(e, prev[1]) do -- ensure prev < e
 | |
| 					next = prev
 | |
| 					prev = @_previous[prev[1]]
 | |
| 				end
 | |
| 				while next ~= nil and not @compare(e, next[1]) do -- ensure e <= next
 | |
| 					prev = next
 | |
| 					next = next[2]
 | |
| 				end
 | |
| 				-- reinsert e in linked list
 | |
| 				let new = { e, next }
 | |
| 				@_previous[e] = prev
 | |
| 				if next then
 | |
| 					@_previous[next[1]] = new
 | |
| 				end
 | |
| 				if prev == true then
 | |
| 					@_first = new
 | |
| 				else
 | |
| 					prev[2] = new
 | |
| 				end
 | |
| 				-- Reorder in subsystems
 | |
| 				for _, s in ipairs(@systems) do
 | |
| 					s:reorder(e)
 | |
| 				end
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @reorder(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Returns `true` if all these entities are in the system.
 | |
| 	--
 | |
| 	-- Complexity: O(1).
 | |
| 	-- @tparam Entity e entity that may be in the system
 | |
| 	-- @tparam Entity... ... other entities that may be in the system
 | |
| 	-- @treturn boolean `true` if every entity is in the system
 | |
| 	has = :(e, ...)
 | |
| 		let has = e == nil or not not @_previous[e]
 | |
| 		if ... then
 | |
| 			return has and @has(...)
 | |
| 		else
 | |
| 			return has
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Returns an iterator that iterate through the entties in this system, in order.
 | |
| 	-- @treturn iterator iterator over the entities in this system
 | |
| 	iter = :()
 | |
| 		return nextEntity, { @_first }
 | |
| 	end,
 | |
| 	--- Get the `i`th entity in the system.
 | |
| 	-- This is a simple wrapper around `iter`; it _will_ iterate over all the entities in the system in order until we reach the desired one.
 | |
| 	-- Complexity: O(i)
 | |
| 	-- @tparam number i the index of the entity
 | |
| 	-- @treturn Entity the entity; `nil` if there is no such entity in the system
 | |
| 	get = :(i)
 | |
| 		local n = 1
 | |
| 		for e in @iter() do
 | |
| 			if n == i then
 | |
| 				return e
 | |
| 			end
 | |
| 			n += 1
 | |
| 		end
 | |
| 		return nil
 | |
| 	end,
 | |
| 	--- Remove every entity from the system and its subsystems.
 | |
| 	clear = :()
 | |
| 		for e in @iter() do
 | |
| 			@remove(e)
 | |
| 		end
 | |
| 		for _, s in ipairs(@systems) do
 | |
| 			s:clear()
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Try to update the system and its subsystems. Should be called on every game update.
 | |
| 	--
 | |
| 	-- Subsystems are updated after their parent system.
 | |
| 	-- @number dt delta-time since last update
 | |
| 	update = :(dt)
 | |
| 		if @active then
 | |
| 			if @interval then
 | |
| 				@_waited += dt
 | |
| 				if @_waited < @interval then
 | |
| 					return
 | |
| 				end
 | |
| 			end
 | |
| 			@onUpdate(dt)
 | |
| 			if @process ~= system_mt.process then
 | |
| 				for e in @iter() do
 | |
| 					@process(e, e[@component], dt)
 | |
| 				end
 | |
| 			end
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				s:update(dt)
 | |
| 			end
 | |
| 			@onUpdateEnd(dt)
 | |
| 			if @interval then
 | |
| 				@_waited -= @interval
 | |
| 			end
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Try to draw the system and its subsystems. Should be called on every game draw.
 | |
| 	--
 | |
| 	-- Subsystems are drawn after their parent system.
 | |
| 	draw = :()
 | |
| 		if @visible then
 | |
| 			@onDraw()
 | |
| 			if @render ~= system_mt.render then
 | |
| 				for e in @iter() do
 | |
| 					@render(e, e[@component])
 | |
| 				end
 | |
| 			end
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				s:draw()
 | |
| 			end
 | |
| 			@onDrawEnd()
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Trigger a custom callback on a single entity.
 | |
| 	--
 | |
| 	-- This will call the `System:name(e, c, ...)` method in this system and its subsystems,
 | |
| 	-- if the method exists and the entity is in the system. `c` is the system [component](#Entity.Component)
 | |
| 	-- associated with the current system, and `e` is the `Entity`.
 | |
| 	--
 | |
| 	-- Think of it as a way to perform custom callbacks issued from an entity event, similar to `System:onAdd`.
 | |
| 	-- @tparam string name name of the callback
 | |
| 	-- @tparam Entity e the entity to perform the callback on
 | |
| 	-- @param ... other arguments to pass to the callback
 | |
| 	callback = :(name, e, ...)
 | |
| 		-- call callback
 | |
| 		if @_previous[e] and @[name] then
 | |
| 			@[name](@, e, e[@component], ...)
 | |
| 		end
 | |
| 		-- callback on subsystems (if it wasn't removed during the callback)
 | |
| 		if @_previous[e] then
 | |
| 			for _, ss in ipairs(@systems) do
 | |
| 				ss:callback(name, e, ...)
 | |
| 			end
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Emit an event on the system.
 | |
| 	--
 | |
| 	-- This will call the `System:name(...)` method in this system and its subsystems,
 | |
| 	-- if the method exists.
 | |
| 	--
 | |
| 	-- Think of it as a way to perform custom callbacks issued from a general event, similar to `System:onUpdate`.
 | |
| 	--
 | |
| 	-- The called methods may return a string value to affect the event propagation behaviour:
 | |
| 	--
 | |
| 	-- * if a callback returns `"stop"`, the event will not be propagated to the subsystems.
 | |
| 	-- * if a callback returns `"capture"`, the event will not be propagated to the subsystems _and_
 | |
| 	--   its sibling systems (i.e. completely stop the propagation of the event).
 | |
| 	--
 | |
| 	-- `"stop"` would be for example used to disable some behaviour in the system and its subsystems (like `active = false` can
 | |
| 	-- disable `System:onUpdate` behaviour on the system and its subsystems).
 | |
| 	--
 | |
| 	-- `"capture"` would be for example used to prevent other systems from handling the event (for example to make sure an
 | |
| 	-- input event is handled only once by a single system).
 | |
| 	--
 | |
| 	-- @tparam string name name of the callback
 | |
| 	-- @param ... other arguments to pass to the callback
 | |
| 	emit = :(name, ...)
 | |
| 		-- call event
 | |
| 		let status
 | |
| 		if @[name] then
 | |
| 			status = @[name](@, ...)
 | |
| 		end
 | |
| 		-- call event on subsystems (if it wasn't stopped or captured)
 | |
| 		if status ~= "stop" and status ~= "capture" then
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				status = s:emit(name, ...)
 | |
| 				if status == "capture" then break end
 | |
| 			end
 | |
| 		end
 | |
| 		return status
 | |
| 	end,
 | |
| 	--- Remove all the entities and subsystems in this system.
 | |
| 	destroy = :()
 | |
| 		recCallOnRemoveFromWorld(@world, { @ })
 | |
| 		recDestroySystems({ systems = { @ } })
 | |
| 	end,
 | |
| }
 | |
| 
 | |
| --- Self descriptive
 | |
| let alwaysTrue = () return true end
 | |
| let alwaysFalse = () return false end
 | |
| 
 | |
| --- Recursively instanciate a list of systems for a world:
 | |
| -- * create their self table with instance fields set
 | |
| -- * create a field with their name in world.s (if name defined)
 | |
| let recInstanciateSystems = (world, systems)
 | |
| 	let t = {}
 | |
| 	for _, s in ipairs(systems) do
 | |
| 		let system
 | |
| 		-- instanciate system
 | |
| 		system = setmetatable({
 | |
| 			systems = recInstanciateSystems(world, s.systems or {}),
 | |
| 			world = world,
 | |
| 			w = world,
 | |
| 			s = world.s,
 | |
| 			_previous = {},
 | |
| 		}, {
 | |
| 			__index = :(k)
 | |
| 				if s[k] ~= nil then
 | |
| 					return s[k]
 | |
| 				else
 | |
| 					return system_mt[k]
 | |
| 				end
 | |
| 			end
 | |
| 		})
 | |
| 		-- create filter
 | |
| 		if type(s.filter) == "string" then
 | |
| 			system.filter = (_, e) return e[s.filter] ~= nil end
 | |
| 		elseif type(s.filter) == "table" then
 | |
| 			system.filter = ecs.all(unpack(s.filter))
 | |
| 		elseif type(s.filter) == "boolean" then
 | |
| 			if s.filter then
 | |
| 				system.filter = alwaysTrue
 | |
| 			else
 | |
| 				system.filter = alwaysFalse
 | |
| 			end
 | |
| 		end
 | |
| 		-- system component fallback on system name
 | |
| 		if not s.component and s.name then
 | |
| 			s.component = s.name
 | |
| 		end
 | |
| 		-- add system
 | |
| 		table.insert(t, system)
 | |
| 		if s.name then
 | |
| 			world.s[s.name] = system
 | |
| 		end
 | |
| 		system:onInstance()
 | |
| 	end
 | |
| 	return t
 | |
| end
 | |
| --- Recursively call :onAddToWorld to a list of systems in a world.
 | |
| let recCallOnAddToWorld = (world, systems)
 | |
| 	for _, s in ipairs(systems) do
 | |
| 		recCallOnAddToWorld(world, s.systems)
 | |
| 		s:onAddToWorld(world)
 | |
| 	end
 | |
| end
 | |
| 
 | |
| --- ECS module.
 | |
| -- @section end
 | |
| ecs = {
 | |
| 	--- Create and returns a world system based on a list of systems.
 | |
| 	-- The systems will be instancied for this world.
 | |
| 	-- @tparam table,... ... list of (uninstancied) systems
 | |
| 	-- @treturn System the world system
 | |
| 	world = (...)
 | |
| 		let world = setmetatable({
 | |
| 			filter = ecs.all(),
 | |
| 			s = {},
 | |
| 			_previous = {}
 | |
| 		}, { __index = system_mt })
 | |
| 		world.world = world
 | |
| 		world.w = world
 | |
| 		world.systems = recInstanciateSystems(world, {...})
 | |
| 		recCallOnAddToWorld(world, world.systems)
 | |
| 		return world
 | |
| 	end,
 | |
| 
 | |
| 	--- Returns a filter that returns `true` if, for every argument, a field with the same name exists in the entity.
 | |
| 	-- @tparam string,... ... list of field names that must be in entity
 | |
| 	-- @treturn function(e) that returns `true` if e has all the fields
 | |
| 	all = (...)
 | |
| 		if ... then
 | |
| 			let l = {...}
 | |
| 			return function(s, e)
 | |
| 				for _, k in ipairs(l) do
 | |
| 					if e[k] == nil then
 | |
| 						return false
 | |
| 					end
 | |
| 				end
 | |
| 				return true
 | |
| 			end
 | |
| 		else
 | |
| 			return alwaysTrue
 | |
| 		end
 | |
| 	end,
 | |
| 
 | |
| 	--- Returns a filter that returns `true` if one of the arguments if the name of a field in the entity.
 | |
| 	-- @tparam string,... ... list of field names that may be in entity
 | |
| 	-- @treturn function(e) that returns `true` if e has at leats one of the fields
 | |
| 	any = (...)
 | |
| 		if ... then
 | |
| 			let l = {...}
 | |
| 			return function(s, e)
 | |
| 				for _, k in ipairs(l) do
 | |
| 					if e[k] ~= nil then
 | |
| 						return true
 | |
| 					end
 | |
| 				end
 | |
| 				return false
 | |
| 			end
 | |
| 		else
 | |
| 			return alwaysFalse
 | |
| 		end
 | |
| 	end,
 | |
| 
 | |
| 	--- If `uqt.scene` is available, returns a new scene that will consist of a ECS world with the specified systems and entities.
 | |
| 	--
 | |
| 	-- When suspending and resuming the scene, the `onSuspend` and `onResume` events will be emitted on the ECS world.
 | |
| 	--
 | |
| 	-- Note that since `uqt.scene` use `require` to load the scenes, the scenes files will be cached - including variables defined in them.
 | |
| 	-- So if you store the entity list directly in a variable in the scene file, it will be reused every time the scene is entered, and will thus
 | |
| 	-- be shared among the different execution of the scene. This may be problematic if an entity is modified by one scene instance, as it will affect others
 | |
| 	-- (this should not be a problem with systems due to system instanciation). To avoid this issue, instead you would typically define entities through a
 | |
| 	-- function that will recreate the entities on every scene load.
 | |
| 	-- @require ubiquitousse.scene
 | |
| 	-- @string name the name of the new scene
 | |
| 	-- @tparam[opt={}] table/function systems list of systems to add to the world. If it is a function, it will be executed every time we enter the scene and returns the list of systems.
 | |
| 	-- @tparam[opt={}] table/function entities list of entities to add to the world. If it is a function, it will be executed every time we enter the scene and returns the list of entities.
 | |
| 	-- @treturn scene the new scene
 | |
| 	scene = (name, systems={}, entities={})
 | |
| 		assert(scene, "ubiquitousse.scene unavailable")
 | |
| 		let s = scene.new(name)
 | |
| 		let w
 | |
| 
 | |
| 		function s:enter()
 | |
| 			local sys, ent = systems, entities
 | |
| 			if type(systems) == "function" then sys = { systems() } end
 | |
| 			if type(entities) == "function" then ent = { entities() } end
 | |
| 			w = ecs.world(unpack(sys))
 | |
| 			w:add(unpack(ent))
 | |
| 		end
 | |
| 		function s:exit()
 | |
| 			w:destroy()
 | |
| 		end
 | |
| 		function s:suspend()
 | |
| 			w:emit("onSuspend")
 | |
| 		end
 | |
| 		function s:resume()
 | |
| 			w:emit("onResume")
 | |
| 		end
 | |
| 		function s:update(dt)
 | |
| 			w:update(dt)
 | |
| 		end
 | |
| 		function s:draw()
 | |
| 			w:draw()
 | |
| 		end
 | |
| 
 | |
| 		return s
 | |
| 	end
 | |
| }
 | |
| 
 | |
| return ecs
 |