CCCPaste

c88676b46c863

--!strict
--!optimize 2

export type SystemPhase = "pre" | "update" | "post"
export type SystemFn = (world: World, dt: number) -> ()
export type ComponentTable = { [number]: any }

export type World = {
	_nextEntityId: number,
	_alive: { [number]: boolean },
	_components: { [string]: ComponentTable },
	_systemsPre: { SystemFn },
	_systemsUpdate: { SystemFn },
	_systemsPost: { SystemFn },

	Spawn: (self: World) -> number,
	Despawn: (self: World, entityId: number) -> (),

	HasEntity: (self: World, entityId: number) -> boolean,

	GetComponentTable: (self: World, name: string) -> ComponentTable,
	AddComponent: (self: World, entityId: number, name: string, data: any) -> (),
	GetComponent: (self: World, entityId: number, name: string) -> any?,
	RemoveComponent: (self: World, entityId: number, name: string) -> (),

	RegisterSystem: (self: World, phase: SystemPhase, system: SystemFn) -> (),
	Step: (self: World, dt: number) -> (),

	ForEach: (self: World, name: string, fn: (entityId: number, data: any) -> ()) -> (),
}

local ECSWorld = {}

local function runSystems(list: { SystemFn }, world: World, dt: number)
	local count = #list
	for i = 1, count do
		list[i](world, dt)
	end
end

local function spawnEntity(self: World): number
	local id = self._nextEntityId + 1
	self._nextEntityId = id
	self._alive[id] = true
	return id
end

local function despawnEntity(self: World, entityId: number)
	if not self._alive[entityId] then
		return
	end
	self._alive[entityId] = nil
	local components = self._components
	for _, tableForComponent in pairs(components) do
		tableForComponent[entityId] = nil
	end
end

local function hasEntity(self: World, entityId: number): boolean
	return self._alive[entityId] == true
end

local function getComponentTable(self: World, name: string): ComponentTable
	local components = self._components
	local tbl = components[name]
	if tbl ~= nil then
		return tbl
	end
	local newTbl: ComponentTable = {}
	components[name] = newTbl
	return newTbl
end

local function addComponent(self: World, entityId: number, name: string, data: any)
	if not self._alive[entityId] then
		return
	end
	local tbl = getComponentTable(self, name)
	tbl[entityId] = data
end

local function getComponent(self: World, entityId: number, name: string): any?
	local tbl = self._components[name]
	if tbl == nil then
		return nil
	end
	return tbl[entityId]
end

local function removeComponent(self: World, entityId: number, name: string)
	local tbl = self._components[name]
	if tbl == nil then
		return
	end
	tbl[entityId] = nil
end

local function registerSystem(self: World, phase: SystemPhase, system: SystemFn)
	if phase == "pre" then
		local list = self._systemsPre
		list[#list + 1] = system
	elseif phase == "update" then
		local list = self._systemsUpdate
		list[#list + 1] = system
	else
		local list = self._systemsPost
		list[#list + 1] = system
	end
end

local function step(self: World, dt: number)
	if dt <= 0 then
		return
	end
	runSystems(self._systemsPre, self, dt)
	runSystems(self._systemsUpdate, self, dt)
	runSystems(self._systemsPost, self, dt)
end

local function forEach(self: World, name: string, fn: (entityId: number, data: any) -> ())
	local tbl = self._components[name]
	if tbl == nil then
		return
	end
	for entityId, data in tbl do
		if self._alive[entityId] then
			fn(entityId, data)
		end
	end
end

function ECSWorld.new(): World
	local world: World = {
		_nextEntityId = 0,
		_alive = {},
		_components = {},
		_systemsPre = {},
		_systemsUpdate = {},
		_systemsPost = {},

		Spawn = spawnEntity,
		Despawn = despawnEntity,
		HasEntity = hasEntity,

		GetComponentTable = getComponentTable,
		AddComponent = addComponent,
		GetComponent = getComponent,
		RemoveComponent = removeComponent,

		RegisterSystem = registerSystem,
		Step = step,

		ForEach = forEach,
	}

	return world
end

return ECSWorld