dragonruby-rendering
This skill covers advanced rendering patterns in DragonRuby GTK. For basic sprite/label/solid output see the main dragonruby skill.
Render Targets (Off-screen Buffers)
Render targets let you draw into a named virtual canvas, then use it as a sprite. They are very powerful for caching, effects, and UI composition.
# Draw into target each tick (transient! clears it each frame):
args.outputs[:hud].transient!
args.outputs[:hud].width = 400
args.outputs[:hud].height = 200
args.outputs[:hud].background_color = [0, 0, 0, 0] # transparent bg
args.outputs[:hud].sprites << { x: 0, y: 0, w: 32, h: 32, path: 'sprites/icon.png' }
args.outputs[:hud].labels << { x: 200, y: 100, text: "HP: 3", alignment_enum: 1 }
# Composite into the main scene:
args.outputs.sprites << { x: 0, y: 520, w: 400, h: 200, path: :hud }
Cached render target (draw once, reuse)
# Build the target only on tick 0 (or when content changes):
if Kernel.tick_count == 0
args.outputs[:map_tiles].width = 2560
args.outputs[:map_tiles].height = 1440
world_tiles.each { |t| args.outputs[:map_tiles].sprites << t }
end
# Render a viewport portion of the cached target each tick:
args.outputs.sprites << {
x: 0, y: 0, w: 1280, h: 720,
path: :map_tiles,
source_x: cam.x, source_y: cam.y,
source_w: 1280, source_h: 720
}
Checking target readiness
return if args.outputs.render_targets.queued?(:my_target)
return unless args.outputs.render_targets.ready?(:my_target)
Blendmodes & Clipping
# Blendmode 0 = ignore alpha (overwrite). Use for hollow borders:
args.outputs[:border_target].primitives << [
{ x: 0, y: 0, w: w, h: h, path: :solid, r: 255, g: 255, b: 255 },
{ x: t, y: t, w: w - t*2, h: h - t*2, path: :empty, blendmode: 0 }
]
Clipping (viewport mask)
args.outputs[:clipped].width = clip_w
args.outputs[:clipped].height = clip_h
args.outputs[:clipped].sprites << all_world_sprites
args.outputs.sprites << {
x: clip_x, y: clip_y, w: clip_w, h: clip_h,
path: :clipped,
source_x: clip_x, source_y: clip_y,
source_w: clip_w, source_h: clip_h
}
Camera Systems
Simple follow camera
# Camera state
args.state.camera ||= { x: 0, y: 0, scale: 1.0 }
# Smooth follow
args.state.camera.x = args.state.camera.x.lerp(player.x - 640, 0.08)
args.state.camera.y = args.state.camera.y.lerp(player.y - 360, 0.08)
# Apply camera to all world entities:
def world_to_screen(camera, rect)
{ **rect,
x: rect.x - camera.x,
y: rect.y - camera.y }
end
Scalable camera (zoom + pan)
VIEWPORT_W = 1280
VIEWPORT_H = 720
def to_screen_space(camera, rect)
{
**rect,
x: rect[:x] * camera[:scale] - camera[:x] * camera[:scale] + VIEWPORT_W / 2,
y: rect[:y] * camera[:scale] - camera[:y] * camera[:scale] + VIEWPORT_H / 2,
w: rect[:w] * camera[:scale],
h: rect[:h] * camera[:scale]
}
end
def to_world_space(camera, rect)
{
x: (rect[:x] - VIEWPORT_W / 2 + camera[:x] * camera[:scale]) / camera[:scale],
y: (rect[:y] - VIEWPORT_H / 2 + camera[:y] * camera[:scale]) / camera[:scale]
}
end
# Usage:
args.outputs.sprites << world_entities.map { |e| to_screen_space(args.state.camera, e) }
Viewport culling (only render visible entities)
viewport = {
x: camera.x - 640 / camera.scale,
y: camera.y - 360 / camera.scale,
w: 1280 / camera.scale,
h: 720 / camera.scale
}
visible = Geometry.find_all_intersect_rect(viewport, args.state.world_entities)
args.outputs.sprites << visible.map { |e| to_screen_space(camera, e) }
Camera shake (trauma system)
args.state.trauma ||= 0.0
args.state.trauma = (args.state.trauma + 0.3).clamp(0, 1) # on hit
# Apply shake:
shake = 20.0 * args.state.trauma ** 2
args.state.camera.x_offset = shake.randomize(:ratio, :sign)
args.state.camera.y_offset = shake.randomize(:ratio, :sign)
args.state.trauma *= 0.93 # decay
Z-targeting / orbit camera
args.state.camera.angle ||= 0
distance = 200
target_x = entity.x + args.state.camera.angle.vector_x * distance
target_y = entity.y + args.state.camera.angle.vector_y * distance
args.state.camera.angle += 1 # orbit
Render target + camera (full pipeline)
# Draw world to a render target using camera offset:
args.outputs[:scene].transient!
args.outputs[:scene].width = 1280
args.outputs[:scene].height = 720
args.outputs[:scene].sprites << visible_tiles.map { |t|
t.merge(x: t.x - cam.x, y: t.y - cam.y)
}
# Blit scene + apply shake:
args.outputs.sprites << {
x: cam.x_offset.to_i, y: cam.y_offset.to_i,
w: 1280, h: 720, path: :scene
}
Pixel Arrays
Direct pixel-level manipulation for dynamic textures:
# Create a pixel array (ABGR 32-bit integers):
w = 64; h = 64
args.pixel_array(:canvas).width = w
args.pixel_array(:canvas).height = h
# Fill solid black:
args.pixel_array(:canvas).pixels.fill(0xFF000000, 0, w * h)
# Draw a horizontal green line at row y:
y = 32
args.pixel_array(:canvas).pixels.fill(0xFF00FF00, y * w, w)
# Set individual pixel (x, y):
def set_pixel(px_array, w, x, y, abgr)
px_array.pixels[y * w + x] = abgr
end
# ABGR hex format: 0xAABBGGRR
RED = 0xFF0000FF
GREEN = 0xFF00FF00
BLUE = 0xFFFF0000
# Animate pixel-by-pixel scanner:
args.state.scan_pos = (args.state.scan_pos || 0 + 1) % h
args.pixel_array(:scanner).width = w
args.pixel_array(:scanner).height = h
args.pixel_array(:scanner).pixels.fill(0xFF000000, 0, w * h)
args.pixel_array(:scanner).pixels.fill(0xFF00FF00, args.state.scan_pos * w, w)
# Use pixel array as a sprite:
args.outputs.sprites << { x: 100, y: 100, w: w, h: h, path: :canvas }
Load pixels from a file
px = GTK.get_pixels('sprites/map.png') # => { w:, h:, pixels: [] }
args.pixel_array(:map).width = px.w
args.pixel_array(:map).height = px.h
args.pixel_array(:map).pixels = px.pixels
HD & High-DPI Rendering
Automatic HD sprite selection
DragonRuby automatically picks the right resolution file by naming convention:
- 720p:
player.png - 1080p:
player@125.png - 1440p:
player@200.png - 4K:
player@300.png
No code changes needed — just provide the additional files.
# Query current rendering scale at runtime:
args.grid.texture_scale_enum # integer scale enum
args.grid.native_scale # float scale multiplier
Allscreen properties (ultrawide/notch support)
args.grid.allscreen_w # full renderable width (beyond safe area)
args.grid.allscreen_h
args.grid.allscreen_offset_x # offset from left to safe area
args.grid.left # safe area left edge
args.grid.right # safe area right edge
# Background that fills to screen edges:
args.outputs.sprites << {
x: args.grid.allscreen_left,
y: args.grid.allscreen_bottom,
w: args.grid.allscreen_w,
h: args.grid.allscreen_h,
path: 'sprites/bg.png',
source_x: 2000 - args.grid.allscreen_w / 2,
source_y: 2000 - args.grid.allscreen_h / 2,
source_w: args.grid.allscreen_w,
source_h: args.grid.allscreen_h
}
Low-Resolution (Pixel Art) Display
Scale a tiny virtual canvas up to fill the screen — classic for pixel art games:
GAME_W = 84; GAME_H = 48 # virtual resolution (e.g. Nokia 3310)
SCREEN_W = 1280; SCREEN_H = 720
ZOOM = [SCREEN_W / GAME_W, SCREEN_H / GAME_H].min
OFFSET_X = (SCREEN_W - GAME_W * ZOOM) / 2
OFFSET_Y = (SCREEN_H - GAME_H * ZOOM) / 2
def tick(args)
# Draw at native resolution into target:
args.outputs[:game].transient!
args.outputs[:game].width = GAME_W
args.outputs[:game].height = GAME_H
args.outputs[:game].background_color = [199, 240, 216]
# ... all game rendering goes here at GAME_W x GAME_H coords
# Scale up to screen:
args.outputs.sprites << {
x: OFFSET_X, y: OFFSET_Y,
w: GAME_W * ZOOM, h: GAME_H * ZOOM,
path: :game
}
end
# Translate mouse input from screen space back to game space:
def game_mouse(args)
{ x: (args.inputs.mouse.x - OFFSET_X).idiv(ZOOM),
y: (args.inputs.mouse.y - OFFSET_Y).idiv(ZOOM) }
end
Thick Lines
DragonRuby only draws 1px lines natively. Emulate thick lines with a rotated solid:
def thick_line(x:, y:, x2:, y2:, thickness: 3, r: 255, g: 255, b: 255, a: 255)
dx = x2 - x; dy = y2 - y
length = Math.sqrt(dx*dx + dy*dy)
angle = Math.atan2(dy, dx).to_degrees
{
x: x, y: y,
w: length, h: thickness,
angle: angle, angle_anchor_x: 0, angle_anchor_y: 0.5,
path: :solid,
r: r, g: g, b: b, a: a
}
end
args.outputs.sprites << thick_line(x: 100, y: 100, x2: 400, y2: 300, thickness: 4)
Repeating / Tiled Textures
def tiled_background(args, path, dest_x:, dest_y:, dest_w:, dest_h:)
sw, sh = args.gtk.calcspritebox(path)
rt_name = :"tile_#{path}"
if Kernel.tick_count == 0
args.outputs[rt_name].width = dest_w
args.outputs[rt_name].height = dest_h
cols = (dest_w.fdiv(sw)).ceil + 1
rows = (dest_h.fdiv(sh)).ceil + 1
args.outputs[rt_name].sprites << rows.map { |r|
cols.map { |c|
{ x: c * sw, y: dest_h - (r + 1) * sh, w: sw, h: sh, path: path }
}
}
end
{ x: dest_x, y: dest_y, w: dest_w, h: dest_h, path: rt_name }
end
args.outputs.sprites << tiled_background(args, 'sprites/grass.png',
dest_x: 0, dest_y: 0,
dest_w: 1280, dest_h: 720)
Coordinate System Origin Switching
args.grid.origin_center! # (0,0) at screen centre
args.grid.origin_bottom_left! # (0,0) at bottom-left (default)
Render targets inherit the current origin mode.
Tile-Based Large Maps
Only load the 9 tiles surrounding the player's position (3×3 grid of chunks):
CHUNK = 640
chunk_x = player.x.idiv(CHUNK)
chunk_y = player.y.idiv(CHUNK)
offset_x = 960 - (player.x - chunk_x * CHUNK)
offset_y = 960 - (player.y - chunk_y * CHUNK)
(-1..1).each do |cx|
(-1..1).each do |cy|
path = :"chunk_#{chunk_x + cx}_#{chunk_y + cy}"
args.outputs.sprites << {
x: offset_x + cx * CHUNK,
y: offset_y + cy * CHUNK,
w: CHUNK, h: CHUNK,
path: path
}
end
end
Screenshots
args.outputs.screenshots << {
filename: "screenshots/shot_#{Kernel.tick_count}.png",
x: 0, y: 0, w: 1280, h: 720,
chroma_key: { r: 0, g: 255, b: 0 } # optional: make green transparent
}
Static Sprites for Performance
# Add once; objects are held by reference and mutated in place:
if Kernel.tick_count == 0
args.state.stars = 500.map { { x: rand(1280), y: rand(720), w: 2, h: 2, path: :solid, r: 255, g: 255, b: 255 } }
args.outputs.static_sprites << args.state.stars
end
# Update positions without re-adding:
args.state.stars.each { |s| s[:x] = (s[:x] + 0.5) % 1280 }
Shaders (Indie/Pro)
args.outputs.sprites << {
x: 0, y: 0, w: 1280, h: 720,
path: 'sprites/scene.png',
shader: 'shaders/scanlines.glsl',
shader_tex1: :render_target_name,
# Uniforms:
shader_u_time: Kernel.tick_count / 60.0
}
More from nitemaeric/dragonruby-skills
dragonruby
Build games and prototypes with DragonRuby GTK (DRGTK). Use this skill when the user asks to write DragonRuby game code, implement game mechanics, work with the DRGTK API, or debug DragonRuby projects.
9dragonruby-ui
UI controls in DragonRuby GTK — buttons, checkboxes, toggles, scroll views, menus, input remapping, tooltips, progress bars, accessibility. Use when building game menus, HUDs, settings screens, or in-game UI widgets.
6dragonruby-audio
Advanced audio in DragonRuby GTK — spatial audio, procedural synthesis, beat synchronisation, crossfading, audio queues. Use when the user asks about DragonRuby music, sound effects beyond basic playback, rhythm games, or generated audio.
5dragonruby-yard
Set up YARD documentation and Solargraph LSP autocomplete for DragonRuby GTK projects. Use when the user asks about IDE autocomplete, type support, Solargraph, or editor integration for DragonRuby.
5dragonruby-3d
3D graphics techniques in DragonRuby GTK — raycasting, Mode7 floor projection, matrix transformations, wireframe rendering, sprite-based 3D, VR patterns. Use when the user asks about 3D visuals or pseudo-3D effects.
2dragonruby-pathfinding
Pathfinding algorithms in DragonRuby GTK — A*, BFS, flood fill, priority queues, spatial hashing, quad trees, line-of-sight. Use when the user asks about enemy AI navigation, movement grids, or reachability.
2