【Scratch3.0】Realtime Database拡張ブロックを作ったので参照実装したら既視感あるアレができちゃった件

# 事始め

訳あってScratchの自主練をしていたところ、楽しそうな記事を見つけました。

* [Scratch 3.0でオリジナルブロックをつくろう - @Hiroyuki_OSAKI](https://qiita.com/Hiroyuki_OSAKI/items/a46e1c881d3aed4661f7)

そんなことができるのですね！

そしたら、[Realtime Database](https://firebase.google.com/docs/database?hl=ja) のブロックとかつくって、ネットワークゲームも制作できるようになる......!!!

これはやるしかないでしょ、ということで。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やってみたいぃぃい✨<br><br>Scratch 3.0 で Web API 呼び出しの拡張ブロックを作る、の手始め - <a href="https://t.co/oJ6pjzKviS">https://t.co/oJ6pjzKviS</a></p>&mdash; kami_teru (@kami_teru) <a href="https://twitter.com/kami_teru/status/1363839618446094336?ref_src=twsrc%5Etfw">February 22, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

やってみた事とその結果をまとめます。
なおNode.jsは今回初めて触りましたので、変な事していたらそこは温かい目で見守っていただければ幸いです。

# 環境構築

Windows上で行います。作業ディレクトリは次の場所にしました。

```powershell
mkdir C:\Users\kami_teru\source\repos\scratch\
cd C:\Users\kami_teru\source\repos\scratch\
```

以下の記事を参考に、Node.jsをインストールし、scratch-gui をlocalhostで起動できるようにしました。

* [Scratch3.0 拡張機能を作って GitHub Pagesで公開する手順メモ - @natsumo](https://qiita.com/natsumo/items/82117d0b76903f10ecb2)

一つだけ注意点。私のWindows環境では `npm run deploy` を実行したとき、次のエラーが出ました。

```powershell:powershell
PS C:\Users\kami_teru\source\repos\scratch\scratch-gui> npm run deploy

> scratch-gui@0.1.0 deploy C:\Users\kami_teru\source\repos\scratch\scratch-gui
> touch build/.nojekyll && gh-pages -t -d build -m "Build for $(git log --pretty=format:%H -n1) [skip ci]"

'touch' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
```

`touch`コマンドは、Git for Windowsをインストールしている方なら、次のようにすれば利用可能になります。

```powershell:powershell
$ENV:Path+=";c:\Program Files\Git\usr\bin"
```

さてさてこれで、Scratchを試せるようになりました。

* Scratch 3.0 GUI - 127.0.0.1:8601
![Scratch 3.0 GUI - teru.k.business - Microsoft​ Edge 2021_02_26 9_39_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/8394e531-febc-3d13-1dc4-bded10789fb9.png)


次は、Realtime Database の準備を行います。

# Realtime Database のセットアップ

## 1) Firebase プロジェクトの作成

[Firebaseコンソール](https://console.firebase.google.com/)にアクセスし、プロジェクトを追加します。

|設定項目|値|
|---|---|
|名前|(任意の名前)|
|Google アナリティクス|無効にする|

アナリティクスは有効にすると、これから作る拡張ブロックの利用頻度などが取得できるようになるのでお勧めですが、本記事に関係のない情報なので無効にします。

## 2) Firebase プロジェクトに Webアプリ を追加

Scratch 3.0はNode.jsで動作するクライアントサイドのWebアプリなので、次のようにします。

|設定項目|値|
|---|---|
|アプリのニックネーム|(任意の名前)|
|Firebase Hosting|設定しない|

LLK/scratch-gui は、Github Pages にデプロイする仕組みを備えており、本記事もそれを利用します。よって Firebase Hosting は設定しません。

Webアプリを登録すると、 `Firebase SDK の追加` のページで「この作業は Firebase サービスを使用する前に行ってください。」という情報が表示されますが、あとで拡張ブロックからやるので、ここではスキップしました。

## 3) Firebase プロジェクトに Realtime Database を追加

Firebase コンソールの構築メニューから Realtime Database を選択。「データベースを作成」をクリックします。

|設定項目|値|
|---|---|
|Realtime Databaseのロケーション|米国 (us-central1)|
|セキュリティルール|テストモードで開始|

拡張ブロックの動作確認までがとりあえずの目的なので、セキュリティルールはテストモードにしました。

## 4) Realtime Database への接続情報の確認

「プロジェクトの設定」ページを開くと、全般タブの「マイアプリ」セクションに作成した Webアプリ があります。それを選択すると、 Firebase SDK snippet 欄に次のような接続情報が表示されます。

```html
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-analytics.js"></script>

<script>
  // Your web app's Firebase configuration
  // For Firebase JS SDK v7.20.0 and later, measurementId is optional
  var firebaseConfig = {
    apiKey: "AI***********************************WQ",
    authDomain: "************.firebaseapp.com",
    databaseURL: "https://************-default-rtdb.firebaseio.com"
    projectId: "************",
    storageBucket: "************.appspot.com",
    messagingSenderId: "************",
    appId: "1:************:web:**********************",
    measurementId: "G-**********",
    };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  firebase.analytics();
</script>
```
※`***`の部分が、作成したFirebaseプロジェクト固有の部分。


以上で準備完了！では Realtime Database 拡張ブロック づくりへ。

# Realtime Database 拡張ブロックの作成

## Scratch に Realtime Database 拡張機能 を追加

Scratch で Realtime Database 拡張ブロックを表示できるようにするための機能情報を記述するために、`C:\Users\kami_teru\source\repos\scratch\scratch-gui` のソースを編集します。

機能名は　`realtimedb`とします。
`src/lib/libraries/extensions/index.jsx` に以下を追加。

```jsx:src/lib/libraries/extensions/index.jsx
...(中略)...
import realtimedbIconURL from './realtimedb/realtimedb.png';
import realtimedbInsetIconURL from './realtimedb/realtimedb-small.png';

export default [
...(中略)...
    {
        name: 'Realtime Database',
        extensionId: 'realtimedb',
        iconURL: realtimedbIconURL,
        insetIconURL: realtimedbInsetIconURL,
        description: (
            <FormattedMessage
                defaultMessage="Connect to Realtime Database."
                description="Realtime Database"
                id="gui.extension.realtimedb.description"
            />
        ),
        featured: true,
        internetConnectionRequired: true
    }
]
```

`src/lib/libraries/extensions/realtimedb/` 配下に、画像ファイルを配置。
[Firebaseブランドガイドライン](https://firebase.google.com/brand-guidelines) から適当なものをピックアップ＆サイズを調整。

* realtimedb.png
![realtimedb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/a7706689-67c8-2ec0-fec9-2302dfc8fee2.png)

* realtimedb-small.png
![realtimedb-small.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/9b6c54fe-633f-ceae-9ce3-1f209b3a02e1.png)

## Realtime Database 拡張機能 の最低限の実装

いよいよ拡張ブロックの実装です。`C:\Users\kami_teru\source\repos\scratch\scratch-vm` のソースを編集。

まずは SDKの組み込み。Node.js から Realtime Database を利用する場合、Firebase SDK が便利です。
次のようにして、Firebase SDK をインストール。

```powershell:powershell
cd C:\Users\kami_teru\source\repos\scratch\scratch-vm
npm install firebase --save
```

次に、Webサイトの Firebase認証 を済ませるための最低限の実装を行います。
こんなブロックを実装していきます。
![スクリーンショット 2021-02-26 112416.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/30d58592-82e8-245d-7a4e-d3c60d83d158.png)

```js:src/extensions/scratch3_realtimedb/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 firebase = require('firebase/app');
//require('firebase/analytics');
require('firebase/database');

const firebaseConfig = {};

class Scratch3Realtimedb {
    constructor (runtime) {
        this.runtime = runtime;
        this._firebaseApp = undefined;
        this._realtimedb = undefined;
    }
    get firebaseApp () {
        if (this._firebaseApp == undefined)
        {
            this._firebaseApp = firebase.default.initializeApp(firebaseConfig);
            //firebase.default.analytics();
        }
        return this._firebaseApp;
    }
    get realtimeDb () {
        if (this._realtimedb == undefined)
        {
            this._realtimedb = this.firebaseApp.database();
        }
        return this._realtimedb;
    }
    getInfo () {
        return {
            id: 'realtimedb',
            name: 'Realtime Database',
            blocks: [
                {
                    opcode: 'setApiKey',
                    blockType: BlockType.COMMAND,
                    text: '【設定1】apiKey は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setAuthDomain',
                    blockType: BlockType.COMMAND,
                    text: '【設定2】authDomain は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setDatabaseURL',
                    blockType: BlockType.COMMAND,
                    text: '【設定3】databaseURL は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setProjectId',
                    blockType: BlockType.COMMAND,
                    text: '【設定4】projectId は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setStorageBucket',
                    blockType: BlockType.COMMAND,
                    text: '【設定5】storageBucket は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setMessagingSenderId',
                    blockType: BlockType.COMMAND,
                    text: '【設定6】messagingSenderId は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setAppId',
                    blockType: BlockType.COMMAND,
                    text: '【設定7】appId は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setMeasurementId',
                    blockType: BlockType.COMMAND,
                    text: '【設定8】measurementId は [TEXT] を使う',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "Firebase SDK snippetから貼り付けてね"
                        }
                    }
                },
                {
                    opcode: 'setText',
                    blockType: BlockType.COMMAND,
                    text: '[PATH] の [VALUENAME] に [TEXT] をセット',
                    arguments: {
                        PATH: {
                            type: ArgumentType.STRING,
                            defaultValue: "データパス"
                        },
                        VALUENAME: {
                            type: ArgumentType.STRING,
                            defaultValue: "データ項目名"
                        },
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "データ値"
                        }
                    }
                }
            ],
            menus: {
            }
        };
    }
    setApiKey(args)
    {
        firebaseConfig.apiKey = Cast.toString(args.TEXT);
    }
    setAuthDomain(args)
    {
        firebaseConfig.authDomain = Cast.toString(args.TEXT);
    }
    setDatabaseURL(args)
    {
        firebaseConfig.databaseURL = Cast.toString(args.TEXT);
    }
    setProjectId(args)
    {
        firebaseConfig.projectId = Cast.toString(args.TEXT);
    }
    setStorageBucket(args)
    {
        firebaseConfig.storageBucket = Cast.toString(args.TEXT);
    }
    setMessagingSenderId(args)
    {
        firebaseConfig.messagingSenderId = Cast.toString(args.TEXT);
    }
    setAppId(args)
    {
        firebaseConfig.appId = Cast.toString(args.TEXT);
    }
    setMeasurementId(args)
    {
        firebaseConfig.measurementId = Cast.toString(args.TEXT);
    }
    setText(args) {
        const path = Scratch3Realtimedb.parsePathInput(args.PATH);
        const valueName = Cast.toString(args.VALUENAME);
        const text = Cast.toString(args.TEXT);
        var data = {};
        data[valueName] = text;
        this.realtimeDb.ref(path).set(data);
    }
}
module.exports = Scratch3Realtimedb;
```
※アナリティクスを利用する場合はコメントアウトの箇所を有効にします。

最低限の実装ができたら、それを機能の一覧に登録するために記述を追加。

```jsx:src/extension-support/extension-manager.js

const builtinExtensions = {
...(中略)...
    realtimedb: () => require('../extensions/scratch3_realtimedb')
};
```

では、ローカルで動作確認してみます。ビルドしてスタート。

```powershell:powershell
cd C:\Users\kami_teru\source\repos\scratch\scratch-vm
yarn install; yarn link
cd C:\Users\kami_teru\source\repos\scratch\scratch-gui
yarn link scratch-vm; yarn install
yarn start
```

* Realtime Database 拡張機能が表示されていますね！
![Scratch 3.0 GUI - teru.k.business - Microsoft​ Edge 2021_02_26 11_38_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/0c642144-5576-47ea-aaf5-06c7579c36be.png)

* Firebase認証を通すためのプログラムを作って、一度実行。※アナリティクスを利用する場合は、設定8も実行します。
![スクリーンショット 2021-02-26 115154.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/61736792-d31c-cccc-de1c-0aeb59028414.png)
※上記の認証情報は記事公開時点で削除しているので利用できません。

一度目はFirebase認証を通すだけになるので、Realtime Databaseへの書き込みは失敗するようです。
でもこれでWebサイトとしての認証が通りました。

* 以降は次の設定だけのプログラムで、RealtimeDatabaseに接続できます。
![スクリーンショット 2021-02-26 115509.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/1cf8072c-55a8-d7c9-f060-7f47f9bd9abd.png)

* ちゃんと書き込めていますね！
![scratch-link - Firebase コンソール - teru.k.business - Microsoft​ Edge 2021_02_26 11_57_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/03b8f2ee-5b2d-8ac8-68cb-37dfa089fa68.png)

これと同じ手順を、Github Pagesでも行います。デプロイ。

```powershell:powershell
yarn run build
yarn run deploy
```

同じように、Github Pagesに公開した Scratch からも Realtime Database に書き込めることが確認できました。

## Realtime Database 拡張機能 の本実装

以下の技術情報を参考に、さらに機能を付け加えていきます。

* https://firebase.google.com/docs/web/setup?hl=ja#node.js-apps
* https://firebase.google.com/docs/database/web/start?hl=ja
* https://firebase.google.com/docs/database/web/read-and-write?hl=ja

以下、実装コードは、github のリンクから併読ください。

|機能|ブロックイメージ|ブロック情報|実装|
|---|---|---|---|
|データ階層の表現|![スクリーンショット 2021-02-26 120453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/39102684-622f-9944-fb36-8046a6f76bed.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L135-L149)|[concatPath](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L274-L278)|
|文字データの送信|![スクリーンショット 2021-02-26 122506.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/1f6fb3c8-9d4d-67c8-ba2e-163a8586d050.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L150-L168)|[setText](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L280-L288)|
|データ変更の監視|![スクリーンショット 2021-02-26 122706.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/fa42095c-c30a-33dc-324c-c7bf86fd7098.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L169-L179)|[listen](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L290-L297)|
|データ追加の監視|![スクリーンショット 2021-02-26 122734.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/82c0e871-46a3-4297-bed9-1b5742ace83d.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L206-L216)|[listenChildAdd](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L299-L306)|
|データ受信の判定|![スクリーンショット 2021-02-26 122758.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/07153f70-c7e2-765b-2aa9-640a46ad9dcf.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L290-L297)|[whenReceived](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L308-L323)|
|文字データの取得|![スクリーンショット 2021-02-26 122831.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/d17b8c92-6aa2-f091-5e5d-d838f2f3195d.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L191-L205)|[receiveText](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L325-L340)|
|追加された子階層名の取得|![スクリーンショット 2021-02-26 122859.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/37ff9eb3-bd45-ad89-2add-d153831191bf.png)|[getInfo](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L217-L227)|[receiveChildKey](https://github.com/atsuteru/scratch-vm/blob/realtimedb_v1.0/src/extensions/scratch3_realtimedb/index.js#L342-L352)|

ではいよいよ、これを使い方を示すものとなる参照実装を行います。

# Realtime Database 拡張ブロック の参照実装

## あいさつをほかのユーザーと共有してみた

まずは指定したパスの「データ変更の監視」と「データ受信の判定」と「文字データの取得」を中心に確認。
こんなプログラムにしてみました。
![スクリーンショット 2021-02-26 161941.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/b21ca257-87a2-d193-7647-662cb06fa9fb.png)

動かしてみましょう。
動作を確認するために、簡単なページを作りました。
※Github-Pagesのscratch-guiをiframeで呼び出してちょっと小細工したものです。
![test01-1-min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/b36074db-e5ac-6e0c-377b-93fae8ec045f.gif)


(2)で表示されたあいさつが、(1)のほうにも表示されましたね😄

データはこんな感じになりました。
![スクリーンショット 2021-02-26 163310.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/1b86e91c-02d7-89d2-5b2f-45ee73b4ea7a.png)



どんどんいきましょう。

## ユーザーが部屋に入ったらキャラクターを表示するようにしてみた

つぎは指定したパスへの「データ追加の監視」と「追加された子階層名の取得」を中心に確認。
プログラムはこんな感じ。

|ねこ|おともだち|
|---|---|
|![スクリーンショット 2021-02-26 164314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/09cbbc4d-1d8f-e204-f35b-fd181e350171.png)|![スクリーンショット 2021-02-26 164328.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/079ccadd-0421-aa5a-f96e-9472de7ae535.png)|

動かしてみましょう。

![test01-2-min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/fa335d03-99d1-a64c-14f8-f1df661b2a6e.gif)


おぉぉ！お友達が部屋を訪れましたね！！😆
じろうさんの部屋にも、たろうさんがちゃんといます。

データはこんな感じになりました。
![スクリーンショット 2021-02-26 174005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/e2f1bf2b-84f2-790e-8471-a56bc2bda42d.png)



## どんどん動かしてみた／つぶやけるようにしてみた

今度は Realtime Database拡張ブロックは使っていませんが・・・つぶやく機能を追加。

|ねこ(追加分)|おともだち|
|---|---|
|![スクリーンショット 2021-02-26 165033.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/7972576a-936a-7c5b-227b-2bae014aff0f.png)|変更なし|

まずはどんどん増やして・・・以下、９人目と１０人目から。
![test01-3-min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/9b4380e7-4e4f-b488-1aeb-fd7b14884bea.gif)

おぉぉぉ、なかなかそうだいな画になってきましたね🤣
さらに２０人追加してみましょう。性能テストですね。

![スクリーンショット 2021-02-26 171350.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/d7d922bb-2653-b47d-7092-f113ceead074.png)

ともだちがいっぱいでも大丈夫！🤣🤣🤣

これで好きな部屋に入って、会話を楽しみむことが出来ますね！！

でも・・・

あれっ？

こんなアプリ、どこかで見たような・・・

🦀🦀🦀 - 完 - 🦀🦀🦀



# おわりに

例のパロディ作品のパクリでした（汗

なお今回作ったものは、以下の場所で公開しています。

* [kami_teru's Scratch Games](https://atsuteru.github.io/scratch-games/)
* [atsuteru/scratch-vm - Github](https://github.com/atsuteru/scratch-vm)
* [atsuteru/scratch-gui - Github](https://github.com/atsuteru/scratch-gui)