Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Created September 25, 2026 06:53
Show Gist options
  • Select an option

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

Select an option

Save amirrajan/b434842a2bacf62eb6e52128eb3738af to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Live in-game map editor with AABB collision and camera. (https://youtu.be/N71tnzHuNq4)
class Camera
class << self
def viewport_w
Grid.allscreen_w
end
def viewport_h
Grid.allscreen_h
end
def viewport_w_half
if Grid.origin_center?
0
else
Grid.allscreen_w.fdiv(2).ceil
end
end
def viewport_h_half
if Grid.origin_center?
0
else
Grid.allscreen_h.fdiv(2).ceil
end
end
def viewport_offset_x
if Grid.origin_center?
0
else
Grid.allscreen_x
end
end
def viewport_offset_y
if Grid.origin_center?
0
else
Grid.allscreen_y
end
end
def __to_world_space__ camera, rect
return nil if !rect
x = (rect.x - viewport_w_half + camera.x * camera.scale - viewport_offset_x) / camera.scale
y = (rect.y - viewport_h_half + camera.y * camera.scale - viewport_offset_y) / camera.scale
if rect.w
w = rect.w / camera.scale
h = rect.h / camera.scale
{ **rect, x: x, y: y, w: w, h: h }
else
{ **rect, x: x, y: y }
end
end
def to_world_space camera, rect
if rect.is_a? Array
rect.map { |r| to_world_space camera, r }
else
__to_world_space__ camera, rect
end
end
def __to_screen_space__ camera, rect
return nil if !rect
x = rect.x * camera.scale - camera.x * camera.scale + viewport_w_half
y = rect.y * camera.scale - camera.y * camera.scale + viewport_h_half
if rect.w
w = rect.w * camera.scale
h = rect.h * camera.scale
{ **rect, x: x, y: y, w: w, h: h }
else
{ **rect, x: x, y: y }
end
end
def to_screen_space camera, rect
if rect.is_a? Array
rect.map { |r| to_screen_space camera, r }
else
__to_screen_space__ camera, rect
end
end
def viewport
if Grid.origin_center?
{
x: viewport_offset_x,
y: viewport_offset_y,
w: viewport_w,
h: viewport_h,
anchor_x: 0.5,
anchor_y: 0.5
}
else
{
x: viewport_offset_x,
y: viewport_offset_y,
w: viewport_w,
h: viewport_h,
}
end
end
def viewport_world camera
to_world_space camera, viewport
end
def find_all_intersect_viewport camera, os
Geometry.find_all_intersect_rect viewport_world(camera), os
end
def primitives(camera, *os)
rects = if os.is_a? Hash
[os]
else
os.flatten
end
find_all_intersect_viewport(camera, rects).map do |o|
to_screen_space camera, o
end
end
end
end
class LevelEditor
attr_dr
def initialize args
@args = args
@tilesheet_rect = Layout.rect(row: 5, col: 0, w: 7, h: 7)
@toggle_collision_button = Layout.rect(row: 4, col: 0, w: 7, h: 1)
@tilesheet_button_size = @tilesheet_rect.w.fdiv(20)
@tilesheet_metadata = {}
@mode = :add
init_tilesheet_buttons
end
def tick camera, terrain
if mouse.click && hovered_tile
@selected_tile = hovered_tile.copy
end
if mouse.click && @selected_tile && inside_toggle_collision_button_rect?
tile_id = @selected_tile.id
selected_tilesheet_button.has_collision = !selected_tilesheet_button.has_collision
@selected_tile.has_collision = selected_tilesheet_button.has_collision
end
world_mouse = Camera.to_world_space camera, inputs.mouse.rect
ifloor_x = world_mouse.x.ifloor(@tilesheet_button_size)
ifloor_y = world_mouse.y.ifloor(@tilesheet_button_size)
@mouse_world_rect = { x: ifloor_x,
y: ifloor_y,
w: @tilesheet_button_size,
h: @tilesheet_button_size }
if @selected_tile
@selected_tile.x = @mouse_world_rect.x
@selected_tile.y = @mouse_world_rect.y
end
if !inside_tilesheet_rect? && !inside_toggle_collision_button_rect?
if delete_mode? && (mouse.click || (mouse.held && mouse.moved))
terrain.reject! { |t| Geometry.intersect_rect? t, @mouse_world_rect }
save_terrain terrain
elsif @selected_tile && (mouse.click || (mouse.held && mouse.moved))
if @mode == :add
terrain.reject! { |t| Geometry.intersect_rect? t, @selected_tile }
terrain << @selected_tile.copy
else
terrain.reject! { |t| Geometry.intersect_rect? t, @selected_tile }
end
save_terrain terrain
end
end
end
def selected_tilesheet_button
return nil if !@selected_tile
@tilesheet_buttons.find { |t| t.id == @selected_tile.id }
end
def hovered_tile
return nil if !inside_tilesheet_rect?
Geometry.find_intersect_rect({ click_box: inputs.mouse.rect }, @tilesheet_buttons, using: :click_box)
end
def inside_tilesheet_rect?
Geometry.intersect_rect? mouse.rect, @tilesheet_rect
end
def inside_toggle_collision_button_rect?
Geometry.intersect_rect? mouse.rect, @toggle_collision_button
end
def delete_mode?
@selected_tile&.x_ordinal == 0 && @selected_tile&.y_ordinal == 0
end
def hovered_tile_primitives
return nil if !hovered_tile
{ x: hovered_tile.click_box.x,
y: hovered_tile.click_box.y,
w: hovered_tile.click_box.w,
h: hovered_tile.click_box.h,
path: :pixel,
r: 255, g: 0, b: 0, a: 128 }
end
def delete_instruction_primitives
return nil if !delete_mode?
{ **@toggle_collision_button.center,
text: "Select tile to delete.",
r: 255,
g: 255,
b: 255,
anchor_x: 0.5,
anchor_y: 0.5 }
end
def current_tile_marker_primitives
return nil if !@selected_tile
{ **selected_tilesheet_button.click_box,
path: :solid,
r: 0,
g: 255,
b: 0,
a: 128 }
end
def overlay_primitives
[
{ **@tilesheet_rect, path: :tilesheet },
hovered_tile_primitives,
delete_instruction_primitives,
toggle_collision_button_primitives,
current_tile_marker_primitives,
current_tile_marker_primitives
]
end
def scene_primitives
return nil if !@selected_tile
if delete_mode?
{ **@selected_tile, path: :solid, r: 255, g: 0, b: 0, a: 128 }
else
[
{ **@selected_tile, path: :solid, r: 0, g: 0, b: 0 },
{ **@selected_tile, r: 128, g: 255, b: 255 }
]
end
end
def toggle_collision_button_primitives
return nil if !@selected_tile
return nil if delete_mode?
text = if selected_tilesheet_button.has_collision
"Collidable? Yes (click to toggle)"
else
"Collidable? No (click to toggle)"
end
[
{ **@toggle_collision_button,
path: :solid,
r: 255,
g: 255,
b: 255 },
{ **@toggle_collision_button.center,
text: text,
anchor_x: 0.5,
anchor_y: 0.5,
size_px: 16,
r: 0,
g: 0,
b: 0 }
]
end
def init_tilesheet_buttons
@tilesheet_buttons = []
rows = 20
cols = 20
height = rows * @tilesheet_button_size
width = cols * @tilesheet_button_size
rows.map_with_index do |row|
cols.map_with_index do |col|
x = col * @tilesheet_button_size
y = height - row * @tilesheet_button_size - @tilesheet_button_size
@tilesheet_buttons << {
id: "#{col},#{row}",
x_ordinal: col,
y_ordinal: row,
click_box: {
x: @tilesheet_rect.x + x,
y: @tilesheet_rect.y + y,
w: @tilesheet_button_size,
h: @tilesheet_button_size,
},
x: x,
y: y,
w: @tilesheet_button_size,
h: @tilesheet_button_size,
path: tile_path(row, col, cols),
has_collision: true
}
end
end
outputs[:tilesheet].w = width
outputs[:tilesheet].h = height
outputs[:tilesheet].primitives << { x: 0, y: 0, w: width, h: height, path: :pixel, r: 0, g: 0, b: 0 }
outputs[:tilesheet].primitives << @tilesheet_buttons
end
def mouse
inputs.mouse
end
def tile_path row, col, cols
file_name = (row * cols + col).to_s.rjust(4, "0")
"sprites/1-bit-platformer/#{file_name}.png"
end
def save_terrain terrain
contents = terrain.uniq.map do |terrain_element|
[
terrain_element.x.to_i,
terrain_element.y.to_i,
terrain_element.w.to_i,
terrain_element.h.to_i,
terrain_element.path,
terrain_element.has_collision
].join(",")
end
File.write "data/terrain.txt", contents.join("\n")
end
def load_terrain
contents = File.read("data/terrain.txt")
return [] if !contents
contents.lines.map do |line|
l = line.strip
if l.empty?
nil
else
x, y, w, h, path, has_collision = l.split ","
parsed_collision = if has_collision.nil?
true
else
has_collision == "true"
end
{ x: x.to_f,
y: y.to_f,
w: w.to_f,
h: h.to_f,
path: path,
has_collision: parsed_collision }
end
end.compact.uniq
end
end
DR.reset
require 'app/level_editor.rb'
require 'app/camera.rb'
module Main
attr :level_editor, :player
def start
@level_editor = LevelEditor.new args
@player ||= {
x: 0,
y: 750,
w: 16,
h: 16,
dy: 0,
dx: 0,
facing: 1,
on_ground: false,
path: "sprites/1-bit-platformer/0280.png"
}
@gravity = 0.25
@camera = {
x: 0,
y: 0,
target_x: 0,
target_y: 0,
target_scale: 2,
scale: 1
}
@terrain = @level_editor.load_terrain
end
def tick
calc_player
calc_camera
calc_level_editor
render
end
def calc_player
if inputs.keyboard.left
@player.dx = -3
@player.facing = -1
elsif inputs.keyboard.right
@player.dx = 3
@player.facing = 1
end
if (inputs.keyboard.key_down.space || inputs.key_down.up) && @player.on_ground
@player.dy = 10
@player.on_ground = false
end
@player.x += @player.dx
collision = @terrain.find do |t|
Geometry.intersect_rect?(t, @player) && t.has_collision
end
if collision
if @player.dx > 0
@player.x = collision.x - @player.w
else
@player.x = collision.x + collision.w
end
@player.dx = 0
end
@player.dx *= 0.8
@player.dx = 0 if @player.dx.abs < 0.5
@player.y += @player.dy
@player.on_ground = false
collision = @terrain.find do |t|
t.intersect_rect?(@player) && t.has_collision
end
if collision
if @player.dy > 0
@player.y = collision.y - @player.h
else
@player.y = collision.y + collision.h
@player.on_ground = true
end
@player.dy = 0
end
@player.dy -= @gravity
if (@player.y + @player.h) < -750
@player.y = 750
@player.dy = 0
end
end
def calc_camera
if inputs.keyboard.key_down.equal_sign || inputs.keyboard.key_down.plus
@camera.target_scale += 0.25
elsif inputs.keyboard.key_down.minus
@camera.target_scale -= 0.25
@camera.target_scale = 0.25 if @camera.target_scale < 0.25
elsif inputs.keyboard.zero
@camera.target_scale = 1
end
ease = 0.1
@camera.scale += ((@camera.target_scale + -@player.dy.abs * 0.05 * @camera.scale) - @camera.scale) * ease
@camera.target_x = @player.x + @player.dx * 8 * @camera.scale
@camera.target_y = @player.y + @player.dy * 8 * @camera.scale
@camera.x += (@camera.target_x - @camera.x) * ease
@camera.y += (@camera.target_y - @camera.y) * ease
end
def calc_level_editor
@level_editor.args = args
@level_editor.tick @camera, @terrain
end
def render
outputs.background_color = [0, 0, 0]
outputs[:scene].set w: Camera.viewport_w,
h: Camera.viewport_h,
background_color: [30, 30, 30, 30]
outputs[:scene].primitives << Camera.primitives(@camera,
@terrain,
player_primitives,
@level_editor.scene_primitives)
outputs.primitives << { **Camera.viewport, path: :scene }
outputs.primitives << @level_editor.overlay_primitives
outputs.labels << { x: 640,
y: 30.from_top,
anchor_x: 0.5,
text: "WASD: move around / jump. +/-: Zoom in and out. MOUSE: select tile/edit map (select very TOP LEFT blank tile to DELETE).",
r: 255,
g: 255,
b: 255 }
end
def player_primitives
path = if !@player.on_ground
# falling
"sprites/1-bit-platformer/0284.png"
elsif @player.dx != 0
# running
frame_index = Numeric.frame_index start_at: 0,
frame_count: 3,
hold_for: 5,
repeat: true
"sprites/1-bit-platformer/028#{frame_index + 1}.png"
else
#standing
"sprites/1-bit-platformer/0280.png"
end
{
**@player,
path: path,
flip_horizontally: @player.facing < 0,
r: 200, g: 255, b: 200
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment