はじめに
タイトルの通り、UnityでLLMを組み込んだ3Dアクションゲームを作ってみました。
具体的にはローカル実行の小型LLM(Qwen 3 1.7B) をチューニングして戦闘AIに組み込む、という実験です。
結論から言うと、正直微妙でしたが収穫もあったので共有させてください。
本記事の内容は以下の通りです。
- 今回のLLM活用ゲームのコンセプトと設計
- ファインチューニング有無による実験結果の比較
- 自然言語指示が行動傾向に与える影響の検証
- 今回の取り組みの総括と、次に活かせる設計案
プロジェクト概要
ゲームの概要
今回のゲーム『AIわからせバトル』は、簡単に言うと「フォーオナー」風の3D近接格闘ゲームです。
フォーオナーを知らない人は何かの3D格闘ゲームなんだなぁ、くらいに思っていただけるといいかと。
本ゲームではプレイヤーとAIキャラクターが1対1で戦い、攻撃・防御・回避・ガード方向の切り替えなどをリアルタイムで行います。
| 見ての通りナマイキなAIをわからせるゲームだ |
|---|
![]() |
AIに職を奪われたおっさんになって逆恨みでAIをブチのめすゲームです。
よろしければ遊んでみてくださいね。
やりたかったこと
今回のLLM活用ゲームという取り組みのコンセプトは、戦況という文脈から行動を生成するAI を作ることでした。
具体的にLLMを使う利点として期待していたのは以下の2点です。
① 前後の文脈を考慮した非線形的な判断を行える
→ 「HPが減ってきたから防御寄りにしよう」「相手がガード固めてるから崩しにいこう」のような、状況の流れを読んだ判断。
② 自然言語を作戦に反映することができる
→ 「常に攻め続けろ」「カウンター重視で戦え」のような自然言語テキストをプロンプトに付加し、AIの行動傾向を変化させる。
システム設計
アーキテクチャ概要
ゲーム全体は以下のような構成になっています。
全てのコンポーネントがStateSystemクラスに情報を集めて、集めた情報をもとに戦況のプロンプトを作成するような感じです。
HPやエネルギーに加え、各アクションが実行した行動の履歴も持ちます。
この文脈に従い、状況別の行動テーブルをLLMが作成するような感じです。
| LLM統合バトルシステム概要図 |
|---|
![]() |
図のポイントは以下の通りです。
- 左側のLLMパイプライン(青い点線枠):自然言語指示 → プロンプト生成器 → LLMという流れで戦術判断を生成
- StateSystem(黄色)が中央ハブとして機能し、行動ログと状態データをLLMパイプラインとAIの両方に供給
- キャラクターAI(赤)がLLMの出力を受け取り、行動を実行する
- 行動実行クラス群(緑)が実際のゲームアクションを実行し、結果をStateSystemに報告(R3ストリーム経由)
LLM出力の設計
LLMには戦況データ(HP・エネルギー・直近の攻防結果など)をJSON形式で入力し、以下のような構造化JSONを出力させています。
{
"AnalysisResult": "Keep attack, modify defense",
"BasicTactic": "Aggressive",
"AttackCriteria": "Dispersion Focus",
"ContinuousAttackCriteria": "Speed Priority",
"DefenseCriteria": "Return Priority",
"ContinuousDefenseCriteria": "Evasive Counter Priority"
}
BasicTacticが基本方針(Aggressive / Defensive / Adaptive / Disruptiveなど)、各Criteriaが具体的な攻防の方針を決定します。
この出力をゲームシステムが解釈し、アクションの選択をする仕組みです。
自然言語指示システム
本プロジェクトの特徴的な機能が 自然言語指示 です。
プロンプトに英語の自然言語の指示テキスト(より攻撃的にふるまえ、とか)を付加することで、LLMの行動傾向を制御します。
ちなみに英語なのはLLMモデルの仕様上、日本語より精度が高くなるからです。
ただ2Bモデルでは性能に限界があり、指示が複雑になるほど判断精度が落ちてしまいます。
細かいルールを守るのは特に苦手で、禁止テキスト(「Must Change」「NEVER」など)も効かず、不適切な選択肢を選び続けてしまいます。
そこで、細かいルールはテキストで制約するのではなく、禁止対象選択肢を物理的に除外する アプローチに切り替えました。
この制限はプロンプトだけでなく JSON Schema GrammarのEnumにも反映 されるため、出力レベルで完全に制御されます。
// Berserkerの場合: Aggressiveのみ許可、防御的な選択肢を物理除外
new NLIOptions
{
BasicTactic = new[] { "Aggressive" },
AttackCriteria = /* 攻撃的な選択肢のみ */,
DefenseCriteria = /* 防御時もカウンター重視の選択肢のみ */
}
小型LLMをゲームに組み込む場合、モデルの判断力に期待するよりも、選択肢の設計で行動品質を担保する 発想が重要だと思います。
おや、それならもうルールベースでよくないか?
自然言語指示タイプ一覧
全17種類の指示タイプを設計しました。今回の実験では以下の3タイプを使用しています。
| 指示タイプ | 概要 | 期待される行動傾向 |
|---|---|---|
| Berserker | 常に攻撃的に圧をかける | Aggressive一辺倒 |
| CounterPuncher | 相手のミスを待って反撃 | Defensive / Adaptive |
| PatternBreaker | 同じ手を繰り返さない | Disruptive / Adaptive |
実験設計
テスト環境
- LLMフレームワーク: LLMUnity
- モデル: Qwen 3 1.7B (チューン済みと未チューンで比較)
- テスト条件: 各指示タイプ × ファインチューニング有無 = 6パターン
- 各パターン100回の推論テスト(5種の戦況 × 20回ずつ)
- 戦況タイプ: 優勢 / 拮抗 / 劣勢 / エネルギー不足 / 体力危険
ファインチューニングの内容
チューニング済みモデルには、戦況に応じた戦術判断を学習させるためのファインチューニングを施しています。
学習データはinstruction / outputペアのJSONL形式で1350件用意し、入力にはゲーム中と同一形式の戦況プロンプト(HP差・攻防パターン統計・前回の効果判定など)、出力には期待される戦術判断のJSONを使用しました。
ファインチューニングの主な目的
学習させたのは 出力形式の安定化 と フィードバックに基づく判断パターン の2点です。
具体的には「効果が高かった戦術は維持し、効果がなかった戦術は変更する」というルールに従った応答パターンを学習させています。
学習データの例(展開)
入力(戦況: CRITICAL DANGER、前回失敗):
Your HP: 55 | Enemy HP: 155 (Enemy has 100 more)
Performance: 【MAJOR FAILURE】
- ContinuousDefenseCriteria "Risk Avoidance"
Success:22 Fail:5 → Highly Effective
- AttackCriteria "Energy Efficiency"
Success:9 Fail:12 → Weak Effect
出力:
{
"AnalysisResult": "Continue successful approach",
"BasicTactic": "Endurance",
"DefenseCriteria": "Risk Avoidance", ← Highly Effectiveなので維持
"AttackCriteria": "Recent Pattern Focus" ← Weak Effectなので変更
}
AnalysisResultが3〜6語の簡潔なテキストである点も学習させたポイントです。
チューニングなしモデルでは冗長なテキストが出力されていましたが、チューニングにより簡潔な応答パターンを獲得しています。
チューニングで獲得させた特性
| 特性 | 詳細 |
|---|---|
| 出力の簡潔さ | AnalysisResultが3〜6語程度の短文になる(トークン節約→高速化に直結) |
| フィードバック追従 | Highly Effective → 維持、Must Change → 変更というルールへの忠実さ |
| JSON形式の安定性 | 指定された6フィールドを過不足なく出力するパターンの定着 |
重要なのは、自然言語指示(NLI)に関する学習データは含まれていない 点です。
チューニングはあくまで出力形式とフィードバック追従の学習に特化しており、自然言語指示の解釈能力は事前学習モデルのものがそのまま使われています。
つまり、チューニング後も自然言語指示が機能していたことは、ファインチューニングが言語処理能力を破壊しなかったことを示しています。
テストで測定した項目
- 応答時間(秒)
- 生成速度(tokens/秒)
- JSON有効率
- 戦術タイプの分布(自然言語指示が行動傾向に影響を与えているか)
実験結果
サマリー比較
パフォーマンス比較
| 指標 | チューニング済み Berserker | チューニングなし Berserker | チューニング済み Counter | チューニングなし Counter | チューニング済み Pattern | チューニングなし Pattern |
|---|---|---|---|---|---|---|
| 総実行時間 | 355.43秒 | 421.62秒 | 376.29秒 | 436.65秒 | 393.66秒 | 440.92秒 |
| 平均応答時間 | 1.57秒 | 2.23秒 | 1.77秒 | 2.38秒 | 1.95秒 | 2.42秒 |
| 最短応答時間 | 1.35秒 | 1.81秒 | 1.43秒 | 1.79秒 | 1.52秒 | 1.92秒 |
| 最長応答時間 | 3.50秒 | 3.23秒 | 3.10秒 | 3.47秒 | 3.48秒 | 3.64秒 |
| 平均生成速度 | 80.6 tok/s | 76.2 tok/s | 73.3 tok/s | 70.5 tok/s | 67.9 tok/s | 65.6 tok/s |
| 成功率 | 100% | 100% | 100% | 100% | 100% | 100% |
| JSON有効率 | 100% | 100% | 100% | 100% | 100% | 100% |
応答速度の高速化率(指示タイプ別)
| 指示タイプ | チューニング済み平均 | チューニングなし平均 | 高速化率 |
|---|---|---|---|
| Berserker | 1.57秒 | 2.23秒 | 約30%高速化 |
| CounterPuncher | 1.77秒 | 2.38秒 | 約26%高速化 |
| PatternBreaker | 1.95秒 | 2.42秒 | 約19%高速化 |
ファインチューニングの効果(全体平均)
指示タイプに依存しない、チューニング有無の純粋な比較です。
| 項目 | チューニングなし | チューニング済み | 改善 |
|---|---|---|---|
| 平均応答時間(全体) | 2.34秒 | 1.76秒 | 24.8%短縮 |
| 平均生成速度(全体) | 70.8 tok/s | 73.9 tok/s | 4.4%向上 |
| 100回テストの総実行時間(平均) | 433.1秒 | 375.1秒 | 13.4%短縮 |
ファインチューニング済みモデルは全指示タイプにおいて 一貫して高速 です。
これはモデルが出力パターンを学習し、より少ないトークン数で効率的に応答を生成できるようになったためと考えられます。
実際、トークン数を比較すると、チューニング済みの方が平均的に少ないトークンで応答しています(例: Berserkerではチューニングなし平均168トークンに対し、チューニング済みは平均125トークン)。
戦術タイプ分布の比較(自然言語指示の影響)
ここが今回の実験の核心部分です。
Berserker(攻撃偏重)
| 戦術タイプ | チューニング済み | チューニングなし |
|---|---|---|
| Aggressive | 100回 | 100回 |
Berserkerは両方とも Aggressive 100% でした。
選択肢がAggressiveの1つだけなので、チューニングの有無に関わらず必ずAggressiveが選ばれます。
Grammar制約がきちんと効いている証拠です。
CounterPuncher(防御 + カウンター)
| 戦術タイプ | チューニング済み | チューニングなし |
|---|---|---|
| Defensive | 42回 | 100回 |
| Adaptive | 58回 | 0回 |
ここが興味深い結果です。
- チューニングなし: Defensive 100%で完全に固定化。「カウンター重視」という指示を「とにかく守れ」と解釈している可能性が高いです。
- チューニング済み: Defensive 42%、Adaptive 58%という混合。カウンターという概念をより柔軟に解釈し、「状況に応じて反撃する」という行動パターンを示しています。
さらにチューニング済みの方を戦況別に分解すると、明確な分岐が見えます。
| 戦況 | Defensive | Adaptive |
|---|---|---|
| 優勢 | 1回 | 19回 |
| 拮抗 | 0回 | 20回 |
| 劣勢 | 20回 | 0回 |
| エネルギー不足 | 1回 | 19回 |
| 体力危険 | 20回 | 0回 |
劣勢・体力危険時はDefensive、優勢・拮抗時はAdaptive と、状況に応じた使い分けが行われています。
「反撃重視」というCounterPuncherのコンセプトを考えると、危険な状況では守りに徹し、余裕がある時は柔軟に対応するという挙動は理にかなっています。
ただし正直なところ「HPが低い→Defensive」程度の判断であり、ルールベースで同等のことは簡単に書けます。①の「知性的な判断」と呼ぶには遠い印象です。
→ ファインチューニングにより、自然言語指示の解釈がより柔軟になっている(ただし知性的とまでは言えない)
CounterPuncher(チューニング済み)の出力例(展開)
優勢時(Adaptive選択):
{
"AnalysisResult": "Keep what works, change what fails",
"BasicTactic": "Adaptive"
}
劣勢時(Defensive選択):
{
"AnalysisResult": "Critical - defensive stance",
"BasicTactic": "Defensive"
}
体力危険時(Defensive選択):
{
"AnalysisResult": "Survival priority - protect HP",
"BasicTactic": "Defensive"
}
チューニング済みは短く的確な分析テキストを出力しています。
一方、チューニングなしでは状況が変わっても一律Defensiveで、分析テキストも冗長でした。
PatternBreaker(パターン崩し)
| 戦術タイプ | チューニング済み | チューニングなし |
|---|---|---|
| Disruptive | 62回 | 75回 |
| Adaptive | 38回 | 25回 |
両方ともDisruptiveとAdaptiveの混合ですが、比率が異なります。
-
チューニングなし: Disruptive 75%とDisruptive寄り。しかし実験結果を詳しく見ると、AdaptiveとDisruptiveの切り替わりパターンがほぼ固定されています。やはり小型LLMにランダムや撹乱は難しそうです。ランダム性を上げるなら、ハイパーパラメーターを変更すべきだったかもしれません
-
チューニング済み: Disruptive 62%、Adaptive 38%とよりバランスが取れており、切り替えの規則性もやや緩和されています。
→ 両方で自然言語指示が機能しているが、ファインチューニング版の方がより多様な戦術選択を示している
レスポンス内容の傾向
Berserker(チューニング済み)のレスポンス例(展開)
{
"AnalysisResult": "Keep attack, modify defense",
"BasicTactic": "Aggressive",
"AttackCriteria": "Dispersion Focus",
"ContinuousAttackCriteria": "Speed Priority",
"DefenseCriteria": "Return Priority",
"ContinuousDefenseCriteria": "Evasive Counter Priority"
}
トークン数: 約120-135 / レスポンス文字数: 約240-270文字
→ 簡潔で効率的な出力。
Berserker(チューニングなし)のレスポンス例(展開)
{
"AnalysisResult": "Attack enemy's weak points with aggressive combos while maintaining evasive readiness",
"BasicTactic": "Aggressive",
"AttackCriteria": "Dispersion Focus",
"ContinuousAttackCriteria": "Speed Priority",
"DefenseCriteria": "Evasive Counter Priority",
"ContinuousDefenseCriteria": "Return Priority"
}
トークン数: 約148-175 / レスポンス文字数: 約296-350文字
→ AnalysisResultが冗長。トークン消費が多く、応答時間にも影響。
ファインチューニングにより、モデルが「何を出力すべきか」を学習し、無駄のないレスポンスを生成できるようになっています。
AnalysisResultフィールドの記述が簡潔になっているのがその証拠です。
考察と総括
期待した①②の達成度
① 前後の文脈を考慮した非線形的な判断 → 微妙
正直に言って、この点はあまり達成できていません。
LLMの出力を見ると、戦況に応じた判断の変化はあるものの(CounterPuncherの戦況別分岐など)、「HPが低い→Defensive」程度の単純なものでした。
それならルールベースAIで十分 です。2Bモデルの限界として、複雑な状況推論は厳しいと言わざるを得ません。
② 自然言語を作戦に反映する → 部分的に成功
こちらは 部分的に成功 しました。
特にファインチューニング済みモデルにおいて、自然言語指示タイプに応じた行動傾向の変化が確認できました。
Berserkerなら攻撃的に、CounterPuncherなら防御/適応的に、PatternBreakerなら撹乱的に、という傾向の制御ができています。
重要なのは、ファインチューニングで出力フォーマットに特化させても、自然言語の解釈能力は失われていなかった という点です。
これは今後のLLMゲーム活用において有意義な知見だと考えています。
ファインチューニングの効果まとめ
| 観点 | 効果 |
|---|---|
| 応答速度 | 19〜30%の高速化(リアルタイムゲームでは体験に直結する差) |
| トークン効率 | より少ないトークンで同等の構造化出力を生成 |
| 自然言語指示の解釈 | より多様な戦術パターンを生成(CounterPuncher等) |
| 出力品質 | AnalysisResultがより簡潔に |
設計上の反省
今回の設計を振り返ると、いくつかの構造的な問題がありました。
-
LLMに判断の主導権を持たせすぎた — 6つのパラメータを同時に決定させるのは、小型モデルの能力を過大評価していました。もっと限定的な役割に絞るべきでした。
-
更新頻度と推論コストのミスマッチ — 5秒間隔の更新は格闘ゲームの展開速度に対して低頻度ですが、2Bモデルの処理速度(1.5〜2.5秒/回)ではこれが限界に近い。リアルタイム性が求められるゲームとLLMの推論コストは根本的に相性が悪いと感じました。
-
プロンプト長の肥大化 — 戦況を正確に伝えるために必要な情報量と、小型LLMが処理できるコンテキスト長(2048トークン)のバランスが難しかった。情報を増やしても判断精度が上がるとは限らず、むしろ混乱してルール順守の精度が落ちたりもします。
次の展望:LLMは「脳」ではなく「通訳」として使うべき
率直に言えば、2Bモデル単体でリアルタイム戦闘AIを構築する という今回の設計は、性能に対して負荷が大きすぎました。
しかし、この結果は LLMがゲームで活用できない という結論に直ちに結びつくものではないと考えています。
今回の経験を踏まえて、以下のようなハイブリッド設計が有効だと考えています。
| ML_LLMハイブリッド設計案 |
|---|
![]() |
- LLMは「通訳」に徹する — 低頻度(試合開始時・作戦変更時のみ)で呼び出し、自然言語の作戦指示をMLが扱える数値パラメータに変換する役割に限定する。推論頻度が激減するので、より高パラメータなモデルも使える。
- リアルタイム判断は軽量MLが担う — 戦況を表す数値入力と、LLMがパラメータ化した「行動の傾向」を組み合わせて、高頻度で行動を出力する。
今回の実験で①の自然言語処理が部分的に機能することは確認できたので、LLMの役割を「自然言語インターフェース」に絞り、リアルタイム判断はMLに任せるのが現実的な設計だと考えています。
この設計なら自然言語から抽出した 「行動の傾向」と「戦況」から「次に何をする?」という粒度の判断をMLでリアルタイム出力 する、まるで人間のようなAIも作れるかもしれません。
おわりに
今回の600回のテストで確認できたことをまとめます。
-
2Bモデル単体のリアルタイムAIは設計として厳しかった。 性能の割に負荷が高く、判断の質もルールベースを大幅に超えるものではなかった。
-
ファインチューニングの効果は明確。 応答速度が全体平均で約25%向上し、自然言語指示の解釈も柔軟になった。
-
自然言語による行動傾向の制御は部分的に成功。 ファインチューニングで特定処理に特化させても、言語処理能力が失われないことが確認できた。
-
LLMの活用方法を「リアルタイムAI」から「自然言語インターフェース」に再設計すべき。 LLMを「脳」ではなく「通訳」として使い、ML+LLMのハイブリッド構成が次のステップ。
小型LLMをゲームに組み込むのは上手くいきませんでしたが、ファインチューニングの効果と自然言語指示の可能性が確認できたのはいい成果でした。
今後は中規模LLMで自然言語の指示をパラメータ化する、という部分について実際に検証したりしてみようかなと考えています。
同じようなことを考えている方の参考になれば幸いです。
それではまた!!
[追記]
最後にプレイ動画を貼っておきます。


