【Scratch拡張機能をJavascriptで自作】RPG風の吹き出しを表示する拡張機能
※ 当ページには【広告/PR】を含む場合があります。
2021/02/10
最近では不定期ながら著者のScratchの腕試しにと、ちょこちょこ業務の空き時間を見ながらレトロなRPG風〇〇を作ってみたりしております。
今回はセリフ吹き出し用の固定ウィンドウを表示する拡張機能を作成しましたので、デモプログラムと併せてご紹介します。
RPG風のセリフウインドウとは?
著者がファミコン世代の人間なので、既にRPG用のセリフ・ウインドウと言われてピンと来ないヤングなデジタル世代の方も増えてきているとは想像します。
この記事でいう往年の2Dスクロール型のRPG風のセリフウインドウとは、他のキャラクターとの会話や戦闘画面などのときに概ね画面の底に表示されてくる文字を表示してくれる枠のことです。

上の図のように、画面に割り込む形でメッセージを表示させてくれるような、まだ画面の小さかった据え置きのゲーム機では慣れ親しまれた機能です。
なお、今回作成した拡張機能を利用したデモプログラムはトップページの
意外と面倒な文字を含む画像の表示
Scratchプログラミングに親しんでいる方ほどその煩わしさを身にしみて分かってはいらっしゃるかと思います。
拡張機能なしでRPG風のセリフ用ウィンドウを作成する方法は
以前この方法を試すために、一般のフォントファイルからフォント画像をゴッソリ引き抜く方法を検証してみました。
結果としてはなんとかフォント文字を画像として取り出せたものの、シェルスクリプトの力でねじ伏せた感があり、一般のScratchユーザーからの知識を逸脱していたのも事実です。 (このため
そして今回のテーマというと、RPG風の吹き出しが出せる拡張機能の作成のお話です。
予め触れておきますが、拡張機能の作成はscratch-guiなどのソースコードを弄る必要があります。
webpackやReact.jsなどのそれなりのフロントエンドの知識が必要になりますので、今回のお題としては上級者より上の開発者向けの技術情報になりますことをご了承ください。
ズバッといきなりソースコード
このサイトでも
以下、今回のscratch-vmに設定する拡張機能(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');
class Scratch3MessageWindow {
constructor (runtime) {
this._extVersion = '0.0.1';
this.runtime = runtime;
this._fontDrawableId = -1;
this._fontSkinId = -1;
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>';
}
getInfo () {
return {
id: 'messagewindow',
name: formatMessage({
id: 'messagewindow.categoryName',
default: 'MessageWindow',
description: 'Label for messagewindow extension category'
}),
blocks: [
{
opcode: 'initSkin',
blockType: BlockType.COMMAND
},
{
opcode: 'updateMessage',
blockType: BlockType.COMMAND,
text: formatMessage({
id: 'messagewindow.updateMessage',
default: 'stamp ( [STR] ) x [SCALE]',
description: 'render current costume on the background'
}),
arguments: {
STR: {
type: ArgumentType.STRING,
defaultValue: ''
},
SCALE: {
type: ArgumentType.NUMBER,
defaultValue: 100
}
}
},
{
opcode: 'goMapToBack',
blockType: BlockType.COMMAND
},
{
opcode: 'goMapToFront',
blockType: BlockType.COMMAND
},
{
opcode: 'clearSkin',
blockType: BlockType.COMMAND
},
{
opcode: 'getVersion',
text: 'Version Info',
blockType: BlockType.REPORTER
}
],
menus: {}
};
}
initSkin() {
if (this._fontSkinId < 0 && this.runtime.renderer) {
const skinId = this.runtime.renderer.createSVGSkin(this.emptyImg);
this._fontSkinId = skinId;
this._fontDrawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER);
this.runtime.renderer.updateDrawableProperties(this._fontDrawableId, {skinId: this._fontSkinId});
}
}
updateMessage(args) {
const text_ = Cast.toString(args.STR);
const scale_ = Cast.toString(args.SCALE);
if (this._fontSkinId >= 0) {
const tArr = this._textSlice(text_);
const windowOrigin = [2, 258];
const fontSize_ = 14;
const fontSvg = `<svg width="480px" height="360px" version="1.1" viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<g stroke-width="4">
<rect transform="translate(2,3)" x="${windowOrigin[0]}" y="${windowOrigin[1]}" width="476" height="100" rx="15" ry="15" opacity=".25" stroke="#000"/>
<rect x="${windowOrigin[0]}" y="${windowOrigin[1]}" width="476" height="100" rx="15" ry="15" fill="#fff" stroke="#000080"/>
<text fill="#0000ff" font-family="sans-serif" font-size="${fontSize_}px" letter-spacing="0px" word-spacing="0px" xml:space="preserve">
<tspan x="${windowOrigin[0] + 12}" y="${windowOrigin[1] + 25}">${tArr[0]}</tspan>
</text>
<text fill="#0000ff" font-family="sans-serif" font-size="${fontSize_}px" letter-spacing="0px" word-spacing="0px" xml:space="preserve">
<tspan x="${windowOrigin[0] + 12}" y="${windowOrigin[1] + 55}">${tArr[1]}</tspan>
</text>
<text fill="#0000ff" font-family="sans-serif" font-size="${fontSize_}px" letter-spacing="0px" word-spacing="0px" xml:space="preserve">
<tspan x="${windowOrigin[0] + 12}" y="${windowOrigin[1] + 85}">${tArr[2]}</tspan>
</text>
</g>
</svg>`;
this.runtime.renderer.updateSVGSkin(this._fontSkinId, fontSvg);
}
}
clearSkin() {
if (this._fontSkinId >= 0) {
this.runtime.renderer.updateSVGSkin(this._fontSkinId, this.emptyImg);
}
}
goMapToBack() {
if (this._fontDrawableId >= 0 && this._fontSkinId >= 0) {
this.runtime.renderer.setDrawableOrder(this._fontDrawableId, 1, StageLayering.SPRITE_LAYER);
}
}
goMapToFront() {
if (this._fontDrawableId >= 0 && this._fontSkinId >= 0) {
this.runtime.renderer.setDrawableOrder(this._fontDrawableId, Infinity, StageLayering.SPRITE_LAYER);
}
}
getVersion(args) {
return this._extVersion;
}
_textSlice(text = '') {
const splitText = [];
const onelineLimit = 32;
const sanitizedText = text.replace(/\s/g,'');
const textLen = sanitizedText.length;
if (sanitizedText) {
const firstRow = sanitizedText.slice(0, onelineLimit);
if (firstRow) {
splitText.push(firstRow);
} else {
splitText.push('');
}
if (textLen > onelineLimit) {
const secondRow = sanitizedText.slice(onelineLimit, onelineLimit * 2);
splitText.push(secondRow);
} else {
splitText.push('');
}
if (textLen > 2 * onelineLimit) {
const thirdRow = sanitizedText.slice(onelineLimit * 2, onelineLimit * 3);
splitText.push(thirdRow);
} else {
splitText.push('');
}
return splitText;
} else {
return ['', '', ''];
}
}
}
module.exports = Scratch3MessageWindow;
このソースコードの機能を一つ一つ紐解きながら解説するととても長くなりますのでここでは全て解説できません。
一から詳しく理解されたい開発者の方は
以前、scratch-renderに関して特集した記事を以下のリンクにありますので、併せてお読みください。
この拡張機能をscratch-gui上で動作するようにすると、以下のようなブロックでGUI上に展開できます。

実際に動かして確認するには、本サイトトップページの
まだまだRPGと呼べるには程遠いのですが、拡張機能の持つScratchゲームの可能性には十分触れていただけたのではないかとと思います。
まとめ
今回はRPG風のセリフ用ウィンドウを操作するようの拡張機能のプロトタイプ作成に関してご紹介してみました。
不定期ではありますが、
その度にどのような拡張機能を盛り込んでいくのかをこのような記事で紹介することもあるかと思います。
トップのサンプルゲームを動かしていただくだけでも有り難いのですが、やはり実際にご自身の手でScratchの拡張機能を自作して動かしてみるのが一番のプログラミング上達の近道ではないかと考えます。