Node.js と GridDB を使用して Unity ゲームデータを保存する

## はじめに

この投稿では、**Feed The Animals** というシンプルなゲームを作ります。このゲームでは、バックエンドサーバーとして Node.js を、ストレージとして GridDB データベースを使用します。

## プロジェクトの実行

GitHub リポジトリからプロジェクトの[ソースコード][1]をクローンします。

```sh
git clone https://github.com/junwatu/unity-node.js-griddb.git
```

ディレクトリをプロジェクトのソースコードに変更します。

```sh
cd unity-node.js-griddb
```

ゲームサーバー用に Node.js の依存関係をインストールします。

```sh
cd app\server
npm install
```

### ゲームサーバー

ゲームを起動する前に、まずゲームサーバーを起動してください。プロジェクトのソースコードから `app\server` フォルダに移動し、ターミナルで以下のコマンドを実行してください。

```sh
npm run start
```

サーバーのデフォルトのホストとポートの設定は `.env` ファイルにあります。他の IP アドレスとポートでサーバを動作させたい場合は変更してください。例えば、IP アドレスを `192.168.0.11`、ポートを `9000` に設定してゲームサーバを動作させる場合は `.env` ファイルを開き `GAME_SERVER` 変数を編集します。

```sh
GAME_SERVER=http://192.168.0.11:9000
```

その後、ゲームサーバーを起動または再起動します。

### ゲームのビルド

実際のビルドを見てみましょう。

#### Windows バイナリ .exe

このゲームビルドは Windows OS マシン上で動作します。バイナリビルドはこちらのリンクからダウンロードしてください。

<https://github.com/junwatu/unity-node.js-griddb/releases/download/demo/FeedTheAnimals.Windows-x64.zip>

それを解凍して、デフォルトのゲームサーバーのアドレスを変更したら、ゲームの IP アドレスも変更してください。`config.json` ファイルを開き `WebSocketURL` を編集します。

```sh
{
  "WebSocketURL": "http://192.168.0.11:9000"
}
```

その後、`feed.the.animals.exe` ファイルをクリックしてゲームを実行することができます！

ゲームプレイは簡単です。動物に餌をやり、何匹か餌をやらなかったら負け、つまりゲームオーバーです！

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/game-screenshot.png" alt="" width="1659" height="933" class="aligncenter size-full wp-image-29822" />][2]

このゲームには3つのキー機能しかありません。

　　　　　　　　　　　　　　 
| コントロールキー　         | アクション      |
| ----------------- | ---------- |
| **左矢印** または **A** | 左方向に移動します。 |
| **右矢印** または **D** | 右方向に移動します。 |
| **スペースバー**        | 動物に餌を与える。  |

## **ビルディング・ブロック： Unity、Node.js、WebSocket、およびGridDB**

シームレスなゲームプレイ、特にマルチプレイヤーシナリオや一貫した同期を必要とするゲームでは、すべてのアクション、決定、ゲームステートを正確かつ迅速に保存することが重要です。私たちは **Unity** ゲームエンジンとバックエンドスタック **Node.js** と **GridDB** データベースを使用してゲームを構築します。それぞれの重要性を検証してみましょう。

### [**Unity**][3]

Unity は 2D スプライトや 3D ワールドを作成するための汎用エンジンを備えたトップクラスのゲーム開発プラットフォームです。このプラットフォームは、ユーザーフレンドリーなインターフェース、豊富なアセット、協力的なコミュニティを持っており、モバイルデバイス、デスクトップ、VR ヘッドセット向けの没入型ゲーム体験の作成を支援します。

### [**Node.js**][4]

Node.js は、開発者が JavaScript を使用して効率的でスケーラブルなバックエンドサービスを作成することを可能にします。そのイベント駆動型、ノンブロッキング I/O モデルは、多数の同時接続を処理するのに理想的で、大規模なユーザーベースを持つゲームに最適です。Unity ゲームと GridDB などのデータベースを接続するブリッジとして機能します。

### [**GridDB**][5]

リアルタイムゲームでは、効率的なデータストレージが重要です。GridDB は、この目的のために設計された、拡張性、可用性、耐久性に優れたデータベースシステムです。GridDB のアーキテクチャは、IoT のユースケースに合わせて設計されており、ゲームにも適しています。GridDB はすべてのプレイヤーのアクションを確実にキャプチャし、低レイテンシーで保存します。

### **WebSocket: Unity と Node.js をリアルタイムに繋ぐ**

一挙手一投足が重要なペースの速いゲームの世界では、従来のリクエスト-レスポンス型以上の通信モデルが必要になるかもしれません。そこで [WebSocket][6] の威力が発揮されます。

*   **WebSocket の基本:** WebSocket は、従来の HTTP モデルとは異なるアプローチを提供します。リクエストを送信した後に応答を待つのではなく、WebSocket は 1 つの長期的な接続を介して全二重通信チャネルを作成します。これにより、常に接続を切断することなくデータの同時送受信が可能になります。接続は HTTP 接続ハンドシェイクによって確立され、その後 WebSocket で使用するためにアップグレードされます。この一貫した中断のない接続は、データ転送が瞬時に行われるため、リアルタイムのフィードバックを必要とするアプリケーションに最適です。

*   **ゲームにおけるリアルタイム通信の重要性:** ゲームでは、プレイヤーは自分の動きに対する素早いレスポンスを求めます。マルチプレイヤーシューティングゲームでは、コミュニケーションによってチームの勝敗が決まったり、リアルタイムストラテジーゲームでは、遅滞なく意思決定を行い、実行する必要があります。このような期待に応えるため、ゲーム開発者は WebSocket を利用しています。これらのツールは、遅延を最小限に抑え、プレイヤーのアクションがさまざまなプラットフォームやデバイス間で同期されるようにします。WebSocket を使用することで、ゲーマーはシームレスで魅力的な体験を楽しむことができます。

## システムアーキテクチャ

このプロジェクトは、先に説明したように3つの主要なスタック Unity、Node.js、GridDB で構成されています。システム図は非常にシンプルで、Node.js と GridDB をバックエンドサーバーとして使用し、Unity ゲームとバックエンドサーバー間の通信は WebSocket 技術を通じて行います。

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/unity-nodejs-griddb.png" alt="" width="1605" height="903" class="aligncenter size-full wp-image-29820" />][7]

このプロジェクトでは、主に 2 つのプログラミング言語を使用します。ゲーム開発用の C# と Node.js 上で動作する JavaScript です。

## インストール

### Node.js のセットアップ

このブログ記事で取り上げるプロジェクトでは、Node.js LTS バージョン 18 を使用しています。Node.js がインストールされているか確認するには、以下のコマンドを実行してください。

```sh
node --version
```

Node.js がインストールされていない場合や古いバージョンの場合は、公式サイト [nodejs.org][4] から Node.js LTS をアップグレードまたはインストールすることを推奨します。

### GridDB のセットアップ

GridDB のセットアップは簡単です。Ubuntu への新規インストールは[こちら][8]、Windows へのWSL(Windows Subsystem Linux) 経由のインストールは[こちら][9]を参照してください。

既にインストールされている場合は、以下のコマンドで griddb サービスが実行されているか確認してください。

```sh
sudo systemctl status gridstore
```

この Ubuntu OS のコマンドライン出力は、griddbサービスが実行されていることを示しています。

```sh
● gridstore.service - GridDB database server.
     Loaded: loaded (/lib/systemd/system/gridstore.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-07-04 04:47:12 +07; 9h ago
   Main PID: 575 (gsserver)
      Tasks: 34 (limit: 7017)
     Memory: 144.7M
     CGroup: /system.slice/gridstore.service
             └─575 /usr/bin/gsserver --conf /var/lib/gridstore/conf

Jul 04 04:47:08 GenAI systemd[1]: Starting GridDB database server....
Jul 04 04:47:09 GenAI gridstore[381]: Starting gridstore service:
Jul 04 04:47:12 GenAI gridstore[526]: ..
Jul 04 04:47:12 GenAI gridstore[526]: Started node.
Jul 04 04:47:12 GenAI gridstore[381]: [ OK ]
Jul 04 04:47:12 GenAI systemd[1]: Started GridDB database server..
```

### Unity のセットアップ

この記事では、Windows OS を使用して Unity をインストールします。

> ゲーム開発をゼロから行うわけではないので、このインストールは任意です。Unity Editor に飛び込み、実機開発を試してみたい方は、このままインストールを続けてください。

#### Unity Editor

このプロジェクトでは Unity 2022 LTS を使用します。これをインストールするには、まず Unity Hub をインストールする必要があります。インストールはこちら [Unity Hub][10] から行ってください。Unity Hub から直接 **Unity 2022.3.6f1** をインストールすることで、より良いプロジェクト管理が可能になります。

**Unity Hubとは?**  
Unity Hub は Unity Editor の複数インストール管理、新規プロジェクトの作成、作業へのアクセスに使用します。

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/unity-hub.png" alt="" width="1330" height="748" class="aligncenter size-full wp-image-29826" />][11]

インストール後、フォルダ内のゲームプロジェクトのソースコードとアセットをインポートすることができます。

```sh
app\game\unity.feed.the.animals
```

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/unity-hub-open.png" alt="" width="1427" height="803" class="aligncenter size-full wp-image-29819" />][12]

その後、Unity Editor でゲームプロジェクトのソースとアセットを開くことができます。

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/unity-editor.png" alt="" width="1921" height="1080" class="aligncenter size-full wp-image-29825" />][13]

## プロジェクトコード

### Node.js と WebSocket の統合

Node.js と WebSocket の統合は、[`ws`][14] npm パッケージを使えば簡単です。この npm パッケージは使いやすく、高速で、徹底的にテストされた WebSocket クライアントとサーバーの実装です。ただし `ws` はサーバ上でのみ利用します。ブラウザには既に WebSocket の実装があるため、クライアント側では利用しません。

#### Node.js HTTP サーバー

このコードでは、Node.js と様々な Node.js パッケージを使用して、基本的な HTTP と WebSocket サーバーをセットアップします。また、いくつかのHTTPルートを公開し、ゲームデータを取得するためにデータベースサービスに接続します。

```javascript
// index.js
import express from 'express';
import http from 'http';
import cors from 'cors';
import path from 'path';
import 'dotenv/config';
import url from 'url';

import setupWebSocket from './websocket.js';
import { __dirname } from './libs/dirname.js';
import { getAllData } from "./griddbservice.js";
import { createRequire } from 'module'; 

const require = createRequire(import.meta.url); 
const WebSocket = require('ws');

const app = express();

const parsedUrl = url.parse(process.env.GAME_SERVER_URL);
const hostname = parsedUrl.hostname;
const port = parsedUrl.port || 8080; 

const server = http.createServer(app);

const wss = new WebSocket.Server({ server });

setupWebSocket(wss);

const publicPath = path.resolve(`${__dirname}`, './public');

app.use(express.static(publicPath));
app.use(cors());

app.get('/', (req, res) => {
    res.send('Unity Game Server!');
});


app.get('/api/gamedata', async (req, res) => {
    try {
        const data = await getAllData();
        res.json(data); 
    } catch (err) {
        console.error(err);
        res.status(500).send('Internal Server Error');
    }
});

server.listen(port, () => {
    console.log(`HTTP and WebSocket server running on http://${hostname}:${port}`);
});
```

HTTP サーバーを動作する前に `env` ファイルを読み込んで `GAME_SERVER_URL` 環境変数を取得します。

```javascript
const parsedUrl = url.parse(process.env.GAME_SERVER_URL);
const hostname = parsedUrl.hostname;
const port = parsedUrl.port || 8080;
```

上記のコードは `GAME_SERVER_URL` を解析し、`hostname` と `port` を抽出します。もし `env` ファイルでポートを定義しなければ、デフォルトで `8080` 番ポートになります。

#### HTTP ルーティング

HTTP　サーバーはホームページや対局データリストを取得するためのルートも公開しています。

| HTTPメソッド | ルート             | 説明 　　　　              | レスポンス・タイプ              |
| -------- | --------------- | -------------------- | ---------------------- |
| GET      | `/`             | シンプルなテキストメッセージを返します。 | テキスト "Unityゲームサーバー！"   |
| GET      | `/api/gamedata` | サービスからゲームデータを取得します。  | JSON または 500 内部サーバーエラー |

**注意:** この表は HTTP ルートのみをカバーしており、WebSocket ルートは含まれていません。

#### WebSocket サーバー

WebSocket サーバーは HTTP サーバーにアタッチされているので、HTTP サーバーが実行されると、WebSocket サーバーも実行されます。

```javascript
const wss = new WebSocket.Server({ server });

setupWebSocket(wss);
```

`setupWebSocket()` はゲームサーバーと Unity ゲームの接続を処理するメイン関数です。コードを見てみましょう。

```javascript
// websocket.js
import { saveData, getAllData } from './griddbservice.js';

function setupWebSocket(wss) {
    wss.on('connection', function connection(ws) {
        ws.on('message', async (data) => {

            let readableData;

            // Check if the data is in JSON format
            try {
                readableData = JSON.parse(data.toString('utf8'));
            } catch (e) {
                console.log('Received data is not JSON. Ignoring...');
                return;
            }

            // If data is of type "save", then save it
            if (readableData.type === 'save') {
                const playerposition = {
                    x: readableData.PlayerX,
                    y: readableData.PlayerY,
                    z: readableData.PlayerZ
                };
                const numberofthrows = readableData.NumberOfMeatThrows;
                const gameover = false;

                await saveData({ playerposition, numberofthrows, gameover });
                ws.send(JSON.stringify(readableData))
            } else if (readableData.type === 'getAll') {
                const allData = await getAllData();
                ws.send(JSON.stringify(allData));
            }

            console.log('data received \n %o', readableData);

        });
    });


    wss.on('listening', () => {
        console.log('WebSocket server is listening');
    });
};

export default setupWebSocket;
```

接続状態になると WebSocket は Unity ゲームからのメッセージを待ち受けます。WebSocket の性質上、Unity ゲーム（クライアント）からのデータは即座に受信されます。このコードでは JSON データのみを受け取ります。

```javascript
try {
    readableData = JSON.parse(data.toString('utf8'));
} catch (e) {
    console.log('Received data is not JSON. Ignoring...');
return;
}
```

Unity ゲームからの JSON データには、最後のプレーヤーの動きと肉投げの数が含まれています。

```javascript
const playerposition = {
        x: readableData.PlayerX,
        y: readableData.PlayerY,
        z: readableData.PlayerZ
};
const numberofthrows = readableData.NumberOfMeatThrows;
```

このデータは後に GridDB データベースに保存されます。

### WebSocket イベント

| イベント         | メッセージタイプ | 説明                                      | レスポンス/アクション                |
| ------------ | -------- | --------------------------------------- | -------------------------- |
| on message   | `save`   | プレイヤーの位置とミート数を保存します。                    | 受信したJSONデータをエコーします。        |
| on message   | `getAll` | 保存されたゲームデータを全てクライアントに送信します。 (将来利用)      | すべてのゲームデータを JSON として送信します。 |
| on listening | N/A      | WebSocket サーバーが着信接続をリスニングしていることを示しています。 | コンソールにメッセージを記録します。         |

`setupWebSocket` 関数は WebSocket サーバーをフックし、様々なタイプのイベントをリッスンします。

*   **`connection`**： 新しい WebSocket 接続が確立されるたびに、以下のメッセージハンドラをセットアップします： 
*   **`message`**：受信メッセージをリスンする。メッセージの`type`フィールド(`save`または`getAll`)に応じて、データを保存するか、保存されたゲームデータをすべて取得してクライアントに送り返します。
*   **`listening`**： WebSocket サーバーがアクティブに接続をリッスンしていることを記録します。

### ゲーム進行の保存

ゲームデータを GridDB データベースに保存するのは非常に簡単です。WebSocket サーバのコードでは、`saveData()` と `getAllData()` の 2 つのメソッドを使用します。

```javascript
import { saveData, getAllData } from './griddbservice.js';
```

`griddbservice.js` は、データベース操作に関連するロジックを処理する GridDB サービスファイルです。これにより、データベースロジックの更新が容易になり、GridDB による CRUD 操作をよりモジュール化することができます。

```javascript
// griddbservice.js
import * as GridDB from './libs/griddb.cjs';
import { generateRandomID } from './libs/rangen.js';

const { collectionDb, store, conInfo, containerName } = await GridDB.initGridDbTS();

export async function saveData({ playerposition, numberofthrows, gameover }) {
    const id = generateRandomID();

    // Serialize player position to a JSON string (if needed)
    const playerpositionStr = JSON.stringify(playerposition);
    const numberofthrowsStr = String(numberofthrows);
    const gameoverStr = String(gameover);

    // Now you can safely insert them into the database as strings
    const playerState = [parseInt(id), playerpositionStr, numberofthrowsStr, gameoverStr];
    const saveStatus = await GridDB.insert(playerState, collectionDb);
    return saveStatus;
}

export async function getAllData() {
    return await GridDB.queryAll(conInfo, store);
}
```

ゲームデータは `saveData()` 関数を用いて保存されます。この関数のシグネチャは以下の通りです。

```javascript
async function saveData({ playerposition, numberofthrows, gameover })
```

`saveData()` 関数は 3 つのゲームデータ `playerposition`、`numberofthrows`、`gameover` を GridDB データベースに保存します。

`getAllData()` 関数は GridDB データベースに保存されている全てのゲームデータを返します。HTTP サーバーの URL を実行することで、ブラウザからこのデータを取得することができます。

```sh
http://localhost:8080/api/gamedata
```

`env` ファイルで HTTP URL サーバーを変更すると、上記の URL も変更されることに注意してください。

### ゲームのアーキテクチャ

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/game-architecture.png" alt="" width="1869" height="1203" class="aligncenter size-full wp-image-29821" />][15]

**Feed the Animals** ゲームのアーキテクチャは、一般的にこの図のように記述することができます。この図では **WebSocket (WsClient)** コンポーネントがさまざまなデータを送信しています：

*   **PlayerX、PlayerY、PlayerZ**： PlayerX、PlayerY、PlayerZ**：プレイヤーの位置で、おそらく PlayerController から収集されたものです。

*   **NumberOfMeatThrows**：GameManager から直接読み込まれます。

これらのデータポイントは JSON としてシリアライズされ、WebSocket 接続を介して送信されます。

### Unity と WebSocket の出会い

#### WebSocket NuGet

Unity はネイティブで WebSocket をサポートしていないので、この問題を解決するために NuGet の WebSocket パッケージを使用します。ご存知の通り、Unity は C# を使用しており、JavaScript を使用する Node.js サーバーとは異なる言語です。

> **NuGetとは？**  
> NuGet は .NET のパッケージマネージャーです。NuGet クライアントツールは、パッケージを生成および消費する機能を提供します。NuGet Gallery は、すべてのパッケージ作成者と消費者が使用する中央パッケージリポジトリです。

このプロジェクトでは、より良いパッケージ管理のために、[NuGetForUnity][16] を使用しました。Unity パッケージはこの[リンク][17]からインストールできます。

> **NuGetForUnity** は、Unity エディタ内で動作するようにゼロから構築された NuGet クライアントです。NuGet はパッケージ管理システムで、サーバー上で配布され、ユーザーによって消費されるパッケージを簡単に作成できます。

このプロジェクトで使用する WebSocket パッケージは `WebSocketSharp-netstandard` です。

[<img src="https://griddb.net/en/wp-content/uploads/2023/09/nuget-unity.png" alt="" width="1918" height="1058" class="aligncenter size-full wp-image-29824" />][18]

#### WebSocket クライアント

このコードでは、ゲーム開始時に WebSocket クライアントを初期化し、ゲーム中にプレイヤーがスペースバーを押したときに WebSocket サーバーにデータを送信します。このデータには、ゲーム世界におけるプレイヤーの位置に関する情報と、`NumberOfMeatThrows` と呼ばれるゲーム固有のメトリックが含まれます。

```javascript
// WsClient.cs
public class WsClient : MonoBehaviour
{
    WebSocket ws;

    void Start()
    {
      //...
    }

    void Update()
    {
        if (ws == null)
        {
            return;
        }
        if (Input.GetKeyDown(KeyCode.Space))
        {
            ws.Send("Save Data...");
            Dictionary&lt;string, object> dataToSave = new Dictionary&lt;string, object>();

            //Add player position
            Vector3 playerPosition = GameObject.Find("Player").transform.position;
            dataToSave.Add("PlayerX", playerPosition.x);
            dataToSave.Add("PlayerY", playerPosition.y);
            dataToSave.Add("PlayerZ", playerPosition.z);
            dataToSave.Add("NumberOfMeatThrows", GameManager.Instance.numberOfMeatThrows);
            dataToSave.Add("type", "save");

            // Convert dictionary to JSON and send it
            string json = JsonConvert.SerializeObject(dataToSave);
            ws.Send(json);
        }
    }
}
```

**ゲーム・ループ： Update() メソッド

このメソッドは 1 フレームに 1 回呼び出され、リアルタイムのインタラクションを可能にします。WebSocket クライアント `ws` が初期化されていない場合、このメソッドは何もせずに戻ります。

このメソッドは **Spacebar** キーが押されているかどうかをチェックします。もし押されていれば、いくつかのデータを収集し、WebSocket サーバーに送信します。

*   **Player** という GameObject の位置が辞書に格納されます。
*   「肉を投げた」回数も保存されます。
*   この辞書データは JSON 文字列に変換され、WebSocket 経由でゲームサーバーに送信されます。

 [1]: https://github.com/junwatu/unity-node.js-griddb
 [2]: https://griddb.net/en/wp-content/uploads/2023/09/game-screenshot.png
 [3]: https://unity.com/
 [4]: https://nodejs.org/en/download
 [5]: https://www.griddb.net/
 [6]: https://en.wikipedia.org/wiki/WebSocket
 [7]: https://griddb.net/en/wp-content/uploads/2023/09/unity-nodejs-griddb.png
 [8]: https://docs.griddb.net/latest/gettingstarted/using-apt/
 [9]: https://docs.griddb.net/latest/gettingstarted/wsl/
 [10]: https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe
 [11]: https://griddb.net/en/wp-content/uploads/2023/09/unity-hub.png
 [12]: https://griddb.net/en/wp-content/uploads/2023/09/unity-hub-open.png
 [13]: https://griddb.net/en/wp-content/uploads/2023/09/unity-editor.png
 [14]: https://github.com/websockets/ws
 [15]: https://griddb.net/en/wp-content/uploads/2023/09/game-architecture.png
 [16]: https://github.com/GlitchEnzo/NuGetForUnity
 [17]: https://github.com/GlitchEnzo/NuGetForUnity/releases
 [18]: https://griddb.net/en/wp-content/uploads/2023/09/nuget-unity.png