Phaser.jsで始める簡単ゲーム開発⑥|効果音とリサイズ対応でゲームを仕上げよう!

Phaser.jsで始める簡単ゲーム開発⑥|効果音とリサイズ対応でゲームを仕上げよう!

この記事では、Phaser.jsを使って初心者でも簡単に楽しいゲームを作れるよう、ステップごとに分かりやすく解説していきます。最終的に、このシリーズを通して「ぱくぱくフルーツ」という、画面に現れる果物をタップして消していくシンプルなゲームを完成させることができます。「ぱくぱくフルーツ」は、クリックやタッチで果物を消すと、次の果物が出現して繰り返し遊べるような仕組みや、インタラクティブな要素やアニメーションを取り入れたゲームです。

こんにちは!前回の記事では、果物が画面の中を動き、画面の端にぶつかると跳ね返るアニメーションと、果物が現れるときに「ぽよよん」としたアニメーションを追加しました。

前回の記事▽

この章では、さらにゲームを仕上げるために、効果音を追加し、画面のリサイズに対応できるようにしていきます。

効果音の追加

ゲームに効果音を追加することで、果物が消えるときや現れるときに、より楽しい体験を提供することができます。効果音の追加は、ゲームに臨場感を与え、プレイヤーがアクションに対してフィードバックを得られるようにします。

効果音の読み込み

まず、効果音のデータを読み込みます。これは、preload関数内で行います。

function preload() {
    this.load.image('apple', 'assets/apple.png');
    this.load.image('grape', 'assets/grape.png');
    this.load.image('banana', 'assets/banana.png');
    this.load.image('melon', 'assets/melon.png');
    this.load.image('orange', 'assets/orange.png');
    this.load.audio('pop', 'assets/pecho.mp3'); // 効果音のデータを読み込み
    this.load.audio('appear', 'assets/poincho.mp3'); // 効果音のデータを読み込み
}
  • this.load.audio('pop', 'assets/pecho.mp3'): 果物が消えるときの効果音を読み込みます。
  • this.load.audio('appear', 'assets/poincho.mp3'): 果物が現れるときの効果音を読み込みます。

これらの音声データは、assetsフォルダに格納しておく必要があります。適切な効果音ファイルを用意し、指定されたパスに配置してください。

効果音の素材の用意に迷ってら、「効果音ラボ」という無料で効果音をダウンロードできるサイトがおすすめです▽

スマートフォンでの音声再生の制約

スマートフォンやタブレットでは、ユーザーが画面をタップするまで音声が自動的に再生されないよう制御されています。これは、突然の音がユーザーにとって不快にならないようにするための仕様です。この制約を考慮し、ゲームの最初のタップが行われるまで音が鳴らないようにします。

このために、コードでは初めの果物が表示される際には効果音が再生されないように設定し、初回のタップ以降に効果音が再生されるようにしています。

if (isFirst) {
    isFirst = false;
    return;
}
  • isFirst: この変数は、ゲームが初めて実行されたときに効果音を抑制するために使用されます。isFirsttrueである場合、効果音は再生されません。isFirstfalseに設定された後、2回目以降のタップでは効果音が正常に再生されます。

効果音を再生する

次に、果物が消えるときと新しい果物が現れるときに、効果音を再生します。

function create() {
    fruits = this.physics.add.group();
    this.input.on('pointerdown', handleTap, this);

    this.popSound = this.sound.add('pop'); // 果物が消えるときの効果音
    this.appearSound = this.sound.add('appear'); // 果物が現れるときの効果音

    // 最初の果物を生成
    generateFruits.call(this, currentFruit);
}
  • this.popSound = this.sound.add('pop'): 果物が消えるときの効果音を設定します。
  • this.appearSound = this.sound.add('appear'): 果物が現れるときの効果音を設定します。

効果音を再生するタイミング

果物が消えるときと新しい果物が現れるときに、効果音を再生する処理を追加します。

function handleTap(pointer) {
    const children = fruits.getChildren();
    for (let i = children.length - 1; i >= 0; i--) {
        const fruit = children[i];
        if (Phaser.Geom.Rectangle.Contains(fruit.getBounds(), pointer.x, pointer.y)) {
            this.tweens.add({
                targets: fruit,
                scaleX: 0,
                scaleY: 0,
                duration: 200,
                ease: 'Power2',
                onComplete: () => {
                    fruit.destroy();
                    this.popSound.play(); // 果物が消えるときの効果音を再生
                    if (fruits.countActive(true) === 0) {
                        const nextIndex = (fruitTypes.indexOf(currentFruit) + 1) % fruitTypes.length;
                        currentFruit = fruitTypes[nextIndex];
                        generateFruits.call(this, currentFruit);
                    }
                }
            });
            break;
        }
    }
}

function generateFruits(type) {
    fruits.clear(true, true);

    // 果物ごとの背景色を設定
    switch (type) {
        case 'apple':
            this.cameras.main.setBackgroundColor('#ffbcbc');
            document.body.style.backgroundColor = '#ffbcbc';
            break;
        case 'grape':
            this.cameras.main.setBackgroundColor('#9ee3b2');
            document.body.style.backgroundColor = '#9ee3b2';
            break;
        case 'banana':
            this.cameras.main.setBackgroundColor('#fff4b3');
            document.body.style.backgroundColor = '#fff4b3';
            break;
        case 'melon':
            this.cameras.main.setBackgroundColor('#ffd1dc');
            document.body.style.backgroundColor = '#ffd1dc';
            break;
        case 'orange':
            this.cameras.main.setBackgroundColor('#ffe6b3');
            document.body.style.backgroundColor = '#ffe6b3';
            break;
    }

    for (let i = 0; i < 5; i++) {
        let x = Phaser.Math.Between(fruitSize, game.config.width - fruitSize);
        let y = Phaser.Math.Between(fruitSize, game.config.height - fruitSize);

        const fruit = fruits.create(x, y, type).setInteractive();

        const originalWidth = fruit.width;
        const scaleX = fruitSize / originalWidth;

        fruit.setScale(scaleX); // 横幅に合わせてスケールを設定

        this.tweens.add({
            targets: fruit,
            scaleX: scaleX,
            scaleY: scaleX,
            duration: 500,
            ease: 'Bounce.easeOut'
        });

        this.tweens.add({
            targets: fruit,
            duration: Phaser.Math.Between(4000, 6000),
            x: x + Phaser.Math.Between(-100, 100),
            y: y + Phaser.Math.Between(-100, 100),
            yoyo: true,
            repeat: -1,
            ease: 'Sine.easeInOut',
        });
    }

    if (isFirst) {
        isFirst = false;
        return;
    }

    this.appearSound.play(); // 新しい果物が現れるときの効果音を再生
}

画面リサイズへの対応

最後に、ゲーム画面がリサイズされたときに、果物のサイズが自動的に調整されるようにしましょう。これにより、どのデバイスでも適切なサイズでゲームが表示されるようになります。

リサイズ時の対応コード

リサイズイベントが発生したときに、果物のサイズを再設定します。

window.onresize = function() {
    game.scale.resize(window.innerWidth, window.innerHeight);
    resizeFruits();
};

function resizeFruits() {
    const fruitSize = Math.min(game.config.width, game.config.height) / 5;
    fruits.getChildren().forEach(fruit => {
        const scaleX = fruitSize / fruit.width;
        fruit.setScale(scaleX); // 横幅に合わせてスケールを再設定
    });
}
  • window.onresize: ウィンドウのサイズが変更されたときに呼び出されるイベントリスナーです。
  • resizeFruits: この関数では、現在のウィンドウサイズに合わせて果物のサイズを再計算し、適用します。

全体のコード

以下は、この章で完成した全体のコードです。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My First Game</title>
    <style>
        body {
            margin: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #add8e6;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
    <script>
        const config = {
            type: Phaser.AUTO,
            width: window.innerWidth,
            height: window.innerHeight,
            backgroundColor: '#add8e6',
            parent: document.body,
            scene: {
                preload: preload,
                create: create
            },
            physics: {
                default: 'arcade',
                arcade: {
                    gravity: { y: 0 },
                    debug: false
                }
            },
            scale: {
                mode: Phaser.Scale.RESIZE,
                autoCenter: Phaser.Scale.CENTER_BOTH
            }
        };

        const game = new Phaser.Game(config);

        let fruits;
        let isFirst = true;
        let currentFruit = 'apple';
        const fruitTypes = ['apple', 'grape', 'banana', 'melon', 'orange'];
        const fruitSize = Math.min(game.config.width, game.config.height) / 3;

        function preload() {
            this.load.image('apple', 'assets/apple.png');
            this.load.image('grape', 'assets/grape.png');
            this.load.image('banana', 'assets/banana.png');
            this.load.image('melon', 'assets/melon.png');
            this.load.image('orange', 'assets/orange.png');
            this.load.audio('pop', 'assets/pecho.mp3');
            this.load.audio('appear', 'assets/poincho.mp3');
        }

        function create() {
            fruits = this.physics.add.group();
            this.input.on('pointerdown', handleTap, this);

            this.popSound = this.sound.add('pop');
            this.appearSound = this.sound.add('appear');

            generateFruits.call(this, currentFruit);
        }

        function handleTap(pointer) {
            const children = fruits.getChildren();
            for (let i = children.length - 1; i >= 0; i--) {
                const fruit = children[i];
                if (Phaser.Geom.Rectangle.Contains(fruit.getBounds(), pointer.x, pointer.y)) {
                    this.tweens.add({
                        targets: fruit,
                        scaleX: 0,
                        scaleY: 0,
                        duration: 200,
                        ease: 'Power2',
                        onComplete: () => {
                            fruit.destroy();
                            this.popSound.play();
                            if (fruits.countActive(true) === 0) {
                                const nextIndex = (fruitTypes.indexOf(currentFruit) + 1) % fruitTypes.length;
                                currentFruit = fruitTypes[nextIndex];
                                generateFruits.call(this, currentFruit);
                            }
                        }
                    });
                    break;
                }
            }
        }

        function generateFruits(type) {
            fruits.clear(true, true);

            switch (type) {
                case 'apple':
                    this.cameras.main.setBackgroundColor('#ffbcbc');
                    document.body.style.backgroundColor = '#ffbcbc';
                    break;
                case 'grape':
                    this.cameras.main.setBackgroundColor('#9ee3b2');
                    document.body.style.backgroundColor = '#9ee3b2';
                    break;
                case 'banana':
                    this.cameras.main.setBackgroundColor('#fff4b3');
                    document.body.style.backgroundColor = '#fff4b3';
                    break;
                case 'melon':
                    this.cameras.main.setBackgroundColor('#ffd1dc');
                    document.body.style.backgroundColor = '#ffd1dc';
                    break;
                case 'orange':
                    this.cameras.main.setBackgroundColor('#ffe6b3');
                    document.body.style.backgroundColor = '#ffe6b3';
                    break;
                default:
                    this.cameras.main.setBackgroundColor('#ffbcbc');
                    document.body.style.backgroundColor = '#ffbcbc';
                    break;
            }

            for (let i = 0; i < 5; i++) {
                let x = Phaser.Math.Between(fruitSize, game.config.width - fruitSize);
                let y = Phaser.Math.Between(fruitSize, game.config.height - fruitSize);

                const fruit = fruits.create(x, y, type).setInteractive();

                const originalWidth = fruit.width;
                const scaleX = fruitSize / originalWidth;

                fruit.setScale(scaleX); // 横幅に合わせてスケールを設定

                this.tweens.add({
                    targets: fruit,
                    scaleX: scaleX,
                    scaleY: scaleX,
                    duration: 500,
                    ease: 'Bounce.easeOut'
                });

                this.tweens.add({
                    targets: fruit,
                    duration: Phaser.Math.Between(4000, 6000),
                    x: x + Phaser.Math.Between(-100, 100),
                    y: y + Phaser.Math.Between(-100, 100),
                    yoyo: true,
                    repeat: -1,
                    ease: 'Sine.easeInOut',
                });
            }

            if (isFirst) {
                isFirst = false;
                return;
            }

            this.appearSound.play(); // 新しい果物が現れるときの効果音を再生
        }

        window.onresize = function() {
            game.scale.resize(window.innerWidth, window.innerHeight);
            resizeFruits();
        };

        function resizeFruits() {
            const fruitSize = Math.min(game.config.width, game.config.height) / 5;
            fruits.getChildren().forEach(fruit => {
                const scaleX = fruitSize / fruit.width;
                fruit.setScale(scaleX); // 横幅に合わせてスケールを再設定
            });
        }
    </script>
</body>
</html>

動作確認をしてみよう

全体のコードが完成したら、実際にブラウザで動作を確認してみましょう。以下の点をチェックしてみてください。

  • ゲームが正しく表示されること。
  • 果物が画面に表示され、クリックまたはタップすると消えること。
  • すべての果物が消えると、次の果物が表示されること。
  • 果物が消えるときと現れるときに効果音が再生されること。
  • 画面サイズが変更されたときに、果物のサイズが適切にリサイズされること。

完成するゲーム「ぱくぱくフルーツ」はこちら▽

すべてが正常に動作していれば、これでゲームは完成です!