From 3a975db66a3711f34e8b64bb27a8eaca79fdeca9 Mon Sep 17 00:00:00 2001 From: Alex Pickering Date: Sun, 1 Feb 2026 13:14:32 -0600 Subject: Initial commit --- src/ecs.moon | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/ecs.moon (limited to 'src/ecs.moon') diff --git a/src/ecs.moon b/src/ecs.moon new file mode 100644 index 0000000..61b73a5 --- /dev/null +++ b/src/ecs.moon @@ -0,0 +1,184 @@ +win = require("window") +log = require("log") +world = require("world") +Component = require("ecs.component") +GraphicsComponent = require("ecs.graphics") +NetworkedComponent = require("ecs.networked") +PredictedComponent = require("ecs.predicted") +ScriptComponent = require("ecs.script") +CharacterControllerComponent = require("ecs.char_controller") +shader_world = require("shaders.world") + +--win.scene:append(shader_world) +--pp = am.postprocess({ + --clear_color: vec4(0.2, 0.2, 0.3, 1), + --depth_buffer: true, + ----stencil_buffer: true, + --width: win.width, + --height: win.height, +--}) +pp = am.group! +pp\append(shader_world.node) +GraphicsComponent.node = pp +ScriptComponent.node = pp +PredictedComponent.node = pp +NetworkedComponent.node = pp + +-- Base entity of our ECS +class Entity + id: 1 + new: (id, components) => -- [id], [components] + ent_reg = world.domain == "client" and world.level.entities or world.level_sync.ents + id_cur = id or #ent_reg + 1 + @created_at = debug.traceback! + assert(id_cur < 10, "Got more then 10 entities") + while ent_reg[id_cur] != nil + id_cur += 1 + log.debug("Want to create an entity with id" .. id_cur .. ", registry is:" .. tostring(ent_reg) .. "\n" .. debug.traceback(), {"ecs"}) + assert(ent_reg[id_cur] == nil, "Attempted to create entity with the same id as one that already exists: " .. tostring(id_cur) .. ", entities were:" .. tostring(ent_reg)) + + log.info("Creating entity " .. id_cur .. " domain is: " .. world.domain, {"ecs"}) + ent_reg[id_cur] = @ + @reg = ent_reg + @id = id_cur + -- Bookkeeping for O(1) access for components + @c_by_type = {} + @c_by_name = {} + --Entity is responsible for the component -> entity link + @components = components or {} + for name, component in pairs(@components) + component.entity = @ + @c_by_type[component.__class] = @c_by_type[component.__class] or {} + @c_by_type[component.__class][name] = component + for name, component in pairs(@components) + assert(component.join, "Component " .. name .. " does not have a join method.") + component\join(@) + component\post_join(@) + add: (component, cid) => + assert(component, "Failed to find component:" .. tostring(component)) + assert(type(component) == "table", "Component must be a table") + assert(component.__class, "Component must have a class") + component.entity = @ + component\join(@, cid) + component\post_join(@, cid) + if cid == nil + cid = #@components + 1 + while @components[cid] + cid += 1 + assert(@components[cid] == nil, "Already had a component with id" .. tostring(cid)) + @components[cid] = component + @c_by_type[component.__class] = @c_by_type[component.__class] or {} + @c_by_type[component.__class][cid] = component + assert(@c_by_name[component.name] == nil, "Duplicate components named" .. component.name) + @c_by_name[component.name] = component + @ + remove: (cid) => + component = @components[cid] + component.entity = nil + component\leave(@) + component + @components[cid] = nil + @c_by_type[component.__class][cid] = nil + @c_by_name[component.name] = nil + destroy: () => + for name, component in pairs(@components) + @remove(name) + @reg[@id] = nil + + get: (cid) => + @components[cid] + + serialize: () => + components = {} + for i, component in pairs(@components) + component_name = component.__class.__name + if not component.serialize + log.warn("Component:" .. component_name .. " does not have a .serialize()", {"ecs"}) + continue + component_data = component\serialize! + components[i] = string.format( + "%d\0%s%d\0%s", + #component_name, component_name, + #component_data, component_data + ) + return table.concat(components, "\0") + + --@deserialize: (data) => + --error("Deserialize called") + --log.info("Deserializing entity from data of length " .. tostring(data and #data or 0), {"ecs"}) + --ent = @! + ---- Empty payload -> empty entity + --if not data or #data == 0 + --return ent + ---- Map serialized component type names back to their classes + --component_types = { + --[GraphicsComponent.__name]: GraphicsComponent + --[NetworkedComponent.__name]: NetworkedComponent + --[PredictedComponent.__name]: PredictedComponent + --[ScriptComponent.__name]: ScriptComponent + --[CharacterControllerComponent.__name]: CharacterControllerComponent + --[PhysicsComponent.__name]: PhysicsComponent + --} + --pointer = 1 + --len = #data + --while pointer <= len + ---- Read component name length (digits up to first NUL) + --name_len_end = string.find(data, "\0", pointer) + --if not name_len_end + --break + --name_len_str = string.sub(data, pointer, name_len_end - 1) + --name_len = tonumber(name_len_str) + --if not name_len or name_len <= 0 + --break + --pointer = name_len_end + 1 + ---- Read component type name + --component_name = string.sub(data, pointer, pointer + name_len - 1) + --pointer += name_len + ---- Read component data length (digits up to next NUL) + --data_len_end = string.find(data, "\0", pointer) + --if not data_len_end + --break + --data_len_str = string.sub(data, pointer, data_len_end - 1) + --data_len = tonumber(data_len_str) + --if not data_len or data_len < 0 + --break + --pointer = data_len_end + 1 + ---- Read component payload + --component_data = string.sub(data, pointer, pointer + data_len - 1) + --pointer += data_len + ---- Skip inter-component separator if present + --if string.sub(data, pointer, pointer) == "\0" + --pointer += 1 + --ctype = component_types[component_name] + --if not ctype + --log.warn("Unknown component type during deserialize: " .. tostring(component_name), {"ecs"}) + --continue + --if not ctype.deserialize + --log.warn("Component type " .. tostring(component_name) .. " does not implement .deserialize()", {"ecs"}) + --continue + ---- Let the component class decide how to interpret its payload + --component = ctype.deserialize(component_data) + --if component + --ent\add(component) + --return ent + + __tostring: () => + components_str = tostring(@components) + return string.format("%s : {\n%s\n}\t%q",@@__name, components_str\gsub("^","\t"), @created_at) + +class PhysicsComponent extends Component + new: (name, properties) => + assert(properties and properties.shape, "Failed to find a shape for physics component") + super(name, properties) + +{ + :Component + :Entity + :NetworkedComponent + :PredictedComponent + :GraphicsComponent + :PhysicsComponent + :ScriptComponent + :CharacterControllerComponent + node: pp +} -- cgit v1.2.3-70-g09d2