From 8cea4e7df7bebbb8a844e42a2949006eb41a641e Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Fri, 9 Oct 2026 21:17:52 -0500 Subject: [PATCH] Import ECS draft implementation from I-MIC TDS project --- lib/cyberarm_engine.rb | 9 ++ lib/cyberarm_engine/ecs/component.rb | 119 ++++++++++++++++++ .../ecs/components/position.rb | 19 +++ .../ecs/components/render_2d.rb | 24 ++++ lib/cyberarm_engine/ecs/components/team.rb | 17 +++ .../ecs/components/velocity.rb | 19 +++ lib/cyberarm_engine/ecs/entity.rb | 35 ++++++ lib/cyberarm_engine/ecs/entity_store.rb | 56 +++++++++ lib/cyberarm_engine/ecs/system.rb | 14 +++ lib/cyberarm_engine/ecs/systems/movement.rb | 18 +++ lib/cyberarm_engine/ecs/systems/render_2d.rb | 16 +++ 11 files changed, 346 insertions(+) create mode 100644 lib/cyberarm_engine/ecs/component.rb create mode 100644 lib/cyberarm_engine/ecs/components/position.rb create mode 100644 lib/cyberarm_engine/ecs/components/render_2d.rb create mode 100644 lib/cyberarm_engine/ecs/components/team.rb create mode 100644 lib/cyberarm_engine/ecs/components/velocity.rb create mode 100644 lib/cyberarm_engine/ecs/entity.rb create mode 100644 lib/cyberarm_engine/ecs/entity_store.rb create mode 100644 lib/cyberarm_engine/ecs/system.rb create mode 100644 lib/cyberarm_engine/ecs/systems/movement.rb create mode 100644 lib/cyberarm_engine/ecs/systems/render_2d.rb diff --git a/lib/cyberarm_engine.rb b/lib/cyberarm_engine.rb index bb8f54d..fb6eb25 100644 --- a/lib/cyberarm_engine.rb +++ b/lib/cyberarm_engine.rb @@ -49,6 +49,15 @@ require_relative "cyberarm_engine/pathfinding/graph_searches/breadth_first_searc require_relative "cyberarm_engine/pathfinding/graph_searches/dijkstra_search" require_relative "cyberarm_engine/pathfinding/graph_searches/astar_search" +require_relative "cyberarm_engine/ecs/entity" +require_relative "cyberarm_engine/ecs/component" +require_relative "cyberarm_engine/ecs/system" +require_relative "cyberarm_engine/ecs/entity_store" +require_relative "cyberarm_engine/ecs/components/position" +require_relative "cyberarm_engine/ecs/components/velocity" +require_relative "cyberarm_engine/ecs/components/render_2d" +require_relative "cyberarm_engine/ecs/components/team" + require_relative "cyberarm_engine/ui/dsl" require_relative "cyberarm_engine/ui/theme" diff --git a/lib/cyberarm_engine/ecs/component.rb b/lib/cyberarm_engine/ecs/component.rb new file mode 100644 index 0000000..b4be26f --- /dev/null +++ b/lib/cyberarm_engine/ecs/component.rb @@ -0,0 +1,119 @@ +module CyberarmEngine + module ECS + # Components MUST not have "logic," only data. + class Component + InternalEncoder = Data.define(:pack, :min, :max, :encoder, :decoder) + + # Note: all encoders use big endian + INTERNAL_ENCODING_DATA = { + float: InternalEncoder.new("g", -Float::INFINITY, Float::INFINITY, nil, nil), + double: InternalEncoder.new("G", -Float::INFINITY, Float::INFINITY, nil, nil), + int8: InternalEncoder.new("c", -128, 127, nil, nil), + uint8: InternalEncoder.new("C", 0, 255, nil, nil), + int16: InternalEncoder.new("s>", -32_768, 32_767, nil, nil), + uint16: InternalEncoder.new("n", 0, 65_535, nil, nil), + int32: InternalEncoder.new("l>", -2_147_483_648, 2_147_483_647, nil, nil), + uint32: InternalEncoder.new("N", 0, 4_294_967_295, nil, nil), + int64: InternalEncoder.new("q>", -9_223_372_036_854_775_808, 9_223_372_036_854_775_807, nil, nil), + uint64: InternalEncoder.new("Q>", 0, 18_446_744_073_709_551_615, nil, nil), + string: InternalEncoder.new("Z", nil, nil, nil, nil), + vector_float: InternalEncoder.new("g4", nil, nil, ->(v) { [v.x, v.y, v.z, v.w] }, ->(v, a) { v.x = a[0]; v.y = a[1]; v.z = a[2]; v.w = a[4] }), + vector_double: InternalEncoder.new("G4", nil, nil, ->(v) { [v.x, v.y, v.z, v.w] }, ->(v, a) { v.x = a[0]; v.y = a[1]; v.z = a[2]; v.w = a[4] }), + vector_int8: InternalEncoder.new("c4", nil, nil, ->(v) { [v.x, v.y, v.z, v.w] }, ->(v, a) { v.x = a[0]; v.y = a[1]; v.z = a[2]; v.w = a[4] }), + vector_int16: InternalEncoder.new("s>4", nil, nil, ->(v) { [v.x, v.y, v.z, v.w] }, ->(v, a) { v.x = a[0]; v.y = a[1]; v.z = a[2]; v.w = a[4] }), + vector_int32: InternalEncoder.new("l>4", nil, nil, ->(v) { [v.x, v.y, v.z, v.w] }, ->(v, a) { v.x = a[0]; v.y = a[1]; v.z = a[2]; v.w = a[4] }), + }.freeze + + @identifier = nil + @values = [] + + def self.register_value(type, name, value, encoder = INTERNAL_ENCODING_DATA[type]) + pp [type, name, value, encoder] + end + + def self.register_custom_value(type, name, value, packed, encoder, decoder) + pp [type, name, value, packed, encoder, decoder] + end + + def self.identifier(value = nil) + unless value + value = [File.basename(__FILE__).bytes.each_slice(4).map(&:sum).sum^0xffffffff].pack("N").unpack1("N") + end + + @identifier = value + end + + def self.float(name, value = 0.0) + register_value(:float, name, value) + end + + def self.double(name, value = 0.0) + register_value(:double, name, value) + end + + def self.int8(name, value = 0) + register_value(:int8, name, value) + end + + def self.uint8(name, value = 0) + register_value(:uint8, name, value) + end + + def self.int16(name, value = 0) + register_value(:int16, name, value) + end + + def self.uint16(name, value = 0) + register_value(:uint16, name, value) + end + + def self.int32(name, value = 0) + register_value(:int32, name, value) + end + + def self.uint32(name, value = 0) + register_value(:uint32, name, value) + end + + def self.int64(name, value = 0) + register_value(:int64, name, value) + end + + def self.uint64(name, value = 0) + register_value(:uint64, name, value) + end + + def self.string(name, value = "") + register_value(:string, name, value) + end + + def self.vector_float(name, value = CyberarmEngine.new(0.0, 0.0, 0.0, 0.0)) + register_value(:vector_float, name, value) + end + + def self.vector_double(name, value = CyberarmEngine.new(0.0, 0.0, 0.0, 0.0)) + register_value(:vector_double, name, value) + end + + def self.vector_int8(name, value = CyberarmEngine.new(0, 0, 0, 0)) + register_value(:vector_int8, name, value) + end + + def self.vector_int16(name, value = CyberarmEngine.new(0, 0, 0, 0)) + register_value(:vector_int16, name, value) + end + + def self.vector_int32(name, value = CyberarmEngine.new(0, 0, 0, 0)) + register_value(:vector_int32, name, value) + end + + def self.custom(name, value, pack, encoder, decoder) + register_value(:custom, name, value, pack, encoder, decoder) + end + + def self.custom_unserializable(name, value) + register_value(:custom, name, value) + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/components/position.rb b/lib/cyberarm_engine/ecs/components/position.rb new file mode 100644 index 0000000..9b5ca63 --- /dev/null +++ b/lib/cyberarm_engine/ecs/components/position.rb @@ -0,0 +1,19 @@ +module CyberarmEngine + module ECS + class Component + class Position < Component + TYPE = 0x00 + + attr_reader :position + + def initialize(position:) + unless position.is_a?(CyberarmEngine::Vector) + raise "Position must be a CyberarmEngine::Vector, got #{position.class}" + end + + @position = position + end + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/components/render_2d.rb b/lib/cyberarm_engine/ecs/components/render_2d.rb new file mode 100644 index 0000000..785012d --- /dev/null +++ b/lib/cyberarm_engine/ecs/components/render_2d.rb @@ -0,0 +1,24 @@ +module CyberarmEngine + module ECS + class Component + class Render2D < Component + TYPE = 0xf0 + + attr_reader :image, :scale, :center + + def initialize(image:, scale: CyberarmEngine::Vector.new(1.0, 1.0), + center: CyberarmEngine::Vector.new(0.5, 0.5)) + raise "Image must be a Gosu::Image, got #{image.class}" unless image.is_a?(Gosu::Image) + raise "Scale must be a CyberarmEngine::Vector, got #{scale.class}" unless scale.is_a?(CyberarmEngine::Vector) + unless center.is_a?(CyberarmEngine::Vector) + raise "Center must be a CyberarmEngine::Vector, got #{center.class}" + end + + @image = image + @scale = scale + @center = center + end + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/components/team.rb b/lib/cyberarm_engine/ecs/components/team.rb new file mode 100644 index 0000000..2420c3e --- /dev/null +++ b/lib/cyberarm_engine/ecs/components/team.rb @@ -0,0 +1,17 @@ +module CyberarmEngine + module ECS + class Component + class Team < Component + TYPE = 0x03 + + attr_accessor :team + + def initialize(team:) + raise "Team must be an Integer, got #{team.class}" unless team.is_a?(Integer) + + @team = team + end + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/components/velocity.rb b/lib/cyberarm_engine/ecs/components/velocity.rb new file mode 100644 index 0000000..cc09ec5 --- /dev/null +++ b/lib/cyberarm_engine/ecs/components/velocity.rb @@ -0,0 +1,19 @@ +module CyberarmEngine + module ECS + class Component + class Velocity < Component + TYPE = 0x01 + + attr_reader :velocity + + def initialize(velocity:) + unless velocity.is_a?(CyberarmEngine::Vector) + raise "Velocity must be a CyberarmEngine::Vector, got #{velocity.class}" + end + + @velocity = velocity + end + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/entity.rb b/lib/cyberarm_engine/ecs/entity.rb new file mode 100644 index 0000000..bb7981b --- /dev/null +++ b/lib/cyberarm_engine/ecs/entity.rb @@ -0,0 +1,35 @@ +module CyberarmEngine + module ECS + class Entity + attr_reader :entity_id, :entity_components + + def initialize(id:) + @entity_id = id + # there are more cache friendly ways to implement component storage/lookup but right now this is "good enough." + @entity_components = [] + end + + def add_component(component) + raise "Component already exists on entity!" if @entity_components.find { |c| c.is_a?(component) } + + @entity_components << component + end + + def component(component) + @entity_components.find { |c| c.is_a?(component) } + end + + def components(components) + components.each do |c| + return false unless @entity_components.include? { |comp| c.is_a?(comp) } + end + + true + end + + def remove_component(component) + @entity_components.delete_if { |c| c.is_a?(component) } + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/entity_store.rb b/lib/cyberarm_engine/ecs/entity_store.rb new file mode 100644 index 0000000..6f6014b --- /dev/null +++ b/lib/cyberarm_engine/ecs/entity_store.rb @@ -0,0 +1,56 @@ +module CyberarmEngine + module ECS + class EntityStore + MAX_ENTITY_ID = 4_294_967_295 + + def initialize + @current_entity_id = -1 + @entities = [] + + @systems = [ + System::Movement, + System::Render2D + ] + end + + def create_entity(components) + ent = Entity.new(id: next_entity_id) + + components.each do |component| + ent.add_component(component) + end + + @entities << ent + + ent + end + + def find_entity(id) + @entities.find { |e| e.id == id } + end + + def entities_with(components) + @entities.select { |e| e.components(components) } + end + + def remove_entity(entity) + @entities.delete_if { |e| e == entity } + end + + def remove_entities(entities) + @entities.delete_if { |e| entities.include?(e) } + end + + def remove_all_entities + @entities.clear + end + + def next_entity_id + # NOTE: we'll be sending entity IDs as uint32's (4 bytes with MAX value of MAX_ENTITY_ID [4.29 billion]) + raise "Next entity ID exceeds maximium value!" if @current_entity_id + 1 > MAX_ENTITY_ID + + @current_entity_id += 1 + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/system.rb b/lib/cyberarm_engine/ecs/system.rb new file mode 100644 index 0000000..c54e33a --- /dev/null +++ b/lib/cyberarm_engine/ecs/system.rb @@ -0,0 +1,14 @@ +module CyberarmEngine + module ECS + class System + # Array of required components for this system + def truple + [] + end + + def update(entity); end + + def fixed_update(entity); end + end + end +end diff --git a/lib/cyberarm_engine/ecs/systems/movement.rb b/lib/cyberarm_engine/ecs/systems/movement.rb new file mode 100644 index 0000000..a37a0cd --- /dev/null +++ b/lib/cyberarm_engine/ecs/systems/movement.rb @@ -0,0 +1,18 @@ +module CyberarmEngine + module ECS + class System + class Movement < System + def truple + [ + Component::Position, + Component::Velocity + ] + end + + def update(entity_store); end + + def fixed_update(entity_store); end + end + end + end +end diff --git a/lib/cyberarm_engine/ecs/systems/render_2d.rb b/lib/cyberarm_engine/ecs/systems/render_2d.rb new file mode 100644 index 0000000..cea0e5a --- /dev/null +++ b/lib/cyberarm_engine/ecs/systems/render_2d.rb @@ -0,0 +1,16 @@ +module CyberarmEngine + module ECS + class System + class Render < System + def truple + [ + Component::Position, + Component::Render + ] + end + + def draw(entity_store); end + end + end + end +end