Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Last active September 8, 2026 03:41
Show Gist options
  • Select an option

  • Save amirrajan/5ec37929e9987a72d98d5adb50547856 to your computer and use it in GitHub Desktop.

Select an option

Save amirrajan/5ec37929e9987a72d98d5adb50547856 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - The Keeper + Shaders (https://youtu.be/Y7Oc2V5fDUc)
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
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;
}
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