衝突した対象と逆方向にベクトルを反転させる【ゲーム開発】
- 作成日: 2024-04-09
- 更新日: 2024-04-09
- カテゴリ: ゲーム開発
連続動作環境のゲーム開発で衝突したオブジェクトと反対方向に自分を移動させたい場合、衝突対象のオブジェクトと自分の座標の差分を取ることで反対方向へのベクトルを得られます。
この記事ではそのコードを解説します。
実行風景
以下がコードを実行したところです。
今回のサンプルではベクトル演算を使って連続動作環境で衝突判定を行い、衝突応答としてベクトルを反転させています。
グローバル変数
グローバル変数は以下になります。
const CW = 600 // キャンバスの横幅
const CH = 500 // キャンバスの高さ
var ct // 2Dコンテキスト
var circles = [] // 円の配列
var collisionResponses = [] // 衝突応答の関数を登録する配列
var circle1, circle2 // 円1、円2
Vector3クラス
ベクトルを表すクラスは以下になります。
// ベクトルクラス
class Vector3 {
constructor (x=0, y=0, z=0) {
this.x = x
this.y = y
this.z = z
}
// ベクトルをクローン(複製)する
clone () {
return new Vector3(this.x, this.y, this.z)
}
// ベクトルにベクトルを加算する
add (other) {
this.x += other.x
this.y += other.y
this.z += other.z
return this
}
// ベクトルにベクトルを減算する
sub (other) {
this.x -= other.x
this.y -= other.y
this.z -= other.z
return this
}
// ベクトルを正規化する
normalize () {
let m = Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z)
let tol = 0.001
if (m <= tol) m = 1
this.x /= m
this.y /= m
this.z /= m
if (Math.abs(this.x) < tol) this.x = 0
if (Math.abs(this.y) < tol) this.y = 0
if (Math.abs(this.z) < tol) this.z = 0
return this
}
}
このVector3
はX座標、Y座標、Z座標を持つベクトルです。
このベクトルは円形の移動処理に使われます。
円形は座標を持っていますが、この座標にベクトルを加算することで座標を更新します。
// ベクトルを正規化する
normalize () {
let m = Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z)
let tol = 0.001
if (m <= tol) m = 1
this.x /= m
this.y /= m
this.z /= m
if (Math.abs(this.x) < tol) this.x = 0
if (Math.abs(this.y) < tol) this.y = 0
if (Math.abs(this.z) < tol) this.z = 0
return this
}
上記のnormalize
メソッドはベクトルを正規化します。
このメソッドはベクトルを次の方程式を満たす単位ベクトルにします。
|v| = √(x^2 + y^2 + z^2) = 1
正規化されたベクトルの長さは1になります。
変数m
はベクトルのマグニチュードでベクトルの大きさを表しています。
変数m
の計算式は
|v| = √(x^2 + y^2 + z^2)
になります。
m
がtol
以下の場合はm
を1
にします。
あとはベクトルの各座標をm
で割るだけです。
Circleクラス
円形のクラスは以下になります。
class Circle {
constructor () {
this.pos = new Vector3() // 座標
this.velocity = new Vector3() // ベクトル
this.radius = 40 // 半径
this.color = '#00ff44' // 色
this.speed = 1 // 速さ
}
update () {
// 画面外への衝突判定
if (this.pos.x < 0) {
this.velocity.x = this.speed
} else if (this.pos.x >= CW) {
this.velocity.x = -this.speed
}
if (this.pos.y < 0) {
this.velocity.y = this.speed
} else if (this.pos.y >= CH) {
this.velocity.y = -this.speed
}
// 他の円との衝突判定
for (const c of circles) {
if (c === this) {
continue
}
// 衝突検出
const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&
Math.abs(this.pos.y - c.pos.y) < this.radius
if (isCollided) {
// 衝突応答を登録
collisionResponses.push(() => {
// 加算したベクトル分を戻す
this.pos.sub(this.velocity)
// 衝突したcと反対方向のベクトルを取得
const p = this.pos.clone().sub(c.pos)
// 正規化
p.normalize()
// ベクトルを更新
this.velocity = p
})
}
}
// 座標にベクトルを加算
this.pos.add(this.velocity)
}
draw () {
// 円を描画
ct.beginPath()
ct.fillStyle = this.color
ct.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI*2, false)
ct.fill()
}
}
円形のクラスの更新処理はupdate
メソッドで行います。
このメソッドではまず画面外にはみ出さないように衝突判定をします。
// 画面外への衝突判定
if (this.pos.x < 0) {
this.velocity.x = this.speed
} else if (this.pos.x >= CW) {
this.velocity.x = -this.speed
}
if (this.pos.y < 0) {
this.velocity.y = this.speed
} else if (this.pos.y >= CH) {
this.velocity.y = -this.speed
}
座標pos
が画面外に出ていたら対応するベクトルを反転させます。
// 他の円との衝突判定
for (const c of circles) {
if (c === this) {
continue
}
// 衝突検出
const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&
Math.abs(this.pos.y - c.pos.y) < this.radius
if (isCollided) {
// 衝突応答を登録
collisionResponses.push(() => {
// 加算したベクトル分を戻す
this.pos.sub(this.velocity)
// 衝突したcと反対方向のベクトルを取得
const p = this.pos.clone().sub(c.pos)
// 正規化
p.normalize()
// ベクトルを更新
this.velocity = p
})
}
}
上記は他の円と衝突判定をしているところです。
circles
には自分も含めた他の円が格納されています。
これと衝突判定をします。
const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&
Math.abs(this.pos.y - c.pos.y) < this.radius
座標の差分の絶対値がthis.radius
(円の半径)より小さければ衝突していると見なします。
if (isCollided) {
// 衝突応答を登録
collisionResponses.push(() => {
// 加算したベクトル分を戻す
this.pos.sub(this.velocity)
// 衝突したcと反対方向のベクトルを取得
const p = this.pos.clone().sub(c.pos)
// 正規化
p.normalize()
// ベクトルを更新
this.velocity = p
})
}
他の円と衝突していたら衝突応答をcollisionResponse
に登録します。
この配列は衝突応答の関数を格納する配列です。
衝突応答ではまず座標からベクトル分を減算します。
// 加算したベクトル分を戻す
this.pos.sub(this.velocity)
こうすることで前回のupdate
で加算されたベクトル分を戻しています。
つまり、衝突対象とめり込まないようにしています。
// 衝突したcと反対方向のベクトルを取得
const p = this.pos.clone().sub(c.pos)
上記では自分の座標と他の円の座標との差分を取ったベクトルp
を取得します。
こうすることで衝突対象との反対方向のベクトルを得ることができます。
// 正規化
p.normalize()
上記ではp
を正規化します。
こうすることでベクトルp
の長さを1にフィックスして、加算しやすい値にしています。
// ベクトルを更新
this.velocity = p
上記ではベクトルを更新します。
main関数
main
関数は以下になります。
function main() {
// キャンバスの初期化
const canvas = document.querySelector('#game-canvas')
canvas.width = CW
canvas.height = CH
// 2Dコンテキストの取得
ct = canvas.getContext('2d')
// 円1
circle1 = new Circle()
circle1.pos.x = CW/2 - 100
circle1.pos.y = CH/2
circle1.velocity.x = 1
circle1.color = '#00ff44'
// 円2
circle2 = new Circle()
circle2.pos.x = CW/2 + 100
circle2.pos.y = CH/2
circle2.color = '#ff4400'
// 円1と円2を登録
circles.push(circle1)
circles.push(circle2)
setInterval(() => {
// 衝突応答をクリア
collisionResponses = []
// 更新
circle1.update()
circle2.update()
// 衝突応答を実行
for (const func of collisionResponses) {
func()
}
// 描画
ct.clearRect(0, 0, CW, CH)
circle1.draw()
circle2.draw()
}, 1000/60)
}
main()
今回のJavaScriptのサンプルコードでは2Dコンテキストを使って絵を出しています。
circle1
がcircle2
に衝突してベクトルを反転させます。
ゲームループはsetInterval()
で行っています。
この関数は第1引数の関数を第2引数のミリ秒間隔で連続して呼び出すものです。
おわりに
なにか参考になれば幸いです。