🐱

デスクトップ版 Scratch の拡張機能の開発環境の構築方法

2022/07/18に公開

デスクトップ版 Scratch(以下 Scratch Desktop)の拡張機能の開発環境を構築する方法です。

なぜ Scratch Desktop なのか

Scratch の拡張機能を開発する場合、通常は scratch-vm と scratch-gui をクローンし、拡張機能を実装します。こうして開発した拡張機能はブラウザ上で動作させることができます。しかし、ブラウザの上で動く以上、ブラウザで動作しない機能は実現できません。具体的には、TCP/UDPで通信する、ローカルの別プロセスと通信する、といったことができません。

Scratch Desktop を使用すれば、Node.js により自由な機能を持つ拡張機能を実装することができます。

開発用ディレクトリの準備

この記事では scratch-ext ディレクトリをプロジェクトルートとします。

mkdir scratch-ext
cd scratch-ext

リポジトリの clone

拡張機能の開発には、以下のリポジトリに変更を加える必要があります。

まずはこれらのリポジトリを clone します。
各リポジトリのバージョンを合わせる必要があります。この記事では以下のバージョンで進めます。

  • scratch-vm: 0.2.0-prerelease.20220222132735
  • scratch-gui: scratch-desktop-v3.29.0
  • scratch-desktop: v3.29.1

バージョンごとにタグまたはブランチが切られているので、バージョンを指定して clone します。

git clone --filter=blob:none https://github.com/LLK/scratch-vm.git -b 0.2.0-prerelease.20220222132735
git clone --filter=blob:none https://github.com/LLK/scratch-gui.git -b scratch-desktop-v3.29.0
git clone --filter=blob:none https://github.com/LLK/scratch-desktop.git -b v3.29.1
--filter=blob:none について

--filter=blob:noneブロブレスクローンを行うための引数です。
過去のコミットをダウンロードしますが、ブロブ(ファイルの内容)をダウンロードしません。
ブロブをダウンロードするためには膨大な時間を必要としますが、拡張機能の開発にあたってはブロブをダウンロードしておく必要がないので、ダウンロードしません。

リポジトリを準備する

それぞれのリポジトリは Node.js のプロジェクトなので、それぞれで npm install を実行していきます。
scratch-desktop は scratch-gui に依存しており、scratch-gui は scratch-vm に依存しています。拡張機能を開発するにあたっては、すべてのリポジトリに変更を加えていくので、それらの変更が依存関係に反映される必要があります。
そのために、npm install に加え、リポジトリをリンクする設定を行います。

scratch-vm

はじめに、scratch-vm をセットアップします。

cd scratch-vm
npm install
npm link
cd ..

scratch-gui

次に、scratch-gui をセットアップします。
scratch-gui がローカルの scratch-vm を利用できるようにします。

cd scratch-gui
npm install
npm link scratch-vm
npm link
cd ..

scratch-gui/node_modules/scratch-vm../../scratch-vm へのシンボリックリンクになっていればOKです。

ls -la scratch-gui/node_modules | grep scratch-vm
lrwxr-xr-x owner staff  16 B  Sun Jul 17 01:09:50 2022 scratch-vm ⇒ ../../scratch-vm

scratch-desktop

次に、scratch-desktop をセットアップします。
scratch-desktop がローカルの scratch-gui を利用できるようにしますが、npm link は使用しません。
scratch-desktop は scratch-gui の依存である autoprefixer が scratch-desktop/node_modules/ 以下に配置されることを前提にコードが書かれています。npm link scratch-gui するとこのディレクトリに autoprefixer が存在しなくなるため、正しく動作しません。
そのため、npm link を使わずにシンボリックリンクを張ります。

cd scratch-desktop
npm install
cd node_modules
rm -rf scratch-gui
ln -s ../../scratch-gui scratch-gui
cd ../../

scratch-desktop/node_modules/scratch-gui../../scratch-gui へのシンボリックリンクになっていればOKです。

ls -la scratch-desktop/node_modules | grep scratch-gui   
lrwxr-xr-x owner staff  17 B  Sun Jul 17 01:13:07 2022 scratch-gui ⇒ ../../scratch-gui

開発環境の動作確認

Scratch Desktop が起動することを確かめます。

cd scratch-desktop
npm start

少し待ち、Scratch Desktop が起動すればOKです。

拡張機能の動作確認

現在時刻を返すブロックを追加する拡張機能を実装します。

本記事では、具体的な実装方法については最小限の解説に留めます。拡張機能の実装方法については、既存の拡張機能のソースコードを参考にすることをお勧めします。

scratch-vm

src/extensions/scratch3_${extension_name} 以下に拡張機能のブロックと動作の定義を書きます。

今回は現在時刻の unixtime を返す now ブロックを追加します。

src/extensions/scratch3_ext/index.js
const BlockType = require('../../extension-support/block-type');

/**
 * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABWhJREFUeNrsW+1x4zgMZXb2v9KB04GyFSgd6K4CbQfaq0DbgdKBLhUoHchbgXwVyKlATgU5awacwAhIQh92bAdvBpPYsinqEQQeQdoYhUKhUCgUCoVCoVAoFAqF4mJwcyb9WO3tznFts7dXJfAQ6d4ewO6F39mCbcDWe3v5agQWe/vJeNsa/u6AnDvymQePdz7u7enaQ0Wyt25vb2DD//ne4hFt1PDdCqxH7bUj27ooZORBk4nt2DYi0rYdmP7ayasnfD+GNirkuRkMAva4EuyqYuCQKJ7R6x3Esluwe0dM28H/vz2xj37HJpfhfv9dg+dFKEYNsa5BnnhsS66BwBwepkHT7xTWkhh5dHw7cvvPIFuOjQ3c58epRfexdWAMGu92ZjtWH1LduB65UkmB6EE7/rmU6bxidJskjpXodbZQPySxMjlVGIhJzEuFHlCApGmJsM6InivRtaXEPCawZ4jKyfUa+rUYoSnxphjeszddjWyvJCTaRFCg94sjEWiTHHWKhqya3tAKaDXlxhE8RMfcPEJLrKliOQ1M7/qIBPrCQwTXauKV2VjiemYNi0eCEruaOEh4ytak3WqBaeQiUBIiqI4tJDfriLclgTXqGxLOcx6wIUIcP6jVlXPX09RigabtSYxMJetXG5PGdKqZmSU7ofhuJ2Rn14oo9/SpJ4RlniR0QF7JfFk6qlPRBWJhzMTjHt6TTPHM0bZrSraOONxwxMdMYPWy7SFwarxKR3hIRqSQlEjOC0uPQuiYNlNutrUoYHM3rAOjtIQHUsHbC+JdQkjpAjEtcgxS6fDUOOA4BwGcYxvHAS7mVCNjZkic92ZalTkhg5mPnDVWQcTCFVCPCawCwdQ3IrVgOoQkTEYGop4RBiriwZwA7jyhonfMRK8HdgINV6JOxZ7ONMJpmjPkTxkAOhitg5wGeVQoy1fCMNOzbApGF5MorclZT/MVV7OFyLN9WBE1YfveBeSRtN5ZjyUQJ42exAvXCKbMiPfMEimeGTtpRi5Jm6HBkxZkIzQAGSUlFo50JegIt36ugdA5ySKUeBpmsBogtEDXO2a1Iylc1FyoKoVz31VJCVmPHqAgDxwtRB724hju13r6VDK6s/I4TYrI72m+WAnTv1TZS6wy8+C7d0PMtfrACwXOYxtmEJzhpmDSfyaorvSBhyigLVxnm1uiyslAWBnUCgauQwTWKJYlghj5oSp0w2TZn45Or5n3HiYSMOxJ/DND59k+Dn+fyFQb9pu35uP5m+G9FyBqeJaNed+bHvZsXkkbD2A7+PyzYQ4z3ThG97eRbwRxnX02h5tAFnfw0MO1HxNkyhoebgftDH38y7xv2HN92yLC1tCHzUKD6RWKFeP6CbIVEz8lRVWJZIrhHjmTeDpHhh+zd7xUEhtVzm+FFY5QcqhRFozQOrY0pzvBUJgzQ4yqxaERTUmyejuxzRXtZ4Huk4jD9cLIXOg5muSEhDVIMEcO2XZxJEaOKswYa2buv2ACZ8XDb59A4KC3/kaS5l+H5OGwIZp0zpmbW4++FeP7J3riC4jgJ5KM7OHLW6TlHglZW/h7P+P+dhGwM18Aufl4MGjO9gHewvgyoFpzTgxszlUPjtGNY08wrAx/WmHsURJcVY8u3ZvGFFMjj/CuTPh4XUyy/0WfpY7Nx8NDIU/KHEVaTuJUaA3NHVfLzBUgIjVIS2Ts+KwlITeHJ8QieC2pBfbmSk7xhyo/dk8lM4e/RpIkDd926eIx7+bMiPwF9b07x2cezXuB0yC9aLXjPdGLOxDqj+YMftl56vVyKZyWXMleklSuzgNDhN56Vh5b8/5zr1ejUCgUCoVCoVAoFAqFQqFQKDj8L8AArqESEfsu3jMAAAAASUVORK5CYII=';

/**
 * Icon svg to be displayed in the category menu, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABWhJREFUeNrsW+1x4zgMZXb2v9KB04GyFSgd6K4CbQfaq0DbgdKBLhUoHchbgXwVyKlATgU5awacwAhIQh92bAdvBpPYsinqEQQeQdoYhUKhUCgUCoVCoVAoFAqF4mJwcyb9WO3tznFts7dXJfAQ6d4ewO6F39mCbcDWe3v5agQWe/vJeNsa/u6AnDvymQePdz7u7enaQ0Wyt25vb2DD//ne4hFt1PDdCqxH7bUj27ooZORBk4nt2DYi0rYdmP7ayasnfD+GNirkuRkMAva4EuyqYuCQKJ7R6x3Esluwe0dM28H/vz2xj37HJpfhfv9dg+dFKEYNsa5BnnhsS66BwBwepkHT7xTWkhh5dHw7cvvPIFuOjQ3c58epRfexdWAMGu92ZjtWH1LduB65UkmB6EE7/rmU6bxidJskjpXodbZQPySxMjlVGIhJzEuFHlCApGmJsM6InivRtaXEPCawZ4jKyfUa+rUYoSnxphjeszddjWyvJCTaRFCg94sjEWiTHHWKhqya3tAKaDXlxhE8RMfcPEJLrKliOQ1M7/qIBPrCQwTXauKV2VjiemYNi0eCEruaOEh4ytak3WqBaeQiUBIiqI4tJDfriLclgTXqGxLOcx6wIUIcP6jVlXPX09RigabtSYxMJetXG5PGdKqZmSU7ofhuJ2Rn14oo9/SpJ4RlniR0QF7JfFk6qlPRBWJhzMTjHt6TTPHM0bZrSraOONxwxMdMYPWy7SFwarxKR3hIRqSQlEjOC0uPQuiYNlNutrUoYHM3rAOjtIQHUsHbC+JdQkjpAjEtcgxS6fDUOOA4BwGcYxvHAS7mVCNjZkic92ZalTkhg5mPnDVWQcTCFVCPCawCwdQ3IrVgOoQkTEYGop4RBiriwZwA7jyhonfMRK8HdgINV6JOxZ7ONMJpmjPkTxkAOhitg5wGeVQoy1fCMNOzbApGF5MorclZT/MVV7OFyLN9WBE1YfveBeSRtN5ZjyUQJ42exAvXCKbMiPfMEimeGTtpRi5Jm6HBkxZkIzQAGSUlFo50JegIt36ugdA5ySKUeBpmsBogtEDXO2a1Iylc1FyoKoVz31VJCVmPHqAgDxwtRB724hju13r6VDK6s/I4TYrI72m+WAnTv1TZS6wy8+C7d0PMtfrACwXOYxtmEJzhpmDSfyaorvSBhyigLVxnm1uiyslAWBnUCgauQwTWKJYlghj5oSp0w2TZn45Or5n3HiYSMOxJ/DND59k+Dn+fyFQb9pu35uP5m+G9FyBqeJaNed+bHvZsXkkbD2A7+PyzYQ4z3ThG97eRbwRxnX02h5tAFnfw0MO1HxNkyhoebgftDH38y7xv2HN92yLC1tCHzUKD6RWKFeP6CbIVEz8lRVWJZIrhHjmTeDpHhh+zd7xUEhtVzm+FFY5QcqhRFozQOrY0pzvBUJgzQ4yqxaERTUmyejuxzRXtZ4Huk4jD9cLIXOg5muSEhDVIMEcO2XZxJEaOKswYa2buv2ACZ8XDb59A4KC3/kaS5l+H5OGwIZp0zpmbW4++FeP7J3riC4jgJ5KM7OHLW6TlHglZW/h7P+P+dhGwM18Aufl4MGjO9gHewvgyoFpzTgxszlUPjtGNY08wrAx/WmHsURJcVY8u3ZvGFFMjj/CuTPh4XUyy/0WfpY7Nx8NDIU/KHEVaTuJUaA3NHVfLzBUgIjVIS2Ts+KwlITeHJ8QieC2pBfbmSk7xhyo/dk8lM4e/RpIkDd926eIx7+bMiPwF9b07x2cezXuB0yC9aLXjPdGLOxDqj+YMftl56vVyKZyWXMleklSuzgNDhN56Vh5b8/5zr1ejUCgUCoVCoVAoFAqFQqFQKDj8L8AArqESEfsu3jMAAAAASUVORK5CYII=';

/**
 * Class for the Ext
 * @param {Runtime} runtime - the runtime instantiating this block package.
 * @constructor
 */
class Scratch3Ext {
    constructor (runtime) {
        /**
         * The runtime instantiating this block package.
         * @type {Runtime}
         */
        this.runtime = runtime;
    }

    /**
     * @returns {object} metadata for this extension and its blocks.
     */
    getInfo () {
        return {
            id: 'ext',
            name: 'Ext',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                {
                    opcode: 'now',
                    text: 'now',
                    blockType: BlockType.REPORTER
                }
            ]
        };
    }

    now () {
        return Date.now();
    }
}
module.exports = Scratch3Ext;

src/extension-support/extension-manager.js を編集して、拡張機能をVMに登録します。

diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js
index 7cb556c5..4aaa33d8 100644
--- a/src/extension-support/extension-manager.js
+++ b/src/extension-support/extension-manager.js
@@ -13,6 +13,7 @@ const builtinExtensions = {
     // 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.
+    ext: () => require('../extension/scratch3_ext'),
     pen: () => require('../extensions/scratch3_pen'),
     wedo2: () => require('../extensions/scratch3_wedo2'),
     music: () => require('../extensions/scratch3_music'),

scratch-gui

scratch-vm に登録した拡張機能をGUIから呼び出すためのメニューを src/lib/libraries/extensions/index.jsx に記述します。

diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx
index ba18b916..79f52f3f 100644
--- a/src/lib/libraries/extensions/index.jsx
+++ b/src/lib/libraries/extensions/index.jsx
@@ -47,6 +47,24 @@ import gdxforConnectionIconURL from './gdxfor/gdxfor-illustration.svg';
 import gdxforConnectionSmallIconURL from './gdxfor/gdxfor-small.svg';
 
 export default [
+    {
+        name: (
+            <FormattedMessage
+                defaultMessage="Ext"
+                description="Name for the 'Ext' extension"
+                id="gui.extension.ext.name"
+            />
+        ),
+        extensionId: 'ext',
+        description: (
+            <FormattedMessage
+                defaultMessage="Ext with Scratch 3.0"
+                description="Description for the 'Ext' extension"
+                id="gui.extension.ext.description"
+            />
+        ),
+        featured: true
+    },
     {
         name: (

scratch-desktop

scratch-desktop のコードに手を加える必要はありません。

npm start で Scratch Desktop を起動します。

cd scratch-desktop
npm start

拡張機能の選択画面を開くと、追加した拡張機能が表示されます。

拡張機能を選択すると、追加したnowブロックが表示されています。

追加したnowブロックからは現在時刻が返ってきています。

これで動作確認は完了です。

おわりに

Scratch Desktop を使用する場合、scratch-vm 以下のコードは Node で実行されるので、scratch-vm 以下に任意の Node.js のコードを記述することで、ブラウザの制約に縛られず、自由な機能を持つ拡張機能を開発することができます。

具体的な実装方法については、私が作成した、トイドローンの Tello を Scratch 3.0 でプログラミングするための拡張機能である kebhr/scratch3-tello を参考にしてください。

Discussion