# Sample ruby DSL to create a game window. # reference of what the engine should build up to. width, height = 720, 480 image = Image.new "assets/loading.png" window = Window.new width, height, title: "My Game", loading_bg: image window.map_key ?w, :forward window.map_key ?s, :backward window.map_key ?a, :left window.map_key ?d, :right window.map_key ?h, :heal window.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} start_button.on_click do window.start :game_scene end menu_scene.add_element start_button game_scene = Scene.new 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| field :health, int: 100 allow_redefine [:health] end world.spawn do |e| # Name is optional e.tags = [:enemy] 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 = true end e.add_component :sprite do |c| c.tiles = Tiles.from "assets/sprite.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 :render do |c| c.z_index = 0 c.renderer = proc 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_text "Hello World", default_font, 0, 0, {r: 255, g: 255, b: 255, a: 255} end # offset from the position of the entity, useful for things like speech bubbles or health bars that should be above the entity, etc c.offset = {x: 0, y: 0} 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.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 # e.remove_component :sprite # compoenents can also be removed 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 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 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 return unless world.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 = match.first 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", 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 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 window.add_scene :menu_scene, menu_scene window.add_scene :game_scene, game_scene window.start :menu_scene # first scene to show when the game starts, stops the loading bg