Files
project_misth/samples/main.rb
T
2026-11-04 09:07:20 +00:00

206 lines
7.8 KiB
Ruby

# Sample ruby DSL to create a game window.
# reference of what the engine should build up to.
width, height = 720, 480
image = Tiles.from("assets/loading.png").first
App.run width, height, title: "My Game", loading_bg: image # can be image or animation
App.map_key ?w, :forward
App.map_key ?s, :backward
App.map_key ?a, :left
App.map_key ?d, :right
App.map_key ?h, :heal
App.map_key ?t, :place
menu_scene = Scene.new
default_font = Font.new "assets/DejaVuSans.ttf", 12, italic: true
start_button = ElementText.new "Start Game", font: default_font, position: {x: 100, y: 100}
game_scene = Scene.new
start_button.on_click do
App.switch_to game_scene
end
menu_scene << start_button
world = ElementWorld.new
# 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
# there are 5 inbuilt groups that have systems written for them, but you can create more components and systems for whatever you want
# this is a custom component
world.component :health do |c|
# one of int or float or boolean which use c stuff and generally faster
# also allow for bulk operations to be done in hotloops.
# can use `any` which allows for any ruby value but uses runtime gc etc and so is worse to use performance wise for each entity.
field :health, :int, 100 # stored for each entity it is attached to.
field :max_health, :int, 100, global: true # stored only once per component
end
world.spawn do |e| # Name is optional
e.tags = [:enemy]
e.z = 0.5
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
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_layer = 1
end
e.add_component :sprite do |c|
tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
## For tilemaps
# tiles is an array of of tiles in sheet, largest one's w and h will be used. or width: height: to override
c.tilemap = Tilemap.new tiles
c.default_tile = tiles.first # default tile to use for empty spaces in the tilemap
c.auto_tile = true # tiles must follow pattern
# c.tile[x: 0, y: 0] = tiles[0] # set tile at position x,y in tilemap to a specific tile from the sheet (if no auto_tile is used)
c.tile[x: 0, y: 0] = true # true/false otherwise
end
e.add_component :health do |c|
c.health = 100
end
end
world.spawn "Trap Preview" do |e|
e.position = {x: 0, y: 0}
e.z = 0
e.tags = [:trap_preview]
e.active = false
e.add_component :sprite do |c|
tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
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 can be created on queries
# the query can be for compoenents or tags or both
# match is set to list of all capturing queries
# for components returns an object that responds to .each with every ceomponents values
# for entities it does similarly
# mouse state can be read in systems always
query = Query.from(tag(:enemy) & component(:health, capture: true) & anti(name("Trap Preview")))
# 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] # get the first component in the query, in this case health
# , this will be a list of all health components for
# entities that match the query
healths[:health].add! 1 * dt
healths[:health].clamp! 0, :max_health
end
trigger = Trigger.from(key(:place))
world.system :trap_placement, trigger: trigger do |dt|
return unless world.globals.holding_trap?
world.spawn do |e|
e.tags = [:trap]
e.position = world.mouse_position_world
e.add_component :sprite do |c|
tiles = Tiles.from "assets/trap.png", tile_width: 50, tile_height: 50
tiles2 = Tiles.from "assets/trap_2.png", tile_width: 50, tile_height: 50
animation1 = {tiles: tiles[0..3] + tiles2[0..3], frame_rate: 10, loop: true}
c.animations = {idle: animation1}
c.current_animation = :idle
end
e.add_component :body do |c|
c.size = {width: 50, height: 50}
c.angle = 0
c.mass = 1
c.velocity = {x: 0, y: 0}
c.collision_layer = 0 # layer 0 doesnt collide.
end
end
end
name_query = Query.from(name("Trap Preview") & component(:sprite, capture: true))
# could be done as so or
world.system :trap_preview, for: name_query do |dt, matches| # example from the game mist trying to be made with this engine
# do trap placement logic when a trap is held
return unless world.globals.holding_trap?
# show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this
e = matches[0] # get the first entity that matches the name query, in
# this case the trap preview entity, this will be nil if no entity matches
e.position = world.mouse_position_world
e.active = true # enable the trap preview entity to show it at the mouse position
end
# OR, this one isnt the exact same but shows that .active works on both entites and components
world.system :trap_preview, for: name("Trap Preview", capture: true) & component(:sprite, capture: true) do |dt, matches|
return unless world.globals.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)
matches.second.each_with_index do |s, i|
matches.first[i].position = world.mouse_position_world
s.active = true
end
end
## ------ ##
world.shader :fog do |shader|
# shaders are per world
vertex "assets/fog.vert"
fragment "assets/fog.frag"
data_format = {
name: :float,
time: :float,
resolution: :vec2,
# data required by teh shader is defined here once
}
frame do |shader|
# example of custom shader code for this shader, this is called every
# frame and is where you would update the shader data based on the
# current state of the world or whatever
shader.set_data :time, Time.now.to_f
shader.set_data :resolution, {x: width, y: height}
end
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
App.start menu_scene # first scene to show when the game starts, stops the loading bg