Skip to content

Instantly share code, notes, and snippets.

@marcheiligers
Last active April 21, 2023 19:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcheiligers/1655e7ca45c4a575a4465c0725677506 to your computer and use it in GitHub Desktop.
Save marcheiligers/1655e7ca45c4a575a4465c0725677506 to your computer and use it in GitHub Desktop.
# Stolen from Xenobrain and remixed (https://gist.github.com/xenobrain/5fa64f8a3e8f8f6f1b31eee4f870dd75)
TIME_STEP = 1 / 60 # delta time isn't required in DragonRuby but it really handy for tuning and debugging physics
COLLISION_BIAS = 0.05 # adds some energy into the collision to get objects to separate. tune this in steps of 0.01
COLLISION_SLOP = 0.1 # amount shapes are allowed to overlap without triggering correction. helps avoid position jitter
COLLISION_ITERATIONS = 10 # how many times to run the solver. a good range is between 5 and 15
CIRCLE_RADIUS = 10
CIRCLE_RADIUS_2 = CIRCLE_RADIUS * 2
CIRCLE_RADIUS_SQ = CIRCLE_RADIUS_2**2
CIRCLE_MASS = 10_000 # Math::PI * CIRCLE_RADIUS * CIRCLE_RADIUS
CIRCLE_ROTATIONAL_INERTIA = 0.5 * CIRCLE_RADIUS * CIRCLE_RADIUS * CIRCLE_MASS
MAX_V_COMPONENT = 3
MAX_ANGULAR_V = 10
MAX_V_SQ = 10_000
CIRCLE_COUNT = 0
ITERS = 10 # how many iterations to run on each frame
GRAVITY = 100 * TIME_STEP
LINE_BOUNCE = 0.6
def tick(args)
init(args) if args.state.tick_count.zero?
inputs(args)
i = -1
while (i += 1) < ITERS
move(args)
collide(args)
end
draw(args)
end
def make_circle(args, x = nil, y = nil)
{
x: x || rand(args.grid.w - CIRCLE_RADIUS_2) + CIRCLE_RADIUS,
y: y || rand(args.grid.h - CIRCLE_RADIUS_2) + CIRCLE_RADIUS,
w: CIRCLE_RADIUS_2,
h: CIRCLE_RADIUS_2,
angle: 0,
path: 'sprites/circle/violet.png',
radius: CIRCLE_RADIUS,
vx: rand(MAX_V_COMPONENT * 2) - MAX_V_COMPONENT, # velocity x
vy: rand(MAX_V_COMPONENT * 2) - MAX_V_COMPONENT, # velocity y
av: rand(MAX_ANGULAR_V), # angular velocity
bounce: 0.95, # 0..1
friction: 0.2,
mass: CIRCLE_MASS,
rotational_inertia: CIRCLE_ROTATIONAL_INERTIA
}.sprite!
end
def init(args)
args.state.gravity = true
# Create a pile of circles
args.state.circles = Array.new(CIRCLE_COUNT) { make_circle(args) }
# Create some borders, and a couple of extra lines for funsies
args.state.lines ||= [
{ x: 0, y: 20, x2: 1280, y2: 20, vx: 0, vy: 0, av: 0, bounce: 1, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!,
{ x: 0, y: 0, x2: 0, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!,
{ x: 1280, y: 0, x2: 1280, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY }.line!,
{ x: 0, y: 720, x2: 1280, y2: 720, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line!,
# { x: 640, y: 360, x2: 700, y2: 250, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line!,
# { x: 200, y: 400, x2: 360, y2: 250, vx: 0, vy: 0, av: 0, bounce: LINE_BOUNCE, friction: 0.4, mass: Float::INFINITY, rotational_inertia: Float::INFINITY}.line!
]
end
def inputs(args)
args.state.gravity = !args.state.gravity if args.inputs.keyboard.key_down.g
args.state.circles << make_circle(args, args.inputs.mouse.x, args.inputs.mouse.y) if args.inputs.keyboard.key_down.c
args.state.circles.shift if args.inputs.keyboard.key_down.d
if args.inputs.keyboard.key_down.l
closest_line = find_closest_line(args.inputs.mouse, args.state.lines)
putz closest_line
args.state.lines.delete(closest_line.second) if !closest_line.nil? && closest_line.first < 15 && !args.state.lines.first(4).include?(closest_line.second)
end
$gtk.reset if args.inputs.keyboard.key_down.r
if args.state.new_line
args.state.new_line.x2 = args.inputs.mouse.x
args.state.new_line.y2 = args.inputs.mouse.y
args.state.new_line = nil if args.inputs.mouse.up
elsif args.state.new_line.nil? && args.inputs.mouse.down
args.state.new_line = {
x: args.inputs.mouse.x,
y: args.inputs.mouse.y,
x2: args.inputs.mouse.x,
y2: args.inputs.mouse.y,
vx: 0,
vy: 0,
av: 0,
bounce: LINE_BOUNCE,
friction: 0.4,
mass: Float::INFINITY,
rotational_inertia: Float::INFINITY
}.line!
args.state.lines << args.state.new_line
end
end
def draw(args)
args.outputs.primitives << args.state.circles << args.state.lines
end
def collide(args)
ls = args.state.lines
kl = ls.length
cs = args.state.circles
cl = cs.length
i = -1
while (i += 1) < cl
c1 = cs[i]
j = i
while (j += 1) < cl
c2 = cs[j]
next if c1.mass == Float::INFINITY && c2.mass == Float::INFINITY
collision = find_circle_circle c1, c2
calc_collision collision
end
k = -1
while (k += 1) < kl
l1 = ls[k]
collision = find_circle_line c1, l1
calc_collision collision
end
end
end
def move(args)
cs = args.state.circles
i = -1
l = cs.length
while (i += 1) < l
c = cs[i]
if c.mass != Float::INFINITY
c.vy -= GRAVITY / ITERS if args.state.gravity
c.x += c.vx / ITERS
c.y += c.vy / ITERS
c.angle += (c.av * TIME_STEP).to_degrees / ITERS
end
end
line = args.state.lines.first
line.y += line.vy / ITERS
line.y2 += line.vy / ITERS
line.vy = (args.state.tick_count % 360 * 40).sin
line.bounce = 1.2 + line.vy / 2
# putz line.bounce
end
def find_closest_line(point, lines)
lines.map { |line| [distance_from_point_to_line(point, line), line] }.sort_by(&:first).first
end
def distance_from_point_to_line(point, line)
a = line.y2 - line.y
b = line.x - line.x2
c = line.y * line.x2 - line.x * line.y2
(a * point.x + b * point.y + c).abs / Math.sqrt(a**2 + b**2)
end
def find_circle_circle a, b
circle_ar = a.radius || [a.w, a.h].max * 0.5
circle_br = b.radius || [b.w, b.h].max * 0.5
circle_ax = a.x + circle_ar
circle_ay = a.y + circle_ar
circle_bx = b.x + circle_br
circle_by = b.y + circle_br
dx = circle_bx - circle_ax
dy = circle_by - circle_ay
distance = dx * dx + dy * dy
min_distance = circle_ar + circle_br
# distance should be less than the sum of radii
# zero distance means the circles have the same centers, do nothing
return if (distance > min_distance * min_distance) || distance.zero?
distance = Math.sqrt distance
dx /= distance
dy /= distance
contact = { r1x: circle_ax + circle_ar * dx,
r1y: circle_ay + circle_ar * dy,
r2x: circle_bx - circle_br * dx,
r2y: circle_by - circle_br * dy,
depth: distance - min_distance,
jn: 0, jt: 0 }
{ a: a, ax: circle_ax, ay: circle_ay,
b: b, bx: circle_bx, by: circle_by,
normal_x: dx, normal_y: dy,
contacts: [contact] }
end
def find_circle_line c, l
circle_r = c.radius || [c.w, c.h].max * 0.5
line_r = l.radius || 0
circle_x = c.x + circle_r
circle_y = c.y + circle_r
line_x = l.x2 - l.x
line_y = l.y2 - l.y
line_len_sq = [line_x * line_x + line_y * line_y, 1].max
t = ((line_x * (circle_x - l.x) + line_y * (circle_y - l.y)) / line_len_sq).clamp(0, 1)
closest_x = l.x + line_x * t
closest_y = l.y + line_y * t
dx = closest_x - circle_x
dy = closest_y - circle_y
distance = dx * dx + dy * dy
min_distance = circle_r + line_r
return if distance > min_distance * min_distance
distance = Math.sqrt distance
dx /= distance
dy /= distance
contact = { r1x: circle_x + circle_r * dx,
r1y: circle_y + circle_r * dy,
r2x: closest_x - line_r * dx,
r2y: closest_y - line_r * dy,
depth: distance - min_distance,
jn: 0, jt: 0 }
{ a: c, ax: circle_x, ay: circle_y,
b: l, bx: line_x, by: line_y,
normal_x: dx, normal_y: dy,
contacts: [contact] }
end
def calc_collision collision
return unless collision
a = collision[:a]
b = collision[:b]
nx = collision[:normal_x]
ny = collision[:normal_y]
average_bounce = a.bounce * b.bounce
average_friction = a.friction * b.friction
inv_m_a = 1.0 / a.mass
inv_m_b = 1.0 / b.mass
inv_i_a = 1.0 / a.rotational_inertia
inv_i_b = 1.0 / b.rotational_inertia
inv_mass_sum = inv_m_a + inv_m_b
fn.each collision.contacts do |contact|
# contact point in local space
r1x = contact[:r1x] - collision[:ax]
r1y = contact[:r1y] - collision[:ay]
r2x = contact[:r2x] - collision[:bx]
r2y = contact[:r2y] - collision[:by]
# contact point cross normal, tangent
r1cn = r1x * ny - r1y * nx
r2cn = r2x * ny - r2y * nx
r1ct = r1x * nx + r1y * ny
r2ct = r2x * nx + r2y * ny
# sum of masses in normal and tangent directions
mass_normal = 1.0 / (inv_mass_sum + inv_i_a * r1cn * r1cn + inv_i_b * r2cn * r2cn)
mass_tangent = 1.0 / (inv_mass_sum + inv_i_a * r1ct * r1ct + inv_i_b * r2ct * r2ct)
# penetration correction -- feed positional error into separation impulse (Baumgarte stabilization)
bias = COLLISION_BIAS * [0.0, contact[:depth] + COLLISION_SLOP].min / TIME_STEP
# relative velocity
rvx = b.vx - r2y * b.av - (a.vx - r1y * a.av)
rvy = b.vy + r2x * b.av - (a.vy + r1x * a.av)
# relative velocity along normal * average bounce
bounce = (rvx * nx + rvy * ny) * average_bounce
COLLISION_ITERATIONS.times do
# update the relative velocity
vrx = b.vx - r2y * b.av - (a.vx - r1y * a.av)
vry = b.vy + r2x * b.av - (a.vy + r1x * a.av)
# relative velocity along normal and tangent
rvn = vrx * nx + vry * ny
rvt = vrx * -ny + vry * nx
# impulse scalar (aka lambda, lagrange multiplier)
jn = -(bounce + rvn + bias) * mass_normal
previous_jn = contact[:jn]
contact[:jn] = [previous_jn + jn, 0.0].max
# tangent scalar, cannot exceed force along normal (Coulomb's law)
max_jt = average_friction * contact[:jn]
jt = -rvt * mass_tangent
previous_jt = contact[:jt]
contact[:jt] = (previous_jt + jt).clamp(-max_jt, max_jt)
jn = contact[:jn] - previous_jn
jt = contact[:jt] - previous_jt
impulse_x = nx * jn - ny * jt
impulse_y = nx * jt + ny * jn
a[:vx] -= impulse_x * inv_m_a
a[:vy] -= impulse_y * inv_m_a
a[:av] -= inv_i_a * (r1x * impulse_y - r1y * impulse_x)
clamp_v(a)
b[:vx] += impulse_x * inv_m_b
b[:vy] += impulse_y * inv_m_b
b[:av] += inv_i_b * (r2x * impulse_y - r2y * impulse_x)
clamp_v(b)
end
end
end
def clamp_v(a)
v = a.vx**2 + a.vy**2
return if v < MAX_V_SQ
f = MAX_V_SQ / v
a.vx *= f
a.vy *= f
end
$gtk.reset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment