ゲーム開発において、NPC(Non-Player Character)の感情表現はプレイヤーの没入感を左右する重要な要素です。近年、Large Language Models(LLM)を活用した高度な感情計算が可能になり、静的なスクリプトでは実現できなかった動的で自然なNPCの感情応答が可能になっています。本稿では、HolySheep AIのAPIを活用したNPC感情システムの構築方法を実践的に解説します。
なぜLLMベースの感情計算なのか
従来のNPC感情システムでは、状態機械(State Machine)や芬蘭式意思決定木(Behavior Tree)が主流でした。しかし、これらの手法には致命的な限界があります。
- 事前定義された感情パターンの繰り返しによる不自然さ
- プレイヤー行動への柔軟な対応が困難
- 感情遷移の複雑さに対応できない
LLMを活用することで、NPCはプレイヤーの行動や会話内容に応じてリアルタイムで適切な感情状態を生成し、文脈に沿った応答を返すことができます。
実装アーキテクチャ
システム構成
┌─────────────────────────────────────────────────────────────┐
│ NPC感情計算システム │
├─────────────────────────────────────────────────────────────┤
│ │
│ [プレイヤー入力] → [感情分析エンジン] → [LLM推論] │
│ ↓ ↓ │
│ [世界状態DB] ← [感情状態管理] → [応答生成] │
│ ↓ ↓ │
│ [ゲームエンジン] ←────────┴──────────── [レンダリング] │
│ │
└─────────────────────────────────────────────────────────────┘
実践的実装:Pythonコード
1. 感情分析エンジン
import json
import httpx
from typing import Optional
from dataclasses import dataclass, asdict
from enum import Enum
class EmotionType(Enum):
JOY = "joy"
SADNESS = "sadness"
ANGER = "anger"
FEAR = "fear"
SURPRISE = "surprise"
TRUST = "trust"
DISGUST = "disgust"
ANTICIPATION = "anticipation"
NEUTRAL = "neutral"
@dataclass
class EmotionState:
primary: EmotionType
intensity: float # 0.0 - 1.0
trigger: str
duration_ms: int
secondary_emotions: list[EmotionType] = None
@dataclass
class NPCContext:
npc_id: str
name: str
personality_traits: dict
relationship_level: float # -1.0 ~ 1.0
current_mood: EmotionState
memory: list[dict]
class HolySheepAPIClient:
"""HolySheep AI APIクライアント - 感情計算専用ラッパー"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
self.timeout = 10.0
def analyze_emotion(
self,
context: NPCContext,
stimulus: str
) -> EmotionState:
"""
NPCの感情状態を分析
実際の遅延: <50ms(HolySheep AI国内最適化API)
コスト: ¥1=$1相当(DeepSeek V3.2モデル使用時)
"""
system_prompt = self._build_emotion_analysis_prompt(context)
payload = {
"model": "deepseek-v3.2",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"プレイヤー行動: {stimulus}"}
],
"temperature": 0.7,
"max_tokens": 200
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
try:
with httpx.Client(timeout=self.timeout) as client:
response = client.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
result = response.json()
return self._parse_emotion_response(result)
except httpx.TimeoutException as e:
raise ConnectionError(
f"感情分析APIタイムアウト: {stimulus[:50]}... "
f"network_latency={self.timeout}s exceeded"
) from e
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise ConnectionError(
"401 Unauthorized: APIキーが無効です。"
"https://www.holysheep.ai/register で新しいキーを発行してください。"
) from e
raise
def _build_emotion_analysis_prompt(self, context: NPCContext) -> str:
traits = ", ".join(
f"{k}:{v}" for k, v in context.personality_traits.items()
)
return f"""あなたはNPCの感情分析エンジンです。
NPC情報:
- 名前: {context.name}
- 性格特性: {traits}
- 関係性スコア: {context.relationship_level:.2f} (-1=敵対, 0=中立, 1=友好)
- 現在の気分: {context.current_mood.primary.value} (強度:{context.current_mood.intensity:.2f})
直近の記憶:
{json.dumps(context.memory[-5:], ensure_ascii=False, indent=2)}
以下のJSON形式で感情状態を返してください:
{{
"primary_emotion": "emotion_type",
"intensity": 0.0-1.0,
"trigger": "感情のトリガーとなった行動",
"duration_ms": estimated_duration,
"secondary_emotions": ["optional", "secondary", "emotions"]
}}
emotion_typeは以下のいずれか: joy, sadness, anger, fear, surprise, trust, disgust, anticipation, neutral"""
def _parse_emotion_response(self, response: dict) -> EmotionState:
content = response["choices"][0]["message"]["content"]
# JSONパースを安全に行う
try:
data = json.loads(content)
return EmotionState(
primary=EmotionType(data["primary_emotion"]),
intensity=float(data["intensity"]),
trigger=data.get("trigger", ""),
duration_ms=int(data.get("duration_ms", 3000)),
secondary_emotions=[
EmotionType(e) for e in data.get("secondary_emotions", [])
]
)
except (json.JSONDecodeError, KeyError, ValueError) as e:
# フォールバック: パース失敗時は中立的な感情を返す
return EmotionState(
primary=EmotionType.NEUTRAL,
intensity=0.3,
trigger="感情解析エラーによるフォールバック",
duration_ms=2000
)
2. NPC感情管理システム
import asyncio
from datetime import datetime
from collections import deque
class NPCEmotionManager:
"""NPCの感情状態を管理し、長期的な感情推移を追跡"""
def __init__(self, api_client: HolySheepAPIClient, max_memory: int = 50):
self.client = api_client
self.npcs: dict[str, NPCContext] = {}
self.emotion_history: dict[str, deque] = {maxlen: max_memory}
self.event_log: list[dict] = []
def register_npc(self, npc_id: str, name: str, personality: dict) -> NPCContext:
"""NPCをシステムに登録"""
context = NPCContext(
npc_id=npc_id,
name=name,
personality_traits=personality,
relationship_level=0.0,
current_mood=EmotionState(
primary=EmotionType.NEUTRAL,
intensity=0.5,
trigger="初期化",
duration_ms=0
),
memory=[]
)
self.npcs[npc_id] = context
self.emotion_history[npc_id] = deque(maxlen=50)
return context
async def process_player_action(
self,
npc_id: str,
player_action: str
) -> tuple[EmotionState, str]:
"""
プレイヤー行動を処理し、NPCの感情変化と応答を生成
Returns:
(新しい感情状態, NPCの音声/テキスト応答)
"""
if npc_id not in self.npcs:
raise ValueError(f"NPC {npc_id} が登録されていません")
npc = self.npcs[npc_id]
# ステップ1: 感情分析(API呼び出し)
start_time = asyncio.get_event_loop().time()
try:
new_emotion = self.client.analyze_emotion(npc, player_action)
except ConnectionError as e:
# API障害時のフォールバック処理
new_emotion = self._fallback_emotion(npc, player_action)
latency_ms = (asyncio.get_event_loop().time() - start_time) * 1000
# ステップ2: 感情状態更新
old_emotion = npc.current_mood
npc.current_mood = new_emotion
# ステップ3: 関係性スコア調整
self._update_relationship(npc, old_emotion, new_emotion)
# ステップ4: 記憶に追加
self._add_memory(npc, player_action, new_emotion)
# ステップ5: 応答生成
response = await self._generate_response(npc, player_action, new_emotion)
# イベントログ記録
self.event_log.append({
"timestamp": datetime.now().isoformat(),
"npc_id": npc_id,
"player_action": player_action,
"emotion": new_emotion.primary.value,
"intensity": new_emotion.intensity,
"latency_ms": round(latency_ms, 2)
})
return new_emotion, response
def _update_relationship(
self,
npc: NPCContext,
old: EmotionState,
new: EmotionState
):
"""感情変化に基づいて関係性スコアを更新"""
# 正の感情変化 → 関係性向上
positive_emotions = {EmotionType.JOY, EmotionType.TRUST, EmotionType.ANTICIPATION}
negative_emotions = {EmotionType.ANGER, EmotionType.SADNESS, EmotionType.DISGUST}
delta = (new.intensity - old.intensity) * 0.1
if new.primary in positive_emotions:
npc.relationship_level = min(1.0, npc.relationship_level + delta)
elif new.primary in negative_emotions:
npc.relationship_level = max(-1.0, npc.relationship_level - delta)
def _add_memory(
self,
npc: NPCContext,
action: str,
emotion: EmotionState
):
"""NPCの記憶にイベントを追加"""
memory_entry = {
"action": action,
"emotion": emotion.primary.value,
"intensity": emotion.intensity,
"timestamp": datetime.now().isoformat()
}
npc.memory.append(memory_entry)
# 記憶上限管理
if len(npc.memory) > 100:
npc.memory = npc.memory[-100:]
def _fallback_emotion(
self,
npc: NPCContext,
action: str
) -> EmotionState:
"""API障害時のフォールバック感情生成"""
action_keywords = {
"attack": EmotionType.FEAR,
"gift": EmotionType.JOY,
"threaten": EmotionType.ANGER,
"help": EmotionType.TRUST
}
primary = EmotionType.NEUTRAL
for keyword, emotion in action_keywords.items():
if keyword in action.lower():
primary = emotion
break
return EmotionState(
primary=primary,
intensity=0.6,
trigger="APIフォールバック",
duration_ms=5000
)
async def _generate_response(
self,
npc: NPCContext,
action: str,
emotion: EmotionState
) -> str:
"""感情に応じたNPC応答を生成"""
system_prompt = f"""あなたは{npc.name}です。
性格: {json.dumps(npc.personality_traits, ensure_ascii=False)}
関係性: {npc.relationship_level:.2f}
現在の感情: {emotion.primary.value} (強度: {emotion.intensity:.2f})
あなたの性格と現在の感情に合わせて、自然な応答を生成してください。
日本語で、最大2文で応答してください。"""
payload = {
"model": "deepseek-v3.2",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"状況: {action}"}
],
"temperature": 0.8,
"max_tokens": 100
}
headers = {
"Authorization": f"Bearer {self.client.api_key}",
"Content-Type": "application/json"
}
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{self.client.base_url}/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except Exception:
# フォールバック応答
return f"({npc.name}は何かを考えている...)"
3. 使用例:ゲームループへの統合
import os
async def main():
"""ゲームループでの使用例"""
# HolySheep AI API初期化
# 登録すると無料クレジット付与: https://www.holysheep.ai/register
api_key = os.environ.get("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
client = HolySheepAPIClient(api_key)
# 感情マネージャー初期化
emotion_manager = NPCEmotionManager(client)
# NPC登録(ショップ主人)
shopkeeper = emotion_manager.register_npc(
npc_id="npc_shop_001",
name="マルコ",
personality={
"friendly": 0.8,
"greedy": 0.6,
"honest": 0.9,
"cautious": 0.4
}
)
print(f"✓ NPC登録完了: {shopkeeper.name}")
# プレイヤー行動のシミュレーション
player_actions = [
"プレイヤーが店に高い金額を持ち込んだ",
"プレイヤーが商品を見て何も買わず帰ろうとした",
"プレイヤーがマルコを公平に扱い、アイテムをありがとうと言った"
]
print("\n=== 感情計算シミュレーション ===\n")
for action in player_actions:
print(f"【プレイヤー】: {action}")
new_emotion, response = await emotion_manager.process_player_action(
npc_id="npc_shop_001",
player_action=action
)
print(f"【{shopkeeper.name}の感情】: {new_emotion.primary.value}")
print(f" 強度: {new_emotion.intensity:.2f}")
print(f" トリガー: {new_emotion.trigger}")
print(f"【応答】: {response}")
print(f" 関係性: {shopkeeper.relationship_level:.2f}")
print()
if __name__ == "__main__":
asyncio.run(main())
4. 出力結果の例
=== 感情計算シミュレーション ===
【プレイヤー】: プレイヤーが店に高い金額を持ち込んだ
【マルコの感情】: anticipation
強度: 0.85
トリガー: 金銭的機会の認識
【応答】: おやおや、これは繁盛の兆しですね!何を探されていますか?
関係性: 0.08
【プレイヤー】: プレイヤーが商品を見て何も買わず帰ろうとした
【マルコの感情】: sadness
強度: 0.65
トリガー: 商売の不安
【応答】: ええ、もう少し見ていかれませんか?きっとお気に召すものが...
関係性: 0.02
【プレイヤー】: プレイヤーがマルコを公平に扱い、アイテムをありがとうと言った
【マルコの感情】: joy
強度: 0.92
トリガー: 友善な交流
【応答】: こちらこそ!またいらっしゃってください。お友達価格 приложしますよ!
関係性: 0.15
HolySheep AIを選ぶ理由
本システムはHolySheep AIのAPIを基盤としています。ゲーム開発において重要な指標を比較してみましょう:
| 項目 | HolySheep AI | 業界平均 |
|---|---|---|
| コスト効率 | ¥1=$1(85%節約) | ¥7.3=$1 |
| レイテンシ | <50ms | 200-500ms |
| DeepSeek V3.2 | $0.42/MTok | $0.42/MTok(同等) |
| Gemini 2.5 Flash | $2.50/MTok | $2.50/MTok(同等) |
| 決済方法 | WeChat Pay / Alipay対応 | 限定的 |
| 無料クレジット | 登録時付与 | 稀 |
リアルタイム性が求められるNPC感情計算において、<50msのレイテンシはプレイヤーの没入感を損なわない応答速度を保証します。
よくあるエラーと対処法
エラー1: ConnectionError: timeout - 感情分析APIタイムアウト
# 問題: API呼び出しが10秒間応答なし
原因: ネットワーク遅延またはAPI側の過負荷
解決策1: タイムアウト設定の調整
client = HolySheepAPIClient(api_key)
client.timeout = 30.0 # デフォルト10秒から30秒に延長
解決策2: 非同期リトライ機構の実装
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
async def resilient_analyze(client, context, stimulus):
return await client.analyze_emotion(context, stimulus)
解決策3: フォールバック感情システムの活用
emotion = emotion_manager._fallback_emotion(npc_context, player_action)
print(f"フォールバック感情: {emotion.primary.value}")
エラー2: 401 Unauthorized - APIキー認証エラー
# 問題: API呼び出し時に401エラーが発生
原因: APIキーが無効または期限切れ
正しいキーの確認と設定
import os
方法1: 環境変数から安全に読み込み
api_key = os.environ.get("HOLYSHEEP_API_KEY")
if not api_key:
raise RuntimeError(
"HOLYSHEEP_API_KEYが環境変数に設定されていません。"
"https://www.holysheep.ai/register でAPIキーを取得してください。"
)
方法2: .envファイルの使用(.gitignoreに追加すること)
from dotenv import load_dotenv
load_dotenv() # .envファイルから読み込み
api_key = os.getenv("HOLYSHEEP_API_KEY")
方法3: キーの有効性チェック
def validate_api_key(key: str) -> bool:
if not key or len(key) < 10:
return False
# 基本的なフォーマットチェック
return key.startswith("hs_") or key.startswith("sk-")
if not validate_api_key(api_key):
raise ValueError("無効なAPIキー形式です")
エラー3: JSONDecodeError - LLM応答のパース失敗
# 問題: LLMの応答が予期したJSON形式でない
原因: モデルが不正な形式を生成、または出力文字数制限超過
解決策1: より厳格なプロンプト設計
system_prompt = """あなたは感情分析エンジンです。
【重要】 반드시以下のJSON形式のみを出力してください。説明や前置詞は不要です:
{"primary_emotion": "joy", "intensity": 0.8, "trigger": "reason", "duration_ms": 3000}
許可される感情: joy, sadness, anger, fear, surprise, trust, disgust, anticipation, neutral
intensityは0.0から1.0の間の小数です。"""
解決策2: パース失敗時のフォールバック処理
def safe_parse_emotion(response_text: str) -> EmotionState:
try:
data = json.loads(response_text)
return EmotionState(
primary=EmotionType(data["primary_emotion"]),
intensity=float(data["intensity"]),
trigger=data.get("trigger", "パース成功"),
duration_ms=int(data.get("duration_ms", 3000))
)
except (json.JSONDecodeError, KeyError, ValueError) as e:
# パース失敗時は構造化プロンプトで再試行
print(f"パース警告: {e} - フォールバック感情を適用")
return EmotionState(
primary=EmotionType.NEUTRAL,