enebularとスプレッドシートでクリスマスギフト置き場を作ってみた

# はじめに
この記事は[enebular Advent Calendar 2022](https://qiita.com/advent-calendar/2022/enebular)に参加している記事になります。
~~登録を忘れていたら24日枠しか空いてなかったぜ~~

今回はタイトル通り、UnityEngine製のアプリと
enebular・SpreadSheetを接続して、
クリスマスギフトをおいておける場所を作ってみました。
~~それはもう絵馬掛所では？~~

せっかくのクリスマスということと、
せっかくの24日枠ということで、
若干のプレッシャーを感じつつも皆に楽しんでもらえそうなモノにしてみました。
各種ソースコードは公開予定なので、
中身が気になった方は是非読んでみてください。

# どんなアプリ？
~~GitHub pagesで実行環境を用意しました。
ギフトの送信も出来るので、遊びに来た人は是非何か書いてみてください。~~

元々WebGLで公開予定でしたが、
CORS問題（詳しくは後述）を突破出来ず、exeでの公開になります。
Googleドライブに置いておくので、興味がある方は是非ダウンロードしてください。

https://drive.google.com/file/d/1MzV9xZHChAR3-Z0b1sBGj4IL5dWmSGs9/view?usp=sharing

https://github.com/GONBEEEproject/christmas_gift_enebular

Enebular側の実装は主にこちらの公式記事を参考にしています。

https://blog.enebular.com/nodes/letsuse-google-spreadsheet-node/

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Enebularのアドベントカレンダーに24日担当で参加しております！<br><br>これはQiita記事に埋め込む用の動画w<br>お楽しみに！<br><br>enebular Advent Calendar 2022 <a href="https://t.co/2ECaxBTY0s">https://t.co/2ECaxBTY0s</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a> <a href="https://t.co/gyjwUxpHKC">pic.twitter.com/gyjwUxpHKC</a></p>&mdash; ごんびぃー (@GONBEEE_project) <a href="https://twitter.com/GONBEEE_project/status/1605898803416125447?ref_src=twsrc%5Etfw">December 22, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

***

ギフト表示の挙動としては
1. UnityからenebularへHTTP Get
1. enebularクラウド実行環境からG Sheetノードを経由して、セル情報を取得
1. セル情報をJSON形式でHTTP Response
1. Unity上で受け取ったJSONをデシリアライズ、プレゼント箱に反映

といった流れで動いています。

***

ギフト送信は
1. Unity上で送信者名、添付メッセージを入力
1. 入力された文字列をByte配列に変換
1. JSON形式でenebularクラウドへHTTP Post
1. enebularクラウドでG Sheetノードを経由して、最後の行にデータをAppend

という形です。

今回はダウンロード用・アップロード用でクラウド実行環境を2つ用意しました。
それぞれ順を追って説明したいと思います。

# ギフト表示側

### 1. UnityからHTTP Get

今回Enebularから取得する文字列は以下のような形です。
```
["gon","こんにちは","ごんびぃー","アドカレの記事読んだ？","うにてい","エディタからテストです"]
```
これは奇数要素が送信者名、偶数要素がメッセージという形で設計しており、
恐らくこれが最もシンプルにデシリアライズが書ける形だと思います。

上記文字列を受け取るソースコードが以下の通りです。
UnityからHTTPを叩くのはUnityEngine公式で提供されている
UnityWebRequestという機能を使います。

```EnebularManager.cs
string result;
using (var request = UnityWebRequest.Get("https://lcdp003.enebular.com/download/"))
{
    yield return request.SendWebRequest();
    result = request.downloadHandler.text;
    result = result.Remove(0, 1);
    result = result.Remove(result.Length - 1, 1);

    string[] s = result.Split(",");
```

URLに対しHTTP Getを叩いて、そのレスポンスで帰ってきた文字列を
RemoveやSplitでデシリアライズをしています。

UnityにはJSONを処理するためのJSONUtilityという機能も存在しますが、
色々と問題のある機能なので今回は採用せず、
自力でデシリアライズしてしまおうという実装手法を取りました。
ざっくり言うとJSONUtilityはルート配列のJSONをうまく処理できません。
不便極まれり。
参考↓

https://takap-tech.com/entry/2021/02/02/222406

### 2.3. Enebularからセル情報を取得

まず前提として
Enebular上でスプレッドシートにアクセスするGSheetノードは
`node-red-contrib-google-sheets`というノードを使っています。

https://flows.nodered.org/node/node-red-contrib-google-sheets

![SnapCrab_NoName_2022-12-22_18-6-57_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/4633d620-fe31-4909-fa1d-99ecb69973dd.png)

GSheetノードを追加するには、
画面右上のハンバーガーメニュー>パレットの管理から
ノードを追加>Google Sheetで検索をすると現れるので、
それを登録してあげましょう。

![SnapCrab_NoName_2022-12-22_18-8-7_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/d1b57f79-d0ac-5fd2-c082-fb9195f76493.png)

Download用のフローはこのような形になっています。
![SnapCrab_NoName_2022-12-22_18-2-40_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/9be38517-ac0d-e036-5e94-0bcc97017eff.png)

やっていることは非常にシンプルで、
クラウド実行のLCDPが発火されたらGSheetノードを使って
スプレッドシートから全てのセル情報を取得、
LCDPでHTTP Responseを返すというような形です。

GSheetノードの設定は以下の通りです。
![InkedSnapCrab_NoName_2022-12-22_18-17-29_No-00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/d3c08224-792a-e3d2-9452-814669227c2c.jpeg)

ミソとなるのは`Flatten Matrix`です。
Unity側の都合ですが、JSONで送られてきても様々な問題で扱いにくいという理由があり、
最初から入れ子配列ではなく、要素を完全にFlattenな状態で送ってもらったほうが
デシリアライズを書きやすいと考えたため、
今回は`Flatten Matrix`をオンにして実装しました。


### 4. Unity側でプレゼント箱に反映
プレゼント箱に実際に反映するコードは極めてUnity的で
enebularアドカレとしては微妙な内容なのでさらっと流します。

```EnebularManager.cs
string[] s = result.Split(",");

for (int i = 0; i < s.Length; i += 2)
{
    string s1 = s[i];
    string s2 = s[i + 1];
    s1 = s1.Replace("\"", "");
    s2 = s2.Replace("\"", "");
    MakeGift(s1, s2);
    yield return new WaitForSeconds(0.2f);
}
```

受け取ったJSONはセル情報が文字列として来ているので、
各要素の前後に`"`が入っています。
一度受け取ってしまえれば`"`は不要なので、
Replaceで除去しています。

（記事書きながら思ったけど、Splitする前に”除去すれば1行減らせるな、、、）

`MakeGift`メソッド内で実際にオブジェクトを生成、各文字列情報を転送して表示しています。


# ギフト送信側
### 1.Unity上で名前、メッセージを入力
![SnapCrab_NoName_2022-12-22_19-0-21_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/3c8a620a-8a4b-15b6-82fb-cdf89f3f4ece.png)

見ての通り名前フォームとメッセージフォームを用意したポップアップ窓を作りました。
ここで入力した文字列を以下の実装の通りに文字列をまとめます。

```EnebularManager.cs
string s1 = nameText.text;
string s2 = commentText.text;

if (s1 == "")
{
    s1 = " ";
}

if (s2 == "")
{
    s2 = " ";
}

string json = $"[\"{s1}\",\"{s2}\"]";
```

ここでのミソとしては名前、メッセージの文字列が完全に空白だった際に
スペースを一つ入れているという点です。
受信側の1.で紹介したように、今回はかなり強引なデシリアライズを書いているので、
セルが一つでも空っぽだと配列の順序がズレて動かなくなります。
そのため空白対策としてスペースを入れるという手法を取りました。
~~運用カバーが過ぎる~~

### 2.3. 入力された文字列をByte配列に変換
HTTP PostでJSONで文字列を送ろうとすると送信先URLなどを含んだ状態でPostされてしまうため、
URLに使えない文字は変換されて送られます。

そのためByte配列に変換した上でPostすることで、
EnebularのGSheetノードで正常にスプレッドシートに登録できるようになります。

```EnebularManager.cs
string json = $"[\"{s1}\",\"{s2}\"]";

byte[] postData = System.Text.Encoding.UTF8.GetBytes(json);
var request = new UnityWebRequest("https://lcdp003.enebular.com/upload/", "POST");
request.SetRequestHeader("Content-Type", "application/json");
request.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData);
request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
yield return request.SendWebRequest();
```

参考記事はこちら

https://qiita.com/s-sanosuke/items/3e0cd02ddc1b61de6cde

### 4. enebularでG SheetノードからデータをAppend
Upload用フローはこのような形です。
![SnapCrab_NoName_2022-12-22_19-41-25_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/5248f3b3-51a0-196a-ccd3-50a7271e1295.png)

こちらも複雑なことは一切やっていません。
受け取った文字列をそのままスプレッドシートに登録するだけです。
GSheetノードの設定はこの通り。

![InkedSnapCrab_NoName_2022-12-22_19-42-10_No-00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/c4e2b823-9dd2-7e5f-1fd2-1cce6fe725d2.jpeg)

MethodをAppendRowに設定しているので、スプレッドシートの最後の行に
受け取ったJSONをそのまま追加していくという挙動です。
こちらもDownloadと同様`Flatten Matrix`にして、挙動を分かりやすくしました。


# CORS問題
今回は元々遊んでもらいやすくするために
GitHub PagesでUnityから作成したWebGLビルドを公開して、
アドカレを読みに来た皆さんにメッセージを置いていってもらおうと考えていました。
ところが、GitHub PagesからEnebularクラウド実行環境にアクセスし、
データを引っ張ってくる際にCORS問題に阻まれうまく動きませんでした。

CORSとはCross-Origin Resource Sharingの略、
オリジン間リソース共有という概念です。
Web屋ではないのでざっくり理解で解説すると、
自分のドメインから別のドメインに何かリソースを要求する時に、
レスポンス側でセキュリティのためにリソースを返さないという制限が入る場合があります。
これがCORS拒否で、今回はEnebularクラウド実行環境がCORSを許可していなかったため
GitHub Pagesからの接続に失敗したという形でした。

ちなみに異なるドメイン間で防護するという挙動なので、
GitHub Pagesではない他のサービスにWebGLを展開しても、
Enebular側で許可できない限りHTTP Responseは帰ってきません。

参考記事は以下の通り

https://qiita.com/mtoutside/items/cee708841cad7e02f85c

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

Enebularの中の人～！
もしこの記事を読んでくださったなら
クラウド実行環境の設定項目にCORS許可のオプションを追加してください～！
お願いします～～～！
一応Enebularサポートにも投げてみますか、、、

ということで、
今回の企みだったGitHub PagesでWebGLを公開して、皆に遊んでもらうという案は
CORS問題に阻まれて出来ませんでした。

EXEビルドはGoogleドライブに置いておくので、
もし「しょうがねぇギフト置いてやるか」という心優しい方がいらっしゃったら
是非ドライブから引っ張っていただき、思い思いのコメントを残して頂ければと思います。

# さいごに
この記事は[enebular Advent Calendar 2022](https://qiita.com/advent-calendar/2022/enebular)に参加している記事でした。

普段は完全にUnityEngineのフロントエンドをやっているので、
こういったWeb系開発は貴重な体験でした。
貴重な体験、故に知見もなくCORS問題にブチ当たるという結果にもなりましたが、
GoogleAPIの認証周りや、セキュリティのために用意されている仕組みなど
レアな知識（当社比）を蓄えることができました。

（個人名出していいか分からんので伏せますが）
「Enebularアドカレ、書いてみない？」と誘ってくださった方、
ありがとうございました！
おかげでWeb系の沼に触れることができました。

今後もUnityメインで色々開発は続けていくつもりなので、
今回のように「動的に何か差し替える必要がある」みたいな場面に遭遇したら
Enebularを活用していこうかなと思っています。

ここまで読んでいただいてありがとうございました！
アドバイスやコメントなどございましたら、コメント欄やTwitterにて話しかけて頂けると幸いです！
https://twitter.com/GONBEEE_project