PhaserでTintアニメーションをするボタンを作る

最近プライベートではPhaserとTypescriptでWebブラウザゲームを開発しています。

Phaserの知見が溜まって来ているのでいつか放出したいのですが,今回は取り急ぎ表題の通り,クリックすると色がグラデーションで変わっていくボタンの作り方をメモします。 コピペで使えます。

実装

Prototype拡張でPhaser.Buttonにメソッドとプロパティを生やします。

// Button.ts
// 拡張メソッド

declare module Phaser {
    interface Button {
        _tintStep: number;
        _clickTintStart: number;
        _clickTintEnd: number;
        _clickTintDuration: number;
        _isClickTintEnabled: boolean;
        _isAddedTintCallback: boolean;
        _clickTintTweenToStart: Phaser.Tween;
        _clickTintTweenToEnd: Phaser.Tween;
        setClickTint(startColor: number, endColor: number, duration: number): void;
        setClickTintEnabled(enabled: boolean): void;
    }
}

// private

function removeTween(context: Phaser.Button) {
    if (context._clickTintTweenToStart != null) {
        context._clickTintTweenToStart.stop();
        context.game.tweens.remove(context._clickTintTweenToStart);
        context._clickTintTweenToStart = null;
    }
    if (context._clickTintTweenToEnd != null) {
        context._clickTintTweenToEnd.stop();
        context.game.tweens.remove(context._clickTintTweenToEnd);
        context._clickTintTweenToEnd = null;
    }
}

function addToEndTween(context: Phaser.Button): Phaser.Tween {
    return context.game.add.tween(context)
        .to({ _tintStep: 100 }, context._clickTintDuration * ((100 - context._tintStep) / 100), Phaser.Easing.Default, false)
        .onUpdateCallback(() => {
            context.tint = Phaser.Color.interpolateColor(context._clickTintStart, context._clickTintEnd, 100, context._tintStep, 1);
        }).start();
}

function addToStartTween(context: Phaser.Button): Phaser.Tween {
    return context.game.add.tween(context)
        .to({ _tintStep: 0 }, context._clickTintDuration * (context._tintStep / 100), Phaser.Easing.Default, false)
        .onUpdateCallback(() => {
            context.tint = Phaser.Color.interpolateColor(context._clickTintStart, context._clickTintEnd, 100, context._tintStep, 1);
        }).start();
}

Phaser.Button.prototype._tintStep = 0;
Phaser.Button.prototype._isAddedTintCallback = false;
Phaser.Button.prototype._isClickTintEnabled = false;
Phaser.Button.prototype._clickTintTweenToStart = null;
Phaser.Button.prototype._clickTintTweenToEnd = null;

Phaser.Button.prototype.setClickTint = function (startColor: number, endColor: number, duration: number) {
    this._clickTintStart = startColor; // 起点の色。InputUpした時点の色からこの色へ向かう
    this._clickTintEnd = endColor; // InputDownした時点の色からこの色へ向かう
    this._clickTintDuration = duration;

    if (this._isAddedTintCallback) {
        return;
    }
    this._isAddedTintCallback = true;

    this.onInputDown.add(() => {
        removeTween(this);

        if (this._isClickTintEnabled) {
            this._clickTintTweenToEnd = addToEndTween(this);
        }
    });

    this.onInputUp.add(() => {
        removeTween(this);

        if (this._isClickTintEnabled) {
            this._clickTintTweenToStart = addToStartTween(this);
        }
    });
};

Phaser.Button.prototype.setClickTintEnabled = function (enabled: boolean) {
    this._isClickTintEnabled = enabled;
    removeTween(this);
};

この拡張定義はエントリーポイントのファイルにインポートするなりよしなに読み込んでいただけると使えるようになります。 使い方は以下のような感じ。

// exsample.ts
const button = this.game.add.button(0, 0, "ResourceName");
button.tint = 0xffffff;
button.setClickTintEnabled(true);
button.setClickTint(0xffffff, 0x838383, 200);

実際の動作イメージ

f:id:azaraseal_7:20171210175515g:plain

補足

色の変化のStep数を適当に100としていますが,もっと細かく刻んでも問題ないと思います。Tintアニメーションが走っている最中にボタンから手を離しても,その状態の色から元の色に違和感なく戻っていきます。これは現在どのステップまでアニメーションが進んでいるかを自身のインスタンスに記録しておくことで,ちゃんと元に戻るまでの適切なdurationがTweenを作成されるたびに計算されるためです。

Tweenのインスタンスを使いまわせたら一番良いのですが,うまくいきませんでした。

当然今回定義した関数やプロパティにユーザは自由にアクセスできてしまうので,扱いには少し注意が必要です。

Phaserについて思うこと

Phaserはかなり多くの機能を提供してくれるゲームエンジンなのですが,こういう凝ったUIを作ろうとすると,かなり苦労する印象です。スクロールビューとリストビューあたりも,デフォルトでは提供されておらず,ライブラリもあることはあるのですが,かなり微妙なのでこれからもPhaserを使っていくとしたら自作の機運があります。。。