ゲーム制作に当たり判定(衝突の検出)は付き物です。これは逆から見た場合、当たり判定を学ぶと制作物に途端に”ゲーム感”が生まれる、ということです。p5.play.jsの場合、Spriteオブジェクトのoverlap()やcollide()、displace()メソッドの使い方を覚えると、作成できそうなゲームのアイデアがどんどん湧いて来ます。
目次
重なり状態を調べるSprite.overlap()メソッド
スプライトのoverlap()メソッドは、対象とするスプライトや、グループに含まれる各スプライトとの重なりを調べます。
リファレンスメモ
Sprite.overlap()
overlap ( target callback ) Boolean
そのスプライトが別のスプライトかグループと重なっているかどうかをチェックする。チェックはコライダーを使って行われる。コライダーが設定されていない場合には、イメージやアニメーションの境界ボックスから自動的に作成される。
コールバック関数には、衝突が発生したときの追加的な操作の実行内容が指定できる。targetがグループの場合には、コールバック関数が、衝突している各スプライトに関して呼び出される。パラメータはそれぞれ、現在のスプライト(overlap()を呼び出したスプライト)と、衝突中のスプライト。
パラメータ
target Object – 現在のスプライトに対して調べる相手のスプライトかグループ。
[callback] Function オプション – 重なり状態にある場合に呼び出される関数
戻り値
Boolean – 重なっている場合true
例:
sprite.overlap(otherSprite, explosion);
function explosion(spriteA, spriteB) {
spriteA.remove();
spriteB.score++;
}
コライダーと言っているのは、下図に示す緑色の枠線で囲まれた部分です。スプライトは人間のような触感を持っていないので、内部でこの矩形を使って、毎フレームほかのスプライトとの重なりを調べます。コライダーの枠線は、スプライトのdebugプロパティをtrueに設定することで表示できます。参考:「5_2:p5.play スプライトの外見」
次のサンプルでは、マウスに追随するプレイヤースプライトが虫のスプライトに重なると、虫のスプライトが削除されます。
let bugsAnim;
let bugsGroup;
const bugsNum = 20;
let playerAnim;
let playerSp;
let backImg;
function preload() {
playerAnim = loadAnimation('assets/player/1.png', 'assets/player/4.png');
bugsAnim = loadAnimation('assets/insect/1.png', 'assets/insect/3.png');
backImg = loadImage('assets/tree.png');
}
function setup() {
createCanvas(500, 360);
// 虫のグループ
bugsGroup = new Group();
// 虫
for (let i = 0; i < bugsNum; i++) {
const bugsSp = createSprite();
bugsSp.addAnimation('insect', bugsAnim);
// スプライトアニメーションのスピードを少し抑える
bugsSp.animation.frameDelay = 15;
bugsSp.scale = 2;
bugsSp.rotation = random(0, 180) + 270;
bugsSp.position.x = random(20, width - 20);
bugsSp.position.y = random(20, height - 20);
// 削除時の確認用にカスタムプロパティを追加
bugsSp.myName = 'bug' + i;
// コライダーの枠線を表示
bugsSp.mouseActive = true;
bugsSp.debug = true;
// スタートフレームをスプライトごとに変える
const f = round(random(0, bugsSp.animation.getLastFrame()));
bugsSp.animation.changeFrame(f);
bugsSp.animation.play();
// 虫スプライトを無視グループに追加
bugsGroup.add(bugsSp);
}
// プレイヤースプライト
playerSp = createSprite();
playerSp.addAnimation('player', playerAnim);
// 円形のコライダーを頭部に設定
playerSp.setCollider('circle', 0, -20, 10);
// コライダーの枠線を表示
playerSp.mouseActive = true;
playerSp.debug = true;
// 確認用にカスタムプロパティを追加
playerSp.myName = 'player';
}
function draw() {
image(backImg, 0, 0);
update();
drawSprites();
}
// 毎フレーム呼び出される
function update(sp, target) {
// プレイヤースプライトをマウスに少し遅れて追随
playerSp.velocity.x = (mouseX - playerSp.position.x) / 10;
playerSp.velocity.y = (mouseY - playerSp.position.y) / 10;
// プレイヤースプライトと虫グループに含まれる虫との重なりを調べる。
// 重なっていると、引数に渡したコールバック関数が呼び出される。
playerSp.overlap(bugsGroup, (currentSp, overlappedSp) => {
// 重なった虫を削除
overlappedSp.remove();
print(currentSp.myName + 'が' + overlappedSp.myName + 'を喰った');
})
}
このサンプルでは、プレイヤースプライトのoverlap()メソッドを、虫のグループ(bugsGroup)に対して実行しています。コールバック関数を渡しているので、第1引数のcurrentSpはplayerSpを、第2引数のoverlappedSpはbugsGroupに含まれる、重なり状態にある虫のスプライトを参照しています。
playerSp.overlap(bugsGroup, (currentSp, overlappedSp) => {
overlappedSp.remove();
print(currentSp.myName + 'が' + overlappedSp.myName + 'を喰った');
});
コライダーはデフォルトで、イメージやアニメーション全体を囲む矩形ですが、Sprite.setCollider()を使用すると、コライダーの形状を円に変えたり、サイズや位置を変更することができます。プレイヤーが虫を食べるのは口なので、プレイヤーのコライダーを円形にして頭部辺りに移すと、”食べる”感じが出ます。
playerSp.setCollider('circle', 0, -20, 10);
下図の左はデフォルトのコライダー、右がsetCollider()で設定したコライダーです。Sprite.setCollider():「5_6:p5.play スプライトのドラッグ」
衝突状態を調べるSprite.collide()メソッド
スプライトのcollide()メソッドは、対象とするスプライトや、グループに含まれる各スプライトとの衝突状態を調べます。
リファレンスメモ
Sprite.collide()
collide ( target callback ) Boolean
そのスプライトが別のスプライトかグループと重なっているかどうかをチェックする。重なっている場合、現在のスプライトは、衝突中の相手スプライトによって、重なっていない最も近い位置に移される。
チェックはコライダーを使って行われる。コライダーが設定されていない場合には、イメージやアニメーションの境界ボックスから自動的に作成される。
コールバック関数には、衝突が発生したときの追加的な操作の実行内容が指定できる。targetがグループの場合には、コールバック関数が、衝突している各スプライトに関して呼び出される。パラメータはそれぞれ、現在のスプライト(collide()を呼び出したスプライト)と、衝突中のスプライト。
パラメータ
target Object – 現在のスプライトに対して調べる相手のスプライトかグループ。
[callback] Function オプション – 重なり状態にある場合に呼び出される関数
戻り値
Boolean – 重なっている場合true
例:
sprite.collide(otherSprite, explosion);
function explosion(spriteA, spriteB) {
spriteA.remove();
spriteB.score++;
}
overlap()との違いは、collide()を実行するスプライトが、相手スプライトによって”押し返される”ことにあります。overlap()の場合、重なり状態は解消されませんが、collide()の場合には、相手スプライトによって最も近い位置に移動させられることで、重なり状態がなくなります。
次のサンプルは開発途中のシューティングゲームのようなものです。マウスボタンの押し下げで弾が連射でき、空に飛んでいる鳥スプライトに弾が命中すると、鳥は死んで落下します。
let birdFlyAnim;
let birdDieAnim;
let birdSp;
let backImg;
let launcherSp;
let bulletSp;
let bulletGroup;
// 弾のスピードを制限 => 弾が速すぎると衝突判定できない
const maxSpeed = 3;
function preload() {
birdFlyAnim = loadAnimation('assets/fly/000.png', 'assets/fly/007.png');
birdDieAnim = loadAnimation('assets/die/000.png', 'assets/die/007.png');
backImg = loadImage('assets/background.png');
}
function setup() {
createCanvas(500, 300);
// 鳥のスプライト
birdSp = createSprite();
// Animationは先に仕込んでおく
birdSp.addAnimation('fly', birdFlyAnim);
birdSp.addAnimation('die', birdDieAnim);
birdSp.scale = 0.7;
birdSp.position.x = random(0, width);
birdSp.position.y = random(30, 130);
birdSp.debug = true;
// 弾の発射台
launcherSp = createSprite(width / 2, height - 10, 100, 10);
// 弾グループ
bulletGroup = new Group();
}
function draw() {
// 背景を描画
image(backImg, 0, 0);
update();
drawSprites();
}
function update() {
// 発射台スプライトをマウスで制御する。
launcherSp.position.x = constrain(mouseX, launcherSp.width / 2, width - launcherSp.width / 2);
// 鳥スプライトが弾グループに含まれる弾と衝突したら、
// コールバック関数を呼び出す。
birdSp.collide(bulletGroup, (currentSp, overlappedSp) => {
// dieアニメーションに変更
currentSp.changeAnimation('die');
// 最後のフレームまで1回だけ再生
currentSp.animation.goToFrame(currentSp.animation.getLastFrame());
// 落下
currentSp.velocity.y += 3;
// ほどなく消える
currentSp.life = 50;
// 弾も削除
overlappedSp.remove();
});
}
// 画面のどこかをマウスプレスしたら
function mousePressed() {
// 発射台スプライトのx位置
const xpos = launcherSp.position.x;
// 弾スプライトを、発射台スプライトのセンターに作成
bulletSp = createSprite(xpos, height - 10, 10, 10);
//bulletSp.shapeColor = color(0);
// 画面上端を超えたら、ほどなく自動的に削除される
bulletSp.life = 100;
// 弾のスピード制限
bulletSp.maxSpeed = maxSpeed;
// 上に移動
bulletSp.setSpeed(maxSpeed, 270);
bulletSp.debug = true;
// 弾グループに追加
bulletGroup.add(bulletSp);
// 弾スプライトのカスタム描画
// スプライトを細長い楕円で描画する
bulletSp.draw = function() {
// (0, 0)は弾スプライトの中心。thisは弾スプライト
ellipse(0, 0, 10 - this.getSpeed(), 10 + this.getSpeed());
}
}
このサンプルには主要な要素が3つあります。弾の発射台(launcherSp)と、弾(bulletSp)、鳥(birdSp)です。
1: launcherSp
launcherSpはイメージやアニメーションを持たない素のスプライトですが、マウスのx位置に応じて左右に移動します。
launcherSp.position.x = constrain(mouseX, launcherSp.width / 2, width - launcherSp.width / 2);
constrain(n, low, high)は、値nをlowとhighの間に制限して返すp5.jsの関数です。ここでは発射台がキャンバスの外に出ないように制限しています。
2: bulletSp
bulletSpは、ページのマウスダウン時に毎回新たに、発射台のセンターに作成しています。
function mousePressed() {
const xpos = launcherSp.position.x;
bulletSp = createSprite(xpos, height - 10, 10, 10);
bulletSp.life = 100;
bulletSp.maxSpeed = maxSpeed;
bulletSp.setSpeed(maxSpeed, 270);
bulletSp.debug = true;
bulletGroup.add(bulletSp);
bulletSp.draw = function() {
ellipse(0, 0, 10 - this.getSpeed(), 10 + this.getSpeed());
}
}
lifeプロパティをに設定しているのは、境界を設定してそれを超えたら弾を削除する、という作業よりもはるかに簡単だからです。弾はキャンバスの上端を超えるともう不要なので、上端を確実に超えるだけの寿命を与えるだけで、後はp5.play.jsが始末してくれます。
弾が上方に進むのはSprite.setSpeed(スピード, 270)メソッドによってです。弾のスピードが速すぎると衝突判定が効かない(1回のdraw()の呼び出し中に、ターゲットをすり抜けてしまう)ので、ここではスピードを制限しています。
そして重要なのが、前持って作成しておいたGroupオブジェクト(bulletGroup)へのbulletSpの追加です。bulletGroupを使うことで、鳥スプライトとの当たり判定が効率よく行えます。
また当たり判定とは関係のないテクニックですが、ここではbulletSp.draw = function() {…}という方法を使って、bulletSpのdraw()メソッドを上書きしています(オーバーライド)。このテクニックは、イメージやアニメーションを持たない「プレースホルダ矩形」のスプライトを使うときにうまく活用できます。
bulletSp.drawプロパティに関数(Functionオブジェクト)を代入することで、bulletSpのプレースホルダ矩形の外見が変更できます。ここではSprite.getSpeed()メソッドを使って、縦長に見える楕円を描画しています。この関数内では、位置(0, 0)はスプライトの中心を指し、thisはそのスプライトを参照します。
3: bulletSp
鳥スプライトには、弾が命中したときに死んだアニメーションを実行させたいので、スプライトの作成時に通常のアニメーションに加え、死んだときのアニメーションも追加しておきます。
birdSp.addAnimation('fly', birdFlyAnim);
birdSp.addAnimation('die', birdDieAnim);
鳥スプライトへの弾の命中は、弾スプライトのcollide()で行うより、鳥スプライトのcollide()で行う方がはるかに効率的です。弾スプライトはbulletGroupオブジェクトにまとめてあるので、birdSp.collide()メソッドにそのまま指定できます。
birdSp.collide(bulletGroup, (currentSp, overlappedSp) => {
currentSp.changeAnimation('die');
currentSp.animation.goToFrame(currentSp.animation.getLastFrame());
currentSp.velocity.y += 3;
currentSp.life = 50;
overlappedSp.remove();
});
collide()の第2引数には、命中時に呼び出されるコールバック関数が指定できます。コールバック関数の引数には、collide()を呼び出したスプライト(currentSp)と今重なり状態にあるスプライト(overlappedSp)が渡されます。上記サンプルでは、currentSpを使ってdieアニメーションに変更し、落下させ、短い寿命を設定しています。これにより鳥スプライトは死ぬアニメーションを再生しながら落下して、キャンバスから削除されます。命中した弾スプライト(overlappedSp)も削除します。
接触相手を押しやるSprite.displace()メソッド
スプライトのdisplace()メソッドは、接触している相手を押すことができます。
リファレンスメモ
Sprite.displace()
displace ( target callback ) Boolean
そのスプライトが別のスプライトかグループと重なっているかどうかをチェックする。重なっている場合、現在のスプライトは、衝突している相手スプライトを、重なっていない最も近い位置に移動する。
チェックはコライダーを使って行われる。コライダーが設定されていない場合には、イメージやアニメーションの境界ボックスから自動的に作成される。
コールバック関数には、衝突が発生したときの追加的な操作の実行内容が指定できる。targetがグループの場合には、コールバック関数が、衝突している各スプライトに関して呼び出される。パラメータはそれぞれ、現在のスプライト(displace()を呼び出したスプライト)と、衝突中のスプライト。
パラメータ
target Object – 現在のスプライトに対して調べる相手のスプライトかグループ。
[callback] Function オプション – 重なり状態にある場合に呼び出される関数
戻り値
Boolean – 重なっている場合true
例:
sprite.displace(otherSprite, explosion);
function explosion(spriteA, spriteB) {
spriteA.remove();
spriteB.score++;
}
次のサンプルでは、岩のスプライトを上下左右の矢印キーで移動させ、3つのステッカースプライトを押すことができます。岩のスプライトは、押している間赤くなり、離れると元に戻ります。
let stickerImg1;
let stickerImg2;
let stickerImg3;
let rockNormalImg;
let rockRedImg;
let playerSp;
let stickerGroup;
let backImg;
function preload() {
backImg = loadImage('assets/calendar.png');
stickerImg1 = loadImage('assets/sticker/1.png');
stickerImg2 = loadImage('assets/sticker/2.png');
stickerImg3 = loadImage('assets/sticker/3.png');
rockNormalImg = loadImage('assets/rockNormal.png');
rockRedImg = loadImage('assets/rockRed.png');
}
function setup() {
createCanvas(300, 300);
// ステッカーグループ
stickerGroup = new Group();
// ステッカースプライトを3つ作って、ステッカーグループに追加
const stickerSp1 = createSprite();
stickerSp1.addImage(stickerImg1);
setProprty(stickerSp1, 187, 185);
stickerGroup.add(stickerSp1);
const stickerSp2 = createSprite();
stickerSp2.addImage(stickerImg2);
setProprty(stickerSp2, 106, 140);
stickerGroup.add(stickerSp2);
const stickerSp3 = createSprite();
stickerSp3.addImage(stickerImg3);
setProprty(stickerSp3, 106, 185);
stickerGroup.add(stickerSp3);
// プレイヤースプライト(岩のイメージ)
playerSp = createSprite();
// イメージを2つ追加する
playerSp.addImage('normal', rockNormalImg);
playerSp.addImage('red', rockRedImg);
setProprty(playerSp, 267, 270);
playerSp.friction = 0.06;
}
function draw() {
image(backImg, 0, 0);
update(playerSp, stickerGroup);
drawSprites();
}
// 毎フレーム呼び出される
function update(sp, target) {
// spスプライトがtargetグループに含まれるスプライトのどれかに衝突したら、
// spスプライトはそのスプライトを押す。
if (sp.displace(target)) {
sp.changeImage('red');
}
else {
sp.changeImage('normal');
}
}
function setProprty(sp, xpos, ypos) {
sp.position.x = xpos;
sp.position.y = ypos;
sp.debug = true;
}
// キー操作
function keyPressed() {
const sp = playerSp;
// 上キーで上方向
if (keyCode === UP_ARROW) {
sp.setSpeed(1, 270);
// 下キーで下方向
}
else if (keyCode === DOWN_ARROW) {
sp.setSpeed(1, 90);
// 右キーで右方向
}
else if (keyCode === RIGHT_ARROW) {
sp.setSpeed(1, 0);
// 左キーで左方向
}
else if (keyCode === LEFT_ARROW) {
sp.setSpeed(1, 180);
}
}
キーボードで操作するには、一度キャンバス画面をクリックして、フォーカスを移す必要があります。
Sprite.displace()は、draw()関数から呼び出されるupdate()関数で、次のように使用しています。これにより、sp(岩のスプライト)がtarget(ステッカーのグループ)に含まれるいずれかのスプライトに衝突したら、spが対象スプライトを押し、spの描画するイメージがredに変わります。衝突状態でなくなったら、元のnormalに戻ります
function update(sp, target) {
if (sp.displace(target)) {
sp.changeImage('red');
}
else {
sp.changeImage('normal');
}
}
リファレンスメモ
Sprite.changeImage()
changeImage ( label )
表示されるイメージやアニメーションを変更する。changeAnimation()と同じ。
パラメータ
label String – イメージやアニメーションの識別子