0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next2DAdvent Calendar 2021

Day 24

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

Last updated at Posted at 2021-12-24

画面を作る

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

TopViewModel

NoCodeToolでシンボルとラベルを設定して、動的にオープンニングのMovieClipを生成します。

OpeningContent

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

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

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

HomeViewModel

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

GamePlayViewModel

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

BlueButtonContent

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

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

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

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

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

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

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

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

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

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

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

GameResultViewModel

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?