Skip to content

Instantly share code, notes, and snippets.

@icculus
Last active January 14, 2021 15:11
Show Gist options
  • Save icculus/a2a3957405d894e348f40370704c6990 to your computer and use it in GitHub Desktop.
Save icculus/a2a3957405d894e348f40370704c6990 to your computer and use it in GitHub Desktop.
# This is the complete source code to Flappy Dragon:
# https://dragonruby.itch.io/flappydragon
#
# You can tinker with this by getting a copy of DragonRuby Game Toolkit:
# https://dragonruby.org/
#
# Amir Rajan wrote this game, catch him at https://twitter.com/amirrajan !
class FlappyDragon
attr_accessor :grid, :inputs, :game, :outputs
def tick
defaults
render
calc
process_inputs
end
def defaults
game.flap_power = 11
game.gravity = 0.9
game.ceiling = 600
game.ceiling_flap_power = 6
game.wall_countdown_length = 100
game.wall_gap_size = 100
game.wall_countdown ||= 0
game.hi_score ||= 0
game.score ||= 0
game.walls ||= []
game.x ||= 50
game.y ||= 500
game.dy ||= 0
game.scene ||= :menu
game.scene_at ||= 0
game.difficulty ||= :normal
game.new_difficulty ||= :normal
game.countdown ||= 4.seconds
game.flash_at ||= 0
end
def render
outputs.sounds << "sounds/flappy-song.ogg" if game.tick_count == 1
render_score
render_menu
render_game
end
def render_score
outputs.primitives << [:labels, 10, 710, "HI SCORE: #{game.hi_score}", large_white_typeset]
outputs.primitives << [:labels, 10, 680, "SCORE: #{game.score}", large_white_typeset]
outputs.primitives << [:labels, 10, 650, "DIFFICULTY: #{game.difficulty.upcase}", large_white_typeset]
end
def render_menu
return unless game.scene == :menu
render_overlay
outputs.labels << [285, 700, "Flappy Dragon", 50, 0, 255, 255, 255]
outputs.labels << [330, 500, "Instructions: Press Spacebar to flap. Don't die.", 4, 0, 255, 255, 255]
outputs.labels << [450, 380, "[Tab] Difficulty: #{game.new_difficulty.capitalize}", 4, 0, 255, 255, 255]
outputs.labels << [450, 350, "[Enter] Start at New Difficulty ", 4, 0, 255, 255, 255]
outputs.labels << [450, 320, "[Escape] Cancel/Resume ", 4, 0, 255, 255, 255]
outputs.labels << [10, 100, "Code: @amirrajan", 255, 255, 255]
outputs.labels << [10, 80, "Art: @mobypixel", 255, 255, 255]
outputs.labels << [10, 60, "Music: @mobypixel", 255, 255, 255]
outputs.labels << [10, 40, "Engine: DragonRuby", 255, 255, 255]
end
def render_overlay
outputs.primitives << [:solids, grid.rect, 0, 0, 0, 230]
end
def render_game
render_game_over
render_background
render_walls
render_dragon
render_flash
end
def render_game_over
return unless game.scene == :game
outputs.labels << [638, 358, score_text, 20, 1]
outputs.labels << [635, 360, score_text, 20, 1, 255, 255, 255]
outputs.labels << [638, 428, countdown_text, 20, 1]
outputs.labels << [635, 430, countdown_text, 20, 1, 255, 255, 255]
end
def render_background
outputs.sprites << [0, 0, 1280, 720, 'sprites/background.png']
scroll_point_at = game.tick_count
scroll_point_at = game.scene_at if game.scene == :menu
scroll_point_at = game.death_at if game.countdown > 0
scroll_point_at ||= 0
outputs.sprites += scrolling_background scroll_point_at, 'sprites/parallax_back.png', 0.25
outputs.sprites += scrolling_background scroll_point_at, 'sprites/parallax_middle.png', 0.50
outputs.sprites += scrolling_background scroll_point_at, 'sprites/parallax_front.png', 1.00, -80
end
def render_walls
update_wall_prefabs
outputs.sprites += game.walls.map(&:sprites)
end
def render_dragon
game.show_death = true if game.countdown == 3.seconds
render_debug_hitbox false
if game.show_death == false || !game.death_at
animation_index = frame_for_animation game.flapped_at, 2, 6
game.flapped_at = false if animation_index == 5
sprite_name = "sprites/dragon_fly#{animation_index + 1}.png"
game.dragon_sprite = [game.x, game.y, 100, 80, sprite_name, game.dy * 1.2]
else
sprite_name = "sprites/dragon_die.png"
game.dragon_sprite = [game.x, game.y, 100, 80, sprite_name, game.dy * 1.2]
sprite_changed_elapsed = elapsed_time(game.death_at) - 1.seconds
game.dragon_sprite.angle += (sprite_changed_elapsed ** 1.3) * game.death_fall_direction * -1
game.dragon_sprite.x += (sprite_changed_elapsed ** 1.2) * game.death_fall_direction
game.dragon_sprite.y += (sprite_changed_elapsed * 14 - sprite_changed_elapsed ** 1.6)
end
outputs.sprites << game.dragon_sprite
end
def render_debug_hitbox show
return unless show
outputs.borders << game.dragon_sprite.rect if game.dragon_sprite
end
def render_flash
return unless game.flash_at
flash_length = 20
outputs.primitives << [:solids,
grid.rect,
white,
255 * game.tick_count.perc_to_zero(game.flash_at, flash_length)]
game.flash_at = 0 if elapsed_time(game.flash_at) > flash_length
end
def calc
return unless game.scene == :game
calc_countdown
game.countdown -= 1 and return if game.countdown > 0
calc_walls
calc_flap
calc_game_over
end
def calc_countdown
reset_game if game.countdown == 1
end
def calc_walls
game.walls.each { |w| w.x -= 8 }
walls_count_before_removal = game.walls.length
game.walls.reject! { |w| w.x < -100 }
game.score += 1 if game.walls.count < walls_count_before_removal
game.wall_countdown -= 1 and return if game.wall_countdown > 0
game.walls << game.new_entity(:wall) do |w|
w.x = grid.right
w.opening = grid.top
.randomize(:ratio)
.greater(200)
.lesser(520)
w.bottom_height = w.opening - game.wall_gap_size
w.top_y = w.opening + game.wall_gap_size
end
game.wall_countdown = game.wall_countdown_length
end
def calc_flap
game.y += game.dy
game.dy = game.dy.lesser game.flap_power
game.dy -= game.gravity
return if game.y < game.ceiling
game.y = game.ceiling
game.dy = game.dy.lesser game.ceiling_flap_power
end
def calc_game_over
return unless game_over?
game.death_at = game.tick_count
game.death_from = game.walls.first
game.death_fall_direction = -1
game.death_fall_direction = 1 if game.x > game.death_from.x
outputs.sounds << "sounds/hit-sound.wav"
begin_countdown
end
def process_inputs
process_inputs_menu
process_inputs_game
end
def process_inputs_menu
return unless game.scene == :menu
if inputs.keyboard.key_down.tab
case game.new_difficulty
when :easy
game.new_difficulty = :normal
when :normal
game.new_difficulty = :hard
when :hard
game.new_difficulty = :flappy
when :flappy
game.new_difficulty = :easy
end
end
if inputs.keyboard.key_down.enter
game.difficulty = game.new_difficulty
change_to_scene :game
reset_game false
game.hi_score = 0
begin_countdown
end
if inputs.keyboard.key_down.escape
game.new_difficulty = game.difficulty
change_to_scene :game
end
end
def process_inputs_game
return unless game.scene == :game
if inputs.keyboard.key_down.escape || inputs.keyboard.key_down.enter
change_to_scene :menu
elsif inputs.keyboard.key_down.space && game.countdown == 0
game.dy = 0
game.dy += game.flap_power
game.flapped_at = game.tick_count
outputs.sounds << "sounds/fly-sound.wav"
end
end
def scrolling_background at, path, rate, y = 0
[
[ 0 - at.*(rate) % 1440, y, 1440, 720, path],
[1440 - at.*(rate) % 1440, y, 1440, 720, path]
]
end
def update_wall_prefabs
game.walls.each do |w|
w.sprites = [
[w.x, w.bottom_height - 720, 100, 720, 'sprites/wall.png', 180],
[w.x, w.top_y, 100, 720, 'sprites/wallbottom.png', 0]
]
end
end
def elapsed_time at
game.tick_count - at
end
def frame_for_animation started_at, hold_time, number_of_key_frames
return 0 unless started_at
elapsed_time(started_at).idiv(hold_time) % number_of_key_frames
end
def white
[255, 255, 255]
end
def large_white_typeset
[5, 0, 255, 255, 255]
end
def at_beginning?
game.walls.count == 0
end
def anchor_center
[0.5, 0.5]
end
def dragon_collision_box
game.dragon_sprite
.scale_rect(1.0 - collision_forgiveness,
anchor_center)
.rect_shift_right(10)
.rect_shift_up(game.dy * 2)
end
def game_over?
return true if game.y <= 0.-(500 * collision_forgiveness) && !at_beginning?
game.walls
.flat_map { |w| w.sprites }
.any? do |s|
s.intersects_rect?(dragon_collision_box)
end
end
def collision_forgiveness
case game.difficulty
when :easy
0.9
when :normal
0.7
when :hard
0.5
when :flappy
0.3
else
0.9
end
end
def countdown_text
game.countdown ||= -1
return "" if game.countdown == 0
return "GO!" if game.countdown.idiv(60) == 0
return "GAME OVER" if game.death_at
return "READY?"
end
def begin_countdown
game.countdown = 4.seconds
end
def score_text
return "" unless game.countdown > 1.seconds
return "" unless game.death_at
return "SCORE: 0 (LOL)" if game.score == 0
return "HI SCORE: #{game.score}" if game.score == game.hi_score
return "SCORE: #{game.score}"
end
def reset_game set_flash = true
game.flash_at = game.tick_count if set_flash
game.walls = []
game.y = 500
game.dy = 0
game.hi_score = game.hi_score.greater(game.score)
game.score = 0
game.wall_countdown = game.wall_countdown_length.fdiv(2)
game.show_death = false
game.death_at = nil
end
def change_to_scene scene
game.scene = scene
game.scene_at = game.tick_count
inputs.keyboard.clear
end
end
$flappy_dragon = FlappyDragon.new
def tick args
$flappy_dragon.grid = args.grid
$flappy_dragon.inputs = args.inputs
$flappy_dragon.game = args.game
$flappy_dragon.outputs = args.outputs
$flappy_dragon.tick
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment