0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DXRuby:「当たり判定」を自分で作ってみよう B. 円の中心からの距離で判定する

Last updated at Posted at 2020-02-17

概要

この記事は中学高校生向けプログラミング教室の教材として作ったものを一部改変したものです。

今回の記事は、
DXRuby:「当たり判定」を自分で作ってみよう - Qiita

● 当たり(衝突)判定の方法
A. 四角形の四隅の座標で判定する
B. 円の中心からの距離で判定する
C. 色で判定する

の内の
B. 円の中心からの距離で判定する
になります。

atari_en7b.png atari_en8.png

前の記事
DXRuby:「当たり判定」を自分で作ってみよう A. 四角形の四隅の座標で判定する - Qiita

次の記事
DXRuby:「当たり判定」を自分で作ってみよう C. 色で判定する

技術解説

  • 使用ライブラリ
  • 参考サイト
    については、上記記事を参照してください。

ソースコード

ライセンス

ソースコード、本解説ともにパブリックドメイン

プログラム解説

B. 円の中心からの距離で判定する

→ 「ブロック崩し」追加課題 1b);衝突判定の自作(円) - noanoa 日々の日記
http://blog.livedoor.jp/noanoa07/archives/2046462.html

B-1a. ===を使った準備:判定相手が単独(atari_en0.rb)

DXRubyのスプライトでは、===を使うと、衝突の有/無でtrue/fasleを返します。

ball(小さい円)が、block(大きい円)と衝突しているかを===で判定します。
衝突/非衝突 の結果をターミナル(コマンドプロンプト)にtrue/falseで出力しつつ、blockの色も/にします。
また、ウィンドウに衝突時はhit!の文字列を表示します。

===はデフォルトでは四角形で衝突を判定するので、 このままでは円同士が接触する前に衝突と判定されています。

atari_en0.rb
require 'dxruby'

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball  = Sprite.new(300, 400, image0)
block = Sprite.new(200, 130, image1)

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  result = (ball === block)
  p result

  if result
    Window.draw_font(0, 0, "hit!", font)
    block.image = image2
  else
    block.image = image1
  end

  block.draw
end
atari_en0a.png atari_en0b.png atari_en0c.png

B-1b. ===を使った準備:衝突判定範囲を円に設定(atari_en1.rb)

スプライト.collision = [配列]で衝突範囲の設定ができます。配列に3つの要素を指定すると円で判定するようになります。

スプライト.collision = [x, y, r];中心(x, y)、半径rの円

これで、円同士がぎりぎり接した時に衝突と判定されるようになります。

→ DXRubyリファレンス:API INDEX;Sprite#collision=

atari_en1.rb
require 'dxruby'

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball  = Sprite.new(300, 400, image0)
block = Sprite.new(200, 130, image1)

ball.collision  = [ 50,  50,  50]
block.collision = [100, 100, 100]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  result = (ball === block)
  p result

  if result
    Window.draw_font(0, 0, "hit!", font)
    block.image = image2
  else
    block.image = image1
  end

  block.draw
end
atari_en1a.png atari_en1b.png atari_en1c.png

B-1c. atari?を自作:判定相手が単独(atari_en2.rb)

atari?(jibun, aite)を作って、 衝突の有/無でtrue/fasleを返すようにします。

判定方法は、互いの円の中心からの距離を比較します。

  1. それぞれを円と考えて半径を決める(長方形の長辺と短辺の平均を半径とする)
  2. 互いの半径を足した長さを求める
  3. それぞれの中心の座標を求める
  4. 互いの中心からの距離を求める
  5. 中心から中心の距離半径の和を比較する
# radius;半径
# center;中心
rad_x0 = jibun.image.width / 2   # jibunの横幅の 1/2
rad_y0 = jibun.image.height / 2  # jibunの縦幅の 1/2

(rad_x0 + rad_y0) / 2   # jibunの半径(長辺と短辺の平均で考える)

x0     = jibun.x        # jibunの左上隅のx座標
y0     = jibun.y        # jibunの左上隅のy座標

cen_x0 = x0 + rad_x0    # jibunの中心のx座標
cen_y0 = y0 + rad_y0    # jibunの中心のy座標

中心から中心の距離の求め方は、直角三角形のピタゴラスの定理を使います。

距離**2 = 中心間のx座標の差**2 + 中心間のy座標の差**2

(Rubyでは、**2 は 2の意味)

実際は距離の2乗同志で比較していますが、これは平方根sqrtより2乗**2の計算の方が高速なためです。

それでは、自作したatari?を使って、===を使ったコードを書き換えてみます。

atari_en2.rb
require 'dxruby'

def atari?(jibun, aite)
  x0     = jibun.x
  rad_x0 = jibun.image.width / 2
  cen_x0 = x0 + rad_x0

  y0     = jibun.y
  rad_y0 = jibun.image.height / 2
  cen_y0 = y0 + rad_y0

  x1     = aite.x
  rad_x1 = aite.image.width / 2
  cen_x1 = x1 + rad_x1

  y1     = aite.y
  rad_y1 = aite.image.height / 2
  cen_y1 = y1 + rad_y1

  # 半径は長径と短径の1/2に設定
  # 半径同士の和
  cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

  # 2乗で比較する
  cir_dis_2 = cir_dis **2
  dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
  cir_dis_2 >= dist_2
end

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball  = Sprite.new(300, 400, image0)
block = Sprite.new(200, 200, image1)

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  result = atari?(ball, block)
  p result

  if result
    Window.draw_font(0, 0, "hit!", font)
    block.image = image2
  else
    block.image = image1
  end

  block.draw
end
atari_en2a.png atari_en2b.png atari_en2c.png

B-2a. ===を使った準備:判定相手が配列(atari_en3.rb)

DXRubyのスプライトでは、===は、相手がスプライトの配列でも、衝突の有/無でtrue/fasleを返します。

ballが、スプライトの配列blocksと衝突しているかを===で判定します。
blocksのいずれかと衝突していたら、ターミナル(コマンドプロンプト)にtrueで出力し、それ以外はfalseを出力します。
また、ウィンドウに衝突時はhit!の文字列を表示します。

atari_en3.rb
require 'dxruby'

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball   = Sprite.new(300, 400, image0)

block1 = Sprite.new(110, 130, image1)
block2 = Sprite.new(390, 130, image1)

ball.collision  = [ 50,  50,  50]
block1.collision = [100, 100, 100]
block2.collision = [100, 100, 100]

blocks = [block1, block2]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  result = (ball === blocks)
  p result

  if result
    Window.draw_font(0, 0, "hit!", font)
  end

  Sprite.draw(blocks)
end
atari_en3a.png atari_en3b.png atari_en3c.png atari_en3d.png

B-2b. atari_array?を自作:判定相手が配列(atari_en4.rb)

===の代わりに、atari_array?(jibun, array)を自作します。
判定相手の配列の要素を一つずつatari?で判定していきます。

自作したatari_array?を使って、コードを書き換えます。

atari_en4.rb
require 'dxruby'

def atari?(jibun, aite)
  x0     = jibun.x
  rad_x0 = jibun.image.width / 2
  cen_x0 = x0 + rad_x0

  y0     = jibun.y
  rad_y0 = jibun.image.height / 2
  cen_y0 = y0 + rad_y0

  x1     = aite.x
  rad_x1 = aite.image.width / 2
  cen_x1 = x1 + rad_x1

  y1     = aite.y
  rad_y1 = aite.image.height / 2
  cen_y1 = y1 + rad_y1

  # 半径は長径と短径の1/2に設定
  # 半径同士の和
  cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

  # 2乗で比較する
  cir_dis_2 = cir_dis **2
  dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
  cir_dis_2 >= dist_2
end

def atari_array?(jibun, array)
  array.each do |a|
    if atari?(jibun, a)
      return true
    end
  end
  false
end

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball   = Sprite.new(300, 400, image0)

block1 = Sprite.new(110, 130, image1)
block2 = Sprite.new(390, 130, image1)

blocks = [block1, block2]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  result = atari_array?(ball, blocks)
  p result

  if result
    Window.draw_font(0, 0, "hit!", font)
  end

  Sprite.draw(blocks)
end
atari_en4a.png atari_en4b.png atari_en4c.png atari_en4d.png

B-3a. checkを使った準備(atari_en5.rb)

DXRubyの===や、自作したatari_array?では、どの相手に衝突したかは分かりませんでした。

一方、DXRubyのスプライトではcheckを使うと、衝突している相手すべてを配列に入れて返します。(衝突していない時は、何も入ってない配列を返す)

この配列をarrayとすると、0番目のarray.firstarray[0]でも同じ)が最初に衝突した相手です。また、array.firstに何か入っていれば衝突している、空(nil)ならば衝突していないという、当たり判定にも使えます。

→ DXRubyリファレンス:Spriteを使うためのチュートリアル;衝突したオブジェクトを取得する
http://mirichi.github.io/dxruby-doc/tutorial/sprite.html

→ DXRubyリファレンス:API INDEX;Sprite;check
http://mirichi.github.io/dxruby-doc/api/Sprite_23check.html

プログラムでは、ballblocksのどれに最初に衝突しているかを check.firstで判定します。
最初に衝突しているブロックをターミナル(コマンドプロンプト)に出力し、そのブロックを赤色にします。それ以外はnilを出力します。
また、ウィンドウに衝突時はhit!の文字列を表示します。

atari_en5.rb
require 'dxruby'

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball   = Sprite.new(300, 400, image0)

block1 = Sprite.new(110, 130, image1)
block2 = Sprite.new(390, 130, image1)

ball.collision  = [ 50,  50,  50]
block1.collision = [100, 100, 100]
block2.collision = [100, 100, 100]

blocks = [block1, block2]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  col = ball.check(blocks).first
  p col

  if col
    Window.draw_font(0, 0, "hit!", font)
    col.image = image2
    Sprite.draw(blocks)
    col.image = image1
  else
    Sprite.draw(blocks)
  end
end
atari_en5a.png atari_en5b.png atari_en5c.png atari_en5d.png

B-3b. atari_arrayを自作(atari_en6.rb)

ぶつかった相手は最初のものだけ分かればよいので、DXRubyのcheck.first相当のatari_arrayを自作します。

つまり、atari_arrayは、衝突していないとnilを返し、衝突すると "ぶつかった最初のもの" を返します。

方法は、判定相手の配列の要素を一つずつatari?で判定していき、最初に衝突している要素を返します。

自作したatari_arrayを使って、コードを書き換えます。

atari_en6.rb
require 'dxruby'

def atari?(jibun, aite)
  x0     = jibun.x
  rad_x0 = jibun.image.width / 2
  cen_x0 = x0 + rad_x0

  y0     = jibun.y
  rad_y0 = jibun.image.height / 2
  cen_y0 = y0 + rad_y0

  x1     = aite.x
  rad_x1 = aite.image.width / 2
  cen_x1 = x1 + rad_x1

  y1     = aite.y
  rad_y1 = aite.image.height / 2
  cen_y1 = y1 + rad_y1

  # 半径は長径と短径の1/2に設定
  # 半径同士の和
  cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

  # 2乗で比較する
  cir_dis_2 = cir_dis **2
  dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
  cir_dis_2 >= dist_2
end

def atari_array(jibun, array)
  array.each do |a|
    if atari?(jibun, a)
      return a
    end
  end
  nil
end

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball   = Sprite.new(300, 400, image0)

block1 = Sprite.new(110, 130, image1)
block2 = Sprite.new(390, 130, image1)

blocks = [block1, block2]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  col = atari_array(ball, blocks)
  p col

  if col
    Window.draw_font(0, 0, "hit!", font)
    col.image = image2
    Sprite.draw(blocks)
    col.image = image1
  else
    Sprite.draw(blocks)
  end
end
atari_en6a.png atari_en6b.png atari_en6c.png atari_en6d.png

B-4. 自作の当たり判定(円)をSpriteクラスのメソッドにする(atari_en7.rb)

元の===checkは、Spriteクラスのメソッド(命令)なので、書き方としては、ball.check(blocks)のようになります。

自作のatari_arrayも同じような書き方になるように、Spriteクラスのメソッドにしてみます。

クラスメソッドについての説明はここでは説明を省きますので、詳しくはRubyのテキストを見てください。

クラスメソッドを追加する方法は簡単で、以下のようにします。

class Sprite
  def 追加したいメソッド
    #追加したいメソッドの内容
  end
end

Spriteクラスにatari?atari_array追加します。

ちなみに、jibun(自分)に相当するものは、selfと書きます。

class Sprite
  def atari?(aite)
    x0     = self.x
    rad_x0 = self.image.width / 2
    cen_x0 = x0 + rad_x0

    y0     = self.y
    rad_y0 = self.image.height / 2
    cen_y0 = y0 + rad_y0

    x1     = aite.x
    rad_x1 = aite.image.width / 2
    cen_x1 = x1 + rad_x1

    y1     = aite.y
    rad_y1 = aite.image.height / 2
    cen_y1 = y1 + rad_y1

    # 半径は長径と短径の1/2に設定
    # 半径同士の和
    cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

    # 2乗で比較する
    cir_dis_2 = cir_dis **2
    dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
    cir_dis_2 >= dist_2
  end

  def atari_array(array)
    array.each do |a|
      if self.atari?(a)
        return a
      end
    end
    nil
  end
end

これで、クラスメソッドになったので、今までのatari_array(ball, blocks)という書き方ではなく、ball.atari_array(blocks)というように書くことができるようになりました。

atari_en7.rb
require 'dxruby'

class Sprite
  def atari?(aite)
    x0     = self.x
    rad_x0 = self.image.width / 2
    cen_x0 = x0 + rad_x0

    y0     = self.y
    rad_y0 = self.image.height / 2
    cen_y0 = y0 + rad_y0

    x1     = aite.x
    rad_x1 = aite.image.width / 2
    cen_x1 = x1 + rad_x1

    y1     = aite.y
    rad_y1 = aite.image.height / 2
    cen_y1 = y1 + rad_y1

    # 半径は長径と短径の1/2に設定
    # 半径同士の和
    cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

    # 2乗で比較する
    cir_dis_2 = cir_dis **2
    dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
    cir_dis_2 >= dist_2
  end

  def atari_array(array)
    array.each do |a|
      if self.atari?(a)
        return a
      end
    end
    nil
  end
end

image0 = Image.new(100, 100).circle_fill( 50,  50,  50, C_WHITE)
image1 = Image.new(200, 200).circle_fill(100, 100, 100, C_WHITE)
image2 = Image.new(200, 200).circle_fill(100, 100, 100, C_RED)

ball   = Sprite.new(300, 400, image0)

block1 = Sprite.new(110, 130, image1)
block2 = Sprite.new(390, 130, image1)

blocks = [block1, block2]

font = Font.new(32)

Window.loop do
  ball.x = Input.mouse_pos_x
  ball.y = Input.mouse_pos_y

  ball.draw

  col = ball.atari_array(blocks)
  p col

  if col
    Window.draw_font(0, 0, "hit!", font)
    col.image = image2
    Sprite.draw(blocks)
    col.image = image1
  else
    Sprite.draw(blocks)
  end
end
atari_en7a.png atari_en7b.png atari_en7c.png atari_en7d.png

B-5. 自作の当たり判定(円)を使ったブロック崩し(atari_en8.rb)

「ブロック崩し」のプログラムblock28.rbのブロックをに変えたものを、自作のatari_arrayに書き換えてみましょう。

円にしたブロックボールの判定だけですが、元のcheck.firstを同じ動きをしているのがわかると思います。

atari_en8.rb
require 'dxruby'

class Sprite
  def atari?(aite)
    x0     = self.x
    rad_x0 = self.image.width / 2
    cen_x0 = x0 + rad_x0

    y0     = self.y
    rad_y0 = self.image.height / 2
    cen_y0 = y0 + rad_y0

    x1     = aite.x
    rad_x1 = aite.image.width / 2
    cen_x1 = x1 + rad_x1

    y1     = aite.y
    rad_y1 = aite.image.height / 2
    cen_y1 = y1 + rad_y1

    # 半径は長径と短径の1/2に設定
    # 半径同士の和
    cir_dis = (rad_x0 + rad_y0) / 2 + (rad_x1 + rad_y1) / 2

    # 2乗で比較する
    cir_dis_2 = cir_dis **2
    dist_2 = (cen_x0 - cen_x1) ** 2 + (cen_y0 - cen_y1) ** 2
    cir_dis_2 >= dist_2
  end

  def atari_array(array)
    array.each do |a|
      if self.atari?(a)
        return a
      end
    end
    nil
  end
end

img_bar   = Image.new(100,  20, C_WHITE)
img_hwall = Image.new( 20, 480, C_BLUE)
img_vwall = Image.new(640,  20, C_BLUE)
img_ball  = Image.new( 20,  20).circle_fill(10, 10, 10, C_RED)
img_block = Image.new(28, 28).circle_fill(14, 14, 14, C_GREEN)   # ブロックを円に
img_block_y = Image.new(28, 28).circle_fill(14, 14, 14, C_YELLOW)   # ブロックを円に

bar   = Sprite.new(  0, 460, img_bar)
lwall = Sprite.new(  0,   0, img_hwall)
rwall = Sprite.new(620,   0, img_hwall)
twall = Sprite.new(  0,   0, img_vwall)

walls = [bar, lwall, rwall, twall]

ball  =  Sprite.new(300, 400, img_ball)
dx =  2
dy = -2

def move(sprite, speed_x, speed_y)
  sprite.x += speed_x
  sprite.y += speed_y
end

blocks = []
20.times do |x|       # ブロックの組み方を少し変更
  5.times do |y|
    blocks << Sprite.new(21 + 30 * x, 21 + 30 * y, img_block)
  end
end


Window.loop do
  bar.x = Input.mouse_pos_x
  Sprite.draw(walls)

  move(ball, dx, 0)
  if ball === walls
    ball.x -= dx
    dx = -dx
  end
  coll_x = ball.atari_array(blocks)
  if coll_x
    coll_x.image = img_block_y
    coll_x.draw     #一瞬色が変わって表示
    coll_x.vanish   #消える
    ball.x -= dx
    dx = -dx
  end

  move(ball, 0, dy)
  if ball=== walls
    ball. y -= dy
    dy = -dy
  end
  coll_y = ball.atari_array(blocks)
  if coll_y
    coll_y.image = img_block_y
    coll_y.draw     #一瞬色が変わって表示
    coll_y.vanish   #消える
    ball. y -= dy
    dy = -dy
  end

  ball.draw

  Sprite.draw(blocks)
end
atari_en8.png

参考

プログラミング初心者向け:DXRubyで 1ステップずつ作っていく「ブロック崩し」 - Qiita
で作った「ブロック崩し」のプログラムです。

block28.rb
require 'dxruby'

img_bar   = Image.new(100,  20, C_WHITE)
img_hwall = Image.new( 20, 480, C_BLUE)
img_vwall = Image.new(640,  20, C_BLUE)
img_ball  = Image.new( 20,  20).circle_fill(10, 10, 10, C_RED)
img_block = Image.new( 58,  18, C_GREEN)
img_block_y = Image.new( 58,  18, C_YELLOW)

bar   = Sprite.new(  0, 460, img_bar)
lwall = Sprite.new(  0,   0, img_hwall)
rwall = Sprite.new(620,   0, img_hwall)
twall = Sprite.new(  0,   0, img_vwall)

walls = [bar, lwall, rwall, twall]

ball  =  Sprite.new(300, 400, img_ball)
dx =  2
dy = -2

def move(sprite, speed_x, speed_y)
  sprite.x += speed_x
  sprite.y += speed_y
end

blocks = []
10.times do |x|
  5.times do |y|
    blocks << Sprite.new(21 + 60 * x, 21 + 20 * y, img_block)
  end
end


Window.loop do
  bar.x = Input.mouse_pos_x
  Sprite.draw(walls)

  move(ball, dx, 0)
  if ball === walls
    ball.x -= dx
    dx = -dx
  end
  coll_x = ball.check(blocks)
  if coll_x[0]
    coll_x[0].image = img_block_y
    coll_x[0].draw     #一瞬色が変わって表示
    coll_x[0].vanish   #消える
    ball.x -= dx
    dx = -dx
  end

  move(ball, 0, dy)
  if ball === walls
    ball. y -= dy
    dy = -dy
  end
  coll_y = ball.check(blocks)
  if coll_y[0]
    coll_y[0].image = img_block_y
    coll_y[0].draw     #一瞬色が変わって表示
    coll_y[0].vanish   #消える
    ball. y -= dy
    dy = -dy
  end

  ball.draw

  Sprite.draw(blocks)
end
block28.png
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?