update syntax plans

This commit is contained in:
2026-05-01 13:04:06 +01:00
parent 0b3470d483
commit 8ef525c8f5
4 changed files with 179 additions and 142 deletions
+23
View File
@@ -0,0 +1,23 @@
window = Window.new 400, 400, title: "Button Test"
main_scene = Scene.new
default_font = Font.new "assets/DejaVuSans.ttf", 24, italic: true
text = ElementText.new "Click me!", font: default_font, position: {x: 50, y: 50}, z: 1
rect = ElementRect.new {x: 50, y: 50, w: 100, h: 25}, color: 0xFF0000, z: 0
rect.on_click do
rect.color = rect.color == 0xFF0000 ?
0x00FF00
:
0xFF0000
end
main_scene.add_element text
main_scene.add_element rect
window.add_scene :main_scene, main_scene
window.start :main_scene
+110 -137
View File
@@ -2,6 +2,7 @@
# reference of what the engine should build up to. # reference of what the engine should build up to.
width, height = 720, 480 width, height = 720, 480
image = Image.new "assets/loading.png" image = Image.new "assets/loading.png"
window = Window.new width, height, title: "My Game", loading_bg: image window = Window.new width, height, title: "My Game", loading_bg: image
@@ -11,6 +12,9 @@ window.map_key ?s, :backward
window.map_key ?a, :left window.map_key ?a, :left
window.map_key ?d, :right window.map_key ?d, :right
window.map_key ?h, :heal
window.map_key ?t, :place
menu_scene = Scene.new menu_scene = Scene.new
default_font = Font.new "assets/DejaVuSans.ttf", 12, italic: true default_font = Font.new "assets/DejaVuSans.ttf", 12, italic: true
@@ -30,165 +34,134 @@ world = ElementWorld.new
# world.cam # camera object to control the view of the world, has position, zoom, rotation # world.cam # camera object to control the view of the world, has position, zoom, rotation
# can define any components with name and groups it is part of, systems will apply behaviour based on their groups # can define any components with name and groups it is part of, systems will apply behaviour based on their groups
# there are 5 inbuilt groups that have systems written for them, but you can create more component groups and systems for whatever you want # there are 5 inbuilt groups that have systems written for them, but you can create more components and systems for whatever you want
# these groups are unique: true, meaning only on of it can be added to an entity and can be accesed using entity.group_name
world.new_component :sample_body, [:body] do # this is a custom component
# these properties exist in the dsl, and are used by the physics engine, but you can also add any custom properties you want to the body and use them in your controller code or whatever
shape = :rectangle world.component :health do |c|
size = {width: 50, height: 50} field :health, int: 100
angle = 0
mass = 1 # this can be set to 0 for static bodies that should not be affected by physics, these bodies will not move but can still collide with other bodies (velocity will be ignored for static bodies) allow_redefine [:health]
velocity = {x: 1, y: 0}
collision = true
# whether this body should collide with other bodies, if false it will not collide with anything and will not be affected by physics, but can still be moved by setting the position directly or by using a controller, useful for things like ghosts or whatever
end end
world.new_component :sample_sprite, [:sprite] do world.spawn do |e| # Name is optional
tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50 e.tags = [:enemy]
animation1 = {tiles: 5..6, frame_rate: 10, loop: true}
animations = {idle: animation1} e.position = {x: 100, y: 100} # entities always have a position, even if they don't have a body component, this is useful for rendering and for entities that don't need physics but still need to be positioned in the world
current_animation = :idle e.add_component :body do |c|
c.shape = :rectangle
c.size = {width: 50, height: 50}
c.angle = 0
c.mass = 1
c.velocity = {x: 0, y: 0}
c.collision = true
end end
e.add_component :sprite do |c|
world.new_component :sample_light, [:light] do c.tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50
color = {r: 255, g: 255, b: 255, a: 255} animation1 = {tiles: 0..3, frame_rate: 10, loop: true}
intensity = 1.0 c.animations = {idle: animation1}
radius = 100 c.current_animation = :idle
# useful if using inbuilt fog light shader only
# mostly cuz i need it though most engines wouldnt have such a feature explicitly (it is dumb addition but i added it cuz i want to)
# it could be achived using other methods / custom properties but this is just a convenient way to do it
end end
e.add_component :render do |c|
world.new_component :sample_render, [:render] do c.z_index = 0
# for extra rendering options like speech bubbles, health bars, etc c.renderer = proc do
z_index = 0 # by default all are y-sorted, but can override with z_index for more control in here
renderer = do
draw_rect 0, 0, 50, 50, {r: 255, g: 0, b: 0, a: 255} # example of custom rendering code for this entity (0,0 is the position of the entity) draw_rect 0, 0, 50, 50, {r: 255, g: 0, b: 0, a: 255} # example of custom rendering code for this entity (0,0 is the position of the entity)
draw_text "Hello World", default_font, 0, 0, {r: 255, g: 255, b: 255, a: 255} draw_text "Hello World", default_font, 0, 0, {r: 255, g: 255, b: 255, a: 255}
end end
# offset from the position of the entity, useful for things like speech bubbles or health bars that should be above the entity, etc # offset from the position of the entity, useful for things like speech bubbles or health bars that should be above the entity, etc
offset = {x: 0, y: 0} c.offset = {x: 0, y: 0}
end end
e.add_component :health do |c|
world.new_component :sample_controller, [:controller] do c.health = 100
# these are defined in the window and mapped to the actual keys, so you can use whatever keys you want and just map them to these actions in the window
# teh user can also remap them in the settings of the game
keyboard_inputs = [
:forward, :backward, :left, :right
]
# direct means only clicks on this entity will trigger the mouse inputs, useful for like chests or doors or whatever
# global means mouse inputs will be triggered as long as the mouse is within the window, useful for like aiming or whatever
# none means mouse inputs will not be triggered for this entity, useful for when the entity should not be interactable with the mouse
# on direct and global mouse mode, you can also get the mouse position relative to the entity or the world
mouse_input_mode = :direct
mouse_position_mode = :relative
mouse_inputs = [:left, :right, :hover]
# when multiple entities match the mouse input, the one with the highest priority will receive the input, useful for when you have multiple entities on top of each other and you want to control which one should receive the mouse input
mouse_priority = 100
# doesnt work without a body component (as they have no size)
update = do |delta_time|
# example of custom controller code for this entity, delta_time is the time since the last update in ms, useful for making movement frame rate independent
if key_down?(:forward)
# move the entity forward, you can access the body component of the entity to move it with physics, or you can set the position directly for more arcade style movement, etc
# if the entity has a body component, you can also apply forces or impulses to it for more realistic physics interactions
# if the entity has a sprite component, you can also change the current animation based on the input or whatever
entity.body.velocity.x = 0
elsif key_down?(:backward)
entity.body.velocity.y = 1
else
entity.body.velocity.y = 0
end
if colliding_with?([:enemies]) # list of groups to check collisions with
puts "Colliding with an enemy!"
end
if mouse_down?(:left)
# example of getting the mouse position relative to the entity or the world
puts "Mouse position relative to entity: #{mouse_position}"
end end
end end
world.spawn "Trap Preview" do |e|
e.position = {x: 0, y: 0}
e.tags = [:trap_preview]
e.active = false
e.add_component :sprite do |c|
c.tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
animation1 = {tiles: 0..3, frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
end end
# e.remove_component :sprite # compoenents can also be removed
# this is a custom compoenent group and component
world.define_group type: :component, name: :health, unique: false
world.new_component :enemy_health, [:health] do
health = 100
# event subscriptions work like this, you can trigger events on the entity itself, or on the world
on_event = {
:damage => do |amount|
health -= amount
puts "Enemy took #{amount} damage, remaining health: #{health}"
end
}
end
world.define_group type: :entity, name: :enemies
world.add_entity "Enemy1" do # Name is optional (as entities can be referenced by group or name)
groups = [:enemies]
position = {x: 100, y: 100} # entities always have a position, even if they don't have a body component, this is useful for rendering and for entities that don't need physics but still need to be positioned in the world
components = [:sample_body, :sample_sprite, :sample_light, :sample_render, :sample_controller, :enemy_health]
end
world.add_entity "Trap Preview" do
position = {x: 0, y: 0}
components = [:sample_sprite]
disabled = true # this entity will not be updated or rendered normally
end end
# systems are called on the world every update frame, some systems are built into the engine for things like physics, rendering, etc, but you can also create your own custom systems for things like trap placement, enemy spawning, etc # systems are called on the world every update frame, some systems are built into the engine for things like physics, rendering, etc, but you can also create your own custom systems for things like trap placement, enemy spawning, etc
world.add_system :health_regen do |dt| # systems can be created on queries
# example of a custom system that regenerates health for all entities with a health component, dt is the time since the last update in ms, useful for making things frame rate independent # the query can be for compoenents or tags or both
world.entities_query(:health).each do |entity| # match is set to list of all capturing queries
health_component = entity.healths.first # get the first health compoenent of teh entity (uses entity.group_names to access the component group - s or es added appropriately) # for components returns an object that responds to .each with every ceomponents values
health_component.health += 0.1 * dt # regenerate 0.1 health per second # for entities it does similarly
query = tag(:enemy) & component(:health, true) & anti(name("Trap Preview", true))
# in the match name will return just the one entity
# tag and componet return list-like tables but they will be parallel and in order
# empty columns will be filled with nil (not possible if only & is used)
# the last part is to show how anti() works
# it doesnt actually do anything as tag(:enemy) and component(:health) will never match name("Trap Preview") but it shows how you can use anti() to negate a query
world.system :health_regen, for: query do |match, dt| # no trigger means always
# example of a simple health regeneration system that regenerates health for all entities with a health component
healths = match[0]
healths.each do |health|
health.health += 1 * dt # regenerate 1 health per second, dt is the delta time since the last update frame, so this will regenerate 1 health per second regardless of the frame rate
health.health = [health.health, 100].min # cap health at 100
end end
end end
world.add_system :trap_placement do |dt| # example from teh game mist trying to be made with this engine trigger = [:place]
world.system :trap_placement, trigger: trigger do |dt|
return unless world.holding_trap?
world.spawn do |e|
e.tags = [:trap]
e.position = world.mouse_position_world
e.add_component :sprite do |c|
c.tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
animation1 = {tiles: 0..3, frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
end
e.add_component :body do |c|
c.shape = :rectangle
c.size = {width: 50, height: 50}
c.angle = 0
c.mass = 1
c.velocity = {x: 0, y: 0}
c.collision = true
end
end
end
# could be done as so or
world.system :trap_preview, for: name("Trap Preview", true) do |match, dt| # example from teh game mist trying to be made with this engine
# do trap placement logic when a trap is held # do trap placement logic when a trap is held
if world.fetch(:holding_trap, false) # example of using the world's store to keep track of global state like whether the player is currently holding a trap or not return unless world.holding_trap?
if mouse_down?(:left)
# place a trap at the mouse position, you can use the mouse position relative to the world for this
world.add_entity "Trap" do
groups = [:traps]
position = mouse_position_world
components = [:trap_body, :trap_sprite, :trap_render, :trap_controller]
end
world.store :holding_trap, false # stop placing traps until the player picks up another one
else
# show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this # show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this
world.entity("Trap Preview").update do e = match.first
position = mouse_position_world e.position = world.mouse_position_world
disabled = false # enable the trap preview entity to show it at the mouse position e.active = true # enable the trap preview entity to show it at the mouse position
end
end
else
# hide the trap preview when not holding a trap
world.entity("Trap Preview").update do
disabled = true
end
end
end end
# this is used by teh engine like so # OR, this one isnt the exact same but shows that .active works on both entites and components
# when a bunch of enities reuse the same components, you can define the components once and then just add them to the entities, which is more efficient than defining the components for each entity separately
# and also for example all the entites that have a body component can be packed and calculated together in the physics engine, all the entities that have a sprite component can be packed and rendered together in the rendering engine, etc.
world.add_shader :fog do |shader| world.system :trap_preview, for: name("Trap Preview", true) & component(:sprite, true) do |match, dt|
return unless world.holding_trap?
# if | was used between teh queries then the arrays would still be parallel but empty rows fit with nil
# for & between they will always match and so the arrays will always be the same length and so you can just use the first one to set the position of the entity and then use the second one to set the active of the sprite component, etc (without nil check)
match.second.each_with_index do |s, i|
match.first[i].position = world.mouse_position_world
s.active = true
end
end
## ------ ##
world.shader :fog do |shader|
# shaders are per world # shaders are per world
vertex "assets/fog.vert" vertex "assets/fog.vert"
fragment "assets/fog.frag" fragment "assets/fog.frag"
@@ -196,7 +169,7 @@ world.add_shader :fog do |shader|
data_format = { data_format = {
name: :float, name: :float,
time: :float, time: :float,
resolution: :vec2 resolution: :vec2,
# data required by teh shader is defined here once # data required by teh shader is defined here once
} }
@@ -209,7 +182,7 @@ world.add_shader :fog do |shader|
z = 100 # shaders always appear above normal sprites but z is used to sort them relative to each other, so a shader with z 100 will appear above a shader with z 50, but both will appear above normal sprites z = 100 # shaders always appear above normal sprites but z is used to sort them relative to each other, so a shader with z 100 will appear above a shader with z 50, but both will appear above normal sprites
end end
window.add_scene :menu_scene => menu_scene window.add_scene :menu_scene, menu_scene
window.add_scene :game_scene => game_scene window.add_scene :game_scene, game_scene
window.start :menu_scene # first scene to show when the game starts, stops the loading bg window.start :menu_scene # first scene to show when the game starts, stops the loading bg
+40
View File
@@ -0,0 +1,40 @@
class Query
def initialize(type, value, capturing)
@type = type
@value = value
@query_l = nil
@query_r = nil
@capturing = capturing
end
def &(other)
@query_l = self
@query_r = other
self.type = :and
self
end
def |(other)
@query_l = self
@query_r = other
self.type = :or
self
end
end
def tag(name, capturing = false)
return Query.new(:tag, name, capturing)
end
def component(name, capturing = false)
return Query.new(:component, name, capturing)
end
def name(name, capturing = false)
return Query.new(:name, name, capturing)
end
def anti(query)
query.type = :"not_#{query.type}"
query
end
+1
View File
@@ -398,6 +398,7 @@ bool Window::poll(Event &event) {
break; break;
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN:
#warning This is a bit of a hack to prevent key down/up events from being generated when text input is active on certian keys, but this list is not enough, need to look more into it
if (text_input_active && sdl_event.key.key > '0' && sdl_event.key.key < 'z') if (text_input_active && sdl_event.key.key > '0' && sdl_event.key.key < 'z')
return false; return false;
event.type = Event::KEY_DOWN; event.type = Event::KEY_DOWN;