7_2:p5.play スプライトの跳ね返り

たとえば、ボールを転がして、それが壁に当たって戻って来る、という跳ね返りを想像しただけでも、ボールのスピードや地面との摩擦、ボールが壁に当たるときの角度、ボールの重さなど、物理学的な知識や計算が必要になりそうに思えます。

しかし、難しいことは無視し、ただ跳ね返っているように見えればよい、というのであれば、「5_4:p5.play スプライトの移動」で見た次のコードが応用できます。

// スプライトが既定高以上になったら(キャンバス外に出たら)
if (sp.position.y >= bounceY) {
    // 垂直方向のスピードを反転 => 上に移動する
    sp.velocity.y *= -1;
    // 位置を既定高に設定して、突き抜けないようにする
    sp.position.y = bounceY;
}
// スプライトを落下させる
sp.addSpeed(gravity, 90);

上下左右の端で跳ね返る

次の例では、ボールが四方の壁で跳ね返ります。境界チェックの方法は、毎フレーム、ボールと四方の壁の位置関係を調べ、ボールが壁を超えたときに、速度を反転させる(velocity.x、velocity.yに-1を掛ける)という方法です。draw()関数の1回の呼び出し中に、X軸方向とY軸方向のチェックを別々に行い、いっしょに更新することで、斜め方向にも跳ね返るので、いかにもそれらしく見えます。

let sp;
// 赤い円スプライトの直径
const diameter = 20;
// 左の壁 => キャンバスの左端(x=0)
const leftWall = 0;
// 上の壁 => キャンバスの上端(y=0)
const topWall = 0;
let rightWall;
let bottomWall;
const offset = diameter / 2;

function setup() {
    createCanvas(400, 300);
    // 右の壁はキャンバスの幅(x=400)
    rightWall = width;
    // 下の壁はキャンバスの高さ(y=300)
    bottomWall = height;

    // 塗り色を赤に
    fill(255, 0, 0);
    // 赤いスプライトを作成
    sp = createSprite();
    sp.position.x = width / 2;
    sp.position.y = height / 2;
    // 円を描く
    sp.draw = function() {
            ellipse(0, 0, diameter);
        }
        // ランダムなスピードとランダムな方向でスタート
    sp.setSpeed(random(4, 5), random(0, 360));
}

function draw() {
    background(200);
    update()
    drawSprite(sp);
}

function update() {
    // ボールスプライトと壁との位置関係を調べ、
    // ボールが壁を超えたら、反転(跳ね返り)
    if (sp.position.x > rightWall) {
        sp.position.x = rightWall - offset;
        // 跳ね返り
        sp.velocity.x = -abs(sp.velocity.x);
    }
    else if (sp.position.x < leftWall) {
        sp.position.x = leftWall + offset;
        // 跳ね返り
        sp.velocity.x = abs(sp.velocity.x);
    }

    if (sp.position.y > bottomWall) {
        sp.position.y = bottomWall - offset;
        // 跳ね返り
        sp.velocity.y = -abs(sp.velocity.y);
    }
    else if (sp.position.y < topWall) {
        sp.position.y = topWall + offset;
        // 跳ね返り
        sp.velocity.y = abs(sp.velocity.y);
    }
}

上下左右の端で跳ね返る(Sprite.bounce()を使用)

上記サンプルと同じようなことは、Sprite.bounce()メソッドを使っても行えます。四方を適切な大きさのスプライトで囲み、非常に重くて動かせない(跳ね返りや衝突に動じない)ことを意味するimmovableプロパティをtrueに設定します。

let sp;
const diameter = 20;

let leftWall;
let topWall;
let rightWall;
let bottomWall;
const wallTthickness = 2;


function setup() {
    createCanvas(400, 300);
    const wallColor = color(0);
    // 四方の壁スプライトを作成
    topWall = createSprite(width / 2, wallTthickness / 2, width + wallTthickness * 2, wallTthickness)
    topWall.shapeColor = wallColor;
    // 重くて動かない
    topWall.immovable = true;

    bottomWall = createSprite(width / 2, height - wallTthickness / 2, width + wallTthickness * 2, wallTthickness);
    bottomWall.shapeColor = wallColor;
    bottomWall.immovable = true;

    leftWall = createSprite(wallTthickness / 2, height / 2, wallTthickness, height);
    leftWall.shapeColor = wallColor;
    leftWall.immovable = true;

    rightWall = createSprite(width - wallTthickness / 2, height / 2, wallTthickness, height);
    rightWall.shapeColor = wallColor;
    rightWall.immovable = true;


    fill(255, 0, 0);
    sp = createSprite();
    // 最大スピードを設定しないと、スピードが上がり過ぎる
    sp.maxSpeed = 10;
    // スプライトの重さ
    sp.mass = 1;
    // 反発係数
    sp.restitution = 1.1;

    sp.position.x = width / 2;
    sp.position.y = height / 2;

    sp.debug = true;
    sp.setCollider("circle", 0, 0, diameter / 2)
    sp.draw = function() {
        ellipse(0, 0, diameter);
    }
    sp.setSpeed(random(4, 5), random(0, 360));
}

function draw() {
    background(200);
    update()
    drawSprites();
}

function update() {
    // ボールと四方の壁との跳ね返り
    sp.bounce(topWall);
    sp.bounce(bottomWall);
    sp.bounce(leftWall);
    sp.bounce(rightWall);
}

跳ね返らせたい相手が設定できれば、あとはbounce()メソッドに合いてスプライトを指定するだけなので、コードは短くて済みます。

スプライトには、massやrestitution、frictionなど、スプライトの振る舞いを物理学的に処理するプロパティが設定できます。ただし値の設定と組み合わせにはテストが必要で、極端な値の設定は避けるようにします。

たとえば反発係数のrestitutionを1以上に設定すると、ボールは跳ね返るたびにスピードが増すので、1フレームの移動距離が壁の厚みを超え、壁の外へ出て行ってしまいます。これを避けるにはmaxSpeedプロパティを設定します。また壁の厚みを十分に厚くします。上記サンプルでは壁の厚みのwallTthicknessを2に設定していますが、20や30に設定する方がよいでしょう。

リファレンスメモ
Sprite.immovable

immovable Boolean

trueに設定すると、そのスプライトは衝突によって跳ね返ったり移動したりしない。無限の質量や固定された物体をシミュレートする。デフォルトはfalse。

Sprite.mass

mass Number

massは、スプライト同士が跳ね返るときの速度交換を決める。Sprite.bounceを参照。質量が大きいほど、スプライトは衝突の影響を受けなくなる。デフォルトは1。

Sprite.restitution

restitution Number

反発係数。跳ね返り後速度は減衰する。1は完全に弾性で、エネルギーは失われない。0は完全な非弾性で、跳ね返らない。1未満は非弾性で、自然界によく見られる。1以上は超弾性で、エネルギーは、ピンボールのバンパーのように、増加する。デフォルトは1。

Sprite.friction

friction Number

摩擦要因。スプライトの速度を遅くする。設定する値は0に近くすべき(0.01など)。0は摩擦なし、1は完全な摩擦。デフォルトは0。

Sprite.bounce()

bounce ( target callback ) Boolean

そのスプライトが別のスプライトかグループと重なっているかどうかをチェックする。重なっている場合、それらのスプライトは、.velocityと.mass、.restitutionプロパティに応じて、互いの軌道に影響を及ぼしながら跳ね返る。

チェックはコライダーを使って行われる。コライダーが設定されていない場合には、イメージやアニメーションの境界ボックスから自動的に作成される。

コールバック関数には、衝突が発生したときの追加的な操作の実行内容が指定できる。targetがグループの場合には、コールバック関数が、衝突している各スプライトに関して呼び出される。パラメータはそれぞれ、現在のスプライト(bounce()を呼び出したスプライト)と、衝突中のスプライト。

パラメータ

target Object – 現在のスプライトに対して調べる相手のスプライトかグループ。
[callback] Function オプション – 重なり状態にある場合に呼び出される関数

戻り値

Boolean – 重なっている場合true

例:

sprite.bounce(otherSprite, explosion);

function explosion(spriteA, spriteB) {
    spriteA.remove();
    spriteB.score++;
}

ピンボール シミュレーション

次のサンプルでは、マウスの押し下げで赤いボールを真上にショットできます。四方の壁は前の例より厚みの1/3ずつ外に移動させています。massやrestitution、frictionにさまざまな値を与えて、それらの組み合わせがテストできます。またscale値をmassに設定すると、大きいものほど重くできます。

let spGroup;
const diameter = 20;
const spNum = 6;

let leftWall;
let topWall;
let rightWall;
let bottomWall;
const wallTthickness = 30;

let ballImg;
let ballSp;

let posData;

function preload() {
    ballImg = loadImage('assets/ball.png');
}

function setup() {
    createCanvas(400, 300);
    // 青ボールスプライトを配置するデータを作成
    const cx = width / 2;
    const cy = height / 2;
    const gutter = 13;
    posData = [{
        x: cx,
        y: cy
    }, {
        x: cx - gutter,
        y: cy - gutter
    }, {
        x: cx + gutter,
        y: cy - gutter
    }, {
        x: cx - gutter * 2,
        y: cy - gutter * 2
    }, {
        x: cx,
        y: cy - gutter * 2
    }, {
        x: cx + gutter * 2,
        y: cy - gutter * 2
    }];

    // 上下左右の壁。どれも動かせなくする。
    const wallColor = color(0);
    topWall = createSprite(width / 2, -wallTthickness / 3, width + wallTthickness * 2, wallTthickness)
    topWall.shapeColor = wallColor;
    topWall.immovable = true;

    bottomWall = createSprite(width / 2, height + wallTthickness / 3, width + wallTthickness * 2, wallTthickness);
    bottomWall.shapeColor = wallColor;
    bottomWall.immovable = true;

    leftWall = createSprite(-wallTthickness / 3, height / 2, wallTthickness, height);
    leftWall.shapeColor = wallColor;
    leftWall.immovable = true;

    rightWall = createSprite(width + wallTthickness / 3, height / 2, wallTthickness, height);
    rightWall.shapeColor = wallColor;
    rightWall.immovable = true;

    spGroup = new Group();

    // 青いボールスプライトを作成して、spGroupに追加
    for (let i = 0; i < spNum; i++) {
        sp = createSprite();
        sp.maxSpeed = 8;
        // 大きいものほど重くする
        sp.scale = random(0.3, 2);
        sp.mass = sp.scale;
        // 反発係数
        sp.restitution = 0.9;
        // 摩擦係数
        sp.friction = 0.01;
        sp.debug = true;
        // コライダーを円の大きさに合わせる
        sp.setCollider("circle", 0, 0, diameter / 2)
            // 青い円を描画
        sp.draw = function() {
                fill(0, 0, 255);
                ellipse(0, 0, diameter);
            }
            // ボーリングのピンのように配置
        sp.position.x = posData[i].x;
        sp.position.y = posData[i].y;
        spGroup.add(sp);
    }

    ballSp = createSprite();
    ballSp.addImage(ballImg);
    ballSp.debug = true;
    ballSp.mass = 3;
    ballSp.friction = 0.01;
    // コライダーを円形にし、画像サイズと合わせる
    ballSp.setCollider("circle", 0, 0, 15);
    ballSp.position.x = width / 2;
    ballSp.position.y = height - 50;
}

function draw() {
    background(200);
    update();
    drawSprites();
}

function update() {
        // 青いボールスプライトの跳ね返り処理
        spGroup.bounce(topWall);
        spGroup.bounce(bottomWall);
        spGroup.bounce(leftWall);
        spGroup.bounce(rightWall);
        spGroup.bounce(spGroup);

        // 赤いボールスプライトの跳ね返り処理
        ballSp.bounce(spGroup);
        ballSp.bounce(topWall);
        ballSp.bounce(bottomWall);
        ballSp.bounce(leftWall);
        ballSp.bounce(rightWall);
    }
    // 真上に動く
function mousePressed() {
    ballSp.setSpeed(10, 270);
}

Sprite.bounce()をグラフで見る

次のサンプルは「5_4:p5.play スプライトの移動」の「落下と跳ね返り」サンプルのSprite.bounce()バージョンで、カミナリ小僧のposition.yとvelocity.yの変化をグラフで描画しています。


let kaminariImage;
let kaminariSp;

let cloudImage;

let trampolineImage;
let trampolineSp;

let cloudPos;

const gravity = 1;

let startButton;
let isStart = false;
// 1フレーム前のvelocity.y
let previousVelocityY = 0;

function preload() {
    kaminariImage = loadImage('assets/kaminari.png');
    cloudImage = loadImage('assets/cloud.png');
    trampolineImage = loadImage('assets/trampoline.png');
}

function setup() {
    const canvas = createCanvas(300, 500);
    // 「Positioning your canvas」
    // https://github.com/processing/p5.js/wiki/Positioning-your-canvas
    canvas.parent('sketch-holder');
    // カミナリスプライト
    kaminariSp = createSprite();
    kaminariSp.addImage('kaminari', kaminariImage);
    kaminariSp.position.x = width / 2;
    kaminariSp.position.y = 80;
    kaminariSp.debug = true;
    // 反発係数の設定
    kaminariSp.restitution = 0.7;
    //kaminariSp.friction = 0.03;
    //kaminariSp.mass = 1;

    // トランポリンスプライト
    trampolineSp = createSprite();
    trampolineSp.addImage('trampoline', trampolineImage);
    trampolineSp.position.x = width / 2;
    trampolineSp.position.y = 450;
    trampolineSp.debug = true;
    // 動かせないスプライトにする(当たり判定に反応しない)
    trampolineSp.immovable = true;
    // トランポリン画像の真ん中辺りで跳ね返って見えるように、コライダーを少し下げる
    trampolineSp.setCollider("rectangle", 0, 20, trampolineSp.width, trampolineSp.height);

    cloudPos = {
        x: width / 2 - cloudImage.width / 2,
        y: 90
    };

    // [スタート]ボタンのクリックでカミナリスプライトの落下を開始
    startButton = createButton('スタート');
    startButton.position(230, 530);
    startButton.size(80, 30);
    startButton.mouseClicked(() => {
        isStart = true;
        startButton.elt.disabled = true;
    });
}

function draw() {
    background(17, 177, 255);
    image(cloudImage, cloudPos.x, cloudPos.y);
    // トランポリンスプライトを描画
    drawSprite(trampolineSp)
        // [スタート]ボタンがクリックされたら
    if (isStart) {
        // カミナリスプライトの移動の論理を実行
        update(kaminariSp, trampolineSp);
    }
    // カミナリスプライトを描画
    drawSprite(kaminariSp);
}

// 毎フレーム呼び出される
function update(sp, target) {
    // spを落下
    sp.addSpeed(gravity, 90);
    // spをtargetに対して跳ね返らせる
    sp.bounce(target);
    // 垂直方向の速度の変化を時系列でグラフに描画
    plot(frameCount, sp.velocity.y)

    // 垂直方向の速度の、1フレーム間の変化量
    const deff = sp.velocity.y - previousVelocityY;
    // deffが -10と-9の間の数値になったら、反発係数を0にしてspの”震え”を止める。
    // 同時にタイマーを使って、1秒後にアニメーション再生も止める(グラフの描画を止める)
    if (deff < -9 && deff > -10) {
        sp.restitution = 0;
        window.setTimeout(() => {
            noLoop();
        }, 1000)
    }
    previousVelocityY = sp.velocity.y;
}

カミナリ小僧のSprite.bounce()バージョン

下図は上記サンプルのグラフの3つの部分を取り出し、各フレームでのvelocity.yとposition.yの数値を表示したものです。

カミナリ小僧は最初落下します。落下はグラフでは、右下に下がっていくオレンジ色の曲線で表されます。これは、毎フレーム呼び出されるsp.addSpeed(gravity, 90)、つまり下方向への加速(重力のシミュレーション)によるものです。毎フレーム、gravityが足されるので加速し、線は曲線になります。この間、velocity.yの値はgravity分ずつ大きくなっていきます。

そしてカミナリ小僧とトランポリンスプライトのコライダーが重なると、sp.bounce(target)によって、カミナリ小僧の跳ね返りが生じます((1)の場面)。トランポリンにはimmovableプロパティが設定してあるので、跳ね返りによる移動は働きません。

それまで正の値(25)であったカミナリ小僧のvelocity.yは、bounce()の発動によって、次のフレームで負の値(-18.2)に大きく変化します((2)の場面)。これによりカミナリ小僧のposition.xは、前のフレームより小さくなり、上昇に転じます((3)の場面)。これが跳ね返りです。どれだけ大きく跳ね返るか(重なった後の次のフレームのvelocity.yの変化量)には、カミナリ小僧のスピードや質量、反発係数といった物理学の要因が計算されています。

カミナリ小僧は上昇しますが、一方でsp.addSpeed(gravity, 90)がつねに作用しているので、これが相殺され、velocity.yは大きくなっていきます(下方向への加速なので、カミナリ小僧の上昇度は次第に小さくなります)。そしてvelocity.yが0を過ぎたとき、カミナリ小僧は再び落下に転じます((4)の場面)。

グラフの描画領域にマウスカーソルを重ねると、そのフレーム再生時点でのvelocity.yとposition.xの値が表示されます。これは、上記サンプルで使用しているplotly.jsライブラリの機能です。plotly.jsについては、「3-3 基本 その3 視覚化」で述べています。

なお上記サンプルでは、次のidex.htmlとgraph.jsを使用しています。

[index.html]

<!doctype html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <title>p5.js</title>
  <script src="lib/p5.min.js"></script>
  <script src="lib/p5.dom.min.js"></script>
  <script src="lib/p5.sound.min.js"></script>
  <script src="lib/p5.play.js"></script>
  <script src="lib/plotly-latest.min.js"></script>
  <script src="lib/graph.js"></script>

  <style>
    .graph-area {
      margin: 10px;
      width: 500px;
      height: 500px;
      border: 1px solid black;
    }
    
    .container {
      display: flex;
      width: 1200px;
    }
    
    #sketch-holder {
      margin: 10px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div id="sketch-holder">
      <!-- Our sketch will go here! -->
    </div>
    <div id="chart" class="graph-area"></div>
  </div>
  <script src="sketch.js"></script>
</body>

</html>

[graph.js]

let xData1 = [];
let yData1 = [];
let xData2 = [];
let yData2 = [];

const plot = (a, b, c, d) => {
    xData1.push(a);
    yData1.push(b);
    xData2.push(c);
    yData2.push(d);

    const trace1 = {
        x: xData1,
        y: yData1,
        type: 'scatter',
        name: 'velocity.y'
    };

    const trace2 = {
        x: xData2,
        y: yData2,
        type: 'scatter',
        name: 'position.y'
    };
    const layout = {
        xaxis: {
            //range: [0, 400],
            range: [60, 150],
            title: 'フレーム数'
        },
        yaxis: {
            //range: [400, -30],
            range: [450, -30],
            title: ''
        },
        title: '跳ね返り'
    };
    Plotly.newPlot('chart', [trace1, trace2], layout, {
        displayModeBar: false
    });
}

コメントを残す

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

CAPTCHA