「NPC の返答を毎回スクリプトで書き切るのは、もう限界かもしれない」——UE5 の制作中にそう感じたことはないでしょうか。NPC のAI挙動、プロシージャル生成、ゲーム内ヘルパーと、AIエージェントを「ゲーム内オブジェクト」として動かしたい場面は、実際の開発現場で着実に増えています。
ところが、いざ実装しようとすると公式ドキュメントは点在していて、Blueprint と C++ の境界、メインスレッドの扱い、パッケージング時の依存関係など、踏み抜きやすい落とし穴が多いのが現状です。本稿は私が知人のインディーゲーム開発者と一緒に、UE5 × Antigravity を実プロジェクトに組み込む過程で得た知見を、実装パターン・コード・失敗事例まで含めてまとめます。
なぜUE5×Antigravityが注目されているのか
正直に書くと、Antigravity と UE5 の連携が話題になり始めたのは最近で、半年前は「Cursor で C++ を書く」が事実上の標準でした。ここに来て一気に流れが変わったのは、3つの技術的要因が揃ったからです。
ひとつ目は、Antigravity の Workspace モデルがゲーム開発の責務分離と相性が良いことです。NPC ロジック・レベルデザイン・ゲームプレイ・最適化、というUE5特有の職能分離が、Workspace の並列エージェントに自然にマッピングできます。
ふたつ目は、Google 系 3D パイプラインとの統合が早く進んだことです。Sketchfab・glTF・USD などのアセットフォーマット周りで Antigravity が文脈を持っていて、これが UE5 のアセットインポートワークフローと噛み合います。
3つ目は、ゲーム内 LLM 推論の現実解として「Antigravity 経由で外部ホストに振る」「ローカル軽量モデルにフォールバック」という二段構えが定着し始めたことです。これにより、レイテンシ・コスト・プライバシーのトレードオフを設計段階で詰められるようになりました。
この3つが揃ったため、いま UE5 × Antigravity を試す価値が一気に上がっています。
統合アーキテクチャの3パターン
UE5 に Antigravity を組み込むときの実装パターンは、私が見てきた限り3つに収斂します。それぞれ向き・不向きがあります。
パターンA:プラグイン直結
UE5 のプラグインとして Antigravity の SDK を直接組み込む方法です。最も統合度が高く、Blueprint ノードからエージェントを呼び出せるレベルまで持っていけます。
// MyAntigravityComponent.h(抜粋)
UCLASS(ClassGroup=(AI), meta=(BlueprintSpawnableComponent))
class MYGAME_API UMyAntigravityComponent : public UActorComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category="AI|Antigravity")
void RequestNPCDialog(const FString& Prompt, const FString& NPCRole);
UPROPERTY(BlueprintAssignable, Category="AI|Antigravity")
FOnDialogReceived OnDialogReceived;
protected:
virtual void BeginPlay() override;
private:
TSharedPtr<class FAntigravityClient> Client;
};このパターンの長所は、ゲームプレイプログラマが Blueprint だけで AI エージェントを組み込めることです。短所は、UE5 のバージョンアップ時にプラグインの追従が必要なことと、プラグインが商用ライセンス対応していない場合は採用できないことです。
パターンB:HTTPブリッジ経由
ゲーム本体とは別プロセスで Antigravity ブリッジサーバーを動かし、UE5 から HTTP/WebSocket で叩く方法です。最もシンプルで、依存関係が綺麗に切れます。
void UMyAIService::SendPromptAsync(const FString& Prompt, FOnResponse OnResponse)
{
auto Request = FHttpModule::Get().CreateRequest();
Request->SetURL(TEXT("http://127.0.0.1:9911/agent/respond"));
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
FString Payload = FString::Printf(TEXT("{\"prompt\":\"%s\"}"), *Prompt);
Request->SetContentAsString(Payload);
Request->OnProcessRequestComplete().BindLambda(
[OnResponse](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid())
{
OnResponse.ExecuteIfBound(Res->GetContentAsString());
}
});
Request->ProcessRequest();
}この方式の最大の長所は、ブリッジ側を Python や TypeScript で書けることです。Antigravity の最新機能を試すときも、ゲーム本体をリビルドせずに済みます。短所はレイテンシが10〜30msほど乗ることと、本番配布時にブリッジサーバーをどうパッケージングするかという問題が残ることです。
パターンC:専用ランタイム(推論ノード分離)
ゲーム本体とは別の専用ランタイムを立てて、推論はそちらに任せる方法です。ホスト側に PC と GPU があり、UE5 はクライアントとして振る舞います。MMO 系や、エージェントが多数同時に動くゲームに向いています。
私が知人のプロジェクトで採用したのはパターンBの HTTPブリッジで、理由は「最初の試作段階ではブリッジ側を頻繁に書き換える」「商用化の判断はリリース直前で決める」という方針だったからです。プラグイン直結(パターンA)は完成形としては理想ですが、開発初期の柔軟性を失います。
NPC AIエージェント実装:状態遷移とエージェント間通信
NPC に「Antigravity エージェント」を持たせるとき、最初に直面するのは「いつエージェントを呼ぶか」という設計判断です。毎フレーム呼べばコストが膨らみ、ゲームのフレームレートを破壊します。逆に呼ばなさすぎると、AI らしい応答性が消えます。
私たちが落ち着いた設計は、Behavior Tree のノードからエージェントを呼ぶ「イベント駆動型」です。NPC が特定の状態に入ったとき(プレイヤー接近・会話開始・敵対関係変化)にだけエージェントを起動し、結果は Blackboard に保存して既存の AI システムから参照します。
// UBTTaskNode_AntigravityRespond.cpp(抜粋)
EBTNodeResult::Type UBTTaskNode_AntigravityRespond::ExecuteTask(
UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory)
{
auto BB = OwnerComp.GetBlackboardComponent();
if (!BB) return EBTNodeResult::Failed;
FString Context = BB->GetValueAsString(ContextKey.SelectedKeyName);
auto AIService = OwnerComp.GetAIOwner()->GetPawn()->FindComponentByClass<UMyAIService>();
if (!AIService) return EBTNodeResult::Failed;
AIService->SendPromptAsync(
Context,
FOnResponse::CreateLambda([&OwnerComp, this](const FString& Response)
{
auto BB = OwnerComp.GetBlackboardComponent();
BB->SetValueAsString(ResponseKey.SelectedKeyName, Response);
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
})
);
return EBTNodeResult::InProgress;
}ここで重要なのは EBTNodeResult::InProgress を返して非同期で結果を待つことです。同期的に結果を待つとメインスレッドがブロックされ、フレームレートが一気に落ちます。
エージェント間通信が必要なケース(NPC同士の協調行動など)では、共通の Blackboard を持つ AI Controller を介すのではなく、Antigravity の Workspace に「町の意識」のような単一エージェントを置いて、各 NPC エージェントがそこに問い合わせる構造が機能しました。
プロシージャル生成への適用
レベルデザインへのAntigravity適用は、リアルタイムではなくビルドタイムで使うのが現実解です。ゲーム実行中にエージェントがレベルを生成するとレイテンシが致命的になりますが、開発中に「このパラメータ範囲でレベルを100個生成してくれ」を Workspace に投げれば、デザイン作業が劇的に加速します。
具体的には、UE5 の Procedural Content Generation Framework(PCG)と Antigravity を以下のような形で組み合わせます。
- デザイナーが Workspace に「砂漠の遺跡、難易度中、所要時間15分、回避必須の罠を3つ含む」のような自然言語仕様を渡す
- Antigravity 側のエージェントが PCG グラフのパラメータ JSON を生成
- UE5 のコマンドラインツール(
UnrealEditor-Cmd.exe)でバッチ実行し、レベルアセットを保存 - デザイナーがエディタで開いて手調整
このフローを構築すると、レベルデザインのイテレーション速度が体感で5倍以上になります。Antigravity の真価は「実行時の魔法」ではなく「制作工程の自動化」にあります。
60fpsを死守するためのパフォーマンス最適化
UE5 でAIエージェントを使うとき、最も重要な制約は「フレームレートを落とさない」ことです。AAAタイトル並みの60fps(あるいは120fps)を維持するためには、以下の3つを徹底します。
ひとつ目は すべてのエージェント呼び出しを非同期化 することです。HttpRequest::ProcessRequest() も FAntigravityClient::Send() も、メインスレッドで結果を待ってはいけません。前述の Behavior Tree 実装のように、InProgress 状態を返してコールバックで結果を受け取る設計を徹底します。
ふたつ目は 応答キャッシュの導入 です。同じ NPC が同じ状況で同じ応答を返すことは多いので、(NPC_ID, 状況コード) をキーにしたキャッシュを LRU で持つだけで、API 呼び出しが半分以下に減ります。私たちのプロジェクトでは、キャッシュヒット率が67%まで上がりました。
class FResponseCache
{
public:
TOptional<FString> Get(const FString& Key)
{
FScopeLock Lock(&CacheMutex);
if (FCacheEntry* Entry = Cache.Find(Key))
{
Entry->LastUsed = FPlatformTime::Seconds();
return Entry->Value;
}
return {};
}
void Put(const FString& Key, const FString& Value)
{
FScopeLock Lock(&CacheMutex);
if (Cache.Num() >= MaxEntries) Evict();
Cache.Add(Key, { Value, FPlatformTime::Seconds() });
}
private:
struct FCacheEntry { FString Value; double LastUsed; };
TMap<FString, FCacheEntry> Cache;
FCriticalSection CacheMutex;
static constexpr int32 MaxEntries = 512;
void Evict();
};3つ目は バッチング です。複数 NPC が同時に「次の発話を決めたい」と要求するなら、フレーム単位で集約して Workspace に1回だけ投げる設計にします。これだけで API コストが NPC 数分の1になります。
私が踏んだ3つの落とし穴
実装してきた中で、明確に「これは事前に知っておきたかった」というポイントが3つあります。
落とし穴1:メインスレッドブロックでカクつく
最初のプロトタイプで、Antigravity からの応答を Future::Wait() で待ってしまい、視覚的にゲームがカクつく現象が出ました。原因は単純で、HTTP応答が来るまでの200msほどの間、メインスレッドが完全に止まっていたためです。
対処は前述の通り「すべての呼び出しを非同期化」ですが、UE5 のラムダキャプチャで this を渡すときは弱参照(TWeakObjectPtr<UMyComponent>)にしないと、応答が来る前にコンポーネントが破棄されてクラッシュします。これは UE5 ガベージコレクション周りで陥りやすい罠です。
落とし穴2:エディタとパッケージング版で挙動が違う
エディタ上では完璧に動いていたものが、パッケージング後に動かなくなる、という事故が起きました。原因は、Antigravity SDK が依存している外部DLLが Shipping ビルドに含まれていなかったためです。
UE5 の *.uplugin ファイルで WhitelistPlatforms と LoadingPhase を正しく設定し、Build.cs で RuntimeDependencies に DLL を明示する必要があります。これは公式ドキュメントには散発的にしか書かれていないので、最初のパッケージングテストはプロジェクトの早い段階で必ず通しておくことを強く推奨します。
// MyAntigravityPlugin.Build.cs
public MyAntigravityPlugin(ReadOnlyTargetRules Target) : base(Target)
{
RuntimeDependencies.Add(Path.Combine(PluginDirectory, "Binaries/ThirdParty/antigravity_sdk.dll"));
PublicDelayLoadDLLs.Add("antigravity_sdk.dll");
}落とし穴3:マルチプレイヤーで状態が同期されない
NPC の AI 応答をホストでだけ生成し、結果を Replication でクライアントに配るのが正解です。最初は各クライアントで独立に Antigravity を呼んでいましたが、当然プレイヤーごとに違う応答が返り、世界観が破綻しました。
UFUNCTION(NetMulticast, Reliable) または UPROPERTY(ReplicatedUsing=) を使って、応答結果を全クライアントに配信する設計が必須です。エージェント呼び出しの権威性は常にホスト側に置きます。
マルチプレイヤー対応の考え方
マルチプレイヤーゲームに Antigravity を組み込むときの設計指針は、「AIの権威はサーバー、表現はクライアント」です。具体的には以下のような分担になります。
サーバー側(Dedicated Server または Listen Server のホスト)が Antigravity を呼び出し、応答テキストとアニメーション ID を生成します。それを Replication でクライアントに配り、各クライアントは受け取った情報をローカルで再生するだけです。
レイテンシ補正には、クライアント側で「AI が何かを話そうとしている」というプレースホルダーアニメーション(NPC が口を開く、考え込む仕草)を即座に再生し、サーバーから本物の応答が届いたら差し替える、という UX が有効です。プレイヤーは200msの待ち時間を「考えている」と認識します。
商用化のための準備
最後に、Antigravity を組み込んだ UE5 ゲームを実際にストアでリリースする際のチェックリストを書きます。
API コストの予測モデルを構築することが第一です。プレイヤー1人あたり1セッションで何回 Antigravity を呼ぶかを実測し、月間アクティブユーザー数で掛け算します。NPC 1体あたり平均10回 / 1セッション、平均セッション30分、月間DAU 1万人なら、月間API呼び出しが300万回になります。これに単価を掛けると、ゲーム収益との比率が見えます。
ふたつ目は、API 障害時のフォールバック実装です。Antigravity が一時的に応答しない場合に、事前に用意したスクリプト応答に切り替える仕組みを必ず用意します。プレイヤーから見て「ゲームが壊れた」状態を作らないことが、レビューを守る鍵です。
3つ目は、ガイドラインへの対応です。各プラットフォーム(Steam・PSN・Xbox Live・Switch)で生成AIコンテンツに関するポリシーが急速に変化しているので、リリース前に最新規約を確認します。特に「ユーザーの入力を AI に流す」場合のプライバシー表示は、各ストアで義務付けが進んでいます。
全体を振り返ってにかえて
UE5 × Antigravity は「魔法のようにNPCが賢くなる」というより、「制作工程と実行時のAIエージェントを統合する設計判断」が9割を占めるトピックです。本稿で紹介したパターンとコードが、あなたのプロジェクトの一歩目を楽にできれば嬉しいです。
次に試すなら、まずパターンB(HTTPブリッジ)で1体のNPCを動かしてみてください。ブリッジ側でレスポンスをログに吐くだけでも、AI挙動の設計勘所が一気に見えてきます。