Compare commits

...

19 Commits

Author SHA1 Message Date
792ed74fc9 Make Element background_image_canvas accessible 2026-11-03 20:49:57 -06:00
881ec0ae6b Initial support for touch callbacks, GuiState now emulates a mouse left click on tap 2026-10-15 10:57:42 -05:00
8cea4e7df7 Import ECS draft implementation from I-MIC TDS project 2026-10-09 21:17:52 -05:00
a1513be709 Get rvx bundle exec rake release functional... 2026-10-04 23:43:02 -05:00
d0d5bbac3a Bump version 2026-10-04 23:39:37 -05:00
0ae3ecc145 Fix up some errors with BFS and pathfinding grid 2026-10-04 19:30:58 -05:00
afa8a9dbb6 Add asset textures for nine sliced rounded corners and the like 2026-09-28 19:13:56 -05:00
0ec55bef55 Fixed typo in BFS file name, require pathfinding bits. 2026-09-28 19:13:25 -05:00
904ac78d89 More work on pathfinding support 2026-09-28 15:22:42 -05:00
70d270688f Added .idea to gitignore, first pass at adding intregrated pathfindiing support using redblobgames.com tutorials as reference 2026-09-28 09:35:45 -05:00
685c94bd1b Fixed using this instead of self for slider and check box 2026-09-27 18:16:47 -05:00
d6d3c07bbc First pass at adding fixed update support 2026-09-27 18:06:13 -05:00
2cc1382696 Fixed Text border color not getting set correctly due to misnamed color parameter, generate Text border image with true white text since we draw it with the requested color later. 2026-09-20 16:10:10 -05:00
9fca64b643 Added render mode to BackgroundNineSlice 2026-09-19 19:10:39 -05:00
abe99ce949 Ignore sibling elements that aren't visible from fill mode space considerations 2026-09-17 12:25:48 -05:00
66fb136c83 Make elements sizing by like css' border-box box-sizing instead of like content-box for elements with specific width/height that isn't a ratio, tweaks to gui debug display, make F12 key toggle gui debug display for convenience 2026-09-11 13:05:08 -05:00
b23e5c2a42 Fix pending element recalculate requests apparently not getting processed properly... 2026-09-10 18:32:01 -05:00
747822aa61 Fixed flow containers not correctly positioning their children on the x axis when they wrap to the the next line, fix up containers vertical and horizontal alignment 2026-09-10 18:31:19 -05:00
140e7a3619 Make elements publish the :size_changed event when they change their size 2026-09-09 22:30:19 -05:00
45 changed files with 706 additions and 60 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.idea
/.bundle/
/.yardoc
/_yardoc/

View File

@@ -1,25 +1,37 @@
PATH
remote: .
specs:
cyberarm_engine (0.24.5)
cyberarm_engine (0.30.0)
gosu (~> 1.1)
GEM
remote: https://rubygems.org/
specs:
gosu (1.4.6)
minitest (5.25.5)
rake (13.2.1)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
gosu (1.5.0-x86_64-linux)
ffi (~> 1.12)
minitest (5.27.0)
rake (13.4.2)
PLATFORMS
x64-mingw-ucrt
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
bundler (~> 2.2)
bundler (~> 4)
cyberarm_engine!
minitest (~> 5.0)
rake (~> 13.0)
CHECKSUMS
bundler (4.1.0.beta1) sha256=8a2dac8ca276ff4eff5efc2c6457b0044080fc1e431534ad7c5cc6187232b800
cyberarm_engine (0.30.0)
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
gosu (1.5.0-x86_64-linux)
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
BUNDLED WITH
2.6.8
4.1.0.beta1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -28,9 +28,8 @@ Gem::Specification.new do |spec|
spec.require_paths = %w[lib assets]
spec.add_dependency "gosu", "~> 1.1"
# spec.add_dependency "ffi", :platforms => [:mswin, :mingw] # Required by Clipboard on Windows
spec.add_development_dependency "bundler", "~> 2.2"
spec.add_development_dependency "bundler", "~> 4"
spec.add_development_dependency "minitest", "~> 5.0"
spec.add_development_dependency "rake", "~> 13.0"
end

View File

@@ -42,6 +42,22 @@ require_relative "cyberarm_engine/console/command"
require_relative "cyberarm_engine/console/subcommand"
require_relative "cyberarm_engine/console/commands/help_command"
require_relative "cyberarm_engine/pathfinding/graph"
require_relative "cyberarm_engine/pathfinding/graphs/grid"
require_relative "cyberarm_engine/pathfinding/graph_search"
require_relative "cyberarm_engine/pathfinding/graph_searches/breadth_first_search"
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"

View File

@@ -4,10 +4,10 @@ module CyberarmEngine
CACHE = {}
attr_accessor :x, :y, :z, :width, :height, :mode, :color
attr_accessor :x, :y, :z, :width, :height, :mode, :color, :render_mode
attr_reader :image, :left, :top, :right, :bottom
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :tiled, left: 1, top: 1, right: 1, bottom: 1, color: Gosu::Color::WHITE)
def initialize(image_path: nil, x: 0, y: 0, z: 0, width: 0, height: 0, mode: :tiled, left: 1, top: 1, right: 1, bottom: 1, color: Gosu::Color::WHITE, render_mode: :default)
@image = get_image(image_path) if image_path
@x = x
@@ -25,6 +25,7 @@ module CyberarmEngine
@bottom = bottom
@color = color
@render_mode = render_mode
nine_slice if @image
end
@@ -121,58 +122,58 @@ module CyberarmEngine
end
def draw_stretched
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
@segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale, 1, @color) # SCALE X
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
@segment_top_left.draw(@x, @y, @z, 1, 1, @color, @render_mode)
@segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale, 1, @color, @render_mode) # SCALE X
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color, @render_mode)
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
@segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale, 1, @color) # SCALE X
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
@segment_left.draw(@x, @y + @top, @z, 1, height_scale, @color) # SCALE Y
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale, @color, @render_mode) # SCALE Y
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color, @render_mode)
@segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale, 1, @color, @render_mode) # SCALE X
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color, @render_mode)
@segment_left.draw(@x, @y + @top, @z, 1, height_scale, @color, @render_mode) # SCALE Y
@segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale, @color) # SCALE X and SCALE Y
@segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale, @color, @render_mode) # SCALE X and SCALE Y
end
def draw_tiled
@segment_top_left.draw(@x, @y, @z, 1, 1, @color)
@segment_top_left.draw(@x, @y, @z, 1, 1, @color, @render_mode)
# p [width_scale, height_scale]
Gosu.clip_to(@x + @segment_top_left.width, @y, @segment_top.width * width_scale, @segment_top.height) do
width_scale.ceil.times do |i|
@segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z, 1, 1, @color) # SCALE X
@segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z, 1, 1, @color, @render_mode) # SCALE X
end
end
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color)
@segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x + @width - @segment_top_right.width, @y + @top, @segment_right.width, @segment_right.height * height_scale) do
height_scale.ceil.times do |i|
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z, 1, 1, @color) # SCALE Y
@segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z, 1, 1, @color, @render_mode) # SCALE Y
end
end
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color)
@segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x + @segment_top_left.width, @y + @height - @segment_bottom.height, @segment_top.width * width_scale, @segment_bottom.height) do
width_scale.ceil.times do |i|
@segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z, 1, 1, @color) # SCALE X
@segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z, 1, 1, @color, @render_mode) # SCALE X
end
end
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color)
@segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z, 1, 1, @color, @render_mode)
Gosu.clip_to(@x, @y + @top, @segment_left.width, @segment_left.height * height_scale) do
height_scale.ceil.times do |i|
@segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z, 1, 1, @color) # SCALE Y
@segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z, 1, 1, @color, @render_mode) # SCALE Y
end
end
Gosu.clip_to(@x + @segment_top_left.width, @y + @segment_top.height, @width - (@segment_left.width + @segment_right.width), @height - (@segment_top.height + @segment_bottom.height)) do
height_scale.ceil.times do |y|
width_scale.ceil.times do |x|
@segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z, 1, 1, @color) # SCALE X and SCALE Y
@segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z, 1, 1, @color, @render_mode) # SCALE X and SCALE Y
end
end
end

View File

@@ -1,4 +1,6 @@
module CyberarmEngine
GUI_DEBUG = false
module Common
ImageBlob = Data.define(:to_blob, :columns, :rows)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -33,6 +33,9 @@ module CyberarmEngine
@timers.delete_if(&:dead?)
end
def fixed_update(dt)
end
def needs_redraw?
true
end
@@ -50,6 +53,18 @@ module CyberarmEngine
def gamepad_disconnected(index)
end
def touch_began(touch)
end
def touch_moved(touch)
end
def touch_ended(touch)
end
def touch_cancelled(touch)
end
def gain_focus
end

View File

@@ -0,0 +1,64 @@
module CyberarmEngine
module Pathfinding
class Graph
class Node
KEY_FORMAT = "%s:%s:%s".freeze
attr_reader :x, :y, :z, :cost, :key, :edge_nodes
def initialize(x:, y:, z: 0, cost: 0)
@x = x
@y = y
@z = z
@cost = cost
@key = format(KEY_FORMAT, @x, @y, @z).freeze
@edge_nodes = []
end
def add_edge_node(node)
index = @edge_nodes.find_index { |n| n.key == node.key }
return if index
@edge_nodes.push(node)
end
def remove_edge_node(node)
index = @edge_nodes.find_index { |n| n.key == node.key }
return unless index
@edge_nodes.delete(index)
end
def inspect
format "#<%<class>s:%<id>#018x @key=%<key>s @cost=%<cost>s @edge_nodes=%<edge_nodes>s>",
class: self.class,
id: object_id << 1,
key: key.inspect,
cost: cost.inspect,
edge_nodes: edge_nodes.map { |n| n&.key }
end
end
def initialize(nodes: {})
@nodes = nodes
end
def node(key)
@nodes[key]
end
def node_at(x, y, z = 0)
key = format(Node::KEY_FORMAT, x, y, z)
@nodes[key]
end
def neighbors(node)
node.edge_nodes
end
end
end
end

View File

@@ -0,0 +1,6 @@
module CyberarmEngine
module Pathfinding
class GraphSearch
end
end
end

View File

@@ -0,0 +1,6 @@
module CyberarmEngine
module Pathfinding
class AStarSearch < GraphSearch
end
end
end

View File

@@ -0,0 +1,27 @@
module CyberarmEngine
module Pathfinding
class BreadthFirstSearch < GraphSearch
def self.search(graph:, start:, goal: nil)
frontier = []
came_from = {}
frontier.push(start)
came_from[start.key] = start
while(current = frontier.shift)
# early exit if we reach our goal
break if current == goal
graph.neighbors(current).each do |neighbor|
next if came_from[neighbor.key]
frontier.push(neighbor)
came_from[neighbor.key] = current
end
end
came_from
end
end
end
end

View File

@@ -0,0 +1,6 @@
module CyberarmEngine
module Pathfinding
class DijkstraSearch < GraphSearch
end
end
end

View File

@@ -0,0 +1,52 @@
module CyberarmEngine
module Pathfinding
class Grid < Graph
def add_node(x:, y:, z: 0, cost: 0, replace: false)
key = format(Node::KEY_FORMAT, x, y, z)
node = @nodes[key]
raise "Node at: #{key} already exists! (#{node})" if node && !replace
node = Node.new(x: x, y: y, z: z, cost: cost)
@nodes[key] = node
update_node_edge_nodes(node)
node
end
def remove_node(node)
update_node_edge_nodes(node, remove_node: true)
@nodes.delete(node.key)
end
def update_node_edge_nodes(node, remove_node: false)
x = node.x
y = node.y
z = node.z
# add node to neighbors
# 2D
left = @nodes[format(Node::KEY_FORMAT, x - 1, y, z)]
right = @nodes[format(Node::KEY_FORMAT, x + 1, y, z)]
top = @nodes[format(Node::KEY_FORMAT, x, y - 1, z)]
bottom = @nodes[format(Node::KEY_FORMAT, x, y + 1, z)]
# 3D
front = @nodes[format(Node::KEY_FORMAT, x, y, z - 1)]
back = @nodes[format(Node::KEY_FORMAT, x, y, z + 1)]
possible_edge_nodes = [left, right, top, bottom, front, back]
if remove_node
possible_edge_nodes.each { |n| n&.remove_edge_node(node) }
else
possible_edge_nodes.each { |n| n&.add_edge_node(node) }
end
end
def update_edge_nodes
@nodes.each { |k, n| update_node_edge_nodes(n) }
end
end
end
end

View File

@@ -149,7 +149,7 @@ module CyberarmEngine
@border_alpha = n
end
def border_color=(n)
def border_color=(color)
old_color = @border_color
if color
@@ -208,17 +208,17 @@ module CyberarmEngine
img = Gosu::Image.send(:"from_#{method.to_s.split("_").last}", @text, @size, font: @font)
@cached_text_border_image = Gosu.render((_width + (@border_size * 2)).ceil, (height + (@border_size * 2)).ceil) do
img.draw(-_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, 0, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(-_x, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(0, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, -_y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, -_y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, 0, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(_x, 0, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(_x, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(0, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(-_x, _y, @z, @factor_x, @factor_y, @border_color, @mode)
img.draw(0, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
img.draw(-_x, _y, @z, @factor_x, @factor_y, Gosu::Color::WHITE, @mode)
end
end
@@ -256,7 +256,7 @@ module CyberarmEngine
@textobject.send(method, @text, _x + @border_size, _y + @border_size, @z, @factor_x, @factor_y, white, @mode)
end
@cached_text_border_image.draw(@x - @border_size, @y - @border_size, @z, @factor_x, @factor_y, border_color)
@cached_text_border_image.draw(@x - @border_size, @y - @border_size, @z, @factor_x, @factor_y, border_color, @mode)
end
if @shadow

View File

@@ -5,7 +5,7 @@ module CyberarmEngine
include Common
attr_accessor :x, :y, :z, :tip, :element_visible
attr_reader :parent, :options, :style, :event_handler, :background_canvas, :border_canvas
attr_reader :parent, :options, :style, :event_handler, :background_image_canvas, :background_canvas, :border_canvas
def initialize(options = {}, block = nil)
@parent = options.delete(:parent) # parent Container (i.e. flow/stack)
@@ -18,7 +18,7 @@ module CyberarmEngine
@visible = !@options.key?(:visible) ? true : @options[:visible]
@tip = @options[:tip] || ""
@debug = @options[:debug] || CyberarmEngine.const_defined?("GUI_DEBUG") && CyberarmEngine::GUI_DEBUG || false
@debug = @options[:debug].nil? ? nil : @options[:debug]
@debug_color = @options[:debug_color].nil? ? Gosu::Color::RED : @options[:debug_color]
@style = Style.new(options)
@@ -36,7 +36,7 @@ module CyberarmEngine
@width = 0
@height = 0
@style.width = default(:width) || nil
@style.width = default(:width) || nil
@style.height = default(:height) || nil
@background_canvas = nil # Background.new
@@ -215,6 +215,7 @@ module CyberarmEngine
event(:blur)
event(:changed)
event(:size_changed)
end
def enter(_sender)
@@ -339,7 +340,7 @@ module CyberarmEngine
end
def debug_draw
return unless @debug # allow elements to opt out of debug drawing, makes debugging some things easier.
return if @debug == false || !CyberarmEngine::GUI_DEBUG # allow elements to opt out of debug drawing, makes debugging some things easier.
return if CyberarmEngine.const_defined?("GUI_DEBUG_ONLY_ELEMENT") && self.class == GUI_DEBUG_ONLY_ELEMENT
Gosu.draw_line(
@@ -393,7 +394,7 @@ module CyberarmEngine
def width
if visible?
inner_width + @width
inner_width + content_width
else
0
end
@@ -417,7 +418,7 @@ module CyberarmEngine
def height
if visible?
inner_height + @height
inner_height + content_height
else
0
end
@@ -487,6 +488,8 @@ module CyberarmEngine
new_size = if size.is_a?(Float) && size.between?(0.0, 1.0)
(@parent.send(:"content_#{dimension}") * size).floor - send(:"noncontent_#{dimension}").floor
elsif size.is_a?(Numeric)
(size - send(:"noncontent_#{dimension}")).floor
else
size
end
@@ -515,7 +518,7 @@ module CyberarmEngine
def space_available_height
# TODO: This may get expensive if there are a lot of children, probably should cache it somehow
fill_siblings = @parent.children.select { |c| c.styled(:fill) }.count.to_f # include self since we're dividing
fill_siblings = @parent.children.select { |c| c.styled(:fill) && c.visible? }.count.to_f # include self since we're dividing
available_space = ((@parent.content_height - (@parent.children.reject { |c| c.styled(:fill) }).map(&:outer_height).sum) / fill_siblings)
(available_space.nan? || available_space.infinite?) ? 0 : available_space.floor # The parent element might not have its dimensions, yet.
@@ -576,6 +579,8 @@ module CyberarmEngine
@old_width = width
@old_height = height
publish(:size_changed)
end
end

View File

@@ -46,7 +46,7 @@ module CyberarmEngine
end
def value=(bool)
old_value = this.value
old_value = self.value
@toggle_button.value = bool
publish(:changed, bool) if bool != old_value

View File

@@ -88,17 +88,20 @@ module CyberarmEngine
Gosu.translate(@scroll_position.x, @scroll_position.y) do
@children.each(&:draw)
@children.each do |child|
next unless child.visible?
child.debug_draw
end
end
end
@children.each(&:debug_draw)
debug_draw
end
def debug_draw
super
@children.each(&:debug_draw)
end
def update
@@ -203,9 +206,9 @@ module CyberarmEngine
case styled(:v_align)
when :center
@y = parent.y + parent.styled(:margin_top) + parent.height / 2 - height / 2
@y = parent.y + parent.outer_height / 2 - outer_height / 2
when :bottom
@y = parent.y + parent.styled(:margin_top) + parent.height - height
@y = parent.y + parent.outer_height - outer_height
end
end
@@ -214,9 +217,9 @@ module CyberarmEngine
case styled(:h_align)
when :center
@x = parent.x + parent.styled(:margin_left) + parent.width / 2 - width / 2
@x = parent.x + parent.outer_width / 2 - outer_width / 2
when :right
@x = parent.x + parent.styled(:margin_left) + parent.width - width
@x = parent.x + parent.outer_width - outer_width
end
end
@@ -285,7 +288,7 @@ module CyberarmEngine
end
def position_on_next_line(element) # Flow
@current_position.x = 0
@current_position.x = styled(:margin_left) + styled(:padding_left)
@current_position.y += tallest_neighbor(element, @current_position.y).outer_height
element.x = @current_position.x

View File

@@ -94,7 +94,7 @@ module CyberarmEngine
end
def value=(n)
old_value = this.value
old_value = self.value
@value = n
position_handle

View File

@@ -33,6 +33,9 @@ module CyberarmEngine
@pending_element_recalculate_requests = []
@hit_elements = []
@touch_down_position = {}
@min_touch_drag_distance = 50
@needs_repaint = false
@menu = nil
@@ -98,7 +101,15 @@ module CyberarmEngine
Stats.frame.start_timing(:gui_element_recalculate_requests)
# puts "PENDING REQUESTS: #{@pending_element_recalculate_requests.size}" if @pending_element_recalculate_requests.size.positive?
@pending_element_recalculate_requests.shift(&:recalculate)
while (e = @pending_element_recalculate_requests.shift)
# its pointless to recalculate individual elements when we've been asked to recalculate the whole gui
if @pending_recalculate_request
@pending_element_recalculate_requests.clear
break
end
e.recalculate
end
Stats.frame.end_timing(:gui_element_recalculate_requests)
@@ -219,6 +230,31 @@ module CyberarmEngine
@page&.update
end
def touch_began(touch)
@touch_down_position[touch.id] = touch
end
def touch_moved(touch)
# TODO: Support scrolling containers and dragging elements
end
def touch_ended(touch)
touch_down_pos = @touch_down_position[touch.id]
distance = Vector.new(touch_down_pos.x, touch_down_pos.y).distance(Vector.new(touch.x, touch.y))
# emulate mouse click
if distance < @min_touch_drag_distance
button_down(Gosu::MS_LEFT)
button_up(Gosu::MS_LEFT)
end
@touch_down_position.delete(touch.id)
end
def touch_cancelled(touch)
@touch_down_position.delete(touch.id)
end
def button_down(id)
super
@@ -231,6 +267,9 @@ module CyberarmEngine
redirect_mouse_button(:right)
when Gosu::KB_F5
request_recalculate
when Gosu::KB_F12
CyberarmEngine.const_set(:GUI_DEBUG, !CyberarmEngine::GUI_DEBUG)
request_repaint
end
@focus.button_down(id) if @focus.respond_to?(:button_down)

View File

@@ -1,4 +1,4 @@
module CyberarmEngine
NAME = "InDev".freeze
VERSION = "0.25.1".freeze
VERSION = "0.30.0".freeze
end

View File

@@ -6,6 +6,9 @@ module CyberarmEngine
SAMPLES = {}
SONGS = {}
FixedUpdateProperties = Data.define(:fixed_timestep, :fixed_timestep_int_ms, :max_timestep)
FixedUpdate = Struct.new(:milliseconds, :accumulator, :interpolation, :alpha)
attr_accessor :show_cursor, :show_stats_plotter
attr_writer :exit_on_opengl_error
attr_reader :last_frame_time, :delta_time, :states
@@ -28,7 +31,7 @@ module CyberarmEngine
@@instance
end
def initialize(width: 800, height: 600, fullscreen: false, update_interval: 1000.0 / 60, resizable: false, borderless: false)
def initialize(width: 800, height: 600, fullscreen: false, update_interval: 1000.0 / 60, resizable: false, borderless: false, fixed_update_timestep: 1.0 / 125, fixed_update_timestep_int_ms: 8, fixed_max_timestep: 1.0 / 60)
@show_cursor = false
@has_focus = false
@show_stats_plotter = false
@@ -46,6 +49,9 @@ module CyberarmEngine
preload_default_shaders if respond_to?(:preload_default_shaders)
@stats_plotter = Stats::StatsPlotter.new(2, 28) # FIXME: Make positioning easy
@fixed_timestep_data = FixedUpdateProperties.new(1.0 / 125, 8, 1.0 / 60)
@fixed_update = FixedUpdate.new(0, 0.0, 0.0, 1.0)
setup
end
@@ -79,11 +85,44 @@ module CyberarmEngine
Stats.frame.start_timing(:update)
current_state&.update
Stats.frame.start_timing(:fixed_update)
fixed_update
Stats.frame.end_timing(:fixed_update)
Stats.frame.end_timing(:update)
Stats.frame.start_timing(:interframe_sleep) unless needs_redraw?
end
def fixed_update
@delta_time = @fixed_timestep_data.max_timestep if @delta_time > @fixed_timestep_data.max_timestep
@fixed_update.accumulator += @delta_time
while @fixed_update.accumulator >= @fixed_timestep_data.fixed_timestep
@fixed_update.accumulator -= @fixed_timestep_data.fixed_timestep
@fixed_update.alpha = @fixed_update.accumulator / @fixed_timestep_data.fixed_timestep
@fixed_update.milliseconds += @fixed_timestep_data.fixed_timestep_int_ms
current_state&.fixed_update(@fixed_timestep_data.fixed_timestep)
end
end
def fixed_update_milliseconds
@fixed_update.milliseconds
end
def fixed_update_accumulator
@fixed_update.accumulator
end
def fixed_update_interpolation
@fixed_update.interpolation
end
def fixed_update_alpha
@fixed_update.alpha
end
def needs_cursor?
@show_cursor
end
@@ -104,6 +143,22 @@ module CyberarmEngine
current_state&.gamepad_disconnected(index)
end
def touch_began(touch)
current_state&.touch_began(touch)
end
def touch_moved(touch)
current_state&.touch_moved(touch)
end
def touch_ended(touch)
current_state&.touch_ended(touch)
end
def touch_cancelled(touch)
current_state&.touch_cancelled(touch)
end
def gain_focus
@has_focus = true