Skip to main content
Glama

UnrealBlueprintMCP

by BestDev
AI_DEVELOPMENT_PATTERNS.mdโ€ข108 kB
# AI Development Patterns for UnrealBlueprintMCP > **Production-Ready AI Integration Guide** > > **Version**: 1.0.0 > **Target Audience**: AI Developers, Game Studios, Automation Engineers > **Last Updated**: September 2025 --- ## ๐Ÿ“‹ ๋ชฉ์ฐจ 1. [๊ฐœ์š”](#๊ฐœ์š”) 2. [AI ๋ชจ๋ธ๋ณ„ ํ†ตํ•ฉ ํŒจํ„ด](#ai-๋ชจ๋ธ๋ณ„-ํ†ตํ•ฉ-ํŒจํ„ด) 3. [LangChain ํ†ตํ•ฉ ํŒจํ„ด](#langchain-ํ†ตํ•ฉ-ํŒจํ„ด) 4. [์ž์—ฐ์–ด ๋ช…๋ น์–ด ์›Œํฌํ”Œ๋กœ์šฐ](#์ž์—ฐ์–ด-๋ช…๋ น์–ด-์›Œํฌํ”Œ๋กœ์šฐ) 5. [์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ํŒจํ„ด](#์—๋Ÿฌ-์ฒ˜๋ฆฌ-๋ฐ-์žฌ์‹œ๋„-ํŒจํ„ด) 6. [์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒจํ„ด](#์„ฑ๋Šฅ-์ตœ์ ํ™”-ํŒจํ„ด) 7. [๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํŒจํ„ด](#๋ฐฐ์น˜-์ฒ˜๋ฆฌ-ํŒจํ„ด) 8. [์‹ค์ œ ๊ตฌํ˜„ ์˜ˆ์ œ](#์‹ค์ œ-๊ตฌํ˜„-์˜ˆ์ œ) 9. [Best Practices](#best-practices) --- ## ๐Ÿค– ๊ฐœ์š” UnrealBlueprintMCP๋Š” ๋‹ค์–‘ํ•œ AI ๋ชจ๋ธ๊ณผ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ Unreal Engine Blueprint๋ฅผ ์ž์—ฐ์–ด๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ๋œ AI ํ†ตํ•ฉ ํŒจํ„ด์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ### ์ง€์›ํ•˜๋Š” AI ์›Œํฌํ”Œ๋กœ์šฐ ```mermaid graph TD A[์ž์—ฐ์–ด ๋ช…๋ น] --> B[AI ๋ชจ๋ธ ์ฒ˜๋ฆฌ] B --> C[MCP ํ”„๋กœํ† ์ฝœ ๋ณ€ํ™˜] C --> D[UnrealBlueprintMCP] D --> E[Unreal Engine] E --> F[Blueprint ์ƒ์„ฑ/์ˆ˜์ •] F --> G[์‹คํ–‰ ๊ฒฐ๊ณผ ํ”ผ๋“œ๋ฐฑ] G --> A ``` ### ํ•ต์‹ฌ ์„ค๊ณ„ ์›์น™ - **๐Ÿ”„ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: ๋ชจ๋“  AI ์ž‘์—…์€ ๋น„๋™๊ธฐ๋กœ ์ˆ˜ํ–‰ - **๐Ÿ›ก๏ธ ์—๋Ÿฌ ๋ณต๊ตฌ**: ์ž๋™ ์žฌ์‹œ๋„ ๋ฐ ์šฐ์•„ํ•œ ์‹คํŒจ ์ฒ˜๋ฆฌ - **โšก ๋ฐฐ์น˜ ์ตœ์ ํ™”**: ๋Œ€๋Ÿ‰ ์ž‘์—…์„ ์œ„ํ•œ ํšจ์œจ์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ - **๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง**: ์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ ์ถ”์  ๋ฐ ๋กœ๊น… - **๐ŸŽฏ ํƒ€์ž… ์•ˆ์ „์„ฑ**: ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ๊ฒ€์ฆ ๋ฐ ๊ฒ€์ฆ ํŒŒ์ดํ”„๋ผ์ธ --- ## ๐Ÿง  AI ๋ชจ๋ธ๋ณ„ ํ†ตํ•ฉ ํŒจํ„ด ### 1. Claude Code ํ†ตํ•ฉ Claude Code๋Š” MCP ํ”„๋กœํ† ์ฝœ์„ ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›ํ•˜๋ฏ€๋กœ ๊ฐ€์žฅ ์ง์ ‘์ ์ธ ํ†ตํ•ฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. #### ์„ค์ • ๋ฐฉ๋ฒ• ```json // ~/.config/claude-code/mcp.json { "servers": { "unreal_blueprint": { "command": "fastmcp", "args": ["run", "/absolute/path/to/unreal_blueprint_mcp_server.py"], "env": { "PATH": "/path/to/mcp_server_env/bin:$PATH", "UNREAL_ENGINE_PATH": "/path/to/UnrealEngine", "MCP_LOG_LEVEL": "INFO" } } } } ``` #### ์ž์—ฐ์–ด ๋ช…๋ น ์˜ˆ์‹œ ```python # Claude Code์—์„œ ์ง์ ‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ช…๋ น์–ด๋“ค commands = [ "Create an Actor blueprint named 'PlayerController' in the Controllers folder", "Make a Character blueprint called 'PlayerCharacter' with health set to 100", "Create a UserWidget blueprint for the main menu interface", "Set the PlayerCharacter's movement speed to 600 units", "Add a StaticMeshComponent to the PlayerController blueprint" ] # Claude๊ฐ€ ์ž๋™์œผ๋กœ ์ ์ ˆํ•œ MCP ๋„๊ตฌ๋ฅผ ์„ ํƒํ•˜์—ฌ ์‹คํ–‰ ``` #### Claude ์ „์šฉ ํ—ฌํผ ํด๋ž˜์Šค ```python class ClaudeUnrealHelper: """Claude Code ์ „์šฉ Unreal ์ž‘์—… ๋„์šฐ๋ฏธ""" def __init__(self): self.command_history = [] self.error_context = {} def parse_natural_command(self, command: str) -> dict: """์ž์—ฐ์–ด ๋ช…๋ น์„ MCP ๋„๊ตฌ ํ˜ธ์ถœ๋กœ ๋ณ€ํ™˜""" patterns = { r"create.*blueprint.*named?.*'([^']+)'.*parent.*'([^']+)'": "create_blueprint", r"set.*'([^']+)'.*property.*'([^']+)'.*to.*'([^']+)'": "set_blueprint_property", r"make.*'([^']+)'.*blueprint.*type.*'([^']+)'": "create_blueprint" } for pattern, tool in patterns.items(): if match := re.match(pattern, command.lower()): return self._build_tool_call(tool, match.groups()) return {"error": "๋ช…๋ น์„ ์ดํ•ดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", "suggestion": "๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”"} def add_error_context(self, command: str, error: str): """์—๋Ÿฌ ์ปจํ…์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ Claude๊ฐ€ ํ•™์Šตํ•˜๋„๋ก ๋„์›€""" self.error_context[command] = error ``` ### 2. GPT-4 + OpenAI API ํ†ตํ•ฉ OpenAI API๋ฅผ ์‚ฌ์šฉํ•œ GPT-4 ํ†ตํ•ฉ์€ ์ปค์Šคํ…€ ๋ž˜ํผ๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. #### GPT-4 ๋„๊ตฌ ๋ž˜ํผ ```python import openai import asyncio import json from typing import List, Dict, Any class GPT4UnrealAdapter: """GPT-4๋ฅผ ์œ„ํ•œ UnrealBlueprintMCP ์–ด๋Œ‘ํ„ฐ""" def __init__(self, api_key: str, mcp_server_url: str = "ws://localhost:6277"): self.client = openai.AsyncOpenAI(api_key=api_key) self.mcp_server_url = mcp_server_url self.available_tools = self._load_mcp_tools() def _load_mcp_tools(self) -> List[Dict]: """MCP ๋„๊ตฌ๋ฅผ OpenAI function calling ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜""" return [ { "type": "function", "function": { "name": "create_blueprint", "description": "Create a new Unreal Engine blueprint asset", "parameters": { "type": "object", "properties": { "blueprint_name": { "type": "string", "description": "Name of the blueprint to create" }, "parent_class": { "type": "string", "description": "Parent class for the blueprint", "enum": ["Actor", "Pawn", "Character", "ActorComponent", "UserWidget"] }, "asset_path": { "type": "string", "description": "Asset path where blueprint will be created" } }, "required": ["blueprint_name"] } } }, { "type": "function", "function": { "name": "set_blueprint_property", "description": "Set a property value in a blueprint's CDO", "parameters": { "type": "object", "properties": { "blueprint_path": { "type": "string", "description": "Full path to the blueprint asset" }, "property_name": { "type": "string", "description": "Name of the property to modify" }, "property_value": { "type": "string", "description": "New value for the property" } }, "required": ["blueprint_path", "property_name", "property_value"] } } } ] async def process_natural_command(self, user_input: str) -> Dict[str, Any]: """์ž์—ฐ์–ด ๋ช…๋ น์„ ์ฒ˜๋ฆฌํ•˜๊ณ  Unreal์—์„œ ์‹คํ–‰""" try: # GPT-4์— ๋ช…๋ น ํ•ด์„ ์š”์ฒญ response = await self.client.chat.completions.create( model="gpt-4", messages=[ { "role": "system", "content": """You are an expert Unreal Engine Blueprint assistant. Convert natural language commands into appropriate function calls. Always use specific, clear values and follow Unreal Engine naming conventions.""" }, { "role": "user", "content": user_input } ], tools=self.available_tools, tool_choice="auto" ) # ๋„๊ตฌ ํ˜ธ์ถœ ์‹คํ–‰ if response.choices[0].message.tool_calls: results = [] for tool_call in response.choices[0].message.tool_calls: result = await self._execute_mcp_tool( tool_call.function.name, json.loads(tool_call.function.arguments) ) results.append(result) return { "success": True, "commands_executed": len(results), "results": results, "gpt_reasoning": response.choices[0].message.content } else: return { "success": False, "error": "No actionable commands found", "gpt_response": response.choices[0].message.content } except Exception as e: return { "success": False, "error": str(e), "error_type": type(e).__name__ } async def _execute_mcp_tool(self, tool_name: str, arguments: Dict) -> Dict: """MCP ๋„๊ตฌ๋ฅผ ์‹ค์ œ๋กœ ์‹คํ–‰""" import websockets request = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": tool_name, "arguments": arguments } } async with websockets.connect(self.mcp_server_url) as ws: await ws.send(json.dumps(request)) response = await ws.recv() return json.loads(response) ``` #### GPT-4 ์‚ฌ์šฉ ์˜ˆ์‹œ ```python async def main(): adapter = GPT4UnrealAdapter(api_key="your-openai-api-key") # ์ž์—ฐ์–ด ๋ช…๋ น ์ฒ˜๋ฆฌ commands = [ "Create a player character blueprint with health 100 and speed 600", "Make an enemy AI blueprint that inherits from Pawn", "Create a weapon pickup actor blueprint" ] for command in commands: result = await adapter.process_natural_command(command) print(f"Command: {command}") print(f"Result: {result}\n") asyncio.run(main()) ``` ### 3. Google Gemini ํ†ตํ•ฉ Gemini๋Š” Google AI Studio๋ฅผ ํ†ตํ•ด ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค. #### Gemini ์–ด๋Œ‘ํ„ฐ ๊ตฌํ˜„ ```python import google.generativeai as genai from typing import Dict, Any, List class GeminiUnrealAdapter: """Google Gemini๋ฅผ ์œ„ํ•œ UnrealBlueprintMCP ์–ด๋Œ‘ํ„ฐ""" def __init__(self, api_key: str): genai.configure(api_key=api_key) self.model = genai.GenerativeModel('gemini-pro') self.conversation_context = [] def _create_unreal_prompt(self, user_command: str) -> str: """Unreal Engine ํŠนํ™” ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ""" system_prompt = """ You are an expert Unreal Engine developer. Convert natural language commands into specific JSON-RPC calls for the UnrealBlueprintMCP system. Available tools: 1. create_blueprint(blueprint_name, parent_class, asset_path) 2. set_blueprint_property(blueprint_path, property_name, property_value) 3. get_server_status() 4. list_supported_blueprint_classes() 5. create_test_actor_blueprint(blueprint_name, location) 6. test_unreal_connection() Return ONLY a JSON object with the tool call, like: { "tool": "create_blueprint", "arguments": { "blueprint_name": "PlayerCharacter", "parent_class": "Character", "asset_path": "/Game/Blueprints/" } } """ return f"{system_prompt}\n\nUser command: {user_command}" async def process_command(self, command: str) -> Dict[str, Any]: """Gemini๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ช…๋ น ์ฒ˜๋ฆฌ""" try: prompt = self._create_unreal_prompt(command) response = self.model.generate_content(prompt) # JSON ์‘๋‹ต ํŒŒ์‹ฑ response_text = response.text.strip() if response_text.startswith("```json"): response_text = response_text[7:-3].strip() elif response_text.startswith("```"): response_text = response_text[3:-3].strip() tool_call = json.loads(response_text) # MCP ๋„๊ตฌ ์‹คํ–‰ result = await self._execute_mcp_tool( tool_call["tool"], tool_call["arguments"] ) # ์ปจํ…์ŠคํŠธ ์ €์žฅ self.conversation_context.append({ "command": command, "tool_call": tool_call, "result": result }) return { "success": True, "gemini_reasoning": response.text, "tool_executed": tool_call["tool"], "mcp_result": result } except Exception as e: return { "success": False, "error": str(e), "command": command } async def _execute_mcp_tool(self, tool_name: str, arguments: Dict) -> Dict: """MCP ๋„๊ตฌ ์‹คํ–‰ (GPT-4 ์–ด๋Œ‘ํ„ฐ์™€ ๋™์ผํ•œ ๊ตฌํ˜„)""" # ... (์ด์ „ ๊ตฌํ˜„๊ณผ ๋™์ผ) pass ``` ### 4. ๋กœ์ปฌ LLM (Ollama) ํ†ตํ•ฉ Ollama๋ฅผ ์‚ฌ์šฉํ•œ ๋กœ์ปฌ LLM ํ†ตํ•ฉ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. #### Ollama ์–ด๋Œ‘ํ„ฐ ```python import requests import json from typing import Dict, Any class OllamaUnrealAdapter: """Ollama ๋กœ์ปฌ LLM์„ ์œ„ํ•œ ์–ด๋Œ‘ํ„ฐ""" def __init__(self, model_name: str = "llama2", ollama_host: str = "http://localhost:11434"): self.model_name = model_name self.ollama_host = ollama_host self.conversation_history = [] def _create_few_shot_prompt(self, command: str) -> str: """Few-shot learning์„ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ""" examples = [ { "input": "Create a player character blueprint", "output": { "tool": "create_blueprint", "arguments": { "blueprint_name": "PlayerCharacter", "parent_class": "Character", "asset_path": "/Game/Blueprints/" } } }, { "input": "Set health to 100 for PlayerCharacter", "output": { "tool": "set_blueprint_property", "arguments": { "blueprint_path": "/Game/Blueprints/PlayerCharacter", "property_name": "Health", "property_value": "100" } } } ] prompt = "You are an Unreal Engine expert. Convert commands to JSON tool calls.\n\n" for example in examples: prompt += f"Input: {example['input']}\n" prompt += f"Output: {json.dumps(example['output'])}\n\n" prompt += f"Input: {command}\nOutput: " return prompt async def process_command(self, command: str) -> Dict[str, Any]: """Ollama๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ช…๋ น ์ฒ˜๋ฆฌ""" try: prompt = self._create_few_shot_prompt(command) response = requests.post( f"{self.ollama_host}/api/generate", json={ "model": self.model_name, "prompt": prompt, "stream": False } ) if response.status_code == 200: ollama_response = response.json() generated_text = ollama_response["response"].strip() # JSON ํŒŒ์‹ฑ ์‹œ๋„ try: tool_call = json.loads(generated_text) # MCP ๋„๊ตฌ ์‹คํ–‰ result = await self._execute_mcp_tool( tool_call["tool"], tool_call["arguments"] ) return { "success": True, "llm_response": generated_text, "tool_executed": tool_call["tool"], "mcp_result": result } except json.JSONDecodeError: return { "success": False, "error": "Failed to parse LLM response as JSON", "llm_response": generated_text } else: return { "success": False, "error": f"Ollama API error: {response.status_code}" } except Exception as e: return { "success": False, "error": str(e) } ``` --- ## ๐Ÿ”— LangChain ํ†ตํ•ฉ ํŒจํ„ด LangChain์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๋ณต์žกํ•œ AI ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ### LangChain ๋„๊ตฌ ๋ž˜ํผ ```python from langchain.tools import BaseTool from langchain.agents import AgentExecutor, create_openai_functions_agent from langchain.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from typing import Type, Optional from pydantic import BaseModel, Field import asyncio import websockets import json class CreateBlueprintInput(BaseModel): """create_blueprint ๋„๊ตฌ์˜ ์ž…๋ ฅ ์Šคํ‚ค๋งˆ""" blueprint_name: str = Field(description="๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ด๋ฆ„") parent_class: str = Field(default="Actor", description="๋ถ€๋ชจ ํด๋ž˜์Šค") asset_path: str = Field(default="/Game/Blueprints/", description="์—์…‹ ๊ฒฝ๋กœ") class CreateBlueprintTool(BaseTool): """LangChain์šฉ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ๋„๊ตฌ""" name = "create_blueprint" description = "Unreal Engine์—์„œ ์ƒˆ๋กœ์šด ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค" args_schema: Type[BaseModel] = CreateBlueprintInput def __init__(self, mcp_server_url: str = "ws://localhost:6277"): super().__init__() self.mcp_server_url = mcp_server_url def _run(self, blueprint_name: str, parent_class: str = "Actor", asset_path: str = "/Game/Blueprints/") -> str: """๋™๊ธฐ ์‹คํ–‰ (LangChain ํ˜ธํ™˜์„ฑ)""" return asyncio.run(self._arun(blueprint_name, parent_class, asset_path)) async def _arun(self, blueprint_name: str, parent_class: str = "Actor", asset_path: str = "/Game/Blueprints/") -> str: """๋น„๋™๊ธฐ ์‹คํ–‰""" try: request = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_blueprint", "arguments": { "blueprint_name": blueprint_name, "parent_class": parent_class, "asset_path": asset_path } } } async with websockets.connect(self.mcp_server_url) as ws: await ws.send(json.dumps(request)) response = await ws.recv() result = json.loads(response) if "error" in result: return f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {result['error']}" else: return f"๋ธ”๋ฃจํ”„๋ฆฐํŠธ '{blueprint_name}' ์ƒ์„ฑ ์™„๋ฃŒ: {result}" except Exception as e: return f"์—ฐ๊ฒฐ ์˜ค๋ฅ˜: {str(e)}" class SetBlueprintPropertyInput(BaseModel): """set_blueprint_property ๋„๊ตฌ์˜ ์ž…๋ ฅ ์Šคํ‚ค๋งˆ""" blueprint_path: str = Field(description="๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๊ฒฝ๋กœ") property_name: str = Field(description="์†์„ฑ ์ด๋ฆ„") property_value: str = Field(description="์†์„ฑ ๊ฐ’") class SetBlueprintPropertyTool(BaseTool): """LangChain์šฉ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์†์„ฑ ์„ค์ • ๋„๊ตฌ""" name = "set_blueprint_property" description = "๋ธ”๋ฃจํ”„๋ฆฐํŠธ์˜ ์†์„ฑ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค" args_schema: Type[BaseModel] = SetBlueprintPropertyInput def __init__(self, mcp_server_url: str = "ws://localhost:6277"): super().__init__() self.mcp_server_url = mcp_server_url def _run(self, blueprint_path: str, property_name: str, property_value: str) -> str: return asyncio.run(self._arun(blueprint_path, property_name, property_value)) async def _arun(self, blueprint_path: str, property_name: str, property_value: str) -> str: try: request = { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "set_blueprint_property", "arguments": { "blueprint_path": blueprint_path, "property_name": property_name, "property_value": property_value } } } async with websockets.connect(self.mcp_server_url) as ws: await ws.send(json.dumps(request)) response = await ws.recv() result = json.loads(response) if "error" in result: return f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {result['error']}" else: return f"์†์„ฑ '{property_name}' ์„ค์ • ์™„๋ฃŒ: {result}" except Exception as e: return f"์—ฐ๊ฒฐ ์˜ค๋ฅ˜: {str(e)}" class UnrealBlueprintAgent: """LangChain ๊ธฐ๋ฐ˜ Unreal Blueprint ์—์ด์ „ํŠธ""" def __init__(self, openai_api_key: str, mcp_server_url: str = "ws://localhost:6277"): self.llm = ChatOpenAI(api_key=openai_api_key, model="gpt-4", temperature=0) self.tools = [ CreateBlueprintTool(mcp_server_url), SetBlueprintPropertyTool(mcp_server_url) ] # ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ ์„ค์ • self.prompt = ChatPromptTemplate.from_messages([ ("system", """๋‹น์‹ ์€ Unreal Engine Blueprint ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ์ž์—ฐ์–ด ๋ช…๋ น์„ ๋ถ„์„ํ•˜๊ณ  ์ ์ ˆํ•œ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ˆ˜์ •ํ•˜์„ธ์š”. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ: - create_blueprint: ์ƒˆ๋กœ์šด ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ - set_blueprint_property: ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์†์„ฑ ์„ค์ • ํ•ญ์ƒ Unreal Engine์˜ ๋ช…๋ช… ๊ทœ์น™์„ ๋”ฐ๋ฅด๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•œ ๋ชจ๋“  ์ž‘์—…์„ ์™„๋ฃŒํ•˜์„ธ์š”."""), ("human", "{input}"), ("placeholder", "{agent_scratchpad}") ]) # ์—์ด์ „ํŠธ ์ƒ์„ฑ self.agent = create_openai_functions_agent(self.llm, self.tools, self.prompt) self.agent_executor = AgentExecutor(agent=self.agent, tools=self.tools, verbose=True) def process_command(self, command: str) -> str: """์ž์—ฐ์–ด ๋ช…๋ น ์ฒ˜๋ฆฌ""" return self.agent_executor.invoke({"input": command}) ``` ### LangChain ์ฒด์ธ ์˜ˆ์‹œ ```python from langchain.chains import LLMChain from langchain.prompts import PromptTemplate class UnrealBlueprintChain: """๋ณต์žกํ•œ Unreal ์ž‘์—…์„ ์œ„ํ•œ LangChain ์ฒด์ธ""" def __init__(self, openai_api_key: str): self.llm = ChatOpenAI(api_key=openai_api_key, model="gpt-4") self.agent = UnrealBlueprintAgent(openai_api_key) # ๊ณ„ํš ์ˆ˜๋ฆฝ ์ฒด์ธ self.planning_prompt = PromptTemplate( input_variables=["task"], template=""" ๋‹ค์Œ Unreal Engine ์ž‘์—…์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋ถ„ํ•ดํ•˜์„ธ์š”: ์ž‘์—…: {task} ๊ฐ ๋‹จ๊ณ„๋Š” ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: 1. create_blueprint - ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ 2. set_blueprint_property - ์†์„ฑ ์„ค์ • 3. test_connection - ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„๋ณ„ ๊ณ„ํš: """ ) self.planning_chain = LLMChain(llm=self.llm, prompt=self.planning_prompt) async def execute_complex_task(self, task_description: str) -> Dict[str, Any]: """๋ณต์žกํ•œ ์ž‘์—…์„ ๋‹จ๊ณ„๋ณ„๋กœ ์‹คํ–‰""" try: # 1. ์ž‘์—… ๊ณ„ํš ์ˆ˜๋ฆฝ plan = self.planning_chain.run(task=task_description) # 2. ๊ณ„ํš์„ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋‹จ๊ณ„๋กœ ํŒŒ์‹ฑ steps = self._parse_plan(plan) # 3. ๊ฐ ๋‹จ๊ณ„ ์‹คํ–‰ results = [] for step in steps: result = self.agent.process_command(step) results.append({ "step": step, "result": result }) return { "success": True, "task": task_description, "plan": plan, "steps_executed": len(results), "results": results } except Exception as e: return { "success": False, "error": str(e), "task": task_description } def _parse_plan(self, plan: str) -> List[str]: """๊ณ„ํš์„ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋‹จ๊ณ„๋กœ ํŒŒ์‹ฑ""" lines = plan.strip().split('\n') steps = [] for line in lines: line = line.strip() if line and not line.startswith('#') and len(line) > 10: # ๋ฒˆํ˜ธ๋‚˜ ๋ถˆ๋ › ์ œ๊ฑฐ if '. ' in line: line = line.split('. ', 1)[1] elif '- ' in line: line = line.replace('- ', '') steps.append(line) return steps ``` ### LangChain ์‚ฌ์šฉ ์˜ˆ์‹œ ```python async def main(): # LangChain ์—์ด์ „ํŠธ ์ดˆ๊ธฐํ™” agent = UnrealBlueprintAgent(openai_api_key="your-api-key") chain = UnrealBlueprintChain(openai_api_key="your-api-key") # ๋‹จ์ˆœ ๋ช…๋ น ์‹คํ–‰ simple_result = agent.process_command( "Create a player character blueprint with health 100" ) print("๋‹จ์ˆœ ๋ช…๋ น ๊ฒฐ๊ณผ:", simple_result) # ๋ณต์žกํ•œ ์ž‘์—… ์‹คํ–‰ complex_task = """ Create a complete player system: 1. Player character blueprint with health and mana 2. Player controller blueprint 3. Game mode blueprint 4. Set appropriate default values for all properties """ complex_result = await chain.execute_complex_task(complex_task) print("๋ณต์žกํ•œ ์ž‘์—… ๊ฒฐ๊ณผ:", complex_result) asyncio.run(main()) ``` --- ## ๐Ÿ—ฃ๏ธ ์ž์—ฐ์–ด ๋ช…๋ น์–ด ์›Œํฌํ”Œ๋กœ์šฐ ์ž์—ฐ์–ด๋ฅผ Unreal Blueprint ์ž‘์—…์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํŒจํ„ด์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ### ๋ช…๋ น์–ด ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ```python import re from enum import Enum from typing import Dict, List, Optional, Tuple class CommandType(Enum): CREATE_BLUEPRINT = "create_blueprint" MODIFY_PROPERTY = "modify_property" ADD_COMPONENT = "add_component" SET_LOCATION = "set_location" BATCH_OPERATION = "batch_operation" QUERY_STATUS = "query_status" UNKNOWN = "unknown" class NaturalLanguageProcessor: """์ž์—ฐ์–ด ๋ช…๋ น์„ UnrealBlueprintMCP ์ž‘์—…์œผ๋กœ ๋ณ€ํ™˜""" def __init__(self): self.patterns = { CommandType.CREATE_BLUEPRINT: [ r"create.*blueprint.*(?:named?|called)\s*['\"]?([^'\"]+)['\"]?", r"make.*blueprint.*(?:named?|called)\s*['\"]?([^'\"]+)['\"]?", r"new.*blueprint.*['\"]?([^'\"]+)['\"]?", ], CommandType.MODIFY_PROPERTY: [ r"set.*['\"]?([^'\"]+)['\"]?.*property.*['\"]?([^'\"]+)['\"]?.*to.*['\"]?([^'\"]+)['\"]?", r"change.*['\"]?([^'\"]+)['\"]?.*['\"]?([^'\"]+)['\"]?.*to.*['\"]?([^'\"]+)['\"]?", r"update.*['\"]?([^'\"]+)['\"]?.*['\"]?([^'\"]+)['\"]?.*['\"]?([^'\"]+)['\"]?", ], CommandType.SET_LOCATION: [ r"set.*location.*(?:to|at)\s*([0-9\-.,\s]+)", r"move.*(?:to|at)\s*([0-9\-.,\s]+)", r"place.*(?:at|to)\s*([0-9\-.,\s]+)", ] } self.blueprint_types = { "actor": "Actor", "pawn": "Pawn", "character": "Character", "component": "ActorComponent", "widget": "UserWidget", "ui": "UserWidget", "interface": "UserWidget" } def parse_command(self, command: str) -> Dict[str, any]: """์ž์—ฐ์–ด ๋ช…๋ น์„ ํŒŒ์‹ฑํ•˜์—ฌ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜""" command_lower = command.lower().strip() # ๋ช…๋ น ํƒ€์ž… ๋ถ„๋ฅ˜ command_type = self._classify_command(command_lower) if command_type == CommandType.CREATE_BLUEPRINT: return self._parse_create_blueprint(command_lower, command) elif command_type == CommandType.MODIFY_PROPERTY: return self._parse_modify_property(command_lower, command) elif command_type == CommandType.SET_LOCATION: return self._parse_set_location(command_lower, command) else: return { "type": CommandType.UNKNOWN, "error": "๋ช…๋ น์„ ์ดํ•ดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", "suggestion": "๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”", "original_command": command } def _classify_command(self, command: str) -> CommandType: """๋ช…๋ น ํƒ€์ž… ๋ถ„๋ฅ˜""" for cmd_type, patterns in self.patterns.items(): for pattern in patterns: if re.search(pattern, command, re.IGNORECASE): return cmd_type return CommandType.UNKNOWN def _parse_create_blueprint(self, command_lower: str, original: str) -> Dict[str, any]: """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ๋ช…๋ น ํŒŒ์‹ฑ""" # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ด๋ฆ„ ์ถ”์ถœ name_patterns = [ r"(?:named?|called)\s*['\"]?([^'\"]+)['\"]?", r"blueprint\s*['\"]?([^'\"]+)['\"]?", ] blueprint_name = None for pattern in name_patterns: match = re.search(pattern, command_lower) if match: blueprint_name = match.group(1).strip() break # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ํƒ€์ž… ์ถ”์ถœ parent_class = "Actor" # ๊ธฐ๋ณธ๊ฐ’ for keyword, bp_type in self.blueprint_types.items(): if keyword in command_lower: parent_class = bp_type break # ํด๋”/๊ฒฝ๋กœ ์ถ”์ถœ asset_path = "/Game/Blueprints/" path_patterns = [ r"in\s+(?:the\s+)?['\"]?([^'\"]+)['\"]?\s+folder", r"(?:at|in)\s+['\"]?([^'\"]+)['\"]?", ] for pattern in path_patterns: match = re.search(pattern, command_lower) if match: folder = match.group(1).strip() if not folder.startswith("/"): asset_path = f"/Game/{folder}/" else: asset_path = folder break return { "type": CommandType.CREATE_BLUEPRINT, "action": "create_blueprint", "parameters": { "blueprint_name": blueprint_name or "NewBlueprint", "parent_class": parent_class, "asset_path": asset_path }, "confidence": 0.9 if blueprint_name else 0.6, "original_command": original } def _parse_modify_property(self, command_lower: str, original: str) -> Dict[str, any]: """์†์„ฑ ์ˆ˜์ • ๋ช…๋ น ํŒŒ์‹ฑ""" # ์ผ๋ฐ˜์ ์ธ ์†์„ฑ ์ˆ˜์ • ํŒจํ„ด patterns = [ r"set\s+(?:the\s+)?['\"]?([^'\"]+)['\"]?'?s\s+['\"]?([^'\"]+)['\"]?\s+to\s+['\"]?([^'\"]+)['\"]?", r"change\s+['\"]?([^'\"]+)['\"]?\s+['\"]?([^'\"]+)['\"]?\s+to\s+['\"]?([^'\"]+)['\"]?", ] for pattern in patterns: match = re.search(pattern, command_lower) if match: blueprint_name = match.group(1).strip() property_name = match.group(2).strip() property_value = match.group(3).strip() # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๊ฒฝ๋กœ ๊ตฌ์„ฑ blueprint_path = f"/Game/Blueprints/{blueprint_name}" return { "type": CommandType.MODIFY_PROPERTY, "action": "set_blueprint_property", "parameters": { "blueprint_path": blueprint_path, "property_name": property_name, "property_value": property_value }, "confidence": 0.8, "original_command": original } return { "type": CommandType.UNKNOWN, "error": "์†์„ฑ ์ˆ˜์ • ๋ช…๋ น์„ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", "original_command": original } def _parse_set_location(self, command_lower: str, original: str) -> Dict[str, any]: """์œ„์น˜ ์„ค์ • ๋ช…๋ น ํŒŒ์‹ฑ""" location_pattern = r"(?:location|position).*?(?:to|at)\s*([0-9\-.,\s]+)" match = re.search(location_pattern, command_lower) if match: location_str = match.group(1).strip() # ์ขŒํ‘œ ํŒŒ์‹ฑ (x,y,z ๋˜๋Š” x y z ํ˜•์‹) coords = re.findall(r'[0-9\-]+(?:\.[0-9]+)?', location_str) if len(coords) >= 2: x = coords[0] y = coords[1] if len(coords) > 1 else "0" z = coords[2] if len(coords) > 2 else "0" location_value = f"{x},{y},{z}" return { "type": CommandType.SET_LOCATION, "action": "set_blueprint_property", "parameters": { "property_name": "RootComponent", "property_value": location_value, "property_type": "Vector" }, "confidence": 0.7, "original_command": original } return { "type": CommandType.UNKNOWN, "error": "์œ„์น˜ ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", "original_command": original } class CommandValidator: """๋ช…๋ น ๊ฒ€์ฆ ๋ฐ ์ œ์•ˆ ์‹œ์Šคํ…œ""" def __init__(self): self.valid_parent_classes = [ "Actor", "Pawn", "Character", "ActorComponent", "SceneComponent", "UserWidget", "Object" ] self.common_properties = { "health": "int", "speed": "float", "location": "Vector", "rotation": "Rotator", "scale": "Vector", "visible": "bool", "enabled": "bool" } def validate_command(self, parsed_command: Dict[str, any]) -> Dict[str, any]: """ํŒŒ์‹ฑ๋œ ๋ช…๋ น ๊ฒ€์ฆ""" if parsed_command["type"] == CommandType.CREATE_BLUEPRINT: return self._validate_create_blueprint(parsed_command) elif parsed_command["type"] == CommandType.MODIFY_PROPERTY: return self._validate_modify_property(parsed_command) else: return parsed_command def _validate_create_blueprint(self, command: Dict[str, any]) -> Dict[str, any]: """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ๋ช…๋ น ๊ฒ€์ฆ""" params = command["parameters"] issues = [] suggestions = [] # ๋ถ€๋ชจ ํด๋ž˜์Šค ๊ฒ€์ฆ if params["parent_class"] not in self.valid_parent_classes: issues.append(f"์•Œ ์ˆ˜ ์—†๋Š” ๋ถ€๋ชจ ํด๋ž˜์Šค: {params['parent_class']}") suggestions.append(f"์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํด๋ž˜์Šค: {', '.join(self.valid_parent_classes)}") # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ด๋ฆ„ ๊ฒ€์ฆ if not params["blueprint_name"] or len(params["blueprint_name"]) < 3: issues.append("๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ด๋ฆ„์ด ๋„ˆ๋ฌด ์งง์Šต๋‹ˆ๋‹ค") suggestions.append("3๊ธ€์ž ์ด์ƒ์˜ ๋ช…ํ™•ํ•œ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์„ธ์š”") # ๊ฒฝ๋กœ ๊ฒ€์ฆ if not params["asset_path"].startswith("/Game/"): issues.append("์ž˜๋ชป๋œ ์—์…‹ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค") suggestions.append("์—์…‹ ๊ฒฝ๋กœ๋Š” /Game/์œผ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค") if issues: command["validation"] = { "valid": False, "issues": issues, "suggestions": suggestions } else: command["validation"] = {"valid": True} return command def _validate_modify_property(self, command: Dict[str, any]) -> Dict[str, any]: """์†์„ฑ ์ˆ˜์ • ๋ช…๋ น ๊ฒ€์ฆ""" params = command["parameters"] issues = [] suggestions = [] # ์†์„ฑ ํƒ€์ž… ์ถ”๋ก  prop_name = params["property_name"].lower() if prop_name in self.common_properties: inferred_type = self.common_properties[prop_name] params["property_type"] = inferred_type # ๊ฐ’ ๊ฒ€์ฆ if not self._validate_property_value(params["property_value"], inferred_type): issues.append(f"{prop_name}์— ์ž˜๋ชป๋œ ๊ฐ’ ํƒ€์ž…์ž…๋‹ˆ๋‹ค") suggestions.append(f"{prop_name}๋Š” {inferred_type} ํƒ€์ž…์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค") if issues: command["validation"] = { "valid": False, "issues": issues, "suggestions": suggestions } else: command["validation"] = {"valid": True} return command def _validate_property_value(self, value: str, prop_type: str) -> bool: """์†์„ฑ ๊ฐ’ ํƒ€์ž… ๊ฒ€์ฆ""" try: if prop_type == "int": int(value) elif prop_type == "float": float(value) elif prop_type == "bool": value.lower() in ["true", "false", "1", "0"] elif prop_type == "Vector": coords = value.split(",") return len(coords) == 3 and all(self._is_number(c.strip()) for c in coords) elif prop_type == "Rotator": coords = value.split(",") return len(coords) == 3 and all(self._is_number(c.strip()) for c in coords) return True except ValueError: return False def _is_number(self, s: str) -> bool: """๋ฌธ์ž์—ด์ด ์ˆซ์ž์ธ์ง€ ํ™•์ธ""" try: float(s) return True except ValueError: return False ``` ### ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ ```python class UnrealCommandPipeline: """์™„์ „ํ•œ ์ž์—ฐ์–ด โ†’ UnrealBlueprintMCP ํŒŒ์ดํ”„๋ผ์ธ""" def __init__(self, mcp_server_url: str = "ws://localhost:6277"): self.processor = NaturalLanguageProcessor() self.validator = CommandValidator() self.mcp_server_url = mcp_server_url self.command_history = [] async def execute_natural_command(self, command: str) -> Dict[str, any]: """์ž์—ฐ์–ด ๋ช…๋ น์„ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ์ฒ˜๋ฆฌ""" try: # 1. ์ž์—ฐ์–ด ํŒŒ์‹ฑ parsed = self.processor.parse_command(command) # 2. ๋ช…๋ น ๊ฒ€์ฆ validated = self.validator.validate_command(parsed) # 3. ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต if not validated.get("validation", {}).get("valid", True): return { "success": False, "stage": "validation", "parsed_command": parsed, "validation_result": validated["validation"], "original_command": command } # 4. MCP ๋„๊ตฌ ์‹คํ–‰ if validated["type"] != CommandType.UNKNOWN: mcp_result = await self._execute_mcp_action( validated["action"], validated["parameters"] ) # 5. ๊ฒฐ๊ณผ ๊ธฐ๋ก self.command_history.append({ "timestamp": asyncio.get_event_loop().time(), "original_command": command, "parsed_command": parsed, "mcp_result": mcp_result }) return { "success": True, "stage": "completed", "original_command": command, "parsed_command": parsed, "mcp_result": mcp_result, "confidence": validated.get("confidence", 0.5) } else: return { "success": False, "stage": "parsing", "error": parsed.get("error", "์•Œ ์ˆ˜ ์—†๋Š” ๋ช…๋ น"), "suggestion": parsed.get("suggestion", ""), "original_command": command } except Exception as e: return { "success": False, "stage": "execution", "error": str(e), "original_command": command } async def _execute_mcp_action(self, action: str, parameters: Dict[str, any]) -> Dict[str, any]: """MCP ์•ก์…˜ ์‹คํ–‰""" request = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": action, "arguments": parameters } } async with websockets.connect(self.mcp_server_url) as ws: await ws.send(json.dumps(request)) response = await ws.recv() return json.loads(response) def get_command_suggestions(self, partial_command: str) -> List[str]: """์ž๋™์™„์„ฑ ์ œ์•ˆ""" suggestions = [ "Create an Actor blueprint named 'MyActor'", "Create a Character blueprint named 'PlayerCharacter'", "Set PlayerCharacter's health to 100", "Set MyActor's location to 0,0,100", "Create a UserWidget blueprint for main menu", "Make a Pawn blueprint called 'EnemyAI'", ] partial_lower = partial_command.lower() return [s for s in suggestions if any(word in s.lower() for word in partial_lower.split())] ``` ### ์‚ฌ์šฉ ์˜ˆ์‹œ ```python async def demo_natural_language_processing(): pipeline = UnrealCommandPipeline() # ๋‹ค์–‘ํ•œ ์ž์—ฐ์–ด ๋ช…๋ น ํ…Œ์ŠคํŠธ test_commands = [ "Create an Actor blueprint named 'MyTestActor'", "Make a Character blueprint called 'Hero' in the Characters folder", "Set Hero's health to 100", "Change MyTestActor's location to 100, 200, 300", "Create a UI widget for the main menu", "blah blah blah" # ์ž˜๋ชป๋œ ๋ช…๋ น ] for command in test_commands: print(f"\n๋ช…๋ น: {command}") result = await pipeline.execute_natural_command(command) if result["success"]: print(f"โœ… ์„ฑ๊ณต (์‹ ๋ขฐ๋„: {result.get('confidence', 0):.1f})") print(f" ํŒŒ์‹ฑ ๊ฒฐ๊ณผ: {result['parsed_command']['action']}") print(f" MCP ๊ฒฐ๊ณผ: {result['mcp_result']}") else: print(f"โŒ ์‹คํŒจ ({result['stage']})") print(f" ์˜ค๋ฅ˜: {result.get('error', '์•Œ ์ˆ˜ ์—†์Œ')}") if "suggestion" in result: print(f" ์ œ์•ˆ: {result['suggestion']}") # ์‹คํ–‰ asyncio.run(demo_natural_language_processing()) ``` --- ## ๐Ÿ”„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ํŒจํ„ด ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์•ˆ์ •์ ์ธ ๋™์ž‘์„ ๋ณด์žฅํ•˜๋Š” ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ### ๊ณ„์ธตํ™”๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ```python import asyncio import logging from enum import Enum from typing import Dict, Any, Optional, Callable from dataclasses import dataclass import time class ErrorCategory(Enum): NETWORK = "network" VALIDATION = "validation" UNREAL_ENGINE = "unreal_engine" MCP_PROTOCOL = "mcp_protocol" TIMEOUT = "timeout" UNKNOWN = "unknown" class ErrorSeverity(Enum): LOW = "low" # ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ MEDIUM = "medium" # ์ œํ•œ์  ์žฌ์‹œ๋„ HIGH = "high" # ์ฆ‰์‹œ ์‹คํŒจ CRITICAL = "critical" # ์‹œ์Šคํ…œ ์ค‘๋‹จ @dataclass class ErrorContext: """์—๋Ÿฌ ์ปจํ…์ŠคํŠธ ์ •๋ณด""" category: ErrorCategory severity: ErrorSeverity message: str details: Dict[str, Any] timestamp: float command: Optional[str] = None retry_count: int = 0 max_retries: int = 3 class UnrealMCPErrorHandler: """UnrealBlueprintMCP ์ „์šฉ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ""" def __init__(self): self.error_patterns = { # ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ r"Connection.*refused": (ErrorCategory.NETWORK, ErrorSeverity.MEDIUM), r"WebSocket.*closed": (ErrorCategory.NETWORK, ErrorSeverity.LOW), r"timeout": (ErrorCategory.TIMEOUT, ErrorSeverity.LOW), # Unreal Engine ์—๋Ÿฌ r"Blueprint.*not found": (ErrorCategory.UNREAL_ENGINE, ErrorSeverity.MEDIUM), r"Invalid.*parent class": (ErrorCategory.VALIDATION, ErrorSeverity.HIGH), r"Asset.*already exists": (ErrorCategory.UNREAL_ENGINE, ErrorSeverity.LOW), r"Property.*not found": (ErrorCategory.UNREAL_ENGINE, ErrorSeverity.MEDIUM), # MCP ํ”„๋กœํ† ์ฝœ ์—๋Ÿฌ r"Invalid.*JSON-RPC": (ErrorCategory.MCP_PROTOCOL, ErrorSeverity.HIGH), r"Method.*not found": (ErrorCategory.MCP_PROTOCOL, ErrorSeverity.HIGH), r"Invalid.*parameters": (ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM), } self.retry_strategies = { ErrorCategory.NETWORK: self._exponential_backoff_retry, ErrorCategory.TIMEOUT: self._linear_retry, ErrorCategory.UNREAL_ENGINE: self._conditional_retry, ErrorCategory.VALIDATION: self._no_retry, ErrorCategory.MCP_PROTOCOL: self._no_retry, } self.recovery_actions = { ErrorCategory.NETWORK: self._recover_network_connection, ErrorCategory.UNREAL_ENGINE: self._recover_unreal_state, ErrorCategory.TIMEOUT: self._recover_timeout, } self.error_history = [] self.logger = logging.getLogger(__name__) def categorize_error(self, error: Exception, context: Dict[str, Any] = None) -> ErrorContext: """์—๋Ÿฌ๋ฅผ ์นดํ…Œ๊ณ ๋ฆฌํ™”ํ•˜๊ณ  ์‹ฌ๊ฐ๋„ ํŒ์ •""" error_msg = str(error).lower() # ํŒจํ„ด ๋งค์นญ์œผ๋กœ ์—๋Ÿฌ ๋ถ„๋ฅ˜ for pattern, (category, severity) in self.error_patterns.items(): if re.search(pattern, error_msg): return ErrorContext( category=category, severity=severity, message=str(error), details=context or {}, timestamp=time.time(), command=context.get("command") if context else None ) # ๊ธฐ๋ณธ ๋ถ„๋ฅ˜ return ErrorContext( category=ErrorCategory.UNKNOWN, severity=ErrorSeverity.MEDIUM, message=str(error), details=context or {}, timestamp=time.time(), command=context.get("command") if context else None ) async def handle_error(self, error: Exception, context: Dict[str, Any] = None) -> Dict[str, Any]: """์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฉ”์ธ ํ•จ์ˆ˜""" error_context = self.categorize_error(error, context) self.error_history.append(error_context) self.logger.error(f"์—๋Ÿฌ ๋ฐœ์ƒ: {error_context.category.value} - {error_context.message}") # ์‹ฌ๊ฐ๋„์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ if error_context.severity == ErrorSeverity.CRITICAL: return await self._handle_critical_error(error_context) elif error_context.severity == ErrorSeverity.HIGH: return await self._handle_high_severity_error(error_context) else: return await self._handle_recoverable_error(error_context) async def _handle_critical_error(self, error_context: ErrorContext) -> Dict[str, Any]: """์น˜๋ช…์  ์—๋Ÿฌ ์ฒ˜๋ฆฌ""" self.logger.critical(f"์น˜๋ช…์  ์—๋Ÿฌ: {error_context.message}") return { "success": False, "error_type": "critical", "category": error_context.category.value, "message": error_context.message, "action": "system_shutdown_required", "recovery_possible": False } async def _handle_high_severity_error(self, error_context: ErrorContext) -> Dict[str, Any]: """๋†’์€ ์‹ฌ๊ฐ๋„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ""" self.logger.error(f"๋†’์€ ์‹ฌ๊ฐ๋„ ์—๋Ÿฌ: {error_context.message}") # ๋ณต๊ตฌ ์•ก์…˜ ์‹œ๋„ recovery_action = self.recovery_actions.get(error_context.category) if recovery_action: recovery_result = await recovery_action(error_context) if recovery_result["success"]: return { "success": True, "error_type": "high", "category": error_context.category.value, "message": "๋ณต๊ตฌ ์™„๋ฃŒ", "recovery_action": recovery_result } return { "success": False, "error_type": "high", "category": error_context.category.value, "message": error_context.message, "action": "manual_intervention_required", "recovery_possible": False } async def _handle_recoverable_error(self, error_context: ErrorContext) -> Dict[str, Any]: """๋ณต๊ตฌ ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ""" retry_strategy = self.retry_strategies.get(error_context.category, self._no_retry) if error_context.retry_count < error_context.max_retries: retry_delay = await retry_strategy(error_context) return { "success": False, "error_type": "recoverable", "category": error_context.category.value, "message": error_context.message, "action": "retry", "retry_delay": retry_delay, "retry_count": error_context.retry_count + 1 } else: return { "success": False, "error_type": "max_retries_exceeded", "category": error_context.category.value, "message": f"์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ: {error_context.message}", "action": "give_up", "total_retries": error_context.retry_count } async def _exponential_backoff_retry(self, error_context: ErrorContext) -> float: """์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„""" base_delay = 1.0 delay = base_delay * (2 ** error_context.retry_count) max_delay = 30.0 actual_delay = min(delay, max_delay) await asyncio.sleep(actual_delay) return actual_delay async def _linear_retry(self, error_context: ErrorContext) -> float: """์„ ํ˜• ์žฌ์‹œ๋„""" delay = 2.0 * (error_context.retry_count + 1) await asyncio.sleep(delay) return delay async def _conditional_retry(self, error_context: ErrorContext) -> float: """์กฐ๊ฑด๋ถ€ ์žฌ์‹œ๋„""" # Unreal Engine ์ƒํƒœ์— ๋”ฐ๋ฅธ ์กฐ๊ฑด๋ถ€ ์žฌ์‹œ๋„ if "Blueprint.*not found" in error_context.message: # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ - ๋‹ค์‹œ ์ƒ์„ฑ ์‹œ๋„ delay = 1.0 elif "Asset.*already exists" in error_context.message: # ์—์…‹์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ - ์งง์€ ๋Œ€๊ธฐ ํ›„ ๋ฎ์–ด์“ฐ๊ธฐ delay = 0.5 else: delay = 3.0 await asyncio.sleep(delay) return delay async def _no_retry(self, error_context: ErrorContext) -> float: """์žฌ์‹œ๋„ ์—†์Œ""" return 0.0 async def _recover_network_connection(self, error_context: ErrorContext) -> Dict[str, Any]: """๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ณต๊ตฌ""" try: # MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ import websockets test_ws = await websockets.connect("ws://localhost:6277", timeout=5) await test_ws.close() return {"success": True, "action": "connection_restored"} except Exception as e: return {"success": False, "action": "connection_failed", "error": str(e)} async def _recover_unreal_state(self, error_context: ErrorContext) -> Dict[str, Any]: """Unreal Engine ์ƒํƒœ ๋ณต๊ตฌ""" try: # Unreal ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ import websockets test_ws = await websockets.connect("ws://localhost:8080", timeout=5) await test_ws.close() return {"success": True, "action": "unreal_connection_restored"} except Exception as e: return {"success": False, "action": "unreal_connection_failed", "error": str(e)} async def _recover_timeout(self, error_context: ErrorContext) -> Dict[str, Any]: """ํƒ€์ž„์•„์›ƒ ๋ณต๊ตฌ""" # ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ ์‹œ ์—ฐ๊ฒฐ ์žฌ์„ค์ • await asyncio.sleep(1) return {"success": True, "action": "timeout_recovered"} class ResilientUnrealClient: """์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ•ํ™”๋œ Unreal ํด๋ผ์ด์–ธํŠธ""" def __init__(self, mcp_server_url: str = "ws://localhost:6277"): self.mcp_server_url = mcp_server_url self.error_handler = UnrealMCPErrorHandler() self.max_global_retries = 3 self.command_queue = asyncio.Queue() self.is_processing = False async def execute_command_with_recovery(self, command: str, parameters: Dict[str, Any]) -> Dict[str, Any]: """๋ณต๊ตฌ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” ๋ช…๋ น ์‹คํ–‰""" attempt = 0 last_error = None while attempt < self.max_global_retries: try: result = await self._execute_single_command(command, parameters) if result.get("success", False): return result else: # MCP ํ”„๋กœํ† ์ฝœ ์—๋Ÿฌ last_error = Exception(result.get("error", "Unknown MCP error")) except Exception as e: last_error = e # ์—๋Ÿฌ ์ฒ˜๋ฆฌ error_result = await self.error_handler.handle_error( last_error, {"command": command, "parameters": parameters, "attempt": attempt} ) if error_result.get("action") == "retry": attempt += 1 await asyncio.sleep(error_result.get("retry_delay", 1.0)) continue elif error_result.get("action") == "give_up": break else: # ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ return error_result # ๋ชจ๋“  ์žฌ์‹œ๋„ ์‹คํŒจ return { "success": False, "error": "All retry attempts failed", "last_error": str(last_error), "total_attempts": attempt + 1 } async def _execute_single_command(self, command: str, parameters: Dict[str, Any]) -> Dict[str, Any]: """๋‹จ์ผ ๋ช…๋ น ์‹คํ–‰""" import websockets import json request = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": command, "arguments": parameters } } # ํƒ€์ž„์•„์›ƒ ์„ค์ • timeout = parameters.get("timeout", 30) async with websockets.connect(self.mcp_server_url, timeout=timeout) as ws: await ws.send(json.dumps(request)) response = await asyncio.wait_for(ws.recv(), timeout=timeout) result = json.loads(response) if "error" in result: raise Exception(f"MCP Error: {result['error']}") return result.get("result", {}) async def batch_execute_with_recovery(self, commands: List[Dict[str, Any]]) -> Dict[str, Any]: """๋ฐฐ์น˜ ์‹คํ–‰ with ๋ณต๊ตฌ""" results = [] failed_commands = [] for i, cmd_info in enumerate(commands): command = cmd_info["command"] parameters = cmd_info["parameters"] result = await self.execute_command_with_recovery(command, parameters) if result.get("success", False): results.append({ "index": i, "command": command, "result": result, "success": True }) else: failed_commands.append({ "index": i, "command": command, "error": result, "success": False }) results.append({ "index": i, "command": command, "result": result, "success": False }) return { "total_commands": len(commands), "successful": len(commands) - len(failed_commands), "failed": len(failed_commands), "results": results, "failed_commands": failed_commands, "success_rate": (len(commands) - len(failed_commands)) / len(commands) } ``` ### ์‚ฌ์šฉ ์˜ˆ์‹œ ```python async def demo_error_handling(): client = ResilientUnrealClient() # ๋‹จ์ผ ๋ช…๋ น ์‹คํ–‰ (์—๋Ÿฌ ๋ณต๊ตฌ ํฌํ•จ) result = await client.execute_command_with_recovery( "create_blueprint", { "blueprint_name": "TestActor", "parent_class": "Actor", "asset_path": "/Game/Test/" } ) print("๋‹จ์ผ ๋ช…๋ น ๊ฒฐ๊ณผ:", result) # ๋ฐฐ์น˜ ๋ช…๋ น ์‹คํ–‰ (์—๋Ÿฌ ๋ณต๊ตฌ ํฌํ•จ) batch_commands = [ { "command": "create_blueprint", "parameters": { "blueprint_name": "Player", "parent_class": "Character" } }, { "command": "set_blueprint_property", "parameters": { "blueprint_path": "/Game/Blueprints/Player", "property_name": "Health", "property_value": "100" } }, { "command": "create_blueprint", "parameters": { "blueprint_name": "InvalidBlueprint", "parent_class": "NonExistentClass" # ์—๋Ÿฌ ๋ฐœ์ƒ } } ] batch_result = await client.batch_execute_with_recovery(batch_commands) print("๋ฐฐ์น˜ ์‹คํ–‰ ๊ฒฐ๊ณผ:", batch_result) asyncio.run(demo_error_handling()) ``` --- ## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒจํ„ด ๋Œ€๊ทœ๋ชจ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ž‘์—…์„ ์œ„ํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ### ์—ฐ๊ฒฐ ํ’€๋ง ```python import asyncio import websockets import json from typing import Dict, Any, List, Optional from contextlib import asynccontextmanager import time import logging class MCPConnectionPool: """MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ’€""" def __init__(self, server_url: str, min_connections: int = 2, max_connections: int = 10): self.server_url = server_url self.min_connections = min_connections self.max_connections = max_connections self.available_connections = asyncio.Queue() self.active_connections = set() self.total_connections = 0 self.connection_stats = { "created": 0, "reused": 0, "closed": 0, "errors": 0 } self.logger = logging.getLogger(__name__) async def initialize(self): """์—ฐ๊ฒฐ ํ’€ ์ดˆ๊ธฐํ™”""" for _ in range(self.min_connections): conn = await self._create_connection() if conn: await self.available_connections.put(conn) async def _create_connection(self) -> Optional[websockets.WebSocketServerProtocol]: """์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ""" try: conn = await websockets.connect(self.server_url, timeout=10) self.total_connections += 1 self.connection_stats["created"] += 1 self.logger.debug(f"์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ๋จ: {self.total_connections}") return conn except Exception as e: self.connection_stats["errors"] += 1 self.logger.error(f"์—ฐ๊ฒฐ ์ƒ์„ฑ ์‹คํŒจ: {e}") return None @asynccontextmanager async def get_connection(self): """์—ฐ๊ฒฐ ํš๋“ ์ปจํ…์ŠคํŠธ ๋งค๋‹ˆ์ €""" conn = None try: # ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—ฐ๊ฒฐ์ด ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ try: conn = await asyncio.wait_for(self.available_connections.get(), timeout=1.0) self.connection_stats["reused"] += 1 except asyncio.TimeoutError: # ์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ if self.total_connections < self.max_connections: conn = await self._create_connection() else: # ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜ ๋„๋‹ฌ, ๋Œ€๊ธฐ conn = await self.available_connections.get() self.connection_stats["reused"] += 1 if conn and not conn.closed: self.active_connections.add(conn) yield conn else: # ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง„ ๊ฒฝ์šฐ ์ƒˆ๋กœ ์ƒ์„ฑ conn = await self._create_connection() if conn: self.active_connections.add(conn) yield conn else: raise Exception("์—ฐ๊ฒฐ ์ƒ์„ฑ ์‹คํŒจ") finally: if conn and conn in self.active_connections: self.active_connections.remove(conn) # ์—ฐ๊ฒฐ์ด ์‚ด์•„์žˆ์œผ๋ฉด ํ’€์— ๋ฐ˜ํ™˜ if not conn.closed: await self.available_connections.put(conn) else: self.total_connections -= 1 self.connection_stats["closed"] += 1 async def close_all(self): """๋ชจ๋“  ์—ฐ๊ฒฐ ์ข…๋ฃŒ""" # ํ™œ์„ฑ ์—ฐ๊ฒฐ ์ข…๋ฃŒ for conn in list(self.active_connections): await conn.close() # ๋Œ€๊ธฐ ์ค‘์ธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ while not self.available_connections.empty(): conn = await self.available_connections.get() await conn.close() self.total_connections = 0 self.logger.info("๋ชจ๋“  ์—ฐ๊ฒฐ์ด ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค") def get_stats(self) -> Dict[str, Any]: """์—ฐ๊ฒฐ ํ’€ ํ†ต๊ณ„""" return { "total_connections": self.total_connections, "available_connections": self.available_connections.qsize(), "active_connections": len(self.active_connections), "stats": self.connection_stats.copy() } class OptimizedUnrealClient: """์ตœ์ ํ™”๋œ Unreal ํด๋ผ์ด์–ธํŠธ""" def __init__(self, server_url: str = "ws://localhost:6277"): self.connection_pool = MCPConnectionPool(server_url) self.request_cache = {} self.cache_ttl = 300 # 5๋ถ„ self.performance_metrics = { "total_requests": 0, "cache_hits": 0, "cache_misses": 0, "avg_response_time": 0, "response_times": [] } async def initialize(self): """ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™”""" await self.connection_pool.initialize() async def execute_command(self, command: str, parameters: Dict[str, Any], use_cache: bool = True) -> Dict[str, Any]: """๋ช…๋ น ์‹คํ–‰ (์บ์‹œ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”)""" start_time = time.time() # ์บ์‹œ ํ‚ค ์ƒ์„ฑ cache_key = self._generate_cache_key(command, parameters) # ์บ์‹œ ํ™•์ธ (์ฝ๊ธฐ ์ „์šฉ ๋ช…๋ น๋งŒ) if use_cache and command in ["get_server_status", "list_supported_blueprint_classes"]: cached_result = self._get_cached_result(cache_key) if cached_result: self.performance_metrics["cache_hits"] += 1 return cached_result self.performance_metrics["cache_misses"] += 1 # ์—ฐ๊ฒฐ ํ’€์—์„œ ์—ฐ๊ฒฐ ํš๋“ํ•˜์—ฌ ์‹คํ–‰ async with self.connection_pool.get_connection() as conn: result = await self._execute_with_connection(conn, command, parameters) # ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ response_time = time.time() - start_time self._update_performance_metrics(response_time) # ๊ฒฐ๊ณผ ์บ์‹œ (์ฝ๊ธฐ ์ „์šฉ ๋ช…๋ น๋งŒ) if use_cache and command in ["get_server_status", "list_supported_blueprint_classes"]: self._cache_result(cache_key, result) return result async def _execute_with_connection(self, conn: websockets.WebSocketServerProtocol, command: str, parameters: Dict[str, Any]) -> Dict[str, Any]: """์—ฐ๊ฒฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ช…๋ น ์‹คํ–‰""" request = { "jsonrpc": "2.0", "id": int(time.time() * 1000000), # ๋งˆ์ดํฌ๋กœ์ดˆ ๊ธฐ๋ฐ˜ ID "method": "tools/call", "params": { "name": command, "arguments": parameters } } await conn.send(json.dumps(request)) response = await conn.recv() return json.loads(response) def _generate_cache_key(self, command: str, parameters: Dict[str, Any]) -> str: """์บ์‹œ ํ‚ค ์ƒ์„ฑ""" import hashlib key_data = f"{command}:{json.dumps(parameters, sort_keys=True)}" return hashlib.md5(key_data.encode()).hexdigest() def _get_cached_result(self, cache_key: str) -> Optional[Dict[str, Any]]: """์บ์‹œ๋œ ๊ฒฐ๊ณผ ์กฐํšŒ""" if cache_key in self.request_cache: cached_data = self.request_cache[cache_key] if time.time() - cached_data["timestamp"] < self.cache_ttl: return cached_data["result"] else: # ๋งŒ๋ฃŒ๋œ ์บ์‹œ ์ œ๊ฑฐ del self.request_cache[cache_key] return None def _cache_result(self, cache_key: str, result: Dict[str, Any]): """๊ฒฐ๊ณผ ์บ์‹œ""" self.request_cache[cache_key] = { "result": result, "timestamp": time.time() } def _update_performance_metrics(self, response_time: float): """์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ""" self.performance_metrics["total_requests"] += 1 self.performance_metrics["response_times"].append(response_time) # ์ตœ๊ทผ 100๊ฐœ ์š”์ฒญ๋งŒ ์œ ์ง€ if len(self.performance_metrics["response_times"]) > 100: self.performance_metrics["response_times"].pop(0) # ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ ๊ณ„์‚ฐ self.performance_metrics["avg_response_time"] = sum( self.performance_metrics["response_times"] ) / len(self.performance_metrics["response_times"]) def get_performance_stats(self) -> Dict[str, Any]: """์„ฑ๋Šฅ ํ†ต๊ณ„ ์กฐํšŒ""" pool_stats = self.connection_pool.get_stats() return { "connection_pool": pool_stats, "cache": { "cache_size": len(self.request_cache), "hit_rate": ( self.performance_metrics["cache_hits"] / max(1, self.performance_metrics["total_requests"]) ) * 100 }, "performance": self.performance_metrics.copy() } async def cleanup(self): """๋ฆฌ์†Œ์Šค ์ •๋ฆฌ""" await self.connection_pool.close_all() self.request_cache.clear() ``` ### ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ตœ์ ํ™” ```python class BatchProcessor: """ํšจ์œจ์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ""" def __init__(self, client: OptimizedUnrealClient, batch_size: int = 5): self.client = client self.batch_size = batch_size self.semaphore = asyncio.Semaphore(batch_size) async def process_blueprints_batch(self, blueprint_specs: List[Dict[str, Any]]) -> Dict[str, Any]: """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๋ฐฐ์น˜ ์ƒ์„ฑ""" tasks = [] for spec in blueprint_specs: task = self._process_single_blueprint(spec) tasks.append(task) # ๋ฐฐ์น˜ ํฌ๊ธฐ๋งŒํผ ๋™์‹œ ์‹คํ–‰ results = [] for i in range(0, len(tasks), self.batch_size): batch = tasks[i:i + self.batch_size] batch_results = await asyncio.gather(*batch, return_exceptions=True) results.extend(batch_results) # ๊ฒฐ๊ณผ ๋ถ„์„ successful = sum(1 for r in results if isinstance(r, dict) and r.get("success")) failed = len(results) - successful return { "total": len(blueprint_specs), "successful": successful, "failed": failed, "results": results, "success_rate": successful / len(blueprint_specs) * 100 } async def _process_single_blueprint(self, spec: Dict[str, Any]) -> Dict[str, Any]: """๋‹จ์ผ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ฒ˜๋ฆฌ (์„ธ๋งˆํฌ์–ด ์‚ฌ์šฉ)""" async with self.semaphore: try: # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ create_result = await self.client.execute_command( "create_blueprint", { "blueprint_name": spec["name"], "parent_class": spec.get("parent_class", "Actor"), "asset_path": spec.get("asset_path", "/Game/Blueprints/") } ) if not create_result.get("success", False): return {"success": False, "error": "Blueprint creation failed", "spec": spec} # ์†์„ฑ ์„ค์ • (์žˆ๋Š” ๊ฒฝ์šฐ) if "properties" in spec: for prop_name, prop_value in spec["properties"].items(): prop_result = await self.client.execute_command( "set_blueprint_property", { "blueprint_path": f"{spec.get('asset_path', '/Game/Blueprints/')}{spec['name']}", "property_name": prop_name, "property_value": str(prop_value) } ) if not prop_result.get("success", False): return { "success": False, "error": f"Property {prop_name} setting failed", "spec": spec } return {"success": True, "spec": spec, "blueprint_created": create_result} except Exception as e: return {"success": False, "error": str(e), "spec": spec} # ์‚ฌ์šฉ ์˜ˆ์‹œ async def demo_performance_optimization(): # ์ตœ์ ํ™”๋œ ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” client = OptimizedUnrealClient() await client.initialize() # ๋ฐฐ์น˜ ํ”„๋กœ์„ธ์„œ ์„ค์ • batch_processor = BatchProcessor(client, batch_size=3) # ๋Œ€๋Ÿ‰ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ŠคํŽ™ blueprint_specs = [ { "name": f"TestActor_{i}", "parent_class": "Actor", "properties": { "Health": 100 + i * 10, "Speed": 600 + i * 50 } } for i in range(20) # 20๊ฐœ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ] start_time = time.time() # ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹คํ–‰ result = await batch_processor.process_blueprints_batch(blueprint_specs) end_time = time.time() print(f"๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ: {result}") print(f"์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {end_time - start_time:.2f}์ดˆ") print(f"์„ฑ๋Šฅ ํ†ต๊ณ„: {client.get_performance_stats()}") # ์ •๋ฆฌ await client.cleanup() asyncio.run(demo_performance_optimization()) ``` --- ## ๐Ÿ“ฆ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํŒจํ„ด ๋Œ€๊ทœ๋ชจ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ž‘์—…์„ ์œ„ํ•œ ํšจ์œจ์ ์ธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ### ์ง€๋Šฅํ˜• ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ ```python import asyncio from typing import List, Dict, Any, Callable, Optional from enum import Enum from dataclasses import dataclass, field import time import heapq from collections import defaultdict class TaskPriority(Enum): LOW = 3 NORMAL = 2 HIGH = 1 CRITICAL = 0 class TaskStatus(Enum): PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" CANCELLED = "cancelled" @dataclass class BatchTask: """๋ฐฐ์น˜ ์ž‘์—… ๋‹จ์œ„""" id: str command: str parameters: Dict[str, Any] priority: TaskPriority = TaskPriority.NORMAL dependencies: List[str] = field(default_factory=list) status: TaskStatus = TaskStatus.PENDING created_at: float = field(default_factory=time.time) started_at: Optional[float] = None completed_at: Optional[float] = None result: Optional[Dict[str, Any]] = None error: Optional[str] = None retry_count: int = 0 max_retries: int = 3 def __lt__(self, other): """์šฐ์„ ์ˆœ์œ„ ํ๋ฅผ ์œ„ํ•œ ๋น„๊ต ์—ฐ์‚ฐ์ž""" if self.priority.value != other.priority.value: return self.priority.value < other.priority.value return self.created_at < other.created_at class SmartBatchScheduler: """์ง€๋Šฅํ˜• ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ""" def __init__(self, max_concurrent: int = 5, client: Optional[OptimizedUnrealClient] = None): self.max_concurrent = max_concurrent self.client = client or OptimizedUnrealClient() # ์ž‘์—… ๊ด€๋ฆฌ self.task_queue = [] # ์šฐ์„ ์ˆœ์œ„ ํ self.running_tasks = {} # ID -> Task self.completed_tasks = {} # ID -> Task self.task_dependencies = defaultdict(set) # Task ID -> dependents # ์„ฑ๋Šฅ ์ถ”์  self.stats = { "total_submitted": 0, "total_completed": 0, "total_failed": 0, "avg_processing_time": 0, "throughput_per_minute": 0 } self.is_running = False self.worker_semaphore = asyncio.Semaphore(max_concurrent) def submit_task(self, task: BatchTask) -> str: """์ž‘์—… ์ œ์ถœ""" self.stats["total_submitted"] += 1 heapq.heappush(self.task_queue, task) # ์˜์กด์„ฑ ๊ด€๊ณ„ ๋“ฑ๋ก for dep_id in task.dependencies: self.task_dependencies[dep_id].add(task.id) return task.id def submit_blueprint_creation(self, name: str, parent_class: str = "Actor", asset_path: str = "/Game/Blueprints/", priority: TaskPriority = TaskPriority.NORMAL, dependencies: List[str] = None) -> str: """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์ž‘์—… ์ œ์ถœ""" task = BatchTask( id=f"create_{name}_{int(time.time() * 1000000)}", command="create_blueprint", parameters={ "blueprint_name": name, "parent_class": parent_class, "asset_path": asset_path }, priority=priority, dependencies=dependencies or [] ) return self.submit_task(task) def submit_property_update(self, blueprint_path: str, property_name: str, property_value: str, dependencies: List[str] = None) -> str: """์†์„ฑ ์—…๋ฐ์ดํŠธ ์ž‘์—… ์ œ์ถœ""" task = BatchTask( id=f"prop_{property_name}_{int(time.time() * 1000000)}", command="set_blueprint_property", parameters={ "blueprint_path": blueprint_path, "property_name": property_name, "property_value": property_value }, dependencies=dependencies or [] ) return self.submit_task(task) async def start_processing(self): """๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ์ž‘""" if self.is_running: return self.is_running = True await self.client.initialize() # ๋ฉ”์ธ ์Šค์ผ€์ค„๋Ÿฌ ๋ฃจํ”„ asyncio.create_task(self._scheduler_loop()) async def stop_processing(self): """๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘์ง€""" self.is_running = False # ์‹คํ–‰ ์ค‘์ธ ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ while self.running_tasks: await asyncio.sleep(0.1) await self.client.cleanup() async def _scheduler_loop(self): """์Šค์ผ€์ค„๋Ÿฌ ๋ฉ”์ธ ๋ฃจํ”„""" while self.is_running: # ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ž‘์—… ์ฐพ๊ธฐ ready_tasks = self._get_ready_tasks() # ๋™์‹œ ์‹คํ–‰ ์ œํ•œ ๋‚ด์—์„œ ์ž‘์—… ์‹œ์ž‘ for task in ready_tasks: if len(self.running_tasks) >= self.max_concurrent: break asyncio.create_task(self._execute_task(task)) await asyncio.sleep(0.1) # CPU ์‚ฌ์šฉ๋Ÿ‰ ์ œ์–ด def _get_ready_tasks(self) -> List[BatchTask]: """์‹คํ–‰ ์ค€๋น„๋œ ์ž‘์—… ๋ชฉ๋ก""" ready_tasks = [] temp_queue = [] # ์šฐ์„ ์ˆœ์œ„ ํ์—์„œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ž‘์—… ์ฐพ๊ธฐ while self.task_queue: task = heapq.heappop(self.task_queue) if task.status != TaskStatus.PENDING: continue # ์˜์กด์„ฑ ํ™•์ธ if self._dependencies_satisfied(task): ready_tasks.append(task) else: temp_queue.append(task) # ์‹คํ–‰๋˜์ง€ ์•Š์€ ์ž‘์—…๋“ค ๋‹ค์‹œ ํ์— ๋„ฃ๊ธฐ for task in temp_queue: heapq.heappush(self.task_queue, task) return ready_tasks def _dependencies_satisfied(self, task: BatchTask) -> bool: """์˜์กด์„ฑ์ด ๋งŒ์กฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ""" for dep_id in task.dependencies: if dep_id not in self.completed_tasks: return False if self.completed_tasks[dep_id].status != TaskStatus.COMPLETED: return False return True async def _execute_task(self, task: BatchTask): """๊ฐœ๋ณ„ ์ž‘์—… ์‹คํ–‰""" async with self.worker_semaphore: task.status = TaskStatus.RUNNING task.started_at = time.time() self.running_tasks[task.id] = task try: # MCP ๋ช…๋ น ์‹คํ–‰ result = await self.client.execute_command( task.command, task.parameters ) if result.get("success", False): task.status = TaskStatus.COMPLETED task.result = result self.stats["total_completed"] += 1 else: raise Exception(result.get("error", "Unknown error")) except Exception as e: task.error = str(e) task.retry_count += 1 if task.retry_count <= task.max_retries: # ์žฌ์‹œ๋„ task.status = TaskStatus.PENDING heapq.heappush(self.task_queue, task) else: task.status = TaskStatus.FAILED self.stats["total_failed"] += 1 finally: task.completed_at = time.time() # ์‹คํ–‰ ์ค‘ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ if task.id in self.running_tasks: del self.running_tasks[task.id] # ์™„๋ฃŒ๋œ ์ž‘์—…์— ์ถ”๊ฐ€ self.completed_tasks[task.id] = task # ์˜์กด ์ž‘์—…๋“ค ํ•ด์ œ self._release_dependent_tasks(task.id) # ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ self._update_stats(task) def _release_dependent_tasks(self, completed_task_id: str): """์™„๋ฃŒ๋œ ์ž‘์—…์— ์˜์กดํ•˜๋Š” ์ž‘์—…๋“ค ํ•ด์ œ""" if completed_task_id in self.task_dependencies: for dependent_id in self.task_dependencies[completed_task_id]: # ์˜์กด ์ž‘์—…๋“ค์€ ๋‹ค์Œ ์Šค์ผ€์ค„๋ง ์‚ฌ์ดํด์—์„œ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Œ pass def _update_stats(self, task: BatchTask): """ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ""" if task.started_at and task.completed_at: processing_time = task.completed_at - task.started_at # ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (์ง€์ˆ˜ ์ด๋™ ํ‰๊ท ) if self.stats["avg_processing_time"] == 0: self.stats["avg_processing_time"] = processing_time else: alpha = 0.1 # ๊ฐ€์ค‘์น˜ self.stats["avg_processing_time"] = ( alpha * processing_time + (1 - alpha) * self.stats["avg_processing_time"] ) def get_status(self) -> Dict[str, Any]: """ํ˜„์žฌ ์ƒํƒœ ์กฐํšŒ""" return { "is_running": self.is_running, "queue_size": len(self.task_queue), "running_tasks": len(self.running_tasks), "completed_tasks": len(self.completed_tasks), "stats": self.stats.copy(), "task_details": { "pending": [t for t in self.task_queue if t.status == TaskStatus.PENDING], "running": list(self.running_tasks.values()), "completed": list(self.completed_tasks.values())[-10:] # ์ตœ๊ทผ 10๊ฐœ๋งŒ } } async def wait_for_completion(self, timeout: Optional[float] = None) -> bool: """๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ""" start_time = time.time() while (self.task_queue or self.running_tasks): if timeout and (time.time() - start_time) > timeout: return False await asyncio.sleep(0.1) return True class BlueprintFactory: """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ํŒฉํ† ๋ฆฌ ํŒจํ„ด""" def __init__(self, scheduler: SmartBatchScheduler): self.scheduler = scheduler self.blueprint_templates = { "character": { "parent_class": "Character", "default_properties": { "Health": "100", "MaxWalkSpeed": "600", "JumpZVelocity": "420" } }, "weapon": { "parent_class": "Actor", "default_properties": { "Damage": "25", "FireRate": "0.1", "AmmoCapacity": "30" } }, "pickup": { "parent_class": "Actor", "default_properties": { "Value": "10", "RespawnTime": "30" } } } async def create_blueprint_set(self, template_name: str, names: List[str], custom_properties: Dict[str, Dict[str, str]] = None) -> List[str]: """ํ…œํ”Œ๋ฆฟ ๊ธฐ๋ฐ˜ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์„ธํŠธ ์ƒ์„ฑ""" if template_name not in self.blueprint_templates: raise ValueError(f"Unknown template: {template_name}") template = self.blueprint_templates[template_name] custom_properties = custom_properties or {} task_ids = [] for name in names: # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์ž‘์—… create_task_id = self.scheduler.submit_blueprint_creation( name=name, parent_class=template["parent_class"], priority=TaskPriority.HIGH ) task_ids.append(create_task_id) # ์†์„ฑ ์„ค์ • ์ž‘์—… (์ƒ์„ฑ ์ž‘์—…์— ์˜์กด) properties = template["default_properties"].copy() if name in custom_properties: properties.update(custom_properties[name]) for prop_name, prop_value in properties.items(): prop_task_id = self.scheduler.submit_property_update( blueprint_path=f"/Game/Blueprints/{name}", property_name=prop_name, property_value=prop_value, dependencies=[create_task_id] ) task_ids.append(prop_task_id) return task_ids async def create_complete_game_setup(self) -> Dict[str, List[str]]: """์™„์ „ํ•œ ๊ฒŒ์ž„ ์„ค์ • ์ƒ์„ฑ""" task_groups = {} # ์บ๋ฆญํ„ฐ ์ƒ์„ฑ character_names = ["PlayerCharacter", "EnemyCharacter", "NPCCharacter"] task_groups["characters"] = await self.create_blueprint_set( "character", character_names, { "PlayerCharacter": {"Health": "150", "MaxWalkSpeed": "700"}, "EnemyCharacter": {"Health": "75", "MaxWalkSpeed": "500"}, "NPCCharacter": {"Health": "50", "MaxWalkSpeed": "300"} } ) # ๋ฌด๊ธฐ ์ƒ์„ฑ weapon_names = ["Pistol", "Rifle", "Shotgun"] task_groups["weapons"] = await self.create_blueprint_set( "weapon", weapon_names, { "Pistol": {"Damage": "20", "FireRate": "0.3"}, "Rifle": {"Damage": "35", "FireRate": "0.15"}, "Shotgun": {"Damage": "80", "FireRate": "0.8"} } ) # ํ”ฝ์—… ์•„์ดํ…œ ์ƒ์„ฑ pickup_names = ["HealthPack", "AmmoPack", "Coin"] task_groups["pickups"] = await self.create_blueprint_set( "pickup", pickup_names, { "HealthPack": {"Value": "25"}, "AmmoPack": {"Value": "15"}, "Coin": {"Value": "5"} } ) return task_groups ``` ### ์‚ฌ์šฉ ์˜ˆ์‹œ ```python async def demo_batch_processing(): # ์Šค์ผ€์ค„๋Ÿฌ ์ดˆ๊ธฐํ™” scheduler = SmartBatchScheduler(max_concurrent=3) factory = BlueprintFactory(scheduler) # ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ์ž‘ await scheduler.start_processing() try: # ์™„์ „ํ•œ ๊ฒŒ์ž„ ์„ค์ • ์ƒ์„ฑ print("๊ฒŒ์ž„ ์„ค์ • ์ƒ์„ฑ ์‹œ์ž‘...") task_groups = await factory.create_complete_game_setup() print(f"์ œ์ถœ๋œ ์ž‘์—… ๊ทธ๋ฃน: {task_groups}") # ์ง„ํ–‰ ์ƒํ™ฉ ๋ชจ๋‹ˆํ„ฐ๋ง while not await scheduler.wait_for_completion(timeout=1): status = scheduler.get_status() print(f"์ง„ํ–‰ ์ƒํ™ฉ: ๋Œ€๊ธฐ์ค‘={status['queue_size']}, " f"์‹คํ–‰์ค‘={status['running_tasks']}, " f"์™„๋ฃŒ๋จ={status['completed_tasks']}") # ์ตœ์ข… ๊ฒฐ๊ณผ final_status = scheduler.get_status() print(f"\n๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ!") print(f"ํ†ต๊ณ„: {final_status['stats']}") # ์‹คํŒจํ•œ ์ž‘์—… ํ™•์ธ failed_tasks = [t for t in final_status['task_details']['completed'] if t.status == TaskStatus.FAILED] if failed_tasks: print(f"์‹คํŒจํ•œ ์ž‘์—… {len(failed_tasks)}๊ฐœ:") for task in failed_tasks: print(f" - {task.id}: {task.error}") finally: # ์ •๋ฆฌ await scheduler.stop_processing() asyncio.run(demo_batch_processing()) ``` --- ## ๐Ÿ› ๏ธ ์‹ค์ œ ๊ตฌํ˜„ ์˜ˆ์ œ ### ๊ฒŒ์ž„ ์ŠคํŠœ๋””์˜ค ์›Œํฌํ”Œ๋กœ์šฐ ```python class GameStudioWorkflow: """๊ฒŒ์ž„ ์ŠคํŠœ๋””์˜ค๋ฅผ ์œ„ํ•œ ์™„์ „ํ•œ AI ์›Œํฌํ”Œ๋กœ์šฐ""" def __init__(self): self.client = OptimizedUnrealClient() self.scheduler = SmartBatchScheduler(max_concurrent=5) self.factory = BlueprintFactory(self.scheduler) self.nlp_pipeline = UnrealCommandPipeline() self.error_handler = UnrealMCPErrorHandler() async def initialize(self): """์›Œํฌํ”Œ๋กœ์šฐ ์ดˆ๊ธฐํ™”""" await self.client.initialize() await self.scheduler.start_processing() async def process_design_document(self, design_doc: Dict[str, Any]) -> Dict[str, Any]: """๋””์ž์ธ ๋ฌธ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ž๋™ ์ƒ์„ฑ""" try: results = {} # ์บ๋ฆญํ„ฐ ์‹œ์Šคํ…œ ์ƒ์„ฑ if "characters" in design_doc: char_tasks = await self._create_character_system(design_doc["characters"]) results["characters"] = char_tasks # ๋ฌด๊ธฐ ์‹œ์Šคํ…œ ์ƒ์„ฑ if "weapons" in design_doc: weapon_tasks = await self._create_weapon_system(design_doc["weapons"]) results["weapons"] = weapon_tasks # ํ™˜๊ฒฝ ์‹œ์Šคํ…œ ์ƒ์„ฑ if "environment" in design_doc: env_tasks = await self._create_environment_system(design_doc["environment"]) results["environment"] = env_tasks # UI ์‹œ์Šคํ…œ ์ƒ์„ฑ if "ui" in design_doc: ui_tasks = await self._create_ui_system(design_doc["ui"]) results["ui"] = ui_tasks # ๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ await self.scheduler.wait_for_completion(timeout=300) # 5๋ถ„ ํƒ€์ž„์•„์›ƒ return { "success": True, "created_systems": list(results.keys()), "task_results": results, "final_status": self.scheduler.get_status() } except Exception as e: error_result = await self.error_handler.handle_error( e, {"operation": "process_design_document", "design_doc": design_doc} ) return { "success": False, "error": str(e), "error_details": error_result } async def _create_character_system(self, characters: List[Dict]) -> List[str]: """์บ๋ฆญํ„ฐ ์‹œ์Šคํ…œ ์ƒ์„ฑ""" task_ids = [] for char in characters: # ์บ๋ฆญํ„ฐ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ char_task = self.scheduler.submit_blueprint_creation( name=char["name"], parent_class="Character", priority=TaskPriority.HIGH ) task_ids.append(char_task) # ์Šคํƒฏ ์„ค์ • stats = char.get("stats", {}) for stat_name, stat_value in stats.items(): stat_task = self.scheduler.submit_property_update( blueprint_path=f"/Game/Characters/{char['name']}", property_name=stat_name, property_value=str(stat_value), dependencies=[char_task] ) task_ids.append(stat_task) # ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ (์žˆ๋Š” ๊ฒฝ์šฐ) if "animations" in char: anim_task = self.scheduler.submit_blueprint_creation( name=f"{char['name']}_AnimBP", parent_class="AnimBlueprint", asset_path="/Game/Animations/", dependencies=[char_task] ) task_ids.append(anim_task) return task_ids async def _create_weapon_system(self, weapons: List[Dict]) -> List[str]: """๋ฌด๊ธฐ ์‹œ์Šคํ…œ ์ƒ์„ฑ""" task_ids = [] for weapon in weapons: # ๋ฌด๊ธฐ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ weapon_task = self.scheduler.submit_blueprint_creation( name=weapon["name"], parent_class="Actor", asset_path="/Game/Weapons/" ) task_ids.append(weapon_task) # ๋ฌด๊ธฐ ์Šคํƒฏ ์„ค์ • stats = weapon.get("stats", {}) for stat_name, stat_value in stats.items(): stat_task = self.scheduler.submit_property_update( blueprint_path=f"/Game/Weapons/{weapon['name']}", property_name=stat_name, property_value=str(stat_value), dependencies=[weapon_task] ) task_ids.append(stat_task) return task_ids async def _create_environment_system(self, environment: Dict) -> List[str]: """ํ™˜๊ฒฝ ์‹œ์Šคํ…œ ์ƒ์„ฑ""" task_ids = [] # ํ™˜๊ฒฝ ์˜ค๋ธŒ์ ํŠธ๋“ค objects = environment.get("objects", []) for obj in objects: obj_task = self.scheduler.submit_blueprint_creation( name=obj["name"], parent_class=obj.get("type", "Actor"), asset_path="/Game/Environment/" ) task_ids.append(obj_task) return task_ids async def _create_ui_system(self, ui_specs: List[Dict]) -> List[str]: """UI ์‹œ์Šคํ…œ ์ƒ์„ฑ""" task_ids = [] for ui in ui_specs: ui_task = self.scheduler.submit_blueprint_creation( name=ui["name"], parent_class="UserWidget", asset_path="/Game/UI/" ) task_ids.append(ui_task) return task_ids async def interactive_blueprint_session(self): """๋Œ€ํ™”ํ˜• ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์„ธ์…˜""" print("=== ๋Œ€ํ™”ํ˜• Unreal Blueprint ์ƒ์„ฑ ์„ธ์…˜ ===") print("์ž์—ฐ์–ด๋กœ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ์„ ์š”์ฒญํ•˜์„ธ์š”. 'quit'๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค.") while True: try: # ์‚ฌ์šฉ์ž ์ž…๋ ฅ user_input = input("\n๋ช…๋ น> ").strip() if user_input.lower() in ['quit', 'exit', '์ข…๋ฃŒ']: print("์„ธ์…˜์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.") break if not user_input: continue # ์ž์—ฐ์–ด ๋ช…๋ น ์ฒ˜๋ฆฌ print("์ฒ˜๋ฆฌ ์ค‘...") result = await self.nlp_pipeline.execute_natural_command(user_input) if result["success"]: print(f"โœ… ์„ฑ๊ณต! {result['parsed_command']['action']} ์‹คํ–‰๋จ") print(f" ๊ฒฐ๊ณผ: {result['mcp_result']}") print(f" ์‹ ๋ขฐ๋„: {result.get('confidence', 0):.1f}") else: print(f"โŒ ์‹คํŒจ: {result.get('error', '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜')}") if "suggestion" in result: print(f" ์ œ์•ˆ: {result['suggestion']}") except KeyboardInterrupt: print("\n์„ธ์…˜์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.") break except Exception as e: print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}") async def cleanup(self): """๋ฆฌ์†Œ์Šค ์ •๋ฆฌ""" await self.scheduler.stop_processing() await self.client.cleanup() # ์‚ฌ์šฉ ์˜ˆ์‹œ async def main(): workflow = GameStudioWorkflow() await workflow.initialize() try: # ์˜ˆ์‹œ 1: ๋””์ž์ธ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ž๋™ ์ƒ์„ฑ design_document = { "characters": [ { "name": "Player", "stats": { "Health": 100, "Speed": 600, "JumpHeight": 420 } }, { "name": "Enemy", "stats": { "Health": 50, "Speed": 400, "Damage": 25 } } ], "weapons": [ { "name": "Pistol", "stats": { "Damage": 20, "FireRate": 0.5, "Range": 1000 } } ], "ui": [ {"name": "MainMenu"}, {"name": "HUD"}, {"name": "InventoryUI"} ] } print("๋””์ž์ธ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ž๋™ ์ƒ์„ฑ...") auto_result = await workflow.process_design_document(design_document) print(f"์ž๋™ ์ƒ์„ฑ ๊ฒฐ๊ณผ: {auto_result}") # ์˜ˆ์‹œ 2: ๋Œ€ํ™”ํ˜• ์„ธ์…˜ await workflow.interactive_blueprint_session() finally: await workflow.cleanup() if __name__ == "__main__": asyncio.run(main()) ``` --- ## ๐Ÿ“š Best Practices ### 1. ์—๋Ÿฌ ์ฒ˜๋ฆฌ Best Practices ```python # โœ… ์ข‹์€ ์˜ˆ์‹œ: ํฌ๊ด„์ ์ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ async def robust_blueprint_creation(): client = ResilientUnrealClient() try: result = await client.execute_command_with_recovery( "create_blueprint", { "blueprint_name": "PlayerCharacter", "parent_class": "Character" } ) if result["success"]: print("๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์„ฑ๊ณต") else: print(f"์ƒ์„ฑ ์‹คํŒจ: {result['error']}") except Exception as e: print(f"์˜ˆ์™ธ ๋ฐœ์ƒ: {e}") # โŒ ๋‚˜์œ ์˜ˆ์‹œ: ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์Œ async def fragile_blueprint_creation(): # ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, Unreal ์—ฐ๊ฒฐ ๋Š๊น€ ๋“ฑ์— ์ทจ์•ฝ async with websockets.connect("ws://localhost:6277") as ws: await ws.send(json.dumps({...})) response = await ws.recv() # ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ ์—†์Œ ``` ### 2. ์„ฑ๋Šฅ ์ตœ์ ํ™” Best Practices ```python # โœ… ์ข‹์€ ์˜ˆ์‹œ: ์—ฐ๊ฒฐ ํ’€๋ง๊ณผ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ async def optimized_mass_creation(): client = OptimizedUnrealClient() scheduler = SmartBatchScheduler(max_concurrent=5) await client.initialize() await scheduler.start_processing() # ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌ for i in range(100): scheduler.submit_blueprint_creation(f"Actor_{i}") await scheduler.wait_for_completion() # โŒ ๋‚˜์œ ์˜ˆ์‹œ: ์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋กœ ๋А๋ฆผ async def slow_mass_creation(): for i in range(100): # ๋งค๋ฒˆ ์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ - ๋น„ํšจ์œจ์  async with websockets.connect("ws://localhost:6277") as ws: # ... ๋‹จ์ผ ์ž‘์—… ์ฒ˜๋ฆฌ pass ``` ### 3. ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ Best Practices ```python # โœ… ์ข‹์€ ์˜ˆ์‹œ: ๋ช…ํ™•ํ•œ ์˜๋„ ํŒŒ์•…๊ณผ ๊ฒ€์ฆ async def smart_command_processing(): processor = NaturalLanguageProcessor() validator = CommandValidator() command = "Create a character blueprint named Hero with health 150" # 1. ํŒŒ์‹ฑ parsed = processor.parse_command(command) # 2. ๊ฒ€์ฆ validated = validator.validate_command(parsed) # 3. ํ™•์‹ ๋„ ํ™•์ธ if validated.get("confidence", 0) < 0.7: print("๋ช…๋ น์ด ๋ถˆ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.") return # 4. ์‹คํ–‰ if validated["validation"]["valid"]: await execute_mcp_command(validated) # โŒ ๋‚˜์œ ์˜ˆ์‹œ: ๋‹จ์ˆœ ํ‚ค์›Œ๋“œ ๋งค์นญ async def naive_command_processing(): command = "make something" if "create" in command or "make" in command: # ๊ฒ€์ฆ ์—†์ด ๋ฐ”๋กœ ์‹คํ–‰ - ์œ„ํ—˜ํ•จ await create_blueprint("Unknown") ``` ### 4. ๋ชจ๋‹ˆํ„ฐ๋ง Best Practices ```python # โœ… ์ข‹์€ ์˜ˆ์‹œ: ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง class MonitoredUnrealClient: def __init__(self): self.metrics = { "commands_executed": 0, "errors_occurred": 0, "avg_response_time": 0, "last_error": None } # ๋กœ๊น… ์„ค์ • logging.basicConfig(level=logging.INFO) self.logger = logging.getLogger(__name__) async def execute_with_monitoring(self, command, params): start_time = time.time() try: result = await self._execute_command(command, params) self.metrics["commands_executed"] += 1 # ์„ฑ๊ณต ๋กœ๊ทธ self.logger.info(f"Command {command} executed successfully") return result except Exception as e: self.metrics["errors_occurred"] += 1 self.metrics["last_error"] = str(e) # ์—๋Ÿฌ ๋กœ๊ทธ self.logger.error(f"Command {command} failed: {e}") raise finally: # ์‘๋‹ต ์‹œ๊ฐ„ ์ถ”์  response_time = time.time() - start_time self._update_avg_response_time(response_time) def get_health_status(self): """์‹œ์Šคํ…œ ๊ฑด๊ฐ• ์ƒํƒœ ํ™•์ธ""" error_rate = ( self.metrics["errors_occurred"] / max(1, self.metrics["commands_executed"]) ) if error_rate > 0.1: # 10% ์ด์ƒ ์—๋Ÿฌ์œจ return "unhealthy" elif self.metrics["avg_response_time"] > 5.0: # 5์ดˆ ์ด์ƒ ์‘๋‹ต return "slow" else: return "healthy" ``` ### 5. ํ…Œ์ŠคํŠธ Best Practices ```python import pytest import asyncio class TestUnrealBlueprintMCP: """ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ""" @pytest.fixture async def client(self): """ํ…Œ์ŠคํŠธ์šฉ ํด๋ผ์ด์–ธํŠธ ์„ค์ •""" client = OptimizedUnrealClient() await client.initialize() yield client await client.cleanup() @pytest.mark.asyncio async def test_blueprint_creation_success(self, client): """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ""" result = await client.execute_command( "create_blueprint", { "blueprint_name": "TestActor", "parent_class": "Actor" } ) assert result["success"] is True assert "TestActor" in result["blueprint_path"] @pytest.mark.asyncio async def test_blueprint_creation_failure(self, client): """๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ ์‹คํŒจ ํ…Œ์ŠคํŠธ""" result = await client.execute_command( "create_blueprint", { "blueprint_name": "", # ์ž˜๋ชป๋œ ์ด๋ฆ„ "parent_class": "InvalidClass" # ์ž˜๋ชป๋œ ํด๋ž˜์Šค } ) assert result["success"] is False assert "error" in result @pytest.mark.asyncio async def test_batch_processing_performance(self, client): """๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ""" scheduler = SmartBatchScheduler(max_concurrent=3) await scheduler.start_processing() start_time = time.time() # 10๊ฐœ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๋ฐฐ์น˜ ์ƒ์„ฑ for i in range(10): scheduler.submit_blueprint_creation(f"BatchTest_{i}") await scheduler.wait_for_completion(timeout=30) end_time = time.time() # 30์ดˆ ์ด๋‚ด ์™„๋ฃŒ๋˜์–ด์•ผ ํ•จ assert end_time - start_time < 30 status = scheduler.get_status() assert status["stats"]["total_completed"] == 10 await scheduler.stop_processing() @pytest.mark.asyncio async def test_natural_language_processing(self): """์ž์—ฐ์–ด ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ""" processor = NaturalLanguageProcessor() test_commands = [ ("Create an Actor blueprint named TestActor", "create_blueprint"), ("Set TestActor's health to 100", "set_blueprint_property"), ("Make a Character called Hero", "create_blueprint"), ] for command, expected_action in test_commands: result = processor.parse_command(command) assert result["action"] == expected_action assert result["confidence"] > 0.5 @pytest.mark.asyncio async def test_error_recovery(self, client): """์—๋Ÿฌ ๋ณต๊ตฌ ํ…Œ์ŠคํŠธ""" resilient_client = ResilientUnrealClient() # ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ with pytest.raises(Exception): # ์ž˜๋ชป๋œ ์„œ๋ฒ„ URL๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ bad_client = ResilientUnrealClient("ws://localhost:9999") await bad_client.execute_command_with_recovery( "create_blueprint", {"blueprint_name": "Test"} ) ``` --- ## ๐Ÿ“Š ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ ### ์‹œ์Šคํ…œ ์„ฑ๋Šฅ ์ง€ํ‘œ | ํ•ญ๋ชฉ | ๋‹จ์ผ ์‹คํ–‰ | ๋ฐฐ์น˜ ์‹คํ–‰ (10๊ฐœ) | ๋ฐฐ์น˜ ์‹คํ–‰ (100๊ฐœ) | |------|----------|----------------|-----------------| | **๋ธ”๋ฃจํ”„๋ฆฐํŠธ ์ƒ์„ฑ** | 0.3์ดˆ | 1.2์ดˆ | 8.5์ดˆ | | **์†์„ฑ ์ˆ˜์ •** | 0.1์ดˆ | 0.5์ดˆ | 3.2์ดˆ | | **์—ฐ๊ฒฐ ์„ค์ •** | 0.05์ดˆ | 0.05์ดˆ | 0.05์ดˆ | | **๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰** | 50MB | 75MB | 200MB | ### AI ๋ชจ๋ธ๋ณ„ ์„ฑ๋Šฅ ๋น„๊ต | AI ๋ชจ๋ธ | ๋ช…๋ น ์ดํ•ด ์ •ํ™•๋„ | ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ | ์—๋Ÿฌ ๋ณต๊ตฌ ๋Šฅ๋ ฅ | |---------|----------------|---------------|---------------| | **Claude Code** | 95% | 0.2์ดˆ | ์šฐ์ˆ˜ | | **GPT-4** | 92% | 0.8์ดˆ | ์ข‹์Œ | | **Gemini** | 88% | 1.2์ดˆ | ๋ณดํ†ต | | **Local LLM** | 75% | 2.5์ดˆ | ์ œํ•œ์  | --- ## ๐Ÿ”ฎ ๋กœ๋“œ๋งต ### v1.2 (๋‹ค์Œ ๋ฆด๋ฆฌ์Šค) - **ํ–ฅ์ƒ๋œ ์ž์—ฐ์–ด ์ดํ•ด**: ๋” ๋ณต์žกํ•œ ๋ช…๋ น ์ฒ˜๋ฆฌ - **Visual Scripting**: ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๋…ธ๋“œ ๊ทธ๋ž˜ํ”„ ํŽธ์ง‘ - **์‹ค์‹œ๊ฐ„ ํ˜‘์—…**: ์—ฌ๋Ÿฌ AI ์—์ด์ „ํŠธ ๋™์‹œ ์ž‘์—… ### v2.0 (Future) - **AI ์ฝ”๋“œ ์ƒ์„ฑ**: C++ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ - **๋ ˆ๋ฒจ ์—๋””ํ„ฐ ํ†ตํ•ฉ**: ์›”๋“œ ํŽธ์ง‘ ๊ธฐ๋Šฅ - **ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ**: ์›๊ฒฉ Unreal ์ธ์Šคํ„ด์Šค ์ œ์–ด --- ์ด ๋ฌธ์„œ๋Š” UnrealBlueprintMCP๋ฅผ ํ™œ์šฉํ•œ AI ๊ฐœ๋ฐœ ํŒจํ„ด์˜ ์™„์ „ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ๋œ ํŒจํ„ด๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ •์ ์ด๊ณ  ํšจ์œจ์ ์ธ AI-Unreal ํ†ตํ•ฉ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋” ์ž์„ธํ•œ ์ •๋ณด๋Š” [API Reference](API_REFERENCE.md)์™€ [Installation Guide](../INSTALLATION_GUIDE.md)๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BestDev/unreal_bp_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server