🏕

Phaser3 BitmapTextの基礎

に公開

タイトルイメージ

主な改定履歴

  • 2026/01/13 「独自のゲームオブジェクトの作成」を追加
  • 2026/01/07 新規公開

はじめに

BitmapTextクラスおよびDynamicBitmapTextクラスについて、その使い方を解説します。

ビットマップテキストとは、文字の画像(ビットマップフォント)を取り込むと、その画像を使って文章を表示できる機能です。
なお、この記事で使用している画像はGeminiで生成しています。

Phaser API Documentation | Phaser.GameObjects.BitmapText
Phaser API Documentation | Phaser.GameObjects.DynamicBitmapText

デモのソースコード(GitHub)

https://github.com/phaser-hiro/phaser-demo-bitmaptext

StackBlitzの使い方

この記事のソースはStackBlitzでも確認できます。
画面右下の Editor をクリックし、左上のハンバーガーメニューからソースが参照できます。

レトロフォント

レトロフォントは、1文字当たりの画像のサイズ(縦×横の長さ)が固定のスプライトシートを渡して、文字を表示する機能です。
必要なものは画像ファイルのみでXMLファイルは不要です。

まず、画像は通常と同じようにthis.load.image()で読み込みます。
RetroFontConfigのオブジェクトを用意して、そこに必要な値を渡します。
渡す値は以下の通り:

  • image:読み込み済みの画像のテクスチャ名
  • width,height:画像一文字の横と縦のサイズ
  • chars:画像内にある文字列。画像の左上から一行ずつ、すべての文字を記載する。
    (一部の既知の文字列については、Phaser.GameObjects.RetroFontネームスペースに文字列の定数が用意してある)
  • charsPerRow:画像内の一行当たりの文字数(行数の指定は不要)
  • lineSpacing:改行時の垂直スペース

Phaser.GameObjects.RetroFont.Parse()メソッドで用意したconfig変数をBitmapFontData
に変換するとともにthis.cache.bitmapFont.add()メソッドでキャッシュに追加します。
最後に、作成したフォント名を指定してthis.add.bitmapText()メソッドで文章を表示します。

// (一部を抜粋)
preload() {
    this.load.setBaseURL('assets');
    // 樹木のフォント
    this.load.image('oldtreefont', 'bitmapfonts/oldtreefont.png');
}

private displayRetroFont(): void {
    const { width: cW, height: cH } = this.game.canvas;

    const FONTNAME = 'oldtreefont';
    const TEXT_SET = `!"#$%&'()*+,-./0123456789:;<=>?@   ABCDEFGHIJKLMNOPQRSTUVWXYZ`;

    const CAPTION = 'HELLO!\nPHASER3 WORLD'; // 表示文字列

    // 樹木のフォント
    this.add.rectangle(0, 0, cW, cH, 0x102028).setOrigin(0); // 背景
    const config = {
        image: FONTNAME,
        width: 64,
        height: 100,
        chars: TEXT_SET,
            //(一部の既知の文字列は`Phaser.GameObjects.RetroFont`ネームスペースに文字列の定数が用意してある)
        charsPerRow: 7
    } as Phaser.Types.GameObjects.BitmapText.RetroFontConfig;
    this.cache.bitmapFont.add(FONTNAME, Phaser.GameObjects.RetroFont.Parse(this, config));
    this.add.bitmapText(50, 160, FONTNAME, CAPTION);
}

ビットマップテキスト

ビットマップテキストを使うにはまずビットマップフォントを作る必要があります。

ビットマップフォントの作り方

  • 既存のFTT等ファイルから作成する場合
    SnowBamboo BMFという、ビットマップフォント生成ツールを使用します。
    使い方解説記事
    事前に文字ごとの画像を用意しておくと、その画像を使って作成することも可能(ただしベースラインの位置等は自分で調整が必要)。

  • 既にビットマップフォントのスプライトシートがある場合。
    https://renderhjs.net/shoebox/
    ビットマップフォントのスプライトシートを渡すと、その配置を基準にベースラインを揃えたデータでXMLファイルの出力が可能です。
    使い方解説記事

基本的な使い方

シーンのpreload()メソッド内で、this.load.bitmapFont()メソッドを使って、XMLと画像の読み込みを行います。
シーンのcreated()メソッド以降で、this.add.bitmapText()メソッドを使って、画面への表示を行います。

// (一部を抜粋)
preload() {
    this.load.setBaseURL('assets');
    // キャンディーのフォント
    this.load.bitmapFont('candy', 'bitmapfonts/candy.png', 'bitmapfonts/candy.xml');
}

private displayBitmapFont(): void {
    const { width: cW, height: cH } = this.game.canvas;

    const FONTNAME = 'candy';
    const CAPTION = '$12345\n4567#\n67% 890%'; // 表示文字列

    // キャンディーのフォント
    this.add.rectangle(0, 0, cW, cH, 0xFAD8D4).setOrigin(0); // 背景
    this.add.bitmapText(50, 80, FONTNAME, CAPTION);
}

ダイナミックビットマップテキスト

ビットマップテキストは文字の表示だけでしたが、ダイナミックビットマップテキストを使うと文字に動きを付けることができます。

ここからは動きを付けた例と、そのコードの書き方を紹介します。

波の演出

文字の行が波打つ演出です。

this.add.dynamicBitmapText()メソッドでビットマップテキストのゲームオブジェクトをシーンに追加した後、setDisplayCallback()を呼び出して、動きのロジックを書いたアロー関数を渡します。
このアロー関数は、ビットマップテキストの描画が行われるたびに、文字ごとに呼ばれるコールバック関数です。
仮引数としてDisplayCallbackConfigのオブジェクトを受け取り、これを文字の表示の計算に使用します。計算した値は仮引数に代入し戻り値としてreturnに渡す必要があります。

DisplayCallbackConfig型の保有する主な値は以下の通り:

  • index:当該文字の文字列の先頭(0)からの番号。
  • charCode:当該文字のcharコード。
  • x,y:当該文字の位置。
  • scale:当該文字のスケール。
  • rotation:当該文字の角度。

なお、各文字の初期位置はビットマップテキストのゲームオブジェクトで保持していないので、保持用の配列を自前で用意して初回の呼び出し時にその値を代入しています。

// (一部を抜粋)
preload() {
    this.load.setBaseURL('assets');
    // 缶フォント
    this.load.bitmapFont('canfont', 'bitmapfonts/canfont.png', 'bitmapfonts/canfont.xml');
}

private displayWavingMotion(): void {
    type Pos = Phaser.Types.Math.Vector2Like;

    // 定数
    const SPEED = 4;         // スピードを1~50くらいで指定。値が大きいほど速い。
    const ANGLE_STEP_X = 30; // 文字間の周期差(X)。-180~180の範囲で指定。
    const ANGLE_STEP_Y = 30; // 文字間の周期差(Y)。-180~180の範囲で指定。
    const RADIUS_X = 20;     // 横のふり幅
    const RADIUS_Y = 15;     // 縦のふり幅

    const CAPTION = 'THESE ARE\nNICE CANNED DRINKS'; // 表示文字列

    // 缶フォント
    const { width: cW, height: cH } = this.game.canvas;
    this.add.rectangle(0, 0, cW, cH, 0xC0F0FF).setOrigin(0); // 背景水色
    const canText = this.add.dynamicBitmapText(30, 200, 'canfont', CAPTION).setScale(0.7);
    const charPos: Array<Pos> = Array.from({ length: CAPTION.length });
    let curSp = 0;
    canText.setDisplayCallback((data: Phaser.Types.GameObjects.BitmapText.DisplayCallbackConfig) => {
        if (!charPos[data.index]) {
            // 初回呼び出し時に文字位置の値を記録
            charPos[data.index] = { x: data.x, y: data.y } as Pos;
        }
        curSp += SPEED / 1000;
        const angX = data.index * Phaser.Math.DegToRad(ANGLE_STEP_X) + curSp;
        const angY = data.index * Phaser.Math.DegToRad(ANGLE_STEP_Y) + curSp;
        data.x = charPos[data.index].x + Math.cos(angX) * RADIUS_X;
        data.y = charPos[data.index].y + Math.sin(angY) * RADIUS_Y;
        if (curSp >= Math.PI * 2) {
            curSp -= Math.PI * 2;
        }
        return data;
    });
}

拡大と縮小

各文字がその文字の中央を軸として拡大と縮小を繰り返す演出です。

先ほどと同様に各文字の初期位置を配列に保存します。
ここでのポイントはscaleを変えるとその文字だけでなく座標体系全体の縮尺も変わるので、位置の計算の際にはスケールの値で商算(/ scale)しています。

また、文字の幅の値をfontDataプロパティから取得しています。

// (一部を抜粋)
preload() {
    this.load.setBaseURL('assets');
    // 岩石のフォント
    this.load.bitmapFont('rockdigits', 'bitmapfonts/rockdigits.png', 'bitmapfonts/rockdigits.xml');
}

private displayScalingMotion(): void {
    type Pos = Phaser.Types.Math.Vector2Like;

    // 定数
    const SPEED = 4;        // スピードを1~50くらいで指定。値が大きいほど速い。
    const MIN_SCALE = 0.5;  // スケールの最小値
    const MAX_SCALE = 1.1;  // スケールの最大値
    const ANGLE_STEP = -30; // 文字間の周期差。-180~180の範囲で指定。

    const CAPTION = '1234567\n890'; // 表示文字列

    // 岩石のフォント
    const { width: cW, height: cH } = this.game.canvas;
    this.add.grid(0, 0, cW, cH, 50, 50, 0x000000, 1, 0xFFFFFF).setOrigin(0); // 背景:碁盤目
    const rockText = this.add.dynamicBitmapText(50, 100, 'rockdigits', CAPTION).setOrigin(0);
    const textRect = rockText.getBounds();
    this.add.rectangle(textRect.left, textRect.top, textRect.width, textRect.height, 0xFFFFFF, 0.5).setOrigin(0);
    this.children.bringToTop(rockText);

    const charPos: Array<Pos> = Array.from({ length: CAPTION.length });
    let curSp = 0;
    rockText.setDisplayCallback((data: Phaser.Types.GameObjects.BitmapText.DisplayCallbackConfig) => {
        if (!charPos[data.index]) {
            // 初回呼び出し時に文字位置の値を記録
            charPos[data.index] = { x: data.x, y: data.y } as Pos;
        }
        const char: Phaser.Types.GameObjects.BitmapText.BitmapFontCharacterData
            = rockText.fontData.chars[data.charCode]
        curSp += SPEED / 1000;
        const ang = data.index * Phaser.Math.DegToRad(ANGLE_STEP) + curSp;
        const scale = (Math.sin(ang) + 1) / 2 * (MAX_SCALE - MIN_SCALE) + MIN_SCALE;
        data.scale = scale;
        const posXBase = charPos[data.index].x / scale;
        const halfCharW = (char.width / 2) / scale;
        data.x = posXBase + halfCharW * (1 - scale);
        data.y = charPos[data.index].y / scale +
            ((rockText.fontData.lineHeight / 2) / scale) * (1 - scale);
        if (curSp >= Math.PI * 2) {
            curSp -= Math.PI * 2;
        }
        return data;
    });
}

フォローパス

カーブのデータを用意して、その線に沿って文字を移動させる演出です。

// (一部を抜粋)
preload() {
    this.load.setBaseURL('assets');
    // チェックポイントフォント
    this.load.bitmapFont('cp-font', 'bitmapfonts/cp-font.png', 'bitmapfonts/cp-font.xml');
}

private displayTextFollowPath() {
    const { width: cW, height: cH } = this.game.canvas;
    const BASE_X = 500;
    const SPEED = 0.001;

    // パスの生成
    const path = new Phaser.Curves.Path(cW, BASE_X);
    path.lineTo(cW - 200, 500);

    const pointArray = [
        cW - 200, 100,
        cW / 2, 300,
        200, 100,
        200, BASE_X,
    ];
    const vecArray = new Array<Phaser.Math.Vector2>();
    for (let i = 0; i < pointArray.length; i += 2) {
        vecArray.push(new Phaser.Math.Vector2(pointArray[i], pointArray[i + 1]));
    }

    path.splineTo(vecArray);
    path.lineTo(-80, BASE_X);

    const FONTNAME = 'cp-font';
    const CHAR_PADDING = 8;   // 文字間隔 (xmlにxadvanceがあるがdynamicBitmapTextでは保持していないため、文字間隔を固定値として設定)
    const CAPTION = '新しい朝が来た!希望の朝だ!'; // 表示文字列
    const text = this.add.dynamicBitmapText(0, 0, FONTNAME, CAPTION);

    const charW: Array<number> = Array.from({ length: CAPTION.length });

    let step = 0.5;
    text.setDisplayCallback((data: Phaser.Types.GameObjects.BitmapText.DisplayCallbackConfig) => {
        if (!charW[data.index]) {
            // 各文字の横幅を取得して保持する
            const char: Phaser.Types.GameObjects.BitmapText.BitmapFontCharacterData
                = text.fontData.chars[data.charCode];
            charW[data.index] = char.width
        }

        if (data.index === 0) {
            step += SPEED;
            if (step >= 1) {
                step -= 1;
            }
        }

        let shift = 0;
        if (0 < data.index) {
            // 2文字目以降はそれまでの文字の幅を加算して、先頭位置から後ろへずらす
            const sum = charW.slice(0, data.index).reduce((acc, curr) => acc + (curr ?? 0) + CHAR_PADDING, 0);
            shift = sum / path.getLength();
        }
        const charStep = step - shift;
        const pos = path.getPoint(charStep - Math.floor(charStep));
        if (pos) {
            data.x = pos.x;
            data.y = pos.y;
        }
        return data;
    });

    // 線と点の描画
    const graphics = this.add.graphics();
    graphics.lineStyle(1, 0xffffff, 1);
    path.draw(graphics, 128);
    graphics.fillStyle(0xff0000, 1);
    vecArray.forEach(e => graphics.fillCircle(e.x, e.y, 5));

    // 文字を最前面へ
    this.children.bringToTop(text);
}

独自のゲームオブジェクトの作成

上で作成したたダイナミックビットマップテキストをモジュール化し、独自のゲームオブジェクトとして扱う方法を紹介します。
これにより、this.add.sprite()と同じ記述方法でシーン内で扱うことができます。

具体的には、GameObjectFactoryクラスにregisterメソッドがありますので、これを呼び出します。

なお、TypeScriptではビルドツールのツリーシェイキング(Tree Shaking)機能により、importしてもモジュールが実行されない場合があるので、静的メソッドのinitを作成し、シーンのinit()で明示的に呼び出すようにしています。

デモのソースコード(GitHub)

https://github.com/phaser-hiro/phaser-demo-bitmaptext-part2

// (一部を抜粋)
import { WavingMotionConfig, DBTWavingMotion } from '../gameobjects/DBTWavingMotion';

export default class Game extends Phaser.Scene {

    init() {
        DBTWavingMotion.init();
    }

    create() {
        // ダイナミックビットマップテキスト ≪波の演出≫
        const wavingConfig: WavingMotionConfig = {
            speed: 8,       // スピードを1~50くらいで指定。値が大きいほど速い。
            angleStepX: 30, // 文字間の周期差(X)。-180~180の範囲で指定。
            angleStepY: 30, // 文字間の周期差(Y)。-180~180の範囲で指定。
            radiusX: 20,    // 横のふり幅
            radiusY: 15     // 縦のふり幅
        }
        this.add.DBTWavingMotion(50, 100, 'canfont', 'ABCDEFG', wavingConfig);
    }
}

Discussion