🐕

ノベルゲーム型MMO - NPC会話システム設計書

に公開

はじめに

  • 生成AIで作ったゲーム設計をそのまま書きます。
  • PostgreSQL内にワールドを構築したノベルゲーム型MMOを作ります。
  • PostgreSQLだけでワールドを同期するので実装の簡易さを実現します。
  • 実装はC#とPython、クライアントはUnityを使います。

ノベルゲーム型MMO - NPC会話システム設計書

概要

本設計書は、プレイヤーごとの個別記憶を持つNPC会話システムの実装計画を詳述する。システムは.NETをコアとしつつ、一部の高度な機能をPythonマイクロサービスで強化する、コストパフォーマンスに優れたハイブリッドアーキテクチャを採用する。

1. システムアーキテクチャ

1.1 全体構成

[Unity Client] <---> [.NET Core Backend] <---> [PostgreSQL]
                           |
                           v
                  [Python Microservices]
                  - 感情分析サービス
                  - 重要度スコアリングサービス

1.2 技術スタック

コンポーネント 技術
フロントエンド Unity
コアバックエンド .NET Core
データベース PostgreSQL (Vector拡張)
AI機能 OpenAI API
補助サービス Python (FastAPI)
通信プロトコル JSON-RPC

1.3 開発フェーズ

  • フェーズ1: .NETコアシステム構築(3ヶ月)
  • フェーズ2: 感情分析マイクロサービス導入(1.5ヶ月)
  • フェーズ3: 重要度スコアリング強化(1.5ヶ月)

2. データモデル

2.1 主要テーブル

NPCマスターテーブル

CREATE TABLE npc_master (
    npc_id SERIAL PRIMARY KEY,
    npc_name VARCHAR(50) NOT NULL,
    personality TEXT NOT NULL,
    role VARCHAR(100) NOT NULL,
    default_prompt TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

プレイヤーテーブル

CREATE TABLE player (
    player_id SERIAL PRIMARY KEY,
    player_name VARCHAR(50) NOT NULL,
    last_login TIMESTAMP NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

NPC・プレイヤー関係テーブル

CREATE TABLE npc_player_relation (
    relation_id SERIAL PRIMARY KEY,
    npc_id INTEGER REFERENCES npc_master(npc_id),
    player_id INTEGER REFERENCES player(player_id),
    familiarity FLOAT NOT NULL DEFAULT 0.0,
    relation_status VARCHAR(50) NOT NULL DEFAULT 'neutral',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

会話履歴テーブル

CREATE TABLE conversation_history (
    conversation_id SERIAL PRIMARY KEY,
    npc_id INTEGER REFERENCES npc_master(npc_id),
    player_id INTEGER REFERENCES player(player_id),
    player_input TEXT NOT NULL,
    npc_response TEXT NOT NULL,
    input_embedding VECTOR(1536) NOT NULL,
    context_embedding VECTOR(1536) NOT NULL,
    importance_score FLOAT NOT NULL DEFAULT 0.5,
    emotion_state JSONB,
    conversation_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

記憶アーカイブテーブル

CREATE TABLE memory_archive (
    archive_id SERIAL PRIMARY KEY,
    npc_id INTEGER REFERENCES npc_master(npc_id),
    player_id INTEGER REFERENCES player(player_id),
    summary TEXT NOT NULL,
    summary_embedding VECTOR(1536) NOT NULL,
    start_date TIMESTAMP NOT NULL,
    end_date TIMESTAMP NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

2.2 インデックス

CREATE INDEX conversation_embedding_idx ON conversation_history USING ivfflat (input_embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX context_embedding_idx ON conversation_history USING ivfflat (context_embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX summary_embedding_idx ON memory_archive USING ivfflat (summary_embedding vector_cosine_ops) WITH (lists = 100);

3. .NET コアシステム設計

3.1 主要コンポーネント

3.1.1 リポジトリレイヤー

  • NPCRepository: NPCの基本情報とプレイヤーとの関係管理
  • ConversationRepository: 会話履歴管理とベクトル検索
  • MemoryRepository: 記憶のアーカイブと検索

3.1.2 サービスレイヤー

  • DialogueService: 会話フロー制御と応答生成
  • EmbeddingService: OpenAIを使用したテキスト埋め込み生成
  • MemoryManagementService: 記憶の整理とアーカイブ
  • AIPipelineService: AIサービスとの連携管理

3.1.3 コントローラーレイヤー

  • ConversationController: 会話APIエンドポイント
  • NPCController: NPC情報管理API
  • MemoryController: 記憶管理API

3.2 主要クラス設計

// 会話サービスの基本設計
public class DialogueService : IDialogueService
{
    private readonly IConversationRepository _conversationRepo;
    private readonly IEmbeddingService _embeddingService;
    private readonly IAIPipelineService _aiPipelineService;
    
    public async Task<DialogueResponse> ProcessDialogue(DialogueRequest request)
    {
        // 1. 入力テキストのエンべディング生成
        var inputEmbedding = await _embeddingService.CreateEmbedding(request.PlayerInput);
        
        // 2. 関連記憶の検索
        var relevantMemories = await _conversationRepo.FindRelevantMemories(
            request.PlayerId, 
            request.NpcId, 
            inputEmbedding, 
            5);
        
        // 3. プロンプト生成と応答取得
        var response = await _aiPipelineService.GenerateResponse(
            request.NpcId,
            request.PlayerInput,
            relevantMemories);
        
        // 4. 新しい会話の記録
        await _conversationRepo.SaveConversation(
            request.PlayerId,
            request.NpcId,
            request.PlayerInput,
            response.NpcResponse,
            inputEmbedding,
            response.ContextEmbedding,
            response.ImportanceScore);
        
        return response;
    }
}

4. Python マイクロサービス設計

4.1 感情分析サービス

4.1.1 機能

  • テキストからの感情状態抽出
  • 8次元感情ベクトル生成(喜び、悲しみ、怒り、恐れ、驚き、嫌悪、信頼、期待)
  • NPCの性格に基づく感情応答スコアリング

4.1.2 実装

# FastAPIを使用した感情分析サービス
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import pipeline
import numpy as np

app = FastAPI()

# 感情分析モデルの初期化
emotion_analyzer = pipeline("text-classification", 
                          model="j-hartmann/emotion-english-distilroberta-base", 
                          top_k=8)

class TextRequest(BaseModel):
    text: str
    npc_personality: str = ""

class EmotionResponse(BaseModel):
    emotions: dict
    dominant_emotion: str
    intensity: float
    response_modifier: dict

@app.post("/analyze_emotion", response_model=EmotionResponse)
async def analyze_emotion(request: TextRequest):
    try:
        # 感情分析の実行
        emotion_results = emotion_analyzer(request.text)
        
        # 結果の整形
        emotions = {item["label"]: item["score"] for item in emotion_results[0]}
        dominant_emotion = max(emotions.items(), key=lambda x: x[1])[0]
        intensity = emotions[dominant_emotion]
        
        # NPC性格に基づく応答修飾子の計算
        response_modifier = calculate_response_modifier(emotions, request.npc_personality)
        
        return EmotionResponse(
            emotions=emotions,
            dominant_emotion=dominant_emotion,
            intensity=intensity,
            response_modifier=response_modifier
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

def calculate_response_modifier(emotions, personality):
    # NPCの性格に基づく応答修飾子を計算するロジック
    # 実際の実装ではより複雑な性格マッピングを行う
    modifiers = {}
    
    # 例: 喜びが高い場合、フレンドリーなNPCはより親しみやすく応答
    if "joy" in emotions and emotions["joy"] > 0.5:
        if "friendly" in personality.lower():
            modifiers["friendliness"] = min(1.0, emotions["joy"] * 1.5)
        elif "stern" in personality.lower():
            modifiers["friendliness"] = emotions["joy"] * 0.5
    
    # その他の感情と性格の組み合わせに基づく修飾子
    
    return modifiers

4.2 重要度スコアリングサービス

4.2.1 機能

  • 会話内容の重要度評価
  • 長期記憶価値の予測
  • 記憶圧縮・要約の優先順位決定

4.2.2 実装

# 重要度スコアリングサービス
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sentence_transformers import SentenceTransformer

app = FastAPI()

# モデルの初期化
sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
tfidf = TfidfVectorizer(max_features=1000)

class ImportanceRequest(BaseModel):
    current_text: str
    previous_conversations: list
    npc_id: int
    player_id: int

class ImportanceResponse(BaseModel):
    importance_score: float
    novelty_score: float
    emotional_impact: float
    information_density: float
    narrative_relevance: float

@app.post("/calculate_importance", response_model=ImportanceResponse)
async def calculate_importance(request: ImportanceRequest):
    try:
        # 現在のテキストの埋め込み
        current_embedding = sentence_model.encode(request.current_text)
        
        # 情報密度の計算(TF-IDFベース)
        all_texts = [request.current_text] + [conv for conv in request.previous_conversations]
        tfidf_matrix = tfidf.fit_transform(all_texts)
        information_density = np.sum(tfidf_matrix[0].toarray()) / len(request.current_text.split())
        
        # 新規性スコアの計算
        if request.previous_conversations:
            previous_embeddings = sentence_model.encode(request.previous_conversations)
            similarities = np.dot(previous_embeddings, current_embedding) / (
                np.linalg.norm(previous_embeddings, axis=1) * np.linalg.norm(current_embedding)
            )
            novelty_score = 1.0 - np.max(similarities)
        else:
            novelty_score = 1.0
            
        # 感情的インパクトの計算(別サービスを呼び出すか、簡易版を実装)
        emotional_impact = calculate_emotional_impact(request.current_text)
        
        # ナラティブ関連性(実際はゲーム状態やクエスト情報も考慮)
        narrative_relevance = 0.7  # 仮の値
        
        # 最終的な重要度スコア
        importance_score = (
            0.25 * novelty_score + 
            0.25 * emotional_impact + 
            0.25 * information_density + 
            0.25 * narrative_relevance
        )
        
        return ImportanceResponse(
            importance_score=importance_score,
            novelty_score=novelty_score,
            emotional_impact=emotional_impact,
            information_density=float(information_density),
            narrative_relevance=narrative_relevance
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

def calculate_emotional_impact(text):
    # 簡易版の感情インパクト計算
    # 実際の実装では感情分析サービスと連携
    emotion_words = {
        "happy": 0.7, "sad": 0.8, "angry": 0.9, "surprised": 0.8,
        "love": 0.9, "hate": 0.9, "fear": 0.8, "excited": 0.7
    }
    
    words = text.lower().split()
    impact = 0.0
    matches = 0
    
    for word in words:
        if word in emotion_words:
            impact += emotion_words[word]
            matches += 1
    
    return impact / max(1, matches) if matches > 0 else 0.5

5. 会話処理フロー

5.1 基本会話フロー

  1. プレイヤーがUnityクライアントで入力
  2. Unity → .NET APIへJSON-RPCリクエスト送信
  3. .NET APIがOpenAI Embeddingで入力をベクトル化
  4. PostgreSQLで類似会話記憶の検索
  5. 必要に応じてPython感情分析サービス呼び出し
  6. プロンプト生成とOpenAI APIでの応答生成
  7. 必要に応じてPython重要度スコアリングサービス呼び出し
  8. 会話記録とメモリ更新
  9. クライアントへの応答返送

5.2 シーケンス図

Player      Unity      .NET API    PostgreSQL   Python       OpenAI
  |           |            |            |           |            |
  |--Input--->|            |            |           |            |
  |           |--Request-->|            |           |            |
  |           |            |--Vector--->|           |            |
  |           |            |<--Mem------|           |            |
  |           |            |----------------------->|            |
  |           |            |<-----------------------|            |
  |           |            |----------------------------->|      |
  |           |            |<-----------------------------|      |
  |           |            |----------------------->|            |
  |           |            |<-----------------------|            |
  |           |            |--Save----->|           |            |
  |           |<--Resp-----|            |           |            |
  |<--Display-|            |            |           |            |

6. プロンプト設計

6.1 基本プロンプトテンプレート

あなたは「{npc_name}」というノベルゲーム内のNPCです。

【性格】
{personality}

【役割】
{role}

【プレイヤーとの関係】
{relation_status}、親密度: {familiarity}/10

【感情状態】
{emotion_state}

【過去の関連する記憶】
{relevant_memories}

プレイヤー「{player_name}」からの入力: 
{player_input}

以下の制約に従って応答してください:
1. {npc_name}の性格に一致する口調で話してください
2. 150文字以内で簡潔に答えてください
3. 感情状態を反映した応答をしてください
4. ゲーム内の設定に沿った内容だけを話してください

6.2 感情反映プロンプト拡張

【感情修飾子】
- 親密さレベル: {friendliness}
- 怒りレベル: {anger}
- 悲しみレベル: {sadness}
- 興奮レベル: {excitement}

これらの感情状態を考慮して、適切に反応してください。
例えば、親密さが高い場合はより友好的に、怒りが高い場合はよりぶっきらぼうに応答するなど。

7. メモリ管理メカニズム

7.1 記憶分類

  • アクティブメモリ: 直近14日間の会話(完全保持)
  • 短期メモリ: 15-60日間の会話(重要度に基づき選別)
  • 長期メモリ: 61日以上前の会話(要約と重要記憶のみ保持)

7.2 記憶圧縮プロセス

  1. 日次バッチジョブで記憶分類を更新
  2. 短期→長期への移行時:
    • 重要度スコアリングで重要会話を特定
    • 関連会話をグループ化し要約を生成
    • 要約とオリジナル会話のマッピングを保存

7.3 記憶の想起アルゴリズム

public async Task<List<ConversationMemory>> RecallMemories(int playerId, int npcId, Vector inputEmbedding, int limit)
{
    // 1. アクティブメモリから直近の会話を取得(時系列)
    var recentMemories = await _dbContext.ConversationHistory
        .Where(c => c.PlayerId == playerId && c.NpcId == npcId)
        .OrderByDescending(c => c.ConversationDatetime)
        .Take(3)
        .ToListAsync();
    
    // 2. ベクトル類似度による関連記憶の検索
    var similarMemories = await _dbContext.ConversationHistory
        .Where(c => c.PlayerId == playerId && c.NpcId == npcId)
        .OrderByDescending(m => m.InputEmbedding.CosineSimilarity(inputEmbedding))
        .Take(limit - recentMemories.Count)
        .ToListAsync();
    
    // 3. 重要度スコアによる調整
    var adjustedMemories = similarMemories
        .OrderByDescending(m => m.ImportanceScore * 0.7 + 
                               m.InputEmbedding.CosineSimilarity(inputEmbedding) * 0.3)
        .Take(limit - recentMemories.Count)
        .ToList();
    
    // 4. アーカイブからの要約記憶検索
    var summarizedMemories = await _dbContext.MemoryArchive
        .Where(m => m.PlayerId == playerId && m.NpcId == npcId)
        .OrderByDescending(m => m.SummaryEmbedding.CosineSimilarity(inputEmbedding))
        .Take(1)
        .ToListAsync();
    
    // 5. 結果の結合と重複排除
    var result = recentMemories
        .Concat(adjustedMemories)
        .Concat(summarizedMemories.Select(s => new ConversationMemory { 
            Content = $"[要約記憶: {s.StartDate} - {s.EndDate}] {s.Summary}" 
        }))
        .DistinctBy(m => m.ConversationId)
        .ToList();
    
    return result;
}

8. パフォーマンスと拡張性

8.1 パフォーマンス目標

  • 平均応答時間: 1.5秒以内
  • 同時接続ユーザー: 1,000名
  • NPCあたりの記憶容量: 最大10,000会話

8.2 スケーリング戦略

  • .NETバックエンドのKubernetes水平スケーリング
  • Pythonマイクロサービスの独立スケーリング
  • PostgreSQLのリードレプリカ活用

8.3 キャッシング戦略

  • よく使われるNPC情報のインメモリキャッシュ
  • 頻繁に呼び出される感情分析結果のキャッシュ
  • 重要度スコアの計算結果キャッシュ

9. セキュリティと運用

9.1 認証と認可

  • JWTによるプレイヤー認証
  • ロールベースの権限管理

9.2 監視体制

  • Prometheusによる指標収集
  • Grafanaによる可視化ダッシュボード
  • ELKスタックによるログ分析

9.3 障害対応

  • サーキットブレーカーパターンの実装
  • フォールバックダイアログの準備
  • 自動リトライ戦略

10. 実装ロードマップ

10.1 第1フェーズ(3ヶ月)

  • .NETコアシステム構築
  • 基本的なOpenAI連携
  • 記憶管理の基礎実装
  • Unityクライアント連携

10.2 第2フェーズ(1.5ヶ月)

  • Python感情分析サービスの実装と統合
  • 感情ベースの応答調整機能
  • NPCキャラクターの個性強化

10.3 第3フェーズ(1.5ヶ月)

  • 重要度スコアリングサービスの実装
  • 記憶圧縮と要約の高度化
  • パフォーマンス最適化

11. 運用コスト予測

11.1 開発コスト

  • バックエンド開発者(.NET): 2名 × 6ヶ月
  • AI/Python開発者: 1名 × 3ヶ月
  • フロントエンド開発者(Unity): 1名 × 4ヶ月
  • テスター: 1名 × 2ヶ月

11.2 インフラコスト(月額)

  • サーバーホスティング: $500-1,000
  • データベース: $200-400
  • OpenAI API: $1,000-2,000(プレイヤー数に依存)

11.3 保守コスト(月額)

  • 運用保守エンジニア: 1名($3,000-5,000)
  • モニタリングサービス: $100-200
  • セキュリティ監査: $300-500

付録: 推奨Pythonライブラリ

目的 ライブラリ
APIフレームワーク FastAPI
感情分析 Transformers, spaCy
ベクトル処理 NumPy, SciPy
テキスト埋め込み sentence-transformers
自然言語処理 NLTK, spaCy
HTTP通信 httpx, aiohttp

Discussion