9_4:p5.play 「Angry Birds」もどきの「Angry Bernie」ゲームサンプル

本稿では、「Game Programming」サイトの「Game Examples」ページで公開されている「Angry Bernie」ゲームサンプルを見ていきます。

「Game Examples」ページによると、「Angry Bernie」はp5.play.jsを使った「Angry Birds」スタイルのゲームだと紹介されています。「Angry Bernie」はこのリンクから実際にプレイできます。

Angry Bernie」を実際にプレイすると、次のことが分かります。

  • プレイヤースプライト(怒ったバーニー)は、マウスクリックかXキーでジャンプでき、その後着地する。
  • ジャンプは連続できる(空中にいる間でもそこからつづけてジャンプできる)。
  • 右から障害物が飛んで来て、それに当たるとゲームオーバーになる。
  • ゲームオーバーになると、ゲームオーバー画面に変わり、何秒耐えることができたかがスコアとして表示される。

以降では、こうした機能の組み込み方を順番に見ていきます。

プレイヤーがジャンプしてその後着地する

プレイヤーがジャンプしてその後着地する動作を実現するには、次の事柄が必要になります。

  1. プレイヤー用スプライトを作成する。歩行時とジャンプ時のアニメーションを変えたい場合には、それ用のアニメーションをスプライトに追加する。
  2. マウスクリックとキーの押し下げを検知する仕組みを組み込む。
  3. プレイヤースプライトが上昇し、下降する仕組みを組み込む。
  4. 地面に着地したことを知る仕組みを組み込む。

これらをゼロから作成するには結構なコード量が必要になりますが、p5.play.jsがそのほとんどを補ってくれます。

実現したい事柄 方法
プレイヤースプライトの作成と複数のアニメーションの追加 p5.play.jsのcreateSprite()、loadAnimation()、Sprite.addAnimation()などを使用する。
マウスクリックとキーの押し下げの検知 p5.play.jsのkeyWentDown()とmouseWentDown()を、draw()内で使用する。
スプライトが上昇し、下降する仕組み 適当な数値を代入した変数GRAVITYとJUMPを用意する。
スプライトが地面に着地したことを知る仕組み 地面用スプライトを作成し、プレイヤースプライトの下降中にSprite.collide()を使用して衝突を調べる。

具体的なコードは次のようになります。

// プレイヤーがページのマウスダウンかXキーの押し下げでジャンプする。
// => ジャンプ:上昇しその後下降、着地する。
// => アニメーションの変更がともなう

// プレイヤーのスプライト
let bernieSp;
// プレイヤーのアニメーション
let bernieWalkAnim;
let bernieJumpAnim;

// 地面スプライト
let groundSp;

// 自由に調整できるグローバルパラメータ
const GRAVITY = 1;
const JUMP = 15;

function preload() {
    // 歩きのアニメーション
    bernieWalkAnim = loadAnimation('assets_jump/bernie-walk-0.png', 'assets_jump/bernie-walk-3.png');
    // ジャンプのアニメーション
    bernieJumpAnim = loadAnimation('assets_jump/bernie-jump.png');
}

function setup() {
    createCanvas(800, 400);
    // プレイヤースプライトを作成
    bernieSp = createSprite();
    // アニメーションを2つ追加
    bernieSp.addAnimation('running', bernieWalkAnim);
    bernieSp.addAnimation('jumping', bernieJumpAnim);
    bernieSp.scale = 0.2;
    // 歩きのアニメーションが速いので少し間引く
    bernieSp.animation.frameDelay = 12;
    bernieSp.debug = true;
    // 地面の上、少し左に配置
    bernieSp.position.x = width / 4;
    bernieSp.position.y = height - 30;

    // 当たり判定に使用する地面スプライト(イメージは追加しない)
    groundSp = createSprite(width / 2, height, width, 30);
    // 後で組み込む地面イメージの色に合わせて、溶け込むようにする
    groundSp.shapeColor = color(121, 70, 32);
    groundSp.debug = true;
}

function draw() {
    // 背景の青
    background(0, 153, 255);
    // 論理を更新
    update();
    drawSprites();
}

// 更新の論理
function update() {
    // まず落下させる
    bernieSp.velocity.y += GRAVITY;
    // 地面と衝突したら、それ以上落ちない
    if (bernieSp.collide(groundSp)) {
        bernieSp.changeAnimation("running");
        bernieSp.velocity.y = 0;
    }

    // Xキーか左マウスボタンの押し下げでジャンプ
    if (keyWentDown("x") || mouseWentDown(LEFT)) {
        bernieSp.changeAnimation("jumping");
        // -JUMP分だけ上昇
        bernieSp.velocity.y = -JUMP;
    }
}

次のリンクをクリックすると、上記コードの実際の動作が確認できます。プレイヤースプライトはマウスクリックかXキーでジャンプし、着地前にマウスクリックするかXキーを押すと、連続ジャンプします。「BernieGame1

プレイヤースプライトのアニメーションには次のPNG画像を使用しています。画像は「Angry Bernie」ページの[download package]リンクからダウンロードできます。

上記コードで一番のポイントはdraw()関数から呼び出されるupdate()関数でしょう。「プレイヤースプライトがジャンプして、地面に着地する」動作をコードで実現するのは難しいように思われますが、実際の現実世界と同じように考えることができます。

わたしたちにはつねに重力(地球の引力)が働いています。これは下向き(鉛直方向)の力です。わたしたちが落ちつづけていかないのは、地面があるからです。地面が支えていてくれるわけです。そしてジャンプは重力に逆らう方向への力です。

障害物が飛んで来て、当たるとゲームオーバーになる

障害物が飛んで来て、当たるとゲームオーバーになる仕組みを作成するには、次の事柄が必要になります。

  • 障害物スプライトの作成。障害物は右から左に移動する。
  • プレイヤースプライトと各障害物との当たり判定。障害物を含めるp5.play.Grounpを使用する。当たった瞬間がゲームオーバーのタイミングになる。
  • 障害物スプライトは間隔を置いて、自動で生成する(マウスクリックなどでなく)

障害物スプライトには雲のイメージを使用します。障害物スプライトの作成や、プレイヤースプライトとの当たり判定はさほど難しくないように思えます。問題は、障害物スプライトを自動生成する方法です。

まず思い付くのは、window.setInterval()メソッドの使用です。次の例では、1秒ごとに障害物スプライトが生成されます。

// 障害物を生成する頻度 => 1秒に1回
const spawnObstacleInterval = 1000;

// spawnObstacleIntervalミリ秒おきにspawnObstacle()関数を呼び出す
const timerID = window.setInterval(spawnObstacle, spawnObstacleInterval);

function spawnObstacle() {
    // 障害物スプライトを生成
}

また、障害物スプライトを生成した時間をメモする変数を設けて、直近の計測時から指定時間だけ経過したら障害物スプライトを生成するという方法もあります。上記が独立したタイマーを使用する方法だとすると、これはタイマーを埋め込むような方法です。「Game Programming」のサンプルではこの方法が取られています。

// 雲を生成する頻度 => 1秒に1回
const spawnObstacleInterval = 1000;
// 障害物を生成した時間をメモする変数
let lastSpawnTime;

function setup() {
    createCanvas(800, 400);
    ...
    // タイマーをスタート
    lastSpawnTime = millis();
}

function update() {
    ...
    // 障害物を生成
    spawnObstacle();
}

function spawnObstacle() {
    // 直近の計測時からspawnObstacleIntervalだけ経過したら
    if (millis() > lastSpawnTime + spawnObstacleInterval) {
        // 障害物スプライトを生成
        ...
        // タイマーは障害物を生成するたびにリセット
        lastSpawnTime = millis();
    }
}

指定された時間間隔で自動的に障害物スプライトが生成され、右から左に移動し、プレイヤースプライトと当たり判定を行うコードは次のようになります。

let bernieSp;
let bernieWalkAnim;
let bernieJumpAnim;

let groundSp;
// 画面左に配置する壁スプライト
let leftWallSp;
// 雲のイメージ
let cloudImg;
// 雲のグループ
let cloudGroup;

// 地面のイメージ
// キャンバスに直接描画する
let groundImg;

// 雲を生成する頻度 => 1秒に1回
// ゲームの難易度に関係する
const spawnObstacleInterval = 1000;
// 障害物を生成した時間をメモする変数
let lastSpawnTime;

const GRAVITY = 1;
const JUMP = 15;

function preload() {
    bernieWalkAnim = loadAnimation('assets_jump/bernie-walk-0.png', 'assets_jump/bernie-walk-3.png');
    bernieJumpAnim = loadAnimation('assets_jump/bernie-jump.png');
    // 雲のイメージ
    cloudImg = loadImage('assets/cloud.png');
    // 地面のイメージ
    groundImg = loadImage('assets/ground.png');
}

function setup() {
    createCanvas(800, 400);

    bernieSp = createSprite();
    bernieSp.addAnimation('running', bernieWalkAnim);
    bernieSp.addAnimation('jumping', bernieJumpAnim);
    bernieSp.scale = 0.2;
    bernieSp.animation.frameDelay = 12;
    bernieSp.debug = true;
    bernieSp.position.x = width / 4;
    bernieSp.position.y = height - 30;

    groundSp = createSprite(width / 2, height, width, 30);
    groundSp.shapeColor = color(121, 70, 32);
    groundSp.debug = true;

    // 当たり判定に使用する左の壁スプライト。実際は左端の外の、見えない位置に置く
    leftWallSp = createSprite(0, height / 2, 10, height);

    // 雲のグループ
    cloudGroup = new Group();

    // タイマーをスタート
    lastSpawnTime = millis();
}

function draw() {
    background(0, 153, 255);
    update();
    drawSprites();
    // 地面のイメージを最後に描画
    image(groundImg, 0, height - 40);
}

function update() {
    bernieSp.velocity.y += GRAVITY;
    if (bernieSp.collide(groundSp)) {
        bernieSp.changeAnimation("running");
        bernieSp.velocity.y = 0;
    }

    if (keyWentDown("x") || mouseWentDown(LEFT)) {
        bernieSp.changeAnimation("jumping");
        bernieSp.velocity.y = -JUMP;
    }
    // 障害物を生成
    spawnObstacle();

    // プレイヤースプライトがいずれかの障害物と重なったら、
    // hitObstacle()コールバック関数が呼び出される
    bernieSp.overlap(cloudGroup, hitObstacle);

    // 左の壁がいずれかの障害物と重なったら、
    // deleteObstacle()コールバック関数が呼び出される
    leftWallSp.overlap(cloudGroup, deleteObstacle);
}

function spawnObstacle() {
    // 直近の計測時からspawnObstacleIntervalだけ経過したら
    if (millis() > lastSpawnTime + spawnObstacleInterval) {
        // 新しい障害物を生成
        const sp = createSprite();
        sp.addImage(cloudImg);
        sp.position.x = width;
        // 障害物が飛んで来る高さをいろいろにする
        sp.position.y = random(height - 30);
        // コライダーを設定
        sp.setCollider("rectangle", 0, 0, sp.width, sp.height);
        // 右から左に進むように負のスピードを与える
        sp.velocity.x = -4;
        sp.debug = true;
        // 障害物グループに追加
        cloudGroup.add(sp);
        // タイマーは障害物を生成するたびにリセット
        lastSpawnTime = millis();
    }
}

// プレイヤースプライトがいずれかの障害物と重なったら、とりあえず障害物をその場にとどめる
function hitObstacle(currentSp, overlappedSp) {
    print('雲に当たった!');
    // とりあえずx移動を止める
    overlappedSp.velocity.x = 0;
}

// 左の壁に当たった雲は削除
function deleteObstacle(currentSp, overlappedSp) {
    overlappedSp.remove();
    print(allSprites.length);
}

上のコードでは、障害物スプライトは生成時に専用のグループに追加しています。また画面左に壁スプライトを置き、それと衝突した障害物は消去しています。

ここでは地面のイメージを新たに作成し、地面スプライトを覆っています。プレイヤースプライトが実際に衝突を調べるのは地面スプライトで、地面のイメージはそれらしく見せる装飾です。

上記コードを実行すると、プレイヤースプライトが障害物と当たったらコンソールに「雲に当たった!」と表示されます。これがゲームオーバーのタイミングです。

ゲームオーバー画面とスコアの表示

ゲームオーバーはゲームが終わった状態であり、ここまでのコードは全部ゲームが進行中の状態です。ゲームはいつ終了するか分からないので、この状態変化は毎フレーム、監視することになります。

ゲームが進行中の状態とゲームオーバーの状態は相反する状態なので、ブーリアン型の変数が使用できます。そして毎フレーム監視するときには、ゲームオーバーの状態から先にチェックし、その後、ゲームが進行中の状態のコードを実行します。次のコードでは変数gameOverを開始時にfalseに設定しています(開始時はゲームオーバー状態でないので)。

// ゲームオーバーかどうか
let gameOver = false;

function draw() {
    // 背景の青
    background(0, 153, 255);
    // ゲームオーバーなら
    if (gameOver) {
        // ゲームオーバー時の処理

    // そうでないなら、ゲームはつづいているので、必要な処理を行う
    }
    else {
        ...
        update();
    }
    ...
}

そして、プレイヤースプライトがいずれかの障害物と重なったらゲームオーバーなので、変数gameOverをtureに設定します。これにより、draw()関数内のゲームオーバー時の処理が実行されます。

// プレイヤースプライトがいずれかの雲と重なったら、ゲームオーバー
function hitObstacle(currentSp, overlappedSp) {
    //print('雲に当たった!');
    // ゲームオーバー
    gameOver = true;
}

スコアをカウントするには専用の変数を使用します。このゲームでは、障害物に当たらず生き残った時間がそのままスコアになります。またスコアをキャンバスに表示する場合には、draw()関数内にコードを記述します。

ここまでを全部まとめると、次のようになります。

// プレイヤーのスプライト
let bernieSp;
// プレイヤーのアニメーション
let bernieWalkAnim;
let bernieJumpAnim;

// 地面スプライト
let groundSp;
// 画面左に配置する壁スプライト
let leftWallSp;
// 雲のイメージ
let cloudImg;
// 雲のグループ
let cloudGroup;

// 地面のイメージ
// キャンバスに直接描画する
let groundImg;

// 雲を生成する頻度 => 1秒に1回
// ゲームの難易度に関係する
const spawnObstacleInterval = 1000;
// 障害物を生成した時間をメモする変数
let lastSpawnTime;

// 自由に調整できるグローバルパラメータ
const GRAVITY = 1;
const JUMP = 15;

// スコアカウンタ
let score = 0;
let endingScore = 0;

// ゲームオーバーかどうか
let gameOver = false;

function preload() {
    bernieWalkAnim = loadAnimation('assets_jump/bernie-walk-0.png', 'assets_jump/bernie-walk-3.png');
    bernieJumpAnim = loadAnimation('assets_jump/bernie-jump.png');
    cloudImg = loadImage('assets/cloud.png');
    groundImg = loadImage('assets/ground.png');
}

function setup() {
    createCanvas(800, 400);
    // プレイヤースプライトを作成
    bernieSp = createSprite();
    bernieSp.addAnimation('running', bernieWalkAnim);
    bernieSp.addAnimation('jumping', bernieJumpAnim);
    bernieSp.scale = 0.2;
    bernieSp.myName = 'bernie';
    // 歩きのアニメーションが速いので少し間引く
    bernieSp.animation.frameDelay = 12;
    bernieSp.debug = true;
    // 地面の上、少し左に配置
    bernieSp.position.x = width / 4;
    bernieSp.position.y = height - 30;

    // 当たり判定に使用する地面スプライト(イメージは追加しない)
    groundSp = createSprite(width / 2, height, width, 30);
    // groundImgの茶色と合わせて、イメージに溶け込ませる
    groundSp.shapeColor = color(121, 70, 32);
    groundSp.debug = true;
    groundSp.myName = 'groundSp';


    // 当たり判定に使用する左の壁スプライト。実際は左端の外の、見えない位置に置く
    leftWallSp = createSprite(0, height / 2, 10, height);
    leftWallSp.myName = 'leftWallSp';
    // 雲のグループ
    cloudGroup = new Group();

    // タイマーをスタート
    lastSpawnTime = millis();
}

function draw() {
    // 背景の青
    background(0, 153, 255);
    // ゲームオーバーなら
    if (gameOver) {
        // スコア(秒数)を表示
        fill(255, 255, 0);
        text('GAME OVER', width / 2, height / 2);
        text('あなたは' + endingScore + "秒耐えました。", width / 2, height / 2 + 20);
        // プレイヤーを落下させ、画面下に消えたら削除する
        bernieSp.velocity.y += GRAVITY;
        if (bernieSp.position.y > height) {
            bernieSp.remove();
            // 世界が止まる
            noLoop();
        }
        // 確認用
        // for (let i = 0; i < allSprites.length; i++) {
        //print(allSprites[i].myName)
        // }

        // そうでないなら、ゲームはつづいているので、必要な処理を行う
    }
    else {
        // スコアは、死なずに耐えている秒数
        score = millis() / 1000;
        // スコアを黄色で示す
        fill(255, 255, 0);
        text("耐えている時間: " + int(score) + " 秒", 10, 15);
        update();
    }

    leftWallSp.overlap(cloudGroup, deleteObstacle);
    drawSprites();
    // 地面のイメージを最後に描画
    image(groundImg, 0, height - 40);
}

// 更新の論理
function update() {
    // まず落下させる
    bernieSp.velocity.y += GRAVITY;
    // 地面と衝突したら、それ以上落ちない
    if (bernieSp.collide(groundSp)) {
        bernieSp.changeAnimation("running");
        bernieSp.velocity.y = 0;
    }

    // Xキーか左マウスボタンの押し下げでジャンプ
    if (keyWentDown("x") || mouseWentDown(LEFT)) {
        bernieSp.changeAnimation("jumping");
        // -JUMP分だけ上昇
        bernieSp.velocity.y = -JUMP;
    }
    // 障害物を生成
    spawnObstacle();

    // プレイヤースプライトがいずれかの障害物と重なったら、
    // hitObstacle()コールバック関数が呼び出される
    bernieSp.overlap(cloudGroup, hitObstacle);

    // 左の壁がいずれかの障害物と重なったら、
    // deleteObstacle()コールバック関数が呼び出される
    // drawSprites()の上に移す
    // leftWallSp.overlap(cloudGroup, deleteObstacle);
}

function spawnObstacle() {
    // 直近の計測時からspawnObstacleIntervalだけ経過したら
    if (millis() > lastSpawnTime + spawnObstacleInterval) {
        // 新しい障害物を生成
        const sp = createSprite();
        sp.addImage(cloudImg);
        sp.position.x = width;
        sp.position.y = random(height - 30);
        // コライダーを設定
        sp.setCollider("rectangle", 0, 0, sp.width, sp.height);
        // 右から左に進むように負のスピードを与える
        sp.velocity.x = -4;
        sp.debug = true;
        sp.myName = 'cloudSp' + millis();
        // 障害物グループに追加
        cloudGroup.add(sp);
        // タイマーは雲を生成するたびにリセット
        lastSpawnTime = millis();
    }
}

// プレイヤースプライトがいずれかの雲と重なったら、ゲームオーバー
function hitObstacle(currentSp, overlappedSp) {
    //print('雲に当たった!');
    // 雲を削除
    overlappedSp.remove();
    // ゲームオーバー
    gameOver = true;
    // スコアの確定
    endingScore = int(score);
}

// 左の壁に当たった雲は削除
function deleteObstacle(currentSp, overlappedSp) {
    overlappedSp.remove();
    //print('左の壁に当たった!');
}

次のリンクをクリックすると、上記コードの実際の動作が確認できます。「BernieGame2

コメントを残す

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

CAPTCHA