はじめに
この投稿では、Feed The Animals というシンプルなゲームを作ります。このゲームでは、バックエンドサーバーとして Node.js を、ストレージとして GridDB データベースを使用します。
プロジェクトの実行
GitHub リポジトリからプロジェクトのソースコードをクローンします。
git clone https://github.com/junwatu/unity-node.js-griddb.git
ディレクトリをプロジェクトのソースコードに変更します。
cd unity-node.js-griddb
ゲームサーバー用に Node.js の依存関係をインストールします。
cd app\server
npm install
ゲームサーバー
ゲームを起動する前に、まずゲームサーバーを起動してください。プロジェクトのソースコードから app\server
フォルダに移動し、ターミナルで以下のコマンドを実行してください。
npm run start
サーバーのデフォルトのホストとポートの設定は .env
ファイルにあります。他の IP アドレスとポートでサーバを動作させたい場合は変更してください。例えば、IP アドレスを 192.168.0.11
、ポートを 9000
に設定してゲームサーバを動作させる場合は .env
ファイルを開き GAME_SERVER
変数を編集します。
GAME_SERVER=http://192.168.0.11:9000
その後、ゲームサーバーを起動または再起動します。
ゲームのビルド
実際のビルドを見てみましょう。
Windows バイナリ .exe
このゲームビルドは Windows OS マシン上で動作します。バイナリビルドはこちらのリンクからダウンロードしてください。
それを解凍して、デフォルトのゲームサーバーのアドレスを変更したら、ゲームの IP アドレスも変更してください。config.json
ファイルを開き WebSocketURL
を編集します。
{
"WebSocketURL": "http://192.168.0.11:9000"
}
その後、feed.the.animals.exe
ファイルをクリックしてゲームを実行することができます!
ゲームプレイは簡単です。動物に餌をやり、何匹か餌をやらなかったら負け、つまりゲームオーバーです!
このゲームには3つのキー機能しかありません。
コントロールキー | アクション |
---|---|
左矢印 または A | 左方向に移動します。 |
右矢印 または D | 右方向に移動します。 |
スペースバー | 動物に餌を与える。 |
ビルディング・ブロック: Unity、Node.js、WebSocket、およびGridDB
シームレスなゲームプレイ、特にマルチプレイヤーシナリオや一貫した同期を必要とするゲームでは、すべてのアクション、決定、ゲームステートを正確かつ迅速に保存することが重要です。私たちは Unity ゲームエンジンとバックエンドスタック Node.js と GridDB データベースを使用してゲームを構築します。それぞれの重要性を検証してみましょう。
Unity
Unity は 2D スプライトや 3D ワールドを作成するための汎用エンジンを備えたトップクラスのゲーム開発プラットフォームです。このプラットフォームは、ユーザーフレンドリーなインターフェース、豊富なアセット、協力的なコミュニティを持っており、モバイルデバイス、デスクトップ、VR ヘッドセット向けの没入型ゲーム体験の作成を支援します。
Node.js
Node.js は、開発者が JavaScript を使用して効率的でスケーラブルなバックエンドサービスを作成することを可能にします。そのイベント駆動型、ノンブロッキング I/O モデルは、多数の同時接続を処理するのに理想的で、大規模なユーザーベースを持つゲームに最適です。Unity ゲームと GridDB などのデータベースを接続するブリッジとして機能します。
GridDB
リアルタイムゲームでは、効率的なデータストレージが重要です。GridDB は、この目的のために設計された、拡張性、可用性、耐久性に優れたデータベースシステムです。GridDB のアーキテクチャは、IoT のユースケースに合わせて設計されており、ゲームにも適しています。GridDB はすべてのプレイヤーのアクションを確実にキャプチャし、低レイテンシーで保存します。
WebSocket: Unity と Node.js をリアルタイムに繋ぐ
一挙手一投足が重要なペースの速いゲームの世界では、従来のリクエスト-レスポンス型以上の通信モデルが必要になるかもしれません。そこで WebSocket の威力が発揮されます。
-
WebSocket の基本: WebSocket は、従来の HTTP モデルとは異なるアプローチを提供します。リクエストを送信した後に応答を待つのではなく、WebSocket は 1 つの長期的な接続を介して全二重通信チャネルを作成します。これにより、常に接続を切断することなくデータの同時送受信が可能になります。接続は HTTP 接続ハンドシェイクによって確立され、その後 WebSocket で使用するためにアップグレードされます。この一貫した中断のない接続は、データ転送が瞬時に行われるため、リアルタイムのフィードバックを必要とするアプリケーションに最適です。
-
ゲームにおけるリアルタイム通信の重要性: ゲームでは、プレイヤーは自分の動きに対する素早いレスポンスを求めます。マルチプレイヤーシューティングゲームでは、コミュニケーションによってチームの勝敗が決まったり、リアルタイムストラテジーゲームでは、遅滞なく意思決定を行い、実行する必要があります。このような期待に応えるため、ゲーム開発者は WebSocket を利用しています。これらのツールは、遅延を最小限に抑え、プレイヤーのアクションがさまざまなプラットフォームやデバイス間で同期されるようにします。WebSocket を使用することで、ゲーマーはシームレスで魅力的な体験を楽しむことができます。
システムアーキテクチャ
このプロジェクトは、先に説明したように3つの主要なスタック Unity、Node.js、GridDB で構成されています。システム図は非常にシンプルで、Node.js と GridDB をバックエンドサーバーとして使用し、Unity ゲームとバックエンドサーバー間の通信は WebSocket 技術を通じて行います。
このプロジェクトでは、主に 2 つのプログラミング言語を使用します。ゲーム開発用の C# と Node.js 上で動作する JavaScript です。
インストール
Node.js のセットアップ
このブログ記事で取り上げるプロジェクトでは、Node.js LTS バージョン 18 を使用しています。Node.js がインストールされているか確認するには、以下のコマンドを実行してください。
node --version
Node.js がインストールされていない場合や古いバージョンの場合は、公式サイト nodejs.org から Node.js LTS をアップグレードまたはインストールすることを推奨します。
GridDB のセットアップ
GridDB のセットアップは簡単です。Ubuntu への新規インストールはこちら、Windows へのWSL(Windows Subsystem Linux) 経由のインストールはこちらを参照してください。
既にインストールされている場合は、以下のコマンドで griddb サービスが実行されているか確認してください。
sudo systemctl status gridstore
この Ubuntu OS のコマンドライン出力は、griddbサービスが実行されていることを示しています。
● 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 から行ってください。Unity Hub から直接 Unity 2022.3.6f1 をインストールすることで、より良いプロジェクト管理が可能になります。
Unity Hubとは?
Unity Hub は Unity Editor の複数インストール管理、新規プロジェクトの作成、作業へのアクセスに使用します。
インストール後、フォルダ内のゲームプロジェクトのソースコードとアセットをインポートすることができます。
app\game\unity.feed.the.animals
その後、Unity Editor でゲームプロジェクトのソースとアセットを開くことができます。
プロジェクトコード
Node.js と WebSocket の統合
Node.js と WebSocket の統合は、ws
npm パッケージを使えば簡単です。この npm パッケージは使いやすく、高速で、徹底的にテストされた WebSocket クライアントとサーバーの実装です。ただし ws
はサーバ上でのみ利用します。ブラウザには既に WebSocket の実装があるため、クライアント側では利用しません。
Node.js HTTP サーバー
このコードでは、Node.js と様々な Node.js パッケージを使用して、基本的な HTTP と WebSocket サーバーをセットアップします。また、いくつかのHTTPルートを公開し、ゲームデータを取得するためにデータベースサービスに接続します。
// 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
環境変数を取得します。
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 サーバーも実行されます。
const wss = new WebSocket.Server({ server });
setupWebSocket(wss);
setupWebSocket()
はゲームサーバーと Unity ゲームの接続を処理するメイン関数です。コードを見てみましょう。
// 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 データのみを受け取ります。
try {
readableData = JSON.parse(data.toString('utf8'));
} catch (e) {
console.log('Received data is not JSON. Ignoring...');
return;
}
Unity ゲームからの JSON データには、最後のプレーヤーの動きと肉投げの数が含まれています。
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 つのメソッドを使用します。
import { saveData, getAllData } from './griddbservice.js';
griddbservice.js
は、データベース操作に関連するロジックを処理する GridDB サービスファイルです。これにより、データベースロジックの更新が容易になり、GridDB による CRUD 操作をよりモジュール化することができます。
// 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()
関数を用いて保存されます。この関数のシグネチャは以下の通りです。
async function saveData({ playerposition, numberofthrows, gameover })
saveData()
関数は 3 つのゲームデータ playerposition
、numberofthrows
、gameover
を GridDB データベースに保存します。
getAllData()
関数は GridDB データベースに保存されている全てのゲームデータを返します。HTTP サーバーの URL を実行することで、ブラウザからこのデータを取得することができます。
http://localhost:8080/api/gamedata
env
ファイルで HTTP URL サーバーを変更すると、上記の URL も変更されることに注意してください。
ゲームのアーキテクチャ
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 を使用しました。Unity パッケージはこのリンクからインストールできます。
NuGetForUnity は、Unity エディタ内で動作するようにゼロから構築された NuGet クライアントです。NuGet はパッケージ管理システムで、サーバー上で配布され、ユーザーによって消費されるパッケージを簡単に作成できます。
このプロジェクトで使用する WebSocket パッケージは WebSocketSharp-netstandard
です。
WebSocket クライアント
このコードでは、ゲーム開始時に WebSocket クライアントを初期化し、ゲーム中にプレイヤーがスペースバーを押したときに WebSocket サーバーにデータを送信します。このデータには、ゲーム世界におけるプレイヤーの位置に関する情報と、NumberOfMeatThrows
と呼ばれるゲーム固有のメトリックが含まれます。
// 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<string, object> dataToSave = new Dictionary<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 経由でゲームサーバーに送信されます。