Last active
October 31, 2024 07:06
-
-
Save amirrajan/9a4d6e45328dbe4df1a4696562a90d94 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Shadows (https://amirrajan.itch.io/shadows) (https://youtu.be/wQknjYk_-dE)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# demo gameplay here: https://youtu.be/wQknjYk_-dE | |
# this is the core game class. the game is pretty small so this is the only class that was created | |
class Game | |
# attr_gtk is a ruby class macro (mixin) that adds the .args, .inputs, .outputs, and .state properties to a class | |
attr_gtk | |
# this is the main tick method that will be called every frame the tick method is your standard game loop. | |
# ie initialize game state, process input, perform simulation calculations, then render | |
def tick | |
defaults | |
input | |
calc | |
render | |
end | |
# defaults method re-initializes the game to itsstarting point if | |
# 1. it hasn't already been initialized (state.clock is nil) | |
# 2. or reinitializes the game if the player died (game_over) | |
def defaults | |
# initialize background music | |
args.audio[:bg_music] ||= { | |
input: "sounds/hymn.ogg", | |
looping: true, | |
game: 1.0, | |
pitch: 1.0, | |
paused: false | |
} | |
new_game if !state.clock || state.game_over == true | |
end | |
# this is where inputs are processed | |
# we process inputs for the player via input_entity | |
# and then process inputs for each enemy using the same | |
# input_entity function | |
def input | |
input_entity player, | |
find_input_timeline(at: player.clock, key: :left_right), | |
find_input_timeline(at: player.clock, key: :space), | |
find_input_timeline(at: player.clock, key: :down) | |
# an enemy could still be spawing | |
shadows.find_all { |shadow| entity_active? shadow } | |
.each do |shadow| | |
input_entity shadow, | |
find_input_timeline(at: shadow.clock, key: :left_right), | |
find_input_timeline(at: shadow.clock, key: :space), | |
find_input_timeline(at: shadow.clock, key: :down) | |
end | |
end | |
# this is the input_entity function that handles the movement of the player (and the enemies) | |
# it's essentially your state machine for player movement | |
def input_entity entity, left_right, jump, fall_through | |
# guard clause that ignores input processing if the entity is still spawning | |
return if !entity_active? entity | |
# increment the dx of the entity by the magnitude of the left_right input value | |
entity.dx += left_right | |
# if the left_right input is zero... | |
if left_right == 0 | |
# if the entity was originally running, then set their "action" to standing | |
# entity_set_action! updates the current action of the entity and takes note of the frame that | |
# the action occured on | |
if (entity.action == :running) | |
entity_set_action! entity, :standing | |
end | |
elsif entity.left_right != left_right && (entity_on_platform? entity) | |
# if the entity is on a platform, and their current left right value is different, mark them as running | |
# this is done because we want to reset the run animation if they changed directions | |
entity_set_action! entity, :running | |
end | |
# capture the left_right input so that it can be consulted on the next frame | |
entity.left_right = left_right | |
# capture the direction the player is facing (this is used to determine the horizontal flip of the sprite | |
entity.orientation = if left_right == -1 | |
:left | |
elsif left_right == 1 | |
:right | |
else | |
entity.orientation | |
end | |
# if the fall_through (down) input was requested, and if they are on a platform... | |
if fall_through && (entity_on_platform? entity) | |
entity.jumped_at = 0 | |
# set their jump_down value (falling through a platform) | |
entity.jumped_down_at = entity.clock | |
# and increment the number of times they jumped (entities get three jumps before needing to touch the ground again) | |
entity.jump_count += 1 | |
end | |
# if the jump input was requested and if they haven't reached their jump limit | |
if jump && entity.jump_count < 3 | |
# update the player's current action to the corresponding jump number (used for rendering | |
# the different jump animations) | |
if entity.jump_count == 0 | |
entity_set_action! entity, :first_jump | |
elsif entity.jump_count == 1 | |
entity_set_action! entity, :midair_jump | |
elsif entity.jump_count == 2 | |
entity_set_action! entity, :midair_jump | |
end | |
# set the entity's dy value and take note of when jump occured (also increment jump | |
# count/eat one of their jumps) | |
entity.dy = entity.jump_power | |
entity.jumped_at = entity.clock | |
entity.jumped_down_at = 0 | |
entity.jump_count += 1 | |
end | |
end | |
# after inputs have been processed, we then determine game over states, collision, win states etc | |
def calc | |
# calculate the new values of the light meter (if the light meter hits zero, it's game over) | |
calc_light_meter | |
# capture the actions that were taken this turn so that they can be "replayed" for the enemies on future | |
# ticks of the simulation | |
calc_action_history | |
# calculate collisions for the player | |
calc_entity player | |
# calculate collisions for the enemies | |
calc_shadows | |
# spawn a new light crystal | |
calc_light_crystal | |
# process "fire and forget" render queues (eg particles and death animations) | |
calc_render_queues | |
# determine game over | |
calc_game_over | |
# increment the internal clocks for all entities this internal clock is used to determine how | |
# a player's past input is replayed. it's also used to determine what animation frame the entity | |
# should be performing when idle, running, and jumping | |
calc_clock | |
end | |
def calc_light_meter | |
# ease the light meters value up or down every time the player captures a light crystal | |
# the "target" light meter value is increased and slowly spills over to the final light meter value | |
# which is used to determine game over | |
state.light_meter -= 1 | |
d = state.light_meter_queue * 0.1 | |
state.light_meter += d | |
state.light_meter_queue -= d | |
end | |
def calc_action_history | |
# keep track of the inputs the player has performed over time as the inputs change for the player, mark the point in time | |
# the specific input changed, and when the change occured. when enemies replay the player's actions, this history (along | |
# with the enemy's interal clock) is consulted to determine what action should be performed | |
# the three possible input events are captured and marked within the input timeline if/when the value changes | |
# left right input events | |
state.curr_left_right = inputs.left_right | |
if state.prev_left_right != state.curr_left_right | |
state.input_timeline.unshift({ at: state.clock, k: :left_right, v: state.curr_left_right }) | |
end | |
state.prev_left_right = state.curr_left_right | |
# jump input events | |
state.curr_space = inputs.keyboard.key_down.space || | |
inputs.controller_one.key_down.a || | |
inputs.keyboard.key_down.up || | |
inputs.controller_one.key_down.b | |
if state.prev_space != state.curr_space | |
state.input_timeline.unshift({ at: state.clock, k: :space, v: state.curr_space }) | |
end | |
state.prev_space = state.curr_space | |
# jump down (fall through platform) | |
state.curr_down = inputs.keyboard.down || inputs.controller_one.down | |
if state.prev_down != state.curr_down | |
state.input_timeline.unshift({ at: state.clock, k: :down, v: state.curr_down }) | |
end | |
state.prev_down = state.curr_down | |
end | |
def calc_entity entity | |
# process entity collision/simulation | |
calc_entity_rect entity | |
# return if the entity is still spawning | |
return if !entity_active? entity | |
# calc collisions | |
calc_entity_collision entity | |
# update the state machine of the entity based on the collision results | |
calc_entity_action entity | |
# calc actions the entity should take based on input timeline | |
calc_entity_movement entity | |
end | |
def calc_entity_rect entity | |
# this function calculates the entity's new collision rect, render rect, hurt box, etc | |
entity.render_rect = { x: entity.x, y: entity.y, w: entity.w, h: entity.h } | |
entity.rect = entity.render_rect.merge x: entity.render_rect.x + entity.render_rect.w * 0.33, | |
w: entity.render_rect.w * 0.33 | |
entity.next_rect = entity.rect.merge x: entity.x + entity.dx, | |
y: entity.y + entity.dy | |
entity.prev_rect = entity.rect.merge x: entity.x - entity.dx, | |
y: entity.y - entity.dy | |
orientation_shift = 0 | |
if entity.orientation == :right | |
orientation_shift = entity.rect.w.half | |
end | |
entity.hurt_rect = entity.rect.merge y: entity.rect.y + entity.h * 0.33, | |
x: entity.rect.x - entity.rect.w.half + orientation_shift, | |
h: entity.rect.h * 0.33 | |
end | |
def calc_entity_collision entity | |
# run of the mill AABB collision | |
calc_entity_below entity | |
calc_entity_left entity | |
calc_entity_right entity | |
end | |
def calc_entity_below entity | |
# exit ground collision detection if they aren't falling | |
return unless entity.dy < 0 | |
tiles_below = find_tiles { |t| t.rect.top <= entity.prev_rect.y } | |
collision = find_collision tiles_below, (entity.rect.merge y: entity.next_rect.y) | |
# exit ground collision detection if no ground was found | |
return unless collision | |
# determine if the entity is allowed to fall through the platform | |
# (you can only fall through a platform if you've been standing on it for 8 frames) | |
can_drop = true | |
if entity.last_standing_at && (entity.clock - entity.last_standing_at) < 8 | |
can_drop = false | |
end | |
# if the entity is allowed to fall through the platform, | |
# and the entity requested the action, then clip them through the platform | |
if can_drop && entity.jumped_down_at.elapsed_time(entity.clock) < 10 && !collision.impassable | |
if (entity_on_platform? entity) && can_drop | |
entity.dy = -1 | |
end | |
entity.jump_count = 1 | |
else | |
entity.y = collision.rect.y + collision.rect.h | |
entity.dy = 0 | |
entity.jump_count = 0 | |
end | |
end | |
def calc_entity_left entity | |
# collision detection left side of screen | |
return unless entity.dx < 0 | |
return if entity.next_rect.x > 8 - 32 | |
entity.x = 8 - 32 | |
entity.dx = 0 | |
end | |
def calc_entity_right entity | |
# collision detection right side of screen | |
return unless entity.dx > 0 | |
return if (entity.next_rect.x + entity.rect.w) < (1280 - 8 - 32) | |
entity.x = (1280 - 8 - entity.rect.w - 32) | |
entity.dx = 0 | |
end | |
def calc_entity_action entity | |
# update the state machine of the entity | |
# based on where they ended up after physics calculations | |
if entity.dy < 0 | |
# mark the entity as falling after the jump animation frames have been processed | |
if entity.action == :midair_jump | |
if entity_action_complete? entity, state.midair_jump_duration | |
entity_set_action! entity, :falling | |
end | |
else | |
entity_set_action! entity, :falling | |
end | |
elsif entity.dy == 0 && !(entity_on_platform? entity) | |
# if the entity's dy is zero, determine if they should be marked as standing or running | |
if entity.left_right == 0 | |
entity_set_action! entity, :standing | |
else | |
entity_set_action! entity, :running | |
end | |
end | |
end | |
def calc_entity_movement entity | |
# increment x and y positions of the entity based on dy and dx | |
calc_entity_dy entity | |
calc_entity_dx entity | |
end | |
def calc_entity_dx entity | |
# horizontal movement application and friction | |
entity.dx = entity.dx.clamp(-5, 5) | |
entity.dx *= 0.9 | |
entity.x += entity.dx | |
end | |
def calc_entity_dy entity | |
# vertical movement application and gravity | |
entity.y += entity.dy | |
entity.dy += state.gravity | |
entity.dy += entity.dy * state.drag ** 2 * -1 | |
end | |
def calc_shadows | |
# every 5 seconds, add a new shadow enemy/increase difficult | |
add_shadow! if state.clock.zmod?(300) | |
# for each shadow, perform a simulation calculation | |
shadows.each do |shadow| | |
calc_entity shadow | |
# decrement the spawn countdown which is used to determine if the enemy is finally active | |
shadow.spawn_countdown -= 1 if shadow.spawn_countdown > 0 | |
end | |
end | |
def calc_light_crystal | |
# determine if the player has intersected with a light crystal | |
light_rect = state.light_crystal | |
if player.hurt_rect.intersect_rect? light_rect | |
# if they have then queue up the partical animation of the light crystal being collected | |
state.jitter_fade_out_render_queue << { x: state.light_crystal.x, | |
y: state.light_crystal.y, | |
w: state.light_crystal.w, | |
h: state.light_crystal.h, | |
a: 255, | |
path: 'sprites/light.png' } | |
# increment the light meter target value | |
state.light_meter_queue += 600 | |
# spawn a new light cristal for the player to try to get | |
state.light_crystal = new_light_crystal | |
# play collection sound | |
outputs.sounds << "sounds/light-#{rand(6)}.wav" | |
end | |
end | |
def calc_render_queues | |
# render all the entries in the "fire and forget" render queues | |
state.jitter_fade_out_render_queue.each do |s| | |
new_w = s.w * 1.02 ** 5 | |
ds = new_w - s.w | |
s.w = new_w | |
s.h = new_w | |
s.x -= ds.half | |
s.y -= ds.half | |
s.a = s.a * 0.97 ** 5 | |
end | |
state.jitter_fade_out_render_queue.reject! { |s| s.a <= 1 } | |
state.game_over_render_queue.each { |s| s.a = s.a * 0.95 } | |
state.game_over_render_queue.reject! { |s| s.a <= 1 } | |
end | |
def calc_game_over | |
# calcuate game over | |
state.game_over = false | |
# it's game over if the player intersects with any of the enemies | |
state.game_over ||= shadows.find_all { |s| s.spawn_countdown <= 0 } | |
.any? { |s| s.hurt_rect.intersect_rect? player.hurt_rect } | |
# it's game over if the light_meter hits 0 | |
state.game_over ||= state.light_meter <= 1 | |
# debug to reset the game/prematurely | |
if inputs.keyboard.key_down.r | |
state.you_win = false | |
state.game_over = true | |
end | |
# update game over states and win/loss | |
if state.game_over | |
state.you_win = false | |
state.game_over = true | |
end | |
if state.light_meter >= 6000 | |
state.you_win = true | |
state.game_over = true | |
end | |
# if it's a game over, fade out all current entities in play | |
if state.game_over | |
state.game_over_render_queue.concat shadows.map { |s| s.sprite.merge(a: 255) } | |
state.game_over_render_queue << player.sprite.merge(a: 255) | |
state.game_over_render_queue << state.light_crystal.merge(a: 255, path: 'sprites/light.png', b: 128) | |
end | |
end | |
def calc_clock | |
return if state.game_over | |
state.clock += 1 | |
player.clock += 1 | |
shadows.each { |s| s.clock += 1 if entity_active? s } | |
end | |
def render | |
# render the game | |
render_stage | |
render_light_meter | |
render_instructions | |
render_render_queues | |
render_light_meter_warning | |
render_light_crystal | |
render_entities | |
end | |
def render_stage | |
# the stage is a simple background | |
outputs.background_color = [255, 255, 255] | |
outputs.sprites << { x: 0, | |
y: 0, | |
w: 1280, | |
h: 720, | |
path: "sprites/stage.png", | |
a: 200 } | |
end | |
def render_light_meter | |
# the light meter sprite is rendered across the top how much of the light meter is light vs dark is based off | |
# of what the current light meter value is (which increases when a crystal is collected and decreses a little bit every | |
# frame | |
meter_perc = state.light_meter.fdiv(6000) + (0.002 * rand) | |
light_w = (1280 * meter_perc).round | |
dark_w = 1280 - light_w | |
# once the light and dark partitions have been computed render the meter sprite and clip its width (source_w) | |
outputs.sprites << { x: 0, y: 64.from_top, w: light_w, | |
source_x: 0, source_y: 0, source_w: light_w, source_h: 128, | |
h: 64, path: 'sprites/meter-light.png' } | |
outputs.sprites << { x: 1280 * meter_perc, y: 64.from_top, w: dark_w, | |
source_x: light_w, source_y: 0, source_w: dark_w, source_h: 128, | |
h: 64, path: 'sprites/meter-dark.png' } | |
end | |
def render_instructions | |
outputs.labels << { x: 640, y: 40, | |
text: '[left/right] to move, [up/space] to jump, [down] to drop through platform', | |
alignment_enum: 1 } | |
if state.you_win | |
outputs.labels << { x: 640, y: 40.from_top, | |
text: 'You win!', | |
size_enum: -1, alignment_enum: 1 } | |
end | |
end | |
def render_render_queues | |
outputs.sprites << state.jitter_fade_out_render_queue | |
outputs.sprites << state.game_over_render_queue | |
end | |
def render_light_meter_warning | |
return if state.light_meter >= 255 | |
# the screen starts to dim if they are close to having a game over because of a depleated light meter | |
outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, | |
a: 255 - state.light_meter, | |
path: :pixel, r: 0, g: 0, b: 0 } | |
outputs.primitives << { x: state.light_crystal.x - 32, | |
y: state.light_crystal.y - 32, | |
w: 128, | |
h: 128, | |
a: 255 - state.light_meter, | |
path: 'sprites/spotlight.png' } | |
end | |
def render_light_crystal | |
jitter_sprite = { x: state.light_crystal.x + 5 * rand, | |
y: state.light_crystal.y + 5 * rand, | |
w: state.light_crystal.w + 5 * rand, | |
h: state.light_crystal.h + 5 * rand, | |
path: 'sprites/light.png' } | |
outputs.primitives << jitter_sprite | |
end | |
def render_entities | |
render_entity player, r: 0, g: 0, b: 0 | |
shadows.each { |shadow| render_entity shadow, g: 0, b: 0 } | |
end | |
def render_entity entity, r: 255, g: 255, b: 255; | |
# this is essentially the entity "prefab" the current action of the entity is consulted to | |
# determine what sprite should be rendered the action_at time is consulted to determine which frame | |
# of the sprite animation should be presented | |
a = 255 | |
entity.sprite = nil | |
if entity.activate_at | |
activation_elapsed_time = state.clock - entity.activate_at | |
if entity.activate_at > state.clock | |
entity.sprite = { x: entity.initial_x + 5 * rand, | |
y: entity.initial_y + 5 * rand, | |
w: 64 + 5 * rand, h: 64 + 5 * rand, | |
path: "sprites/light.png", g: 0, b: 0, a: a } | |
outputs.sprites << entity.sprite | |
return | |
elsif !entity.activated | |
entity.activated = true | |
state.jitter_fade_out_render_queue << { x: entity.initial_x + 5 * rand, | |
y: entity.initial_y + 5 * rand, | |
w: 86 + 5 * rand, h: 86 + 5 * rand, | |
path: "sprites/light.png", | |
g: 0, b: 0, a: 255 } | |
end | |
end | |
# this is the render outputs for an entities action state machine | |
if entity.action == :standing | |
path = "sprites/player/stand.png" | |
elsif entity.action == :running | |
sprint_index = entity.action_at | |
.frame_index count: 4, | |
hold_for: 8, | |
repeat: true, | |
tick_count_override: entity.clock | |
path = "sprites/player/run-#{sprint_index}.png" | |
elsif entity.action == :first_jump | |
sprint_index = entity.action_at | |
.frame_index count: 2, | |
hold_for: 8, | |
repeat: false, | |
tick_count_override: entity.clock | |
path = "sprites/player/jump-#{sprint_index || 1}.png" | |
elsif entity.action == :midair_jump | |
sprint_index = entity.action_at | |
.frame_index count: state.midair_jump_frame_count, | |
hold_for: state.midair_jump_hold_for, | |
repeat: false, | |
tick_count_override: entity.clock | |
path = "sprites/player/midair-jump-#{sprint_index || 8}.png" | |
elsif entity.action == :falling | |
path = "sprites/player/falling.png" | |
end | |
flip_horizontally = true if entity.orientation == :left | |
entity.sprite = entity.render_rect.merge path: path, a: a, r: r, g: g, b: b, flip_horizontally: flip_horizontally | |
outputs.sprites << entity.sprite | |
end | |
def new_game | |
state.clock = 0 | |
state.game_over = false | |
state.gravity = -0.4 | |
state.drag = 0.15 | |
state.activation_time = 90 | |
state.light_meter = 600 | |
state.light_meter_queue = 0 | |
state.midair_jump_frame_count = 9 | |
state.midair_jump_hold_for = 6 | |
state.midair_jump_duration = state.midair_jump_frame_count * state.midair_jump_hold_for | |
# hard coded collision tiles | |
state.tiles = [ | |
{ impassable: true, x: 0, y: 0, w: 1280, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ impassable: true, x: 0, y: 0, w: 8, h: 1500, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ impassable: true, x: 1280 - 8, y: 0, w: 8, h: 1500, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 80 + 320 + 80, y: 128, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 80 + 320 + 80 + 320 + 80, y: 192, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 160, y: 320, w: 400, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 160 + 400 + 160, y: 400, w: 400, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 320, y: 600, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 8, y: 500, w: 100, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
{ x: 8, y: 60, w: 100, h: 8, path: :pixel, r: 0, g: 0, b: 0 }, | |
] | |
state.player = new_entity | |
state.player.jump_count = 1 | |
state.player.jumped_at = state.player.clock | |
state.player.jumped_down_at = 0 | |
state.shadows = [] | |
state.input_timeline = [ | |
{ at: 0, k: :left_right, v: inputs.left_right }, | |
{ at: 0, k: :space, v: false }, | |
{ at: 0, k: :down, v: false }, | |
] | |
state.jitter_fade_out_render_queue = [] | |
state.game_over_render_queue ||= [] | |
state.light_crystal = new_light_crystal | |
end | |
def new_light_crystal | |
r = { x: 124 + rand(1000), y: 135 + rand(500), w: 64, h: 64 } | |
return new_light_crystal if tiles.any? { |t| t.intersect_rect? r } | |
return new_light_crystal if (player.x - r.x).abs < 200 | |
r | |
end | |
def entity_active? entity | |
return true unless entity.activate_at | |
return entity.activate_at <= state.clock | |
end | |
def add_shadow! | |
s = new_entity(from_entity: player) | |
s.activate_at = state.clock + state.activation_time * (shadows.length + 1) | |
s.spawn_countdown = state.activation_time | |
shadows << s | |
end | |
def find_input_timeline at:, key:; | |
state.input_timeline.find { |t| t.at <= at && t.k == key }.v | |
end | |
def new_entity from_entity: nil | |
# these are all the properties of an entity an optional from_entity can be passed in | |
# for "cloning" an entity/setting an entities starting state | |
pe = state.new_entity(:body) | |
pe.w = 96 | |
pe.h = 96 | |
pe.jump_power = 12 | |
pe.y = 500 | |
pe.x = 640 - 8 | |
pe.initial_x = pe.x | |
pe.initial_y = pe.y | |
pe.dy = 0 | |
pe.dx = 0 | |
pe.jumped_down_at = 0 | |
pe.jumped_at = 0 | |
pe.jump_count = 0 | |
pe.clock = state.clock | |
pe.orientation = :right | |
pe.action = :falling | |
pe.action_at = state.clock | |
pe.left_right = 0 | |
if from_entity | |
pe.w = from_entity.w | |
pe.h = from_entity.h | |
pe.jump_power = from_entity.jump_power | |
pe.x = from_entity.x | |
pe.y = from_entity.y | |
pe.initial_x = from_entity.x | |
pe.initial_y = from_entity.y | |
pe.dy = from_entity.dy | |
pe.dx = from_entity.dx | |
pe.jumped_down_at = from_entity.jumped_down_at | |
pe.jumped_at = from_entity.jumped_at | |
pe.orientation = from_entity.orientation | |
pe.action = from_entity.action | |
pe.action_at = from_entity.action_at | |
pe.jump_count = from_entity.jump_count | |
pe.left_right = from_entity.left_right | |
end | |
pe | |
end | |
def entity_on_platform? entity | |
entity.action == :standing || entity.action == :running | |
end | |
def entity_action_complete? entity, action_duration | |
entity.action_at.elapsed_time(entity.clock) + 1 >= action_duration | |
end | |
def entity_set_action! entity, action | |
entity.action = action | |
entity.action_at = entity.clock | |
entity.last_standing_at = entity.clock if action == :standing | |
end | |
def player | |
state.player | |
end | |
def shadows | |
state.shadows | |
end | |
def tiles | |
state.tiles | |
end | |
def find_tiles &block | |
tiles.find_all(&block) | |
end | |
def find_collision tiles, target | |
tiles.find { |t| t.rect.intersect_rect? target } | |
end | |
end | |
def boot args | |
# initialize the game on boot | |
$game = Game.new | |
end | |
def tick args | |
# tick the game class after setting .args | |
# (which is provided by the engine) | |
$game.args = args | |
$game.tick | |
end | |
# debug function for resetting the game if requested | |
def reset args | |
$game = Game.new | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment