mirror of
				https://github.com/Reuh/ubiquitousse.git
				synced 2025-10-27 17:19:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			499 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| --- ubiquitousse.ecs
 | |
| -- Optional dependency: ubiquitousse.scene, to allow quick creation of ECS-based scenes.
 | |
| local loaded, scene = pcall(require, (...):match("^(.-)ecs").."scene")
 | |
| if not loaded then scene = nil end
 | |
| 
 | |
| --- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include:
 | |
| -- * ability to nest systems;
 | |
| -- * instanciation of systems for each world (no shared state);
 | |
| -- * adding and removing entities is done instantaneously
 | |
| -- * ability to add and remove components from entities after they were added to the world.
 | |
| let ecs
 | |
| 
 | |
| -- TODO: Implement a skip list for faster search.
 | |
| -- better control over system order: process, draw, methods? (for lag reasons and dependencies)
 | |
| -- more generic events?
 | |
| -- populate component?
 | |
| 
 | |
| --- 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. No cycle detection.
 | |
| let copy = (a, b)
 | |
| 	for k, v in pairs(a) do
 | |
| 		if type(v) == "table" then
 | |
| 			if b[k] == nil then
 | |
| 				b[k] = {}
 | |
| 				copy(v, b[k])
 | |
| 			elseif b[k] == "table" then
 | |
| 				copy(v, b[k])
 | |
| 			end
 | |
| 		elseif b[k] == nil then
 | |
| 			b[k] = v
 | |
| 		end
 | |
| 	end
 | |
| end
 | |
| 
 | |
| --- System fields and methods.
 | |
| -- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system").
 | |
| -- Instancied systems can be retrieved in system.s or system.systems.
 | |
| -- Oh, the "world" is just the top-level system, behaving in exactly the same way as other systems.
 | |
| -- Every field defined below is optional.
 | |
| let system_mt = {
 | |
| 	--- Read-only after creation system options ---
 | |
| 	-- I mean, you can try to change them afterwards. But, heh.
 | |
| 
 | |
| 	--- Name of the system (optional).
 | |
| 	-- Used to create a field with the system's name in world.s and into each entity (the "entity's system table").
 | |
| 	name = nil,
 | |
| 
 | |
| 	--- List of subsystems.
 | |
| 	-- On a instancied system, this is a list of the same subsystems, but instancied for this world.
 | |
| 	systems = nil,
 | |
| 
 | |
| 	--- Modifiable system options ---
 | |
| 
 | |
| 	--- 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.any.
 | |
| 	-- 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.
 | |
| 	filter = :(e) return false end,
 | |
| 	--- Returns true if e1 <= e2.
 | |
| 	-- 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.
 | |
| 	compare = :(e1, e2) return true end,
 | |
| 
 | |
| 	--- Called when adding an entity to the system.
 | |
| 	onAdd = :(s) end,
 | |
| 	--- Called when removing an entity from the system.
 | |
| 	onRemove = :(s) end,
 | |
| 	--- Called when the system is instancied, before any call to :onnAddToWorld (including other systems in the world).
 | |
| 	onInstance = :() end,
 | |
| 	--- Called when the system is added to a world.
 | |
| 	onAddToWorld = :(world) end,
 | |
| 	--- Called when the system is removed from a world (i.e., the world is destroyed).
 | |
| 	onRemoveFromWorld = :(world) end,
 | |
| 	--- Called when the world is destroyed, after every call to :onRemoveFromWorld (including other systems in the world).
 | |
| 	onDestroy = :() end,
 | |
| 	--- Called when updating the system.
 | |
| 	onUpdate = :(dt) end,
 | |
| 	--- Called when drawing the system.
 | |
| 	onDraw = :() end,
 | |
| 	--- Called when updating the system, for every entity the system contains. Called after :onUpdate was called on the system.
 | |
| 	process = :(s, dt) end,
 | |
| 	--- Called when drawing the system, for every entity the system contains. Called after :onDraw was called on the system.
 | |
| 	render = :(s) end,
 | |
| 
 | |
| 	--- If not false, the system will only update every interval seconds.
 | |
| 	interval = false,
 | |
| 	--- The system and its susbsystems will only update if this is true.
 | |
| 	active = true,
 | |
| 	--- The system and its subsystems will only draw if this is true.
 | |
| 	visible = true,
 | |
| 
 | |
| 	--- Defaults value to put into the entities's system table when they are added. Will recursively fill missing values.
 | |
| 	-- When an entity is added to a system, a .entity field is created in the system table, referring to the full entity table.
 | |
| 	-- Changing this will not affect entities already in the system.
 | |
| 	default = nil,
 | |
| 	--- Defaults methods to assign to the entities's system table when they are added.
 | |
| 	-- When calling the methods with entity.systemName:method(...), the method will actually receive the
 | |
| 	-- arguments method(system, entity system table, ...). Methamethods are accepted. New methods can be
 | |
| 	-- created anytime.
 | |
| 	methods = nil,
 | |
| 
 | |
| 	--- Read-only system options ---
 | |
| 
 | |
| 	--- The world the system belongs to.
 | |
| 	world = nil,
 | |
| 	--- Number of entities in the system.
 | |
| 	entityCount = 0,
 | |
| 	--- Map of named systems in the world (not only subsystems). Same for every system from the same world.
 | |
| 	s = nil,
 | |
| 
 | |
| 	--- Private fields ---
 | |
| 
 | |
| 	--- First element of the linked list of entities: { entity, next_element }.
 | |
| 	_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).
 | |
| 	_previous = nil,
 | |
| 	--- Amount of time waited since last update (if interval is set).
 | |
| 	_waited = 0,
 | |
| 	--- Metatable of entities' system table.
 | |
| 	-- Contains the methods defined in the methods field, wrapped to be called with the correct arguments, as well as a
 | |
| 	-- __index field (if not redefined in methods).
 | |
| 	_methods_mt = nil,
 | |
| 
 | |
| 	--- Methods ---
 | |
| 
 | |
| 	--- 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.
 | |
| 	-- Since :remove will not search for entities in systems where they should have been filtered out, the added entities will not be removed
 | |
| 	-- when calling :remove on a parent system or the world. The entity can be removed by calling :remove on the system :add was called on.
 | |
| 	-- Complexity: O(1) per unordered system, O(entityCount) per ordered system.
 | |
| 	add = :(e, ...)
 | |
| 		if e ~= nil and not @_previous[e] and @filter(e) then
 | |
| 			-- setup entity
 | |
| 			if @name then
 | |
| 				if not e[@name] e[@name] = {}
 | |
| 				if @default copy(@default, e[@name])
 | |
| 				if @methods setmetatable(e[@name], @_methods_mt)
 | |
| 				e[@name].entity = 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[@name])
 | |
| 			-- add to subsystems
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				s:add(e)
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @add(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Refresh an entity's systems.
 | |
| 	-- Behave similarly to :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.
 | |
| 	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,
 | |
| 	--- Remove entities to 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 :add.
 | |
| 	-- Returns all removed entities.
 | |
| 	-- Complexity: O(1) per system.
 | |
| 	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[@name])
 | |
| 			end
 | |
| 		end
 | |
| 		if ... then
 | |
| 			return e, @remove(...)
 | |
| 		else
 | |
| 			return e
 | |
| 		end
 | |
| 	end,
 | |
| 	--- Returns true if every entity is in the system.
 | |
| 	-- Complexity: O(1).
 | |
| 	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.
 | |
| 	iter = :()
 | |
| 		return nextEntity, { @_first }
 | |
| 	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.
 | |
| 	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[@name], dt)
 | |
| 				end
 | |
| 			end
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				s:update(dt)
 | |
| 			end
 | |
| 			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[@name])
 | |
| 				end
 | |
| 			end
 | |
| 			for _, s in ipairs(@systems) do
 | |
| 				s:draw()
 | |
| 			end
 | |
| 		end
 | |
| 	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 true 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
 | |
| 		-- setup method table
 | |
| 		let methods_mt = {}
 | |
| 		if s.methods then
 | |
| 			methods_mt.__index = methods_mt
 | |
| 			for k, v in pairs(s.methods) do
 | |
| 				methods_mt[k] = :(...)
 | |
| 					return v(system, @, ...)
 | |
| 				end
 | |
| 			end
 | |
| 			setmetatable(s.methods, {
 | |
| 				__newindex = :(k, v)
 | |
| 					rawset(@, k, v)
 | |
| 					methods_mt[k] = :(...)
 | |
| 						return v(system, @, ...)
 | |
| 					end
 | |
| 				end
 | |
| 			})
 | |
| 		end
 | |
| 		-- instanciate system
 | |
| 		system = setmetatable({
 | |
| 			systems = recInstanciateSystems(world, s.systems or {}),
 | |
| 			world = world,
 | |
| 			s = world.s,
 | |
| 			_previous = {},
 | |
| 			_methods_mt = methods_mt
 | |
| 		}, {
 | |
| 			__index = :(k)
 | |
| 				if s[k] ~= nil then
 | |
| 					return s[k]
 | |
| 				else
 | |
| 					return system_mt[k]
 | |
| 				end
 | |
| 			end
 | |
| 		})
 | |
| 		if type(s.filter) == "string" then
 | |
| 			system.filter = (_, e) return e[s.filter] ~= nil end
 | |
| 		elseif type(s.filter) == "table" then
 | |
| 			system.filter = ecs.any(unpack(s.filter))
 | |
| 		elseif type(s.filter) == "boolean" then
 | |
| 			if s.filter then
 | |
| 				system.filter = alwaysTrue
 | |
| 			else
 | |
| 				system.filter = alwaysFalse
 | |
| 			end
 | |
| 		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.
 | |
| ecs = {
 | |
| 	--- Create and returns a world system based on a list of systems.
 | |
| 	-- The systems will be instancied for this world.
 | |
| 	-- @impl ubiquitousse
 | |
| 	world = (...)
 | |
| 		let world = setmetatable({
 | |
| 			filter = ecs.all(),
 | |
| 			s = {},
 | |
| 			_previous = {}
 | |
| 		}, { __index = system_mt })
 | |
| 		world.world = 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.
 | |
| 	-- @impl ubiquitousse
 | |
| 	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.
 | |
| 	-- @impl ubiquitousse
 | |
| 	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.
 | |
| 	-- @impl ubiquitousse
 | |
| 	scene = (name, systems={}, entities={})
 | |
| 		assert(scene, "ubiquitousse.scene unavailable")
 | |
| 		let s = scene.new(name)
 | |
| 		let w
 | |
| 
 | |
| 		function s:enter()
 | |
| 			w = ecs.world(unpack(systems))
 | |
| 			w:add(unpack(entities))
 | |
| 		end
 | |
| 		function s:exit()
 | |
| 			w:destroy()
 | |
| 		end
 | |
| 		function s:update(dt)
 | |
| 			w:update(dt)
 | |
| 		end
 | |
| 		function s:draw()
 | |
| 			w:draw()
 | |
| 		end
 | |
| 
 | |
| 		return s
 | |
| 	end
 | |
| }
 | |
| 
 | |
| return ecs
 |