【Scratch拡張機能をJavascriptで自作】svg画像からRPG風のマップを生成する拡張機能を作る〜拡張機能の実装③


※ 当ページには【広告/PR】を含む場合があります。
2021/02/05
【Scratch拡張機能をJavascriptで自作】svg画像からRPG風のマップを生成する拡張機能を作る〜マップ作成編②
【Scratch拡張機能をJavascriptで自作】RPG風の吹き出しを表示する拡張機能

前回から2回に分けて特集してきた、RPG風のタイルパターンを使ったマップを作成する拡張機能のお話も今回で最終回です。

この記事では拡張機能を実装を具体的に見ていきます。

なお前回の記事は以下のリンクから辿れます。

合同会社タコスキングダム|タコキンのPスクール
【Scratch拡張機能をJavascriptで自作】svg画像からRPG風のマップを生成する拡張機能を作る〜準備編①

Scratchの自作拡張機能の例として、svg画像からRPGマップを直接内部でレンダリングする高度な拡張機能を作ってみます。

合同会社タコスキングダム|タコキンのPスクール
【Scratch拡張機能をJavascriptで自作】svg画像からRPG風のマップを生成する拡張機能を作る〜マップ作成編②

Scratchの自作拡張機能で、RPGゲームのマップの種となるcsvファイルを使ってBase64化したタイルの画像を所定の座標に張り合わせるプログラムを作ります。

今回はJavacriptによるプログラミングの内容が中心ですので、svg画像の取扱を操作するソースコードを取り扱うポイントを理解することが目的です。

おそらく前回の記事2回までの内容を読んでいなくても、svgをソースコード側から読み込みたいという開発者の方はこの記事の内容から読んでいただいてもOKです。


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

【Svelte.js入門】ReactやVueに挫折した人でも大丈夫!Svelteとfirebaseでシンプルアプリ開発

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

スクラッチでRPGはどこまで作れるか?

据え置きのゲーム専用機の処理能力も30年前のファミリーコンピュータの時代と比べると比べ物にならないくらいに向上し、もはやゲームといえば3Dを使った作品が席巻してしまったようにも思いますが、簡単に遊べる2Dのレトロなゲームにもまだまだ需要が存在しているので、昔のゲームのようなものを作って遊んでみるのも意味のないことではありません。

スクラッチのページでも、なかなかの作り込みを行っていらっしゃるプログラマーの方がいらっしゃるようです。

例えばこちらの
ネコRPG Ver0.3.0.9のようにスクラッチの基本機能だけで構成されている作品は、やっているプログラミングはシンプルですが作り込み度が凄いと感心します。

とはいえ、よりゲームのストーリーに発展性をもたせたり、沢山のアイテムのバリエーションを管理させたりとしていくと、どうしてもスクラッチの基本機能だけでは実現することが困難になってきます。

このようなもっと高度な機能をもった進んだスクラッチアプリを開発するためには、拡張機能(エクステンション)の作成方法に慣れておく必要が出てきます。

往年の2DスクロールのRPGは、切り替えるシーンが多いことと、ストーリーの流れを管理すること、キャラクターの状態を常に管理しなくてはいけないこと...かなり複雑な機能が複合的に使われているプログラムであり、スクラッチの拡張機能を駆使することで本格的なRPGが作成できる可能性は十分にあります。

ということで、今回はその中の一つの機能である
タイルマップの描画操作を行える拡張機能を簡単に作ってみたいと思います。


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

【Svelte.js入門】ReactやVueに挫折した人でも大丈夫!Svelteとfirebaseでシンプルアプリ開発

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

タイルパターンマップを生成・描画する拡張機能の作成

ここから具体的な拡張機能の実装を解説していきます。

なお、Scratch3の拡張機能を作るのが初めての方や、基本的なの作成方法をおさらいしたい方は、以前特集した基礎的な拡張機能の作成手順をまとめた記事をしたためていましたので、下のリンクでその内容のほうをご確認ください。

合同会社タコスキングダム|タコキンのPスクール
【Scratch拡張機能・自作】テキストデータを読み出し・保存するエクステンションを作ろう

Scratchの拡張機能の自作手順の基礎を、テキストを読み出し・書き出しできるような機能の実装を例に解説します。

この記事で解説したように、拡張機能の作成手順をダイジェストにまとめると、
①scratch-guiでの作業、②scratch-vmでの作業の大きく2つに分かれます。

            
            ① scratch-gui側でやること:
    1. 拡張機能のカードリスト用に600x372と80x80のpng画像を
        用意して、
    2. 拡張機能用のライブラリファイル(index.js)に登録する

② scratch-vm側でやること:
    1. ブロックアイコン用の画像をBase64化して、
        jsコード内に貼り付け
    2. エクステンションのコード本体を実装
    3. extension-manager.jsに拡張機能を登録
        
という流れになります。

作成した拡張機能を第三者へ公開する場合には、上の
① 1. - 2.および② 1.は作成する必要がありますが、個人的な用途や開発チーム内で既知のツールとして提供するときには必ずしも画像が要るわけではないので、画像を空にしておいてもOKです。

この記事では
② 2. -3.の解説にフォーカスして、アイコン画像などの仕込みの手順はスキップします。

拡張機能の実装例

拡張機能のメインコードはscratch-vm/src/extensionsフォルダ以下にリソースの類を追加します。

今回はこのフォルダ内に
scratch3_tilemap/index.jsというディレクトリで、拡張機能用のjsファイルを追加します。

そしてこのエクステンション本体コード(
index.js)と同じフォルダに、前回の内容で作成したsvg画像とマップを生成したcsvファイルを2つ置いておきます。

            
            $ tree
.
├── index.js
├── map1.csv
└── map1.dat
        
エクステンションのフォルダへリソースを入れる時、map1.svgとsvgの拡張子を変えずにそのまま入れると、webpackが勝手にsvgの画像を解釈して複雑な処理を行おうとしますので、webpackの監視下に無い拡張子(ここでは.dat)などにリネームしておきます。

今回の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 MathUtil = require('../../util/math-util');
const log = require('../../util/log');
const StageLayering = require('../../engine/stage-layering');

const svgTileMap_ = require('raw-loader!./map1.dat');
const csvTileData_ = require('raw-loader!./map1.csv');

class Scratch3Tilemap {
    constructor (runtime) {
        this.runtime = runtime;
        this._x = 0;
        this._y = 0;
        this._xOrigin = 0;
        this._yOrigin = 0;
        this._mapDrawableId = -1;
        this._mapSkinId = -1;
        this.svgElements = [];
        this._initMaps();
    }

    _getMapLayerID () {
        if (this._mapSkinId < 0 && this.runtime.renderer) {
            this._mapSkinId = this.runtime.renderer.createSVGSkin('<svg xmlns="http://www.w3.org/2000/svg" version="1.1"></svg>');
            this._mapDrawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
            this.runtime.renderer.updateDrawableProperties(this._mapDrawableId, {skinId: this._mapSkinId});
        }
        return this._mapSkinId;
    }

    _updateSvgDraw(svgXml) {
        const mapSkinId = this._getMapLayerID();
        if (mapSkinId >= 0) {
            const skin = /** @type {SVGSkin} */ this.runtime.renderer._allSkins[mapSkinId];
            skin.setSVG(svgXml);
            this.runtime.renderer.updateDrawableProperties(this._mapDrawableId, {
                skinId: mapSkinId,
                position: [this._xOrigin, this._yOrigin], // Set origin for svg layer
                scale: [100, 100],
                direction: 90
            });
            this.runtime.requestRedraw(); // overlay image over sprites!
        }
    }

    _initMaps() {
        this.tilemaps = [];
        const mapBody = svgTileMap_.default;
        const mapLoc = this._cvs2Maparray(csvTileData_.default);
        const sampleMap = {mapBody, mapLoc};
        this.tilemaps.push(sampleMap);
    }

    _cvs2Maparray(raw_csvdata = '') {
        const parsed_csv = [];
        if (raw_csvdata) {
            const rows_ = raw_csvdata.split(/\r?\n/);
            for (const row_ of rows_) {
                if (row_) {
                    parsed_csv.push(row_.split(/,/));
                }
            }
        }
        return parsed_csv;
    }

    getInfo () {
        return {
            id: 'tilemap',
            name: formatMessage({
                id: 'tilemap.categoryName',
                default: 'Tilemap',
                description: 'Label for the tilemap extension category'
            }),
            blockIconURI: '',
            blocks: [
                {
                    opcode: 'stamp',
                    blockType: BlockType.COMMAND,
                    text: formatMessage({
                        id: 'tilemap.stamp',
                        default: 'stamp [MAP_ID]',
                        description: 'Render svg on background'
                    }),
                    arguments: {
                        MAP_ID: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 1
                        }
                    }
                },
                {
                    opcode: 'checkTileFromPosition',
                    text: 'Check Tile ? <= ([X_POS], [Y_POS])',
                    blockType: BlockType.REPORTER,
                    arguments: {
                        X_POS: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 0
                        },
                        Y_POS: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 0
                        }
                    }
                }
            ],
            menus: {
            }
        };
    }

    stamp (args, util) {
        this._x = Cast.toNumber(args.X_POS);
        this._y = Cast.toNumber(args.Y_POS);
        this._updateSvgDraw(this.tilemaps[0].mapBody);
    }

    checkTileFromPosition(args, util) {
        this._x = Cast.toNumber(args.X_POS);
        this._y = Cast.toNumber(args.Y_POS);

        const revX = Math.floor((this._x + 240) / 24);
        const revY = Math.floor((180 - this._y) / 24);
        if (0 <= revX && revX < 20 && 0 <= revY && revY < 15) {
            return this.tilemaps[0].mapLoc[revY][revX];
        } else {
            return 0;
        }
    }

}

module.exports = Scratch3Tilemap;
        
さて、このソースコードは主に、スクラッチのデフォルト拡張機能のペン機能(scratch-vm/src/extensions/scratch3_pen/index.js)を改造して作成しています。

今回のタイルパターン画像を生成する拡張機能の各実装の細かい解説は長くなりますので省かせていただきます。

scratch-vmの技術解説はほとんどないので、各関数の挙動を詳しく理解されたい方は
本家ペン機能の実装を自身で紐解く必要があります。

コアライブラリのscratch-renderに関しては、ちょっとした技術解説を以下の記事にしたことがありますので、併せてお読みください。

合同会社タコスキングダム|タコキンのPスクール
Scratchのディープな使い方〜レンダラー(scratch-render)だけを動かしてみる

Scratch-guiから、文字もしくは文字列からユニコード(16進数)に変換するための拡張機能を自作します。

raw-loaderで生のテキストを読み込む

scratch-vmのリソースの読み込みは通常file-loaderのプラグインを使ったwebpack側から内部で読み込まれている仕様です。

今回のように自作のsvgなどの画像をローカルから読み込んで使うには
raw-loaderを利用したほうが楽になります。

折角なので、
raw-loaderのscratch-vmへの導入法をメモしておきます。

            
            $ cd scratch-vm
$ yarn add raw-loader -D
        
これでraw-loaderがプロジェクトで利用できるようになりました。

webpackプラグインとして利用するには通常
webpack.config.jsの中に特定の拡張子を含むファイルにローダーを指定するやり方も可能です。

            
            //...
module.exports = {
    module: {
        rules: [
        {
            test: /\.txt$/i,
            use: 'raw-loader',
        },
        ],
    },
};
//...
        
としておいて、プロジェクト内のどこかのコードで、

            
            import txt from './file.txt';
//...
        
とすれば.txtをもつ拡張子にraw-loaderが適用されます。

スクラッチのプロジェクト開発では
svg|png|wav|gif|jpgはfile-loaderで取り扱うことが前提の仕様になっており、webpackの設定がますますややこしくなるのを避けるため、これらの拡張子をraw-loaderで読み込むようにwebpack.config.js側へ追記するべきではありません。

という理由から、raw-loaderの指定はファイル単位で以下のように読み出すと、webpack環境の無用な汚染を防ぐことができます。

            
            //...
const svgTileMap_ = require('raw-loader!./map1.dat');
const csvTileData_ = require('raw-loader!./map1.csv');

//...

//👇ローダーからのファイルの中身はdefaultプロパティで取り出す
// console.log(svgTileMap_.default);
// console.log(csvTileData_.default);

//...
        

extension-manager.jsへ登録

仕上がった拡張機能はextension-manager.js(場所:scratch-vm/src/extension-support/extension-manager.js)の中のbuiltinExtensionsのフィールドへ登録することでプロジェクトで利用可能となります。

            
            ///...

const builtinExtensions = {
    // This is an example that isn't loaded with the other core blocks,
    // but serves as a reference for loading core blocks as extensions.
    coreExample: () => require('../blocks/scratch3_core_example'),
    // These are the non-core built-in extensions.
    pen: () => require('../extensions/scratch3_pen'),
    wedo2: () => require('../extensions/scratch3_wedo2'),
    music: () => require('../extensions/scratch3_music'),
    microbit: () => require('../extensions/scratch3_microbit'),
    text2speech: () => require('../extensions/scratch3_text2speech'),
    translate: () => require('../extensions/scratch3_translate'),
    videoSensing: () => require('../extensions/scratch3_video_sensing'),
    ev3: () => require('../extensions/scratch3_ev3'),
    makeymakey: () => require('../extensions/scratch3_makeymakey'),
    boost: () => require('../extensions/scratch3_boost'),
    gdxfor: () => require('../extensions/scratch3_gdx_for'),
    //👇自作の拡張機能を定義・登録する
    tilemap: () => require('../extensions/scratch3_tilemap'),
};

///...
        
スクラッチのアプリでは自作の拡張機能のファイルサイズが大きくなると、プログラムのサイズも非常に大きくなることが避けられません。

もっとプログラムのサイズの容量を減らしたい・読み込みのオーバーヘッド時間を最適化したい場合には、使っていない拡張機能は読み込まないことがベストです。

今回のプログラムでは、自作の拡張機能以外は使わないので、以下のように要らないエクステンションはコメントアウトするか削除することで最適化可能です。

            
            ///...

const builtinExtensions = {
    //👇自作の拡張機能のみ使う場合
    tilemap: () => require('../extensions/scratch3_tilemap'),
};

///...
        
これでエクステンションが使えるようになったら通常のスクラッチプログラミングをすることができます。

以下ではこの自作エクステンションを使った簡単なプログラムになります。

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

こちらの記事内でもで実際のプログラムを公開しています。

合同会社タコスキングダム|タコキンのPスクール
【ミ二ゲーム開発・リリースノート】とても狭い猫の冒険〜『猫くえすと』まとめ

サイト内で不定期ながら開発中のスクラッチ・ミニRPG『猫くえすと』のリリースノートです。


JavaScriptとHTMLで「レトロ風RPG」を作ろう 全コード解説

【Svelte.js入門】ReactやVueに挫折した人でも大丈夫!Svelteとfirebaseでシンプルアプリ開発

合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

まとめ

今回はレトロなRPG風のタイルパターンマップを操作する拡張機能の作成を全3回に渡って解説させていただきました。

ここから本格的なRPGを作成するかどうかは未定ですが、まだまだ作り込む機能は多そうで、気の長くなる時間が必要になるかもしれません。

また著者の気の赴いたときにRPG風〇〇を作ってみるような内容でご紹介させていただくかもしれませんので、今後共ちょくちょく本ブログのほうを覗いてみてください。