🎡

新井式回転抽選器シミュレーション

2023/10/27に公開

球 vs 壁

a = wall.p0
b = wall.p1
ab = b - a
ac = ball.location - a
n = ab.normalize.perp
distance = ac.dot(n)
t = distance / ball.radius
if t < 1.0
  ball.location = ball.location + n * (1.0 - t) * ball.radius
  ball.velocity = ball.velocity - ball.velocity.project_onto_normalized(n) * (1 + ball.restitution)
end

https://zenn.dev/megeton/articles/c227b46cc437c8
https://zenn.dev/megeton/articles/754dcb6d2325da

球 vs 球

ab = b.location - a.location
distance = ab.length
gap = distance - a.radius - b.radius
if gap.round.negative?
  len = -gap
  n = ab.normalize
  am = a.mass
  bm = b.mass
  a.location = a.location + n * len * -bm / (am + bm)
  b.location = b.location + n * len *  am / (am + bm)
  as = a.velocity.dot(n)
  bs = b.velocity.dot(n)
  e = 1.0
  as2 = (am*as + bm*bs - (e * bm * (as - bs))) / (am + bm)
  bs2 = (am*as + bm*bs + (e * am * (as - bs))) / (am + bm)
  a.velocity = a.velocity.reject_from_normalized(n) + n * as2
  b.velocity = b.velocity.reject_from_normalized(n) + n * bs2
end

https://zenn.dev/megeton/articles/4446fcbfe8390d
https://zenn.dev/megeton/articles/772701e51cc190
https://zenn.dev/megeton/articles/c346df1a351364

コード
class Ball
  attr_accessor :location
  attr_accessor :velocity
  attr_accessor :radius
  attr_accessor :mass

  def initialize
    @location = $app.window_wh.center + V.rand_norm
    @velocity = V.rand_norm
    @acceleration = V.zero
    @gravity = V[0.0, 0.12]
    @radius = rand(12..24)
    @mass = @radius**2
    @restitution = 0.999
  end

  def update
    $app.rebound(self, $app.pivot)

    $app.walls.each do |wall|
      a = wall.p0
      b = wall.p1
      ab = b - a
      ac = @location - a
      n = ab.normalize.perp
      distance = ac.dot(n)
      t = distance / @radius
      if t < 1.0
        @location = @location + n * (1.0 - t) * @radius
        @velocity = @velocity - @velocity.project_onto_normalized(n) * (1 + @restitution)
      end
    end

    acceleration_add(@gravity * @mass)

    @velocity += @acceleration
    @location += @velocity
    @acceleration = V.zero
  end

  def draw
    $app.circle_draw(@location, @radius, sides: 16)
    $app.line_draw(@location, @location + @velocity.clamp_length_min(@radius))
  end

  def acceleration_add(force)
    @acceleration += force / @mass
  end
end

class Wall
  attr_accessor :p0
  attr_accessor :p1

  def initialize(index)
    @index = index
  end

  def update
    @p0 = position(0)
    @p1 = position(1)
  end

  def draw
    $app.line_draw(p0, p1, line_width: 2)
  end

  def position(i)
    center = $app.window_wh.center
    speed = 60 * 12
    angle = (1.0 / $app.walls.size * (@index + i)) + (1.0 / speed) * $app.frame_count
    center + V.from_angle(angle.turn_to_rad) * center * 0.95
  end
end

class Pivot
  attr_accessor :location
  attr_accessor :velocity
  attr_accessor :radius
  attr_accessor :mass

  def initialize
    @location = $app.window_wh.center
    @velocity = V.zero
    @mass = 1000000000.0
    @radius = 30.0
  end

  def update
    @location += velocity
  end

  def draw
    $app.circle_draw(location, radius, sides: 16, line_width: 3)
  end
end

class App < Base
  attr_accessor :pivot
  attr_accessor :balls
  attr_accessor :walls

  def initialize
    super

    reset
  end

  def reset
    @pivot = Pivot.new
    @walls = 8.times.collect.with_index { |i| Wall.new(i) }
    @balls = 50.times.collect { Ball.new }
  end

  def button_down(id)
    super

    case id
    when Gosu::KB_R
      reset
    end
  end

  def update
    super

    @pivot.update
    @walls.each(&:update)
    @balls.combination(2) { |a, b| rebound(a, b) }
    @balls.each(&:update)
  end

  def draw
    super

    @pivot.draw
    @walls.each(&:draw)
    @balls.each(&:draw)
  end

  def rebound(a, b)
    ab = b.location - a.location
    distance = ab.length
    gap = distance - a.radius - b.radius
    if gap.round.negative?
      len = -gap
      n = ab.normalize

      am = a.mass
      bm = b.mass

      a.location = a.location + n * len * -bm / (am + bm)
      b.location = b.location + n * len *  am / (am + bm)

      as = a.velocity.dot(n)
      bs = b.velocity.dot(n)

      e = 1.0
      as2 = (am*as + bm*bs - (e * bm * (as - bs))) / (am + bm)
      bs2 = (am*as + bm*bs + (e * am * (as - bs))) / (am + bm)

      a.velocity = a.velocity.reject_from_normalized(n) + n * as2
      b.velocity = b.velocity.reject_from_normalized(n) + n * bs2
    end
  end

  def window_size_default
    V[800, 800]
  end

  show
end

Discussion