Next2Dでゲームを作ってみる(中盤)

# 画面を作る
本来はデザインがあり、それをViewに落とし込んでいくのが一般的なフローだとは思うのですが、僕はデザイン力がないので、行き当たりでViewを作っていきます。前日の記事でも書きましたが、見た目は最後に整えるので今日は画面の配置を優先して製作して行こうと思います。

## TopViewModel
NoCodeToolでシンボルとラベルを設定して、動的にオープンニングのMovieClipを生成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80502/58de3bd2-4515-90c7-e124-7398dbbebaf1.png" width="600">

### OpeningContent
シンボルから動的に生成する為の準備

```javascript:src/content/OpeningContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class OpeningContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        // シンボルを呼び出せるようNoCode Toolと命名を合わせる
        return "Opening";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        // どのJSONか判別できるよう、routing.jsonのnameを設定する
        return "OpeningContent";
    }
}
```

### TopViewModel
```javascript:src/top/TopViewModel.js
import { OpeningContent } from "../../content/OpeningContent";

/**
 * @class
 * @extends {next2d.fw.ViewModel}
 */
export class TopViewModel extends next2d.fw.ViewModel
{
    /**
     * @param {next2d.fw.View} view
     * @constructor
     * @public
     */
    constructor (view)
    {
        super(view);
    }

    /**
     * @param  {next2d.fw.View} view
     * @return {void}
     * @abstract
     */
    bind (view)
    {
        return new Promise((resolve) =>
        {
            // オープニングのMovieClipを動的に生成
            const content = view.addChild(new OpeningContent());
            content.name  = "opening";

            // ラベルをつけたフレーム番号を次の処理に引き継ぐ
            const labels = content.currentLabels;
            for (let idx = 0; labels.length > idx; ++idx) {
                const label = labels[idx];
                if (label.name === "end") {
                    resolve(label.frame);
                }
            }
        })
            .then((frame) =>
            {
                const { Sprite }     = next2d.display;
                const { MouseEvent } = next2d.events;

                // 透明なタップエリアを生成
                const sprite = view.addChild(new Sprite());
                const stage  = this.config.stage;
                sprite
                    .graphics
                    .beginFill(0, 0)
                    .drawRect(0, 0, stage.width, stage.height);

                // PC版でマウスがポインターに切り替わるように設定をONにする
                sprite.buttonMode = true;

                // マウスアップもしくはタップエンドでイベント発火
                sprite.addEventListener(MouseEvent.MOUSE_UP, (event) =>
                {
                    const parent = event.currentTarget.parent;
                    if (frame > parent.opening.currentFrame) {

                        // フレームがラベル位置前であればラベル位置まで移動
                        parent.opening.gotoAndPlay("end");

                    } else {

                        // HomeViewに移動
                        this.app.gotoView("home");

                    }
                });
            });
    }
}
```

### View
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80502/83a6e48b-3e06-0ae8-9313-eaac702fd949.gif)

## HomeViewModel

```javascript:src/home/HomeViewModel.js
/**
 * @class
 * @extends {next2d.fw.ViewModel}
 */
export class HomeViewModel extends next2d.fw.ViewModel
{
    /**
     * @param {next2d.fw.View} view
     * @constructor
     * @public
     */
    constructor (view)
    {
        super(view);
    }

    /**
     * @param  {next2d.fw.View} view
     * @return {void}
     * @abstract
     */
    bind (view)
    {
        return new Promise((resolve) =>
        {
            const { Sprite } = next2d.display;
            const { MouseEvent } = next2d.events;
            const { TextField, TextFieldAutoSize, TextFormat } = next2d.text;

            const parent = view.addChild(new Sprite());

            let positionX = 0;
            let positionY = 0;

            // ゲームで得点加算される倍数の値、2から9までの選択肢を表示
            const numbers = this.config.game.numbers;
            for (let idx = 0; numbers.length > idx; ++idx) {

                const number = numbers[idx];

                // 半分で改行
                if (numbers.length / 2 === idx) {
                    positionX  = 0;
                    positionY += 60;
                }

                const sprite = parent.addChild(new Sprite());

                // ボタンの角丸矩形を生成
                sprite
                    .graphics
                    .beginFill("#e6159c")
                    .drawRoundRect(0, 0, 50, 50, 10);

                sprite.x = positionX;
                sprite.y = positionY;

                const textFormat = new TextFormat();
                textFormat.font  = "Arial";
                textFormat.size  = 20;
                textFormat.bold  = true;

                // 倍数をテキストとして表示
                const textField = new TextField();
                textField.defaultTextFormat = textFormat;

                textField.name     = "number";
                textField.autoSize = TextFieldAutoSize.CENTER;
                textField.text     = `${number}`;

                textField.x = (sprite.width  - textField.width)  / 2 - 1;
                textField.y = (sprite.height - textField.height) / 2;

                sprite.addChild(textField);

                sprite.buttonMode = true;
                sprite.addEventListener(MouseEvent.MOUSE_UP, (event) =>
                {
                    const target = event.currentTarget;

                    // 選択した倍数をQueryStringとしてゲームに引き継ぐ
                    this.app.gotoView(`game/play?number=${target.number.text}`);
                });

                positionX += sprite.width + 10;
            }

            parent.x = (this.config.stage.width  - parent.width)  / 2;
            parent.y = (this.config.stage.height - parent.height) / 2;

            resolve();
        });
    }
}
```

### View
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80502/543d8f17-6c68-ca3e-96c6-d571ddba0728.png" width="300">

## GamePlayViewModel
ランダムに四色のボタンを配置、数字も1から9の数字をランダムに表示し、ゲームロジックを適用する。
ゲームの時間を60秒とし、タイムゲージを1秒づつ減算するよう演出、パズルが指定の倍数になるとタイムバーが回復するように設定

### BlueButtonContent
ボタンをシンボルから動的に生成する為の準備

```javascript:src/content/BlueButtonContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class BlueButtonContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        return "BlueButton";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        return "MaterialContent";
    }
}
```

### GreenButtonContent
ボタンをシンボルから動的に生成する為の準備

```javascript:src/content/GreenButtonContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class GreenButtonContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        return "GreenButton";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        return "MaterialContent";
    }
}
```

### RedButtonContent
ボタンをシンボルから動的に生成する為の準備

```javascript:src/content/RedButtonContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class RedButtonContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        return "RedButton";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        return "MaterialContent";
    }
}
```

### YellowButtonContent
ボタンをシンボルから動的に生成する為の準備

```javascript:src/content/YellowButtonContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class YellowButtonContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        return "YellowButton";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        return "MaterialContent";
    }
}
```

### TimeBarContent
タイムバーをシンボルから動的に生成する為の準備

```javascript:src/content/TimeBarContent.js
/**
 * @class
 * @extends {next2d.fw.Context}
 */
export class TimeBarContent extends next2d.fw.Content
{
    /**
     * @constructor
     * @public
     */
    constructor ()
    {
        super();
    }

    /**
     * @return {string}
     */
    get namespace ()
    {
        return "TimeBar";
    }

    /**
     * @return {string}
     */
    get contentName ()
    {
        return "MaterialContent";
    }
}
```

### GamePlayViewModel

```javascript:src/game/GamePlayViewModel.js
import { RedButtonContent } from "../../content/RedButtonContent";
import { GreenButtonContent } from "../../content/GreenButtonContent";
import { BlueButtonContent } from "../../content/BlueButtonContent";
import { YellowButtonContent } from "../../content/YellowButtonContent";
import { TimeBarContent } from "../../content/TimeBarContent";

/**
 * @class
 * @extends {next2d.fw.ViewModel}
 */
export class GamePlayViewModel extends next2d.fw.ViewModel
{
    /**
     * @param {next2d.fw.View} view
     * @constructor
     * @public
     */
    constructor (view)
    {
        super(view);

        // ランダムにボタンを生成する為の配列
        this.pieceList = [
            RedButtonContent,
            GreenButtonContent,
            BlueButtonContent,
            YellowButtonContent
        ];
    }

    /**
     * @param  {next2d.fw.View} view
     * @return {void}
     * @abstract
     */
    bind (view)
    {
        return new Promise((resolve) =>
        {
            const { Sprite } = next2d.display;
            const { MouseEvent } = next2d.events;

            const sprite = view.addChild(new Sprite());

            let positionX = 0;
            let positionY = 0;

            const pieceCount = this.config.game.piece;
            for (let idx = 0; idx < pieceCount; ++idx) {

                for (let idx = 0; idx < pieceCount; ++idx) {

                    const PieceClass = this.pieceList[Math.floor(Math.random() * 4)];
                    const piece = sprite.addChild(new PieceClass());

                    piece.number.text = Math.floor(Math.random() * 8) + 1;

                    piece.x = positionX;
                    piece.y = positionY;

                    piece.buttonMode = true;
                    piece.addEventListener(MouseEvent.MOUSE_DOWN, (event) =>
                    {
                        const target = event.currentTarget;
                        const parent = target.parent;
                        parent.setChildIndex(target, parent.numChildren - 1);

                        target.setLocalVariable("startMouseX", target.mouseX);
                        target.setLocalVariable("startMouseY", target.mouseY);

                        target.addEventListener(MouseEvent.MOUSE_MOVE, (event) =>
                        {
                            const target = event.currentTarget;
                            target.setLocalVariable("moveMouseX", target.mouseX);
                            target.setLocalVariable("moveMouseY", target.mouseY);
                        });

                        piece.addEventListener(MouseEvent.MOUSE_UP, (event) =>
                        {
                            const target = event.currentTarget;
                            target.removeAllEventListener(MouseEvent.MOUSE_UP);
                            target.removeAllEventListener(MouseEvent.MOUSE_MOVE);
                        });
                    });

                    positionX += piece.width + 10;
                }

                positionX  = 0;
                positionY += 125;
            }

            sprite.removeChild(
                sprite.getChildAt(sprite.numChildren - 1)
            );

            sprite.scaleX = sprite.scaleY = 0.4;
            sprite.x = (this.config.stage.width  - sprite.width)  / 2;
            sprite.y = (this.config.stage.height - sprite.height) / 2;

            resolve();
        })
            .then(() =>
            {
                return new Promise((resolve) =>
                {
                    // スコア数値
                    const { TextField, TextFieldAutoSize, TextFormat } = next2d.text;

                    const textFormat = new TextFormat();
                    textFormat.font  = "Arial";
                    textFormat.size  = 40;
                    textFormat.bold  = true;
                    textFormat.color = "#ffffff";

                    const textField = new TextField();
                    textField.defaultTextFormat = textFormat;

                    textField.name      = "score";
                    textField.autoSize  = TextFieldAutoSize.CENTER;
                    textField.thickness = 3;
                    textField.text      = "999999";

                    textField.x = (this.config.stage.width - textField.width) / 2;
                    textField.y = 40;

                    view.addChild(textField);

                    resolve();
                });
            })
            .then(() =>
            {
                return new Promise((resolve) =>
                {
                    // タイムバー
                    const content = view.addChild(new TimeBarContent());

                    content.scaleX = content.scaleY = 0.8;
                    content.x = (this.config.stage.width - content.width) / 2;
                    content.y = 110;

                    // 60秒で終了するよう設定
                    const end = content.width;
                    const sub = end / this.config.game.timeLimit;
                    const timerId = setInterval(function ()
                    {
                        content.bar.x -= sub;
                        if (Math.abs(content.bar.x) > end) {
                            // ゲーム終了
                            clearInterval(timerId);
                            this.app.gotoView(`game/result?score=${view.score.text}`);
                        }
                    }.bind(this), 1000);

                    resolve();
                });
            });
    }
}
```

### View
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80502/9f7e347e-141c-d19c-b4b6-23fd08b041df.png" width="300">

## GameResultViewModel
スコアを表示し、Homeに戻るボタンを設置

```javascript:src/game/GameResultViewModel.js
/**
 * @class
 * @extends {next2d.fw.ViewModel}
 */
export class GameResultViewModel extends next2d.fw.ViewModel
{
    /**
     * @param {next2d.fw.View} view
     * @constructor
     * @public
     */
    constructor (view)
    {
        super(view);
    }

    /**
     * @param  {next2d.fw.View} view
     * @return {Promise|void}
     * @abstract
     */
    bind (view)
    {
        return new Promise((resolve) =>
        {
            const { TextField, TextFieldAutoSize, TextFormat } = next2d.text;

            const textFormat = new TextFormat();
            textFormat.font  = "Arial";
            textFormat.size  = 40;
            textFormat.bold  = true;
            textFormat.color = "#ffffff";

            const textField = new TextField();
            textField.defaultTextFormat = textFormat;
            textField.autoSize  = TextFieldAutoSize.CENTER;
            textField.thickness = 3;
            textField.text      = "Score";

            textField.x = (this.config.stage.width  - textField.width)  / 2;
            textField.y = 80;

            view.addChild(textField);

            resolve();
        })
            .then(() =>
            {
                return new Promise((resolve) =>
                {
                    // スコアを受け取って表示
                    const { TextField, TextFieldAutoSize, TextFormat } = next2d.text;

                    const textFormat = new TextFormat();
                    textFormat.font  = "Arial";
                    textFormat.size  = 40;
                    textFormat.bold  = true;
                    textFormat.color = "#ffffff";

                    const textField = new TextField();
                    textField.defaultTextFormat = textFormat;

                    textField.autoSize  = TextFieldAutoSize.CENTER;
                    textField.thickness = 3;
                    textField.text      = this.query.get("score");

                    textField.x = (this.config.stage.width  - textField.width)  / 2;
                    textField.y = 180;

                    view.addChild(textField);

                    resolve();
                });
            })
            .then(() =>
            {
                return new Promise((resolve) =>
                {
                    // Homeに移動するテキスト
                    const { TextField, TextFieldAutoSize, TextFormat } = next2d.text;

                    const textFormat = new TextFormat();
                    textFormat.font  = "Arial";
                    textFormat.size  = 20;
                    textFormat.bold  = true;
                    textFormat.color = "#ffffff";

                    const textField = new TextField();
                    textField.defaultTextFormat = textFormat;

                    textField.autoSize  = TextFieldAutoSize.CENTER;
                    textField.thickness = 3;
                    textField.text      = "Back to Home";

                    textField.x = (this.config.stage.width  - textField.width)  / 2;
                    textField.y = 300;

                    view.addChild(textField);

                    resolve();
                });
            })
            .then(() =>
            {
                return new Promise((resolve) =>
                {
                    // HomeViewに移動
                    const { Sprite }     = next2d.display;
                    const { MouseEvent } = next2d.events;

                    // 透明なタップエリアを生成
                    const sprite = view.addChild(new Sprite());
                    const stage  = this.config.stage;
                    sprite
                        .graphics
                        .beginFill(0, 0)
                        .drawRect(0, 0, stage.width, stage.height);

                    // PC版でマウスがポインターに切り替わるように設定をONにする
                    sprite.buttonMode = true;

                    // マウスアップもしくはタップエンドでイベント発火
                    sprite.addEventListener(MouseEvent.MOUSE_UP, () =>
                    {
                        this.app.gotoView("home");
                    });

                    resolve();
                });
            });
    }
} 
```

### View
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80502/1a5b7729-4e33-3dc7-d8ed-fe0488bf2f72.png" width="300">

シーン遷移と、Viewはこれで配置完了したので、最終日の明日はmodel直下のロジックと単体テストを実施して、残った時間で、見た目を整えていければと思います。