【Scratch3.0で自作エクステンション入門】テキストデータを読み出し・保存するエクステンションを作ろう


※ 当ページには【広告/PR】を含む場合があります。
2020/10/11
2021/12/11

今回はスクラッチ3.0の
エクステンション(拡張機能)を作成する基本的な手順を解説していきます。

ここでは、ゲームの中間状態をテキストデータにて読み出し(インポート)・書き出し(エクスポート)できるようなセーブ機能の実装手順を解説します。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchを学べるオンライン・駅前プログラミングスクール5選

オリジナルのエクステンションを作成

公式のScratch 3.0の拡張機能を作ってみようの記事にある手順をベースにエクステンションを作成してみます。

ちなみに以前、
エクステンションを作成するための環境構築を特集しました。

詳しいスクラッチプログラミング環境の構築手順はそちらの記事にお任せするとして、
scratch-guiscratch-vmは既にお手元の環境で動作してるものとさせて導入の前手順の話はスキップさせていただきます。

GUIの設定

まずはエクステンションの店看板とも言える画像を2つお絵描きして準備します。

作法は決まっていて、
600x37280x80のサイズのpng形式の画像が2つ必要とのことです。

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

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

名前も固定で大きいほうが
fileio.png、小さい方をfileio-small.pngとしてscratch-gui/src/lib/libraries/extensions/fileioというフォルダーを作って保存しておきます。

index.jsx

scratch-gui/src/lib/libraries/extensionsの中にあるindex.jsxが全てのエクステンションを操作するGUIのエンドポイントになりますので、ここに今回作成するfileioエクステンションを新しく登録しておきます。

            
            import React from 'react';
import {FormattedMessage} from 'react-intl';

//...中略

//👇先程保存しておいた画像の場所を登録
import fileioIconURL from './fileio/fileio.png';
import fileioInsetIconURL from './fileio/fileio-small.png';

export default [
    //...中略
    //👇新しいエクステンションUIを登録
    {
        name: 'Custom File I/O using Browser\'s Dialog',
        extensionId: 'fileio',
        iconURL: fileioIconURL,
        insetIconURL: fileioInsetIconURL,
        description: (
            <FormattedMessage
                defaultMessage="Handle files between local and server."
                description="Description for the 'Fileio' extension"
                id="gui.extension.fileio.description"
            />
        ),
        featured: true
    }
];
        

余談〜2つの画像の必要性

この大小の2つの画像は必ずしも必要ではありません。

無いならないで良いのですが、エクステンションを呼び出すときは以下のような見栄えになります。

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

見ての通りで、とても殺風景なアイコンになってしまいます。

でもコレはコレでメリットがあって、自分しか使わないのに、大小2種類も絵をわざわざ用意するのも面倒な話です。

後付でエクステンションにアイコン画像を貼り付けることもできますし、見てくれが気にならないのであれば先程の
index.jsxの中身も以下のようなコードに簡略化できます。

            
            import React from 'react';
import {FormattedMessage} from 'react-intl';

//...中略

export default [
    //...中略
    //👇新しいエクステンションUIを登録
    {
        name: 'Custom File I/O using Browser\'s Dialog',
        extensionId: 'fileio',
        featured: true
    }
];
        
またdescriptionフィールドも蛇足ではあるので、ざっくりと削除してスッキリしました。

ブロックアイコンを作る

コマンドブロックに表示されるアイコンを作成していきます。

このアイコンはブロックごとに一つ一つ好みの画像を設定していくこともできますが、ここでは1つの画像を共有するように使ってみます。

※ ブロックアイコンもデザインの話ですので、先程と同様に必ずしも必要ではありません。

Scratch3のどこかのスプライト上でペイントエディターを使って、40x40の正方形の範囲のベクター画像を何か描いてそれを使います。

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

書き終わったらグループ化して
書き出しからsvg形式でどこかに保存しておきましょう。

次に保存したアイコン用の画像はbase64形式に変換してもらえるサイト(例えば
こちらのサイト)でbase64文字列しておきます。base64画像とはdata:image/svg+xml;base64,...で始まる形式の文字列です。

例えば先ほどのsvgアイコン画像をbase64化すると以下のような文字列になります。

            
            data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDJweCIgaGVpZ2h0PSI0MnB4IiBjb250ZW50PScmbHQ7bXhmaWxlIGhvc3Q9ImFwcC5kaWFncmFtcy5uZXQiIG1vZGlmaWVkPSIyMDIwLTEwLTA4VDA2OjU1OjEyLjgxOFoiIGFnZW50PSI1LjAgKFgxMSkiIHZlcnNpb249IjEzLjcuOCIgZXRhZz0iWExVelVlU252T3hYM29taTNiZWEiJmd0OyZsdDtkaWFncmFtIGlkPSJiTXp5dW5TbUNHanV6aFlfa2ttYSImZ3Q7alpOTmI0TXdESVovRGRlcUpGdTdIbGRLdDhNbVRlcGh4eW9GRjZJRmpFS2d0TDkrempDbHFLcTBDNG9mTy9IckR3SVpGZDJiVlZYK2lTbVlRTXpUTHBDYlFJam5NS1N2QitjZXlKWG9RV1oxMnFOd0JEdDlBWVp6cG8xT29aNEVPa1RqZERXRkNaWWxKRzdDbExWNG1vWWQwVXl6VmlxRE83QkxsTG1uM3pwMWVVOWZ4SExrNzZDemZNZ2NMbGE5cDFCRE1GZFM1eXJGMHcyU2NTQWppK2o2VTlGRllIenZocjcwOTdZUHZGZGhGa3Izbnd2YzkxYVpobXRqWGU0OEZOdUNkWnBxLzFBSE1GOVlhNmV4Sk5jQm5jTWlrT3ZjRllic2tJNUQ3S3ZSbVk5eFdCRlZiQ1VrQ2l5QjJsbjhnUWdOa3JVcHNhUlU2Nk0yWmtDQmtQUDVPdDRLSDV5cnlrc3B1c3d2MDB4ZEdndXp4R0NUN211d3JVNmczdE9ranpwcnJQTGk5dlNVZjdGQzdSUEdMZVd0V1NIWFN6cWhlOWl6OERvSjJtREFBcHc5VXdoZmtBc2VIbSt2V0xKOUduZmhpVkYrc3dZRFU3eDkyZlhsY1VCMDRCa041cmdMZjc2Ykgwckd2dz09Jmx0Oy9kaWFncmFtJmd0OyZsdDsvbXhmaWxlJmd0OycgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSItLjUgLS41IDQyIDQyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgcG9pbnRlci1ldmVudHM9ImFsbCI+CiAgPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTAgNC42MWMwLjMtMi40MyAyLjMtNC4zNCA0LjgyLTQuNjFoMjYuODhsOC4zIDcuOTZ2MjcuMDdjLTAuMTQgMi42Ni0yLjMzIDQuOC01LjA5IDQuOTdoLTMwLjA5Yy0yLjQ4LTAuMjUtNC40Ny0yLjExLTQuODItNC40OXptMy4yNyAzMC4xOGMwIDAuOTYgMC43IDEuNzkgMS42OCAxLjk4aDMwLjAyYzAuOTYtMC4yMSAxLjY0LTEuMDMgMS42My0xLjk4di0yNC45N2gtNi43di02LjU1aC0yNC45NWMtMC45IDAuMjItMS41NSAwLjk3LTEuNiAxLjg2em03LjIxLTIxLjk4aDE4LjUzdjMuMzFoLTE4LjUzem0wIDYuM2gxOC41M3YzLjMxaC0xOC41M3ptMCA2LjNoMTguNTN2My4zNmgtMTguNTN6IiBmaWxsPSIjMDBiZWYyIi8+CiA8L2c+Cjwvc3ZnPgo=
        
このbase64画像はソースコード内の埋め込みで利用しますので、どこかのテキストファイルにコピーして控えておきましょう。

拡張機能の追加

ここからエクステンション本体の実装を行います。

scratch-vm/src/extensions以下に新しいフォルダをエクステンションに対応した名前で作ります。

ここでは
scratch3_fileioというフォルダ名にしておきます。

また、メインファイルの
index.jsここのテンプレートを参考に同フォルダ内に新規作成・追加しておきましょう。

もしくは以下のように今回のエクステンションのサンプルコードの雛形は以下のようになりますのでこれをコピーして利用してもらってもよいと思います。

            
            const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');

// 👇先程作成したアイコン画像
const commonSvgIcon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDJweCIgaGVpZ2h0PSI0MnB4IiBjb250ZW50PScmbHQ7bXhmaWxlIGhvc3Q9ImFwcC5kaWFncmFtcy5uZXQiIG1vZGlmaWVkPSIyMDIwLTEwLTA4VDA2OjU1OjEyLjgxOFoiIGFnZW50PSI1LjAgKFgxMSkiIHZlcnNpb249IjEzLjcuOCIgZXRhZz0iWExVelVlU252T3hYM29taTNiZWEiJmd0OyZsdDtkaWFncmFtIGlkPSJiTXp5dW5TbUNHanV6aFlfa2ttYSImZ3Q7alpOTmI0TXdESVovRGRlcUpGdTdIbGRLdDhNbVRlcGh4eW9GRjZJRmpFS2d0TDkrempDbHFLcTBDNG9mTy9IckR3SVpGZDJiVlZYK2lTbVlRTXpUTHBDYlFJam5NS1N2QitjZXlKWG9RV1oxMnFOd0JEdDlBWVp6cG8xT29aNEVPa1RqZERXRkNaWWxKRzdDbExWNG1vWWQwVXl6VmlxRE83QkxsTG1uM3pwMWVVOWZ4SExrNzZDemZNZ2NMbGE5cDFCRE1GZFM1eXJGMHcyU2NTQWppK2o2VTlGRllIenZocjcwOTdZUHZGZGhGa3Izbnd2YzkxYVpobXRqWGU0OEZOdUNkWnBxLzFBSE1GOVlhNmV4Sk5jQm5jTWlrT3ZjRllic2tJNUQ3S3ZSbVk5eFdCRlZiQ1VrQ2l5QjJsbjhnUWdOa3JVcHNhUlU2Nk0yWmtDQmtQUDVPdDRLSDV5cnlrc3B1c3d2MDB4ZEdndXp4R0NUN211d3JVNmczdE9ranpwcnJQTGk5dlNVZjdGQzdSUEdMZVd0V1NIWFN6cWhlOWl6OERvSjJtREFBcHc5VXdoZmtBc2VIbSt2V0xKOUduZmhpVkYrc3dZRFU3eDkyZlhsY1VCMDRCa041cmdMZjc2Ykgwckd2dz09Jmx0Oy9kaWFncmFtJmd0OyZsdDsvbXhmaWxlJmd0OycgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSItLjUgLS41IDQyIDQyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgcG9pbnRlci1ldmVudHM9ImFsbCI+CiAgPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTAgNC42MWMwLjMtMi40MyAyLjMtNC4zNCA0LjgyLTQuNjFoMjYuODhsOC4zIDcuOTZ2MjcuMDdjLTAuMTQgMi42Ni0yLjMzIDQuOC01LjA5IDQuOTdoLTMwLjA5Yy0yLjQ4LTAuMjUtNC40Ny0yLjExLTQuODItNC40OXptMy4yNyAzMC4xOGMwIDAuOTYgMC43IDEuNzkgMS42OCAxLjk4aDMwLjAyYzAuOTYtMC4yMSAxLjY0LTEuMDMgMS42My0xLjk4di0yNC45N2gtNi43di02LjU1aC0yNC45NWMtMC45IDAuMjItMS41NSAwLjk3LTEuNiAxLjg2em03LjIxLTIxLjk4aDE4LjUzdjMuMzFoLTE4LjUzem0wIDYuM2gxOC41M3YzLjMxaC0xOC41M3ptMCA2LjNoMTguNTN2My4zNmgtMTguNTN6IiBmaWxsPSIjMDBiZWYyIi8+CiA8L2c+Cjwvc3ZnPgo=';

const blockIconURI = commonSvgIcon;
const menuIconURI = commonSvgIcon;

class Scratch3FileIO {
    constructor (runtime) {
        this.runtime = runtime;
    }

    getInfo () {
        return {
            id: 'fileio',
            name: 'Custom File I/O',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                {
                    opcode: 'writeLog',
                    blockType: BlockType.COMMAND,
                    text: 'log [TEXT]',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "hello"
                        }
                    }
                },
                {
                    opcode: 'getBrowser',
                    text: 'browser',
                    blockType: BlockType.REPORTER
                }
            ],
            menus: {
            }
        };
    }

    writeLog (args) {
        const text = Cast.toString(args.TEXT);
        log.log(text);
    }

    getBrowser () {
        return navigator.userAgent;
    }
}

module.exports = Scratch3FileIO;
        
とりあえずこの雛形コードのまま、ビルドが通るかを次の節で試してみましょう。

余談〜ブロックアイコンが不要なとき

余談として、先程のように自分しか使わないようなエクステンションを作る際には、わざわざアイコン画像を作成するのも面倒です。

以下は画像なしの
index.jsを作りたいときのコードになります。

            
            const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');

class Scratch3FileIO {
    constructor (runtime) {
        this.runtime = runtime;
    }

    getInfo () {
        return {
            id: 'fileio',
            name: 'Custom File I/O',
            blocks: [
                {
                    opcode: 'writeLog',
                    blockType: BlockType.COMMAND,
                    text: 'log [TEXT]',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "hello"
                        }
                    }
                },
                {
                    opcode: 'getBrowser',
                    text: 'browser',
                    blockType: BlockType.REPORTER
                }
            ],
            menus: {
            }
        };
    }

    writeLog (args) {
        const text = Cast.toString(args.TEXT);
        log.log(text);
    }

    getBrowser () {
        return navigator.userAgent;
    }
}

module.exports = Scratch3FileIO;
        

こちらの方がコードとしてとてもスッキリしてプログラミングだけに集中できると思います。

vmの拡張機能リストに追加

新しく作成したエクステンションをScratch3へ組み込むには、エクステンションの機能を統括しているプログラムコードであるscratch-vm/src/extension-support/extension-manager.jsを編集します。

変更の内容は単純で、
builtinExtensionsのメンバーフィールドにエクステンションの名前で関数登録をするだけになります。

            
            const dispatch = require('../dispatch/central-dispatch');
const log = require('../util/log');
const maybeFormatMessage = require('../util/maybe-format-message');

const BlockType = require('./block-type');

// These extensions are currently built into the VM repository but should not be loaded at startup.
// TODO: move these out into a separate repository?
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods

const builtinExtensions = {
    //...中略
    //👇新規エクステンションのindex.jsのパスで登録
    fileio: () => require('../extensions/scratch3_fileio')
};

//...以下略
        
ではここまでで正常にビルドできるかscratch-guiのフォルダに入り、ローカルサーバーモードを立ち上げてみます。

            
            $ npm start
#OR
$ yarn start
        
サーバー起動後、http://localhost:8601にアクセスし、Scratch3の画面で拡張機能が新しく見えていたら正しく登録されています。

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

またこのエクステンションを追加すると、scratch3のブロックメニュー内に新しくブロックが追加されているはずです。

試しにサンプルとして紹介されていた2つのブロックを試すと、以下のようにブラウザのデバッグコンソールの出力する機能と使用中のブラウザを表示してくれる機能が動作しています。

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


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

スクラッチからブラウザーのファイル操作ダイアログを呼び出す

ゲームを途中から再開するためのセーブデータ中身を保存したり読み込んだりするための機能が最低限必要になります。

先ほど途中まで作りかけていたエクステンションのメインファイル
scratch-vm/src/extensions/scratch3_fileio/index.jsを編集していきます。

            
            Firefoxなどでは動かないので注意が必要です。
この記事では原則Google Chromeで動作確認をしています。
ローカルのファイルを選択する際に、ブラウザのファイルピッカーダイアログを利用します。
ブラウザのセキュリティ設計の関係によっては、
javascript側からのファイルピッカーの呼び出しが利用できない場合があります。
        
なお、今回のエクステンション機能のお試し特設サイトはこちらです。

エクステンション・プログラミングの基礎

スクラッチのエクステンションの作成手順は、こちらの記事に解説してある通り、何通りかのブロックタイプ(BlockType)から選択して、カスタマイズしたブロックの機能を盛り込んでいく流れになります。

ブロックタイプは何通りかあるとはいえ、既にプロジェクトにある他のエクステンションを参考にしても、基本的には以下の4つのタイプでほぼ構成されているようです。

            
            + COMMAND:
    関数を実行するブロック。
    引数を定義して利用することも可能
+ REPORTER:
    特定の文字列や数値などを返すブロック。
    引数を与えて利用することもできる
+ BOOLEAN:
    判定に特化したブロック。
    true/falseのどちらかを返す。
    引数を与えて真偽を判断して返すことも可能。
+ HAT:
    エクステンションのコード内部の変数(true/false)を監視して、
    trueのときにだけ処理を実行するブロック。
    イベントのトリガーとして利用できる
        
...ブロックとはいえ中身はjavascriptのコードそのものですが、これらのブロックにはコールバックなども直接仕込めないですし、同期処理もPromise程度なら利用できますがasync/await構文には対応していないようです。

つまるところ、一つのブロックで全ての機能を盛り込もうとすると、ブロックタイプの制約により上手く処理が実行出来ない場合あります。一つのブロックが出来るだけシンプルな実装になるように、できるだけ機能を分割しながら複数のブロックタイプで構成するような作り方にならざるを得ないことを留意しましょう。

ファイルから読み出す

まずはローカルからテキストサンプルを読み出しするブロックを作ります。

以下のようにindex.jsの
Scratch3FileIOクラスにファイルを呼び出すブロックを定義してきます。

            
            //...中略
class Scratch3FileIO {
    constructor (runtime) {
        this.runtime = runtime;

        //👇テキスト読み込み時の完了時のトリガーとして利用
        this.isReadText = false;

        //👇アップロードさせたテキストの中身を取得
        this.reader = new FileReader();
        this.reader.onload = () => {
            log.log(this.reader.result);
            this.isReadText = true;
        };

        //👇input要素を隠れdomとして作成
        this.input = document.createElement('input');
        this.input.type = 'file';
        this.input.accept = 'text/plain';
        this.input.addEventListener('change', this.clickCallback);
        this.clickCallback = (event) => {
            log.log(event.target.files[0]);
            this.reader.readAsText(event.target.files[0]);
        };
    }

    getInfo() {
        return {
            id: 'fileio',
            name: 'Custom File I/O',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                //...中略
                //👇readFile, loadedFile, getTextの3つのブロックを定義
                {
                    opcode: 'readFile',
                    text: 'Upload',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'loadedFile',
                    text: 'Loaded text',
                    blockType: BlockType.HAT
                },
                {
                    opcode: 'getText',
                    text: 'Show text',
                    blockType: BlockType.REPORTER
                }
            ],
            //...中略
        };
    }
    //...中略

    //👇readFile, loadedFile, getTextの3つのブロックに対応した関数の定義
    readFile() {
        if (this.input) {
            this.input.click();
        }
    }

    loadedFile() {
        const triggered = this.isReadText;
        this.isReadText = false;
        return triggered;
    }

    getText () {
        return this.reader.result;
    }

}
//...中略
        
まず、ブロックの定義をクラス関数のgetInfo内に定義しておきます。

カスタムブロックは
opcodeプロバティで指定した関数がそのままブロックの処理の実体になります。textはブロックに表示される見出しの名前です。

今回はローカルのファイルを呼び出すだけですが、普段のjsでやっているようなテクニックがそのまま利用できるわけではありません。

readFileではまずブラウザのファイルピッカーを呼び出すだけの処理を行っています。

これにはクラスのコンストラクタ内で仕込んだ
type=fileにしたinput要素を擬似的にクリックしているだけの機能ですので、これだけを切り取ってCOMMANDブロックタイプとして割り当てています。

上でも述べたように直接コールバックに対応した機能を持つブロック内ので、ファイルピッカーからファイルを選んだ後、ファイルの読み込み完了を知らせるトリガーが必要になります。

これには
HATブロックタイプを割り当てたloadedFileを使って、コンストラクタで定義したisReadTextフィールドの真偽の切り替えによりイベントトリガーとなるブロックを作ります。

これによりファイルを読み込んだタイミングをスクラッチが検知できるようになります。

アップロードされたファイルの中身はクラスメンバのFileReaderインスタンスの
readerresultプロバティから取得できます。

getTextREPORTERブロックタイプとして割り当て、loadedFileと組み合わせて使うことで、ファイルの中身をこのブロックから利用できるようになります。

以上の三段構えで、ブラウザからスクラッチを通してファイルがアップロードできるようになります。

試しに以下のような内容のテキストファイルを読ませてみます。

            
            たこ:とてもげんぎ
x座標:-132
y座標:-82
スコア:520
ステージ:うみのなか
プレイヤー:あなた
        
こちらでも試せますが、エクステンションを追加し、この3つのブロックを下のように組んで使うとテキストファイルの内容が反映できていることが分かります。

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

ファイルとしてダウンロードする

次にゲームの状態をセーブデータのテキストファイルとしてダウンロードさせる簡単な機能も作成します。

index.jsにさらに以下の内容を追加します。

            
            //...中略
class Scratch3FileIO {
    constructor (runtime) {
        //...中略

        //👇擬似的なa要素をdownload属性付きで追加
        this.downloadLink = document.createElement('a');
        this.downloadLink.setAttribute('href', '#');
        this.downloadLink.setAttribute('download', 'savedata.txt');
    }

    getInfo() {
        return {
            id: 'fileio',
            name: 'Custom File I/O',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                //...中略
                //👇writeFileのブロックを定義
                {
                    opcode: 'writeFile',
                    text: 'Download [TEXT]',
                    blockType: BlockType.COMMAND,
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: 'hello'
                        }
                    }
                }
            ],
            //...中略
        };
    }
    //...中略

    //👇writeFileのブロックに対応した関数の定義
    writeFile(args) {
        this.downloadCallback = () => {
            const content = Cast.toString(args.TEXT);
            const blob = new Blob([ content ], { "type" : "text/plain" });
            if (window.navigator.msSaveBlob) {
                window.navigator.msSaveBlob(blob, "test.txt");
            } else {
                this.downloadLink.href = window.URL.createObjectURL(blob);
            }
        };

        this.downloadLink.addEventListener('click', this.downloadCallback);
        this.downloadLink.click();
        this.downloadLink.removeEventListener('click', this.downloadCallback);
    }
}
//...中略
        
名前固定のデータのダウンロードの場合にはテキストファイルのアップロードよりも単純です。

まず
getInfowriteFileブロックをCOMMANDブロックタイプとして登録し、TEXTという引数を一つ取れるようにしておきます。この変数TEXTの部分にセーブデータとしてダウンロードしたい中身が入るようになりました。

この引数を文字列として変換するために
Cast.toStringを利用しています。

また変換した文字列を
Blobクラスに経由で、ObjectURL形式にすることでファイルをdownload指定した擬似のa要素からファイル形式でダウンロードできる仕組みとなっています。

また擬似要素をクリックさせるために
addEventListener > click > removeEventListenerでイベントの付け外しのようなトリッキーなことをしています。

こうしておかないとスクラッチプログラムが暴走してしまうので、エクステンション作成のときの擬似要素のイベントコールバックの扱いには注意が必要です。

では最後に折角ですので、このダウンロードするブロックを使ってみます。

実際にゲームによってセーブデータとして残すデータ構造は違うので、セーブデータを組み立てるブロックは各自でコーディングしてもらうとして、今回は先ほどアップロードしたテキストの内容をそのままダウンロードすると以下のようになります。

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


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

まとめ

公式のScratch3ではゲームの中断・再開を行わせる機能がサポートされていないので、そのようなゲームを作りたいなら今回のような独自のエクステンションを作ってユーザーにセーブデータの呼び出し、セーブデータの保存の機能を提供してあげる必要があります。

実例でお見せしたように、ゲームのセーブ機能を作り込むならjavascriptとhtmlのプログラミングにある程度使いこなす知識が必要となります。

エクステンション作成は自身のゲームをさらに高度に拡張したい場合には避けては通れないので、腕試しにチャレンジしてみてはいかがかと思います。