Friction River Software

  • お問い合わせ

Phaser4入門【Phaser4基礎編①】生成AIへの指示

A子

よし!
それじゃ、記念すべき最初のゲームは何にしようか?

C菜

何でも良いですけど~

オーソドックスに「ブロック崩し」とかで良いんじゃないでしょうか~?

A子

マンネリ…っつーか、ワンパターンだけどそれで良いか(苦笑)

あ、でも一口に「生成AI」と言っても、色々あるよね
どれを使えば良いんだろ?

B美

そうねぇ

・OpenAIオープンエーアイの「ChatGPTチャットジーピーティー」
・Googleグーグルの「Geminiジェミニ」
・Microsoftマイクロソフトの「Copilotコパイロット」
・Anthropicアンソロピックの「Claudeクロード」

…ってところが有名どころかしら
(画像生成AIの分野だったらもっといっぱいあるけど…)

C菜

どれでも良いんですか~?

B美

多分だけど、どれを選んでもそれほど変わらないはずよ

まぁ、プログラミングに関して最も良いのは、(有料の)「Claudeクロード Codeコード」という噂だけどね
(使ったことがないから、よく分からん(苦笑))

A子

CakePHPのときは「ChatGPT」ばっかり使ってたから、今度は「Claude」にしてみようか
(もちろん、無料版だけどさ)

B美

OK

あ、「Claude」では無料会員登録するときにWindowsアプリを一緒にインストールすると便利よ
(わざわざログインする手間が省けるからね)

C菜

それ良いですね~

それでは使えるように準備します~



C菜

終わりました~
それではさっそく「Claude」に対して、以下のプロンプトを入れてみますね~

Phaser4を使ってブロック崩しのゲームを作りたい。
プロジェクト作成ツールを起動して、すでにひな形は作っている(「Web Bundler」→「vite」→「Complete」→「TypeScript」)。
「MainMenu.ts」や「Game.ts」に記述する内容を教えてください。

A子

30秒くらいで結果が表示されたよ
(早っ!)

B美

それじゃ、あなたたちで実装作業を行ってみなさい

まず新規のプロジェクトを作ることからね

C菜

了解です~
コマンドプロンプトを開いてから、以下のコードを入力しますね~

> cd /d d:\phaser[Enter]
> npm create @phaserjs/game@latest[Enter]

項目値
プロジェクト名block
1番目の選択肢Web Bundler
2番目の選択肢vite
3番目の選択肢Complete
4番目の選択肢TypeScript

上記はAIに指示した内容と一致させてます~

> cd block[Enter]
> npm install[Enter]

で完了です~



A子

ふむ
んじゃ、AIに教えてもらった通り、コード修正をやっていこう

まずは「D:\phaser\block\src\game\main.ts」だね

const config: Phaser.Types.Core.GameConfig = {
    type: AUTO,
    width: 1024,
    height: 768,
    parent: 'game-container',
    backgroundColor: '#028af8',
    scene: [
        Boot,
        Preloader,
        MainMenu,
        MainGame,
        GameOver
    ]
};

の部分を

const config: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 1024,
    height: 768,
    parent: 'game-container',
    backgroundColor: '#028af8',
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    physics: {
        default: 'arcade',
        arcade: { debug: false }
    },

    scene: [Boot, Preloader, MainMenu, MainGame, GameOver]
};

に変えたよ
(赤字部分が追加した箇所)

C菜

次は「D:\phaser\block\src\game\scenes\MainMenu.ts」です~
ここは丸ごと書き換えちゃいますね~

import { Scene } from 'phaser';

export class MainMenu extends Scene {
    constructor() {
        super('MainMenu');
    }

    create() {
        this.cameras.main.setBackgroundColor(0x111827);

        this.add.text(512, 280, 'BLOCK BREAKER', {
            fontFamily: 'Arial Black',
            fontSize: 72,
            color: '#ffffff'
        }).setOrigin(0.5);

        const guide = this.add.text(512, 450, 'クリック または スペースキーでスタート', {
            fontFamily: 'sans-serif',
            fontSize: 28,
            color: '#cbd5e1'
        }).setOrigin(0.5);

        // 点滅演出
        this.tweens.add({
            targets: guide,
            alpha: 0.2,
            duration: 700,
            yoyo: true,
            repeat: -1
        });

        this.input.once('pointerdown', () => this.scene.start('Game'));
        this.input.keyboard?.once('keydown-SPACE', () => this.scene.start('Game'));
    }
}

教えてもらったコードをコピペしただけですけどね~

A子

んじゃ、最後に「D:\phaser\block\src\game\scenes\Game.ts」を修正するよ

import { Scene } from 'phaser';

const WIDTH = 1024;
const HEIGHT = 768;
const PADDLE_W = 140;
const PADDLE_H = 20;
const BALL_R = 10;
const BALL_SPEED = 450;

export class Game extends Scene {
    private paddle!: Phaser.GameObjects.Rectangle;
    private ball!: Phaser.GameObjects.Arc;
    private ballBody!: Phaser.Physics.Arcade.Body;
    private bricks!: Phaser.Physics.Arcade.StaticGroup;
    private cursors?: Phaser.Types.Input.Keyboard.CursorKeys;
    private scoreText!: Phaser.GameObjects.Text;
    private livesText!: Phaser.GameObjects.Text;

    private score = 0;
    private lives = 3;
    private launched = false;
    private gameEnded = false;

    constructor() {
        super('Game');
    }

    create() {
        // シーン再開時の状態リセット
        this.score = 0;
        this.lives = 3;
        this.launched = false;
        this.gameEnded = false;

        this.cameras.main.setBackgroundColor(0x111827);

        // 下の壁だけ衝突なし(落下判定のため)
        this.physics.world.setBoundsCollision(true, true, true, false);

        // パドル
        this.paddle = this.add.rectangle(WIDTH / 2, HEIGHT - 60, PADDLE_W, PADDLE_H, 0xffffff);
        this.physics.add.existing(this.paddle);
        const paddleBody = this.paddle.body as Phaser.Physics.Arcade.Body;
        paddleBody.setImmovable(true);
        paddleBody.setCollideWorldBounds(true);

        // ボール
        this.ball = this.add.circle(WIDTH / 2, HEIGHT - 60 - PADDLE_H / 2 - BALL_R, BALL_R, 0xffd54a);
        this.physics.add.existing(this.ball);
        this.ballBody = this.ball.body as Phaser.Physics.Arcade.Body;
        this.ballBody.setCircle(BALL_R);
        this.ballBody.setBounce(1, 1);
        this.ballBody.setCollideWorldBounds(true);

        // ブロック
        this.createBricks();

        // 衝突
        this.physics.add.collider(this.ball, this.paddle, () => this.hitPaddle());
        this.physics.add.collider(this.ball, this.bricks, (_ball, brick) =>
            this.hitBrick(brick as Phaser.GameObjects.Rectangle)
        );

        // UI
        this.scoreText = this.add.text(20, 16, 'SCORE: 0', {
            fontFamily: 'Arial', fontSize: 24, color: '#ffffff'
        });
        this.livesText = this.add.text(WIDTH - 20, 16, 'LIVES: 3', {
            fontFamily: 'Arial', fontSize: 24, color: '#ffffff'
        }).setOrigin(1, 0);

        // 入力
        this.cursors = this.input.keyboard?.createCursorKeys();
        this.input.on('pointermove', (p: Phaser.Input.Pointer) => {
            if (!this.gameEnded) this.movePaddleTo(p.x);
        });
        this.input.on('pointerdown', () => this.launchBall());
        this.input.keyboard?.on('keydown-SPACE', () => this.launchBall());
    }

    update(_time: number, delta: number) {
        if (this.gameEnded) return;

        // キーボード操作
        if (this.cursors?.left.isDown) {
            this.movePaddleTo(this.paddle.x - 0.6 * delta);
        } else if (this.cursors?.right.isDown) {
            this.movePaddleTo(this.paddle.x + 0.6 * delta);
        }

        // 発射前はボールをパドルに追従
        if (!this.launched) {
            this.ball.x = this.paddle.x;
            this.ball.y = this.paddle.y - PADDLE_H / 2 - BALL_R;
            this.ballBody.reset(this.ball.x, this.ball.y);
        }

        // 落下判定
        if (this.launched && this.ball.y > HEIGHT + BALL_R) {
            this.loseLife();
        }
    }

    private createBricks() {
        this.bricks = this.physics.add.staticGroup();

        const cols = 10;
        const rows = 6;
        const w = 88;
        const h = 28;
        const gap = 8;
        const totalW = cols * w + (cols - 1) * gap;
        const startX = (WIDTH - totalW) / 2 + w / 2;
        const startY = 100;
        const colors = [0xef4444, 0xf97316, 0xeab308, 0x22c55e, 0x3b82f6, 0xa855f7];

        for (let r = 0; r < rows; r++) {
            for (let c = 0; c < cols; c++) {
                const x = startX + c * (w + gap);
                const y = startY + r * (h + gap);
                const brick = this.add.rectangle(x, y, w, h, colors[r]);
                this.bricks.add(brick);
            }
        }
    }

    private movePaddleTo(x: number) {
        const half = PADDLE_W / 2;
        this.paddle.x = Phaser.Math.Clamp(x, half, WIDTH - half);
    }

    private launchBall() {
        if (this.launched || this.gameEnded) return;
        this.launched = true;
        this.ballBody.setVelocity(Phaser.Math.Between(-150, 150), -BALL_SPEED);
    }

    // パドルのどこに当たったかで反射角を変える
    private hitPaddle() {
        const diff = (this.ball.x - this.paddle.x) / (PADDLE_W / 2); // -1 ~ 1
        const angle = Phaser.Math.Clamp(diff, -1, 1) * Phaser.Math.DegToRad(60);
        this.ballBody.setVelocity(Math.sin(angle) * BALL_SPEED, -Math.cos(angle) * BALL_SPEED);
    }

    private hitBrick(brick: Phaser.GameObjects.Rectangle) {
        brick.destroy();
        this.score += 10;
        this.scoreText.setText(`SCORE: ${this.score}`);

        if (this.bricks.countActive() === 0) {
            this.endGame(true);
        }
    }

    private loseLife() {
        this.lives--;
        this.livesText.setText(`LIVES: ${this.lives}`);

        if (this.lives <= 0) {
            this.endGame(false);
            return;
        }
        this.launched = false;
        this.ballBody.setVelocity(0, 0);
    }

    private endGame(cleared: boolean) {
        this.gameEnded = true;
        this.ballBody.setVelocity(0, 0);

        this.add.text(WIDTH / 2, HEIGHT / 2 - 20, cleared ? 'CLEAR!' : 'GAME OVER', {
            fontFamily: 'Arial Black', fontSize: 72, color: '#ffffff'
        }).setOrigin(0.5);

        this.add.text(WIDTH / 2, HEIGHT / 2 + 50, 'クリックでタイトルへ', {
            fontFamily: 'sans-serif', fontSize: 28, color: '#cbd5e1'
        }).setOrigin(0.5);

        // 連打で即遷移しないよう少し待つ
        this.time.delayedCall(500, () => {
            this.input.once('pointerdown', () => this.scene.start('MainMenu'));
        });
    }
}

めっちゃ長いけど、全部コピペね(苦笑)
(「Claude」さんの提示したコードそのまま)




C菜

それでは実行確認してみますね~

まずは「管理者モード」でコマンドプロンプトを開いて~
(Windowsの検索欄に以下を入力)

cmd[Ctrl]+[Shift]+[Enter]

以下のコマンドを打ち込んだら

> cd /d d:\phaser\block[Enter]
> npm run dev[Enter]

ブラウザで確認してみます~

http://localhost:8080

↓

A子

ありゃ?
画面が真っ暗なまま、何も出ないよ

B美

そういうときもAIに聞いてみなさい

きちんと教えてくれるから…

A子

『ブラウザで[F12]キーを押して開発者ツールを開いたあと、コンソールに出ているエラーを見ろ』
…って言われたよ

んで、それ(エラー内容)をAIに伝えたら、すぐに解決策を教えてくれた

C菜

何だったんですか~?

A子

「main.ts」と「Game.ts」を修正しろってさ

まず「D:\phaser\block\src\game\main.ts」だけど

行番号変更前変更後
5import { AUTO, Game } from 'phaser';import { AUTO, Game, Scale, Types } from 'phaser';
11type: Phaser.AUTO,type: AUTO,
17mode: Phaser.Scale.FIT,mode: Scale.FIT,
18autoCenter: Phaser.Scale.CENTER_BOTHautoCenter: Scale.CENTER_BOTH

…って感じで「Phaser.」を取れば良いらしい

あと「D:\phaser\block\src\game\scenes\Game.ts」のほうはもっと簡単で、ファイルの先頭行に以下の一文を入れるだけだった
(本当は「main.ts」と同様に「Phaser.」を削除したほうが良いみたいだけど、この一文があれば削除しなくても良いってことみたい)

import * as Phaser from 'phaser';


C菜

それではあらためて実行してみますね~

うわぁ、出ました~
すごいです~

メッセージが文字化けしてますけど、マウスでもキーボード(のカーソルキー)でも遊べましたよ~

↓

↓

A子

それ(文字化け)についてもAIに聞いてみた
文字コードを「シフトJIS」じゃなくて「UTF-8」にしろってさ

「D:\phaser\block\src\game\scenes\MainMenu.ts」と「D:\phaser\block\src\game\scenes\Game.ts」の二つだけ「UTF-8」に変更したらうまくいったよ
(超簡単!)

C菜

読者の皆さんにも見てほしいので、ビルドしてから我が社のWebサーバにアップしておきましょう~

> npm run build[Enter]

こちらをクリックしてくださいです~
(クリックすると別ウィンドウで開きます)

B美

【Phaser4】を使えば、ゲームがいかに簡単に作れるかってよくわかったでしょ?

次回は、ゲーム性を高める(より面白くする)ための改良を加えていきましょう

A子

OK

しっかし、ほんとに簡単だったよ