Last active
September 8, 2026 03:41
-
-
Save amirrajan/5ec37929e9987a72d98d5adb50547856 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - The Keeper + Shaders (https://youtu.be/Y7Oc2V5fDUc)
This file contains hidden or 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
| class Game | |
| attr_gtk | |
| attr :queues | |
| def initialize | |
| @keeper = { | |
| max_speed: 3, | |
| acceleration: 0.1, | |
| x: 659, | |
| y: 260, | |
| w: 191, | |
| h: 156, | |
| speed: 0, | |
| angle: 0, | |
| action: :idle, | |
| sprite_data: load_keeper_sprite_data, | |
| ripples: [] | |
| } | |
| @queues = { | |
| ripples: [] | |
| } | |
| @lilies = 30.map { random_lily_pads } | |
| end | |
| def tick | |
| @keeper.angle -= inputs.left_right | |
| @keeper.speed *= 0.99 | |
| if inputs.up || inputs.controller_one.r1 | |
| @keeper.action = :moving | |
| @keeper.move_at ||= Kernel.tick_count | |
| @keeper.speed += @keeper.acceleration | |
| else | |
| @keeper.action = :idle | |
| @keeper.move_at = nil | |
| end | |
| calc_keeper | |
| calc_lilies | |
| calc_ripples | |
| end | |
| def primitives | |
| [ | |
| background_primitives, | |
| clouds_primitives, | |
| ripples_primitives, | |
| island_reflection_primitives, | |
| island_primitives, | |
| lilies_primitives, | |
| keeper_primitives, | |
| horus_primitives, | |
| fog_primitives, | |
| ] | |
| end | |
| def background_primitives | |
| { x: 0, y: 0, w: 1280, h: 720, path: "sprites/background.png" } | |
| end | |
| def ripples_primitives | |
| @queues.ripples | |
| end | |
| def clouds_primitives | |
| [ | |
| { x: Kernel.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" }, | |
| { x: -1280 + Kernel.tick_count % 1280, y: 0, w: 1280, h: 720, path: "sprites/clouds.png" } | |
| ] | |
| end | |
| def willow_tree_primitives | |
| index = 0.frame_index(count: 24, hold_for: 6, repeat: true) | |
| [ | |
| { x: 75, y: 455, w: 250 / 1.5, h: 350 / 1.5, path: "sprites/tree_yellow.png" }, | |
| { x: 30, y: 450, w: 393 / 1.5, h: 409 / 1.5, path: "sprites/willowtree/#{index}.png" } | |
| ] | |
| end | |
| def island_primitives | |
| [ | |
| { x: 0, y: 0, w: 1280, h: 720, path: "sprites/land.png" }, | |
| { x: 0, y: 0, w: 1280, h: 720, path: "sprites/house.png" }, | |
| { x: 0, y: 0, w: 1280, h: 720, path: "sprites/vignette.png" }, | |
| willow_tree_primitives | |
| ] | |
| end | |
| def island_reflection_primitives | |
| [ | |
| frame_for(sprite_directory: "sprites/dock-shadow", frame_count: 11), | |
| frame_for(sprite_directory: "sprites/grass-land", frame_count: 15), | |
| frame_for(sprite_directory: "sprites/grass-cliff", frame_count: 16), | |
| frame_for(sprite_directory: "sprites/dock-water", frame_count: 6), | |
| frame_for(sprite_directory: "sprites/cliff-water", frame_count: 11), | |
| ] | |
| end | |
| def keeper_primitives | |
| keeper_sprite = @keeper.sprite_data[@keeper.angle.round] | |
| boat_sprite_path = "sprites/boat/#{keeper_sprite.boat_sprite_path}" | |
| keeper_animation_frame = (@keeper.move_at || 0).frame_index count: 30, hold_for: 8, repeat: true | |
| [ | |
| { x: @keeper.x, | |
| y: @keeper.y, | |
| w: @keeper.w, | |
| h: @keeper.h, | |
| path: boat_sprite_path, | |
| flip_horizontally: keeper_sprite.boat_sprite_flip, | |
| angle: keeper_sprite.boat_delta_angle, | |
| angle_anchor_x: 0.5, | |
| angle_anchor_y: 0.5 }, | |
| { x: @keeper.x, | |
| y: @keeper.y, | |
| w: 191, | |
| h: 156, | |
| path: "sprites/keeper/#{@keeper.action}/#{keeper_sprite.keeper_sprite_angle}-#{keeper_animation_frame}.png", | |
| flip_horizontally: keeper_sprite.keeper_sprite_flip, | |
| angle_anchor_x: 0.5, | |
| angle_anchor_y: 0.5 } | |
| ] | |
| end | |
| def horus_primitives | |
| index = 0.frame_index(count: 15, | |
| hold_for: 10, | |
| repeat: true) | |
| [ | |
| { x: 325, | |
| y: 150, | |
| w: 134 / 2, | |
| h: 163 / 2, | |
| path: "sprites/horus/#{index}.png" } | |
| ] | |
| end | |
| def fog_primitives | |
| { x: 0, y: 0, w: 1280, h: 720, path: "sprites/fog.png" } | |
| end | |
| def lilies_primitives | |
| @lilies | |
| end | |
| def water_noise_primitives | |
| szw = 1000 | |
| szh = 1000 | |
| w = 2 * szw | |
| h = 2 * szh | |
| easing_perc = ((Kernel.tick_count + 3000) % 6000) / 6000 | |
| sprite_props = { w: szw, h: szh, path: "sprites/water-displacement.png", a: 255 } | |
| tile_count = (((szw * 2).idiv szw) * 3) | |
| tile_count.flat_map do |i| | |
| tile_count.map do |j| | |
| [ | |
| { x: szw * (j - tile_count.idiv(2) - 1) + szw * easing_perc + szw * (i - 1), | |
| y: szh * (i - tile_count.idiv(2)), | |
| **sprite_props, a: 128 }, | |
| { x: (2 * szw) - (szw * (j - tile_count.idiv(2) - 1) + szw * (i - 1)), | |
| y: szh * easing_perc + szh * (i - tile_count.idiv(2)), | |
| **sprite_props, a: 128 }, | |
| ] | |
| end | |
| end | |
| end | |
| def frame_for(sprite_directory:, frame_count:) | |
| frame_index = Numeric.frame_index(start_at: 0, | |
| frame_count: frame_count, | |
| hold_for: 6, | |
| repeat: true) | |
| { x: 0, y: 0, w: 1280, h: 720, path: "#{sprite_directory}/#{frame_index}.png", a: 255 } | |
| end | |
| def calc_keeper | |
| @keeper.angle = @keeper.angle % 360 | |
| @keeper.angle = 360 - @keeper.angle if @keeper.angle < 0 | |
| @keeper.speed = @keeper.speed.clamp 0, @keeper.max_speed | |
| @keeper.x += @keeper.angle.vector_x @keeper.speed | |
| @keeper.y += @keeper.angle.vector_y @keeper.speed | |
| top_edge = 720 | |
| bottom_edge = 0 - @keeper.w | |
| left_edge = 550 | |
| right_edge = 1280 - @keeper.w | |
| if @keeper.x > right_edge | |
| @keeper.x = right_edge | |
| elsif @keeper.x < left_edge | |
| @keeper.x = left_edge | |
| end | |
| if @keeper.y > top_edge | |
| @keeper.y = bottom_edge | |
| elsif @keeper.y < bottom_edge | |
| @keeper.y = top_edge | |
| end | |
| end | |
| def calc_lilies | |
| @lilies.each do |groups| | |
| groups.each do |l| | |
| g = 100 | |
| m1 = 10 | |
| m2 = 5 | |
| selected_point = { x: @keeper.x + 96, y: @keeper.y + 78 } | |
| angle = Geometry.angle_to selected_point, l | |
| boat_vector = angle.to_vector | |
| d = Geometry.distance selected_point, l | |
| origin_d = Geometry.distance l[:origin], l | |
| origin_angle = Geometry.angle_from l[:origin], l | |
| k = 0.03 | |
| f = (g * m1 * m2).fdiv(d**2) | |
| f = 5 if f > 10 | |
| s = origin_d * k | |
| dx = (boat_vector.x * f) + (origin_angle.vector_x * s) | |
| dy = (boat_vector.y * f) + (origin_angle.vector_y * s) | |
| l.x += dx | |
| l.y += dy | |
| magnitude = (dx + dy).abs | |
| zm = if magnitude > 3 | |
| 5 | |
| elsif magnitude > 2 | |
| 10 | |
| elsif magnitude > 1 | |
| 15 | |
| else | |
| 30 | |
| end | |
| next if d == 0 | |
| next if magnitude < 0.5 | |
| next if !Kernel.tick_count.zmod?(zm) | |
| front_ripple_rect = { | |
| x: l.x - l.w / 2, | |
| y: l.y - l.h / 2, | |
| w: 5, | |
| h: 5 | |
| } | |
| @queues.ripples << front_ripple_rect.merge( | |
| path: "sprites/ripples/ripple-0-front.png", | |
| angle_anchor_x: 0.5, | |
| angle_anchor_y: 0.5, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| angle: 0, | |
| dw: 0.5 * magnitude, | |
| dh: 0.5 * magnitude, | |
| da: 1, | |
| dx: 0, | |
| dy: 0 | |
| ) | |
| end | |
| end | |
| end | |
| def calc_ripples | |
| if @keeper.speed.round > 0 && Kernel.tick_count.zmod?(20) | |
| queue_ripples | |
| end | |
| if Kernel.tick_count.zmod?(60) | |
| queue_ripples | |
| end | |
| @queues.ripples.each do |r| | |
| r.dw ||= 2 | |
| r.dh ||= 2 | |
| r.da ||= 2 | |
| r.a ||= 255 | |
| if r.w > 100 | |
| r.a -= r.da.abs | |
| end | |
| r.w += r.dw | |
| r.h += r.dh | |
| if !r.anchor_x | |
| r.x -= r.dw / 2 | |
| end | |
| if !r.anchor_y | |
| r.y -= r.dh / 2 | |
| end | |
| r.x += r.dx | |
| r.y += r.dy | |
| r.dx *= 0.95 | |
| r.dy *= 0.95 | |
| end | |
| @queues.ripples.reject! { |r| r.a < 0 } | |
| end | |
| def load_keeper_sprite_data | |
| contents = DR.read_file "app/keeper-sprites.txt" | |
| sprite_data = {} | |
| contents.each_line do |l| | |
| tokens = l.split "," | |
| angle = tokens[0].strip.to_i | |
| sprite_data[angle] = { | |
| angle: tokens[0].strip.to_i, | |
| keeper_sprite_angle: tokens[1].strip.to_i, | |
| keeper_sprite_flip: tokens[2].strip == "true", | |
| boat_sprite_path: tokens[3].strip, | |
| boat_sprite_flip: tokens[4].strip == "true", | |
| boat_delta_angle: tokens[5].strip.to_i | |
| } | |
| end | |
| sprite_data | |
| end | |
| def random_lily_pads | |
| big_lily = random_big_lily | |
| small_lily_1 = random_small_lily big_lily | |
| small_lily_2 = random_small_lily big_lily | |
| lotus = random_lotus big_lily | |
| [big_lily, small_lily_1, small_lily_2, lotus].reject_nil | |
| end | |
| def random_big_lily | |
| x = 600 * rand + 600 | |
| y = 600 * rand + 30 | |
| big_lily_1 = { path: 'sprites/lilies/lilypad1.png', w: 30, h: 25 } | |
| big_lily_2 = { path: 'sprites/lilies/lilypad3.png', w: 42, h: 26 } | |
| [big_lily_1, big_lily_2].sample | |
| .merge(x: x, | |
| y: y, | |
| origin: { x: x, y: y }) | |
| end | |
| def random_small_lily big_lily | |
| small_lily_1 = { path: 'sprites/lilies/lilypad2.png', w: 22, h: 17 } | |
| small_lily_2 = { path: 'sprites/lilies/lilypad4.png', w: 26, h: 20 } | |
| small_lily = [small_lily_1, small_lily_2].sample | |
| place_lily_nearby big_lily, small_lily | |
| end | |
| def random_lotus big_lily | |
| if rand > 0.65 | |
| flower = { path: 'sprites/lilies/lotus.png', w: 37, h: 33 } | |
| place_lily_nearby big_lily, flower | |
| elsif rand > 0.5 | |
| lotuspod1 = { path: 'sprites/lilies/lotuspod1.png', w: 18, h: 63 } | |
| lotuspod2 = { path: 'sprites/lilies/lotuspod2.png', w: 16, h: 32 } | |
| place_lily_nearby big_lily, [lotuspod1, lotuspod2].sample | |
| end | |
| end | |
| def place_lily_nearby big_lily, small_lily | |
| small_lily_distance = -5 | |
| coin_flip = rand | |
| small_lily_origin = if coin_flip > 0.75 | |
| { x: big_lily.right + small_lily_distance, | |
| y: big_lily.top + small_lily_distance } | |
| elsif coin_flip > 0.5 | |
| { x: big_lily.right + small_lily_distance, | |
| y: big_lily.bottom - small_lily_distance - small_lily.h } | |
| elsif coin_flip > 0.25 | |
| { x: big_lily.left - small_lily_distance - small_lily.w, | |
| y: big_lily.top } | |
| else | |
| { x: big_lily.left - small_lily_distance - small_lily.w, | |
| y: big_lily.bottom - small_lily_distance - small_lily.h } | |
| end | |
| small_lily.merge(x: small_lily_origin.x, | |
| y: small_lily_origin.y, | |
| origin: { x: small_lily_origin.x, y: small_lily_origin.y }) | |
| end | |
| def queue_ripples | |
| front_ripple_rect = { | |
| x: @keeper.x, | |
| y: @keeper.y, | |
| w: 20, | |
| h: 17 | |
| }.center_inside_rect(@keeper) | |
| front_ripple_rect.x += @keeper.angle.vector_x * (@keeper.w / 4) | |
| front_ripple_rect.y += @keeper.angle.vector_y * (@keeper.h / 4) | |
| @queues.ripples << front_ripple_rect.merge(path: "sprites/ripples/ripple-0-front.png", | |
| angle_anchor_x: 0.5, | |
| angle_anchor_y: 0.5, | |
| angle: @keeper.angle, | |
| dx: @keeper.angle.vector_x * @keeper.speed, | |
| dy: @keeper.angle.vector_y * @keeper.speed) | |
| end | |
| end |
This file contains hidden or 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
| Texture2D scene : register(t0, space2); | |
| Texture2D displacement_map : register(t1, space2); | |
| Texture2D water_mask : register(t2, space2); | |
| SamplerState sampler0 : register(s0, space2); | |
| SamplerState sampler1 : register(s1, space2); | |
| SamplerState sampler2 : register(s2, space2); | |
| struct Input { | |
| float4 tex_color : COLOR0; | |
| float2 tex_coord : TEXCOORD0; | |
| }; | |
| struct Output { | |
| float4 frag_color : SV_Target; | |
| }; | |
| Output main(Input input) { | |
| Output output; | |
| float4 tex1_color = displacement_map.Sample(sampler1, input.tex_coord); | |
| float displacement_perc = 0.1; | |
| float2 displacement_uv = float2( | |
| input.tex_coord.x + (tex1_color.r * displacement_perc - displacement_perc / 2.0), | |
| input.tex_coord.y + (tex1_color.r * displacement_perc - displacement_perc / 2.0) | |
| ); | |
| float2 resolved_uv = float2( | |
| clamp(displacement_uv.x, 0.0, 1.0), | |
| clamp(displacement_uv.y, 0.0, 1.0) | |
| ); | |
| float3 dst_mask_rgb = water_mask.Sample(sampler2, input.tex_coord).rgb; | |
| float3 src_mask_rgb = water_mask.Sample(sampler2, resolved_uv).rgb; | |
| float dst_mask = max(dst_mask_rgb.r, max(dst_mask_rgb.g, dst_mask_rgb.b)); | |
| float src_mask = max(src_mask_rgb.r, max(src_mask_rgb.g, src_mask_rgb.b)); | |
| float can_displace = (dst_mask > 0.99 && src_mask > 0.99) ? 1.0 : 0.0; | |
| float2 scene_uv = lerp(input.tex_coord, resolved_uv, can_displace); | |
| output.frag_color = scene.Sample(sampler0, scene_uv) * input.tex_color; | |
| return output; | |
| } |
This file contains hidden or 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
| require 'app/game.rb' | |
| require 'shaders/keeper.frag.hlsl' | |
| module Main | |
| def start | |
| @game = Game.new | |
| end | |
| def tick | |
| @game.args = args | |
| @game.tick | |
| outputs[:scrolling_water].set w: 2000, | |
| h: 2000, | |
| background_color: [0, 0, 0, 0] | |
| outputs[:scrolling_water].primitives << @game.water_noise_primitives | |
| outputs[:water_displacement].set w: 1280, | |
| h: 720, | |
| background_color: [128, 128, 128, 255] | |
| outputs[:water_displacement].primitives << { x: 640, | |
| y: 380, | |
| w: 1440, | |
| h: 1440, | |
| path: :scrolling_water, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5 } | |
| outputs[:water_displacement].primitives << @game.queues.ripples.map { |r| r.merge(path: "sprites/ripple-displacement.png") } | |
| outputs[:water_mask].set w: 1280, h: 720, background_color: [255, 255, 255, 255] | |
| outputs[:water_mask].primitives << @game.island_primitives.flatten.map do |s| | |
| { **s, r: 0, g: 0, b: 0, a: 255 } | |
| end | |
| outputs[:water_mask].primitives << @game.keeper_primitives.flatten.map do |s| | |
| { **s, r: 0, g: 0, b: 0, a: 255 } | |
| end | |
| outputs[:water_mask].primitives << @game.lilies_primitives.flatten.map do |s| | |
| { **s, r: 0, g: 0, b: 0, a: 255 } | |
| end | |
| outputs[:water_mask].primitives << @game.horus_primitives.flatten.map do |s| | |
| { **s, r: 0, g: 0, b: 0, a: 255 } | |
| end | |
| outputs.background_color = [0, 0, 0] | |
| outputs[:scene].set w: 1280, h: 720, background_color: [0, 0, 0, 255] | |
| outputs[:scene].shader = { | |
| path: "shaders/keeper.frag.hlsl", | |
| textures: [ | |
| :water_displacement, | |
| :water_mask | |
| ] | |
| } | |
| outputs[:scene].primitives << @game.primitives | |
| outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :scene } | |
| # outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :water_displacement } | |
| # outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :water_mask } | |
| end | |
| end | |
| DR.reboot |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment