[実践・Scratch3拡張機能]〜scratch-vmでスコアボードのゲーム・モジュール化
※ 当ページには【広告/PR】を含む場合があります。
2021/02/22
以前の記事でスコアボードの作り方を記事として取り上げたことがあります。
スクラッチ言語の開発環境(scratch-gui)のメインライブラリである
今回はこのScratchで作り込んだスコアボードを、モジュール部品として統合・カプセル化して使えるようにしてみたいと面白い、くらいの内容でやってみます。
スクラッチゲームのモジュール化

つまり、スプライトを一つか二つ用意して、アイテムのアイコンとしていくつかのコスチュームを割り当てて、得点ごとに表示する処理を作り込むのが、
自然な
対して、実装はとても難しいのですがscratch-vmの標準関数を使い込むと、拡張機能だけでゲームの一部品として扱えるjavascriptコード(この記事では
ゲーム・モジュール
例えば先程のゲーム上のスコアボードをモジュール化すると、以下のようなイメージになります。

モジュール化したスコアボードを利用することで、スクラッチプログラマーからすると専用のブロックによって全ての操作を完了することができるようになります。
今回のスコアボードだけでなく、ゲーム・モジュールとしておけるものはゲームパッド、メッセージウィンドウ、アニメーションエフェクト...考えつく限り様々な応用が考えられます。
ただし、このような便利な自作ゲーム・モジュールを作るためには、Javascriptによるプログラミングの知識が必須になります。
今回紹介するゲームモジュール化の要点だけを先にまとめておきますと、
メリット:
+ プロックコードによるプログラミング作業が楽になる
+ 色々なスクラッチゲームへ機能を簡単に移植できる
+ モジュール単位なのでコードのメンテナンスがしやすい
デメリット:
- 高度なJavascriptプログラミングの知識は必須
- scratch-vmの技術関連の資料が少ない
- scratchの公式サイトではアプリ開発できない(scratch-gui推奨)
ということなのですが、この時点で興味のない方は
Scratch-vmを活用したエクステンションの実装
もうこのブログではおなじみ(?)になりました拡張機能の実装手順ですが、以下ダイジェストにまとめますと、
1. scratch-vmの src/extensions フォルダ以下にindex.jsを作成
2. extention-manager.jsに登録
3. scratch-guiの方へエクスポート
以上の3点セットです。
同様の手順は以前の記事でも紹介していましたので、今回は割愛させていただきます。
モジュール化したスコアボード(拡張機能の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のインラインコードは全て紹介すると長いので省略します。
以下の図はモジュール化したスコアボードをコントロールする機能をゲームに組込直した際の利用イメージです。

先程のjsソースコード一つで、色々とゴチャゴチャしやすいスクラッチのブロックコードを置き換えることができました。
まとめ
以上、スクラッチプログラミングでの拡張機能によるゲーム・モジュール化の話を再度まとめると、
メリット:
+ プロックコードによるプログラミング作業が楽になる
+ 色々なスクラッチゲームへ機能を簡単に移植できる
+ モジュール単位なのでコードのメンテナンスがしやすい
デメリット:
- 高度なJavascriptプログラミングの知識は必須
- scratch-vmの技術関連の資料が少ない
- scratchの公式サイトではアプリ開発できない(scratch-gui推奨)
なお、今回の変更の内容は