はじめに
Pixi.jsを使って円同士の衝突判定を実装する機会があったので、初めてPixi.jsを使用する方に出来るだけ分かりやすくまとめていきます。
① PIXI.Application の作成
まずは、PIXI.Applicationを作成します。
PIXI.Applicationとは土台のようなもので、ここに色々なオブジェクトを追加することで、画像を表示させたり、図形を描画したりできます。
PIXI.Applicationには色々なオプションを設定することができますが、今回はサイズ、背景色を設定します。
Canvasの縦幅、横幅は画面いっぱいにして、背景色は白色にします。
Pixi.jsでの色の指定は全て16進数で行うので、白色を設定する場合には0xffffffと記述します。
import * as PIXI from 'pixi.js'
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0xffffff
});
// bodyにApplication追加
document.body.appendChild(app.view);
② 衝突させる円を作成する
次に、衝突させる円を作成していきます。
円を作成するにはPIXI.Graphicsの drawCircle を使用し、色指定にはbeginFillを使用して ff00ff に塗りつぶします。
// 円の半径
const radius = 50
const circle1 = new PIXI.Graphics()
.beginFill(0xff00ff)
.drawCircle(0, 0, radius)
.endFill()
次に、作成した円をPIXI.Spriteに追加します。
PIXI.Spriteは基本的に画像を表示させるために使用しますが、今回はGraphicを追加します。
PIXI.Graphicsで座標の情報を保持してしまうと、基準点が左上になってしまうので Drag & Dropの際にズレが生じてしまいます。
そのため、描画する座標の情報はPIXI.Spriteで保持し、基準点は中心に設定します。
circleSprite1.anchor.set(0.5)
circleSprite1.position.set(200, 200)
また、物体の衝突判定の際に使用するため、作成したSpriteの情報を配列に格納します。
最後にapp.stageに作成したSpriteを追加して表示させます。
以下、ここまでのコードの全容です。
// 円の情報をもつ配列
let circles = []
// 円の半径
const radius = 50
// 衝突させる円1
const circleSprite1 = new PIXI.Sprite()
const circle1 = new PIXI.Graphics()
.beginFill(0xff00ff)
.drawCircle(0, 0, radius)
.endFill()
circleSprite1.addChild(circle1)
circleSprite1.anchor.set(0.5)
circleSprite1.position.set(200, 200)
circles.push(circleSprite1)
// 衝突させる円2
const circleSprite2 = new PIXI.Sprite()
const circle2 = new PIXI.Graphics()
.beginFill(0xff00ff)
.drawCircle(0, 0, radius)
.endFill()
circleSprite2.addChild(circle2)
circleSprite2.anchor.set(0.5)
circleSprite2.position.set(400,400)
circles.push(circleSprite2)
app.stage.addChild(circleSprite1)
app.stage.addChild(circleSprite2)
③ drag and dropで操作する円を作成する
次にdrag and dropで操作する円を作成します。
①と同じ要領でPIXI.Graphicsで円を描画し、Spriteに追加します。
// dragさせる円の半径
const dragCircleRadius = 50
// dragさせる円
const dragCircle = new PIXI.Graphics()
.beginFill(0x0000ff)
.drawCircle(0, 0, dragCircleRadius)
.endFill()
const dragCircleSprite = new PIXI.Sprite()
dragCircleSprite.addChild(dragCircle)
Spriteのpointer up や pointer moveイベントを有効化するために、interactive を true に設定します。
わかりやすいように buttonMode もtrueにして、カーソルがpointerになるようにします。
dragCircleSprite.interactive = true
dragCircleSprite.buttonMode = true
次に作成したSpriteにイベントを登録させます。
第一引数にイベントの種類、第二引数に実行する関数を指定します。
dragCircleSprite.on('pointerdown', onPointerDown)
dragCircleSprite.on('pointerup', onPointerUp)
今ドラッグしているか否かの状態は isDragging で保持します。
// ドラッグ状態を管理
let isDragging = false
// pointer down時のevent
function onPointerDown(){
isDragging = true
dragCircleSprite.on('pointermove', onPointerMove)
}
// pointer up時のevent
function onPointerUp() {
isDragging = false
dragCircleSprite.removeListener('pointermove')
}
pointer moveイベントではドラッグ状態がtrueの場合のみ、Spriteの座標を現在のマウスの位置に合わせます。
引数からInteraction Eventを参照できるので、座標の情報はそこから取得します。
// pointer move時のevent
function onPointerMove(e) {
if(isDragging) {
const target = e.currentTarget
const position = e.data.getLocalPosition(app.stage)
target.position.set(position.x, position.y)
}
}
以下ここまでのコードの全容です。
公式Exampleの drag and drop も参考にしてみてください。
// ドラッグ状態を管理
let isDragging = false
// dragさせる円の半径
const dragCircleRadius = 50
// dragさせる円
const dragCircle = new PIXI.Graphics()
.beginFill(0x0000ff)
.drawCircle(0, 0, dragCircleRadius)
.endFill()
const dragCircleSprite = new PIXI.Sprite()
dragCircleSprite.addChild(dragCircle)
// interactiveをtrueにしないとイベントが発火しない
dragCircleSprite.interactive = true
dragCircleSprite.buttonMode = true
// 初期のポジション
dragCircleSprite.position.set(200,400)
// アンカーポイントを真ん中に設定
dragCircleSprite.anchor.set(0.5)
// pointerイベントの登録
dragCircleSprite.on('pointerdown', onPointerDown)
dragCircleSprite.on('pointerup', onPointerUp)
// pointer down時のevent
function onPointerDown(){
isDragging = true
dragCircleSprite.on('mousemove', onPointerMove)
}
// pointer up時のevent
function onPointerUp() {
isDragging = false
dragCircleSprite.removeListener('mousemove')
}
// pointer move時のevent
function onPointerMove(e) {
if(isDragging) {
const target = e.currentTarget
const position = e.data.getLocalPosition(app.stage)
target.position.set(position.x, position.y)
}
}
app.stage.addChild(dragCircleSprite)
③ 円同士の衝突を判定するロジック
では、本題の円同士の衝突判定です。
まず、当たり判定となる基準ですが、ドラッグしている円が既に描画されている円に衝突した時(重なった時)に当たり判定とします。
これを実現するために、2つの円それぞれの中心からの距離と半径の和を比べて、半径の和 > 2つの円の距離となれば当たり、と判定します。
2つの円の距離は三平方の定理を使って求めます。
直角三角形では、斜辺cの2乗は,他の辺a,bをそれぞれ2乗した数の和に等しい、というやつです。
この定理をどのように使用するかと言いますと、、、
図のように、2つの円の中心を結んだものを斜辺cとし直角三角形を作ると、それぞれx座標同士、y座標同士の距離が2辺 a, bとなり、
直角三角形では,2つの辺の長さがわかると,三平方の定理を使って他の1辺の長さが計算できるので、中心座標の数字がわかればcの長さを計算することが可能になります。
Math.hypot()
関数は、各引数の二乗の合計値の平方根を返しますので、これを使用すると簡単に上記の式が実現できそうです。
コードに落とし込むと、、、
const a = x1 - x2
const b = y1 - y2
const c = Math.hypot(a, b)
if(c < radius + dragCircleRadius) {
// 当たり
return
} else {
// 当たってない
}
④ pointer moveの時に衝突判定
上記のロジックをpointer move時のイベント関数内に実装します。
衝突判定させる円が複数個あるため、円をまとめた配列をループして判定するようにします。
衝突した時にSpriteの透明度を下げるようにします。
function onPointerMove(e) {
if(isDragging) {
const target = e.currentTarget
const position = e.data.getLocalPosition(app.stage)
target.position.set(position.x, position.y)
for(let i = 0; i < circles.length; i++) {
const centerPosition = {
x: circles[i].x,
y: circles[i].y
}
const a = centerPosition.x - position.x
const b = centerPosition.y - position.y
const c = Math.hypot(a, b)
if(c < radius + dragCircleRadius) {
dragCircleSprite.alpha = 0.3
return
} else {
dragCircleSprite.alpha = 1
}
}
}
}
いい感じになりました。
コードの全容はこちらのcodepen に載せましたのでよければご参考までに!