Phaser3 : Web Speech Apiを用いた読み上げ神経衰弱

今回はWeb Speech APIを用いた合成音声の読み上げをしてみます。
Web Speech APIはPhaser3独自の機能というわけではないのですが、Phaser3と組み合わせて使うことができるので、試しにこちらを使って読み上げ式の神経衰弱を作ってみようと思います。 f:id:Phaser_3:20181212165525p:plain

SpeechSynthesisを作る

合成音声の読み上げにはSpeechSynthesisSpeechSynthesisUtteranceの設定が最低限必要になります。
SpeechSynsethisで読み上げ用を行うオブジェクトを作成し、SpeechSynthesisUtteranceは読み上げの設定をするというようなイメージです。

//テキスト読み上げ用のSpeechSynthesisを作成
protected Voice: SpeechSynthesis;
protected VoiceText: SpeechSynthesisUtterance;

Web Speech APIはPhaserの機能ではないので、ブラウザから取得する必要があります。

create(){
~~
//PhaserではなくブラウザからSpeechSynthesisを取得
this.Voice = (<any>window).speechSynthesis;
this.VoiceText = new (<any>window).SpeechSynthesisUtterance("");

~~
}

SpeechSynthesisUtterrance.langで読み上げるテキストのロケールを選択することができます。
"ja-JP"を指定すれば日本語で読み上げることができます。

//読み上げる声のロケールを選択
this.VoiceText.lang = "ja-JP";

以上でWebSpeechSynthesisの設定ができました。
あとはSpeechSynthesisUtterance.textで読み上げるテキストを設定し、SpeechSynthesis.speakで設定したテキストを読み上げることができます。

//音声の再生
public Speech(str: string) {
    this.VoiceText.text = str;
    this.Voice.speak(this.VoiceText);

}

graphicsに当たり判定をつける

次はクリックされたときに音声を呼び出すタイルオブジェクトSpeechTileを作成します。
SpeechTileはcontainerを拡張して作ることにします。

export class SpeechTile extends Phaser.GameObjects.Container {

    public graphics: Phaser.GameObjects.Graphics;
    public myNum: number;

    constructor(scene: Phaser.Scene, x?: number, y?: number, children?:Phaser.GameObjects.GameObject[]) {
        super(scene, x, y, children);

    }
}

SpeechTileクラスに初期設定用の関数Initを作成します。
Initではgraphicsの描画に用いたrectを流用してgraphics.setInteractive(rect, Phaser.Geom.Rectangle.Contains);でgraphicsにクリック判定を設定しています。
昨日textにsetintractiveを設定したときは引数が不要でしたが、graphicsにはデフォルトではクリック判定が用意されていないのでrectを渡す必要があります。
this.graphics.setInteractiveは第一引数を設定した場合、第二引数のcallbackを設定しないとエラーが起きてしまうのでPhaser.Geom.Rectangle.Containsを設定しています。

export class SpeechTile extends Phaser.GameObjects.Container {
~~~
    public Init(mynumber: number, color: number) {
        //当たり判定と表示に使用する矩形を作成する
        let rect = new Phaser.Geom.Rectangle(0, 0, 32, 32);

        //グラフィックス作成
        this.graphics = this.scene.add.graphics();
        this.graphics.fillStyle(color);
        this.graphics.fillRectShape(rect);

        //コンテナにグラフィックスを追加
        this.add(this.graphics);

        //rectを範囲としたクリック判定
        this.graphics.setInteractive(rect, Phaser.Geom.Rectangle.Contains);
        this.myNum = mynumber;

        this.graphics.on('pointerdown', function () {
            let scene = this.scene as MyScene1;

            //mynumberの読み上げ
            scene.Speech(mynumber.toString());
            scene.judgeArray.push(this);
            scene.Judge();
        }, this);
    }
~~

    }

クリック時にはMyScene1のSpeechと判定用のJudge関数を呼び出しています。

        this.graphics.on('pointerdown', function () {
            let scene = this.scene as MyScene1;

            //mynumberの読み上げ
            scene.Speech(mynumber.toString());
            scene.judgeArray.push(this);
            scene.Judge();
        }, this);

MyScene1に戻り、判定用の配列と関数を作成します。
タイルをクリックすることでjudgeArrayにクリックされたタイルが格納され、配列の長さに応じてjudge関数が実行されます。

export class MyScene1 extends Phaser.Scene {
    //グラフィックオブジェクトを用意
    public graphics: Phaser.GameObjects.Graphics;

    //テキスト読み上げ用のSpeechSynthesisを作成
    protected Voice: SpeechSynthesis;
    protected VoiceText: SpeechSynthesisUtterance;

    //左用のタイルと右用のタイルを作成
    public lefts: SpeechTile[] = [];
    public rights: SpeechTile[] = [];

    public judgeArray: SpeechTile[] = [];


    constructor() {
        super({ key: 'MyScene1', active: false });
    }

    preload() {

    }

    create() {

        //タイルのために0から9までの数値をシャッフルした配列を作る
        let l: number[] = [];
        let r: number[] = [];

        for (let i = 0; i <= 9; i++) {
            l.push(i);
            r.push(i);
        }
        l = this.Shuffle(l);
        r = this.Shuffle(r);

        //背景を白に
        this.cameras.main.setBackgroundColor("#ffffff");
        //PhaserではなくブラウザからSpeechSynthesisを取得
        this.Voice = (<any>window).speechSynthesis;
        this.VoiceText = new (<any>window).SpeechSynthesisUtterance("");

        //読み上げる声のロケールを選択
        this.VoiceText.lang = "ja-JP";

        //SpeechTileを並べる
        for (let i = 0; i < 9; i++) {

            let left = new SpeechTile(this, this.sys.canvas.width/2 -60, 50 * i + 50);
            left.Init(l[i], 0x2a2a2a);
            this.add.existing(left);
            this.lefts.push(left);

            let right = new SpeechTile(this, this.sys.canvas.width / 2 + 60, 50 * i + 50);
            right.Init(r[i], 0x8a8a8a);
            this.add.existing(right);
            this.rights.push(right);
            right.graphics.input.enabled = false;
        }
    }

    //配列のシャッフル
    public Shuffle(array: number[]) {
        let ary = array.slice();
        for (let i = ary.length - 1; 0 < i; i--) {
            let r = Math.floor(Math.random() * (i + 1));
            // ary[i] <-> ary[r]
            [ary[i], ary[r]] = [ary[r], ary[i]];
        }
        return ary;
    }

    update() {

    }

    //音声の再生
    public Speech(str: string) {
        this.VoiceText.text = str;
        this.Voice.speak(this.VoiceText);

    }

    //神経衰弱の判定
    public Judge() {

        switch(this.judgeArray.length){
            case 1:
                for (let l of this.lefts) {
                    l.graphics.input.enabled = false;
                    if (l !== this.judgeArray[0]) {
                        l.Tween(0.3);
                    }
                }

                for (let r of this.rights) {
                    r.graphics.input.enabled = true;
                    r.Tween(1);
                }
                break;

            case 2:

                for (let l of this.lefts) {
                    l.graphics.input.enabled = true;
                    l.Tween(1);
                }

                for (let r of this.rights) {
                    r.graphics.input.enabled = false;
                    if (r !== this.judgeArray[1]) {
                        r.Tween(0.3);
                    }
                }

                if (this.judgeArray[0].myNum === this.judgeArray[1].myNum) {
                    this.lefts.splice(this.lefts.indexOf(this.judgeArray[0]),1)
                    this.rights.splice(this.rights.indexOf(this.judgeArray[1]), 1)
                    this.judgeArray[0].Clear();
                    this.judgeArray[1].Clear();
                }

                this.judgeArray = [];
                break;
        }


    }
}

以上で簡単な読み上げ式の神経衰弱ができました。
プレイしてみた動画がこちらです。

http://gpnote.tumblr.com/post/181039686538

今回のソース全文はこちらです。

/// <reference path="../app.ts" />

namespace MyGame {

export class SpeechTile extends Phaser.GameObjects.Container {

    public graphics: Phaser.GameObjects.Graphics;
    public myNum: number;

    constructor(scene: Phaser.Scene, x?: number, y?: number, children?:Phaser.GameObjects.GameObject[]) {
        super(scene, x, y, children);

    }

    public Init(mynumber: number, color: number) {
        //当たり判定と表示に使用する矩形を作成する
        let rect = new Phaser.Geom.Rectangle(0, 0, 32, 32);

        //グラフィックス作成
        this.graphics = this.scene.add.graphics();
        this.graphics.fillStyle(color);
        this.graphics.fillRectShape(rect);

        //コンテナにグラフィックスを追加
        this.add(this.graphics);

        //rectを範囲としたクリック判定
        this.graphics.setInteractive(rect, Phaser.Geom.Rectangle.Contains);
        this.myNum = mynumber;

        this.graphics.on('pointerdown', function () {
            let scene = this.scene as MyScene1;

            //mynumberの読み上げ
            scene.Speech(mynumber.toString());
            scene.judgeArray.push(this);
            scene.Judge();
        }, this);
    }

    //各種演出
    public Tween(n: number) {
        this.scene.add.tween({
            targets: this,
            alpha: n,
            duration:100
        })
    }

    public Clear() {
        this.scene.add.tween({
            targets: this,
            scaleX: 0,
            duration: 100,
            onComplete: () => {
                this.destroy();
            }
        })
    }
}



    export class MyScene1 extends Phaser.Scene {
        //グラフィックオブジェクトを用意
        public graphics: Phaser.GameObjects.Graphics;

        //テキスト読み上げ用のSpeechSynthesisを作成
        protected Voice: SpeechSynthesis;
        protected VoiceText: SpeechSynthesisUtterance;

        //左用のタイルと右用のタイルを作成
        public lefts: SpeechTile[] = [];
        public rights: SpeechTile[] = [];

        public judgeArray: SpeechTile[] = [];


        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        preload() {

        }

        create() {

            //タイルのために0から9までの数値をシャッフルした配列を作る
            let l: number[] = [];
            let r: number[] = [];

            for (let i = 0; i <= 9; i++) {
                l.push(i);
                r.push(i);
            }
            l = this.Shuffle(l);
            r = this.Shuffle(r);

            //背景を白に
            this.cameras.main.setBackgroundColor("#ffffff");
            //PhaserではなくブラウザからSpeechSynthesisを取得
            this.Voice = (<any>window).speechSynthesis;
            this.VoiceText = new (<any>window).SpeechSynthesisUtterance("");

            //読み上げる声のロケールを選択
            this.VoiceText.lang = "ja-JP";

            //SpeechTileを並べる
            for (let i = 0; i < 9; i++) {

                let left = new SpeechTile(this, this.sys.canvas.width/2 -60, 50 * i + 50);
                left.Init(l[i], 0x2a2a2a);
                this.add.existing(left);
                this.lefts.push(left);

                let right = new SpeechTile(this, this.sys.canvas.width / 2 + 60, 50 * i + 50);
                right.Init(r[i], 0x8a8a8a);
                this.add.existing(right);
                this.rights.push(right);
                right.graphics.input.enabled = false;
            }
        }

        //配列のシャッフル
        public Shuffle(array: number[]) {
            let ary = array.slice();
            for (let i = ary.length - 1; 0 < i; i--) {
                let r = Math.floor(Math.random() * (i + 1));
                // ary[i] <-> ary[r]
                [ary[i], ary[r]] = [ary[r], ary[i]];
            }
            return ary;
        }

        update() {

        }

        //音声の再生
        public Speech(str: string) {
            this.VoiceText.text = str;
            this.Voice.speak(this.VoiceText);

        }

        //神経衰弱の判定
        public Judge() {

            switch(this.judgeArray.length){
                case 1:
                    for (let l of this.lefts) {
                        l.graphics.input.enabled = false;
                        if (l !== this.judgeArray[0]) {
                            l.Tween(0.3);
                        }
                    }

                    for (let r of this.rights) {
                        r.graphics.input.enabled = true;
                        r.Tween(1);
                    }
                    break;

                case 2:

                    for (let l of this.lefts) {
                        l.graphics.input.enabled = true;
                        l.Tween(1);
                    }

                    for (let r of this.rights) {
                        r.graphics.input.enabled = false;
                        if (r !== this.judgeArray[1]) {
                            r.Tween(0.3);
                        }
                    }

                    if (this.judgeArray[0].myNum === this.judgeArray[1].myNum) {
                        this.lefts.splice(this.lefts.indexOf(this.judgeArray[0]),1)
                        this.rights.splice(this.rights.indexOf(this.judgeArray[1]), 1)
                        this.judgeArray[0].Clear();
                        this.judgeArray[1].Clear();
                    }

                    this.judgeArray = [];
                    break;
            }


        }
    }


}