はじめに
今回、私はGoogle Gemini APIを活用したプレイヤーのチャット指示によるブラウザベースの脱出ゲーム「Swataro Escape」を制作しました。
プレイヤーは、監視カメラ越しにこのゲームのキャラクターである「swataro」を言葉による指示で操作します。
本記事では、このゲームのコアとなる技術スタックと、Three.jsやAI(LLM)をどう連携させたのかについて、アーキテクチャやコードを交えて解説します。

キャラクター紹介

彼の名前はswataroです。この記事で最も重要な登場人物なので覚えてください。
使用技術
- フロントエンド
- HTML
- JavaScript(Three.js,Connon.jsなど)
- CSS
- バックエンド
- Node.js
- Render
- AIエンジン
- Google Gemini API
- データベース
- MongoDB
ゲームのフロー
ゲームは、以下のサイクルで進行します。
⒈【観察】
プレイヤーは、Three.jsで構築された3Dの牢屋を、監視カメラ視点で確認できる。
⒉【指示】
チャットUIから、swataroへ自然言語(日本語など)で指示を送る。
例:「ベッドの下を調べて」、「ジャンプして」、「照明を消して」
⒊【思考・解析】
AI(Google Gemini API)が指示を解釈し、swataroの「セリフ」と「アクション」を決定する。
⒋【実行】
Three.js上のswataroがアクション(寝る、回る、アイテムを取る など)を実行し、脱出のためのフラグが進行する。
実装とコード解説
このゲームの肝となるのは、「プレイヤーの自由なチャット(自然言語)」を「3D空間での具体的なアクション」に変換し、同期させる仕組みです。ここでは、その連携を実現している2つの重要な実装について解説します。
⒈チャット入力からAI経由で「アクションタグ」を抽出する
プレイヤーが入力したテキストは、そのままではゲームシステム(Three.js)で扱うことはできません。そこで、LLM(今回はGoogle Gemini APIを使用)にプロンプトを送り、swataroの「セリフ」と同時に、ゲーム側で解釈可能な「アクションの指示(JSON等)」を返してもらえるように設計しました。
以下は、AIとの通信を行い、レスポンスをパースする部分の簡略化したコードです。
// プレイヤーの入力をAIに送信し、結果を受け取る関数
async function sendCommandToSwataro(userInput) {
// システムプロンプトで、必ず特定のフォーマットで返すよう制約をかける
const prompt = `
あなたは牢屋に閉じ込められたキャラクター「Swataro」です。
ハッカー(プレイヤー)からの指示: "${userInput}"
指示に対して、あなたの「返答メッセージ」と、実行する「アクション」を以下のJSON形式で返してください。
アクションは必ず [ "spin", "sleep", "search", "idle" ] のいずれかにしてください。
形式: { "message": "...", "action": "..." }
`;
try {
// LLMのAPIを叩く(ダミー関数)
const responseText = await callAiApi(prompt);
// AIからの返答をJSONとしてパース
const responseData = JSON.parse(responseText);
// UIにSwataroのセリフを表示
displayChatMessage('Swataro', responseData.message);
// 抽出したアクションを3D空間の制御関数へ渡す
executeSwataroAction(responseData.action);
} catch (error) {
console.error("AIの解釈に失敗しました", error);
}
}
AIが想定外のフォーマットで返してくることを防ぐために、プロンプト内で「出力は必ずJSON形式」「アクションは指定の文字列のみ」と制限をかけています。これにより、自然な会話を成立させつつ、システムとしてのルールを厳守するようになります。
⒉抽出したアクションと3Dアニメーション(Three.js)の連動
AIkから「何をするか(action)」を受け取ったら、次はそれをThree.jsで構築された3Dシーン上のswataroモデルに反映させます。
単に座標を変えるだけでなく、「スピンする(回転し続ける)」などの継続的なアニメーションを表現するため、状態管理のフラグとレンダリングループ(requestAnimationFrame)を連携させています。
// --- 状態管理フラグ ---
let isSpinning = false;
let isSleeping = false;
// 3Dモデルの初期状態を保存しておく
const defaultRotation = { x: 0, y: 0, z: 0 };
const bedPosition = new THREE.Vector3(-3, 0.5, -5);
// AIから受け取ったアクションを実行する関数
function executeSwataroAction(actionType) {
// アクション実行前に状態をリセット
resetSwataroState();
switch (actionType) {
case 'spin':
// レンダリングループで回転させるためのフラグを立てる
isSpinning = true;
break;
case 'sleep':
isSleeping = true;
// Z軸を90度回転させて「寝転がっている」ように見せる
swataroModel.rotation.z = Math.PI / 2;
// ベッドの座標へ移動
swataroModel.position.copy(bedPosition);
break;
case 'search':
// アイテムを探すロジック(近辺のRaycaster判定など)
performSearch();
break;
case 'idle':
default:
// 何もしない(立ち姿に戻る)
break;
}
}
// 状態を初期化(立ち姿に戻す)関数
function resetSwataroState() {
isSpinning = false;
isSleeping = false;
swataroModel.rotation.set(defaultRotation.x, defaultRotation.y, defaultRotation.z);
swataroModel.position.set(0, 0, 0); // 中央に戻す
}
// --- Three.jsのレンダリングループ ---
function animate() {
requestAnimationFrame(animate);
// フラグが立っている場合、毎フレームY軸に回転を加える(スピン)
if (isSpinning) {
swataroModel.rotation.y += 0.05;
}
renderer.render(scene, camera);
}
animate(); // ループ開始
⒊未知のアクション指示に対するリアルタイムコード生成と保管
このプロジェクトで1番のこだわりの詰まった技術がこの「リアルタイムコード生成」です。つまり、「プレイヤーの無茶振り」への対応です。
今までは「スピンして」や「寝て」といった事前に用意したアクションは実行できてましたが、プレイヤーが「踊って!」「暴れて!」など、予測不可能な指示を出してきた場合、既存のアクションでは対応できませんでした。
これを解決するため、「システムに登録されていないアクションタグをAIが返してきた場合、その場でAIにThree.js用のアニメーションコード(座標や回転などのパラメータ)を生成させ、それを実行、保存する(データベース)」という仕組みを実装しました。
以下は、その生成ロジックを簡略化したサンプルコードです。
// 登録済みのアクションリスト(最初は基本動作のみ)
const knownActions = {
spin: executeSpin,
sleep: executeSleep,
search: executeSearch
};
// アクション実行のメイン関数(拡張版)
async function executeSwataroAction(actionType) {
// 1. 既に知っている(実装済み)アクションなら、それを実行
if (knownActions[actionType]) {
knownActions[actionType]();
return;
}
// 2. 未知のアクションの場合、リアルタイム生成フェーズへ移行
console.log(`未知のアクション "${actionType}" を検知。アニメーションを生成します...`);
try {
// LLMにThree.jsのアニメーションパラメータ(キーフレームなど)を生成させる
const animationData = await generateAnimationParamsWithAI(actionType);
// 3. 生成されたアニメーションデータを元に動的関数を作成
const newActionFunction = createDynamicActionFunction(animationData);
// 4. 今後のためにknownActionsに保存(学習)
knownActions[actionType] = newActionFunction;
// 5. 生成したてのアニメーションを実行
newActionFunction();
} catch (error) {
console.error("アニメーションの生成に失敗しました", error);
// 生成失敗時のフォールバック処理(困惑する動作など)
executeConfused();
}
}
// AIにアニメーションのパラメータを生成させる処理
async function generateAnimationParamsWithAI(actionName) {
const prompt = `
Three.jsの3Dモデルに「${actionName}」という動作をさせたいです。
以下のモデル構成(腕、脚、胴体など)を前提に、
0.0秒から2.0秒までの間の主要なキーフレーム(回転角や座標)を
JSON形式で生成してください。
...(制約や出力フォーマットの詳細)...
`;
const response = await callAiApi(prompt);
return JSON.parse(response);
}
// パラメータから実行可能な関数を生成するファクトリー関数
function createDynamicActionFunction(animationData) {
return function() {
// アニメーションミキサーやTween.js等を使って、
// animationDataのキーフレーム通りにモデルを動かす処理
playCustomAnimation(animationData);
};
}
Swataro Escapeで実際に遊んでみよう!
と、言いたかったのですが、API側からエラーを吐かれてしまったので、今解析&修正中です。
最後に
今回の記事では、このプロジェクトの大まかな概要や技術について紹介しましたが、気が向いたらそれぞれの仕組みについてピックアップして書きたいと思います。
あと、今このプロジェクトの技術を発展させて、さらに面白い開発をしているので、これも後々記事にしようかなと。