【Scratch拡張機能をJavascriptで自作】svg画像からRPG風のマップを生成する拡張機能を作る〜拡張機能の実装③
※ 当ページには【広告/PR】を含む場合があります。
2021/02/05
前回から2回に分けて特集してきた、RPG風のタイルパターンを使ったマップを作成する拡張機能のお話も今回で最終回です。
この記事では拡張機能を実装を具体的に見ていきます。
なお前回の記事は以下のリンクから辿れます。
今回はJavacriptによるプログラミングの内容が中心ですので、svg画像の取扱を操作するソースコードを取り扱うポイントを理解することが目的です。
おそらく前回の記事2回までの内容を読んでいなくても、svgをソースコード側から読み込みたいという開発者の方はこの記事の内容から読んでいただいてもOKです。
スクラッチでRPGはどこまで作れるか?
据え置きのゲーム専用機の処理能力も30年前のファミリーコンピュータの時代と比べると比べ物にならないくらいに向上し、もはやゲームといえば3Dを使った作品が席巻してしまったようにも思いますが、簡単に遊べる2Dのレトロなゲームにもまだまだ需要が存在しているので、昔のゲームのようなものを作って遊んでみるのも意味のないことではありません。
スクラッチのページでも、なかなかの作り込みを行っていらっしゃるプログラマーの方がいらっしゃるようです。
例えばこちらの
とはいえ、よりゲームのストーリーに発展性をもたせたり、沢山のアイテムのバリエーションを管理させたりとしていくと、どうしてもスクラッチの基本機能だけでは実現することが困難になってきます。
このようなもっと高度な機能をもった進んだスクラッチアプリを開発するためには、拡張機能(エクステンション)の作成方法に慣れておく必要が出てきます。
往年の2DスクロールのRPGは、切り替えるシーンが多いことと、ストーリーの流れを管理すること、キャラクターの状態を常に管理しなくてはいけないこと...かなり複雑な機能が複合的に使われているプログラムであり、スクラッチの拡張機能を駆使することで本格的なRPGが作成できる可能性は十分にあります。
ということで、今回はその中の一つの機能である
タイルマップの描画操作
タイルパターンマップを生成・描画する拡張機能の作成
ここから具体的な拡張機能の実装を解説していきます。
なお、Scratch3の拡張機能を作るのが初めての方や、基本的なの作成方法をおさらいしたい方は、以前特集した基礎的な拡張機能の作成手順をまとめた記事をしたためていましたので、下のリンクでその内容のほうをご確認ください。
この記事で解説したように、拡張機能の作成手順をダイジェストにまとめると、
①scratch-gui
②scratch-vm
① scratch-gui側でやること:
1. 拡張機能のカードリスト用に600x372と80x80のpng画像を
用意して、
2. 拡張機能用のライブラリファイル(index.js)に登録する
② scratch-vm側でやること:
1. ブロックアイコン用の画像をBase64化して、
jsコード内に貼り付け
2. エクステンションのコード本体を実装
3. extension-manager.jsに拡張機能を登録
という流れになります。
作成した拡張機能を第三者へ公開する場合には、上の
① 1. - 2.
② 1.
この記事では
② 2. -3.
拡張機能の実装例
拡張機能のメインコードは
scratch-vm/src/extensions
今回はこのフォルダ内に
scratch3_tilemap/index.js
そしてこのエクステンション本体コード(
index.js
$ tree
.
├── index.js
├── map1.csv
└── map1.dat
エクステンションのフォルダへリソースを入れる時、
map1.svg
今回の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に関しては、ちょっとした技術解説を以下の記事にしたことがありますので、併せてお読みください。
raw-loaderで生のテキストを読み込む
scratch-vmのリソースの読み込みは通常file-loaderのプラグインを使ったwebpack側から内部で読み込まれている仕様です。
今回のように自作のsvgなどの画像をローカルから読み込んで使うには
折角なので、
$ 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
という理由から、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'),
};
///...
これでエクステンションが使えるようになったら通常のスクラッチプログラミングをすることができます。
以下ではこの自作エクステンションを使った簡単なプログラムになります。

まとめ
今回はレトロなRPG風のタイルパターンマップを操作する拡張機能の作成を全3回に渡って解説させていただきました。
ここから本格的なRPGを作成するかどうかは未定ですが、まだまだ作り込む機能は多そうで、気の長くなる時間が必要になるかもしれません。
また著者の気の赴いたときにRPG風〇〇を作ってみるような内容でご紹介させていただくかもしれませんので、今後共ちょくちょく本ブログのほうを覗いてみてください。