[実践・Scratch3拡張機能]〜scratch-vmでスコアボードのゲーム・モジュール化


※ 当ページには【広告/PR】を含む場合があります。
2021/02/22
【Scratchで簡単なゲーム作成編②】スコアボードの作成と得点の表示

以前の記事でスコアボードの作り方を記事として取り上げたことがあります。

合同会社タコスキングダム|タコキンのPスクール
【Scratchで簡単なゲーム作成編②】スコアボードの作成と得点の表示

Scratchでスコアボードの作り方を中心に、ゲームとしての機能を盛り込んでいきます。

スクラッチ言語の開発環境(scratch-gui)のメインライブラリである
scratch-vmをフル活用すると、Scratch3の拡張機能をゲームの一部品として独立に組み込んだような使い方ができるようになります。

今回はこのScratchで作り込んだスコアボードを、モジュール部品として統合・カプセル化して使えるようにしてみたいと面白い、くらいの内容でやってみます。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchをしっかり学ぶためのオススメ書籍まとめ

スクラッチゲームのモジュール化

前回の記事で紹介させていただいた画面の上に表示されるスコアボードの作り方は、通常のスクラッチプログラミングで作成しました。

合同会社タコスキングダム|タコキンのPスクール

つまり、スプライトを一つか二つ用意して、アイテムのアイコンとしていくつかのコスチュームを割り当てて、得点ごとに表示する処理を作り込むのが、
自然なスクラッチのプログラミングです。

対して、実装はとても難しいのですがscratch-vmの標準関数を使い込むと、拡張機能だけでゲームの一部品として扱えるjavascriptコード(この記事では
ゲーム・モジュールと呼んでいます)を作ることができます。

例えば先程のゲーム上のスコアボードをモジュール化すると、以下のようなイメージになります。

合同会社タコスキングダム|タコキンのPスクール

モジュール化したスコアボードを利用することで、スクラッチプログラマーからすると専用のブロックによって全ての操作を完了することができるようになります。

今回のスコアボードだけでなく、ゲーム・モジュールとしておけるものはゲームパッド、メッセージウィンドウ、アニメーションエフェクト...考えつく限り様々な応用が考えられます。

ただし、このような便利な自作ゲーム・モジュールを作るためには、Javascriptによるプログラミングの知識が必須になります。

今回紹介するゲームモジュール化の要点だけを先にまとめておきますと、

            
            メリット:
    + プロックコードによるプログラミング作業が楽になる
    + 色々なスクラッチゲームへ機能を簡単に移植できる
    + モジュール単位なのでコードのメンテナンスがしやすい
デメリット:
    - 高度なJavascriptプログラミングの知識は必須
    - scratch-vmの技術関連の資料が少ない
    - scratchの公式サイトではアプリ開発できない(scratch-gui推奨)
        
ということなのですが、この時点で興味のない方はトップページのゲームに、何やら妙ちきりんな改造を施した、程度の話を長々としているだけの内容を以降で展開していくブログです...ハイ。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchをしっかり学ぶためのオススメ書籍まとめ

Scratch-vmを活用したエクステンションの実装

もうこのブログではおなじみ(?)になりました拡張機能の実装手順ですが、以下ダイジェストにまとめますと、

            
            1. scratch-vmの src/extensions フォルダ以下にindex.jsを作成
2. extention-manager.jsに登録
3. scratch-guiの方へエクスポート
        
以上の3点セットです。

同様の手順は以前の記事でも紹介していましたので、今回は割愛させていただきます。

合同会社タコスキングダム|タコキンのPスクール
【Scratch拡張機能をJavascriptで自作】SVG画像からスプライトをアニメーション的に動かす拡張機能の作成方法

clipPath要素・use要素を用いたSVGアニメーションをScratchアプリに組み込む拡張機能を自作してみます。

モジュール化したスコアボード(拡張機能のindex.js)

今回は実装の概要紹介だけになるのでご容赦ください...以下がindex.jsの主要な実装例です。

            
            const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const TargetType = require('../../extension-support/target-type');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const log = require('../../util/log');
const StageLayering = require('../../engine/stage-layering');

const gsp = require('raw-loader!./svg_parts/grasses_part.dat');
const tcp = require('raw-loader!./svg_parts/taco_part.dat');
const app = require('raw-loader!./svg_parts/apple_part.dat');
const wmp = require('raw-loader!./svg_parts/watermeron_part.dat');
const dnp = require('raw-loader!./svg_parts/dounuts_part.dat');
const fp = require('raw-loader!./svg_parts/fish_part.dat');
const dp = require('raw-loader!./svg_parts/dragon_part.dat');

class TacoswishScoreboard {
    constructor (runtime) {
        this.runtime = runtime;
        this.totalScore = 0;
        this.apCount = 0;
        this.tcCount = 0;
        this.wmCount = 0;
        this.dntCount = 0;
        this.fCount = 0;
        this.dCount = 0;
        this._scoreboardDrawbleId = -1;
        this._svgSkinId = -1;
        this.scoreboardConfig = {
            isShowGrasses: true,
            isShowTaco: true,
            isShowApple: true,
            isShowWaterMeron: false,
            isShowDounuts: false,
            isShowFish: false,
            isShowDragon: false,
        };
        this.svgElements = [];
        this.emptyImg = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0" viewBox="0 0 0 0"></svg>';
        this._initParts();
    }

    _initParts() {
        this.svgElements.push(gsp.default); // 0 > grasses
        this.svgElements.push(app.default); // 1 > apple
        this.svgElements.push(tcp.default); // 2 > tacos
        this.svgElements.push(wmp.default); // 3 > watermeron
        this.svgElements.push(dnp.default); // 4 > dounuts
        this.svgElements.push(fp.default); // 5 > fish
        this.svgElements.push(dp.default); // 6 > dragon
    }

    getInfo () {
        return {
            id: 'tacoswishsb',
            name: formatMessage({
                id: 'tacoswishsb.categoryName',
                default: 'Tacoswishsb',
                description: 'Label for the tacoswishsb extension category'
            }),
            blocks: [
                {
                    opcode: 'initScoreboard',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'clearScoreboard',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'goScoreboardToBack',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'goScoreboardToFront',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'updateScoreboard',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'addScore',
                    text: 'Change to ([TARGET], [NEW_SCORE])',
                    blockType: BlockType.COMMAND,
                    arguments: {
                        TARGET: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 0
                        },
                        NEW_SCORE: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 0
                        }
                    }
                },
                {
                    opcode: 'showElement',
                    blockType: BlockType.COMMAND,
                    text: 'Have an item#([TARGET]) shown',
                    arguments: {
                        TARGET: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 1
                        }
                    }
                }
            ],
            menus: {}
        };
    }

    initScoreboard() {
        this.totalScore = 0;
        this.apCount = 0;
        this.tcCount = 0;
        this.wmCount = 0;
        this.dntCount = 0;
        this.fCount = 0;
        this.dCount = 0;
        this.scoreboardConfig = {
            isShowGrasses: true,
            isShowTaco: true,
            isShowApple: true,
            isShowWaterMeron: false,
            isShowDounuts: false,
            isShowFish: false,
            isShowDragon: false,
        };
        if (this._svgSkinId < 0 && this.runtime.renderer) {
            const skinId = this.runtime.renderer.createSVGSkin(this.emptyImg);
            this._svgSkinId = skinId;
            this._scoreboardDrawbleId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
            this.runtime.renderer.updateDrawableProperties(this._scoreboardDrawbleId, {
                skinId: this._svgSkinId
            });
        }
    }

    clearScoreboard() {
        if (this._svgSkinId >= 0) {
            this.runtime.renderer.updateSVGSkin(this._svgSkinId, this.emptyImg);
        }
    }

    addScore(args, util) {
        const _target = Cast.toNumber(args.TARGET);
        const _score = Cast.toNumber(args.NEW_SCORE);
        switch(_target) {
            case 0:
                this._updateScore(_score, null, null, null, null, null, null);
                break
            case 1:
                this._updateScore(null, _score, null, null, null, null, null);
                break
            case 2:
                this._updateScore(null, null, _score, null, null, null, null);
                break
            case 3:
                this._updateScore(null, null, null, _score, null, null, null);
                break
            case 4:
                this._updateScore(null, null, null, null, _score, null, null);
                break
            case 5:
                this._updateScore(null, null, null, null, null, _score, null);
                break
            case 6:
                this._updateScore(null, null, null, null, null, null, _score,);
                break
            default:
                break
        }
    }

    showElement(args, util) {
        const _target = Cast.toNumber(args.TARGET);
        switch(_target) {
            case 0:
                this.scoreboardConfig.isShowGrasses = true;
                break
            case 1:
                this.scoreboardConfig.isShowApple = true;
                break
            case 2:
                this.scoreboardConfig.isShowTaco = true;
                break
            case 3:
                this.scoreboardConfig.isShowWaterMeron = true;
                break
            case 4:
                this.scoreboardConfig.isShowDounuts = true;
                break
            case 5:
                this.scoreboardConfig.isShowFish = true;
                break
            case 6:
                this.scoreboardConfig.isShowDragon = true;
                break
            default:
                break
        }
    }

    updateScoreboard(args, util) {
        if (this._scoreboardDrawbleId >= 0 && this._svgSkinId >= 0) {
            this.runtime.renderer.updateSVGSkin(this._svgSkinId, this._drawScoreBoard());
        }
    }

    goScoreboardToBack() {
        if (this._scoreboardDrawbleId >= 0 && this._svgSkinId >= 0) {
            this.runtime.renderer.setDrawableOrder(this._scoreboardDrawbleId, 1, StageLayering.SPRITE_LAYER);
        }
    }

    goScoreboardToFront() {
        if (this._scoreboardDrawbleId >= 0 && this._svgSkinId >= 0) {
            this.runtime.renderer.setDrawableOrder(this._scoreboardDrawbleId, Infinity, StageLayering.SPRITE_LAYER);
        }
    }

    _drawScoreBoard() {
        const gsp_str = this.scoreboardConfig.isShowGrasses ? this.svgElements[0] : '';
        const app_str = this.scoreboardConfig.isShowApple ? this.svgElements[1] : '';
        const tcp_str = this.scoreboardConfig.isShowTaco ? this.svgElements[2] : '';
        const wmp_str = this.scoreboardConfig.isShowWaterMeron ? this.svgElements[3] : '';
        const dnp_str = this.scoreboardConfig.isShowDounuts ? this.svgElements[4] : '';
        const fp_str = this.scoreboardConfig.isShowFish ? this.svgElements[5] : '';
        const dp_str = this.scoreboardConfig.isShowDragon ? this.svgElements[6] : '';

        const total_score_str = this.scoreboardConfig.isShowGrasses ? this._convDrawbleText(this.totalScore, 55, 50, 16) : '';
        const apple_count_str = this.scoreboardConfig.isShowApple ? this._convDrawbleText(this.apCount, 130, 50, 14) : '';
        const tacos_count_str = this.scoreboardConfig.isShowTaco ? this._convDrawbleText(this.tcCount, 200, 50, 14) : '';
        const wm_count_str = this.scoreboardConfig.isShowWaterMeron ? this._convDrawbleText(this.wmCount, 260, 50, 14) : '';
        const dnt_count_str = this.scoreboardConfig.isShowDounuts ? this._convDrawbleText(this.dntCount, 320, 50, 14) : '';
        const fish_count_str = this.scoreboardConfig.isShowFish ? this._convDrawbleText(this.fCount, 390, 50, 14) : '';
        const drgn_count_str = this.scoreboardConfig.isShowDragon ? this._convDrawbleText(this.dCount, 450, 50, 14) : '';

        return `<svg version="1.1" width="480" height="360" viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
        <g transform="(121 -5)">
        <g stroke-width="0" fill-opacity="50%"><path d="m-121 57v-57h719v57z"/></g>
         ${gsp_str} ${total_score_str}
         ${app_str} ${apple_count_str}
         ${tcp_str} ${tacos_count_str}
         ${wmp_str} ${wm_count_str}
         ${dnp_str} ${dnt_count_str}
         ${fp_str} ${fish_count_str}
         ${dp_str} ${drgn_count_str}
        </g></svg>`;
    }

    _convDrawbleText(txt_, x_, y_, fontSize_) {
        return `<g transform="translate(${x_} ${y_})" >
        <text fill="#F0E68C" font-family="sans-serif" font-size="${fontSize_}px">
            <tspan>${txt_}</tspan>
        </text>
        </g>`;
    }

    _updateScore(ts_, ap_, tc_, wm_, dnt_, f_, d_) {
        this.totalScore = ts_ !== null ? ts_ : this.totalScore;
        this.apCount = ap_ !== null ? ap_ : this.apCount;
        this.tcCount = tc_ !== null ? tc_ : this.tcCount;
        this.wmCount = wm_ !== null ? wm_ : this.wmCount;
        this.dntCount = dnt_ !== null ? dnt_ : this.dntCount;
        this.fCount = f_ !== null ? f_ : this.fCount;
        this.dCount = d_ !== null ? d_ : this.dCount;
    }
}

module.exports = TacoswishScoreboard;
        
なお、svg画像のリソースコードはindex.jsファイルの階層にsvg_partsというフォルダの中に取りまとめておいてraw-loaderから利用しています。

これらのsvgのインラインコードは全て紹介すると長いので省略します。

以下の図はモジュール化したスコアボードをコントロールする機能をゲームに組込直した際の利用イメージです。

合同会社タコスキングダム|タコキンのPスクール

先程のjsソースコード一つで、色々とゴチャゴチャしやすいスクラッチのブロックコードを置き換えることができました。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchをしっかり学ぶためのオススメ書籍まとめ

まとめ

以上、スクラッチプログラミングでの拡張機能によるゲーム・モジュール化の話を再度まとめると、

            
            メリット:
    + プロックコードによるプログラミング作業が楽になる
    + 色々なスクラッチゲームへ機能を簡単に移植できる
    + モジュール単位なのでコードのメンテナンスがしやすい
デメリット:
    - 高度なJavascriptプログラミングの知識は必須
    - scratch-vmの技術関連の資料が少ない
    - scratchの公式サイトではアプリ開発できない(scratch-gui推奨)
        
なお、今回の変更の内容は特集記事内のゲームへ反映させてあります。