# 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