7_1:p5.play スプライトの当たり判定

ゲーム制作に当たり判定(衝突の検出)は付き物です。これは逆から見た場合、当たり判定を学ぶと制作物に途端に”ゲーム感”が生まれる、ということです。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());
    }
}

Segel Artwork

このサンプルには主要な要素が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 – イメージやアニメーションの識別子

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA