📐
円同士を重ならない位置まで補正する
円aは重いのであまりずれていない
衝突判定
a = V[300.0, 150.0] # 円aの位置
ar = 125 # 円aの半径
am = 4.0 # 円aの質量
b = V[500.0, 150.0] # 円bの位置
br = 125 # 円bの半径
bm = 1.0 # 円bの質量
ab ベクトルを求める。ここを間違えて a - b
として ba ベクトルとしてしまうと重なりを解消するのではなく余計に近づくことになるので注意。
ab = b - a # => (200.0, 0.0)
その長さから両方の半径を引いた残りが円と円の隙間になる。
gap = ab.length - ar - br # => -50.0
gap.negative? # => true
これが負なら円はめり込んでいる。ただこの判定では浮動小数点の誤差が問題になる(後述)
円を離す
めり込んでいる長さを分配して反発する方向にずらす。ずらす量は相手の質量に比例する。係数は「相手の質量 / 両方足した質量」になる。
ae = bm / (am + bm) # => 0.2
be = am / (am + bm) # => 0.8
ただ a から b の視点で見ているので a は引いて b は押す。つまり a 側だけマイナスにする。
ae = -bm / (am + bm) # => -0.2
be = am / (am + bm) # => 0.8
そこに長さを掛けると実際にずらす量が求まる。
len = -gap # => 50.0
len * ae # => -10.0
len * be # => 40.0
ずらす方向は ab の単位ベクトル
ab.normalize # => (1.0, 0.0)
なので、補正後の青と赤の位置は、
a + ab.normalize * len * ae # => (290.0, 150.0)
b + ab.normalize * len * be # => (540.0, 150.0)
となる。
離しても重なっていると判定される? (重要)
上の計算では意図的に整数になるようにしていたので問題が見えなかった。実際は浮動小数点の誤差の影響でぴったり隣接するように離すのは難しい。離したつもりが 0.00000000000003 ほど重なってしまう。具体的な値で言うと、二つの円の半径が 50 だったとき 100 の距離を取るように離しても実際の距離は 99.99999999999997 となる。こうなると永遠に重なりを解消できない。
(99.99999999999997 - 50 - 50).negative? # => true
その場合、重なっているかの判定時に距離を四捨五入するのが望ましい。
(99.99999999999997 - 50 - 50).round.negative? # => false
計算手順まとめ
a = V[300.0, 150.0]
ar = 125
am = 4.0
b = V[500.0, 150.0]
br = 125
bm = 1.0
ab = b - a # => (200.0, 0.0)
gap = ab.length - ar - br # => -50.0
if gap.round.negative?
len = -gap # => 50.0
abn = ab.normalize # => (1.0, 0.0)
a += abn * len * -bm / (am + bm) # => (290.0, 150.0)
b += abn * len * +am / (am + bm) # => (540.0, 150.0)
end
コード
class App < Base
def initialize
super
a = window_wh * V[3.0/8, 0.5] # => (300.0, 150.0)
b = window_wh * V[5.0/8, 0.5] # => (500.0, 150.0)
# a = window_wh * V[4.0/8, 0.5] # => (300.0, 150.0)
# b = window_wh * V[4.0/8, 0.5] # => (500.0, 150.0)
# a += V.one
@points = [a, b]
@mode = 0
end
def button_down(id)
super
if id == Gosu::KB_Z
@mode = @mode.next.modulo(4)
end
end
def draw
super
a, b = @points
ar = 125
am = 4.0
br = 125
bm = 1.0
ab = b - a
abn = ab.normalize_or_zero
gap = ab.length - ar - br
len = -gap
ae = -bm / (am + bm)
be = +am / (am + bm)
a2 = a + abn * len * ae
b2 = b + abn * len * be
if false
a.replace(a2)
b.replace(b2)
end
if true
arrow_head(a + V.from_angle(270.deg_to_rad), a, "a(#{am.round})")
circle_draw(a, ar)
point_draw(a)
if a2
point_draw(a2, color: :blue_light)
circle_draw(a2, ar, color: :blue_light, line_width: 3)
end
end
if true
arrow_head(b + V.from_angle(270.deg_to_rad), b, "b(#{bm.round})")
circle_draw(b, br)
point_draw(b)
if b2
point_draw(b2, color: :red_light)
circle_draw(b2, br, color: :red_light, line_width: 3)
end
end
vputs "a: #{a.round}"
vputs "ar: #{ar}"
vputs "am: #{am.round}"
vputs
vputs "b: #{b.round}"
vputs "br: #{br}"
vputs "bm: #{bm.round}"
vputs
vputs "ab: #{ab.round(2)}"
vputs "ab間: #{ab.length.round}"
vputs "円の隙間: #{gap.round}"
vputs "ae: #{ae.round(2)}"
vputs "be: #{be.round(2)}"
vputs "aの補正長: %+d" % [len * ae]
vputs "bの補正長: %+d" % [len * be]
vputs "青: #{a2.round}"
vputs "赤: #{b2.round}"
vputs "補正後の隙間: #{((b2 - a2).length - ar - br).round(8)}"
line_draw(a, b, color: :grey)
end
def window_size_default
V[800, 300]
end
def font_size_default
16
end
show
end
参照
Discussion