diff --git a/.a0proj/memory/embedding.json b/.a0proj/memory/embedding.json
new file mode 100644
index 000000000..4b4ceb4b4
--- /dev/null
+++ b/.a0proj/memory/embedding.json
@@ -0,0 +1 @@
+{"model_provider": "huggingface", "model_name": "sentence-transformers/all-MiniLM-L6-v2"}
\ No newline at end of file
diff --git a/.a0proj/memory/index.faiss b/.a0proj/memory/index.faiss
new file mode 100644
index 000000000..7cd655482
Binary files /dev/null and b/.a0proj/memory/index.faiss differ
diff --git a/.a0proj/memory/index.pkl b/.a0proj/memory/index.pkl
new file mode 100644
index 000000000..2d8ceed1f
Binary files /dev/null and b/.a0proj/memory/index.pkl differ
diff --git a/.a0proj/memory/knowledge_import.json b/.a0proj/memory/knowledge_import.json
new file mode 100644
index 000000000..41e022b24
--- /dev/null
+++ b/.a0proj/memory/knowledge_import.json
@@ -0,0 +1 @@
+{"/a0/knowledge/default/main/about/github_readme.md": {"file": "/a0/knowledge/default/main/about/github_readme.md", "checksum": "a926595b358b89980ef11546ee435b4b", "ids": ["4b9nmQfaEe", "xSj8XiaAlb"]}, "/a0/knowledge/default/main/about/installation.md": {"file": "/a0/knowledge/default/main/about/installation.md", "checksum": "e7da35be7021a6702ec890548379df00", "ids": ["0T6q1xBxz9", "A9ZHmDhMMK", "WKONuOM08J", "pH0UEgbb8O", "ofG6pnIqaJ", "6hgSxqrJGJ", "e8tRxLRsFA", "4Tteb39ipP"]}, "/a0/instruments/default/yt_download/yt_download.md": {"file": "/a0/instruments/default/yt_download/yt_download.md", "checksum": "6393d0328d9ff92dffdc934ed89c30c8", "ids": ["mp0QZoWh2l"]}}
\ No newline at end of file
diff --git a/.a0proj/project.json b/.a0proj/project.json
new file mode 100644
index 000000000..858292ecc
--- /dev/null
+++ b/.a0proj/project.json
@@ -0,0 +1 @@
+{"title": "UnityMLcreator", "description": "", "instructions": "# Project Context\n\n## Purpose\n\n**MLCreator** is a Unity-based multiplayer game development framework designed to achieve **10x improvement** in performance and development efficiency through AI-powered assistance and advanced optimization systems.\n\n### Core Goals\n- **AI-Driven Development**: Integration with Antigravity Editor (Gemini 3 Pro) and Serena MCP Server for intelligent code assistance\n- **Performance Optimization**: Custom C# framework providing memory management, caching, object pooling, and asset optimization\n- **Multiplayer Excellence**: Built on Unity Netcode for GameObjects with Game Creator integration\n- **Knowledge Management**: Foam-based neural map with 10,095+ interconnected documentation files\n- **Batch Processing**: Overnight task system for asset analysis, validation, and optimization\n\n## Tech Stack\n\n### Core Engine & Runtime\n- **Unity 6 (6000.2.13f1)** - Game engine (LTS version for stability)\n- **C# 9.0+** - Primary programming language (UTF-8 encoding required)\n- **Unity DOTS** (Data-Oriented Technology Stack) - High-performance systems\n - Unity.Entities (ECS framework)\n - Unity.Collections (Memory-efficient collections)\n - Unity.Mathematics (SIMD math operations)\n - Unity.Transforms (Transform system)\n - Unity.Physics (DOTS physics)\n - Unity.Burst (JIT compilation for performance)\n- **Unity Netcode for GameObjects 2.7.0** - Multiplayer networking\n - NetworkBehaviour/NetworkObject patterns\n - ServerRpc/ClientRpc communication\n - NetworkVariable synchronization\n - Distributed Authority support\n\n### Game Creator Framework\n- **Game Creator 2.0+** - Visual scripting and modular game systems\n - **18 installed modules**: Addressables, Behavior, Core, Dialogue, EditorPro, Factions, Finder, GameCreator_Multiplayer, Inventory, Localization, Mailbox, NanoSave, Perception, Platforming, Quests, Shooter, Stats, Tactile\n - **Custom multiplayer integration**: Invasive modification of core GameCreator files for Unity Netcode compatibility\n - **10,095+ documented components**: Auto-generated Foam knowledge base with comprehensive API documentation\n - **Visual Scripting**: Actions, Conditions, Triggers, and Events with multiplayer synchronization\n\n### AI & Developer Tools\n- **Antigravity Editor** - AI-powered code editor using Gemini 3 Pro (200K context window)\n- **Serena MCP Server** - Hierarchical memory management system with predictive preloading\n - Memory tiers: CRITICAL (must-read), CORE (foundation), INTEGRATION (framework-specific), TOOLS (development utilities)\n - Proactive intelligence with code health monitoring and dependency tracking\n - OpenSpec integration for unified workflow management\n- **Unity MCP Server** - Local stdio-based Unity integration (safe mode enabled)\n- **Foam** - Knowledge graph and neural mapping system\n - 10,095+ markdown documentation files (auto-generated from C# source)\n - Automated tagging and wikilink generation\n - Consolidated into docs/_knowledge-base/ for unified access\n- **OpenSpec** - Specification-driven development workflow\n - Change proposals, task tracking, and implementation management\n - Standards enforcement for Assemblies, folder structure, APIs, and MCP servers\n\n### Additional Libraries\n- **Unity TextMeshPro** - Advanced text rendering\n- **Unity Input System** - Modern input handling\n- **Unity Cinemachine** - Camera system\n- **Supabase** - Backend services integration (planned/in-progress)\n\n### Development Environment\n- **Python 3.x** - Automation scripts and tooling\n- **PowerShell** - Windows automation and task scheduling\n- **Git** - Version control\n- **Windows Task Scheduler** - Overnight batch processing\n\n## Project Conventions\n\n### Code Style\n\n#### C# Conventions\n- **Namespace Structure**: `MLCreator.{Module}.{Feature}` (UTF-8 encoding required)\n - Examples: `MLCreator.Framework`, `MLCreator.Core`, `MLCreator.Multiplayer`, `MLCreator.AI`\n - **Assembly Boundaries**: Strict namespace-to-assembly mapping enforced\n- **Naming Conventions**:\n - Classes: PascalCase (e.g., `MLCreatorInitializer`, `NetworkCharacterAdapter`)\n - Methods: PascalCase (e.g., `InitializeOptimizationSystems`, `RequestMovementServerRpc`)\n - Private fields: camelCase with `m_` prefix (e.g., `m_character`, `m_weapon`, `m_networkVariable`)\n - Properties: PascalCase (e.g., `Title`, `DefaultResult`, `IsNetworkSpawned`)\n - Network RPCs: `ServerRpc`/`ClientRpc` suffix (e.g., `SendDataServerRpc`)\n- **Performance Focus**: All code must consider performance implications\n - Memory pooling mandatory for frequently instantiated objects\n - DOTS/ECS patterns for performance-critical systems\n - Network traffic optimization required\n- **Singleton Pattern**: Core systems use singleton pattern for global access\n- **Data-Oriented Design**: Prefer DOTS and ECS patterns where applicable\n- **Network Architecture**: Server-authoritative with client prediction where appropriate\n\n#### Game Creator Integration\n- **Actions**: Prefixed with `Instruction` (e.g., `InstructionShooterFirePull`)\n- **Conditions**: Prefixed with `Condition` (e.g., `ConditionShooterIsReloading`)\n- **Triggers**: Prefixed with `Event` (e.g., `EventShooterWeaponFired`)\n- **Visual Scripting Nodes**: No CancellationToken in Task.Run() methods (compilation error)\n- **Attributes**: Use C# attributes for metadata and visual scripting integration\n ```csharp\n [Version(1, 0, 0)]\n [Title(\"Pull Fire Trigger\")]\n [Description(\"Pulls the fire trigger on a shooter weapon\")]\n [Category(\"Shooter/Shooting/Pull Fire Trigger\")]\n [Image(typeof(IconShooter), ColorTheme.Type.Red)]\n [Parameter(\"Character\", \"The Character reference with a Weapon equipped\")]\n [Keywords(\"Shooter\", \"Combat\", \"Shoot\", \"Execute\", \"Trigger\", \"Press\", \"Blast\")]\n ```\n- **Property Binding**: Use `PropertyGet*` types for variable binding (e.g., `PropertyGetGameObject`)\n- **Event System**: `EventChange` for reactive updates\n- **Invasive Integration**: Direct modification of GameCreator core files for Netcode compatibility\n\n### Architecture Patterns\n\n#### Core Architecture\n1. **Modular Systems**: Framework divided into specialized assemblies\n - `MLCreator.Core` - Core systems and managers\n - `MLCreator.Gameplay` - Game mechanics and features\n - `MLCreator.Multiplayer` - Network synchronization\n - `MLCreator.AI` - Artificial intelligence systems\n - `MLCreator.UI` - User interface components\n\n2. **Singleton Pattern**: Used for core initializer and system managers\n ```csharp\n public class MLCreatorInitializer : MonoBehaviour\n {\n private static MLCreatorInitializer instance;\n public static MLCreatorInitializer Instance => instance;\n\n // Core system initialization order\n private void Awake()\n {\n instance = this;\n InitializeMemoryManagement();\n InitializeNetworkSystems();\n InitializeGameCreatorIntegration();\n }\n }\n ```\n\n3. **Data-Oriented Design**: Utilize Unity DOTS for high-performance systems\n - ECS (Entity Component System) for game logic\n - Job System for parallel processing\n - Burst Compiler for optimized code\n - Native collections for memory efficiency\n\n4. **Multiplayer Architecture**:\n - **NetworkBehaviour** components for synchronized objects\n - **Server-authoritative** design with client prediction\n - **NetworkCharacterAdapter** for GameCreator character sync\n - **Invasive GameCreator integration** for seamless multiplayer\n - **RPC patterns**: ServerRpc/ClientRpc with proper ownership checks\n\n5. **Batch Processing**:\n - JSON-based task queue configuration (`claudedocs/overnight-tasks/queue.json`)\n - Overnight processing at 2 AM via Windows Task Scheduler\n - Results stored in `claudedocs/overnight-results/`\n - Asset analysis, prefab validation, and optimization tasks\n\n### Testing Strategy\n\n#### Manual Testing\n- Unity Editor Play Mode testing\n- **MLCreator Menu Items**:\n - `MLCreator → Unity MCP → Check Server Health`\n - `MLCreator → Batch Processing → Process Overnight Tasks`\n\n#### Automated Testing\n- Overnight batch validation (asset analysis, prefab validation)\n- Network object audits\n- Script reference mapping\n\n#### Performance Testing\n- Frame rate monitoring\n- Memory profiling\n- Asset optimization metrics\n\n### Git Workflow\n\n#### Branch Strategy\n- **main** - Production-ready code\n- **develop** - Integration branch\n- **feature/** - Feature development branches\n- **hotfix/** - Emergency fixes\n\n#### Commit Conventions\n- Use descriptive commit messages\n- Reference related documentation updates\n- Include performance impact notes when relevant\n- Reference OpenSpec change IDs when applicable\n\n### OpenSpec Workflow\n\n#### Change Categories\n- **Assembly Creation**: Assembly definitions, references, compilation settings\n- **Folder Reorganization**: Project structure optimization and organization\n- **API Integration**: External service integration following best practices\n- **MCP Server Setup**: MCP server configuration and tooling integration\n\n#### Proposal Process\n1. **Create Proposal**: Use `openspec` CLI or templates\n2. **Standards Validation**: Automatic compliance checking\n3. **Implementation**: Follow specification-driven development\n4. **Archive**: Move completed changes to archive with proper versioning\n\n#### Standards Enforcement\n- **Assembly**: Namespace patterns, compilation settings, reference validation\n- **Folders**: Functional grouping, scalability patterns, dependency flow\n- **APIs**: Error handling, security validation, performance monitoring\n- **MCP**: Authentication standards, tool validation, integration testing\n\n## Domain Context\n\n### Unity Game Development\n- **Multiplayer Game**: Built with Unity Netcode for GameObjects\n- **Visual Scripting**: Extensive use of Game Creator 2 framework\n- **Performance Critical**: Focus on optimization and efficient resource usage\n\n### AI-Assisted Development\n- **Antigravity Integration**: AI-powered editor using Gemini 3 Pro\n- **Serena MCP**: Contextual AI assistant with project knowledge\n- **Foam Knowledge Graph**: 10,095+ interconnected documentation files\n - Auto-generated from C# source code\n - Comprehensive Game Creator module documentation\n - Wikilink-based navigation\n\n### Specialized Systems\n- **Memory Management**: Custom pooling and caching\n- **Asset Optimization**: Automated analysis and recommendations\n- **Network Synchronization**: Multiplayer state management\n- **Batch Processing**: Overnight task execution for long-running operations\n\n## Important Constraints\n\n### Technical Constraints\n1. **Unity Version**: Must maintain compatibility with Unity 6 (6000.0+)\n2. **Performance Target**: Maintain 60+ FPS in multiplayer scenarios\n3. **Platform**: Primary target is Windows (x64)\n4. **Netcode Compatibility**: All multiplayer code must be compatible with Unity Netcode for GameObjects\n5. **DOTS Integration**: Performance-critical systems should use DOTS when possible\n6. **Memory Budget**: Optimized for efficient memory usage through pooling and caching\n\n### Development Constraints\n1. **AI Integration**: All major systems must be documented for AI assistant understanding\n2. **Foam Documentation**: Code changes require corresponding Foam documentation updates\n3. **Batch Processing**: Long-running tasks (>5 minutes) should use overnight processing\n4. **MCP Server**: Local Unity MCP Server must be accessible via stdio\n\n### Business Constraints\n1. **Performance Focus**: 10x improvement target across all optimization metrics\n2. **AI-First Development**: Prioritize AI-assisted workflows\n3. **Modular Design**: Systems must be independently testable and swappable\n\n## External Dependencies\n\n### AI Services\n- **Gemini 3 Pro API** - Powers Antigravity Editor\n- **Serena MCP Server** - Project context and memory management\n - Local Python server (`serena-env`)\n - Stdio communication protocol\n\n### Unity MCP Integration\n- **Unity MCP Server** - Local executable\n - Location: `Library/mcp-server/win-x64/unity-mcp-server.exe`\n - Communication: stdio\n - Configuration: `.antigravity/mcp_config.json`\n\n### Backend Services\n- **Supabase** (planned/in-progress)\n - Authentication\n - Real-time multiplayer data\n - Database services\n\n### Unity Asset Store Packages\n- **Game Creator 2** - Core framework + 18 modules\n - Addressables, Behavior, Core, Dialogue, EditorPro\n - Factions, Finder, GameCreator_Multiplayer, Inventory\n - Localization, Mailbox, NanoSave, Perception\n - Platforming, Quests, Shooter, Stats, Tactile\n\n### Development Tools\n- **Python Environment** (`serena-env`) - Automation scripting\n- **PowerShell** - Windows automation\n- **Windows Task Scheduler** - Overnight batch processing\n\n### Documentation System\n- **Foam** - Knowledge graph system\n - VSCode extension\n - Markdown-based\n - Wikilink navigation\n\n## Key Directory Structure\n\n```\nMLCreator/\n├── .antigravity/ # Antigravity Editor configuration\n│ └── mcp_config.json # MCP server configuration\n├── .foam/ # Foam workspace configuration\n│ └── foam.config.json # Foam settings (consolidated ~284 files)\n├── .serena/ # Serena AI memory system\n│ ├── config.yaml # Serena configuration\n│ ├── project.yml # Project-specific settings\n│ ├── ai/ # AI processing files\n│ ├── cache/ # Multi-tier caching system\n│ ├── logs/ # Health monitoring logs\n│ └── memories/ # Hierarchical memory tiers\n├── Assets/ # Unity project assets\n│ ├── MLCreator.asmdef # Assembly definition (MLCreator.*)\n│ ├── MLCreatorInitializer.cs # Core initializer (singleton pattern)\n│ ├── Plugins/\n│ │ └── GameCreator/ # Game Creator 2.0+ (18 modules)\n│ └── Resources/\n│ └── Unity-MCP-ConnectionConfig.json\n├── claudedocs/ # Claude AI documentation\n│ ├── overnight-tasks/ # Batch task configurations\n│ │ └── queue.json # Task queue (JSON-based)\n│ └── overnight-results/ # Batch processing results\n├── docs/ # Unified documentation system\n│ ├── _knowledge-base/ # Single source of truth (~40 files)\n│ │ ├── 01-critical/ # Must-read essentials\n│ │ ├── 02-guides/ # How-to documentation (13+ files)\n│ │ ├── 03-api-reference/ # Technical references (17+ files)\n│ │ ├── 04-architecture/ # System design\n│ │ └── 05-workflows/ # Process documentation\n│ └── _archive/ # Historical documentation (6,000+ files)\n├── foam/ # Foam knowledge base (10,095+ files)\n│ ├── gamecreator/ # Game Creator documentation\n│ │ ├── modules/ # Module overviews\n│ │ ├── actions/ # Action documentation\n│ │ ├── conditions/ # Condition documentation\n│ │ ├── triggers/ # Trigger documentation\n│ │ └── classes/ # Class documentation\n│ ├── templates/ # Foam templates\n│ └── scripts/ # Auto-generated content (5,754 files)\n├── Library/ # Unity Library (generated)\n│ └── mcp-server/ # Unity MCP Server\n│ └── win-x64/\n│ └── unity-mcp-server.exe\n├── openspec/ # OpenSpec specification system\n│ ├── AGENTS.md # AI assistant instructions\n│ ├── project.md # This file - project conventions\n│ ├── changes/ # Change proposals and tracking\n│ ├── specs/ # Technical specifications\n│ └── templates/ # Proposal templates (4 categories)\n├── scripts/ # Automation scripts\n│ ├── activate-environment.ps1 # Environment activation\n│ ├── generate-gamecreator-docs.py # Foam documentation generation\n│ ├── foam-tag-generator.py # Automated tagging\n│ ├── run-overnight-tasks.ps1 # Batch processing\n│ ├── setup-task-scheduler.ps1 # Windows Task Scheduler setup\n│ └── validate-foam-tags.ps1 # Tag validation\n├── serena-env/ # Python virtual environment (AI tooling)\n├── GEMINI.md # AI assistant context\n├── QUICK_START.md # Quick start guide\n└── README.md # Project overview\n```\n\n## Quick Reference\n\n### Essential Commands\n\n```powershell\n# Activate environment\n.\\activate-environment.ps1\n\n# Generate/update Foam documentation\npython scripts/generate-gamecreator-docs.py\n\n# Validate Foam tags\npowershell -ExecutionPolicy Bypass -File scripts/validate-foam-tags.ps1\n\n# Run overnight tasks manually\n.\\scripts\\run-overnight-tasks.ps1\n\n# Setup automated scheduling\n.\\scripts\\setup-task-scheduler.ps1\n```\n\n### Key Documentation\n- **Unified Knowledge Base**: `docs/_knowledge-base/` - Single source of truth\n- **Project Overview**: `GEMINI.md`\n- **Quick Start**: `QUICK_START.md`\n- **AI Mandatory Checklist**: `docs/_knowledge-base/01-critical/AI_MANDATORY_CHECKLIST.md`\n- **MCP Token Limits**: `docs/_knowledge-base/01-critical/MCP_TOKEN_LIMITS_GUIDE.md`\n- **OpenSpec Instructions**: `openspec/AGENTS.md`\n\n### AI Assistant Context\n- **Serena MCP**: Hierarchical memory management with predictive preloading\n- **Antigravity**: Gemini 3 Pro-powered editor (200K context window)\n- **Foam Graph**: 10,095+ interconnected documentation files (consolidated access)\n- **OpenSpec**: Specification-driven development with standards enforcement\n\n### Recent Major Updates (2025-11-20)\n- **Documentation Consolidation**: 6,140+ files → 40 essential files (99.2% reduction)\n- **Unified Knowledge Base**: Single source of truth at `docs/_knowledge-base/`\n- **OpenSpec Integration**: Complete workflow management for Assemblies/Folders/APIs/MCP\n- **Standards Enforcement**: Automated compliance checking for best practices\n- **Memory Optimization**: Hierarchical Serena system with proactive intelligence\n\n---\n\n**Last Updated**: 2025-11-20\n**Documentation Consolidation**: ✅ Complete (99.2% reduction in active files)\n**OpenSpec Integration**: ✅ Complete (standards enforcement active)\n**Knowledge Base**: ✅ Unified (docs/_knowledge-base/)\n**Status**: Active Development, AI-Optimized, Specification-Driven\n**Version**: MLCreator 10x Framework v2.0 (Post-Consolidation)\n", "color": "#7b2cbf", "memory": "own", "file_structure": {"enabled": true, "max_depth": 5, "max_files": 20, "max_folders": 20, "max_lines": 250, "gitignore": "# A0 project meta folder\n.a0proj/\n\n# Python environments & cache\nvenv/\n**/__pycache__/\n\n# Node.js dependencies\n**/node_modules/\n**/.npm/\n\n# Version control metadata\n**/.git/\n"}}
\ No newline at end of file
diff --git a/.a0proj/secrets.env b/.a0proj/secrets.env
new file mode 100644
index 000000000..e69de29bb
diff --git a/.a0proj/variables.env b/.a0proj/variables.env
new file mode 100644
index 000000000..e69de29bb
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/NetworkPlayerControllerEditor.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/NetworkPlayerControllerEditor.cs
new file mode 100644
index 000000000..a463ba55d
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/NetworkPlayerControllerEditor.cs
@@ -0,0 +1,526 @@
+using UnityEngine;
+using UnityEditor;
+using GameCreator.Runtime.Characters;
+using GameCreator.Multiplayer.Runtime.Components;
+using GameCreator.Multiplayer.Runtime.Core;
+
+namespace GameCreator.Multiplayer.Editor
+{
+ ///
+ /// Editor tools for NetworkPlayerController - the unified multiplayer character solution
+ ///
+ public static class NetworkPlayerControllerEditor
+ {
+ // ============================================================================================
+ // MENU ITEMS
+ // ============================================================================================
+
+ [MenuItem("GameObject/GameCreator/Multiplayer/Create Network Player", false, 10)]
+ public static void CreateNetworkPlayer()
+ {
+ // Create new GameObject
+ GameObject playerGO = new GameObject("NetworkPlayer");
+
+ // Add Character (required by GameCreator)
+ Character character = playerGO.AddComponent();
+
+ // Add NetworkObject (required by Netcode)
+ playerGO.AddComponent();
+
+ // Add NetworkPlayerController (our unified solution)
+ playerGO.AddComponent();
+
+ // Select the new object
+ Selection.activeGameObject = playerGO;
+
+ // Focus in scene view
+ SceneView.FrameLastActiveSceneView();
+
+ Debug.Log("[NetworkPlayerController] Created new Network Player. Add a model with Animator as child.");
+ EditorUtility.DisplayDialog(
+ "Network Player Created",
+ "A new Network Player has been created.\n\n" +
+ "Next steps:\n" +
+ "1. Add your character model as a child\n" +
+ "2. Ensure the model has an Animator with proper controller\n" +
+ "3. Configure module settings in the inspector\n" +
+ "4. Create a prefab for spawning",
+ "OK");
+ }
+
+ [MenuItem("GameCreator/Multiplayer/Setup Selected as Network Player", false, 100)]
+ public static void SetupSelectedAsNetworkPlayer()
+ {
+ GameObject selected = Selection.activeGameObject;
+
+ if (selected == null)
+ {
+ EditorUtility.DisplayDialog("No Selection", "Please select a GameObject to convert to a Network Player.", "OK");
+ return;
+ }
+
+ SetupAsNetworkPlayer(selected);
+ }
+
+ [MenuItem("GameCreator/Multiplayer/Validate Network Player Setup", false, 101)]
+ public static void ValidateSelectedNetworkPlayer()
+ {
+ GameObject selected = Selection.activeGameObject;
+
+ if (selected == null)
+ {
+ EditorUtility.DisplayDialog("No Selection", "Please select a Network Player to validate.", "OK");
+ return;
+ }
+
+ ValidateNetworkPlayer(selected);
+ }
+
+ [MenuItem("GameCreator/Multiplayer/Fix Animation Issues", false, 102)]
+ public static void FixAnimationIssues()
+ {
+ GameObject selected = Selection.activeGameObject;
+
+ if (selected == null)
+ {
+ EditorUtility.DisplayDialog("No Selection", "Please select a Network Player to fix.", "OK");
+ return;
+ }
+
+ FixAnimationSetup(selected);
+ }
+
+ [MenuItem("GameCreator/Multiplayer/Migrate from Legacy Components", false, 103)]
+ public static void MigrateFromLegacy()
+ {
+ GameObject selected = Selection.activeGameObject;
+
+ if (selected == null)
+ {
+ EditorUtility.DisplayDialog("No Selection", "Please select a character to migrate.", "OK");
+ return;
+ }
+
+ MigrateLegacyComponents(selected);
+ }
+
+ // ============================================================================================
+ // SETUP METHODS
+ // ============================================================================================
+
+ public static void SetupAsNetworkPlayer(GameObject target)
+ {
+ Undo.RecordObject(target, "Setup Network Player");
+
+ bool modified = false;
+
+ // 1. Ensure Character component
+ Character character = target.GetComponent();
+ if (character == null)
+ {
+ character = Undo.AddComponent(target);
+ Debug.Log($"[Setup] Added Character to {target.name}");
+ modified = true;
+ }
+
+ // 2. Ensure NetworkObject
+ var networkObject = target.GetComponent();
+ if (networkObject == null)
+ {
+ networkObject = Undo.AddComponent(target);
+ Debug.Log($"[Setup] Added NetworkObject to {target.name}");
+ modified = true;
+ }
+
+ // 3. Ensure NetworkPlayerController (our unified solution)
+ var playerController = target.GetComponent();
+ if (playerController == null)
+ {
+ playerController = Undo.AddComponent(target);
+ Debug.Log($"[Setup] Added NetworkPlayerController to {target.name}");
+ modified = true;
+ }
+
+ // 4. Check for and warn about legacy components
+ RemoveLegacyComponentsWithWarning(target);
+
+ // 5. Validate animator
+ Animator animator = target.GetComponentInChildren();
+ if (animator == null)
+ {
+ Debug.LogWarning($"[Setup] No Animator found on {target.name} or children. Animation sync will not work.");
+ }
+
+ if (modified)
+ {
+ EditorUtility.SetDirty(target);
+
+ EditorUtility.DisplayDialog(
+ "Network Player Setup Complete",
+ $"{target.name} has been configured as a Network Player.\n\n" +
+ "Components added:\n" +
+ "- NetworkObject\n" +
+ "- NetworkPlayerController\n\n" +
+ "The NetworkPlayerController will automatically add:\n" +
+ "- NetworkCharacterAdapter (motion sync)\n" +
+ "- NetworkGameCreatorAnimator (animation sync)\n\n" +
+ "Optional modules (Stats, Inventory, Perception) will be auto-detected at runtime.",
+ "OK");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog(
+ "Already Configured",
+ $"{target.name} is already set up as a Network Player.",
+ "OK");
+ }
+ }
+
+ public static void ValidateNetworkPlayer(GameObject target)
+ {
+ var issues = new System.Collections.Generic.List();
+ var warnings = new System.Collections.Generic.List();
+ var info = new System.Collections.Generic.List();
+
+ // Check required components
+ if (target.GetComponent() == null)
+ issues.Add("Missing Character component (REQUIRED)");
+ else
+ info.Add("Character: OK");
+
+ if (target.GetComponent() == null)
+ issues.Add("Missing NetworkObject component (REQUIRED)");
+ else
+ info.Add("NetworkObject: OK");
+
+ if (target.GetComponent() == null)
+ warnings.Add("Missing NetworkPlayerController (recommended unified solution)");
+ else
+ info.Add("NetworkPlayerController: OK");
+
+ // Check animator
+ Animator animator = target.GetComponentInChildren();
+ if (animator == null)
+ {
+ warnings.Add("No Animator found - animation sync disabled");
+ }
+ else if (animator.runtimeAnimatorController == null)
+ {
+ warnings.Add("Animator has no controller assigned");
+ }
+ else
+ {
+ info.Add("Animator: OK");
+ }
+
+ // Check for legacy/problematic components
+ var legacyV2 = target.GetComponent();
+ if (legacyV2 != null)
+ {
+ issues.Add("NetworkGameCreatorCharacterV2 present - this disables Character and breaks animation!");
+ }
+
+ // Check Character enabled state
+ var character = target.GetComponent();
+ if (character != null && !character.enabled)
+ {
+ issues.Add("Character component is DISABLED - animation will not work!");
+ }
+
+ // Check for motion sync
+ if (target.GetComponent() == null)
+ {
+ warnings.Add("NetworkCharacterAdapter missing (will be added at runtime by NetworkPlayerController)");
+ }
+ else
+ {
+ info.Add("NetworkCharacterAdapter: OK");
+ }
+
+ // Check for animator sync
+ if (animator != null && target.GetComponent() == null)
+ {
+ warnings.Add("NetworkGameCreatorAnimator missing (will be added at runtime)");
+ }
+ else if (animator != null)
+ {
+ info.Add("NetworkGameCreatorAnimator: OK");
+ }
+
+ // Build report
+ string report = $"=== Validation Report for {target.name} ===\n\n";
+
+ if (issues.Count > 0)
+ {
+ report += "ERRORS:\n";
+ foreach (var issue in issues)
+ report += $" [X] {issue}\n";
+ report += "\n";
+ }
+
+ if (warnings.Count > 0)
+ {
+ report += "WARNINGS:\n";
+ foreach (var warning in warnings)
+ report += $" [!] {warning}\n";
+ report += "\n";
+ }
+
+ report += "STATUS:\n";
+ foreach (var item in info)
+ report += $" [OK] {item}\n";
+
+ // Show dialog
+ if (issues.Count > 0)
+ {
+ EditorUtility.DisplayDialog("Validation Failed", report, "OK");
+ Debug.LogError(report);
+ }
+ else if (warnings.Count > 0)
+ {
+ EditorUtility.DisplayDialog("Validation Passed (with warnings)", report, "OK");
+ Debug.LogWarning(report);
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("Validation Passed", report, "OK");
+ Debug.Log(report);
+ }
+ }
+
+ public static void FixAnimationSetup(GameObject target)
+ {
+ Undo.RecordObject(target, "Fix Animation Setup");
+ bool fixedAnything = false;
+
+ // 1. Enable Character component
+ var character = target.GetComponent();
+ if (character != null && !character.enabled)
+ {
+ character.enabled = true;
+ Debug.Log($"[Fix] Enabled Character component on {target.name}");
+ fixedAnything = true;
+ }
+
+ // 2. Remove problematic V2 component
+ var legacyV2 = target.GetComponent();
+ if (legacyV2 != null)
+ {
+ Undo.DestroyObjectImmediate(legacyV2);
+ Debug.Log($"[Fix] Removed NetworkGameCreatorCharacterV2 from {target.name}");
+ fixedAnything = true;
+ }
+
+ // 3. Ensure NetworkPlayerController
+ var playerController = target.GetComponent();
+ if (playerController == null)
+ {
+ playerController = Undo.AddComponent(target);
+ Debug.Log($"[Fix] Added NetworkPlayerController to {target.name}");
+ fixedAnything = true;
+ }
+
+ // 4. Ensure NetworkCharacterAdapter
+ var adapter = target.GetComponent();
+ if (adapter == null)
+ {
+ adapter = Undo.AddComponent(target);
+ Debug.Log($"[Fix] Added NetworkCharacterAdapter to {target.name}");
+ fixedAnything = true;
+ }
+
+ // 5. Ensure NetworkGameCreatorAnimator (if has animator)
+ var animator = target.GetComponentInChildren();
+ if (animator != null)
+ {
+ var animatorSync = target.GetComponent();
+ if (animatorSync == null)
+ {
+ animatorSync = Undo.AddComponent();
+ Debug.Log($"[Fix] Added NetworkGameCreatorAnimator to {target.name}");
+ fixedAnything = true;
+ }
+
+ // Configure optimal settings
+ animatorSync.syncRate = 60f;
+ animatorSync.interpolationSpeed = 15f;
+ }
+
+ if (fixedAnything)
+ {
+ EditorUtility.SetDirty(target);
+ EditorUtility.DisplayDialog(
+ "Animation Issues Fixed",
+ $"Fixed animation setup on {target.name}.\n\n" +
+ "Changes made:\n" +
+ "- Character component enabled\n" +
+ "- Legacy V2 component removed (if present)\n" +
+ "- NetworkPlayerController added\n" +
+ "- NetworkCharacterAdapter added\n" +
+ "- NetworkGameCreatorAnimator configured (60Hz)",
+ "OK");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog(
+ "No Issues Found",
+ $"Animation setup looks correct on {target.name}.",
+ "OK");
+ }
+ }
+
+ public static void MigrateLegacyComponents(GameObject target)
+ {
+ Undo.RecordObject(target, "Migrate Legacy Components");
+
+ var migratedComponents = new System.Collections.Generic.List();
+
+ // Remove NetworkGameCreatorCharacterV2
+ var legacyV2 = target.GetComponent();
+ if (legacyV2 != null)
+ {
+ Undo.DestroyObjectImmediate(legacyV2);
+ migratedComponents.Add("NetworkGameCreatorCharacterV2 -> Removed");
+ }
+
+ // Add NetworkPlayerController if missing
+ var playerController = target.GetComponent();
+ if (playerController == null)
+ {
+ playerController = Undo.AddComponent(target);
+ migratedComponents.Add("Added NetworkPlayerController (unified solution)");
+ }
+
+ // Add NetworkCharacterAdapter if missing
+ var adapter = target.GetComponent();
+ if (adapter == null)
+ {
+ adapter = Undo.AddComponent(target);
+ migratedComponents.Add("Added NetworkCharacterAdapter (motion sync)");
+ }
+
+ // Enable Character
+ var character = target.GetComponent();
+ if (character != null && !character.enabled)
+ {
+ character.enabled = true;
+ migratedComponents.Add("Enabled Character component");
+ }
+
+ if (migratedComponents.Count > 0)
+ {
+ EditorUtility.SetDirty(target);
+
+ string report = $"Migration complete for {target.name}:\n\n";
+ foreach (var item in migratedComponents)
+ report += $"- {item}\n";
+
+ EditorUtility.DisplayDialog("Migration Complete", report, "OK");
+ Debug.Log(report);
+ }
+ else
+ {
+ EditorUtility.DisplayDialog(
+ "No Migration Needed",
+ $"{target.name} has no legacy components to migrate.",
+ "OK");
+ }
+ }
+
+ private static void RemoveLegacyComponentsWithWarning(GameObject target)
+ {
+ var legacyV2 = target.GetComponent();
+ if (legacyV2 != null)
+ {
+ bool remove = EditorUtility.DisplayDialog(
+ "Legacy Component Detected",
+ "NetworkGameCreatorCharacterV2 was found on this object.\n\n" +
+ "This component disables the Character and breaks animation.\n\n" +
+ "Would you like to remove it?",
+ "Remove (Recommended)",
+ "Keep");
+
+ if (remove)
+ {
+ Undo.DestroyObjectImmediate(legacyV2);
+ Debug.Log($"[Setup] Removed legacy NetworkGameCreatorCharacterV2 from {target.name}");
+ }
+ }
+ }
+ }
+
+ // ============================================================================================
+ // CUSTOM INSPECTOR
+ // ============================================================================================
+
+ [CustomEditor(typeof(NetworkPlayerController))]
+ public class NetworkPlayerControllerInspector : UnityEditor.Editor
+ {
+ private bool showDebugInfo = false;
+
+ public override void OnInspectorGUI()
+ {
+ NetworkPlayerController controller = (NetworkPlayerController)target;
+
+ // Header
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Network Player Controller", EditorStyles.boldLabel);
+ EditorGUILayout.HelpBox(
+ "Unified multiplayer solution for GameCreator characters.\n" +
+ "Coordinates: Motion, Animation, Stats, Inventory, Perception.",
+ MessageType.Info);
+ EditorGUILayout.Space();
+
+ // Validation button
+ if (GUILayout.Button("Validate Setup", GUILayout.Height(30)))
+ {
+ NetworkPlayerControllerEditor.ValidateNetworkPlayer(controller.gameObject);
+ }
+
+ EditorGUILayout.Space();
+
+ // Draw default inspector
+ DrawDefaultInspector();
+
+ EditorGUILayout.Space();
+
+ // Quick actions
+ EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("Fix Animation"))
+ {
+ NetworkPlayerControllerEditor.FixAnimationSetup(controller.gameObject);
+ }
+ if (GUILayout.Button("Migrate Legacy"))
+ {
+ NetworkPlayerControllerEditor.MigrateLegacyComponents(controller.gameObject);
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space();
+
+ // Debug section
+ showDebugInfo = EditorGUILayout.Foldout(showDebugInfo, "Debug Info");
+ if (showDebugInfo && Application.isPlaying)
+ {
+ EditorGUI.indentLevel++;
+ EditorGUILayout.LabelField($"Is Initialized: {controller.IsInitialized}");
+ EditorGUILayout.LabelField($"Player Name: {controller.PlayerName}");
+ EditorGUILayout.LabelField($"Is Ready: {controller.IsReady}");
+ EditorGUILayout.LabelField($"Team ID: {controller.TeamId}");
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Components:", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField($"Character: {(controller.Character != null ? "OK" : "Missing")}");
+ EditorGUILayout.LabelField($"CharacterAdapter: {(controller.CharacterAdapter != null ? "OK" : "Missing")}");
+ EditorGUILayout.LabelField($"AnimatorSync: {(controller.AnimatorSync != null ? "OK" : "Missing")}");
+ EditorGUILayout.LabelField($"TraitsAdapter: {(controller.TraitsAdapter != null ? "OK" : "Missing")}");
+ EditorGUILayout.LabelField($"InventorySync: {(controller.InventorySync != null ? "OK" : "Missing")}");
+ EditorGUILayout.LabelField($"PerceptionSync: {(controller.PerceptionSync != null ? "OK" : "Missing")}");
+
+ EditorGUI.indentLevel--;
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/SetupPlayerNetworkPrefab.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/SetupPlayerNetworkPrefab.cs
new file mode 100644
index 000000000..04c9a6278
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Editor/SetupPlayerNetworkPrefab.cs
@@ -0,0 +1,300 @@
+using UnityEngine;
+using UnityEditor;
+using Unity.Netcode;
+using GameCreator.Runtime.Characters;
+using GameCreator.Runtime.Stats;
+using GameCreator.Runtime.Inventory;
+using GameCreator.Runtime.Perception;
+using GameCreator.Multiplayer.Runtime.Components;
+
+namespace GameCreator.Multiplayer.Editor
+{
+ ///
+ /// Utility to automatically configure the Player_Network prefab with all necessary multiplayer components
+ ///
+ /// USAGE:
+ /// 1. In Unity Editor, go to menu: GameCreator/Multiplayer/Setup Player Network Prefab
+ /// 2. Script will automatically add all required components
+ /// 3. Prefab will be saved automatically
+ ///
+ /// COMPONENTS ADDED:
+ /// - NetworkCombatAdapter (Combat/Weapons sync)
+ /// - NetworkTraitsAdapter (Stats/HP/MP sync)
+ /// - Traits (Stats module - HP, MP, Stamina, Stats)
+ /// - Bag (Inventory module - Items, Equipment, Currency)
+ /// - Perception (Awareness system)
+ ///
+ public class SetupPlayerNetworkPrefab : EditorWindow
+ {
+ private const string PREFAB_PATH = "Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Player/Player_Network.prefab";
+
+ [MenuItem("GameCreator/Multiplayer/Setup Player Network Prefab")]
+ public static void SetupPrefab()
+ {
+ // Load the prefab
+ GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(PREFAB_PATH);
+
+ if (prefabAsset == null)
+ {
+ Debug.LogError($"[SetupPlayerNetworkPrefab] Could not find prefab at: {PREFAB_PATH}");
+ return;
+ }
+
+ Debug.Log("[SetupPlayerNetworkPrefab] Starting prefab setup...");
+
+ // Open prefab in isolation mode
+ string prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
+ GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
+
+ try
+ {
+ // Verify required base components exist
+ if (!VerifyBaseComponents(prefabRoot))
+ {
+ Debug.LogError("[SetupPlayerNetworkPrefab] Prefab is missing base components!");
+ return;
+ }
+
+ // Add multiplayer network adapters
+ AddNetworkCombatAdapter(prefabRoot);
+ AddNetworkTraitsAdapter(prefabRoot);
+
+ // Add GameCreator gameplay components
+ AddTraitsComponent(prefabRoot);
+ AddBagComponent(prefabRoot);
+ AddPerceptionComponent(prefabRoot);
+
+ // Save the modified prefab
+ PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
+ Debug.Log($"[SetupPlayerNetworkPrefab] ✅ Prefab setup complete! Saved to: {prefabPath}");
+
+ // Show summary
+ ShowSetupSummary(prefabRoot);
+ }
+ finally
+ {
+ // Always unload prefab contents
+ PrefabUtility.UnloadPrefabContents(prefabRoot);
+ }
+
+ // Refresh AssetDatabase
+ AssetDatabase.Refresh();
+ }
+
+ ///
+ /// Verify that base networking components exist
+ ///
+ private static bool VerifyBaseComponents(GameObject prefabRoot)
+ {
+ bool isValid = true;
+
+ // Check for NetworkObject
+ NetworkObject networkObject = prefabRoot.GetComponent();
+ if (networkObject == null)
+ {
+ Debug.LogError("[SetupPlayerNetworkPrefab] ❌ Missing NetworkObject component!");
+ isValid = false;
+ }
+ else
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ✓ NetworkObject found");
+ }
+
+ // Check for Character
+ Character character = prefabRoot.GetComponent();
+ if (character == null)
+ {
+ Debug.LogError("[SetupPlayerNetworkPrefab] ❌ Missing Character component!");
+ isValid = false;
+ }
+ else
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ✓ Character found");
+ }
+
+ // Check for NetworkCharacterAdapter
+ NetworkCharacterAdapter networkCharacterAdapter = prefabRoot.GetComponent();
+ if (networkCharacterAdapter == null)
+ {
+ Debug.LogError("[SetupPlayerNetworkPrefab] ❌ Missing NetworkCharacterAdapter component!");
+ isValid = false;
+ }
+ else
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ✓ NetworkCharacterAdapter found");
+ }
+
+ return isValid;
+ }
+
+ ///
+ /// Add NetworkCombatAdapter for combat/weapon synchronization
+ ///
+ private static void AddNetworkCombatAdapter(GameObject prefabRoot)
+ {
+ NetworkCombatAdapter existing = prefabRoot.GetComponent();
+ if (existing != null)
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ NetworkCombatAdapter already exists, skipping");
+ return;
+ }
+
+ NetworkCombatAdapter adapter = prefabRoot.AddComponent();
+ Debug.Log("[SetupPlayerNetworkPrefab] ✅ Added NetworkCombatAdapter");
+
+ // Configure default settings (optional)
+ SerializedObject so = new SerializedObject(adapter);
+ so.FindProperty("syncWeaponState").boolValue = true;
+ so.FindProperty("syncAiming").boolValue = true;
+ so.FindProperty("syncBlocking").boolValue = true;
+ so.FindProperty("serverAuthoritativeDamage").boolValue = true;
+ so.FindProperty("debugMode").boolValue = false;
+ so.ApplyModifiedProperties();
+ }
+
+ ///
+ /// Add NetworkTraitsAdapter for stats/health synchronization
+ ///
+ private static void AddNetworkTraitsAdapter(GameObject prefabRoot)
+ {
+ NetworkTraitsAdapter existing = prefabRoot.GetComponent();
+ if (existing != null)
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ NetworkTraitsAdapter already exists, skipping");
+ return;
+ }
+
+ NetworkTraitsAdapter adapter = prefabRoot.AddComponent();
+ Debug.Log("[SetupPlayerNetworkPrefab] ✅ Added NetworkTraitsAdapter");
+
+ // Configure default settings
+ SerializedObject so = new SerializedObject(adapter);
+ so.FindProperty("serverAuthority").boolValue = true;
+ so.FindProperty("attributeSyncRate").floatValue = 10f; // 10Hz for HP/MP
+ so.FindProperty("statSyncRate").floatValue = 2f; // 2Hz for stats
+ so.FindProperty("debugMode").boolValue = false;
+ so.ApplyModifiedProperties();
+ }
+
+ ///
+ /// Add Traits component for stats (HP, MP, Stamina, etc.)
+ ///
+ private static void AddTraitsComponent(GameObject prefabRoot)
+ {
+ Traits existing = prefabRoot.GetComponent();
+ if (existing != null)
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ Traits already exists, skipping");
+ return;
+ }
+
+ Traits traits = prefabRoot.AddComponent();
+ Debug.Log("[SetupPlayerNetworkPrefab] ✅ Added Traits component");
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ MANUAL STEP REQUIRED: Assign a Class asset to Traits component!");
+ }
+
+ ///
+ /// Add Bag component for inventory (items, equipment, currency)
+ ///
+ private static void AddBagComponent(GameObject prefabRoot)
+ {
+ Bag existing = prefabRoot.GetComponent();
+ if (existing != null)
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ Bag already exists, skipping");
+ return;
+ }
+
+ Bag bag = prefabRoot.AddComponent();
+ Debug.Log("[SetupPlayerNetworkPrefab] ✅ Added Bag component");
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ MANUAL STEP REQUIRED: Configure Bag settings (capacity, shape, etc.)");
+ }
+
+ ///
+ /// Add Perception component for awareness system
+ ///
+ private static void AddPerceptionComponent(GameObject prefabRoot)
+ {
+ // Check if Perception module is installed
+ System.Type perceptionType = System.Type.GetType("GameCreator.Runtime.Perception.Perception, GameCreator.Runtime.Perception");
+ if (perceptionType == null)
+ {
+ Debug.LogWarning("[SetupPlayerNetworkPrefab] ⚠ Perception module not installed, skipping Perception component");
+ return;
+ }
+
+ Component existing = prefabRoot.GetComponent(perceptionType);
+ if (existing != null)
+ {
+ Debug.Log("[SetupPlayerNetworkPrefab] ⚠ Perception already exists, skipping");
+ return;
+ }
+
+ Component perception = prefabRoot.AddComponent(perceptionType);
+ Debug.Log("[SetupPlayerNetworkPrefab] ✅ Added Perception component");
+ }
+
+ ///
+ /// Show setup summary
+ ///
+ private static void ShowSetupSummary(GameObject prefabRoot)
+ {
+ Debug.Log("===========================================");
+ Debug.Log("PLAYER_NETWORK PREFAB SETUP SUMMARY");
+ Debug.Log("===========================================");
+
+ Debug.Log("\n✅ NETWORK COMPONENTS:");
+ Debug.Log($" - NetworkObject: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+ Debug.Log($" - NetworkCharacterAdapter: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+ Debug.Log($" - NetworkCombatAdapter: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+ Debug.Log($" - NetworkTraitsAdapter: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+
+ Debug.Log("\n✅ GAMECREATOR COMPONENTS:");
+ Debug.Log($" - Character: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+ Debug.Log($" - Traits: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+ Debug.Log($" - Bag: {(prefabRoot.GetComponent() != null ? "✓" : "✗")}");
+
+ System.Type perceptionType = System.Type.GetType("GameCreator.Runtime.Perception.Perception, GameCreator.Runtime.Perception");
+ if (perceptionType != null)
+ {
+ Debug.Log($" - Perception: {(prefabRoot.GetComponent(perceptionType) != null ? "✓" : "✗")}");
+ }
+ else
+ {
+ Debug.Log(" - Perception: (Module not installed)");
+ }
+
+ Debug.Log("\n⚠ MANUAL STEPS REQUIRED:");
+ Debug.Log(" 1. Open the Player_Network prefab");
+ Debug.Log(" 2. Assign a Class asset to the Traits component");
+ Debug.Log(" 3. Configure Bag settings (capacity, shape, equipment slots)");
+ Debug.Log(" 4. Test in multiplayer scene");
+
+ Debug.Log("\n===========================================");
+ }
+
+ [MenuItem("GameCreator/Multiplayer/Verify Player Network Prefab")]
+ public static void VerifyPrefab()
+ {
+ GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(PREFAB_PATH);
+
+ if (prefabAsset == null)
+ {
+ Debug.LogError($"[VerifyPrefab] Could not find prefab at: {PREFAB_PATH}");
+ return;
+ }
+
+ string prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
+ GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
+
+ try
+ {
+ ShowSetupSummary(prefabRoot);
+ }
+ finally
+ {
+ PrefabUtility.UnloadPrefabContents(prefabRoot);
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCharacterAdapter.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCharacterAdapter.cs
index 68f3afe98..603373239 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCharacterAdapter.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCharacterAdapter.cs
@@ -558,8 +558,20 @@ public void ForceSetPosition(Vector3 position, Quaternion rotation)
// If this is a remote player, update interpolation targets
if (!IsOwner)
{
- networkPosition.Value = position;
- networkRotation.Value = rotation;
+ if (IsServer)
+ {
+ SendForceSetPositionToOwnerClientRpc(
+ position,
+ rotation,
+ new ClientRpcParams
+ {
+ Send = new ClientRpcSendParams
+ {
+ TargetClientIds = new[] { NetworkObject.OwnerClientId }
+ }
+ }
+ );
+ }
}
// Force GameCreator Character to update its position
@@ -576,6 +588,18 @@ public void ForceSetPosition(Vector3 position, Quaternion rotation)
}
}
+ [ClientRpc]
+ private void SendForceSetPositionToOwnerClientRpc(
+ Vector3 position,
+ Quaternion rotation,
+ ClientRpcParams clientRpcParams = default)
+ {
+ if (!IsOwner) return;
+
+ // Owner can safely update the NetworkVariables without write-permission errors
+ ForceSetPosition(position, rotation);
+ }
+
///
/// Applied by NetworkPlayerManager once the authoritative spawn transform is known.
///
@@ -681,5 +705,90 @@ private IEnumerator EnableControllerDelayed(CharacterController controller)
}
}
}
+
+ // ============================================================================================
+ // JUMP & DASH SYNCHRONIZATION (Phase 2)
+ // ============================================================================================
+
+ ///
+ /// Call this when the local player jumps to sync to all clients
+ ///
+ public void SyncJump(float force)
+ {
+ if (!IsOwner) return;
+
+ if (IsServer)
+ {
+ JumpClientRpc(force);
+ }
+ else
+ {
+ JumpServerRpc(force);
+ }
+ }
+
+ [ServerRpc]
+ private void JumpServerRpc(float force)
+ {
+ // Server receives jump request from client
+ JumpClientRpc(force);
+ }
+
+ [ClientRpc]
+ private void JumpClientRpc(float force)
+ {
+ // All clients (except owner) play jump animation
+ if (IsOwner) return;
+
+ if (character != null && character.Motion != null)
+ {
+ // Remote players: trigger jump visual only (no physics)
+ // The jump animation will be handled by the animation system
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCharacterAdapter] Remote player jump: {force}");
+ }
+ }
+ }
+
+ ///
+ /// Call this when the local player dashes to sync to all clients
+ ///
+ public void SyncDash(Vector3 direction, float speed, float duration)
+ {
+ if (!IsOwner) return;
+
+ if (IsServer)
+ {
+ DashClientRpc(direction, speed, duration);
+ }
+ else
+ {
+ DashServerRpc(direction, speed, duration);
+ }
+ }
+
+ [ServerRpc]
+ private void DashServerRpc(Vector3 direction, float speed, float duration)
+ {
+ // Server receives dash request from client
+ DashClientRpc(direction, speed, duration);
+ }
+
+ [ClientRpc]
+ private void DashClientRpc(Vector3 direction, float speed, float duration)
+ {
+ // All clients (except owner) play dash animation
+ if (IsOwner) return;
+
+ if (character != null && character.Motion != null)
+ {
+ // Remote players: trigger dash visual only (no physics)
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCharacterAdapter] Remote player dash: direction={direction}, speed={speed}");
+ }
+ }
+ }
}
}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCombatAdapter.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCombatAdapter.cs
new file mode 100644
index 000000000..9bd3332bb
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkCombatAdapter.cs
@@ -0,0 +1,550 @@
+using System;
+using Unity.Netcode;
+using UnityEngine;
+using GameCreator.Runtime.Characters;
+using GameCreator.Runtime.Common;
+
+namespace GameCreator.Multiplayer.Runtime.Components
+{
+ ///
+ /// Network adapter for GameCreator Combat system - PHASE 2
+ /// Synchronizes weapon state, combat actions, and damage events across the network
+ ///
+ /// ARCHITECTURE:
+ /// - NetworkVariables for continuous state (current weapon, aiming, blocking)
+ /// - RPCs for discrete events (equip, fire, hit, damage)
+ /// - Server authority for damage validation
+ ///
+ /// EXECUTION ORDER: Runs AFTER Character (-1) to ensure proper initialization
+ ///
+ [RequireComponent(typeof(Character))]
+ [RequireComponent(typeof(NetworkObject))]
+ [AddComponentMenu("GameCreator/Multiplayer/Network Combat Adapter")]
+ [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_FIRST)] // -50: Runs early but AFTER Character (-1)
+ public class NetworkCombatAdapter : NetworkBehaviour
+ {
+ // ============================================================================================
+ // SERIALIZABLE STRUCTS
+ // ============================================================================================
+
+ ///
+ /// Weapon state for network synchronization
+ ///
+ [Serializable]
+ public struct WeaponState : INetworkSerializable
+ {
+ public int WeaponId; // Hash of weapon IdString
+ public bool IsEquipped;
+ public bool IsAiming;
+ public bool IsCharging;
+ public float ChargeAmount;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ serializer.SerializeValue(ref WeaponId);
+ serializer.SerializeValue(ref IsEquipped);
+ serializer.SerializeValue(ref IsAiming);
+ serializer.SerializeValue(ref IsCharging);
+ serializer.SerializeValue(ref ChargeAmount);
+ }
+ }
+
+ ///
+ /// Hit data for damage validation
+ ///
+ [Serializable]
+ public struct HitData : INetworkSerializable
+ {
+ public ulong TargetNetworkId;
+ public Vector3 HitPoint;
+ public Vector3 HitNormal;
+ public float Damage;
+ public int WeaponId;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ serializer.SerializeValue(ref TargetNetworkId);
+ serializer.SerializeValue(ref HitPoint);
+ serializer.SerializeValue(ref HitNormal);
+ serializer.SerializeValue(ref Damage);
+ serializer.SerializeValue(ref WeaponId);
+ }
+ }
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ ///
+ /// Current weapon state - Owner authority
+ ///
+ private NetworkVariable m_NetworkWeaponState = new NetworkVariable(
+ new WeaponState { WeaponId = -1, IsEquipped = false },
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner
+ );
+
+ ///
+ /// Blocking state - Owner authority
+ ///
+ private NetworkVariable m_IsBlocking = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner
+ );
+
+ ///
+ /// Defense value - Owner authority (synced from Traits if using defense stat)
+ ///
+ private NetworkVariable m_CurrentDefense = new NetworkVariable(
+ 0f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner
+ );
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Combat Synchronization")]
+ [Tooltip("Synchronize weapon equip/unequip events")]
+ [SerializeField] private bool syncWeaponState = true;
+
+ [Tooltip("Synchronize aiming state")]
+ [SerializeField] private bool syncAiming = true;
+
+ [Tooltip("Synchronize blocking state")]
+ [SerializeField] private bool syncBlocking = true;
+
+ [Tooltip("Server validates all damage (prevents cheating)")]
+ [SerializeField] private bool serverAuthoritativeDamage = true;
+
+ [Header("Debug")]
+ [SerializeField] private bool debugMode = false;
+
+ // ============================================================================================
+ // COMPONENTS
+ // ============================================================================================
+
+ private Character character;
+ private Combat combat;
+
+ // Local tracking
+ private int currentWeaponId = -1;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ character = GetComponent();
+ if (character == null)
+ {
+ Debug.LogError("[NetworkCombatAdapter] Character component is missing!");
+ enabled = false;
+ return;
+ }
+
+ combat = character.Combat;
+ if (combat == null)
+ {
+ Debug.LogError("[NetworkCombatAdapter] Combat system is missing!");
+ enabled = false;
+ }
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] OnNetworkSpawn - IsOwner: {IsOwner}");
+ }
+
+ // Subscribe to combat events on owner
+ if (IsOwner && combat != null)
+ {
+ combat.EventEquip += OnLocalEquip;
+ combat.EventUnequip += OnLocalUnequip;
+ }
+
+ // Subscribe to network state changes on remote players
+ if (!IsOwner)
+ {
+ m_NetworkWeaponState.OnValueChanged += OnNetworkWeaponStateChanged;
+ m_IsBlocking.OnValueChanged += OnNetworkBlockingChanged;
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // Unsubscribe from combat events
+ if (IsOwner && combat != null)
+ {
+ combat.EventEquip -= OnLocalEquip;
+ combat.EventUnequip -= OnLocalUnequip;
+ }
+
+ // Unsubscribe from network state changes
+ if (!IsOwner)
+ {
+ m_NetworkWeaponState.OnValueChanged -= OnNetworkWeaponStateChanged;
+ m_IsBlocking.OnValueChanged -= OnNetworkBlockingChanged;
+ }
+
+ base.OnNetworkDespawn();
+ }
+
+ // ============================================================================================
+ // LOCAL COMBAT EVENTS (Owner Only)
+ // ============================================================================================
+
+ ///
+ /// Called when local player equips a weapon
+ ///
+ private void OnLocalEquip(IWeapon weapon, GameObject user)
+ {
+ if (!syncWeaponState) return;
+
+ int weaponId = weapon.GetHashCode(); // TODO: Use proper weapon ID from GameCreator
+ currentWeaponId = weaponId;
+
+ // Update network state
+ var state = m_NetworkWeaponState.Value;
+ state.WeaponId = weaponId;
+ state.IsEquipped = true;
+ m_NetworkWeaponState.Value = state;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Local weapon equipped: {weaponId}");
+ }
+
+ // Notify all clients
+ if (IsServer)
+ {
+ EquipWeaponClientRpc(weaponId);
+ }
+ else
+ {
+ EquipWeaponServerRpc(weaponId);
+ }
+ }
+
+ ///
+ /// Called when local player unequips a weapon
+ ///
+ private void OnLocalUnequip(IWeapon weapon, GameObject user)
+ {
+ if (!syncWeaponState) return;
+
+ currentWeaponId = -1;
+
+ // Update network state
+ var state = m_NetworkWeaponState.Value;
+ state.WeaponId = -1;
+ state.IsEquipped = false;
+ state.IsAiming = false;
+ state.IsCharging = false;
+ m_NetworkWeaponState.Value = state;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Local weapon unequipped");
+ }
+
+ // Notify all clients
+ if (IsServer)
+ {
+ UnequipWeaponClientRpc();
+ }
+ else
+ {
+ UnequipWeaponServerRpc();
+ }
+ }
+
+ // ============================================================================================
+ // NETWORK STATE CHANGE CALLBACKS (Remote Players Only)
+ // ============================================================================================
+
+ ///
+ /// Called when networked weapon state changes (remote players)
+ ///
+ private void OnNetworkWeaponStateChanged(WeaponState oldState, WeaponState newState)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Remote weapon state changed: {newState.WeaponId}, Equipped: {newState.IsEquipped}");
+ }
+
+ // Apply visual changes for remote player
+ // TODO: Trigger weapon equip/unequip animations
+ // TODO: Show/hide weapon props
+ }
+
+ ///
+ /// Called when networked blocking state changes (remote players)
+ ///
+ private void OnNetworkBlockingChanged(bool oldValue, bool newValue)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Remote blocking state changed: {newValue}");
+ }
+
+ // Apply visual changes for remote player
+ // TODO: Trigger blocking animations
+ }
+
+ // ============================================================================================
+ // RPCs - WEAPON EQUIP/UNEQUIP
+ // ============================================================================================
+
+ [ServerRpc]
+ private void EquipWeaponServerRpc(int weaponId)
+ {
+ // Server receives equip request from client
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Server received equip request: {weaponId}");
+ }
+
+ // Broadcast to all clients
+ EquipWeaponClientRpc(weaponId);
+ }
+
+ [ClientRpc]
+ private void EquipWeaponClientRpc(int weaponId)
+ {
+ // All clients (except owner) receive equip notification
+ if (IsOwner) return;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Client received equip notification: {weaponId}");
+ }
+
+ // TODO: Trigger equip animation
+ // TODO: Show weapon prop
+ }
+
+ [ServerRpc]
+ private void UnequipWeaponServerRpc()
+ {
+ // Server receives unequip request from client
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Server received unequip request");
+ }
+
+ // Broadcast to all clients
+ UnequipWeaponClientRpc();
+ }
+
+ [ClientRpc]
+ private void UnequipWeaponClientRpc()
+ {
+ // All clients (except owner) receive unequip notification
+ if (IsOwner) return;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Client received unequip notification");
+ }
+
+ // TODO: Trigger unequip animation
+ // TODO: Hide weapon prop
+ }
+
+ // ============================================================================================
+ // RPCs - COMBAT ACTIONS
+ // ============================================================================================
+
+ ///
+ /// Fire weapon (owner -> server -> all clients)
+ ///
+ [ServerRpc]
+ public void FireWeaponServerRpc(Vector3 origin, Vector3 direction, int weaponId)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Server received fire request: weapon {weaponId}");
+ }
+
+ // Server validates fire action
+ // TODO: Check if weapon can fire (ammo, cooldown, etc.)
+
+ // Broadcast to all clients for visual effects
+ FireWeaponClientRpc(origin, direction, weaponId);
+ }
+
+ [ClientRpc]
+ private void FireWeaponClientRpc(Vector3 origin, Vector3 direction, int weaponId)
+ {
+ // All clients (except owner) play fire effects
+ if (IsOwner) return;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Client playing fire effects for weapon {weaponId}");
+ }
+
+ // TODO: Play muzzle flash
+ // TODO: Play firing animation
+ // TODO: Play firing sound
+ // TODO: Spawn projectile if needed
+ }
+
+ ///
+ /// Report hit to server for validation
+ ///
+ [ServerRpc]
+ public void ReportHitServerRpc(HitData hitData)
+ {
+ if (!serverAuthoritativeDamage)
+ {
+ // Non-authoritative mode: trust client
+ ApplyDamageClientRpc(hitData);
+ return;
+ }
+
+ // Server validates hit
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Server validating hit on {hitData.TargetNetworkId}, damage: {hitData.Damage}");
+ }
+
+ // TODO: Validate hit (raycast, distance, etc.)
+ // TODO: Check if target is invincible
+ // TODO: Calculate final damage (with defense, resistances, etc.)
+
+ // If valid, apply damage
+ ApplyDamageClientRpc(hitData);
+ }
+
+ [ClientRpc]
+ private void ApplyDamageClientRpc(HitData hitData)
+ {
+ // Find target
+ if (!NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(hitData.TargetNetworkId, out NetworkObject targetNetworkObject))
+ {
+ Debug.LogWarning($"[NetworkCombatAdapter] Target {hitData.TargetNetworkId} not found");
+ return;
+ }
+
+ Character targetCharacter = targetNetworkObject.GetComponent();
+ if (targetCharacter == null)
+ {
+ Debug.LogWarning($"[NetworkCombatAdapter] Target has no Character component");
+ return;
+ }
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Applying damage {hitData.Damage} to {targetCharacter.name}");
+ }
+
+ // TODO: Apply damage to target's Traits (health)
+ // TODO: Play hit effects (blood, impact sounds, etc.)
+ // TODO: Trigger hit reactions (flinch, knockback, etc.)
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Update aiming state (call from owner)
+ ///
+ public void SetAiming(bool isAiming)
+ {
+ if (!IsOwner || !syncAiming) return;
+
+ var state = m_NetworkWeaponState.Value;
+ state.IsAiming = isAiming;
+ m_NetworkWeaponState.Value = state;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Aiming state updated: {isAiming}");
+ }
+ }
+
+ ///
+ /// Update blocking state (call from owner)
+ ///
+ public void SetBlocking(bool isBlocking)
+ {
+ if (!IsOwner || !syncBlocking) return;
+
+ m_IsBlocking.Value = isBlocking;
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkCombatAdapter] Blocking state updated: {isBlocking}");
+ }
+ }
+
+ ///
+ /// Update charge amount (call from owner)
+ ///
+ public void SetChargeAmount(float chargeAmount)
+ {
+ if (!IsOwner) return;
+
+ var state = m_NetworkWeaponState.Value;
+ state.IsCharging = chargeAmount > 0f;
+ state.ChargeAmount = chargeAmount;
+ m_NetworkWeaponState.Value = state;
+ }
+
+ ///
+ /// Fire weapon (call from owner)
+ ///
+ public void Fire(Vector3 origin, Vector3 direction)
+ {
+ if (!IsOwner) return;
+
+ if (IsServer)
+ {
+ FireWeaponClientRpc(origin, direction, currentWeaponId);
+ }
+ else
+ {
+ FireWeaponServerRpc(origin, direction, currentWeaponId);
+ }
+ }
+
+ ///
+ /// Report hit (call from owner)
+ ///
+ public void ReportHit(NetworkObject target, Vector3 hitPoint, Vector3 hitNormal, float damage)
+ {
+ if (!IsOwner) return;
+
+ HitData hitData = new HitData
+ {
+ TargetNetworkId = target.NetworkObjectId,
+ HitPoint = hitPoint,
+ HitNormal = hitNormal,
+ Damage = damage,
+ WeaponId = currentWeaponId
+ };
+
+ ReportHitServerRpc(hitData);
+ }
+
+ ///
+ /// Get current weapon state
+ ///
+ public WeaponState GetWeaponState() => m_NetworkWeaponState.Value;
+
+ ///
+ /// Get blocking state
+ ///
+ public bool IsBlocking() => m_IsBlocking.Value;
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorAnimator.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorAnimator.cs
index 3b1575deb..49e629126 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorAnimator.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkGameCreatorAnimator.cs
@@ -8,6 +8,14 @@ namespace GameCreator.Multiplayer.Components
/// Synchronizes GameCreator animator parameters across the network.
/// GameCreator uses float parameters (Speed, Intent-X, etc.) which NetworkAnimator doesn't sync by default.
/// This component handles proper network synchronization of all animation-related parameters.
+ ///
+ /// CRITICAL FOR ANIMATION:
+ /// - Owner: Reads animator parameters from UnitAnimimKinematic and syncs to network
+ /// - Remote: Receives parameters and interpolates smoothly for visual quality
+ ///
+ /// REQUIRES:
+ /// - Character component must be ENABLED (for UnitAnimimKinematic to update parameters)
+ /// - Animator must have parameters: Speed, Speed-X, Speed-Y, Speed-Z, Speed-XZ, Intent-X, Intent-Z, Grounded
///
[RequireComponent(typeof(Character))]
[RequireComponent(typeof(Animator))]
@@ -17,10 +25,14 @@ public class NetworkGameCreatorAnimator : NetworkBehaviour
[Header("Animation Settings")]
[Tooltip("Enable animation synchronization")]
public bool syncAnimations = true;
-
- [Tooltip("How often to sync animator parameters (per second)")]
+
+ [Tooltip("How often to sync animator parameters (per second). Higher = smoother but more bandwidth")]
[Range(10f, 60f)]
- public float syncRate = 30f;
+ public float syncRate = 60f; // IMPROVED: 60Hz for smoother animation
+
+ [Tooltip("Interpolation speed for remote players. Higher = more responsive, lower = smoother")]
+ [Range(5f, 30f)]
+ public float interpolationSpeed = 15f;
// ============================================================================================
// PROPERTIES
@@ -150,13 +162,17 @@ public override void OnNetworkDespawn()
private void Update()
{
if (!IsSpawned || !syncAnimations) return;
-
+
if (IsOwner)
{
// Owner: Read animator parameters and sync to network
SyncAnimatorToNetwork();
}
- // Remote clients apply network state in OnAnimatorStateChanged callback
+ else
+ {
+ // Remote: Smoothly interpolate animator parameters towards network state
+ InterpolateAnimatorState();
+ }
}
// ============================================================================================
@@ -194,22 +210,41 @@ private void SyncAnimatorToNetwork()
}
}
+ // Target values for interpolation (remote players only)
+ private AnimatorState m_TargetState;
+ private bool m_HasTargetState = false;
+
///
- /// Remote: Apply network state to animator
+ /// Remote: Store new network state as interpolation target
///
private void OnAnimatorStateChanged(AnimatorState previousValue, AnimatorState newValue)
{
if (IsOwner || m_Animator == null) return;
-
- // Apply all parameters to animator
- m_Animator.SetFloat(PARAM_Speed, newValue.speed);
- m_Animator.SetFloat(PARAM_SpeedX, newValue.speedX);
- m_Animator.SetFloat(PARAM_SpeedY, newValue.speedY);
- m_Animator.SetFloat(PARAM_SpeedZ, newValue.speedZ);
- m_Animator.SetFloat(PARAM_SpeedXZ, newValue.speedXZ);
- m_Animator.SetFloat(PARAM_IntentX, newValue.intentX);
- m_Animator.SetFloat(PARAM_IntentZ, newValue.intentZ);
- m_Animator.SetFloat(PARAM_Grounded, newValue.grounded);
+
+ // Store target state for smooth interpolation in Update()
+ m_TargetState = newValue;
+ m_HasTargetState = true;
+ }
+
+ ///
+ /// Remote: Interpolate animator parameters smoothly towards network state
+ /// This prevents jerky animation from instant parameter changes
+ ///
+ private void InterpolateAnimatorState()
+ {
+ if (!m_HasTargetState || m_Animator == null) return;
+
+ float lerpFactor = interpolationSpeed * Time.deltaTime;
+
+ // Smoothly interpolate all animator parameters
+ m_Animator.SetFloat(PARAM_Speed, Mathf.Lerp(m_Animator.GetFloat(PARAM_Speed), m_TargetState.speed, lerpFactor));
+ m_Animator.SetFloat(PARAM_SpeedX, Mathf.Lerp(m_Animator.GetFloat(PARAM_SpeedX), m_TargetState.speedX, lerpFactor));
+ m_Animator.SetFloat(PARAM_SpeedY, Mathf.Lerp(m_Animator.GetFloat(PARAM_SpeedY), m_TargetState.speedY, lerpFactor));
+ m_Animator.SetFloat(PARAM_SpeedZ, Mathf.Lerp(m_Animator.GetFloat(PARAM_SpeedZ), m_TargetState.speedZ, lerpFactor));
+ m_Animator.SetFloat(PARAM_SpeedXZ, Mathf.Lerp(m_Animator.GetFloat(PARAM_SpeedXZ), m_TargetState.speedXZ, lerpFactor));
+ m_Animator.SetFloat(PARAM_IntentX, Mathf.Lerp(m_Animator.GetFloat(PARAM_IntentX), m_TargetState.intentX, lerpFactor));
+ m_Animator.SetFloat(PARAM_IntentZ, Mathf.Lerp(m_Animator.GetFloat(PARAM_IntentZ), m_TargetState.intentZ, lerpFactor));
+ m_Animator.SetFloat(PARAM_Grounded, Mathf.Lerp(m_Animator.GetFloat(PARAM_Grounded), m_TargetState.grounded, lerpFactor));
}
// ============================================================================================
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkPlayerController.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkPlayerController.cs
new file mode 100644
index 000000000..3ad205775
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkPlayerController.cs
@@ -0,0 +1,635 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.Characters;
+using GameCreator.Multiplayer.Runtime.Core;
+using GameCreator.Multiplayer.Runtime.Modules;
+
+namespace GameCreator.Multiplayer.Runtime.Components
+{
+ ///
+ /// UNIFIED Network Player Controller - Consolidates ALL GameCreator multiplayer modules
+ ///
+ /// This is the single entry point for setting up a networked GameCreator character.
+ /// It coordinates: Character Motion, Animation, Stats/Traits, Inventory, and Perception.
+ ///
+ /// ARCHITECTURE:
+ /// - Uses INetworkModuleSync interface for unified module synchronization
+ /// - Owner authority for movement/animation
+ /// - Server authority for stats/inventory/perception
+ /// - Ensures Character component stays ENABLED (critical for animation)
+ ///
+ /// REQUIRED COMPONENTS (auto-added if missing):
+ /// - Character (GameCreator)
+ /// - NetworkObject (Netcode)
+ /// - NetworkCharacterAdapter (motion sync)
+ /// - NetworkGameCreatorAnimator (animation sync)
+ ///
+ /// OPTIONAL MODULES (enabled via inspector):
+ /// - NetworkTraitsAdapter (stats/attributes sync)
+ /// - NetworkInventorySync (inventory/equipment sync)
+ /// - NetworkPerceptionSync (awareness/targeting sync)
+ ///
+ [RequireComponent(typeof(Character))]
+ [RequireComponent(typeof(NetworkObject))]
+ [AddComponentMenu("GameCreator/Multiplayer/Network Player Controller")]
+ [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_FIRST)]
+ public class NetworkPlayerController : NetworkBehaviour
+ {
+ // ============================================================================================
+ // ENUMS
+ // ============================================================================================
+
+ public enum ModuleState
+ {
+ Disabled,
+ Enabled,
+ AutoDetect
+ }
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Core Settings")]
+ [Tooltip("Sync rate for movement updates (Hz)")]
+ [SerializeField] private float m_MovementSyncRate = 30f;
+
+ [Tooltip("Sync rate for animation parameters (Hz)")]
+ [SerializeField] private float m_AnimationSyncRate = 60f;
+
+ [Tooltip("Interpolation speed for remote player animations")]
+ [SerializeField] private float m_AnimationInterpolation = 15f;
+
+ [Header("Module Configuration")]
+ [Tooltip("Enable Stats/Traits synchronization")]
+ [SerializeField] private ModuleState m_TraitsModule = ModuleState.AutoDetect;
+
+ [Tooltip("Enable Inventory synchronization")]
+ [SerializeField] private ModuleState m_InventoryModule = ModuleState.AutoDetect;
+
+ [Tooltip("Enable Perception synchronization")]
+ [SerializeField] private ModuleState m_PerceptionModule = ModuleState.AutoDetect;
+
+ [Header("Authority Settings")]
+ [Tooltip("Server validates all stat/inventory changes (anti-cheat)")]
+ [SerializeField] private bool m_ServerAuthorityForStats = true;
+
+ [Tooltip("Server validates all inventory changes (anti-cheat)")]
+ [SerializeField] private bool m_ServerAuthorityForInventory = true;
+
+ [Header("Debug")]
+ [SerializeField] private bool m_DebugMode = false;
+
+ // ============================================================================================
+ // RUNTIME REFERENCES
+ // ============================================================================================
+
+ private Character m_Character;
+ private Animator m_Animator;
+ private NetworkObject m_NetworkObject;
+
+ // Module sync components
+ private NetworkCharacterAdapter m_CharacterAdapter;
+ private NetworkGameCreatorAnimator m_AnimatorSync;
+ private NetworkTraitsAdapter m_TraitsAdapter;
+ private NetworkInventorySync m_InventorySync;
+ private NetworkPerceptionSync m_PerceptionSync;
+
+ // Module registry
+ private List m_RegisteredModules = new List();
+
+ // State tracking
+ private bool m_IsInitialized = false;
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ ///
+ /// Player display name synced across network
+ ///
+ private NetworkVariable m_PlayerName =
+ new NetworkVariable(
+ default,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner);
+
+ ///
+ /// Player ready state (for lobby/game start)
+ ///
+ private NetworkVariable m_IsReady =
+ new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Owner);
+
+ ///
+ /// Player team/faction ID
+ ///
+ private NetworkVariable m_TeamId =
+ new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server);
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public Character Character => m_Character;
+ public Animator Animator => m_Animator;
+ public bool IsInitialized => m_IsInitialized;
+
+ public string PlayerName
+ {
+ get => m_PlayerName.Value.ToString();
+ set
+ {
+ if (IsOwner)
+ {
+ m_PlayerName.Value = new Unity.Collections.FixedString64Bytes(value);
+ }
+ }
+ }
+
+ public bool IsReady
+ {
+ get => m_IsReady.Value;
+ set
+ {
+ if (IsOwner)
+ {
+ m_IsReady.Value = value;
+ }
+ }
+ }
+
+ public int TeamId => m_TeamId.Value;
+
+ // Module accessors
+ public NetworkCharacterAdapter CharacterAdapter => m_CharacterAdapter;
+ public NetworkGameCreatorAnimator AnimatorSync => m_AnimatorSync;
+ public NetworkTraitsAdapter TraitsAdapter => m_TraitsAdapter;
+ public NetworkInventorySync InventorySync => m_InventorySync;
+ public NetworkPerceptionSync PerceptionSync => m_PerceptionSync;
+
+ // ============================================================================================
+ // EVENTS
+ // ============================================================================================
+
+ public event Action OnPlayerInitialized;
+ public event Action OnPlayerNameChanged;
+ public event Action OnReadyStateChanged;
+ public event Action OnTeamChanged;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ // Get required components
+ m_Character = GetComponent();
+ m_Animator = GetComponentInChildren();
+ m_NetworkObject = GetComponent();
+
+ if (m_Character == null)
+ {
+ Debug.LogError("[NetworkPlayerController] Character component is required!");
+ enabled = false;
+ return;
+ }
+
+ if (m_Animator == null)
+ {
+ Debug.LogWarning("[NetworkPlayerController] No Animator found. Animation sync disabled.");
+ }
+ }
+
+ private void Start()
+ {
+ // Validate setup (non-networked validation)
+ ValidateSetup();
+ }
+
+ private void Update()
+ {
+ if (!IsSpawned || !m_IsInitialized) return;
+
+ // CRITICAL: Ensure Character stays enabled
+ EnsureCharacterEnabled();
+ }
+
+ // ============================================================================================
+ // NETWORK LIFECYCLE
+ // ============================================================================================
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] OnNetworkSpawn - IsOwner: {IsOwner}, IsServer: {IsServer}, ClientId: {OwnerClientId}");
+ }
+
+ // Initialize all modules
+ InitializeModules();
+
+ // Subscribe to network variable changes
+ m_PlayerName.OnValueChanged += OnPlayerNameValueChanged;
+ m_IsReady.OnValueChanged += OnReadyValueChanged;
+ m_TeamId.OnValueChanged += OnTeamValueChanged;
+
+ // Register with global manager
+ RegisterWithManager();
+
+ m_IsInitialized = true;
+ OnPlayerInitialized?.Invoke();
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Initialization complete for {gameObject.name}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // Unsubscribe from events
+ m_PlayerName.OnValueChanged -= OnPlayerNameValueChanged;
+ m_IsReady.OnValueChanged -= OnReadyValueChanged;
+ m_TeamId.OnValueChanged -= OnTeamValueChanged;
+
+ // Unregister modules
+ UnregisterModules();
+
+ // Unregister from global manager
+ UnregisterFromManager();
+
+ m_IsInitialized = false;
+
+ base.OnNetworkDespawn();
+ }
+
+ // ============================================================================================
+ // MODULE INITIALIZATION
+ // ============================================================================================
+
+ private void InitializeModules()
+ {
+ // 1. Character Adapter (motion sync) - REQUIRED
+ InitializeCharacterAdapter();
+
+ // 2. Animator Sync - REQUIRED if animator exists
+ InitializeAnimatorSync();
+
+ // 3. Traits Adapter (stats) - OPTIONAL
+ InitializeTraitsAdapter();
+
+ // 4. Inventory Sync - OPTIONAL
+ InitializeInventorySync();
+
+ // 5. Perception Sync - OPTIONAL
+ InitializePerceptionSync();
+ }
+
+ private void InitializeCharacterAdapter()
+ {
+ m_CharacterAdapter = GetComponent();
+
+ if (m_CharacterAdapter == null)
+ {
+ m_CharacterAdapter = gameObject.AddComponent();
+ if (m_DebugMode) Debug.Log("[NetworkPlayerController] Added NetworkCharacterAdapter");
+ }
+
+ // Configure sync rate
+ // Note: NetworkCharacterAdapter uses its own internal rate
+ }
+
+ private void InitializeAnimatorSync()
+ {
+ if (m_Animator == null) return;
+
+ m_AnimatorSync = GetComponent();
+
+ if (m_AnimatorSync == null)
+ {
+ m_AnimatorSync = gameObject.AddComponent();
+ if (m_DebugMode) Debug.Log("[NetworkPlayerController] Added NetworkGameCreatorAnimator");
+ }
+
+ // Configure animation sync settings
+ m_AnimatorSync.syncRate = m_AnimationSyncRate;
+ m_AnimatorSync.interpolationSpeed = m_AnimationInterpolation;
+ }
+
+ private void InitializeTraitsAdapter()
+ {
+ bool shouldEnable = ShouldEnableModule(m_TraitsModule, typeof(GameCreator.Runtime.Stats.Traits));
+
+ if (!shouldEnable) return;
+
+ m_TraitsAdapter = GetComponent();
+
+ if (m_TraitsAdapter == null && shouldEnable)
+ {
+ m_TraitsAdapter = gameObject.AddComponent();
+ if (m_DebugMode) Debug.Log("[NetworkPlayerController] Added NetworkTraitsAdapter");
+ }
+ }
+
+ private void InitializeInventorySync()
+ {
+ // Note: Inventory uses Bag component, check for it
+ bool shouldEnable = ShouldEnableModule(m_InventoryModule, typeof(Character));
+
+ if (!shouldEnable) return;
+
+ m_InventorySync = GetComponent();
+
+ if (m_InventorySync == null && shouldEnable)
+ {
+ m_InventorySync = gameObject.AddComponent();
+ if (m_DebugMode) Debug.Log("[NetworkPlayerController] Added NetworkInventorySync");
+ }
+ }
+
+ private void InitializePerceptionSync()
+ {
+ bool shouldEnable = ShouldEnableModule(m_PerceptionModule, typeof(GameCreator.Runtime.Perception.Perception));
+
+ if (!shouldEnable) return;
+
+ m_PerceptionSync = GetComponent();
+
+ if (m_PerceptionSync == null && shouldEnable)
+ {
+ m_PerceptionSync = gameObject.AddComponent();
+ if (m_DebugMode) Debug.Log("[NetworkPlayerController] Added NetworkPerceptionSync");
+ }
+
+ // Register with module sync interface
+ if (m_PerceptionSync != null)
+ {
+ m_RegisteredModules.Add(m_PerceptionSync);
+ }
+ }
+
+ private bool ShouldEnableModule(ModuleState state, Type requiredComponent)
+ {
+ switch (state)
+ {
+ case ModuleState.Enabled:
+ return true;
+
+ case ModuleState.Disabled:
+ return false;
+
+ case ModuleState.AutoDetect:
+ // Check if the required GameCreator component exists
+ return GetComponent(requiredComponent) != null;
+
+ default:
+ return false;
+ }
+ }
+
+ // ============================================================================================
+ // VALIDATION
+ // ============================================================================================
+
+ private void ValidateSetup()
+ {
+ // Check for problematic legacy component
+ var legacyV2 = GetComponent();
+ if (legacyV2 != null)
+ {
+ Debug.LogWarning($"[NetworkPlayerController] {gameObject.name} has NetworkGameCreatorCharacterV2 which disables Character! Consider removing it.");
+ }
+
+ // Ensure Character is enabled
+ if (m_Character != null && !m_Character.enabled)
+ {
+ Debug.LogWarning($"[NetworkPlayerController] Character component was disabled on {gameObject.name}. Re-enabling.");
+ m_Character.enabled = true;
+ }
+
+ // Validate animator setup
+ if (m_Animator == null)
+ {
+ Debug.LogWarning($"[NetworkPlayerController] No Animator found on {gameObject.name}. Animation sync will not work.");
+ }
+ }
+
+ ///
+ /// CRITICAL: Ensures Character component stays enabled.
+ /// Some legacy components disable it, breaking animation.
+ ///
+ private void EnsureCharacterEnabled()
+ {
+ if (m_Character != null && !m_Character.enabled)
+ {
+ m_Character.enabled = true;
+ if (m_DebugMode)
+ {
+ Debug.LogWarning($"[NetworkPlayerController] Re-enabled Character component on {gameObject.name}");
+ }
+ }
+ }
+
+ // ============================================================================================
+ // MANAGER REGISTRATION
+ // ============================================================================================
+
+ private void RegisterWithManager()
+ {
+ // Register with NetworkModuleSyncManager for coordinated syncing
+ if (NetworkModuleSyncManager.Instance != null)
+ {
+ foreach (var module in m_RegisteredModules)
+ {
+ // Modules register themselves, but we track them here
+ }
+ }
+
+ // Register with NetworkAuthorityManager
+ if (NetworkAuthorityManager.Instance != null)
+ {
+ // Authority manager handles ownership queries
+ }
+ }
+
+ private void UnregisterModules()
+ {
+ m_RegisteredModules.Clear();
+ }
+
+ private void UnregisterFromManager()
+ {
+ // Cleanup manager registrations
+ }
+
+ // ============================================================================================
+ // NETWORK VARIABLE CALLBACKS
+ // ============================================================================================
+
+ private void OnPlayerNameValueChanged(Unity.Collections.FixedString64Bytes oldValue, Unity.Collections.FixedString64Bytes newValue)
+ {
+ OnPlayerNameChanged?.Invoke(newValue.ToString());
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Player name changed: {newValue}");
+ }
+ }
+
+ private void OnReadyValueChanged(bool oldValue, bool newValue)
+ {
+ OnReadyStateChanged?.Invoke(newValue);
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Ready state changed: {newValue}");
+ }
+ }
+
+ private void OnTeamValueChanged(int oldValue, int newValue)
+ {
+ OnTeamChanged?.Invoke(newValue);
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Team changed: {newValue}");
+ }
+ }
+
+ // ============================================================================================
+ // SERVER RPCs
+ // ============================================================================================
+
+ ///
+ /// Request team assignment from server
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
+ public void RequestTeamAssignmentServerRpc(int requestedTeam, RpcParams rpcParams = default)
+ {
+ // Server validates and assigns team
+ // Add your team assignment logic here
+ m_TeamId.Value = requestedTeam;
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Server assigned team {requestedTeam} to client {rpcParams.Receive.SenderClientId}");
+ }
+ }
+
+ ///
+ /// Force sync all modules (useful after reconnection)
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
+ public void RequestFullSyncServerRpc(RpcParams rpcParams = default)
+ {
+ // Trigger full state sync to requesting client
+ FullSyncClientRpc(RpcTarget.Single(rpcParams.Receive.SenderClientId, RpcTargetUse.Temp));
+ }
+
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void FullSyncClientRpc(RpcParams rpcParams = default)
+ {
+ // Force all modules to sync from network
+ foreach (var module in m_RegisteredModules)
+ {
+ module?.SyncFromNetwork();
+ }
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[NetworkPlayerController] Full sync completed for {gameObject.name}");
+ }
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Manually trigger a sync of all dirty modules
+ ///
+ public void SyncAllModules()
+ {
+ foreach (var module in m_RegisteredModules)
+ {
+ if (module != null && module.IsDirty)
+ {
+ module.SyncToNetwork();
+ module.ClearDirty();
+ }
+ }
+ }
+
+ ///
+ /// Get a specific module by type
+ ///
+ public T GetModule() where T : class, INetworkModuleSync
+ {
+ foreach (var module in m_RegisteredModules)
+ {
+ if (module is T typedModule)
+ {
+ return typedModule;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Check if a module type is active
+ ///
+ public bool HasModule() where T : class
+ {
+ return GetComponent() != null;
+ }
+
+ // ============================================================================================
+ // DEBUG
+ // ============================================================================================
+
+ [ContextMenu("Debug Player State")]
+ private void DebugPlayerState()
+ {
+ Debug.Log($"=== NetworkPlayerController Debug ===");
+ Debug.Log($"GameObject: {gameObject.name}");
+ Debug.Log($"IsSpawned: {IsSpawned}");
+ Debug.Log($"IsOwner: {IsOwner}");
+ Debug.Log($"IsServer: {IsServer}");
+ Debug.Log($"ClientId: {OwnerClientId}");
+ Debug.Log($"PlayerName: {PlayerName}");
+ Debug.Log($"IsReady: {IsReady}");
+ Debug.Log($"TeamId: {TeamId}");
+ Debug.Log($"--- Components ---");
+ Debug.Log($"Character: {(m_Character != null ? "OK" : "MISSING")} (enabled: {m_Character?.enabled})");
+ Debug.Log($"Animator: {(m_Animator != null ? "OK" : "MISSING")}");
+ Debug.Log($"CharacterAdapter: {(m_CharacterAdapter != null ? "OK" : "MISSING")}");
+ Debug.Log($"AnimatorSync: {(m_AnimatorSync != null ? "OK" : "MISSING")}");
+ Debug.Log($"TraitsAdapter: {(m_TraitsAdapter != null ? "OK" : "MISSING")}");
+ Debug.Log($"InventorySync: {(m_InventorySync != null ? "OK" : "MISSING")}");
+ Debug.Log($"PerceptionSync: {(m_PerceptionSync != null ? "OK" : "MISSING")}");
+ Debug.Log($"Registered Modules: {m_RegisteredModules.Count}");
+ Debug.Log($"===================================");
+ }
+
+ [ContextMenu("Force Enable Character")]
+ private void ForceEnableCharacter()
+ {
+ if (m_Character != null)
+ {
+ m_Character.enabled = true;
+ Debug.Log($"[NetworkPlayerController] Forced Character enabled on {gameObject.name}");
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkTraitsAdapter.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkTraitsAdapter.cs
new file mode 100644
index 000000000..09246a6a0
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Components/NetworkTraitsAdapter.cs
@@ -0,0 +1,589 @@
+using System;
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using GameCreator.Runtime.Stats;
+using GameCreator.Runtime.Common;
+
+namespace GameCreator.Multiplayer.Runtime.Components
+{
+ ///
+ /// Network adapter for GameCreator Traits (Stats) system - PHASE 3
+ /// Synchronizes stats, attributes (HP/MP/Stamina), and status effects across the network
+ ///
+ /// ARCHITECTURE:
+ /// - NetworkVariables for critical attributes (HP, MP, Stamina) - real-time sync
+ /// - NetworkLists for stats and status effects - dynamic collections
+ /// - RPCs for stat modifications and status effect applications
+ /// - Server authority for all stat changes (prevents cheating)
+ ///
+ /// EXECUTION ORDER: Runs AFTER Character (-1) to ensure proper initialization
+ ///
+ [RequireComponent(typeof(Traits))]
+ [RequireComponent(typeof(NetworkObject))]
+ [AddComponentMenu("GameCreator/Multiplayer/Network Traits Adapter")]
+ [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_FIRST)] // -50: Runs early but AFTER Character (-1)
+ public class NetworkTraitsAdapter : NetworkBehaviour
+ {
+ // ============================================================================================
+ // SERIALIZABLE STRUCTS
+ // ============================================================================================
+
+ ///
+ /// Attribute state (HP, MP, Stamina) for network synchronization
+ ///
+ [Serializable]
+ public struct AttributeState : INetworkSerializable
+ {
+ public int AttributeId; // Hash of attribute IdString
+ public float Current;
+ public float Max;
+ public float Min;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ serializer.SerializeValue(ref AttributeId);
+ serializer.SerializeValue(ref Current);
+ serializer.SerializeValue(ref Max);
+ serializer.SerializeValue(ref Min);
+ }
+ }
+
+ ///
+ /// Stat value for network synchronization
+ ///
+ [Serializable]
+ public struct StatState : INetworkSerializable
+ {
+ public int StatId; // Hash of stat IdString
+ public float Value;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ serializer.SerializeValue(ref StatId);
+ serializer.SerializeValue(ref Value);
+ }
+ }
+
+ ///
+ /// Status effect for network synchronization
+ ///
+ [Serializable]
+ public struct StatusEffectState : INetworkSerializable
+ {
+ public int EffectId; // Hash of status effect IdString
+ public float Duration;
+ public float RemainingTime;
+ public int Stacks;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ serializer.SerializeValue(ref EffectId);
+ serializer.SerializeValue(ref Duration);
+ serializer.SerializeValue(ref RemainingTime);
+ serializer.SerializeValue(ref Stacks);
+ }
+ }
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ ///
+ /// Critical attributes (HP, MP, Stamina) - Server authority
+ /// These update frequently and need real-time sync
+ ///
+ private NetworkVariable m_Health = new NetworkVariable(
+ new AttributeState(),
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server // Server authority
+ );
+
+ private NetworkVariable m_Mana = new NetworkVariable(
+ new AttributeState(),
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server // Server authority
+ );
+
+ private NetworkVariable m_Stamina = new NetworkVariable(
+ new AttributeState(),
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server // Server authority
+ );
+
+ ///
+ /// All stats (STR, DEF, INT, etc.) - Server authority
+ /// These update less frequently
+ ///
+ private NetworkList m_Stats;
+
+ ///
+ /// Active status effects (buffs/debuffs) - Server authority
+ ///
+ private NetworkList m_StatusEffects;
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Traits Synchronization")]
+ [Tooltip("Server validates all stat/attribute changes (prevents cheating)")]
+ [SerializeField] private bool serverAuthority = true;
+
+ [Tooltip("Sync rate for attributes (updates per second) - Higher = more accurate, more bandwidth")]
+ [SerializeField] private float attributeSyncRate = 10f; // 10 updates/sec for HP/MP
+
+ [Tooltip("Sync rate for stats (updates per second) - Stats change less frequently")]
+ [SerializeField] private float statSyncRate = 2f; // 2 updates/sec for stats
+
+ [Header("Attribute IDs (from GameCreator Stats)")]
+ [Tooltip("IdString hash for Health attribute")]
+ [SerializeField] private int healthAttributeId = "health".GetHashCode();
+
+ [Tooltip("IdString hash for Mana attribute")]
+ [SerializeField] private int manaAttributeId = "mana".GetHashCode();
+
+ [Tooltip("IdString hash for Stamina attribute")]
+ [SerializeField] private int staminaAttributeId = "stamina".GetHashCode();
+
+ [Header("Debug")]
+ [SerializeField] private bool debugMode = false;
+
+ // ============================================================================================
+ // COMPONENTS
+ // ============================================================================================
+
+ private Traits traits;
+
+ // Sync throttling
+ private float lastAttributeSyncTime;
+ private float lastStatSyncTime;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ // Initialize NetworkLists in Awake (required for Netcode)
+ m_Stats = new NetworkList();
+ m_StatusEffects = new NetworkList();
+
+ traits = GetComponent();
+ if (traits == null)
+ {
+ Debug.LogError("[NetworkTraitsAdapter] Traits component is missing!");
+ enabled = false;
+ }
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] OnNetworkSpawn - IsServer: {IsServer}, IsOwner: {IsOwner}");
+ }
+
+ if (IsServer)
+ {
+ // Server: Initialize network state from Traits
+ InitializeNetworkState();
+
+ // Subscribe to Traits changes
+ traits.EventChange += OnTraitsChanged;
+ }
+ else
+ {
+ // Client: Subscribe to network state changes
+ m_Health.OnValueChanged += OnHealthChanged;
+ m_Mana.OnValueChanged += OnManaChanged;
+ m_Stamina.OnValueChanged += OnStaminaChanged;
+ m_Stats.OnListChanged += OnStatsListChanged;
+ m_StatusEffects.OnListChanged += OnStatusEffectsListChanged;
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ if (IsServer && traits != null)
+ {
+ traits.EventChange -= OnTraitsChanged;
+ }
+ else
+ {
+ m_Health.OnValueChanged -= OnHealthChanged;
+ m_Mana.OnValueChanged -= OnManaChanged;
+ m_Stamina.OnValueChanged -= OnStaminaChanged;
+ m_Stats.OnListChanged -= OnStatsListChanged;
+ m_StatusEffects.OnListChanged -= OnStatusEffectsListChanged;
+ }
+
+ base.OnNetworkDespawn();
+ }
+
+ // ============================================================================================
+ // SERVER INITIALIZATION
+ // ============================================================================================
+
+ ///
+ /// Initialize network state from Traits (server only)
+ ///
+ private void InitializeNetworkState()
+ {
+ if (!IsServer || traits == null) return;
+
+ // Initialize critical attributes
+ SyncAttributeToNetwork(healthAttributeId, ref m_Health);
+ SyncAttributeToNetwork(manaAttributeId, ref m_Mana);
+ SyncAttributeToNetwork(staminaAttributeId, ref m_Stamina);
+
+ // Initialize stats
+ // TODO: Sync all stats from RuntimeStats
+
+ // Initialize status effects
+ // TODO: Sync all active status effects
+
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Server initialized network state");
+ }
+ }
+
+ ///
+ /// Sync a single attribute to network variable
+ ///
+ private void SyncAttributeToNetwork(int attributeId, ref NetworkVariable networkVar)
+ {
+ // TODO: Get attribute from traits.RuntimeAttributes using IdString
+ // For now, using placeholder values
+ AttributeState state = new AttributeState
+ {
+ AttributeId = attributeId,
+ Current = 100f, // TODO: Get from RuntimeAttributes
+ Max = 100f,
+ Min = 0f
+ };
+
+ networkVar.Value = state;
+ }
+
+ // ============================================================================================
+ // TRAITS CHANGE EVENT (Server Only)
+ // ============================================================================================
+
+ ///
+ /// Called when Traits change locally (server only)
+ ///
+ private void OnTraitsChanged()
+ {
+ if (!IsServer) return;
+
+ // Throttle attribute sync
+ if (Time.time - lastAttributeSyncTime >= 1f / attributeSyncRate)
+ {
+ // Sync critical attributes
+ SyncAttributeToNetwork(healthAttributeId, ref m_Health);
+ SyncAttributeToNetwork(manaAttributeId, ref m_Mana);
+ SyncAttributeToNetwork(staminaAttributeId, ref m_Stamina);
+
+ lastAttributeSyncTime = Time.time;
+ }
+
+ // Throttle stat sync
+ if (Time.time - lastStatSyncTime >= 1f / statSyncRate)
+ {
+ // TODO: Sync all stats to m_Stats NetworkList
+
+ lastStatSyncTime = Time.time;
+ }
+
+ // Status effects sync (immediate - they change less frequently)
+ // TODO: Sync status effects to m_StatusEffects NetworkList
+ }
+
+ // ============================================================================================
+ // NETWORK STATE CHANGE CALLBACKS (Client Only)
+ // ============================================================================================
+
+ ///
+ /// Called when networked health changes (client only)
+ ///
+ private void OnHealthChanged(AttributeState oldValue, AttributeState newValue)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Health changed: {newValue.Current}/{newValue.Max}");
+ }
+
+ // Apply to local Traits
+ // TODO: Apply to traits.RuntimeAttributes.Set(IdString health, newValue.Current)
+ }
+
+ ///
+ /// Called when networked mana changes (client only)
+ ///
+ private void OnManaChanged(AttributeState oldValue, AttributeState newValue)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Mana changed: {newValue.Current}/{newValue.Max}");
+ }
+
+ // Apply to local Traits
+ // TODO: Apply to traits.RuntimeAttributes
+ }
+
+ ///
+ /// Called when networked stamina changes (client only)
+ ///
+ private void OnStaminaChanged(AttributeState oldValue, AttributeState newValue)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Stamina changed: {newValue.Current}/{newValue.Max}");
+ }
+
+ // Apply to local Traits
+ // TODO: Apply to traits.RuntimeAttributes
+ }
+
+ ///
+ /// Called when stats list changes (client only)
+ ///
+ private void OnStatsListChanged(NetworkListEvent changeEvent)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Stats list changed: {changeEvent.Type}");
+ }
+
+ // Apply stats changes to local Traits
+ // TODO: Apply to traits.RuntimeStats
+ }
+
+ ///
+ /// Called when status effects list changes (client only)
+ ///
+ private void OnStatusEffectsListChanged(NetworkListEvent changeEvent)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Status effects changed: {changeEvent.Type}");
+ }
+
+ // Apply status effect changes to local Traits
+ // TODO: Apply to traits.RuntimeStatusEffects
+ }
+
+ // ============================================================================================
+ // RPCs - STAT MODIFICATIONS
+ // ============================================================================================
+
+ ///
+ /// Modify attribute (e.g., damage, heal)
+ /// Client -> Server -> All Clients
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void ModifyAttributeServerRpc(int attributeId, float amount, ulong clientId, RpcParams rpcParams = default)
+ {
+ if (!serverAuthority)
+ {
+ // Non-authoritative mode: trust client
+ ApplyAttributeModificationClientRpc(attributeId, amount);
+ return;
+ }
+
+ // Server validates modification
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Server validating attribute modification: {attributeId}, amount: {amount}");
+ }
+
+ // TODO: Validate modification (check for invincibility, etc.)
+ // TODO: Apply to traits.RuntimeAttributes
+
+ // Broadcast to all clients
+ ApplyAttributeModificationClientRpc(attributeId, amount);
+ }
+
+ [ClientRpc]
+ private void ApplyAttributeModificationClientRpc(int attributeId, float amount)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Client applying attribute modification: {attributeId}, amount: {amount}");
+ }
+
+ // TODO: Apply to local traits.RuntimeAttributes
+ // TODO: Play visual effects (damage numbers, health bar update, etc.)
+ }
+
+ ///
+ /// Apply status effect
+ /// Client -> Server -> All Clients
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void ApplyStatusEffectServerRpc(int effectId, float duration, int stacks, ulong clientId, RpcParams rpcParams = default)
+ {
+ if (!serverAuthority)
+ {
+ // Non-authoritative mode: trust client
+ ApplyStatusEffectClientRpc(effectId, duration, stacks);
+ return;
+ }
+
+ // Server validates status effect
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Server applying status effect: {effectId}, duration: {duration}, stacks: {stacks}");
+ }
+
+ // TODO: Validate status effect
+ // TODO: Apply to traits.RuntimeStatusEffects
+
+ // Add to network list
+ StatusEffectState state = new StatusEffectState
+ {
+ EffectId = effectId,
+ Duration = duration,
+ RemainingTime = duration,
+ Stacks = stacks
+ };
+ m_StatusEffects.Add(state);
+
+ // Broadcast to all clients
+ ApplyStatusEffectClientRpc(effectId, duration, stacks);
+ }
+
+ [ClientRpc]
+ private void ApplyStatusEffectClientRpc(int effectId, float duration, int stacks)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Client applying status effect: {effectId}");
+ }
+
+ // TODO: Apply to local traits.RuntimeStatusEffects
+ // TODO: Play visual effects (buff/debuff icons, particle effects, etc.)
+ }
+
+ ///
+ /// Remove status effect
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void RemoveStatusEffectServerRpc(int effectId, ulong clientId, RpcParams rpcParams = default)
+ {
+ if (!IsServer) return;
+
+ // Find and remove from list
+ for (int i = 0; i < m_StatusEffects.Count; i++)
+ {
+ if (m_StatusEffects[i].EffectId == effectId)
+ {
+ m_StatusEffects.RemoveAt(i);
+ break;
+ }
+ }
+
+ // TODO: Remove from traits.RuntimeStatusEffects
+
+ // Broadcast to all clients
+ RemoveStatusEffectClientRpc(effectId);
+ }
+
+ [ClientRpc]
+ private void RemoveStatusEffectClientRpc(int effectId)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[NetworkTraitsAdapter] Client removing status effect: {effectId}");
+ }
+
+ // TODO: Remove from local traits.RuntimeStatusEffects
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Get current health
+ ///
+ public float GetHealth() => m_Health.Value.Current;
+
+ ///
+ /// Get max health
+ ///
+ public float GetMaxHealth() => m_Health.Value.Max;
+
+ ///
+ /// Get current mana
+ ///
+ public float GetMana() => m_Mana.Value.Current;
+
+ ///
+ /// Get max mana
+ ///
+ public float GetMaxMana() => m_Mana.Value.Max;
+
+ ///
+ /// Get current stamina
+ ///
+ public float GetStamina() => m_Stamina.Value.Current;
+
+ ///
+ /// Get max stamina
+ ///
+ public float GetMaxStamina() => m_Stamina.Value.Max;
+
+ ///
+ /// Damage this character (call from anywhere)
+ ///
+ public void TakeDamage(float amount)
+ {
+ if (IsServer)
+ {
+ ModifyAttributeServerRpc(healthAttributeId, -amount, 0);
+ }
+ else
+ {
+ ModifyAttributeServerRpc(healthAttributeId, -amount, NetworkManager.Singleton.LocalClientId);
+ }
+ }
+
+ ///
+ /// Heal this character (call from anywhere)
+ ///
+ public void Heal(float amount)
+ {
+ if (IsServer)
+ {
+ ModifyAttributeServerRpc(healthAttributeId, amount, 0);
+ }
+ else
+ {
+ ModifyAttributeServerRpc(healthAttributeId, amount, NetworkManager.Singleton.LocalClientId);
+ }
+ }
+
+ ///
+ /// Apply a status effect (call from anywhere)
+ ///
+ public void ApplyStatusEffect(int effectId, float duration, int stacks = 1)
+ {
+ if (IsServer)
+ {
+ ApplyStatusEffectServerRpc(effectId, duration, stacks, 0);
+ }
+ else
+ {
+ ApplyStatusEffectServerRpc(effectId, duration, stacks, NetworkManager.Singleton.LocalClientId);
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerCoordinator.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerCoordinator.cs
index 37b3520bf..fd58c7bce 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerCoordinator.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerCoordinator.cs
@@ -130,7 +130,7 @@ private void InitializeReadyStates()
m_ReadyStates[ManagerType.NetworkManager] = false;
m_ReadyStates[ManagerType.NetworkPlayer] = false;
m_ReadyStates[ManagerType.RPC] = false;
- m_ReadyStates[ManagerType.Spawn] = false;
+ // Spawn system tracking removed - spawn functionality now handled by NetworkPlayerManager
m_ReadyStates[ManagerType.Supabase] = false;
}
@@ -162,9 +162,6 @@ private bool UpdateManagerStates()
// NetworkRPCManager
changed |= UpdateState(ManagerType.RPC, CheckRPCManagerReady());
- // SmartSpawnSystem (optional - may not exist in scene)
- changed |= UpdateState(ManagerType.Spawn, CheckSpawnSystemReady());
-
// SupabaseManager (optional)
changed |= UpdateState(ManagerType.Supabase, CheckSupabaseManagerReady());
@@ -250,13 +247,6 @@ private bool CheckRPCManagerReady()
return true;
}
- private bool CheckSpawnSystemReady()
- {
- // Spawn system is optional and no longer used
- // Always return true as spawn functionality is now handled by NetworkPlayerManager
- return true;
- }
-
private bool CheckSupabaseManagerReady()
{
// Supabase is optional
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerType.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerType.cs
index 3bf23e596..2ff61f717 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerType.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Core/ManagerType.cs
@@ -13,6 +13,7 @@ public enum ManagerType
NetworkManager = 1 << 0,
NetworkPlayer = 1 << 1,
RPC = 1 << 2,
+ [Obsolete("Spawn functionality is now handled by NetworkPlayerManager")]
Spawn = 1 << 3,
Supabase = 1 << 4,
All = ~0
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Debugging/NetworkDebugManager.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Debugging/NetworkDebugManager.cs
index daaa5c75e..7d8cc4af0 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Debugging/NetworkDebugManager.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Debugging/NetworkDebugManager.cs
@@ -4,6 +4,7 @@
using UnityEngine;
using Unity.Netcode;
using GameCreator.Runtime.Common;
+using GameCreator.Multiplayer.Runtime.RPC;
namespace GameCreator.Multiplayer.Runtime.Debugging
{
@@ -219,7 +220,7 @@ public static void LogCritical(DebugCategory category, string message, UnityEngi
///
/// Log RPC call
///
- public static void LogRPC(string methodName, TargetType target, bool isServer)
+ public static void LogRPC(string methodName, NetworkRPCManager.TargetType target, bool isServer)
{
if (!Instance || !Instance.m_ShowRPCCalls) return;
@@ -487,22 +488,4 @@ private void OnGUI()
}
#endif
}
-
- // ============================================================================================
- // TARGET TYPE (shared with RPC Manager)
- // ============================================================================================
-
- public enum TargetType
- {
- Self,
- LocalPlayer,
- AllPlayers,
- OtherPlayers,
- NearestPlayer,
- NearestNPC,
- Host,
- Server,
- Team,
- Custom
- }
}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs
index 689268aeb..00f70bf86 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs
@@ -126,8 +126,6 @@ public enum PlayerRole
// Ground validation settings
private const float GROUND_CHECK_DISTANCE = 10f;
- private const float GROUND_CHECK_RADIUS = 0.5f;
- private const float CHARACTER_HEIGHT_OFFSET = 0f; // ✅ FIXED: Was 1f, causing players to float 1m above ground
// Spawn point cache
private GameObject[] cachedSpawnPoints = null;
@@ -520,79 +518,6 @@ private void RequestSpawnLocalPlayerServerRpc(RpcParams rpcParams = default)
}
}
- // Note: ForceSetPlayerPositionClientRpc removed - using NetworkCharacterAdapter.ForceSetPosition() instead
- // This avoids RPC parameter API compatibility issues and leverages existing component infrastructure
-
- ///
- /// Delayed force position for last resort fixing
- ///
- // DelayedForcePosition method removed - position is now correctly handled in NetworkCharacterAdapter
- private System.Collections.IEnumerator DelayedForcePosition_REMOVED(GameObject playerObj, Vector3 targetPosition, Quaternion targetRotation, ulong clientId)
- {
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- Debug.LogWarning($"{role} [PlayerManager] DelayedForcePosition: Waiting 0.5s then forcing position...");
-
- yield return new WaitForSeconds(0.5f);
-
- if (playerObj != null)
- {
- var netObj = playerObj.GetComponent();
- // We use NetworkCharacterAdapter for position sync, not NetworkTransform
- var adapter = playerObj.GetComponent();
-
- if (adapter != null && IsServer && netObj.IsSpawned)
- {
- // Force position through our custom adapter
- adapter.ForceSetPosition(targetPosition, targetRotation);
- Debug.Log($"{role} [PlayerManager] DelayedForcePosition: Called ForceSetPosition to set position {targetPosition}");
- }
- else
- {
- // Direct set for server-authoritative
- var controller = playerObj.GetComponent();
- if (controller != null) controller.enabled = false;
-
- playerObj.transform.SetPositionAndRotation(targetPosition, targetRotation);
- Physics.SyncTransforms();
-
- if (controller != null) controller.enabled = true;
-
- Debug.Log($"{role} [PlayerManager] DelayedForcePosition: Directly set position to {targetPosition}");
- }
-
- // Final verification
- yield return null;
- float distance = Vector3.Distance(playerObj.transform.position, Vector3.zero);
- if (distance < 2f)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ FINAL: Player STILL at origin after all attempts!");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ FINAL: Player successfully moved to spawn point!");
- }
- }
- }
-
- ///
- /// OPTIMIZED: Replaced async Task with coroutine to avoid Task allocation
- ///
- private void SpawnPlayerDelayed(ulong clientId, bool isReconnection)
- {
- StartCoroutine(SpawnPlayerDelayedCoroutine(clientId, isReconnection));
- }
-
- private System.Collections.IEnumerator SpawnPlayerDelayedCoroutine(ulong clientId, bool isReconnection)
- {
- // Small delay to ensure client is ready
- yield return new WaitForSeconds(0.1f);
-
- if (NetworkManager != null && NetworkManager.ConnectedClients.ContainsKey(clientId))
- {
- SpawnPlayerInternal(clientId, isReconnection);
- }
- }
-
private void SpawnPlayerInternal(ulong clientId, bool isReconnection)
{
// ✅ FULL CONTROL: Use coroutine for deterministic spawn sequence
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup
deleted file mode 100644
index 2073f24d6..000000000
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup
+++ /dev/null
@@ -1,1955 +0,0 @@
-#nullable enable
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Unity.Netcode;
-using UnityEngine;
-using GameCreator.Runtime.Characters;
-using GameCreator.Runtime.Common;
-using Unity.Profiling;
-using GameCreator.Multiplayer.Runtime.Components;
-using GameCreator.Multiplayer.Runtime.Core;
-
-namespace GameCreator.Multiplayer.Runtime.Managers
-{
- ///
- /// Manages all networked players including connection lifecycle, spawning, and state management
- /// EXECUTION ORDER: Runs AFTER Character (-1) and NetworkCharacterAdapter (-50)
- ///
- [DefaultExecutionOrder(ApplicationManager.EXECUTION_ORDER_FIRST_LATER)] // -49: After NetworkCharacterAdapter
- public class NetworkPlayerManager : NetworkBehaviour
- {
- // ============================================================================================
- // SINGLETON
- // ============================================================================================
-
- private static NetworkPlayerManager s_Instance;
- public static NetworkPlayerManager Instance
- {
- get
- {
- if (s_Instance == null)
- {
- s_Instance = FindAnyObjectByType();
- if (s_Instance == null)
- {
- GameObject go = new GameObject("NetworkPlayerManager");
- s_Instance = go.AddComponent();
- }
- }
- return s_Instance;
- }
- }
-
- // ============================================================================================
- // CLASSES
- // ============================================================================================
-
- [Serializable]
- public class PlayerSessionData
- {
- public ulong clientId;
- public string playerName;
- public int teamId;
- public float connectionTime;
- public NetworkPlayerState state;
- public Vector3 lastKnownPosition;
- public Quaternion lastKnownRotation;
- public Dictionary customData;
- public bool isReconnecting;
- public float disconnectTime;
-
- public PlayerSessionData(ulong id)
- {
- clientId = id;
- playerName = $"Player_{id}";
- connectionTime = Time.time;
- state = NetworkPlayerState.Connecting;
- customData = new Dictionary();
- }
- }
-
- public enum NetworkPlayerState
- {
- Connecting,
- Connected,
- Spawning,
- Active,
- Dead,
- Respawning,
- Disconnecting,
- Disconnected
- }
-
- ///
- /// Player role types for clear host/client/NPC distinction
- ///
- public enum PlayerRole
- {
- HostPlayer, // Host's player character
- ClientPlayer, // Regular client player
- NPC // AI-controlled character
- }
-
- // ============================================================================================
- // FIELDS
- // ============================================================================================
-
- [Header("Player Prefabs")]
- [SerializeField] private GameObject playerPrefab;
- // Active player prefab for manual spawning
- // Keep Unity's NetworkManager.PlayerPrefab in sync with this one for consistency, but only this one is actually used.
-
- [Header("Configuration")]
- [SerializeField] private float reconnectionTimeout = 30f;
- [SerializeField] private bool persistSessionOnDisconnect = true;
- [SerializeField] private bool autoSpawnOnConnect = false; // Changed to false for trigger-based spawning
- [SerializeField] private float respawnDelay = 3f;
- [SerializeField] private float spawnDelayOnTrigger = 0f; // Additional delay when spawning from trigger
-
- [Header("Debug")]
- [SerializeField] private bool debugMode = false;
-
- // Player Registry
- private Dictionary playerSessions = new Dictionary();
- private Dictionary playerCharacters = new Dictionary();
- private Dictionary playerGameObjects = new Dictionary();
- private Dictionary inputBuffers = new Dictionary();
-
- // ============================================================================================
- // PROFILING MARKERS
- // ============================================================================================
-
- private static readonly ProfilerMarker s_SpawnMarker = new ProfilerMarker("NetworkPlayerManager.SpawnPlayer");
- private static readonly ProfilerMarker s_DespawnMarker = new ProfilerMarker("NetworkPlayerManager.DespawnPlayer");
- private static readonly ProfilerMarker s_GroundCheckMarker = new ProfilerMarker("NetworkPlayerManager.GroundCheck");
-
- // Ground validation settings
- private const float GROUND_CHECK_DISTANCE = 10f;
- private const float GROUND_CHECK_RADIUS = 0.5f;
- private const float CHARACTER_HEIGHT_OFFSET = 0f; // ✅ FIXED: Was 1f, causing players to float 1m above ground
-
- // Spawn point cache
- private GameObject[] cachedSpawnPoints = null;
- private int lastSpawnPointIndex = -1;
-
- // Local player cache
- private Character localPlayerCharacter;
- private ulong localPlayerId;
-
- // Events
- public event Action OnPlayerJoined;
- public event Action OnPlayerLeft;
- public event Action OnPlayerSpawned;
- public event Action OnPlayerDespawned;
- public event Action OnPlayerDied;
- public event Action OnPlayerRespawned;
-
- // Role-specific events
- public event Action OnHostPlayerSpawned;
- public event Action OnClientPlayerSpawned;
-
- // ============================================================================================
- // UNITY LIFECYCLE
- // ============================================================================================
-
- protected void Awake()
- {
- if (s_Instance != null && s_Instance != this)
- {
- Destroy(gameObject);
- return;
- }
- s_Instance = this;
- DontDestroyOnLoad(gameObject);
-
- // Register connection approval callback EARLY to prevent timing issues
- // This must happen before NetworkManager starts to avoid warnings
- RegisterConnectionApprovalCallback();
- }
-
- private void RegisterConnectionApprovalCallback()
- {
- if (NetworkManager.Singleton == null)
- {
- // NetworkManager not ready yet, will register in OnNetworkSpawn
- if (debugMode)
- {
- Debug.Log("[NetworkPlayerManager] NetworkManager not ready in Awake, will register callback in OnNetworkSpawn");
- }
- return;
- }
-
- // Check for conflicts
- if (NetworkManager.Singleton.ConnectionApprovalCallback != null)
- {
- Debug.LogWarning("[NetworkPlayerManager] ⚠️ ConnectionApprovalCallback already registered! " +
- "This may cause spawn conflicts. Check for NetworkConnectionApprovalHandler in scene.");
- }
-
- NetworkManager.Singleton.ConnectionApprovalCallback = OnConnectionApprovalCallback;
-
- if (debugMode)
- {
- Debug.Log("[NetworkPlayerManager] ✅ Registered ConnectionApprovalCallback");
- }
- }
-
- ///
- /// Public method to ensure ConnectionApprovalCallback is registered before StartHost/StartServer
- /// Call this from visual scripting or other systems before starting the network
- ///
- public void EnsureConnectionApprovalRegistered()
- {
- RegisterConnectionApprovalCallback();
- }
-
- public override void OnNetworkSpawn()
- {
- base.OnNetworkSpawn();
-
- Debug.Log($"[NetworkPlayerManager] OnNetworkSpawn called. IsSpawned: {IsSpawned}, IsServer: {IsServer}");
-
- if (IsServer)
- {
- // Register connection approval callback if not already registered
- if (NetworkManager.ConnectionApprovalCallback == null)
- {
- RegisterConnectionApprovalCallback();
- }
- else if (debugMode)
- {
- Debug.Log("[NetworkPlayerManager] ConnectionApprovalCallback already registered (from Awake)");
- }
-
- NetworkManager.OnClientConnectedCallback += OnClientConnected;
- NetworkManager.OnClientDisconnectCallback += OnClientDisconnected;
-
- // REMOVED: Automatic host spawn - now controlled manually via InitializeSceneForSpawning()
- // This gives full control over when spawning happens
- }
-
- if (IsClient)
- {
- localPlayerId = NetworkManager.LocalClientId;
- }
- }
-
- ///
- /// Ensure this manager is spawned on the network (call after NetworkManager starts)
- ///
- public void EnsureSpawned()
- {
- if (!NetworkManager.Singleton.IsListening)
- {
- Debug.LogWarning("[NetworkPlayerManager] Cannot spawn - network not started");
- return;
- }
-
- if (IsSpawned)
- {
- Debug.Log("[NetworkPlayerManager] Already spawned");
- return;
- }
-
- if (NetworkObject != null)
- {
- NetworkObject.Spawn();
- Debug.Log("[NetworkPlayerManager] Spawned on network");
- }
- else
- {
- Debug.LogError("[NetworkPlayerManager] No NetworkObject component! Add NetworkObject to NetworkPlayerManager GameObject.");
- }
- }
-
- public override void OnNetworkDespawn()
- {
- base.OnNetworkDespawn();
-
- if (IsServer)
- {
- // Unregister connection approval callback
- NetworkManager.ConnectionApprovalCallback = null;
-
- NetworkManager.OnClientConnectedCallback -= OnClientConnected;
- NetworkManager.OnClientDisconnectCallback -= OnClientDisconnected;
- }
- }
-
- // ============================================================================================
- // CONNECTION MANAGEMENT
- // ============================================================================================
-
- private void OnConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
- {
- if (debugMode)
- {
- Debug.Log($"[PlayerManager] Connection approval request from client {request.ClientNetworkId}");
- }
-
- // TODO: Add Supabase authentication check here
- // For now, approve all connections for development
- // Future enhancement: Validate Supabase session token from ConnectionData
-
- response.Approved = true;
- response.CreatePlayerObject = false; // We handle player spawning manually via NetworkPlayerManager
- response.PlayerPrefabHash = null;
- response.Position = null;
- response.Rotation = null;
-
- if (debugMode)
- {
- Debug.Log($"[PlayerManager] Connection approved for client {request.ClientNetworkId}");
- }
- }
-
- private void OnClientConnected(ulong clientId)
- {
- if (!IsServer) return;
-
- if (debugMode) Debug.Log($"[PlayerManager] Client {clientId} connected");
-
- // Check if this is a reconnection
- PlayerSessionData session = null;
-
- if (persistSessionOnDisconnect && playerSessions.TryGetValue(clientId, out var existingSession))
- {
- if (Time.time - existingSession.disconnectTime < reconnectionTimeout)
- {
- session = existingSession;
- session.isReconnecting = true;
- session.state = NetworkPlayerState.Connected;
-
- if (debugMode) Debug.Log($"[PlayerManager] Client {clientId} reconnected");
- }
- }
-
- if (session == null)
- {
- session = new PlayerSessionData(clientId);
- session.state = NetworkPlayerState.Connected;
- playerSessions[clientId] = session;
- }
-
- // Notify other systems
- OnPlayerJoined?.Invoke(clientId);
-
- // ✅ FIXED: Only auto-spawn if autoSpawnOnConnect is enabled
- if (autoSpawnOnConnect)
- {
- if (debugMode) Debug.Log($"[PlayerManager] Auto-spawning client {clientId} (autoSpawnOnConnect=true)");
- SpawnPlayer(clientId);
- }
- else if (debugMode)
- {
- Debug.Log($"[PlayerManager] Waiting for manual spawn trigger for client {clientId} (autoSpawnOnConnect=false)");
- }
- }
-
- private void OnClientDisconnected(ulong clientId)
- {
- if (!IsServer) return;
-
- if (debugMode) Debug.Log($"[PlayerManager] Client {clientId} disconnected");
-
- if (playerSessions.TryGetValue(clientId, out var session))
- {
- session.state = NetworkPlayerState.Disconnected;
- session.disconnectTime = Time.time;
-
- // Save last known state
- if (playerCharacters.TryGetValue(clientId, out var character))
- {
- session.lastKnownPosition = character.transform.position;
- session.lastKnownRotation = character.transform.rotation;
- }
-
- // Despawn player
- DespawnPlayer(clientId);
-
- // Remove session if not persisting
- if (!persistSessionOnDisconnect)
- {
- playerSessions.Remove(clientId);
- }
- }
-
- // Notify other systems
- OnPlayerLeft?.Invoke(clientId);
- }
-
- // ============================================================================================
- // PLAYER SPAWNING
- // ============================================================================================
-
- public void SpawnPlayer(ulong clientId)
- {
- if (!IsServer)
- {
- Debug.LogError("[PlayerManager] Only server can spawn players");
- return;
- }
-
- SpawnPlayerInternal(clientId, false);
- }
-
- ///
- /// Spawn all connected players that haven't been spawned yet.
- /// Useful for trigger-based spawning from GameCreator.
- ///
- public void SpawnAllConnectedPlayers()
- {
- if (!IsServer)
- {
- Debug.LogError("[PlayerManager] Only server can spawn players");
- return;
- }
-
- StartCoroutine(SpawnAllConnectedPlayersCoroutine());
- }
-
- private System.Collections.IEnumerator SpawnAllConnectedPlayersCoroutine()
- {
- // Apply any trigger-based spawn delay
- if (spawnDelayOnTrigger > 0)
- {
- yield return new WaitForSeconds(spawnDelayOnTrigger);
- }
-
- foreach (var kvp in playerSessions)
- {
- ulong clientId = kvp.Key;
- PlayerSessionData session = kvp.Value;
-
- // Only spawn if connected but not yet spawned
- if (session.state == NetworkPlayerState.Connected &&
- !playerGameObjects.ContainsKey(clientId))
- {
- if (debugMode) Debug.Log($"[PlayerManager] Spawning player {clientId} from trigger");
- SpawnPlayerInternal(clientId, session.isReconnecting);
-
- // Small delay between spawns to prevent congestion
- yield return new WaitForSeconds(0.1f);
- }
- }
- }
-
- ///
- /// Spawn the local player only. Useful for single-player trigger spawning.
- /// This sends a request to the server to spawn this client's player.
- ///
- public void SpawnLocalPlayer()
- {
- if (!IsSpawned)
- {
- Debug.LogError("[NetworkPlayerManager] Cannot spawn player - NetworkPlayerManager is not spawned on the network!");
- return;
- }
-
- ulong localClientId = NetworkManager.LocalClientId;
-
- // Duplicate spawn protection
- if (playerGameObjects.ContainsKey(localClientId) || playerCharacters.ContainsKey(localClientId))
- {
- if (debugMode) Debug.LogWarning($"[PlayerManager] Duplicate spawn attempt blocked for client {localClientId}");
- return;
- }
-
- // Scene-wide check (last resort)
- var allNetworkCharacters = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
- foreach (var netChar in allNetworkCharacters)
- {
- if (netChar.NetworkObject != null && netChar.NetworkObject.IsSpawned && netChar.NetworkObject.OwnerClientId == localClientId)
- {
- // Recovery: re-add to dictionaries
- playerGameObjects[localClientId] = netChar.gameObject;
- if (netChar.GetCharacter() != null)
- {
- playerCharacters[localClientId] = netChar.GetCharacter();
- }
- if (debugMode) Debug.LogWarning($"[PlayerManager] Found existing player in scene, re-registered in dictionaries");
- return;
- }
- }
-
- if (IsServer)
- {
- SpawnPlayer(NetworkManager.LocalClientId);
- }
- else
- {
- if (localClientId == 0)
- {
- Debug.LogError("[PlayerManager] Client has LocalClientId=0! Aborting spawn.");
- return;
- }
- RequestSpawnLocalPlayerServerRpc();
- }
- }
-
- [Rpc(SendTo.Server)]
- private void RequestSpawnLocalPlayerServerRpc(RpcParams rpcParams = default)
- {
- ulong clientId = rpcParams.Receive.SenderClientId;
-
- if (clientId == 0)
- {
- Debug.LogError("[PlayerManager] SenderClientId is 0! This RPC should only be called by clients.");
- return;
- }
-
- if (!NetworkManager.ConnectedClientsIds.Contains(clientId))
- {
- Debug.LogError($"[PlayerManager] Client {clientId} is not in ConnectedClientsIds. Aborting spawn.");
- return;
- }
-
- if (playerSessions.TryGetValue(clientId, out var session) && session.state == NetworkPlayerState.Connected)
- {
- if (debugMode) Debug.Log($"[PlayerManager] Client {clientId} requested spawn");
- SpawnPlayerInternal(clientId, session.isReconnecting);
- }
- else if (playerSessions.TryGetValue(clientId, out var session2))
- {
- if (debugMode) Debug.LogWarning($"[PlayerManager] Client {clientId} requested spawn but state is {session2.state}");
- }
- else
- {
- Debug.LogError($"[PlayerManager] Client {clientId} requested spawn but no session found!");
- }
- }
-
- ///
- /// OPTIMIZED: Replaced async Task with coroutine to avoid Task allocation
- ///
- private void SpawnPlayerDelayed(ulong clientId, bool isReconnection)
- {
- StartCoroutine(SpawnPlayerDelayedCoroutine(clientId, isReconnection));
- }
-
- private System.Collections.IEnumerator SpawnPlayerDelayedCoroutine(ulong clientId, bool isReconnection)
- {
- // Small delay to ensure client is ready
- yield return new WaitForSeconds(0.1f);
-
- if (NetworkManager != null && NetworkManager.ConnectedClients.ContainsKey(clientId))
- {
- SpawnPlayerInternal(clientId, isReconnection);
- }
- }
-
- private void SpawnPlayerInternal(ulong clientId, bool isReconnection)
- {
- // ✅ FULL CONTROL: Use coroutine for deterministic spawn sequence
- StartCoroutine(SpawnPlayerWithFullControl(clientId, isReconnection));
- }
-
- ///
- /// Spawn player with full control over execution timing and order
- /// Uses coroutine for deterministic sequence and state machine for tracking
- ///
- private System.Collections.IEnumerator SpawnPlayerWithFullControl(ulong clientId, bool isReconnection)
- {
- float spawnTime = Time.time;
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- bool isHostPlayer = clientId == NetworkManager.LocalClientId;
- string playerType = isHostPlayer ? "HOST PLAYER" : "CLIENT PLAYER";
-
- Debug.Log($"[TIMING] SpawnPlayerWithFullControl({clientId}) started at Time.time={spawnTime:F2}s, Frame={Time.frameCount}");
-
- // CRITICAL: Check for duplicate spawn BEFORE doing anything
- if (playerGameObjects.ContainsKey(clientId))
- {
- GameObject existingPlayer = playerGameObjects[clientId];
- Debug.LogError($"{role} [PlayerManager] ❌❌❌ DUPLICATE SPAWN BLOCKED AT ENTRY POINT! ❌❌❌");
- Debug.LogError($"{role} [PlayerManager] Player for client {clientId} ALREADY EXISTS!");
- Debug.LogError($"{role} [PlayerManager] Existing player: {(existingPlayer != null ? existingPlayer.name : "NULL")}");
- Debug.LogError($"{role} [PlayerManager] SpawnPlayerInternal() will ABORT to prevent duplicate!");
-
- // Log call stack to identify spawn source
- var stackTrace = System.Environment.StackTrace;
- var lines = stackTrace.Split('\n');
- var relevantLines = new System.Text.StringBuilder();
- relevantLines.AppendLine($"{role} [PlayerManager] Spawn Call Source:");
- int lineCount = 0;
- foreach (var line in lines)
- {
- if (lineCount >= 12) break;
- if (line.Contains("SpawnPlayerInternal") || line.Contains("SpawnPlayer") ||
- line.Contains("RequestSpawnLocalPlayerServerRpc") || line.Contains("Instruction") ||
- line.Contains("Trigger") || line.Contains("EventOn") || line.Contains("OnClientConnected"))
- {
- relevantLines.AppendLine($" {line.Trim()}");
- lineCount++;
- }
- }
- Debug.LogError(relevantLines.ToString());
- yield break; // Exit coroutine early
- }
-
- Debug.Log($"{role} [PlayerManager] ========== SpawnPlayerInternal CALLED ==========");
- Debug.Log($"{role} [PlayerManager] Spawning {playerType} for clientId: {clientId}");
- Debug.Log($"{role} [PlayerManager] Server's LocalClientId: {NetworkManager.LocalClientId}");
- Debug.Log($"{role} [PlayerManager] Is this the host's player? {isHostPlayer}");
- Debug.Log($"{role} [PlayerManager] IsReconnection: {isReconnection}");
- Debug.Log($"{role} [PlayerManager] IsServer: {IsServer}, IsHost: {IsHost}, IsClient: {IsClient}");
-
- using (s_SpawnMarker.Auto())
- {
- if (!playerSessions.TryGetValue(clientId, out var session))
- {
- Debug.LogError($"[PlayerManager] ❌ No session found for client {clientId}");
- Debug.LogError($"[PlayerManager] Available sessions: {string.Join(", ", playerSessions.Keys)}");
- yield break; // Exit coroutine early
- }
-
- session.state = NetworkPlayerState.Spawning;
-
- // Get spawn position
- Vector3 spawnPosition;
- Quaternion spawnRotation;
-
- if (isReconnection && session.lastKnownPosition != Vector3.zero)
- {
- // Restore last position for reconnection
- spawnPosition = session.lastKnownPosition;
- spawnRotation = session.lastKnownRotation;
-
- Debug.Log($"[PlayerManager] RECONNECTION: Using saved position {spawnPosition}");
- }
- else
- {
- // Get spawn point - CRITICAL: All players must spawn at designated spawn points
- Debug.Log($"[PlayerManager] ========== SPAWN POINT SELECTION ==========");
- Debug.Log($"[PlayerManager] Scene: {UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}");
- Debug.Log($"[PlayerManager] Player Type: {playerType}");
- Debug.Log($"[PlayerManager] Client ID: {clientId}");
-
- // Force refresh spawn points cache
- cachedSpawnPoints = null;
-
- if (!GetNextSpawnPoint(out spawnPosition, out spawnRotation))
- {
- // CRITICAL ERROR: No spawn points found
- Debug.LogError($"{role} [PlayerManager] ❌ ❌ ❌ CRITICAL ERROR: NO SPAWN POINTS FOUND! ❌ ❌ ❌");
- Debug.LogError($"{role} [PlayerManager] Scene: {UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}");
- Debug.LogError($"{role} [PlayerManager] Required: GameObjects with tag 'SpawnPoint' in active scene");
- Debug.LogError($"{role} [PlayerManager] Current spawn point tag: 'SpawnPoint'");
-
- // Search for potential spawn point objects that might be incorrectly tagged
- var allObjects = GameObject.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None);
- var possibleSpawnPoints = new System.Collections.Generic.List();
-
- foreach (var obj in allObjects)
- {
- if (obj.name.ToLower().Contains("spawn"))
- {
- possibleSpawnPoints.Add(obj);
- }
- }
-
- if (possibleSpawnPoints.Count > 0)
- {
- Debug.LogError($"{role} [PlayerManager] ⚠️ Found {possibleSpawnPoints.Count} objects with 'spawn' in name:");
- foreach (var obj in possibleSpawnPoints)
- {
- Debug.LogError($"{role} [PlayerManager] - '{obj.name}' (tag: '{obj.tag}', active: {obj.activeInHierarchy}, position: {obj.transform.position})");
- }
- Debug.LogError($"{role} [PlayerManager] HINT: These objects might need 'SpawnPoint' tag!");
- Debug.LogError($"{role} [PlayerManager] Fix: Select objects in hierarchy → Inspector → Tag → SpawnPoint");
- }
- else
- {
- Debug.LogError($"{role} [PlayerManager] No objects with 'spawn' in name found!");
- Debug.LogError($"{role} [PlayerManager] You need to create spawn point GameObjects in your scene!");
- }
-
- // 🔧 AUTO-FIX: Create spawn points dynamically if none exist
- Debug.LogWarning($"{role} [PlayerManager] 🔧 AUTO-FIX: Creating spawn points dynamically...");
- CreateDefaultSpawnPoints();
-
- // Try again after creating spawn points
- cachedSpawnPoints = null;
- if (!GetNextSpawnPoint(out spawnPosition, out spawnRotation))
- {
- Debug.LogError($"{role} [PlayerManager] ❌ Auto-fix failed! Using fallback position.");
- // Use fallback position as last resort
- spawnPosition = new Vector3(0f, 2f, 0f); // Start slightly elevated
- spawnRotation = Quaternion.identity;
- Debug.LogError($"{role} [PlayerManager] ⚠️ Using fallback position: (0, 2, 0)");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ Auto-fix successful! Using created spawn point.");
- }
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ ✅ ✅ SPAWN POINT SELECTED SUCCESSFULLY ✅ ✅ ✅");
- Debug.Log($"{role} [PlayerManager] Selected spawn point position: {spawnPosition}");
- Debug.Log($"{role} [PlayerManager] Selected spawn point rotation: {spawnRotation.eulerAngles}");
- Debug.Log($"{role} [PlayerManager] {playerType} will spawn at designated spawn point");
- }
- }
-
- // Validate and adjust spawn position for ground
- Vector3 finalSpawnPosition = ValidateSpawnPosition(spawnPosition, clientId);
-
- Debug.Log($"{role} [PlayerManager] Spawn position BEFORE validation: {spawnPosition}");
- Debug.Log($"{role} [PlayerManager] Spawn position AFTER validation: {finalSpawnPosition}");
- Debug.Log($"{role} [PlayerManager] Spawning {playerType} at {finalSpawnPosition}");
-
- // ✅ CRITICAL FIX: Instantiate at CORRECT position, not origin
- // Instantiating at origin causes CharacterController to initialize at wrong position
- // This leads to player spawning at (0,0,0) and getting stuck
- GameObject playerObj = Instantiate(playerPrefab, finalSpawnPosition, spawnRotation);
-
- // CRITICAL: Better GameObject naming for debugging
- string playerName;
- if (isHostPlayer)
- {
- playerName = $"Player_Host_{clientId}";
- }
- else
- {
- playerName = $"Player_Client_{clientId}";
- }
- playerObj.name = playerName;
-
- Debug.Log($"{role} [PlayerManager] Instantiated {playerType} GameObject: {playerObj.name} at position: {playerObj.transform.position}");
-
- // ✅ STEP 2: Get and validate required components ONCE (no duplicates!)
- NetworkObject netObj = playerObj.GetComponent();
- Character character = playerObj.GetComponent();
- NetworkCharacterAdapter networkAdapter = playerObj.GetComponent();
-
- // Validate all required components exist
- if (netObj == null)
- {
- Debug.LogError($"[PlayerManager] CRITICAL: Player prefab missing NetworkObject component! Cannot spawn. Fix prefab: {playerPrefab.name}");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break;
- }
-
- if (character == null)
- {
- Debug.LogError($"[PlayerManager] CRITICAL: Player prefab missing Character component! Cannot spawn. Fix prefab: {playerPrefab.name}");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break;
- }
-
- if (networkAdapter == null)
- {
- Debug.LogError($"{role} [PlayerManager] CRITICAL: NetworkCharacterAdapter component not found on prefab!");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break;
- }
-
- // ✅ STEP 3: Pre-configure components BEFORE spawning
- Debug.Log($"{role} [PlayerManager] Configuring components before network spawn...");
-
- // CRITICAL: NetworkObject must NOT be spawned yet
- if (netObj.IsSpawned)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ CRITICAL: NetworkObject is already spawned! Cannot call SpawnAsPlayerObject()!");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break;
- }
-
- // ═══════════════════════════════════════════════════════════════════════════════
- // ROOT CAUSE FIX: CharacterController Position/Center Correction
- // ═══════════════════════════════════════════════════════════════════════════════
- // PROBLEM: Instantiate() calls Awake() immediately, which creates CharacterController
- // with center (0,0,0) because IsNetworkSpawned is false at that point.
- // Unity physics detects capsule underground → teleports character to origin.
- //
- // SOLUTION: Immediately after instantiation (before Update/FixedUpdate runs):
- // 1. Disable CharacterController to prevent physics interference
- // 2. Force position back to spawn point
- // 3. Fix CharacterController.center to (0,1,0)
- // 4. Re-enable CharacterController
- // 5. Sync physics
- //
- // NOTE: We DON'T set IsNetworkSpawned=true here because it would break Update/FixedUpdate
- // logic (owner would be treated as remote). InitializeNetwork() sets it correctly.
- // ═══════════════════════════════════════════════════════════════════════════════
-
- CharacterController controller = character.GetComponent();
- bool controllerWasEnabled = false;
- if (controller != null)
- {
- controllerWasEnabled = controller.enabled;
- controller.enabled = false;
- Debug.Log($"{role} [PlayerManager] Step 1: Disabled CharacterController");
- }
-
- // ✅ FIX 2: Force position back to correct spawn point
- playerObj.transform.SetPositionAndRotation(finalSpawnPosition, spawnRotation);
- Debug.Log($"{role} [PlayerManager] Step 2: Forced position to {finalSpawnPosition}");
-
- // ✅ FIX 3: Fix CharacterController.center and re-enable
- if (controller != null)
- {
- Vector3 correctCenter = new Vector3(0f, 1f, 0f);
- controller.center = correctCenter;
- Debug.Log($"{role} [PlayerManager] Step 3: Fixed CharacterController.center to {correctCenter}");
-
- controller.enabled = controllerWasEnabled;
- Debug.Log($"{role} [PlayerManager] Step 4: Re-enabled CharacterController");
- }
-
- // ✅ FIX 4: Sync physics
- Physics.SyncTransforms();
- Debug.Log($"{role} [PlayerManager] Step 5: Physics synced - position: {playerObj.transform.position}");
-
- // ═══════════════════════════════════════════════════════════════════════════════
- // After this point, UnitDriverController.UpdatePhysicProperties() (line 377-400)
- // will continuously enforce correct center=(0,1,0) every FixedUpdate.
- // ═══════════════════════════════════════════════════════════════════════════════
-
- // Set character as player (vs NPC)
- character.IsPlayer = true;
- if (character.Player != null)
- {
- character.Player.IsControllable = true;
- }
- Debug.Log($"{role} [PlayerManager] ✅ Set Character.IsPlayer=true for client {clientId}");
-
- // Store references
- playerCharacters[clientId] = character;
- playerGameObjects[clientId] = playerObj;
-
- // Fire PrePlayerSpawn event - allows triggers to modify spawn or cancel
- var preSpawnData = new Runtime.VisualScripting.PrePlayerSpawnEventData
- {
- ClientId = clientId,
- SpawnPosition = finalSpawnPosition,
- SpawnRotation = spawnRotation,
- CanCancel = false
- };
- EventBus.Fire(preSpawnData);
-
- // Check if spawn was cancelled by event handlers
- if (preSpawnData.CanCancel)
- {
- Debug.LogWarning($"[PlayerManager] Spawn cancelled by PrePlayerSpawn event handler for client {clientId}");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break; // Exit coroutine early
- }
-
- // Update spawn position/rotation if modified by event handlers
- if (preSpawnData.SpawnPosition != finalSpawnPosition || preSpawnData.SpawnRotation != spawnRotation)
- {
- finalSpawnPosition = preSpawnData.SpawnPosition;
- spawnRotation = preSpawnData.SpawnRotation;
- playerObj.transform.position = finalSpawnPosition;
- playerObj.transform.rotation = spawnRotation;
- Debug.Log($"[PlayerManager] Spawn position/rotation modified by PrePlayerSpawn event: {finalSpawnPosition}");
- }
-
- // Spawn on network as player object
- Debug.Log($"{role} [PlayerManager] ========== CALLING SpawnAsPlayerObject ==========");
- Debug.Log($"{role} [PlayerManager] Spawning {playerType} for clientId: {clientId}");
- Debug.Log($"{role} [PlayerManager] Server LocalClientId: {NetworkManager.LocalClientId}");
- Debug.Log($"{role} [PlayerManager] Is this the host's player? {isHostPlayer}");
- Debug.Log($"{role} [PlayerManager] NetworkObject BEFORE spawn:");
- Debug.Log($"{role} [PlayerManager] IsSpawned: {netObj.IsSpawned}");
- Debug.Log($"{role} [PlayerManager] OwnerClientId: {netObj.OwnerClientId}");
- Debug.Log($"{role} [PlayerManager] IsPlayerObject: {netObj.IsPlayerObject}");
- Debug.Log($"{role} [PlayerManager] Player position BEFORE: {playerObj.transform.position}");
-
- // CRITICAL: Log what we're about to do
- if (isHostPlayer)
- {
- Debug.Log($"{role} [PlayerManager] ✅ Host player - calling SpawnAsPlayerObject({clientId}) is CORRECT");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ⚠️ Client player - calling SpawnAsPlayerObject({clientId})");
- Debug.Log($"{role} [PlayerManager] ⚠️ Expected: clientId should be {clientId} (the client's ID, NOT 0)");
-
- // CRITICAL: Abort if clientId is 0 for a client player
- if (clientId == 0)
- {
- Debug.LogError($"{role} [PlayerManager] ❌❌❌ CRITICAL ERROR: clientId is 0 (host) but this is a CLIENT player!");
- Debug.LogError($"{role} [PlayerManager] ❌ This will cause ownership to be wrong - player will be tagged as RemotePlayer!");
- Debug.LogError($"{role} [PlayerManager] ❌ ABORTING SPAWN to prevent ownership issues!");
- Destroy(playerObj);
- session.state = NetworkPlayerState.Connected;
- yield break; // Exit coroutine early
- }
- }
-
- // ✅ STEP 3: Set position BEFORE network spawn
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] STEP 3: SETTING POSITION");
- Debug.Log($"{role} [PlayerManager] Final spawn position: {finalSpawnPosition}");
- Debug.Log($"{role} [PlayerManager] Current position (before): {playerObj.transform.position}");
-
- playerObj.transform.SetPositionAndRotation(finalSpawnPosition, spawnRotation);
- Physics.SyncTransforms(); // CRITICAL: Sync physics with transform immediately
-
- Debug.Log($"{role} [PlayerManager] ✅ Position set: {playerObj.transform.position}");
- yield return null; // Wait one frame for position to be set
-
- // ✅ STEP 4: Spawn on network
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] STEP 4: NETWORK SPAWN");
- Debug.Log($"{role} [PlayerManager] Calling SpawnAsPlayerObject({clientId})");
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- netObj.SpawnAsPlayerObject(clientId);
-
- // ✅ STEP 5: Wait for OnNetworkSpawn() to complete
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] STEP 5: WAITING FOR OnNetworkSpawn()");
- Debug.Log($"{role} [PlayerManager] Waiting for NetworkObject.IsSpawned...");
-
- float timeout = 5f; // 5 second timeout
- float elapsed = 0f;
- while (!networkAdapter.IsSpawned && elapsed < timeout)
- {
- yield return null;
- elapsed += Time.deltaTime;
- }
-
- if (!networkAdapter.IsSpawned)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ TIMEOUT: NetworkObject.IsSpawned still false after {timeout}s!");
- yield break;
- }
-
- Debug.Log($"{role} [PlayerManager] ✅ OnNetworkSpawn() completed - IsSpawned: {networkAdapter.IsSpawned}");
- yield return null; // Wait one frame for state to stabilize
-
- // ✅ STEP 6: Verify Character network initialization (PHASE 5 automatic)
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] STEP 6: VERIFYING CHARACTER INITIALIZATION");
- Debug.Log($"{role} [PlayerManager] Phase 5: Character.InitializeNetwork() happens automatically!");
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
-
- // Wait one frame for Character.InitializeNetwork() to complete
- yield return null;
-
- // ✅ STEP 7: Verify Character has network state
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] STEP 7: CHECKING CHARACTER NETWORK STATE");
-
- // Character already retrieved at line 722 - no need to get it again
- if (!character.IsNetworkSpawned)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ TIMEOUT: Character.IsNetworkSpawned still false!");
- yield break;
- }
-
- Debug.Log($"{role} [PlayerManager] ✅✅✅ SPAWN SEQUENCE COMPLETE ✅✅✅");
- Debug.Log($"{role} [PlayerManager] Player {clientId} is READY - IsNetworkSpawned: {character.IsNetworkSpawned}, IsNetworkOwner: {character.IsNetworkOwner}");
-
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
- Debug.Log($"{role} [PlayerManager] AFTER SpawnAsPlayerObject()");
- Debug.Log($"{role} [PlayerManager] Input clientId was: {clientId}");
- Debug.Log($"{role} [PlayerManager] IsSpawned: {netObj.IsSpawned}");
- Debug.Log($"{role} [PlayerManager] OwnerClientId (after): {netObj.OwnerClientId}");
- Debug.Log($"{role} [PlayerManager] IsOwner (on server): {netObj.IsOwner}");
- Debug.Log($"{role} [PlayerManager] IsPlayerObject: {netObj.IsPlayerObject}");
- Debug.Log($"{role} [PlayerManager] Player position AFTER spawn: {playerObj.transform.position}");
- Debug.Log($"{role} [PlayerManager] ════════════════════════════════════════════════════");
-
- // CRITICAL VALIDATION: Verify ownership was set correctly
- if (netObj.OwnerClientId != clientId)
- {
- Debug.LogError($"{role} [PlayerManager] ❌❌❌ OWNERSHIP MISMATCH DETECTED!");
- Debug.LogError($"{role} [PlayerManager] Expected OwnerClientId: {clientId}");
- Debug.LogError($"{role} [PlayerManager] Actual OwnerClientId: {netObj.OwnerClientId}");
- Debug.LogError($"{role} [PlayerManager] This is a Unity Netcode bug or timing issue!");
- Debug.LogError($"{role} [PlayerManager] Player will have WRONG ownership and control will FAIL!");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ Ownership correctly set to {clientId}");
- }
-
- // CRITICAL VALIDATION: Verify position is still correct after spawn
- // Check if position was reset to origin (within 2 units of 0,0,0)
- float distanceFromOrigin = Vector3.Distance(playerObj.transform.position, Vector3.zero);
- if (distanceFromOrigin < 2f)
- {
- Debug.LogError($"{role} [PlayerManager] ❌❌❌ CRITICAL: Position reset near origin after SpawnAsPlayerObject()!");
- Debug.LogError($"{role} [PlayerManager] Position: {playerObj.transform.position}, Distance from origin: {distanceFromOrigin}");
- Debug.LogError($"{role} [PlayerManager] Expected position: {finalSpawnPosition}");
- Debug.LogError($"{role} [PlayerManager] Attempting recovery...");
-
- // FORCE position back to spawn point
- playerObj.transform.SetPositionAndRotation(finalSpawnPosition, spawnRotation);
- Physics.SyncTransforms();
-
- float newDistance = Vector3.Distance(playerObj.transform.position, Vector3.zero);
- if (newDistance < 2f)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ Recovery failed! Position still at origin: {playerObj.transform.position}");
- }
- else
- {
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Recovery successful: {playerObj.transform.position}");
- }
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ Position verified correct after spawn: {playerObj.transform.position}");
- }
- Debug.Log($"{role} [PlayerManager] CharacterController.center BEFORE fix: {controller?.center}");
-
- // ✅ ADDITIONAL FIX: Force CharacterController.center again after network spawn
- // Double-check that it's still correct (in case something reset it)
- if (controller != null)
- {
- Vector3 correctCenter = new Vector3(0f, 1f, 0f);
- if (Vector3.Distance(controller.center, correctCenter) > 0.01f)
- {
- controller.center = correctCenter;
- Debug.Log($"{role} [PlayerManager] ✅ FIXED CharacterController.center from {controller.center} to {correctCenter}");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ CharacterController.center already correct: {correctCenter}");
- }
- }
-
- Debug.Log($"{role} [PlayerManager] CharacterController.center AFTER fix: {controller?.center}");
- Debug.Log($"{role} [PlayerManager] Character.IsNetworkSpawned: {character.IsNetworkSpawned}");
-
- // CRITICAL VALIDATION: Verify ownership was set correctly by SpawnAsPlayerObject()
- // This is what NetworkTransform relied on - if this is wrong, everything breaks
- if (!isHostPlayer && netObj.OwnerClientId == 0)
- {
- Debug.LogError($"{role} [PlayerManager] ❌❌❌ CRITICAL OWNERSHIP FAILURE ❌❌❌");
- Debug.LogError($"{role} [PlayerManager] Client player has OwnerClientId=0 (host)!");
- Debug.LogError($"{role} [PlayerManager] This means SpawnAsPlayerObject(0) was called instead of SpawnAsPlayerObject({clientId})!");
- Debug.LogError($"{role} [PlayerManager] ROOT CAUSE: clientId extraction in RequestSpawnLocalPlayerServerRpc() returned 0!");
- Debug.LogError($"{role} [PlayerManager] This player will be tagged as RemotePlayer and uncontrollable!");
- Debug.LogError($"{role} [PlayerManager] Check console logs above for clientId extraction errors!");
-
- // CRITICAL FIX: Cannot change ownership after spawn, but we MUST fix this
- // The only way is to destroy and respawn with correct clientId
- Debug.LogError($"{role} [PlayerManager] ⚠️ ATTEMPTING EMERGENCY FIX: Destroying and respawning with correct clientId...");
- Destroy(playerObj);
-
- // Respawn immediately with correct clientId
- Debug.LogError($"{role} [PlayerManager] ⚠️ Respawning player with correct clientId {clientId}...");
- StartCoroutine(RespawnPlayerWithCorrectClientId(clientId, finalSpawnPosition, spawnRotation, isReconnection));
- yield break; // Exit early - respawn will happen in coroutine
- }
- else if (!isHostPlayer && netObj.OwnerClientId != clientId)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ VALIDATION FAILED: OwnerClientId ({netObj.OwnerClientId}) != expected clientId ({clientId})!");
- Debug.LogError($"{role} [PlayerManager] This indicates SpawnAsPlayerObject was called with wrong clientId!");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅✅✅ OWNERSHIP VALIDATION PASSED ✅✅✅");
- Debug.Log($"{role} [PlayerManager] OwnerClientId={netObj.OwnerClientId}, expected={clientId}");
- Debug.Log($"{role} [PlayerManager] On client {clientId}, IsOwner will be TRUE");
- Debug.Log($"{role} [PlayerManager] On all other clients, IsOwner will be FALSE");
- }
-
- // NEW: NETWORK VISIBILITY VERIFICATION
- Debug.Log($"{role} [PlayerManager] ========== NETWORK VISIBILITY VERIFICATION ==========");
-
- // Verify NetworkObject is spawned
- if (!netObj.IsSpawned)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ CRITICAL: NetworkObject.IsSpawned is FALSE after SpawnAsPlayerObject!");
- Debug.LogError($"{role} [PlayerManager] This indicates spawn failure - player will NOT be visible on network!");
- Debug.LogError($"{role} [PlayerManager] Player object may need to be destroyed and respawned");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] ✅ NetworkObject.IsSpawned: TRUE");
- Debug.Log($"{role} [PlayerManager] NetworkObjectId: {netObj.NetworkObjectId}");
- }
-
- // If we're the server, verify all connected clients should see this player
- if (IsServer)
- {
- var connectedClients = NetworkManager.ConnectedClientsIds;
- Debug.Log($"{role} [PlayerManager] Connected clients count: {connectedClients.Count}");
- Debug.Log($"{role} [PlayerManager] Connected client IDs: {string.Join(", ", connectedClients)}");
-
- Debug.Log($"{role} [PlayerManager] This player (NetworkObjectId: {netObj.NetworkObjectId}) should be visible to:");
- foreach (var otherClientId in connectedClients)
- {
- if (otherClientId == clientId)
- {
- Debug.Log($"{role} [PlayerManager] - Client {otherClientId} (OWNER - will see as local player)");
- }
- else
- {
- Debug.Log($"{role} [PlayerManager] - Client {otherClientId} (REMOTE - will see as remote player)");
- }
- }
-
- // List all currently spawned players for visibility tracking
- Debug.Log($"{role} [PlayerManager] Currently spawned players in scene:");
- foreach (var kvp in playerGameObjects)
- {
- if (kvp.Value != null)
- {
- var playerNetObj = kvp.Value.GetComponent();
- if (playerNetObj != null && playerNetObj.IsSpawned)
- {
- Debug.Log($"{role} [PlayerManager] - Client {kvp.Key}: {kvp.Value.name} (NetworkObjectId: {playerNetObj.NetworkObjectId})");
- }
- }
- }
-
- // CRITICAL: For client player spawns, verify host player is already spawned
- if (!isHostPlayer && connectedClients.Count >= 2)
- {
- if (playerGameObjects.ContainsKey(0) && playerGameObjects[0] != null)
- {
- var hostNetObj = playerGameObjects[0].GetComponent();
- if (hostNetObj != null && hostNetObj.IsSpawned)
- {
- Debug.Log($"{role} [PlayerManager] ✅ Host player is spawned (NetworkObjectId: {hostNetObj.NetworkObjectId})");
- Debug.Log($"{role} [PlayerManager] ✅ Client should be able to see host player");
- }
- else
- {
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Host player exists but is NOT spawned on network!");
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Client may not see host player!");
- }
- }
- else
- {
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Host player not found in playerGameObjects dictionary!");
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Client may not see host player!");
- }
- }
- }
-
- Debug.Log($"{role} [PlayerManager] ========== VISIBILITY VERIFICATION COMPLETE ==========");
-
- // VALIDATION: Check CharacterController.center not reset after spawn
- // GameCreator's FixedUpdate might try to reset it, so check after one physics frame
- StartCoroutine(ValidateCharacterControllerCenterAfterSpawn(character, clientId));
-
- // NOTE: Client notification is handled automatically by NetworkCharacterAdapter.OnNetworkSpawn()
- // The OnNetworkSpawn() method runs on all clients when the NetworkObject spawns
- // No additional notification RPC needed - NetworkObject spawning handles this automatically
-
- // PHASE 5: Motion initialization handled automatically via invasive hooks
- // Character.Motion checks IsNetworkOwner internally (Phase 2 hook)
- Debug.Log($"[PlayerManager] ========== MOTION SYSTEM DIAGNOSTIC ==========");
- Debug.Log($"[PlayerManager] clientId: {clientId}");
- Debug.Log($"[PlayerManager] Motion initialization automatic via Phase 2 hooks");
- Debug.Log($"[PlayerManager] Character.Motion checks IsNetworkOwner internally");
-
- if (character != null && character.Motion != null)
- {
- // Verify motion initialization after NetworkCharacterAdapter.OnNetworkSpawn() completes
- StartCoroutine(CompleteMotionInitialization(character, clientId));
- Debug.Log($"[PlayerManager] ✅ Motion verification coroutine started for client {clientId}");
- }
- else
- {
- Debug.LogWarning($"[PlayerManager] ⚠️ Character or Motion is NULL!");
- Debug.LogWarning($"[PlayerManager] clientId: {clientId}");
- Debug.LogWarning($"[PlayerManager] character != null: {character != null}");
- Debug.LogWarning($"[PlayerManager] character.Motion != null: {character != null && character.Motion != null}");
- }
-
- if (debugMode)
- {
- Debug.Log($"[PlayerManager] ✅ Spawned player for client {clientId} at {finalSpawnPosition}");
- }
-
- // Update session
- session.state = NetworkPlayerState.Active;
-
- // Register with RPC Manager
- Runtime.RPC.NetworkRPCManager.Instance.RegisterPlayer(character);
-
- // Apply saved state if reconnecting
- if (isReconnection)
- {
- RestorePlayerState(clientId, character);
- }
-
- // Initialize player systems
- InitializePlayerSystemsClientRpc(clientId);
-
- // If this is the host's player, register immediately (won't wait for ClientRpc)
- if (NetworkManager.LocalClientId == clientId)
- {
- localPlayerCharacter = character;
- var resolver = Runtime.Targeting.LocalPlayerResolver.Instance;
- if (resolver != null)
- {
- resolver.RegisterLocalPlayer(playerObj);
- if (debugMode) Debug.Log($"[PlayerManager] Host player registered with LocalPlayerResolver immediately");
- }
-
- // CRITICAL FIX: Trigger OnLocalPlayerSpawned event for visual scripting
- Runtime.VisualScripting.EventOnLocalPlayerSpawned.TriggerLocalPlayerSpawned(playerObj);
- }
-
- // Notify
- OnPlayerSpawned?.Invoke(clientId, character);
-
- // Fire role-specific events
- PlayerRole playerRole = GetPlayerRole(clientId);
- if (playerRole == PlayerRole.HostPlayer)
- {
- OnHostPlayerSpawned?.Invoke(clientId, character);
- }
- else if (playerRole == PlayerRole.ClientPlayer)
- {
- OnClientPlayerSpawned?.Invoke(clientId, character);
- }
-
- // Fire PostPlayerSpawn event - notifies visual scripting that spawn is complete
- var postSpawnData = new Runtime.VisualScripting.PostPlayerSpawnEventData
- {
- ClientId = clientId,
- SpawnedPlayer = playerObj,
- IsRespawn = isReconnection
- };
- EventBus.Fire(postSpawnData);
-
- if (debugMode) Debug.Log($"[PlayerManager] ✅ Spawned player for client {clientId} at {finalSpawnPosition}");
- }
- }
-
- // ============================================================================================
- // SPAWN POINT SELECTION & VALIDATION
- // ============================================================================================
-
- ///
- /// Get the next spawn point from available spawn points (supports multiple spawn points)
- ///
- private bool GetNextSpawnPoint(out Vector3 position, out Quaternion rotation)
- {
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- Debug.Log($"{role} [PlayerManager] ========== GetNextSpawnPoint CALLED ==========");
-
- position = Vector3.zero;
- rotation = Quaternion.identity;
-
- // ALWAYS refresh spawn points (don't rely on cache)
- Debug.Log($"{role} [PlayerManager] ========== SPAWN POINT SEARCH ==========");
- Debug.Log($"{role} [PlayerManager] Scene: {UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}");
-
- cachedSpawnPoints = GameObject.FindGameObjectsWithTag("SpawnPoint");
-
- Debug.Log($"{role} [PlayerManager] Found {cachedSpawnPoints?.Length ?? 0} spawn points");
-
- if (cachedSpawnPoints != null && cachedSpawnPoints.Length > 0)
- {
- for (int i = 0; i < cachedSpawnPoints.Length; i++)
- {
- if (cachedSpawnPoints[i] != null)
- {
- Debug.Log($"[PlayerManager] [{i}] '{cachedSpawnPoints[i].name}' at {cachedSpawnPoints[i].transform.position}, active={cachedSpawnPoints[i].activeInHierarchy}");
- }
- }
- }
- else
- {
- Debug.LogError($"[PlayerManager] ❌ NO SPAWN POINTS FOUND!");
- Debug.LogError($"[PlayerManager] Make sure GameObjects are tagged 'SpawnPoint' in active scene!");
- }
-
- if (cachedSpawnPoints == null || cachedSpawnPoints.Length == 0)
- {
- Debug.LogError($"{role} [PlayerManager] GetNextSpawnPoint RETURNING: false (no spawn points found)");
- return false;
- }
-
- // Round-robin selection of spawn points
- lastSpawnPointIndex = (lastSpawnPointIndex + 1) % cachedSpawnPoints.Length;
- GameObject spawnPoint = cachedSpawnPoints[lastSpawnPointIndex];
-
- if (spawnPoint == null)
- {
- Debug.LogError($"[PlayerManager] Spawn point at index {lastSpawnPointIndex} is null! Re-caching spawn points.");
- cachedSpawnPoints = null; // Force re-cache
- return false;
- }
-
- position = spawnPoint.transform.position;
- rotation = spawnPoint.transform.rotation;
-
- Debug.Log($"{role} [PlayerManager] ✅ Selected spawn point: {spawnPoint.name} at {position}");
- Debug.Log($"{role} [PlayerManager] GetNextSpawnPoint RETURNING: true, position={position}");
-
- return true;
- }
-
- ///
- /// Validate spawn position is above ground and adjust if needed
- /// FIXED: Now supports multi-floor buildings by checking for ground NEAR spawn point, not just below
- ///
- private Vector3 ValidateSpawnPosition(Vector3 proposedPosition, ulong clientId)
- {
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- string playerType = clientId == NetworkManager.LocalClientId ? "HOST PLAYER" : "CLIENT PLAYER";
- Debug.Log($"{role} [PlayerManager] ========== ValidateSpawnPosition CALLED ==========");
- Debug.Log($"{role} [PlayerManager] Validating position for {playerType} (clientId: {clientId})");
- Debug.Log($"{role} [PlayerManager] Proposed position: {proposedPosition}");
-
- using (s_GroundCheckMarker.Auto())
- {
- // CRITICAL FIX: Don't modify spawn points that are already correctly positioned
- // If spawn point is explicitly set at a specific Y position, trust it!
-
- // First: Check if there's ground very close below (within 0.5m)
- // This handles spawn points placed exactly at floor level
- Vector3 rayStartClose = proposedPosition + Vector3.up * 0.1f;
- if (Physics.Raycast(rayStartClose, Vector3.down, out RaycastHit closeHit, 0.6f,
- ~(1 << LayerMask.NameToLayer("Player")), QueryTriggerInteraction.Ignore))
- {
- // Ground is very close - spawn point is likely correctly positioned
- // Use the spawn point position as-is (it's already at the right height)
- Debug.Log($"{role} [PlayerManager] ✅ Ground very close at Y={closeHit.point.y:F2}, " +
- $"using original spawn position Y={proposedPosition.y:F2}");
- Debug.Log($"{role} [PlayerManager] ValidateSpawnPosition RETURNING: {proposedPosition}");
-
- return proposedPosition;
- }
-
- // Second: Check for ground within reasonable distance (2m down)
- // This handles spawn points placed slightly above ground
- Vector3 rayStartNear = proposedPosition + Vector3.up * 0.5f;
- if (Physics.Raycast(rayStartNear, Vector3.down, out RaycastHit nearHit, 2.5f,
- ~(1 << LayerMask.NameToLayer("Player")), QueryTriggerInteraction.Ignore))
- {
- // Found ground nearby but not super close - adjust to ground + character offset
- // Use small offset (0.1) to ensure character spawns slightly above ground
- float characterOffset = 0.1f; // Small offset to prevent stuck in ground
- Vector3 adjustedPosition = nearHit.point + Vector3.up * characterOffset;
-
- Debug.Log($"{role} [PlayerManager] Ground found at Y={nearHit.point.y:F2}, " +
- $"adjusted to Y={adjustedPosition.y:F2} (added {characterOffset}m offset)");
- Debug.Log($"{role} [PlayerManager] ValidateSpawnPosition RETURNING: {adjustedPosition}");
-
- return adjustedPosition;
- }
-
- // Third: Check further down for ground (up to 10m)
- // This handles spawn points placed high in the air
- Vector3 rayStartFar = proposedPosition + Vector3.up * 1f;
- if (Physics.Raycast(rayStartFar, Vector3.down, out RaycastHit farHit, GROUND_CHECK_DISTANCE,
- ~(1 << LayerMask.NameToLayer("Player")), QueryTriggerInteraction.Ignore))
- {
- // Found ground far below - spawn point is definitely in air
- float characterOffset = 0.1f; // Small offset to prevent stuck in ground
- Vector3 adjustedPosition = farHit.point + Vector3.up * characterOffset;
-
- float heightDifference = Mathf.Abs(proposedPosition.y - farHit.point.y);
-
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Spawn point at Y={proposedPosition.y:F2} " +
- $"is {heightDifference:F2}m above ground at Y={farHit.point.y:F2}! Adjusted to ground level.");
- Debug.Log($"{role} [PlayerManager] ValidateSpawnPosition RETURNING: {adjustedPosition}");
-
- return adjustedPosition;
- }
- else
- {
- // No ground found at all - use spawn point as-is
- // This might be intentional (e.g., spawning in air, on a platform, etc.)
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ No ground found below {proposedPosition} within {GROUND_CHECK_DISTANCE}m! " +
- $"Using spawn point position as-is (Y={proposedPosition.y:F2}).");
-
- Debug.Log($"{role} [PlayerManager] ValidateSpawnPosition RETURNING: {proposedPosition}");
- return proposedPosition;
- }
- }
- }
-
- ///
- /// Clear spawn point cache (call when scene changes)
- ///
- public void ClearSpawnPointCache()
- {
- cachedSpawnPoints = null;
- lastSpawnPointIndex = -1;
-
- if (debugMode)
- {
- Debug.Log("[PlayerManager] Spawn point cache cleared");
- }
- }
-
- ///
- /// 🔧 AUTO-FIX: Creates default spawn points when none exist in the scene
- /// This ensures players can always spawn even without manual spawn point setup
- ///
- private void CreateDefaultSpawnPoints()
- {
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- Debug.Log($"{role} [PlayerManager] Creating 4 default spawn points in a circle...");
-
- // Create spawn points in a circle around origin
- float radius = 5f;
- for (int i = 0; i < 4; i++)
- {
- float angle = i * 90f * Mathf.Deg2Rad;
- Vector3 position = new Vector3(
- Mathf.Cos(angle) * radius,
- 0f,
- Mathf.Sin(angle) * radius
- );
-
- GameObject spawnPoint = new GameObject($"SpawnPoint_Auto_{i}");
- spawnPoint.transform.position = position;
- spawnPoint.transform.rotation = Quaternion.LookRotation(position - Vector3.zero); // Face center
- spawnPoint.tag = "SpawnPoint";
-
- // Add the SpawnPointMarker component for consistency (uses default values)
- var marker = spawnPoint.AddComponent();
-
- Debug.Log($"{role} [PlayerManager] Created spawn point: {spawnPoint.name} at {position}");
- }
-
- Debug.Log($"{role} [PlayerManager] ✅ Created 4 default spawn points");
- }
-
- ///
- /// 🔧 CRITICAL FIX: Complete motion system initialization with proper timing
- ///
- private System.Collections.IEnumerator CompleteMotionInitialization(Character character, ulong clientId)
- {
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- Debug.Log($"{role} [PlayerManager] Completing motion initialization for client {clientId}...");
-
- // Wait for physics to initialize
- yield return new WaitForFixedUpdate();
- yield return new WaitForFixedUpdate();
-
- if (character != null && character.Motion != null)
- {
- // Verify initialization was successful
- if (character.Motion.MovementType == Character.MovementType.MoveToDirection)
- {
- Debug.Log($"{role} [PlayerManager] ✅✅✅ Motion system ACTIVATED for client {clientId}! MovementType = MoveToDirection");
- Debug.Log($"{role} [PlayerManager] Player movement should now work correctly.");
- }
- else
- {
- Debug.LogError($"{role} [PlayerManager] ❌ Motion initialization FAILED for client {clientId}!");
- Debug.LogError($"{role} [PlayerManager] MovementType: {character.Motion.MovementType} (expected: MoveToDirection)");
-
- // Emergency fix: try to force activation
- try
- {
- Vector3 emergencyVelocity = Vector3.forward * 0.02f;
- character.Motion.MoveToDirection(emergencyVelocity, Space.World, 0);
- Debug.LogWarning($"{role} [PlayerManager] ⚠️ Applied emergency motion activation for client {clientId}");
- }
- catch (System.Exception ex)
- {
- Debug.LogError($"{role} [PlayerManager] ❌ Emergency motion fix also failed: {ex.Message}");
- }
- }
-
- // Log final CharacterController state
- var controller = character.GetComponent();
- if (controller != null)
- {
- Debug.Log($"{role} [PlayerManager] Final CharacterController state for client {clientId}:");
- Debug.Log($"{role} [PlayerManager] enabled: {controller.enabled}");
- Debug.Log($"{role} [PlayerManager] isGrounded: {controller.isGrounded}");
- Debug.Log($"{role} [PlayerManager] center: {controller.center}");
- Debug.Log($"{role} [PlayerManager] height: {controller.height}");
- }
- }
- }
-
- // ============================================================================================
- // DESPAWN LOGIC
- // ============================================================================================
-
- public void DespawnPlayer(ulong clientId)
- {
- if (!IsServer) return;
-
- using (s_DespawnMarker.Auto())
- {
- if (playerGameObjects.TryGetValue(clientId, out var playerObj))
- {
- // Unregister from RPC Manager
- if (playerCharacters.TryGetValue(clientId, out var character))
- {
- Runtime.RPC.NetworkRPCManager.Instance.UnregisterPlayer(character);
- }
-
- // Despawn from network
- var netObj = playerObj.GetComponent();
- if (netObj != null && netObj.IsSpawned)
- {
- netObj.Despawn();
- }
-
- // Destroy player object
- Destroy(playerObj);
-
- // Clean up references
- playerGameObjects.Remove(clientId);
- playerCharacters.Remove(clientId);
- inputBuffers.Remove(clientId);
-
- // Notify
- OnPlayerDespawned?.Invoke(clientId);
-
- if (debugMode) Debug.Log($"[PlayerManager] Despawned player for client {clientId}");
- }
- }
- }
-
- // ============================================================================================
- // PLAYER DEATH & RESPAWN
- // ============================================================================================
-
- public void OnPlayerDeath(ulong clientId)
- {
- if (!IsServer) return;
-
- if (playerSessions.TryGetValue(clientId, out var session))
- {
- session.state = NetworkPlayerState.Dead;
- OnPlayerDied?.Invoke(clientId);
-
- // Start respawn timer
- StartCoroutine(RespawnPlayer(clientId));
- }
- }
-
- private System.Collections.IEnumerator RespawnPlayer(ulong clientId)
- {
- yield return new WaitForSeconds(respawnDelay);
-
- if (playerSessions.TryGetValue(clientId, out var session))
- {
- if (session.state == NetworkPlayerState.Dead)
- {
- session.state = NetworkPlayerState.Respawning;
-
- // Despawn current player object
- DespawnPlayer(clientId);
-
- // Spawn new player object
- SpawnPlayerInternal(clientId, false);
-
- OnPlayerRespawned?.Invoke(clientId);
- }
- }
- }
-
- ///
- /// Emergency coroutine to respawn player with correct clientId when ownership is wrong
- ///
- private System.Collections.IEnumerator RespawnPlayerWithCorrectClientId(ulong correctClientId, Vector3 position, Quaternion rotation, bool isReconnection)
- {
- Debug.LogError($"[PlayerManager] ⚠️ EMERGENCY RESPAWN: Waiting 0.5s before respawning with correct clientId {correctClientId}...");
- yield return new WaitForSeconds(0.5f);
-
- Debug.LogError($"[PlayerManager] ⚠️ EMERGENCY RESPAWN: Respawning player for clientId {correctClientId}...");
- SpawnPlayerInternal(correctClientId, isReconnection);
- }
-
- // ============================================================================================
- // STATE MANAGEMENT
- // ============================================================================================
-
- private void RestorePlayerState(ulong clientId, Character character)
- {
- if (!playerSessions.TryGetValue(clientId, out var session))
- return;
-
- // TODO: Restore stats, inventory, etc. from session.customData
- // This would integrate with GameCreator's save system
-
- if (debugMode) Debug.Log($"[PlayerManager] Restored state for client {clientId}");
- }
-
- public void SavePlayerState(ulong clientId)
- {
- if (!playerSessions.TryGetValue(clientId, out var session))
- return;
-
- if (!playerCharacters.TryGetValue(clientId, out var character))
- return;
-
- // TODO: Save stats, inventory, etc. to session.customData
- // This would integrate with GameCreator's save system
-
- session.lastKnownPosition = character.transform.position;
- session.lastKnownRotation = character.transform.rotation;
- }
-
- // ============================================================================================
- // QUERIES
- // ============================================================================================
-
- public Character GetLocalPlayerCharacter()
- {
- if (localPlayerCharacter == null && IsClient)
- {
- playerCharacters.TryGetValue(localPlayerId, out localPlayerCharacter);
- }
- return localPlayerCharacter;
- }
-
- public Character GetPlayerCharacter(ulong clientId)
- {
- return playerCharacters.TryGetValue(clientId, out var character) ? character : null;
- }
-
- ///
- /// Gets the role of a player by client ID
- ///
- public PlayerRole GetPlayerRole(ulong clientId)
- {
- // Check if this is the host client ID
- if (NetworkAuthorityManager.Instance != null && NetworkAuthorityManager.Instance.IsHostClientId(clientId))
- {
- return PlayerRole.HostPlayer;
- }
-
- // Check if this is a registered player session
- if (playerSessions.ContainsKey(clientId))
- {
- return PlayerRole.ClientPlayer;
- }
-
- // Default to NPC if not found (could be an NPC)
- return PlayerRole.NPC;
- }
-
- ///
- /// Checks if a client ID belongs to the host player
- ///
- public bool IsHostPlayer(ulong clientId)
- {
- if (NetworkAuthorityManager.Instance == null)
- return false;
-
- return NetworkAuthorityManager.Instance.IsHostClientId(clientId);
- }
-
- public GameObject GetPlayerGameObject(ulong clientId)
- {
- return playerGameObjects.TryGetValue(clientId, out var go) ? go : null;
- }
-
- public PlayerSessionData GetPlayerSession(ulong clientId)
- {
- return playerSessions.TryGetValue(clientId, out var session) ? session : null;
- }
-
- public List GetAllPlayers()
- {
- return playerCharacters.Values.Where(c => c != null).ToList();
- }
-
- public List GetPlayersInTeam(int teamId)
- {
- var teamPlayers = new List();
-
- foreach (var kvp in playerSessions)
- {
- if (kvp.Value.teamId == teamId)
- {
- if (playerCharacters.TryGetValue(kvp.Key, out var character))
- {
- teamPlayers.Add(character);
- }
- }
- }
-
- return teamPlayers;
- }
-
- public int GetPlayerCount()
- {
- return playerCharacters.Count;
- }
-
- public bool IsPlayerConnected(ulong clientId)
- {
- return playerSessions.ContainsKey(clientId) &&
- playerSessions[clientId].state != NetworkPlayerState.Disconnected;
- }
-
- // Public property to expose registered players
- public IReadOnlyDictionary RegisteredPlayers => playerCharacters;
-
- // ============================================================================================
- // TEAM MANAGEMENT
- // ============================================================================================
-
- public void AssignPlayerToTeam(ulong clientId, int teamId)
- {
- if (playerSessions.TryGetValue(clientId, out var session))
- {
- session.teamId = teamId;
-
- // Update visual representation if spawned
- if (playerGameObjects.TryGetValue(clientId, out var playerObj))
- {
- UpdatePlayerTeamVisuals(playerObj, teamId);
- }
- }
- }
-
- private void UpdatePlayerTeamVisuals(GameObject playerObj, int teamId)
- {
- // TODO: Update player colors, tags, etc. based on team
- }
-
- // ============================================================================================
- // RPC METHODS
- // ============================================================================================
-
- [ClientRpc]
- private void InitializePlayerSystemsClientRpc(ulong clientId)
- {
- if (NetworkManager.LocalClientId == clientId)
- {
- // This is our player, initialize local systems
- if (playerCharacters.TryGetValue(clientId, out var character))
- {
- localPlayerCharacter = character;
-
- // CRITICAL: Double-check IsPlayer is set (should already be set from SpawnPlayerInternal)
- if (!character.IsPlayer)
- {
- character.IsPlayer = true;
- Debug.LogWarning($"[PlayerManager] Had to set IsPlayer in ClientRpc - was not set earlier!");
- }
-
- // Double-check IsControllable
- if (character.Player != null && !character.Player.IsControllable)
- {
- character.Player.IsControllable = true;
- Debug.LogWarning($"[PlayerManager] Had to set IsControllable in ClientRpc!");
- }
-
- // Register with LocalPlayerResolver immediately
- var resolver = Runtime.Targeting.LocalPlayerResolver.Instance;
- if (resolver != null)
- {
- resolver.RegisterLocalPlayer(character.gameObject);
- if (debugMode) Debug.Log($"[PlayerManager] Registered local player with LocalPlayerResolver");
- }
-
- // CRITICAL FIX: Trigger OnLocalPlayerSpawned event for visual scripting
- Runtime.VisualScripting.EventOnLocalPlayerSpawned.TriggerLocalPlayerSpawned(character.gameObject);
-
- if (debugMode) Debug.Log("[PlayerManager] ✅ Local player initialized - Input should be working now");
- }
- }
- }
-
- // ============================================================================================
- // UTILITY
- // ============================================================================================
-
- // REMOVED: Team-based player prefabs not needed - using single playerPrefab for all players
-
- ///
- /// Improved post-spawn monitoring with proper delay
- ///
-
- ///
- /// Wait for scene to be fully ready before allowing spawns
- ///
- private System.Collections.IEnumerator WaitForSceneReady(System.Action onReady)
- {
- // Wait for physics system
- yield return new WaitForFixedUpdate();
- yield return new WaitForFixedUpdate();
-
- // Wait for NetworkManager to be fully listening
- while (NetworkManager == null || !NetworkManager.IsListening)
- {
- yield return new WaitForSeconds(0.1f);
- }
-
- if (debugMode) Debug.Log("[PlayerManager] Scene ready check complete");
-
- onReady?.Invoke();
- }
-
- ///
- /// Manually initiate scene ready check and enable spawning
- /// Call this from GameCreator Instruction or UI
- ///
- public void InitializeSceneForSpawning()
- {
- if (!IsServer)
- {
- if (debugMode) Debug.LogWarning("[PlayerManager] Only server can initialize scene for spawning");
- return;
- }
-
- StartCoroutine(WaitForSceneReady(() =>
- {
- if (debugMode) Debug.Log("[PlayerManager] Scene ready - spawning enabled");
-
- // ✅ FIXED: Only auto-spawn if autoSpawnOnConnect is enabled
- // Otherwise, wait for manual trigger-based spawning
- if (autoSpawnOnConnect && IsHost && !playerGameObjects.ContainsKey(NetworkManager.LocalClientId))
- {
- if (debugMode) Debug.Log("[PlayerManager] Auto-spawning host player (autoSpawnOnConnect=true)");
- SpawnPlayer(NetworkManager.LocalClientId);
- }
- else if (debugMode)
- {
- Debug.Log("[PlayerManager] Waiting for manual spawn trigger (autoSpawnOnConnect=false)");
- }
- }));
- }
-
- ///
- /// Set spawn position using FusionGameCreator pattern
- /// Waits for Character's initialization to complete, then sets position
- /// This matches FusionGameCreator's exact approach: SetPositionAndRotation + Physics.SyncTransforms()
- ///
- private System.Collections.IEnumerator SetSpawnPositionFusionPattern(GameObject playerObj, Vector3 position, Quaternion rotation, NetworkCharacterAdapter networkAdapter)
- {
- if (playerObj == null) yield break;
-
- // Wait for Character component to fully initialize (Awake/Start complete)
- Character character = playerObj.GetComponent();
- int waitFrames = 0;
- while (character == null && waitFrames < 10)
- {
- yield return null;
- character = playerObj.GetComponent();
- waitFrames++;
- }
-
- if (character == null)
- {
- Debug.LogError("[PlayerManager] ❌ Character component not found after 10 frames!");
- yield break;
- }
-
- // Wait one more frame to ensure Character's Start() has run
- yield return null;
-
- // ✅ FUSION PATTERN: Set position AFTER spawn and initialization
- // This is the EXACT pattern from FusionGameCreator PlayerManager.cs line 252-253
- playerObj.transform.SetPositionAndRotation(position, rotation);
- Physics.SyncTransforms(); // CRITICAL: Sync physics with transform
-
- // Store intended position for NetworkCharacter (if needed)
- // Phase 5: No SetIntendedSpawnPosition needed - NetworkCharacterAdapter handles spawn position automatically
- // Position/rotation are set directly via transform before this coroutine
- if (networkAdapter != null)
- {
- // Verify NetworkCharacterAdapter is ready (just for logging/debugging)
- }
-
- string role = IsServer ? "[HOST]" : "[CLIENT]";
- Debug.Log($"{role} [PlayerManager] ✅ Set spawn position (Fusion pattern): {position}");
- Debug.Log($"{role} [PlayerManager] ✅ Called Physics.SyncTransforms()");
- Debug.Log($"{role} [PlayerManager] ✅ Verified position: {playerObj.transform.position}");
-
- // Verify CharacterController is synced
- CharacterController cc = character.GetComponent();
- if (cc != null)
- {
- Debug.Log($"{role} [PlayerManager] ✅ CharacterController.center: {cc.center}");
- Debug.Log($"{role} [PlayerManager] ✅ CharacterController.height: {cc.height}");
- }
- }
-
- ///
- /// Validate that CharacterController.center was not reset by GameCreator after spawn
- /// This checks the GameCreator Character override (IsNetworkSpawned flag) is working
- ///
- private System.Collections.IEnumerator ValidateCharacterControllerCenterAfterSpawn(Character character, ulong clientId)
- {
- // Wait for GameCreator's FixedUpdate to run at least once
- // This is when UnitDriverController.UpdatePhysicProperties() would reset the center
- yield return new WaitForFixedUpdate();
- yield return new WaitForFixedUpdate(); // Wait one more frame to be safe
-
- CharacterController controller = character.GetComponent();
- if (controller == null)
- {
- Debug.LogWarning($"[PlayerManager] Cannot validate CharacterController for client {clientId} - component missing");
- yield break;
- }
-
- Vector3 expectedCenter = new Vector3(0, 1, 0);
-
- if (Vector3.Distance(controller.center, expectedCenter) > 0.01f)
- {
- Debug.LogError($"[PlayerManager] ❌ CharacterController.center VALIDATION FAILED for client {clientId}!");
- Debug.LogError($"[PlayerManager] Expected: {expectedCenter}, Got: {controller.center}");
- // Log network spawned status in error
- var networkSpawnedProp2 = character.GetType().GetProperty("IsNetworkSpawned");
- bool isNetworkSpawned2 = false;
- if (networkSpawnedProp2 != null)
- {
- try { isNetworkSpawned2 = (bool)networkSpawnedProp2.GetValue(character); }
- catch { /* ignore */ }
- }
- Debug.LogError($"[PlayerManager] Character.IsNetworkSpawned: {isNetworkSpawned2}");
- Debug.LogError("[PlayerManager] This means GameCreator override may not be working correctly.");
- Debug.LogError("[PlayerManager] Check that Character.cs and UnitDriverController.cs have the override code.");
- Debug.LogError("[PlayerManager] See: claudedocs/GAMECREATOR_CHARACTER_OVERRIDE_COMPLETE.md");
-
- // Recovery attempt
- controller.center = expectedCenter;
- Debug.LogWarning($"[PlayerManager] Forced CharacterController.center back to {expectedCenter}");
- }
- else
- {
- if (debugMode)
- {
- Debug.Log($"[PlayerManager] ✓ CharacterController.center validation passed for client {clientId}");
- // Log network spawned status
- var networkSpawnedProp3 = character.GetType().GetProperty("IsNetworkSpawned");
- bool isNetworkSpawned3 = false;
- if (networkSpawnedProp3 != null)
- {
- try { isNetworkSpawned3 = (bool)networkSpawnedProp3.GetValue(character); }
- catch { /* ignore */ }
- }
- Debug.Log($"[PlayerManager] Center: {controller.center}, IsNetworkSpawned: {isNetworkSpawned3}");
- }
- }
- }
-
- ///
- /// Check if scene is ready for spawning
- ///
- public bool IsSceneReadyForSpawning()
- {
- return NetworkManager != null && NetworkManager.IsListening;
- }
-
- public void SetPlayerName(ulong clientId, string playerName)
- {
- if (playerSessions.TryGetValue(clientId, out var session))
- {
- session.playerName = playerName;
-
- if (playerGameObjects.TryGetValue(clientId, out var playerObj))
- {
- playerObj.name = playerName;
- }
- }
- }
- }
-}
-
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup.meta b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup.meta
deleted file mode 100644
index b49c8cef1..000000000
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Managers/NetworkPlayerManager.cs.backup.meta
+++ /dev/null
@@ -1,7 +0,0 @@
-fileFormatVersion: 2
-guid: a47459d7d4afd824b8bdafce09684b12
-DefaultImporter:
- externalObjects: {}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncBase.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncBase.cs
new file mode 100644
index 000000000..608072ec3
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncBase.cs
@@ -0,0 +1,382 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Multiplayer.Runtime.Core;
+
+namespace GameCreator.Multiplayer.Runtime.Modules
+{
+ ///
+ /// Base class for all GameCreator network module sync components.
+ /// Provides common functionality: dirty tracking, sync throttling, authority handling.
+ ///
+ /// AUTHORITY PATTERNS:
+ /// - Owner Authority: Movement, input-driven state
+ /// - Server Authority: Stats, inventory, game state (anti-cheat)
+ /// - Hybrid: Prediction with server validation
+ ///
+ /// Inherit from this class when creating new module sync components.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public abstract class NetworkModuleSyncBase : NetworkBehaviour, INetworkModuleSync
+ {
+ // ============================================================================================
+ // ENUMS
+ // ============================================================================================
+
+ public enum AuthorityMode
+ {
+ OwnerAuthority, // Owner writes, others read
+ ServerAuthority, // Server writes, all read
+ Hybrid // Client predicts, server validates
+ }
+
+ public enum SyncPriority
+ {
+ Low = 0, // 1-2 Hz (rarely changing data)
+ Normal = 1, // 5-10 Hz (standard sync)
+ High = 2, // 20-30 Hz (important state)
+ Critical = 3 // 60+ Hz (movement, animation)
+ }
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Sync Settings")]
+ [Tooltip("Authority model for this module")]
+ [SerializeField] protected AuthorityMode m_AuthorityMode = AuthorityMode.ServerAuthority;
+
+ [Tooltip("Sync priority determines update frequency")]
+ [SerializeField] protected SyncPriority m_Priority = SyncPriority.Normal;
+
+ [Tooltip("Custom sync rate (overrides priority if > 0)")]
+ [SerializeField] protected float m_CustomSyncRate = 0f;
+
+ [Header("Debug")]
+ [SerializeField] protected bool m_DebugMode = false;
+
+ // ============================================================================================
+ // RUNTIME STATE
+ // ============================================================================================
+
+ protected bool m_IsDirty = false;
+ protected float m_LastSyncTime = 0f;
+ protected NetworkObject m_CachedNetworkObject;
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public bool IsDirty => m_IsDirty;
+
+ NetworkObject INetworkModuleSync.NetworkObject => m_CachedNetworkObject;
+
+ ///
+ /// Gets the effective sync rate based on priority or custom setting
+ ///
+ protected float SyncRate
+ {
+ get
+ {
+ if (m_CustomSyncRate > 0f) return m_CustomSyncRate;
+
+ return m_Priority switch
+ {
+ SyncPriority.Low => 2f,
+ SyncPriority.Normal => 10f,
+ SyncPriority.High => 30f,
+ SyncPriority.Critical => 60f,
+ _ => 10f
+ };
+ }
+ }
+
+ ///
+ /// Time between syncs based on rate
+ ///
+ protected float SyncInterval => 1f / SyncRate;
+
+ ///
+ /// Should this client/server process updates?
+ ///
+ protected bool ShouldProcess
+ {
+ get
+ {
+ return m_AuthorityMode switch
+ {
+ AuthorityMode.OwnerAuthority => IsOwner,
+ AuthorityMode.ServerAuthority => IsServer,
+ AuthorityMode.Hybrid => IsOwner || IsServer,
+ _ => false
+ };
+ }
+ }
+
+ ///
+ /// Can write to network variables?
+ ///
+ protected bool CanWrite
+ {
+ get
+ {
+ return m_AuthorityMode switch
+ {
+ AuthorityMode.OwnerAuthority => IsOwner,
+ AuthorityMode.ServerAuthority => IsServer,
+ AuthorityMode.Hybrid => IsServer, // Server has final say
+ _ => false
+ };
+ }
+ }
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ protected virtual void Awake()
+ {
+ m_CachedNetworkObject = GetComponent();
+ }
+
+ protected virtual void Update()
+ {
+ if (!IsSpawned) return;
+ if (!ShouldProcess) return;
+
+ // Check if enough time has passed since last sync
+ if (Time.time - m_LastSyncTime < SyncInterval) return;
+
+ // Process module update
+ OnModuleUpdate();
+
+ // Auto-sync if dirty
+ if (m_IsDirty)
+ {
+ SyncToNetwork();
+ m_LastSyncTime = Time.time;
+ }
+ }
+
+ // ============================================================================================
+ // NETWORK LIFECYCLE
+ // ============================================================================================
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ // Register with manager
+ RegisterWithManager();
+
+ // Initialize module
+ OnModuleInitialize();
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[{GetType().Name}] Spawned - Authority: {m_AuthorityMode}, Priority: {m_Priority}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // Unregister from manager
+ UnregisterFromManager();
+
+ // Cleanup module
+ OnModuleCleanup();
+
+ base.OnNetworkDespawn();
+ }
+
+ // ============================================================================================
+ // MANAGER REGISTRATION
+ // ============================================================================================
+
+ protected virtual void RegisterWithManager()
+ {
+ if (NetworkModuleSyncManager.Instance != null)
+ {
+ NetworkModuleSyncManager.Instance.RegisterModule(GetType(), this);
+ }
+ }
+
+ protected virtual void UnregisterFromManager()
+ {
+ if (NetworkModuleSyncManager.Instance != null)
+ {
+ // Note: UnregisterModule uses generic, we need reflection or different approach
+ // For now, manager handles cleanup via WeakReferences or null checks
+ }
+ }
+
+ // ============================================================================================
+ // INetworkModuleSync IMPLEMENTATION
+ // ============================================================================================
+
+ public virtual void SyncToNetwork()
+ {
+ if (!IsSpawned) return;
+
+ // Host player: Direct sync
+ if (NetworkAuthorityManager.Instance != null && NetworkAuthorityManager.Instance.IsHostPlayer())
+ {
+ SyncStateDirectly();
+ }
+ // Client with owner authority: Use RPC
+ else if (m_AuthorityMode == AuthorityMode.OwnerAuthority && IsOwner && !IsServer)
+ {
+ RequestSyncServerRpc();
+ }
+ // Server authority: Direct sync
+ else if (m_AuthorityMode == AuthorityMode.ServerAuthority && IsServer)
+ {
+ SyncStateDirectly();
+ }
+ // Hybrid: Client predicts locally, server validates
+ else if (m_AuthorityMode == AuthorityMode.Hybrid)
+ {
+ if (IsOwner && !IsServer)
+ {
+ // Apply locally for prediction
+ ApplyStateLocally();
+ // Send to server for validation
+ RequestValidationServerRpc();
+ }
+ else if (IsServer)
+ {
+ SyncStateDirectly();
+ }
+ }
+
+ ClearDirty();
+ }
+
+ public virtual void SyncFromNetwork()
+ {
+ // Override in derived classes to handle incoming network state
+ // NetworkVariables auto-sync, this is for custom logic
+ }
+
+ public void MarkDirty()
+ {
+ m_IsDirty = true;
+ }
+
+ public void ClearDirty()
+ {
+ m_IsDirty = false;
+ }
+
+ // ============================================================================================
+ // ABSTRACT METHODS - Override in derived classes
+ // ============================================================================================
+
+ ///
+ /// Called when module should initialize (after network spawn)
+ ///
+ protected abstract void OnModuleInitialize();
+
+ ///
+ /// Called on update tick when this client should process
+ ///
+ protected abstract void OnModuleUpdate();
+
+ ///
+ /// Called when module should cleanup (before network despawn)
+ ///
+ protected abstract void OnModuleCleanup();
+
+ ///
+ /// Directly sync state to network (server/host only)
+ ///
+ protected abstract void SyncStateDirectly();
+
+ ///
+ /// Apply state locally (for prediction)
+ ///
+ protected virtual void ApplyStateLocally() { }
+
+ // ============================================================================================
+ // RPCs
+ // ============================================================================================
+
+ ///
+ /// Request sync from server (for owner authority mode)
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
+ protected virtual void RequestSyncServerRpc(RpcParams rpcParams = default)
+ {
+ // Server processes the sync request
+ OnSyncRequestReceived(rpcParams.Receive.SenderClientId);
+ }
+
+ ///
+ /// Request validation from server (for hybrid mode)
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
+ protected virtual void RequestValidationServerRpc(RpcParams rpcParams = default)
+ {
+ // Server validates and responds
+ OnValidationRequestReceived(rpcParams.Receive.SenderClientId);
+ }
+
+ ///
+ /// Called on server when sync request received
+ ///
+ protected virtual void OnSyncRequestReceived(ulong clientId)
+ {
+ // Override to handle sync request
+ SyncStateDirectly();
+ }
+
+ ///
+ /// Called on server when validation request received
+ ///
+ protected virtual void OnValidationRequestReceived(ulong clientId)
+ {
+ // Override to handle validation
+ }
+
+ // ============================================================================================
+ // UTILITY METHODS
+ // ============================================================================================
+
+ ///
+ /// Log debug message if debug mode enabled
+ ///
+ protected void LogDebug(string message)
+ {
+ if (m_DebugMode)
+ {
+ Debug.Log($"[{GetType().Name}] {message}");
+ }
+ }
+
+ ///
+ /// Log warning message
+ ///
+ protected void LogWarning(string message)
+ {
+ Debug.LogWarning($"[{GetType().Name}] {message}");
+ }
+
+ ///
+ /// Log error message
+ ///
+ protected void LogError(string message)
+ {
+ Debug.LogError($"[{GetType().Name}] {message}");
+ }
+
+ ///
+ /// Check if this is the host player (server + owner)
+ ///
+ protected bool IsHostPlayer()
+ {
+ return NetworkAuthorityManager.Instance != null &&
+ NetworkAuthorityManager.Instance.IsHostPlayer();
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncManager.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncManager.cs
index 4ce6d9e38..bf45c3d1f 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncManager.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/Modules/NetworkModuleSyncManager.cs
@@ -77,30 +77,51 @@ private void Update()
// ============================================================================================
///
- /// Registers a module sync component
+ /// Registers a module sync component (generic version)
///
public void RegisterModule(INetworkModuleSync sync) where T : class
{
- Type moduleType = typeof(T);
-
+ RegisterModule(typeof(T), sync);
+ }
+
+ ///
+ /// Registers a module sync component by Type
+ ///
+ public void RegisterModule(Type moduleType, INetworkModuleSync sync)
+ {
+ if (moduleType == null || sync == null)
+ {
+ if (m_DebugMode) Debug.LogWarning("[ModuleSyncManager] Cannot register null module or type.");
+ return;
+ }
+
if (m_ModuleSyncs.ContainsKey(moduleType))
{
if (m_DebugMode) Debug.LogWarning($"[ModuleSyncManager] Module {moduleType.Name} already registered. Replacing.");
}
-
+
m_ModuleSyncs[moduleType] = sync;
-
+
if (m_DebugMode) Debug.Log($"[ModuleSyncManager] Registered module sync: {moduleType.Name}");
}
///
- /// Unregisters a module sync component
+ /// Unregisters a module sync component (generic version)
///
public void UnregisterModule() where T : class
{
- Type moduleType = typeof(T);
+ UnregisterModule(typeof(T));
+ }
+
+ ///
+ /// Unregisters a module sync component by Type
+ ///
+ public void UnregisterModule(Type moduleType)
+ {
+ if (moduleType == null) return;
+
m_ModuleSyncs.Remove(moduleType);
-
+
if (m_DebugMode) Debug.Log($"[ModuleSyncManager] Unregistered module sync: {moduleType.Name}");
}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerConsumer.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerConsumer.cs
new file mode 100644
index 000000000..f8c75eb70
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerConsumer.cs
@@ -0,0 +1,481 @@
+using System;
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ ///
+ /// Power consumer component for houses/buildings
+ /// Manages power consumption and distributes to connected devices
+ ///
+ [AddComponentMenu("GameCreator/Multiplayer/Power Grid/Power Consumer")]
+ public class PowerConsumer : NetworkBehaviour
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Power Configuration")]
+ [SerializeField] private float basePowerRequirement = 10f;
+ [SerializeField] private bool autoConnectToNearestSource = true;
+ [SerializeField] private float maxConnectionDistance = 50f;
+
+ [Header("Visual Feedback")]
+ [SerializeField] private Light[] powerIndicatorLights;
+ [SerializeField] private Color poweredColor = Color.green;
+ [SerializeField] private Color unpoweredColor = Color.red;
+ [SerializeField] private Material[] materialsToToggle;
+ [SerializeField] private string emissiveColorProperty = "_EmissionColor";
+
+ [Header("Audio Feedback")]
+ [SerializeField] private AudioSource audioSource;
+ [SerializeField] private AudioClip powerOnSound;
+ [SerializeField] private AudioClip powerOffSound;
+
+ [Header("Debug")]
+ [SerializeField] private bool debugMode = false;
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ private NetworkVariable m_NetworkIsPowered = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable m_NetworkIsConnected = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable m_NetworkCurrentConsumption = new NetworkVariable(
+ 0f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public bool IsPowered => m_NetworkIsPowered.Value;
+ public bool IsConnected => m_NetworkIsConnected.Value;
+ public float RequiredPower => basePowerRequirement + GetDevicesTotalConsumption();
+ public float CurrentConsumption => m_NetworkCurrentConsumption.Value;
+ public PowerSource ConnectedPowerSource { get; private set; }
+
+ // ============================================================================================
+ // PRIVATE FIELDS
+ // ============================================================================================
+
+ private readonly List m_ConnectedDevices = new List();
+ private bool m_LastPowerState = false;
+
+ // ============================================================================================
+ // EVENTS
+ // ============================================================================================
+
+ public event Action OnPowerStateChanged;
+ public event Action OnDeviceConnected;
+ public event Action OnDeviceDisconnected;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ // Find all devices in children
+ var devices = GetComponentsInChildren();
+ foreach (var device in devices)
+ {
+ RegisterDevice(device);
+ }
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ // Subscribe to network variable changes
+ m_NetworkIsPowered.OnValueChanged += OnPowerStateChangedCallback;
+ m_NetworkIsConnected.OnValueChanged += OnConnectionStateChangedCallback;
+
+ // Auto-connect to nearest power source
+ if (IsServer && autoConnectToNearestSource)
+ {
+ Invoke(nameof(AutoConnectToNearestSource), 0.5f); // Delay to ensure all power sources are spawned
+ }
+
+ UpdateVisualFeedback();
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} spawned - IsServer: {IsServer}, Connected: {IsConnected}, Powered: {IsPowered}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ m_NetworkIsPowered.OnValueChanged -= OnPowerStateChangedCallback;
+ m_NetworkIsConnected.OnValueChanged -= OnConnectionStateChangedCallback;
+
+ // Disconnect from power source
+ if (IsServer && ConnectedPowerSource != null)
+ {
+ DisconnectFromPowerSource();
+ }
+
+ base.OnNetworkDespawn();
+ }
+
+ private void Update()
+ {
+ if (!IsSpawned) return;
+
+ // Update visual feedback on all clients
+ if (m_LastPowerState != IsPowered)
+ {
+ UpdateVisualFeedback();
+ m_LastPowerState = IsPowered;
+ }
+
+ // Server updates consumption
+ if (IsServer && Time.frameCount % 60 == 0) // Every second
+ {
+ UpdateConsumption();
+ }
+ }
+
+ private void OnDestroy()
+ {
+ // Clean up
+ if (ConnectedPowerSource != null)
+ {
+ ConnectedPowerSource.UnregisterConsumer(this);
+ }
+ }
+
+ // ============================================================================================
+ // POWER CONNECTION
+ // ============================================================================================
+
+ ///
+ /// Connect to a power source (Server only)
+ ///
+ public bool ConnectToPowerSource(PowerSource source)
+ {
+ if (!IsServer)
+ {
+ Debug.LogWarning("[PowerConsumer] ConnectToPowerSource can only be called on server!");
+ return false;
+ }
+
+ if (source == null) return false;
+
+ // Disconnect from previous source
+ if (ConnectedPowerSource != null)
+ {
+ DisconnectFromPowerSource();
+ }
+
+ // Check distance
+ float distance = Vector3.Distance(transform.position, source.transform.position);
+ if (distance > maxConnectionDistance)
+ {
+ if (debugMode)
+ {
+ Debug.LogWarning($"[PowerConsumer] {name} too far from power source ({distance:F1}m > {maxConnectionDistance}m)");
+ }
+ return false;
+ }
+
+ // Register with power source
+ if (source.RegisterConsumer(this))
+ {
+ ConnectedPowerSource = source;
+ m_NetworkIsConnected.Value = true;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} connected to {source.name}");
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Disconnect from current power source (Server only)
+ ///
+ public void DisconnectFromPowerSource()
+ {
+ if (!IsServer) return;
+
+ if (ConnectedPowerSource != null)
+ {
+ ConnectedPowerSource.UnregisterConsumer(this);
+ ConnectedPowerSource = null;
+ m_NetworkIsConnected.Value = false;
+ SetPowerStatus(false);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} disconnected from power source");
+ }
+ }
+ }
+
+ ///
+ /// Auto-connect to nearest power source in range
+ ///
+ private void AutoConnectToNearestSource()
+ {
+ if (!IsServer) return;
+
+ var allSources = FindObjectsByType(FindObjectsSortMode.None);
+ PowerSource nearestSource = null;
+ float nearestDistance = float.MaxValue;
+
+ foreach (var source in allSources)
+ {
+ if (!source.IsSpawned) continue;
+
+ float distance = Vector3.Distance(transform.position, source.transform.position);
+ if (distance <= maxConnectionDistance && distance < nearestDistance)
+ {
+ nearestSource = source;
+ nearestDistance = distance;
+ }
+ }
+
+ if (nearestSource != null)
+ {
+ ConnectToPowerSource(nearestSource);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} auto-connected to {nearestSource.name} ({nearestDistance:F1}m)");
+ }
+ }
+ else if (debugMode)
+ {
+ Debug.LogWarning($"[PowerConsumer] {name} could not find power source within {maxConnectionDistance}m");
+ }
+ }
+
+ ///
+ /// Set power status (called by PowerSource)
+ ///
+ public void SetPowerStatus(bool powered)
+ {
+ if (!IsServer) return;
+
+ bool previousState = m_NetworkIsPowered.Value;
+ m_NetworkIsPowered.Value = powered;
+
+ // Update all connected devices
+ foreach (var device in m_ConnectedDevices)
+ {
+ if (device != null)
+ {
+ device.OnConsumerPowerChanged(powered);
+ }
+ }
+
+ if (debugMode && previousState != powered)
+ {
+ Debug.Log($"[PowerConsumer] {name} power state: {previousState} → {powered}");
+ }
+ }
+
+ // ============================================================================================
+ // DEVICE MANAGEMENT
+ // ============================================================================================
+
+ ///
+ /// Register a device to this consumer
+ ///
+ public bool RegisterDevice(PowerDevice device)
+ {
+ if (device == null || m_ConnectedDevices.Contains(device))
+ return false;
+
+ m_ConnectedDevices.Add(device);
+ device.SetConsumer(this);
+ OnDeviceConnected?.Invoke(device);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} registered device: {device.name} ({m_ConnectedDevices.Count} total)");
+ }
+
+ UpdateConsumption();
+ return true;
+ }
+
+ ///
+ /// Unregister a device from this consumer
+ ///
+ public void UnregisterDevice(PowerDevice device)
+ {
+ if (m_ConnectedDevices.Remove(device))
+ {
+ OnDeviceDisconnected?.Invoke(device);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} unregistered device: {device.name} ({m_ConnectedDevices.Count} remaining)");
+ }
+
+ UpdateConsumption();
+ }
+ }
+
+ ///
+ /// Get total power consumption of all active devices
+ ///
+ private float GetDevicesTotalConsumption()
+ {
+ float total = 0f;
+ foreach (var device in m_ConnectedDevices)
+ {
+ if (device != null && device.IsActive)
+ {
+ total += device.PowerConsumption;
+ }
+ }
+ return total;
+ }
+
+ ///
+ /// Update current power consumption (Server only)
+ ///
+ private void UpdateConsumption()
+ {
+ if (!IsServer) return;
+
+ // Clean up null devices
+ m_ConnectedDevices.RemoveAll(d => d == null);
+
+ float consumption = IsConnected ? RequiredPower : 0f;
+ m_NetworkCurrentConsumption.Value = consumption;
+ }
+
+ // ============================================================================================
+ // RPC METHODS (GameCreator Visual Scripting)
+ // ============================================================================================
+
+ ///
+ /// Request connection to power source (Client → Server)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void RequestConnectionServerRpc(RpcParams rpcParams = default)
+ {
+ AutoConnectToNearestSource();
+ }
+
+ ///
+ /// Request disconnection from power source (Client → Server)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void RequestDisconnectionServerRpc(RpcParams rpcParams = default)
+ {
+ DisconnectFromPowerSource();
+ }
+
+ // ============================================================================================
+ // CALLBACKS
+ // ============================================================================================
+
+ private void OnPowerStateChangedCallback(bool previousValue, bool newValue)
+ {
+ OnPowerStateChanged?.Invoke(newValue);
+ UpdateVisualFeedback();
+ PlayPowerStateSound(newValue);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} power changed: {previousValue} → {newValue}");
+ }
+ }
+
+ private void OnConnectionStateChangedCallback(bool previousValue, bool newValue)
+ {
+ if (debugMode)
+ {
+ Debug.Log($"[PowerConsumer] {name} connection changed: {previousValue} → {newValue}");
+ }
+ }
+
+ // ============================================================================================
+ // VISUAL & AUDIO FEEDBACK
+ // ============================================================================================
+
+ private void UpdateVisualFeedback()
+ {
+ // Update indicator lights
+ if (powerIndicatorLights != null)
+ {
+ foreach (var light in powerIndicatorLights)
+ {
+ if (light != null)
+ {
+ light.color = IsPowered ? poweredColor : unpoweredColor;
+ light.enabled = IsPowered;
+ }
+ }
+ }
+
+ // Update emissive materials
+ if (materialsToToggle != null)
+ {
+ Color emissiveColor = IsPowered ? poweredColor : Color.black;
+ foreach (var mat in materialsToToggle)
+ {
+ if (mat != null && mat.HasProperty(emissiveColorProperty))
+ {
+ mat.SetColor(emissiveColorProperty, emissiveColor);
+ }
+ }
+ }
+ }
+
+ private void PlayPowerStateSound(bool powered)
+ {
+ if (audioSource == null) return;
+
+ AudioClip clip = powered ? powerOnSound : powerOffSound;
+ if (clip != null)
+ {
+ audioSource.PlayOneShot(clip);
+ }
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Get status info for UI/debugging
+ ///
+ public string GetStatusInfo()
+ {
+ return $"Consumer: {name}\n" +
+ $"Connected: {IsConnected}\n" +
+ $"Powered: {IsPowered}\n" +
+ $"Base Requirement: {basePowerRequirement:F1}\n" +
+ $"Devices Consumption: {GetDevicesTotalConsumption():F1}\n" +
+ $"Total Required: {RequiredPower:F1}\n" +
+ $"Devices: {m_ConnectedDevices.Count}";
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerDevice.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerDevice.cs
new file mode 100644
index 000000000..a21d7e2f0
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerDevice.cs
@@ -0,0 +1,488 @@
+using System;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ ///
+ /// Individual power device (lights, appliances, etc.)
+ /// Can be turned on/off and consumes power when active
+ ///
+ [AddComponentMenu("GameCreator/Multiplayer/Power Grid/Power Device")]
+ public class PowerDevice : NetworkBehaviour
+ {
+ // ============================================================================================
+ // ENUMS
+ // ============================================================================================
+
+ public enum DeviceType
+ {
+ Light,
+ Appliance,
+ HVAC,
+ Entertainment,
+ Security,
+ Industrial,
+ Custom
+ }
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Device Configuration")]
+ [SerializeField] private DeviceType deviceType = DeviceType.Light;
+ [SerializeField] private string deviceName = "Device";
+ [SerializeField] private float powerConsumption = 5f;
+ [SerializeField] private bool startActive = false;
+ [SerializeField] private bool requiresPower = true;
+
+ [Header("Visual Feedback")]
+ [SerializeField] private Light[] lights;
+ [SerializeField] private Renderer[] renderers;
+ [SerializeField] private string emissiveColorProperty = "_EmissionColor";
+ [SerializeField] private Color activeColor = Color.white;
+ [SerializeField] private float activeIntensity = 1f;
+ [SerializeField] private ParticleSystem[] particleSystems;
+ [SerializeField] private GameObject[] objectsToToggle;
+
+ [Header("Audio Feedback")]
+ [SerializeField] private AudioSource audioSource;
+ [SerializeField] private AudioClip turnOnSound;
+ [SerializeField] private AudioClip turnOffSound;
+ [SerializeField] private AudioClip runningSound;
+ [SerializeField] private bool loopRunningSound = true;
+
+ [Header("Animation")]
+ [SerializeField] private Animator animator;
+ [SerializeField] private string activeStateName = "Active";
+ [SerializeField] private string activeTriggerName = "TurnOn";
+ [SerializeField] private string inactiveTriggerName = "TurnOff";
+
+ [Header("Debug")]
+ [SerializeField] private bool debugMode = false;
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ private NetworkVariable m_NetworkIsActive = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable m_NetworkHasPower = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public bool IsActive => m_NetworkIsActive.Value;
+ public bool HasPower => m_NetworkHasPower.Value;
+ public float PowerConsumption => powerConsumption;
+ public DeviceType Type => deviceType;
+ public string DeviceName => deviceName;
+ public bool IsOperational => HasPower && IsActive;
+ public PowerConsumer Consumer { get; private set; }
+
+ // ============================================================================================
+ // EVENTS
+ // ============================================================================================
+
+ public event Action OnActiveStateChanged;
+ public event Action OnPowerStateChanged;
+
+ // ============================================================================================
+ // PRIVATE FIELDS
+ // ============================================================================================
+
+ private Color[] m_OriginalEmissiveColors;
+ private float[] m_OriginalLightIntensities;
+ private bool m_LastOperationalState = false;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ // Cache original values
+ CacheOriginalValues();
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ // Subscribe to network variable changes
+ m_NetworkIsActive.OnValueChanged += OnActiveStateChangedCallback;
+ m_NetworkHasPower.OnValueChanged += OnPowerStateChangedCallback;
+
+ // Initialize state
+ if (IsServer)
+ {
+ m_NetworkIsActive.Value = startActive;
+ }
+
+ UpdateVisualState();
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} spawned - IsServer: {IsServer}, Active: {IsActive}, HasPower: {HasPower}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ m_NetworkIsActive.OnValueChanged -= OnActiveStateChangedCallback;
+ m_NetworkHasPower.OnValueChanged -= OnPowerStateChangedCallback;
+
+ base.OnNetworkDespawn();
+ }
+
+ private void Update()
+ {
+ if (!IsSpawned) return;
+
+ // Update visual state on all clients
+ bool currentOperationalState = IsOperational;
+ if (m_LastOperationalState != currentOperationalState)
+ {
+ UpdateVisualState();
+ m_LastOperationalState = currentOperationalState;
+ }
+ }
+
+ // ============================================================================================
+ // POWER CONSUMER INTEGRATION
+ // ============================================================================================
+
+ ///
+ /// Set the power consumer this device belongs to (called by PowerConsumer)
+ ///
+ public void SetConsumer(PowerConsumer consumer)
+ {
+ Consumer = consumer;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} assigned to consumer: {consumer?.name}");
+ }
+ }
+
+ ///
+ /// Called by PowerConsumer when power state changes (Server only)
+ ///
+ public void OnConsumerPowerChanged(bool hasPower)
+ {
+ if (!IsServer) return;
+
+ m_NetworkHasPower.Value = hasPower;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} consumer power changed: {hasPower}");
+ }
+ }
+
+ // ============================================================================================
+ // DEVICE CONTROL
+ // ============================================================================================
+
+ ///
+ /// Turn on the device (Server RPC)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void TurnOnServerRpc(RpcParams rpcParams = default)
+ {
+ if (!requiresPower || HasPower)
+ {
+ m_NetworkIsActive.Value = true;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} turned ON");
+ }
+ }
+ else if (debugMode)
+ {
+ Debug.LogWarning($"[PowerDevice] {deviceName} cannot turn on - no power available");
+ }
+ }
+
+ ///
+ /// Turn off the device (Server RPC)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void TurnOffServerRpc(RpcParams rpcParams = default)
+ {
+ m_NetworkIsActive.Value = false;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} turned OFF");
+ }
+ }
+
+ ///
+ /// Toggle device state (Server RPC)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void ToggleServerRpc(RpcParams rpcParams = default)
+ {
+ if (IsActive)
+ {
+ TurnOffServerRpc();
+ }
+ else
+ {
+ TurnOnServerRpc();
+ }
+ }
+
+ ///
+ /// Set device active state (Server only, direct call)
+ ///
+ public void SetActive(bool active)
+ {
+ if (!IsServer)
+ {
+ Debug.LogWarning("[PowerDevice] SetActive can only be called on server!");
+ return;
+ }
+
+ if (active && requiresPower && !HasPower)
+ {
+ if (debugMode)
+ {
+ Debug.LogWarning($"[PowerDevice] {deviceName} cannot activate - no power");
+ }
+ return;
+ }
+
+ m_NetworkIsActive.Value = active;
+ }
+
+ // ============================================================================================
+ // CALLBACKS
+ // ============================================================================================
+
+ private void OnActiveStateChangedCallback(bool previousValue, bool newValue)
+ {
+ OnActiveStateChanged?.Invoke(newValue);
+ UpdateVisualState();
+ PlayStateChangeSound(newValue);
+ UpdateAnimation(newValue);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} active state changed: {previousValue} → {newValue}");
+ }
+ }
+
+ private void OnPowerStateChangedCallback(bool previousValue, bool newValue)
+ {
+ OnPowerStateChanged?.Invoke(newValue);
+
+ // If power is lost while device is on, turn it off
+ if (IsServer && IsActive && !newValue && requiresPower)
+ {
+ m_NetworkIsActive.Value = false;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerDevice] {deviceName} turned off due to power loss");
+ }
+ }
+
+ UpdateVisualState();
+ }
+
+ // ============================================================================================
+ // VISUAL FEEDBACK
+ // ============================================================================================
+
+ private void CacheOriginalValues()
+ {
+ // Cache original emissive colors
+ if (renderers != null && renderers.Length > 0)
+ {
+ m_OriginalEmissiveColors = new Color[renderers.Length];
+ for (int i = 0; i < renderers.Length; i++)
+ {
+ if (renderers[i] != null && renderers[i].material.HasProperty(emissiveColorProperty))
+ {
+ m_OriginalEmissiveColors[i] = renderers[i].material.GetColor(emissiveColorProperty);
+ }
+ }
+ }
+
+ // Cache original light intensities
+ if (lights != null && lights.Length > 0)
+ {
+ m_OriginalLightIntensities = new float[lights.Length];
+ for (int i = 0; i < lights.Length; i++)
+ {
+ if (lights[i] != null)
+ {
+ m_OriginalLightIntensities[i] = lights[i].intensity;
+ }
+ }
+ }
+ }
+
+ private void UpdateVisualState()
+ {
+ bool operational = IsOperational;
+
+ // Update lights
+ if (lights != null)
+ {
+ for (int i = 0; i < lights.Length; i++)
+ {
+ if (lights[i] != null)
+ {
+ lights[i].enabled = operational;
+ if (operational)
+ {
+ lights[i].color = activeColor;
+ lights[i].intensity = m_OriginalLightIntensities != null && i < m_OriginalLightIntensities.Length
+ ? m_OriginalLightIntensities[i] * activeIntensity
+ : activeIntensity;
+ }
+ }
+ }
+ }
+
+ // Update emissive materials
+ if (renderers != null)
+ {
+ for (int i = 0; i < renderers.Length; i++)
+ {
+ if (renderers[i] != null && renderers[i].material.HasProperty(emissiveColorProperty))
+ {
+ Color emissiveColor = operational ? activeColor : Color.black;
+ renderers[i].material.SetColor(emissiveColorProperty, emissiveColor);
+ }
+ }
+ }
+
+ // Update particle systems
+ if (particleSystems != null)
+ {
+ foreach (var ps in particleSystems)
+ {
+ if (ps != null)
+ {
+ if (operational && !ps.isPlaying)
+ {
+ ps.Play();
+ }
+ else if (!operational && ps.isPlaying)
+ {
+ ps.Stop();
+ }
+ }
+ }
+ }
+
+ // Update objects to toggle
+ if (objectsToToggle != null)
+ {
+ foreach (var obj in objectsToToggle)
+ {
+ if (obj != null)
+ {
+ obj.SetActive(operational);
+ }
+ }
+ }
+
+ // Update running sound
+ UpdateRunningSound(operational);
+ }
+
+ private void PlayStateChangeSound(bool active)
+ {
+ if (audioSource == null) return;
+
+ // Stop running sound first
+ if (runningSound != null && audioSource.clip == runningSound)
+ {
+ audioSource.Stop();
+ }
+
+ // Play state change sound
+ AudioClip clip = active ? turnOnSound : turnOffSound;
+ if (clip != null)
+ {
+ audioSource.PlayOneShot(clip);
+ }
+
+ // Start running sound if active
+ if (active && HasPower)
+ {
+ UpdateRunningSound(true);
+ }
+ }
+
+ private void UpdateRunningSound(bool operational)
+ {
+ if (audioSource == null || runningSound == null) return;
+
+ if (operational && loopRunningSound)
+ {
+ if (audioSource.clip != runningSound || !audioSource.isPlaying)
+ {
+ audioSource.clip = runningSound;
+ audioSource.loop = true;
+ audioSource.Play();
+ }
+ }
+ else if (audioSource.clip == runningSound && audioSource.isPlaying)
+ {
+ audioSource.Stop();
+ }
+ }
+
+ private void UpdateAnimation(bool active)
+ {
+ if (animator == null) return;
+
+ if (!string.IsNullOrEmpty(activeTriggerName) && !string.IsNullOrEmpty(inactiveTriggerName))
+ {
+ animator.SetTrigger(active ? activeTriggerName : inactiveTriggerName);
+ }
+ else if (!string.IsNullOrEmpty(activeStateName))
+ {
+ animator.SetBool(activeStateName, active);
+ }
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Get status info for UI/debugging
+ ///
+ public string GetStatusInfo()
+ {
+ return $"Device: {deviceName}\n" +
+ $"Type: {deviceType}\n" +
+ $"Active: {IsActive}\n" +
+ $"Has Power: {HasPower}\n" +
+ $"Operational: {IsOperational}\n" +
+ $"Consumption: {powerConsumption:F1}W\n" +
+ $"Consumer: {(Consumer != null ? Consumer.name : "None")}";
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerSource.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerSource.cs
new file mode 100644
index 000000000..cc1a6b840
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/PowerSource.cs
@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ ///
+ /// Central power source that distributes power to connected consumers
+ /// Server-authoritative power distribution with network synchronization
+ ///
+ [AddComponentMenu("GameCreator/Multiplayer/Power Grid/Power Source")]
+ public class PowerSource : NetworkBehaviour
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Power Configuration")]
+ [SerializeField] private float maxPowerCapacity = 1000f;
+ [SerializeField] private float currentPowerGeneration = 1000f;
+ [SerializeField] private bool isActive = true;
+
+ [Header("Distribution")]
+ [SerializeField] private float distributionRadius = 50f;
+ [SerializeField] private LayerMask consumerLayer = -1;
+
+ [Header("Visual Feedback")]
+ [SerializeField] private Light statusLight;
+ [SerializeField] private Color poweredColor = Color.green;
+ [SerializeField] private Color overloadedColor = Color.red;
+ [SerializeField] private Color offlineColor = Color.gray;
+
+ [Header("Debug")]
+ [SerializeField] private bool debugMode = false;
+
+ // ============================================================================================
+ // NETWORK VARIABLES
+ // ============================================================================================
+
+ private NetworkVariable m_NetworkIsActive = new NetworkVariable(
+ true,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable m_NetworkCurrentLoad = new NetworkVariable(
+ 0f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable m_NetworkPowerGeneration = new NetworkVariable(
+ 1000f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public bool IsActive => m_NetworkIsActive.Value;
+ public float MaxPowerCapacity => maxPowerCapacity;
+ public float CurrentPowerGeneration => m_NetworkPowerGeneration.Value;
+ public float CurrentLoad => m_NetworkCurrentLoad.Value;
+ public float AvailablePower => Mathf.Max(0, CurrentPowerGeneration - CurrentLoad);
+ public bool IsOverloaded => CurrentLoad > CurrentPowerGeneration;
+ public float LoadPercentage => CurrentPowerGeneration > 0 ? (CurrentLoad / CurrentPowerGeneration) * 100f : 0f;
+
+ // ============================================================================================
+ // PRIVATE FIELDS
+ // ============================================================================================
+
+ private readonly List m_ConnectedConsumers = new List();
+ private float m_LastUpdateTime = 0f;
+ private const float UPDATE_INTERVAL = 1f; // Update every second
+
+ // ============================================================================================
+ // EVENTS
+ // ============================================================================================
+
+ public event Action OnActiveStateChanged;
+ public event Action OnLoadChanged;
+ public event Action OnConsumerConnected;
+ public event Action OnConsumerDisconnected;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (IsServer)
+ {
+ m_NetworkIsActive.Value = isActive;
+ m_NetworkPowerGeneration.Value = currentPowerGeneration;
+ }
+
+ // Subscribe to network variable changes
+ m_NetworkIsActive.OnValueChanged += OnActiveStateChangedCallback;
+ m_NetworkCurrentLoad.OnValueChanged += OnLoadChangedCallback;
+
+ UpdateVisualFeedback();
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Network spawned - IsServer: {IsServer}, Active: {IsActive}");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ m_NetworkIsActive.OnValueChanged -= OnActiveStateChangedCallback;
+ m_NetworkCurrentLoad.OnValueChanged -= OnLoadChangedCallback;
+
+ base.OnNetworkDespawn();
+ }
+
+ private void Update()
+ {
+ if (!IsSpawned) return;
+
+ // Server updates power distribution
+ if (IsServer && Time.time - m_LastUpdateTime >= UPDATE_INTERVAL)
+ {
+ UpdatePowerDistribution();
+ m_LastUpdateTime = Time.time;
+ }
+
+ // All clients update visual feedback
+ UpdateVisualFeedback();
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ // Visualize distribution radius
+ Gizmos.color = IsActive ? Color.green : Color.red;
+ Gizmos.DrawWireSphere(transform.position, distributionRadius);
+ }
+
+ // ============================================================================================
+ // POWER DISTRIBUTION (SERVER ONLY)
+ // ============================================================================================
+
+ private void UpdatePowerDistribution()
+ {
+ if (!IsServer) return;
+
+ // Remove null or destroyed consumers
+ m_ConnectedConsumers.RemoveAll(c => c == null || !c.IsSpawned);
+
+ // Calculate total load
+ float totalLoad = 0f;
+ foreach (var consumer in m_ConnectedConsumers)
+ {
+ if (consumer.IsConnected)
+ {
+ totalLoad += consumer.CurrentConsumption;
+ }
+ }
+
+ m_NetworkCurrentLoad.Value = totalLoad;
+
+ // Distribute power based on availability
+ if (IsActive && !IsOverloaded)
+ {
+ // Sufficient power - all consumers get full power
+ foreach (var consumer in m_ConnectedConsumers)
+ {
+ if (consumer.IsConnected)
+ {
+ consumer.SetPowerStatus(true);
+ }
+ }
+ }
+ else if (IsActive && IsOverloaded)
+ {
+ // Overloaded - distribute proportionally or implement priority system
+ float availablePower = CurrentPowerGeneration;
+ foreach (var consumer in m_ConnectedConsumers)
+ {
+ if (consumer.IsConnected && availablePower > 0)
+ {
+ float allocation = Mathf.Min(consumer.RequiredPower, availablePower);
+ bool hasPower = allocation >= consumer.RequiredPower * 0.5f; // Needs at least 50%
+ consumer.SetPowerStatus(hasPower);
+ availablePower -= allocation;
+ }
+ else
+ {
+ consumer.SetPowerStatus(false);
+ }
+ }
+
+ if (debugMode && Time.frameCount % 60 == 0)
+ {
+ Debug.LogWarning($"[PowerSource] OVERLOADED! Load: {CurrentLoad:F1}/{CurrentPowerGeneration:F1} ({LoadPercentage:F1}%)");
+ }
+ }
+ else
+ {
+ // Power source offline - no power to anyone
+ foreach (var consumer in m_ConnectedConsumers)
+ {
+ consumer.SetPowerStatus(false);
+ }
+ }
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Register a consumer to this power source (called by PowerConsumer)
+ ///
+ public bool RegisterConsumer(PowerConsumer consumer)
+ {
+ if (consumer == null || m_ConnectedConsumers.Contains(consumer))
+ return false;
+
+ // Check if within range
+ float distance = Vector3.Distance(transform.position, consumer.transform.position);
+ if (distance > distributionRadius)
+ {
+ if (debugMode)
+ {
+ Debug.LogWarning($"[PowerSource] Consumer {consumer.name} is out of range ({distance:F1}m > {distributionRadius}m)");
+ }
+ return false;
+ }
+
+ m_ConnectedConsumers.Add(consumer);
+ OnConsumerConnected?.Invoke(consumer);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Consumer registered: {consumer.name} ({m_ConnectedConsumers.Count} total)");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Unregister a consumer from this power source
+ ///
+ public void UnregisterConsumer(PowerConsumer consumer)
+ {
+ if (m_ConnectedConsumers.Remove(consumer))
+ {
+ OnConsumerDisconnected?.Invoke(consumer);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Consumer unregistered: {consumer.name} ({m_ConnectedConsumers.Count} remaining)");
+ }
+ }
+ }
+
+ ///
+ /// Find and auto-connect consumers within range
+ ///
+ public void AutoConnectConsumersInRange()
+ {
+ if (!IsServer) return;
+
+ Collider[] colliders = Physics.OverlapSphere(transform.position, distributionRadius, consumerLayer);
+ int connectedCount = 0;
+
+ foreach (var collider in colliders)
+ {
+ var consumer = collider.GetComponent();
+ if (consumer != null && !m_ConnectedConsumers.Contains(consumer))
+ {
+ consumer.ConnectToPowerSource(this);
+ connectedCount++;
+ }
+ }
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Auto-connected {connectedCount} consumers");
+ }
+ }
+
+ ///
+ /// Set power source active state (Server RPC)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void SetActiveServerRpc(bool active, RpcParams rpcParams = default)
+ {
+ m_NetworkIsActive.Value = active;
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Active state changed: {active}");
+ }
+ }
+
+ ///
+ /// Set power generation capacity (Server RPC)
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ ///
+ [Rpc(SendTo.Server)]
+ public void SetPowerGenerationServerRpc(float power, RpcParams rpcParams = default)
+ {
+ m_NetworkPowerGeneration.Value = Mathf.Clamp(power, 0, maxPowerCapacity);
+
+ if (debugMode)
+ {
+ Debug.Log($"[PowerSource] Power generation set to: {power}");
+ }
+ }
+
+ ///
+ /// Get power status info for UI/debugging
+ ///
+ public string GetStatusInfo()
+ {
+ return $"Power Source Status:\n" +
+ $"Active: {IsActive}\n" +
+ $"Generation: {CurrentPowerGeneration:F1} / {MaxPowerCapacity:F1}\n" +
+ $"Load: {CurrentLoad:F1} ({LoadPercentage:F1}%)\n" +
+ $"Available: {AvailablePower:F1}\n" +
+ $"Consumers: {m_ConnectedConsumers.Count}\n" +
+ $"Status: {(IsOverloaded ? "OVERLOADED" : "Normal")}";
+ }
+
+ // ============================================================================================
+ // CALLBACKS
+ // ============================================================================================
+
+ private void OnActiveStateChangedCallback(bool previousValue, bool newValue)
+ {
+ OnActiveStateChanged?.Invoke(newValue);
+ UpdateVisualFeedback();
+ }
+
+ private void OnLoadChangedCallback(float previousValue, float newValue)
+ {
+ OnLoadChanged?.Invoke(newValue);
+ }
+
+ // ============================================================================================
+ // VISUAL FEEDBACK
+ // ============================================================================================
+
+ private void UpdateVisualFeedback()
+ {
+ if (statusLight == null) return;
+
+ if (!IsActive)
+ {
+ statusLight.color = offlineColor;
+ }
+ else if (IsOverloaded)
+ {
+ statusLight.color = overloadedColor;
+ // Pulse effect for overload
+ float pulse = Mathf.PingPong(Time.time * 2f, 1f);
+ statusLight.intensity = 1f + pulse;
+ }
+ else
+ {
+ statusLight.color = poweredColor;
+ statusLight.intensity = 1f;
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/README.md b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/README.md
new file mode 100644
index 000000000..76869fc9e
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/README.md
@@ -0,0 +1,94 @@
+# City Power Grid System
+
+A comprehensive power distribution system for GameCreator with full multiplayer support.
+
+## Quick Start
+
+### 1. Add a Power Source
+```
+GameObject → GameCreator → Multiplayer → Power Grid → Power Source
+```
+
+### 2. Add a Power Consumer (House)
+```
+GameObject → GameCreator → Multiplayer → Power Grid → Power Consumer
+```
+
+### 3. Add Power Devices
+```
+Add PowerDevice component to any GameObject inside a consumer
+```
+
+## Features
+
+✅ **Realistic Power Distribution** - Server-authoritative power calculations
+✅ **Multiplayer Support** - Full Unity Netcode integration
+✅ **GameCreator Integration** - Visual scripting Instructions & Conditions
+✅ **Overload Detection** - Automatic load balancing
+✅ **Visual Feedback** - Lights, materials, particles, animations
+✅ **Audio Feedback** - Power on/off sounds
+✅ **Auto-Connection** - Automatically find nearest power source
+✅ **Range-Based** - Distance-limited power distribution
+
+## Components
+
+- **PowerSource** - Generates and distributes power
+- **PowerConsumer** - Houses/buildings that consume power
+- **PowerDevice** - Individual devices (lights, appliances, etc.)
+
+## Visual Scripting
+
+### Instructions
+- Turn On/Off/Toggle Power Device
+- Connect/Disconnect Power Consumer
+- Set Power Source Active
+
+### Conditions
+- Is Device Active/Powered/Operational
+- Is Consumer Powered/Connected
+- Is Source Active/Overloaded
+
+## Complete Documentation
+
+See: `/claudedocs/guides/power-grid-system-guide.md` for:
+- Detailed setup instructions
+- Configuration reference
+- Example scenarios
+- API reference
+- Troubleshooting guide
+
+## File Structure
+
+```
+PowerGrid/
+├── PowerSource.cs # Main power generator
+├── PowerConsumer.cs # Building/house consumer
+├── PowerDevice.cs # Individual device
+├── VisualScripting/
+│ ├── Instructions/
+│ │ ├── InstructionPowerDeviceTurnOn.cs
+│ │ ├── InstructionPowerDeviceTurnOff.cs
+│ │ ├── InstructionPowerDeviceToggle.cs
+│ │ ├── InstructionPowerSourceSetActive.cs
+│ │ ├── InstructionPowerConsumerConnect.cs
+│ │ └── InstructionPowerConsumerDisconnect.cs
+│ └── Conditions/
+│ ├── ConditionPowerDeviceIsActive.cs
+│ ├── ConditionPowerDeviceHasPower.cs
+│ ├── ConditionPowerDeviceIsOperational.cs
+│ ├── ConditionPowerConsumerIsPowered.cs
+│ ├── ConditionPowerConsumerIsConnected.cs
+│ ├── ConditionPowerSourceIsActive.cs
+│ └── ConditionPowerSourceIsOverloaded.cs
+└── README.md
+```
+
+## Requirements
+
+- Unity 2021.3+
+- GameCreator 2.x
+- Unity Netcode for GameObjects
+
+## License
+
+Part of GameCreator Multiplayer package.
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsConnected.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsConnected.cs
new file mode 100644
index 000000000..c3e64ab0b
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsConnected.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Consumer Connected")]
+ [Description("Returns true if the power consumer is connected to a power source")]
+
+ [Category("Power Grid/Consumer/Is Power Consumer Connected")]
+
+ [Keywords("Power", "Consumer", "Connected", "Linked", "House", "Building", "Electricity")]
+
+ [Image(typeof(IconPlug), ColorTheme.Type.Blue)]
+ [Serializable]
+ public class ConditionPowerConsumerIsConnected : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Consumer = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_Consumer} is Connected";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject consumerObject = this.m_Consumer.Get(args);
+ if (consumerObject == null) return false;
+
+ PowerConsumer consumer = consumerObject.Get();
+ return consumer != null && consumer.IsConnected;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsPowered.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsPowered.cs
new file mode 100644
index 000000000..b7770b182
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerConsumerIsPowered.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Consumer Powered")]
+ [Description("Returns true if the power consumer (house/building) has power")]
+
+ [Category("Power Grid/Consumer/Is Power Consumer Powered")]
+
+ [Keywords("Power", "Consumer", "Powered", "House", "Building", "Electricity")]
+
+ [Image(typeof(IconHouse), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionPowerConsumerIsPowered : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Consumer = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_Consumer} is Powered";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject consumerObject = this.m_Consumer.Get(args);
+ if (consumerObject == null) return false;
+
+ PowerConsumer consumer = consumerObject.Get();
+ return consumer != null && consumer.IsPowered;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceHasPower.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceHasPower.cs
new file mode 100644
index 000000000..30f00b2a8
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceHasPower.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Power Device Has Power")]
+ [Description("Returns true if the power device has power available")]
+
+ [Category("Power Grid/Device/Power Device Has Power")]
+
+ [Keywords("Power", "Device", "Available", "Supply", "Electricity")]
+
+ [Image(typeof(IconBoltSolid), ColorTheme.Type.Yellow)]
+ [Serializable]
+ public class ConditionPowerDeviceHasPower : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_Device} has Power";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return false;
+
+ PowerDevice device = deviceObject.Get();
+ return device != null && device.HasPower;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsActive.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsActive.cs
new file mode 100644
index 000000000..0f25cb5ee
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsActive.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Device Active")]
+ [Description("Returns true if the power device is turned on")]
+
+ [Category("Power Grid/Device/Is Power Device Active")]
+
+ [Keywords("Power", "Device", "Active", "On", "Enabled", "Electricity")]
+
+ [Image(typeof(IconBoltOutline), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionPowerDeviceIsActive : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_Device} is Active";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return false;
+
+ PowerDevice device = deviceObject.Get();
+ return device != null && device.IsActive;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsOperational.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsOperational.cs
new file mode 100644
index 000000000..72a28177a
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerDeviceIsOperational.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Device Operational")]
+ [Description("Returns true if the power device is both active and has power")]
+
+ [Category("Power Grid/Device/Is Power Device Operational")]
+
+ [Keywords("Power", "Device", "Operational", "Working", "Running", "Electricity")]
+
+ [Image(typeof(IconBoltSolid), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionPowerDeviceIsOperational : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_Device} is Operational";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return false;
+
+ PowerDevice device = deviceObject.Get();
+ return device != null && device.IsOperational;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsActive.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsActive.cs
new file mode 100644
index 000000000..1492f2f8a
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsActive.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Source Active")]
+ [Description("Returns true if the power source is generating power")]
+
+ [Category("Power Grid/Source/Is Power Source Active")]
+
+ [Keywords("Power", "Source", "Generator", "Active", "Running", "Electricity")]
+
+ [Image(typeof(IconLightbulb), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionPowerSourceIsActive : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_PowerSource = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_PowerSource} is Active";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject sourceObject = this.m_PowerSource.Get(args);
+ if (sourceObject == null) return false;
+
+ PowerSource source = sourceObject.Get();
+ return source != null && source.IsActive;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsOverloaded.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsOverloaded.cs
new file mode 100644
index 000000000..15edb1793
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Conditions/ConditionPowerSourceIsOverloaded.cs
@@ -0,0 +1,38 @@
+using System;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Title("Is Power Source Overloaded")]
+ [Description("Returns true if the power source is overloaded (demand exceeds capacity)")]
+
+ [Category("Power Grid/Source/Is Power Source Overloaded")]
+
+ [Keywords("Power", "Source", "Generator", "Overload", "Capacity", "Electricity")]
+
+ [Image(typeof(IconLightbulb), ColorTheme.Type.Red)]
+ [Serializable]
+ public class ConditionPowerSourceIsOverloaded : Condition
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_PowerSource = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ protected override string Summary => $"{this.m_PowerSource} is Overloaded";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override bool Run(Args args)
+ {
+ GameObject sourceObject = this.m_PowerSource.Get(args);
+ if (sourceObject == null) return false;
+
+ PowerSource source = sourceObject.Get();
+ return source != null && source.IsOverloaded;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerConnect.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerConnect.cs
new file mode 100644
index 000000000..2a8c4b647
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerConnect.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Connect to Power Source")]
+ [Description("Connects a power consumer to the nearest power source")]
+
+ [Category("Power Grid/Consumer/Connect to Power Source")]
+
+ [Parameter("Consumer", "The power consumer (house/building) to connect")]
+
+ [Keywords("Power", "Consumer", "Connect", "Link", "House", "Building", "Electricity")]
+ [Image(typeof(IconPlug), ColorTheme.Type.Green)]
+
+ [Serializable]
+ public class InstructionPowerConsumerConnect : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Consumer = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Connect {this.m_Consumer} to Power";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject consumerObject = this.m_Consumer.Get(args);
+ if (consumerObject == null) return DefaultResult;
+
+ PowerConsumer consumer = consumerObject.Get();
+ if (consumer == null)
+ {
+ Debug.LogWarning($"[InstructionPowerConsumerConnect] No PowerConsumer component found on {consumerObject.name}");
+ return DefaultResult;
+ }
+
+ consumer.RequestConnectionServerRpc();
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerDisconnect.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerDisconnect.cs
new file mode 100644
index 000000000..d9be21964
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerConsumerDisconnect.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Disconnect from Power Source")]
+ [Description("Disconnects a power consumer from its power source")]
+
+ [Category("Power Grid/Consumer/Disconnect from Power Source")]
+
+ [Parameter("Consumer", "The power consumer (house/building) to disconnect")]
+
+ [Keywords("Power", "Consumer", "Disconnect", "Unlink", "House", "Building", "Electricity")]
+ [Image(typeof(IconPlug), ColorTheme.Type.Red)]
+
+ [Serializable]
+ public class InstructionPowerConsumerDisconnect : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Consumer = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Disconnect {this.m_Consumer} from Power";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject consumerObject = this.m_Consumer.Get(args);
+ if (consumerObject == null) return DefaultResult;
+
+ PowerConsumer consumer = consumerObject.Get();
+ if (consumer == null)
+ {
+ Debug.LogWarning($"[InstructionPowerConsumerDisconnect] No PowerConsumer component found on {consumerObject.name}");
+ return DefaultResult;
+ }
+
+ consumer.RequestDisconnectionServerRpc();
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceToggle.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceToggle.cs
new file mode 100644
index 000000000..86120712f
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceToggle.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Toggle Power Device")]
+ [Description("Toggles a power device on or off")]
+
+ [Category("Power Grid/Device/Toggle Power Device")]
+
+ [Parameter("Device", "The power device to toggle")]
+
+ [Keywords("Power", "Device", "Toggle", "Switch", "Electricity")]
+ [Image(typeof(IconBoltOutline), ColorTheme.Type.Yellow)]
+
+ [Serializable]
+ public class InstructionPowerDeviceToggle : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Toggle {this.m_Device}";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return DefaultResult;
+
+ PowerDevice device = deviceObject.Get();
+ if (device == null)
+ {
+ Debug.LogWarning($"[InstructionPowerDeviceToggle] No PowerDevice component found on {deviceObject.name}");
+ return DefaultResult;
+ }
+
+ device.ToggleServerRpc();
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOff.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOff.cs
new file mode 100644
index 000000000..163fc2ef8
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOff.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Turn Off Power Device")]
+ [Description("Turns off a power device")]
+
+ [Category("Power Grid/Device/Turn Off Power Device")]
+
+ [Parameter("Device", "The power device to turn off")]
+
+ [Keywords("Power", "Device", "Off", "Disable", "Deactivate", "Electricity")]
+ [Image(typeof(IconBoltOutline), ColorTheme.Type.Red)]
+
+ [Serializable]
+ public class InstructionPowerDeviceTurnOff : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Turn Off {this.m_Device}";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return DefaultResult;
+
+ PowerDevice device = deviceObject.Get();
+ if (device == null)
+ {
+ Debug.LogWarning($"[InstructionPowerDeviceTurnOff] No PowerDevice component found on {deviceObject.name}");
+ return DefaultResult;
+ }
+
+ device.TurnOffServerRpc();
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOn.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOn.cs
new file mode 100644
index 000000000..d6e0426b2
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerDeviceTurnOn.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Turn On Power Device")]
+ [Description("Turns on a power device if it has power available")]
+
+ [Category("Power Grid/Device/Turn On Power Device")]
+
+ [Parameter("Device", "The power device to turn on")]
+
+ [Keywords("Power", "Device", "On", "Enable", "Activate", "Electricity")]
+ [Image(typeof(IconBoltOutline), ColorTheme.Type.Green)]
+
+ [Serializable]
+ public class InstructionPowerDeviceTurnOn : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_Device = GetGameObjectInstance.Create();
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Turn On {this.m_Device}";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject deviceObject = this.m_Device.Get(args);
+ if (deviceObject == null) return DefaultResult;
+
+ PowerDevice device = deviceObject.Get();
+ if (device == null)
+ {
+ Debug.LogWarning($"[InstructionPowerDeviceTurnOn] No PowerDevice component found on {deviceObject.name}");
+ return DefaultResult;
+ }
+
+ device.TurnOnServerRpc();
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerSourceSetActive.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerSourceSetActive.cs
new file mode 100644
index 000000000..eeaacbef1
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/PowerGrid/VisualScripting/Instructions/InstructionPowerSourceSetActive.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+using GameCreator.Runtime.Common;
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.PowerGrid
+{
+ [Version(0, 1, 0)]
+
+ [Title("Set Power Source Active")]
+ [Description("Enables or disables a power source")]
+
+ [Category("Power Grid/Source/Set Power Source Active")]
+
+ [Parameter("Power Source", "The power source to control")]
+ [Parameter("Active", "Whether the power source should be active")]
+
+ [Keywords("Power", "Source", "Generator", "Enable", "Disable", "Electricity")]
+ [Image(typeof(IconLightbulb), ColorTheme.Type.Blue)]
+
+ [Serializable]
+ public class InstructionPowerSourceSetActive : Instruction
+ {
+ // MEMBERS: -------------------------------------------------------------------------------
+
+ [SerializeField]
+ private PropertyGetGameObject m_PowerSource = GetGameObjectInstance.Create();
+
+ [SerializeField]
+ private PropertyGetBool m_Active = new PropertyGetBool(true);
+
+ // PROPERTIES: ----------------------------------------------------------------------------
+
+ public override string Title => $"Set {this.m_PowerSource} Active: {this.m_Active}";
+
+ // RUN METHOD: ----------------------------------------------------------------------------
+
+ protected override Task Run(Args args)
+ {
+ GameObject sourceObject = this.m_PowerSource.Get(args);
+ if (sourceObject == null) return DefaultResult;
+
+ PowerSource source = sourceObject.Get();
+ if (source == null)
+ {
+ Debug.LogWarning($"[InstructionPowerSourceSetActive] No PowerSource component found on {sourceObject.name}");
+ return DefaultResult;
+ }
+
+ bool active = this.m_Active.Get(args);
+ source.SetActiveServerRpc(active);
+
+ return DefaultResult;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/RPC/NetworkRPCManager.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/RPC/NetworkRPCManager.cs
index 9affb655e..8f54a9098 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/RPC/NetworkRPCManager.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/RPC/NetworkRPCManager.cs
@@ -66,6 +66,11 @@ public enum TargetType
OwnerAndHost // Owner + host authority
}
+ ///
+ /// Authority types for RPC targeting.
+ /// Note: Consider using NetworkAuthorityManager.NetworkAuthorityType for more complete authority handling.
+ ///
+ [Obsolete("Use NetworkAuthorityManager.NetworkAuthorityType for new code")]
public enum AuthorityType
{
Client,
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionAdapter.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionAdapter.cs
index 398ad1b7f..a567c5442 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionAdapter.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Core/NetworkInstructionAdapter.cs
@@ -84,10 +84,11 @@ public void BroadcastInstructionClientRpc(string instructionName)
///
/// [Rpc(SendTo.Server)] Requests server to validate and execute Instruction.
/// Used by NetworkExecutionMode.ServerAuthority.
- /// RequireOwnership = false allows any client to call this RPC.
+ /// NOTE: Uses Rpc(SendTo.Server) for Netcode 2.7.0 compatibility (RequireOwnership deprecated)
+ /// Any client can invoke this RPC by default.
///
- [Rpc(SendTo.Server, RequireOwnership = false)]
- public void RequestServerExecutionServerRpc(string instructionName)
+ [Rpc(SendTo.Server)]
+ public void RequestServerExecutionServerRpc(string instructionName, RpcParams rpcParams = default)
{
var action = ConsumeAction(instructionName);
if (action == null)
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetIsNetworkOwner.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetIsNetworkOwner.cs
new file mode 100644
index 000000000..60b11223d
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetIsNetworkOwner.cs
@@ -0,0 +1,48 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Is Network Owner")]
+ [Category("Multiplayer/Network/Is Owner")]
+ [Description("Returns true if the local client owns the specified NetworkObject. False if not owned or not networked.")]
+ [Image(typeof(IconBoolToggleOn), ColorTheme.Type.Green)]
+
+ [Serializable]
+ public class GetIsNetworkOwner : PropertyTypeGetBool
+ {
+ [SerializeField] private PropertyGetGameObject m_Target = GetGameObjectInstance.Create();
+
+ public override bool Get(Args args)
+ {
+ GameObject target = m_Target.Get(args);
+ if (target == null)
+ {
+ return false;
+ }
+
+ NetworkObject networkObject = target.GetComponent();
+ if (networkObject == null || !networkObject.IsSpawned)
+ {
+ return false;
+ }
+
+ return networkObject.IsOwner;
+ }
+
+ public override bool Get(GameObject gameObject)
+ {
+ return Get(new Args(gameObject));
+ }
+
+ public override string String => $"Is Owner of {m_Target}";
+
+ public static PropertyGetBool Create()
+ {
+ GetIsNetworkOwner instance = new GetIsNetworkOwner();
+ return new PropertyGetBool(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkClientId.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkClientId.cs
new file mode 100644
index 000000000..c73fcba70
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkClientId.cs
@@ -0,0 +1,36 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Network Client ID")]
+ [Category("Multiplayer/Network/Client ID")]
+ [Description("Returns the local client's network ID (ulong). Returns 0 if not connected.")]
+ [Image(typeof(IconID), ColorTheme.Type.Blue)]
+
+ [Serializable]
+ public class GetNetworkClientId : PropertyTypeGetDecimal
+ {
+ public override double Get(Args args)
+ {
+ if (NetworkManager.Singleton == null || !NetworkManager.Singleton.IsConnectedClient)
+ {
+ return 0;
+ }
+
+ return NetworkManager.Singleton.LocalClientId;
+ }
+
+ public override double Get(GameObject gameObject) => Get(Args.EMPTY);
+
+ public override string String => "Local Client ID";
+
+ public static PropertyGetDecimal Create()
+ {
+ GetNetworkClientId instance = new GetNetworkClientId();
+ return new PropertyGetDecimal(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkConnectedClientCount.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkConnectedClientCount.cs
new file mode 100644
index 000000000..ae4ff5c79
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkConnectedClientCount.cs
@@ -0,0 +1,36 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Connected Client Count")]
+ [Category("Multiplayer/Network/Connected Clients")]
+ [Description("Returns the number of currently connected clients (including host). Returns 0 if not connected.")]
+ [Image(typeof(IconPeople), ColorTheme.Type.Blue)]
+
+ [Serializable]
+ public class GetNetworkConnectedClientCount : PropertyTypeGetDecimal
+ {
+ public override double Get(Args args)
+ {
+ if (NetworkManager.Singleton == null || !NetworkManager.Singleton.IsServer)
+ {
+ return 0;
+ }
+
+ return NetworkManager.Singleton.ConnectedClients.Count;
+ }
+
+ public override double Get(GameObject gameObject) => Get(Args.EMPTY);
+
+ public override string String => "Connected Client Count";
+
+ public static PropertyGetDecimal Create()
+ {
+ GetNetworkConnectedClientCount instance = new GetNetworkConnectedClientCount();
+ return new PropertyGetDecimal(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsConnected.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsConnected.cs
new file mode 100644
index 000000000..3c405199d
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsConnected.cs
@@ -0,0 +1,38 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Is Network Connected")]
+ [Category("Multiplayer/Network/Is Connected")]
+ [Description("Returns true if connected to the network (as host, server, or client). False if not connected.")]
+ [Image(typeof(IconConnection), ColorTheme.Type.Green)]
+
+ [Serializable]
+ public class GetNetworkIsConnected : PropertyTypeGetBool
+ {
+ public override bool Get(Args args)
+ {
+ if (NetworkManager.Singleton == null)
+ {
+ return false;
+ }
+
+ return NetworkManager.Singleton.IsConnectedClient ||
+ NetworkManager.Singleton.IsServer ||
+ NetworkManager.Singleton.IsHost;
+ }
+
+ public override bool Get(GameObject gameObject) => Get(Args.EMPTY);
+
+ public override string String => "Is Network Connected";
+
+ public static PropertyGetBool Create()
+ {
+ GetNetworkIsConnected instance = new GetNetworkIsConnected();
+ return new PropertyGetBool(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsHost.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsHost.cs
new file mode 100644
index 000000000..380d30891
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsHost.cs
@@ -0,0 +1,36 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Is Network Host")]
+ [Category("Multiplayer/Network/Is Host")]
+ [Description("Returns true if this client is the host (server + client). False if not connected or client-only.")]
+ [Image(typeof(IconBoolToggleOn), ColorTheme.Type.Green)]
+
+ [Serializable]
+ public class GetNetworkIsHost : PropertyTypeGetBool
+ {
+ public override bool Get(Args args)
+ {
+ if (NetworkManager.Singleton == null)
+ {
+ return false;
+ }
+
+ return NetworkManager.Singleton.IsHost;
+ }
+
+ public override bool Get(GameObject gameObject) => Get(Args.EMPTY);
+
+ public override string String => "Is Network Host";
+
+ public static PropertyGetBool Create()
+ {
+ GetNetworkIsHost instance = new GetNetworkIsHost();
+ return new PropertyGetBool(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsServer.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsServer.cs
new file mode 100644
index 000000000..b3a411e7b
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkIsServer.cs
@@ -0,0 +1,36 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Is Network Server")]
+ [Category("Multiplayer/Network/Is Server")]
+ [Description("Returns true if this instance is running as the server (dedicated server or host). False otherwise.")]
+ [Image(typeof(IconBoolToggleOn), ColorTheme.Type.Yellow)]
+
+ [Serializable]
+ public class GetNetworkIsServer : PropertyTypeGetBool
+ {
+ public override bool Get(Args args)
+ {
+ if (NetworkManager.Singleton == null)
+ {
+ return false;
+ }
+
+ return NetworkManager.Singleton.IsServer;
+ }
+
+ public override bool Get(GameObject gameObject) => Get(Args.EMPTY);
+
+ public override string String => "Is Network Server";
+
+ public static PropertyGetBool Create()
+ {
+ GetNetworkIsServer instance = new GetNetworkIsServer();
+ return new PropertyGetBool(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkObjectOwner.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkObjectOwner.cs
new file mode 100644
index 000000000..3fb52be88
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Get/GetNetworkObjectOwner.cs
@@ -0,0 +1,48 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using Unity.Netcode;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting
+{
+ [Title("Network Object Owner ID")]
+ [Category("Multiplayer/Network/Object Owner")]
+ [Description("Returns the client ID of the owner of a NetworkObject. Returns -1 if not a valid NetworkObject.")]
+ [Image(typeof(IconID), ColorTheme.Type.Yellow)]
+
+ [Serializable]
+ public class GetNetworkObjectOwner : PropertyTypeGetDecimal
+ {
+ [SerializeField] private PropertyGetGameObject m_Target = GetGameObjectInstance.Create();
+
+ public override double Get(Args args)
+ {
+ GameObject target = m_Target.Get(args);
+ if (target == null)
+ {
+ return -1;
+ }
+
+ NetworkObject networkObject = target.GetComponent();
+ if (networkObject == null || !networkObject.IsSpawned)
+ {
+ return -1;
+ }
+
+ return networkObject.OwnerClientId;
+ }
+
+ public override double Get(GameObject gameObject)
+ {
+ return Get(new Args(gameObject));
+ }
+
+ public override string String => $"Owner of {m_Target}";
+
+ public static PropertyGetDecimal Create()
+ {
+ GetNetworkObjectOwner instance = new GetNetworkObjectOwner();
+ return new PropertyGetDecimal(instance);
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/README.md b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/README.md
index 2bb2978e1..05c85d77c 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/README.md
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/README.md
@@ -148,20 +148,57 @@ double healAmount = formula.Calculate(localPlayer, allyPlayer);
- For party UI showing ally stats, use `NetworkTraits` (advanced)
- For competitive games, implement server-authoritative stats (advanced)
-## Advanced: Server-Authoritative Stats
+## Network Properties (Netcode 2.7.0 Compatible)
-For competitive multiplayer or anti-cheat:
+The following Get properties are available for multiplayer state queries:
+
+### GetNetworkClientId
+**Category**: Multiplayer > Network > Client ID
+Returns the local client's network ID (ulong). Returns 0 if not connected.
+
+### GetNetworkIsHost
+**Category**: Multiplayer > Network > Is Host
+Returns true if this client is the host (server + client). False if not connected or client-only.
+
+### GetNetworkIsServer
+**Category**: Multiplayer > Network > Is Server
+Returns true if this instance is running as the server. False otherwise.
+
+### GetNetworkIsConnected
+**Category**: Multiplayer > Network > Is Connected
+Returns true if connected to the network (as host, server, or client).
+
+### GetNetworkConnectedClientCount
+**Category**: Multiplayer > Network > Connected Clients
+Returns the number of currently connected clients (server only).
+
+### GetIsNetworkOwner
+**Category**: Multiplayer > Network > Is Owner
+Returns true if the local client owns the specified NetworkObject.
+
+### GetNetworkObjectOwner
+**Category**: Multiplayer > Network > Object Owner
+Returns the client ID of the owner of a NetworkObject.
+
+## Advanced: Server-Authoritative Stats (Netcode 2.7.0)
+
+For competitive multiplayer or anti-cheat, use the modern `[Rpc(SendTo.Server)]` pattern:
```csharp
+using Unity.Netcode;
+
public class NetworkTraits : NetworkBehaviour
{
private Traits m_Traits;
- [ServerRpc(RequireOwnership = false)]
- private void ChangeAttributeServerRpc(string attributeID, float value)
+ // NOTE: Netcode 2.7.0 deprecated [ServerRpc(RequireOwnership = false)]
+ // Use [Rpc(SendTo.Server)] instead for any client to invoke
+ [Rpc(SendTo.Server)]
+ private void ChangeAttributeServerRpc(string attributeID, float value, RpcParams rpcParams = default)
{
// Server validates the change
- if (!IsValidChange(attributeID, value))
+ // rpcParams.Receive.SenderClientId gives the client who called this
+ if (!IsValidChange(attributeID, value, rpcParams.Receive.SenderClientId))
{
Debug.LogWarning("Invalid stat change rejected");
return;
@@ -183,6 +220,16 @@ public class NetworkTraits : NetworkBehaviour
}
```
+## WebGL Multiplayer Considerations
+
+When building for WebGL:
+
+1. **Transport**: Use WebSockets transport (Unity Transport 2.0.0+)
+2. **CORS**: Configure server for cross-origin resource sharing
+3. **Async**: Avoid synchronous blocking code - use coroutines/async
+4. **Netcode Version**: Requires Netcode for GameObjects 1.2.0+ (WebGL support added)
+```
+
## Support
For issues or questions:
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Set/SetNetworkVariable.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Set/SetNetworkVariable.cs
index 184f4235a..636b657b6 100644
--- a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Set/SetNetworkVariable.cs
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/Properties/Set/SetNetworkVariable.cs
@@ -7,50 +7,58 @@ namespace GameCreator.Multiplayer.Runtime.VisualScripting
{
[Title("Set Network Variable")]
[Category("Multiplayer/Variables/Set Variable")]
- [Description("Sets a synchronized network variable value across all clients")]
+ [Description("Sets a synchronized network variable value across all clients. Registers the variable for sync if not already registered.")]
[Image(typeof(IconNameVariable), ColorTheme.Type.Purple)]
-
+
[Serializable]
public class SetNetworkVariable : PropertyTypeSetString
{
[SerializeField] private PropertyGetString m_VariableName = new PropertyGetString("MyVariable");
- [SerializeField] private PropertyGetString m_Value = new PropertyGetString("");
- [SerializeField] private NetworkVariablesSync.VariableType m_VariableType = NetworkVariablesSync.VariableType.Float;
+ [SerializeField] private NetworkVariablesSync.VariableType m_VariableType = NetworkVariablesSync.VariableType.String;
+ [SerializeField] private NetworkVariablesSync.SyncType m_SyncType = NetworkVariablesSync.SyncType.Everyone;
+ [SerializeField] private bool m_AutoRegister = true;
[SerializeField] private bool m_ForceSync = true;
-
+
public override void Set(string value, Args args)
{
if (NetworkVariablesSync.Instance == null)
{
- Debug.LogError("[SetNetworkVariable] NetworkVariablesSync not found in scene!");
+ Debug.LogError("[SetNetworkVariable] NetworkVariablesSync not found in scene! Add NetworkVariablesSync component to your NetworkManager or a persistent GameObject.");
return;
}
-
+
string variableName = m_VariableName.Get(args);
if (string.IsNullOrEmpty(variableName))
{
Debug.LogError("[SetNetworkVariable] Variable name is empty!");
return;
}
-
- // For now, NetworkVariablesSync doesn't expose public Set methods
- // This component is a placeholder for future implementation
- Debug.LogWarning("[SetNetworkVariable] NetworkVariablesSync doesn't expose public Set methods. Use GlobalVariables directly for now.");
-
- // TODO: Implement public API in NetworkVariablesSync for setting variables
- // NetworkVariablesSync.Instance.SetVariable(variableName, value);
+
+ // Auto-register the variable for network sync if not already registered
+ if (m_AutoRegister)
+ {
+ NetworkVariablesSync.Instance.RegisterVariable(variableName, m_VariableType, m_SyncType);
+ }
+
+ // Force sync to push the change to all clients
+ if (m_ForceSync)
+ {
+ NetworkVariablesSync.Instance.ForceSync(variableName);
+ }
+
+ Debug.Log($"[SetNetworkVariable] Variable '{variableName}' registered for network sync (Type: {m_VariableType}, Sync: {m_SyncType})");
}
-
+
public override void Set(string value, GameObject gameObject)
{
Set(value, new Args(gameObject));
}
-
+
public static PropertySetString Create => new PropertySetString(
new SetNetworkVariable()
);
-
- public override string String => $"Set Network Variable '{m_VariableName}'";
+
+ public override string String => $"Network Sync '{m_VariableName}'";
}
}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCEventReceiver.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCEventReceiver.cs
new file mode 100644
index 000000000..5e6c0cfac
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCEventReceiver.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Component that listens for incoming RPC messages and triggers Actions.
+ /// Place on any GameObject to react to network events via visual scripting.
+ ///
+ /// USAGE:
+ /// 1. Add this component to a GameObject
+ /// 2. Configure message filters
+ /// 3. Assign Actions to execute when messages are received
+ /// 4. Incoming RPC data is passed via Args context
+ ///
+ [AddComponentMenu("GameCreator/Multiplayer/RPC Event Receiver")]
+ [Icon(RuntimePaths.GIZMOS + "GizmoActions.png")]
+ public class RPCEventReceiver : MonoBehaviour
+ {
+ // ============================================================================================
+ // STRUCTS
+ // ============================================================================================
+
+ [Serializable]
+ public class MessageHandler
+ {
+ [Tooltip("The message ID to listen for (leave empty for all)")]
+ public string messageFilter = "";
+
+ [Tooltip("Only trigger if sender matches")]
+ public SenderFilter senderFilter = SenderFilter.Anyone;
+
+ [Tooltip("Actions to execute when message received")]
+ public Actions actions;
+
+ [Tooltip("Pass RPC data to actions via Target")]
+ public bool passDataAsTarget = false;
+ }
+
+ public enum SenderFilter
+ {
+ Anyone,
+ ServerOnly,
+ ClientsOnly,
+ OwnerOnly,
+ SpecificClient
+ }
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Message Handlers")]
+ [SerializeField] private List m_Handlers = new List();
+
+ [Header("Filtering")]
+ [Tooltip("Only process messages when this object is the target")]
+ [SerializeField] private bool m_OnlyWhenTargeted = false;
+
+ [Header("Debug")]
+ [SerializeField] private bool m_DebugMode = false;
+
+ // ============================================================================================
+ // RUNTIME
+ // ============================================================================================
+
+ private NetworkObject m_NetworkObject;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ m_NetworkObject = GetComponent();
+ }
+
+ private void OnEnable()
+ {
+ // Subscribe to all RPC receiver events
+ RPCBroadcastReceiver.OnBroadcastReceived += OnBroadcastReceived;
+ RPCPlayerMessageReceiver.OnPlayerMessageReceived += OnPlayerMessageReceived;
+ RPCNearbyReceiver.OnNearbyMessageReceived += OnNearbyMessageReceived;
+ RPCServerRequestHandler.OnServerRequestReceived += OnServerRequestReceived;
+ RPCVariableSyncReceiver.OnStringVariableSynced += OnStringVariableSynced;
+ RPCVariableSyncReceiver.OnNumberVariableSynced += OnNumberVariableSynced;
+ }
+
+ private void OnDisable()
+ {
+ // Unsubscribe from all events
+ RPCBroadcastReceiver.OnBroadcastReceived -= OnBroadcastReceived;
+ RPCPlayerMessageReceiver.OnPlayerMessageReceived -= OnPlayerMessageReceived;
+ RPCNearbyReceiver.OnNearbyMessageReceived -= OnNearbyMessageReceived;
+ RPCServerRequestHandler.OnServerRequestReceived -= OnServerRequestReceived;
+ RPCVariableSyncReceiver.OnStringVariableSynced -= OnStringVariableSynced;
+ RPCVariableSyncReceiver.OnNumberVariableSynced -= OnNumberVariableSynced;
+ }
+
+ // ============================================================================================
+ // EVENT HANDLERS
+ // ============================================================================================
+
+ private void OnBroadcastReceived(string message, string data, float number, Args args)
+ {
+ ProcessMessage(message, data, number, Vector3.zero, args, "broadcast");
+ }
+
+ private void OnPlayerMessageReceived(string message, string data, float number, Args args)
+ {
+ ProcessMessage(message, data, number, Vector3.zero, args, "player");
+ }
+
+ private void OnNearbyMessageReceived(string message, string data, float number, Vector3 origin, Args args)
+ {
+ ProcessMessage(message, data, number, origin, args, "nearby");
+ }
+
+ private void OnServerRequestReceived(string requestType, string data, Vector3 params3, Vector3 position, Args args)
+ {
+ ProcessMessage(requestType, data, params3.x, position, args, "server");
+ }
+
+ private void OnStringVariableSynced(string varName, string value)
+ {
+ ProcessMessage($"var:{varName}", value, 0, Vector3.zero, new Args(gameObject), "variable");
+ }
+
+ private void OnNumberVariableSynced(string varName, double value)
+ {
+ ProcessMessage($"var:{varName}", "", (float)value, Vector3.zero, new Args(gameObject), "variable");
+ }
+
+ // ============================================================================================
+ // MESSAGE PROCESSING
+ // ============================================================================================
+
+ private void ProcessMessage(string message, string data, float number, Vector3 position, Args args, string source)
+ {
+ if (m_DebugMode)
+ {
+ Debug.Log($"[RPCEventReceiver] Received: {message} from {source}");
+ }
+
+ // Check if this object is the target
+ if (m_OnlyWhenTargeted && args.Target != gameObject)
+ {
+ return;
+ }
+
+ // Find matching handlers
+ foreach (var handler in m_Handlers)
+ {
+ if (!MatchesFilter(handler, message, args))
+ {
+ continue;
+ }
+
+ // Execute actions
+ ExecuteHandler(handler, message, data, number, position, args);
+ }
+ }
+
+ private bool MatchesFilter(MessageHandler handler, string message, Args args)
+ {
+ // Check message filter
+ if (!string.IsNullOrEmpty(handler.messageFilter))
+ {
+ if (handler.messageFilter != message && !message.StartsWith(handler.messageFilter))
+ {
+ return false;
+ }
+ }
+
+ // Check sender filter
+ switch (handler.senderFilter)
+ {
+ case SenderFilter.ServerOnly:
+ // Would need sender info in args
+ break;
+ case SenderFilter.ClientsOnly:
+ break;
+ case SenderFilter.OwnerOnly:
+ if (m_NetworkObject != null && args.Self != null)
+ {
+ var senderNet = args.Self.GetComponent();
+ if (senderNet == null || senderNet.OwnerClientId != m_NetworkObject.OwnerClientId)
+ {
+ return false;
+ }
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ private async void ExecuteHandler(MessageHandler handler, string message, string data, float number, Vector3 position, Args args)
+ {
+ if (handler.actions == null)
+ {
+ if (m_DebugMode)
+ {
+ Debug.LogWarning($"[RPCEventReceiver] Handler for '{message}' has no Actions assigned");
+ }
+ return;
+ }
+
+ // Create args context
+ Args newArgs;
+ if (handler.passDataAsTarget && args.Self != null)
+ {
+ newArgs = new Args(gameObject, args.Self);
+ }
+ else
+ {
+ newArgs = new Args(gameObject, args.Target);
+ }
+
+ // Store additional data in a way the actions can access
+ // Note: This uses GameCreator's property system
+ RPCReceivedData.LastMessage = message;
+ RPCReceivedData.LastStringData = data;
+ RPCReceivedData.LastNumberData = number;
+ RPCReceivedData.LastPosition = position;
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[RPCEventReceiver] Executing actions for '{message}'");
+ }
+
+ await handler.actions.Run(newArgs);
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Add a handler at runtime
+ ///
+ public void AddHandler(string messageFilter, Actions actions, SenderFilter senderFilter = SenderFilter.Anyone)
+ {
+ m_Handlers.Add(new MessageHandler
+ {
+ messageFilter = messageFilter,
+ actions = actions,
+ senderFilter = senderFilter
+ });
+ }
+
+ ///
+ /// Remove handlers for a specific message
+ ///
+ public void RemoveHandlers(string messageFilter)
+ {
+ m_Handlers.RemoveAll(h => h.messageFilter == messageFilter);
+ }
+
+ ///
+ /// Clear all handlers
+ ///
+ public void ClearHandlers()
+ {
+ m_Handlers.Clear();
+ }
+ }
+
+ ///
+ /// Static storage for the most recently received RPC data.
+ /// Access via PropertyGet classes in visual scripting.
+ ///
+ public static class RPCReceivedData
+ {
+ public static string LastMessage { get; set; }
+ public static string LastStringData { get; set; }
+ public static float LastNumberData { get; set; }
+ public static Vector3 LastPosition { get; set; }
+ public static ulong LastSenderId { get; set; }
+ public static float LastReceivedTime { get; set; }
+
+ public static void Clear()
+ {
+ LastMessage = "";
+ LastStringData = "";
+ LastNumberData = 0f;
+ LastPosition = Vector3.zero;
+ LastSenderId = 0;
+ LastReceivedTime = 0f;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCResponseHandler.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCResponseHandler.cs
new file mode 100644
index 000000000..26b00c89b
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Components/RPCResponseHandler.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Component that handles RPC responses with success/failure/timeout branches.
+ /// Provides visual scripting integration for request-response patterns.
+ ///
+ /// USAGE:
+ /// 1. Add this component alongside RPC Instructions
+ /// 2. Configure success/failure/timeout actions
+ /// 3. Call SendRequest() to initiate request with response handling
+ ///
+ [AddComponentMenu("GameCreator/Multiplayer/RPC Response Handler")]
+ [Icon(RuntimePaths.GIZMOS + "GizmoActions.png")]
+ public class RPCResponseHandler : MonoBehaviour
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Response Actions")]
+ [Tooltip("Actions to execute when RPC succeeds")]
+ [SerializeField] private Actions m_OnSuccess;
+
+ [Tooltip("Actions to execute when RPC fails")]
+ [SerializeField] private Actions m_OnFailure;
+
+ [Tooltip("Actions to execute when RPC times out")]
+ [SerializeField] private Actions m_OnTimeout;
+
+ [Header("Settings")]
+ [Tooltip("Default timeout in seconds")]
+ [SerializeField] private float m_Timeout = 5f;
+
+ [Tooltip("Retry on failure")]
+ [SerializeField] private bool m_RetryOnFailure = false;
+
+ [Tooltip("Maximum retry attempts")]
+ [SerializeField] private int m_MaxRetries = 3;
+
+ [Tooltip("Delay between retries")]
+ [SerializeField] private float m_RetryDelay = 1f;
+
+ [Header("Debug")]
+ [SerializeField] private bool m_DebugMode = false;
+
+ // ============================================================================================
+ // RUNTIME STATE
+ // ============================================================================================
+
+ private Dictionary m_PendingRequests = new Dictionary();
+ private uint m_NextRequestId = 1;
+
+ private struct PendingRequest
+ {
+ public uint requestId;
+ public float expirationTime;
+ public int retryCount;
+ public Action onRetry;
+ public Args args;
+ }
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public int PendingCount => m_PendingRequests.Count;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Update()
+ {
+ CheckTimeouts();
+ }
+
+ // ============================================================================================
+ // PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Send an RPC request with response handling
+ ///
+ public async Task SendRequest(
+ VisualScriptingPayload payload,
+ RPCTargetType targetType,
+ Args args,
+ ulong targetClientId = 0)
+ {
+ if (!VisualScriptingRPCManager.Instance.IsReady)
+ {
+ await ExecuteActions(m_OnFailure, args, "Not connected");
+ return VisualScriptingRPCManager.RPCExecutionResult.NotConnected;
+ }
+
+ var tcs = new TaskCompletionSource();
+ uint requestId = m_NextRequestId++;
+
+ // Register pending request
+ m_PendingRequests[requestId] = new PendingRequest
+ {
+ requestId = requestId,
+ expirationTime = Time.time + m_Timeout,
+ retryCount = 0,
+ args = args
+ };
+
+ // Send with callback
+ var result = await VisualScriptingRPCManager.Instance.ExecuteRPCAsync(
+ payload,
+ targetType,
+ targetClientId,
+ (executionResult, responsePayload) => OnResponseReceived(requestId, executionResult, responsePayload, args)
+ );
+
+ // For fire-and-forget calls, handle immediately
+ if (result == VisualScriptingRPCManager.RPCExecutionResult.Success)
+ {
+ // Wait for actual response or timeout
+ // Note: Response comes via callback
+ }
+
+ return result;
+ }
+
+ ///
+ /// Cancel a pending request
+ ///
+ public void CancelRequest(uint requestId)
+ {
+ m_PendingRequests.Remove(requestId);
+ }
+
+ ///
+ /// Cancel all pending requests
+ ///
+ public void CancelAllRequests()
+ {
+ m_PendingRequests.Clear();
+ }
+
+ // ============================================================================================
+ // RESPONSE HANDLING
+ // ============================================================================================
+
+ private async void OnResponseReceived(
+ uint requestId,
+ VisualScriptingRPCManager.RPCExecutionResult result,
+ VisualScriptingPayload responsePayload,
+ Args args)
+ {
+ // Remove from pending
+ m_PendingRequests.Remove(requestId);
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[RPCResponseHandler] Response received: {result}");
+ }
+
+ // Store response data
+ RPCReceivedData.LastMessage = responsePayload.GetString1();
+ RPCReceivedData.LastStringData = responsePayload.GetString2();
+ RPCReceivedData.LastNumberData = responsePayload.floatData1;
+ RPCReceivedData.LastPosition = responsePayload.vectorData1;
+ RPCReceivedData.LastReceivedTime = Time.time;
+
+ // Execute appropriate actions
+ switch (result)
+ {
+ case VisualScriptingRPCManager.RPCExecutionResult.Success:
+ await ExecuteActions(m_OnSuccess, args, "Success");
+ break;
+
+ case VisualScriptingRPCManager.RPCExecutionResult.Failed:
+ await ExecuteActions(m_OnFailure, args, "Failed");
+ break;
+
+ case VisualScriptingRPCManager.RPCExecutionResult.Timeout:
+ await ExecuteActions(m_OnTimeout, args, "Timeout");
+ break;
+
+ case VisualScriptingRPCManager.RPCExecutionResult.NoAuthority:
+ await ExecuteActions(m_OnFailure, args, "No authority");
+ break;
+
+ case VisualScriptingRPCManager.RPCExecutionResult.InvalidTarget:
+ await ExecuteActions(m_OnFailure, args, "Invalid target");
+ break;
+
+ case VisualScriptingRPCManager.RPCExecutionResult.NotConnected:
+ await ExecuteActions(m_OnFailure, args, "Not connected");
+ break;
+ }
+ }
+
+ private void CheckTimeouts()
+ {
+ var timedOut = new List();
+
+ foreach (var kvp in m_PendingRequests)
+ {
+ if (Time.time >= kvp.Value.expirationTime)
+ {
+ timedOut.Add(kvp.Key);
+ }
+ }
+
+ foreach (var requestId in timedOut)
+ {
+ if (m_PendingRequests.TryGetValue(requestId, out var request))
+ {
+ m_PendingRequests.Remove(requestId);
+
+ // Check for retry
+ if (m_RetryOnFailure && request.retryCount < m_MaxRetries)
+ {
+ if (m_DebugMode)
+ {
+ Debug.Log($"[RPCResponseHandler] Retrying request {requestId} (attempt {request.retryCount + 1})");
+ }
+ // Note: Would need to store retry action
+ }
+ else
+ {
+ // Final timeout
+ OnResponseReceived(
+ requestId,
+ VisualScriptingRPCManager.RPCExecutionResult.Timeout,
+ default,
+ request.args
+ );
+ }
+ }
+ }
+ }
+
+ private async Task ExecuteActions(Actions actions, Args args, string reason)
+ {
+ if (actions == null) return;
+
+ if (m_DebugMode)
+ {
+ Debug.Log($"[RPCResponseHandler] Executing actions: {reason}");
+ }
+
+ await actions.Run(new Args(gameObject, args?.Target));
+ }
+
+ // ============================================================================================
+ // EVENTS
+ // ============================================================================================
+
+ public event Action OnResponse;
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCCanReach.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCCanReach.cs
new file mode 100644
index 000000000..04c0322f9
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCCanReach.cs
@@ -0,0 +1,111 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Checks if an RPC can reach a specific target.
+ /// Validates network connection, target existence, and authority.
+ ///
+ [Version(1, 0, 0)]
+ [Title("Can RPC Reach Target")]
+ [Description("Checks if an RPC can be sent to the specified target")]
+ [Category("Multiplayer/RPC/Can Reach Target")]
+ [Parameter("Target Type", "The type of target to check")]
+ [Parameter("Target Client", "Specific client ID when using SpecificClient")]
+ [Keywords("Network", "RPC", "Reach", "Target", "Available", "Connected")]
+ [Image(typeof(IconConnection), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionRPCCanReach : RPCConditionBase
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Target")]
+ [SerializeField] private RPCTargetType m_TargetType = RPCTargetType.Server;
+ [SerializeField] private PropertyGetInteger m_TargetClientId = new PropertyGetInteger(0);
+ [SerializeField] private PropertyGetGameObject m_TargetObject = new PropertyGetGameObject();
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ protected override string Summary => $"can RPC reach {m_TargetType}";
+
+ // ============================================================================================
+ // EVALUATION
+ // ============================================================================================
+
+ protected override bool Evaluate(Args args)
+ {
+ // Check RPC manager availability
+ if (!IsRPCManagerAvailable())
+ {
+ return false;
+ }
+
+ switch (m_TargetType)
+ {
+ case RPCTargetType.Server:
+ return IsNetworkAvailable; // Can always reach server if connected
+
+ case RPCTargetType.AllClients:
+ case RPCTargetType.OtherClients:
+ return IsNetworkAvailable && ConnectedClientCount > 0;
+
+ case RPCTargetType.SpecificClient:
+ return CanReachSpecificClient(args);
+
+ case RPCTargetType.Owner:
+ return CanReachOwner(args);
+
+ case RPCTargetType.Local:
+ return true; // Can always execute locally
+
+ case RPCTargetType.HostPlayer:
+ return IsNetworkAvailable; // Can reach host if connected
+
+ case RPCTargetType.Team:
+ case RPCTargetType.NearbyPlayers:
+ return IsNetworkAvailable; // Requires network
+
+ default:
+ return false;
+ }
+ }
+
+ private bool CanReachSpecificClient(Args args)
+ {
+ if (!IsNetworkAvailable) return false;
+
+ ulong targetId = (ulong)m_TargetClientId.Get(args);
+
+ // Check if client is connected
+ return NetworkManager.Singleton.ConnectedClientsIds.Contains(targetId);
+ }
+
+ private bool CanReachOwner(Args args)
+ {
+ if (!IsNetworkAvailable) return false;
+
+ GameObject targetObj = m_TargetObject.Get(args);
+ if (targetObj == null) return false;
+
+ var netObj = targetObj.GetComponent();
+ if (netObj == null || !netObj.IsSpawned) return false;
+
+ // Check if owner client is connected
+ return NetworkManager.Singleton.ConnectedClientsIds.Contains(netObj.OwnerClientId);
+ }
+
+ protected override bool EvaluateLocally(Args args)
+ {
+ // When offline, only local execution is possible
+ return m_TargetType == RPCTargetType.Local;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCHasAuthority.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCHasAuthority.cs
new file mode 100644
index 000000000..6112b7af2
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCHasAuthority.cs
@@ -0,0 +1,79 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Checks if the local client has the required authority to execute an RPC.
+ ///
+ [Version(1, 0, 0)]
+ [Title("Has RPC Authority")]
+ [Description("Checks if the local client has authority to execute an RPC with the specified requirements")]
+ [Category("Multiplayer/RPC/Has Authority")]
+ [Parameter("Required Authority", "The authority level required")]
+ [Parameter("Context Object", "The object to check ownership for")]
+ [Keywords("Network", "RPC", "Authority", "Permission", "Owner", "Server")]
+ [Image(typeof(IconPermission), ColorTheme.Type.Yellow)]
+ [Serializable]
+ public class ConditionRPCHasAuthority : RPCConditionBase
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Authority Check")]
+ [SerializeField] private RPCAuthority m_RequiredAuthority = RPCAuthority.Anyone;
+ [SerializeField] private PropertyGetGameObject m_ContextObject = GetGameObjectSelf.Create();
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ protected override string Summary => $"has {m_RequiredAuthority} authority";
+
+ // ============================================================================================
+ // EVALUATION
+ // ============================================================================================
+
+ protected override bool Evaluate(Args args)
+ {
+ switch (m_RequiredAuthority)
+ {
+ case RPCAuthority.Anyone:
+ return true;
+
+ case RPCAuthority.OwnerOnly:
+ return CheckOwnership(args);
+
+ case RPCAuthority.ServerOnly:
+ return IsServer;
+
+ case RPCAuthority.OwnerOrServer:
+ return CheckOwnership(args) || IsServer;
+
+ default:
+ return false;
+ }
+ }
+
+ private bool CheckOwnership(Args args)
+ {
+ GameObject contextObj = m_ContextObject.Get(args);
+ if (contextObj == null) return false;
+
+ var netObj = contextObj.GetComponent();
+ if (netObj == null) return false;
+
+ return netObj.IsOwner;
+ }
+
+ protected override bool EvaluateLocally(Args args)
+ {
+ // When offline, treat as having full authority
+ return true;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCIsTargetType.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCIsTargetType.cs
new file mode 100644
index 000000000..1d66ab6fc
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCIsTargetType.cs
@@ -0,0 +1,92 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Checks if the local client matches a specific RPC target type.
+ /// Useful for conditional execution based on network role.
+ ///
+ [Version(1, 0, 0)]
+ [Title("Is RPC Target Type")]
+ [Description("Checks if the local client would receive an RPC sent to the specified target type")]
+ [Category("Multiplayer/RPC/Is Target Type")]
+ [Parameter("Target Type", "The target type to check against")]
+ [Keywords("Network", "RPC", "Target", "Type", "Server", "Client", "Owner")]
+ [Image(typeof(IconTarget), ColorTheme.Type.Blue)]
+ [Serializable]
+ public class ConditionRPCIsTargetType : RPCConditionBase
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Target Type")]
+ [SerializeField] private RPCTargetType m_TargetType = RPCTargetType.Server;
+ [SerializeField] private PropertyGetGameObject m_ContextObject = GetGameObjectSelf.Create();
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ protected override string Summary => $"is {m_TargetType} target";
+
+ // ============================================================================================
+ // EVALUATION
+ // ============================================================================================
+
+ protected override bool Evaluate(Args args)
+ {
+ switch (m_TargetType)
+ {
+ case RPCTargetType.Server:
+ return IsServer;
+
+ case RPCTargetType.AllClients:
+ return true; // All clients receive
+
+ case RPCTargetType.OtherClients:
+ return true; // Would receive if not sender
+
+ case RPCTargetType.SpecificClient:
+ return true; // Would need to check sender context
+
+ case RPCTargetType.Owner:
+ return CheckOwnership(args);
+
+ case RPCTargetType.Local:
+ return true; // Local always receives
+
+ case RPCTargetType.HostPlayer:
+ return IsHostPlayer();
+
+ case RPCTargetType.Team:
+ return true; // Would need team check
+
+ case RPCTargetType.NearbyPlayers:
+ return true; // Would need range check
+
+ default:
+ return false;
+ }
+ }
+
+ private bool CheckOwnership(Args args)
+ {
+ GameObject contextObj = m_ContextObject.Get(args);
+ if (contextObj == null) return false;
+
+ var netObj = contextObj.GetComponent();
+ return netObj != null && netObj.IsOwner;
+ }
+
+ protected override bool EvaluateLocally(Args args)
+ {
+ // When offline, only local target applies
+ return m_TargetType == RPCTargetType.Local;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCManagerReady.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCManagerReady.cs
new file mode 100644
index 000000000..14d8fac7f
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Conditions/ConditionRPCManagerReady.cs
@@ -0,0 +1,42 @@
+using System;
+using UnityEngine;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Checks if the VisualScripting RPC Manager is ready for use.
+ ///
+ [Version(1, 0, 0)]
+ [Title("RPC Manager Ready")]
+ [Description("Checks if the Visual Scripting RPC Manager is initialized and ready")]
+ [Category("Multiplayer/RPC/Manager Ready")]
+ [Keywords("Network", "RPC", "Manager", "Ready", "Initialized", "Available")]
+ [Image(typeof(IconCircleSolid), ColorTheme.Type.Green)]
+ [Serializable]
+ public class ConditionRPCManagerReady : RPCConditionBase
+ {
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ protected override string Summary => "RPC Manager is ready";
+
+ // ============================================================================================
+ // EVALUATION
+ // ============================================================================================
+
+ protected override bool Evaluate(Args args)
+ {
+ return VisualScriptingRPCManager.Instance != null &&
+ VisualScriptingRPCManager.Instance.IsReady;
+ }
+
+ protected override bool EvaluateLocally(Args args)
+ {
+ // When offline, manager is not needed
+ return true;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCConditionBase.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCConditionBase.cs
new file mode 100644
index 000000000..02aa17334
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCConditionBase.cs
@@ -0,0 +1,231 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Base class for network-aware Conditions.
+ /// Supports local checks with network context awareness.
+ ///
+ /// DESIGN NOTE:
+ /// Conditions are synchronous in GameCreator, so we can't do async RPC calls.
+ /// Instead, this base class provides:
+ /// - Network state awareness (IsServer, IsOwner, IsConnected)
+ /// - Cached network query results
+ /// - Helper methods for common network checks
+ ///
+ /// For async network validation, use Instructions with WaitForResponse.
+ ///
+ [Serializable]
+ public abstract class RPCConditionBase : Condition
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Network Context")]
+ [Tooltip("How to handle condition when not connected")]
+ [SerializeField] protected OfflineMode m_OfflineMode = OfflineMode.ReturnTrue;
+
+ // ============================================================================================
+ // ENUMS
+ // ============================================================================================
+
+ public enum OfflineMode
+ {
+ /// Return true when offline
+ ReturnTrue,
+
+ /// Return false when offline
+ ReturnFalse,
+
+ /// Evaluate locally as if it were a single-player game
+ EvaluateLocally
+ }
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ /// Is network available and connected?
+ protected bool IsNetworkAvailable =>
+ NetworkManager.Singleton != null &&
+ NetworkManager.Singleton.IsConnectedClient;
+
+ /// Is this the server/host?
+ protected bool IsServer => NetworkManager.Singleton?.IsServer ?? false;
+
+ /// Is this a client (not server)?
+ protected bool IsClient => NetworkManager.Singleton?.IsClient ?? false && !IsServer;
+
+ /// Is this the host (server + client)?
+ protected bool IsHost => NetworkManager.Singleton?.IsHost ?? false;
+
+ /// Local client ID
+ protected ulong LocalClientId => NetworkManager.Singleton?.LocalClientId ?? 0;
+
+ /// Total connected clients
+ protected int ConnectedClientCount => NetworkManager.Singleton?.ConnectedClientsIds?.Count ?? 0;
+
+ // ============================================================================================
+ // MAIN EXECUTION
+ // ============================================================================================
+
+ protected sealed override bool Run(Args args)
+ {
+ // Handle offline mode
+ if (!IsNetworkAvailable)
+ {
+ switch (m_OfflineMode)
+ {
+ case OfflineMode.ReturnTrue:
+ return true;
+ case OfflineMode.ReturnFalse:
+ return false;
+ case OfflineMode.EvaluateLocally:
+ return EvaluateLocally(args);
+ }
+ }
+
+ // Network is available - evaluate with network context
+ return Evaluate(args);
+ }
+
+ // ============================================================================================
+ // ABSTRACT METHODS - Override in derived classes
+ // ============================================================================================
+
+ ///
+ /// Evaluate the condition with full network context.
+ /// Called when network is available.
+ ///
+ protected abstract bool Evaluate(Args args);
+
+ // ============================================================================================
+ // VIRTUAL METHODS - Optionally override
+ // ============================================================================================
+
+ ///
+ /// Evaluate the condition locally when offline.
+ /// Default returns the same as Evaluate.
+ /// Override if offline behavior should differ.
+ ///
+ protected virtual bool EvaluateLocally(Args args)
+ {
+ return Evaluate(args);
+ }
+
+ // ============================================================================================
+ // HELPER METHODS FOR DERIVED CLASSES
+ // ============================================================================================
+
+ ///
+ /// Check if Args.Self has a NetworkObject
+ ///
+ protected bool HasNetworkObject(Args args)
+ {
+ if (args?.Self == null) return false;
+ return args.Self.GetComponent() != null;
+ }
+
+ ///
+ /// Get NetworkObject from Args.Self
+ ///
+ protected NetworkObject GetNetworkObject(Args args)
+ {
+ return args?.Self?.GetComponent();
+ }
+
+ ///
+ /// Check if local player is owner of Args.Self
+ ///
+ protected bool IsOwner(Args args)
+ {
+ var netObj = GetNetworkObject(args);
+ return netObj != null && netObj.IsOwner;
+ }
+
+ ///
+ /// Check if Args.Self is spawned on the network
+ ///
+ protected bool IsSpawned(Args args)
+ {
+ var netObj = GetNetworkObject(args);
+ return netObj != null && netObj.IsSpawned;
+ }
+
+ ///
+ /// Get the owner client ID of Args.Self
+ ///
+ protected ulong GetOwnerClientId(Args args)
+ {
+ var netObj = GetNetworkObject(args);
+ return netObj?.OwnerClientId ?? 0;
+ }
+
+ ///
+ /// Check if a specific client ID is the owner
+ ///
+ protected bool IsOwnedBy(Args args, ulong clientId)
+ {
+ return GetOwnerClientId(args) == clientId;
+ }
+
+ ///
+ /// Check if local player is the host player
+ ///
+ protected bool IsHostPlayer()
+ {
+ return IsHost && LocalClientId == NetworkManager.ServerClientId;
+ }
+
+ ///
+ /// Check if a client ID corresponds to the host
+ ///
+ protected bool IsHostClient(ulong clientId)
+ {
+ return clientId == NetworkManager.ServerClientId;
+ }
+
+ ///
+ /// Get the NetworkObject for Args.Target
+ ///
+ protected NetworkObject GetTargetNetworkObject(Args args)
+ {
+ return args?.Target?.GetComponent();
+ }
+
+ ///
+ /// Check if Args.Target is owned by local player
+ ///
+ protected bool IsTargetOwner(Args args)
+ {
+ var netObj = GetTargetNetworkObject(args);
+ return netObj != null && netObj.IsOwner;
+ }
+
+ ///
+ /// Check if Args.Self and Args.Target are owned by the same client
+ ///
+ protected bool SameOwner(Args args)
+ {
+ var selfNet = GetNetworkObject(args);
+ var targetNet = GetTargetNetworkObject(args);
+
+ if (selfNet == null || targetNet == null) return false;
+ return selfNet.OwnerClientId == targetNet.OwnerClientId;
+ }
+
+ ///
+ /// Check if the RPC manager is available
+ ///
+ protected bool IsRPCManagerAvailable()
+ {
+ return VisualScriptingRPCManager.Instance != null &&
+ VisualScriptingRPCManager.Instance.IsSpawned;
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCIcons.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCIcons.cs
new file mode 100644
index 000000000..bf57e8404
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCIcons.cs
@@ -0,0 +1,42 @@
+using UnityEngine;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Icon definitions for RPC visual scripting components.
+ /// These are placeholder classes - in production, use proper icon textures.
+ ///
+
+ // Broadcast icon - for sending to all clients
+ public class IconBroadcast : Texture2D { public IconBroadcast() : base(1, 1) { } }
+
+ // Player icon - for player-specific RPCs
+ public class IconPlayer : Texture2D { public IconPlayer() : base(1, 1) { } }
+
+ // Server icon - for server-bound RPCs
+ public class IconServer : Texture2D { public IconServer() : base(1, 1) { } }
+
+ // Variable icon - for variable sync
+ public class IconVariable : Texture2D { public IconVariable() : base(1, 1) { } }
+
+ // Radius icon - for nearby/range RPCs
+ public class IconRadius : Texture2D { public IconRadius() : base(1, 1) { } }
+
+ // Connection icon - for network connectivity
+ public class IconConnection : Texture2D { public IconConnection() : base(1, 1) { } }
+
+ // Permission icon - for authority checks
+ public class IconPermission : Texture2D { public IconPermission() : base(1, 1) { } }
+
+ // Target icon - for targeting checks
+ public class IconTarget : Texture2D { public IconTarget() : base(1, 1) { } }
+
+ // Circle solid icon - for status indicators
+ public class IconCircleSolid : Texture2D { public IconCircleSolid() : base(1, 1) { } }
+
+ // Actions icon - for execute actions RPC
+ public class IconActions : Texture2D { public IconActions() : base(1, 1) { } }
+
+ // Arrow overlay - for directional indicators
+ public class OverlayArrowRight : Texture2D { public OverlayArrowRight() : base(1, 1) { } }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCInstructionBase.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCInstructionBase.cs
new file mode 100644
index 000000000..c3f2a31d4
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/RPCInstructionBase.cs
@@ -0,0 +1,365 @@
+using System;
+using System.Threading.Tasks;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Base class for all RPC-capable Instructions.
+ /// Provides built-in network execution, targeting, and response handling.
+ ///
+ /// USAGE:
+ /// 1. Inherit from this class instead of Instruction
+ /// 2. Override RunLocally() for the actual instruction logic
+ /// 3. Set m_TargetType and m_Authority in inspector
+ /// 4. Optionally override PackPayload/UnpackPayload for custom data
+ ///
+ /// EXECUTION FLOW:
+ /// 1. Run() checks network mode and authority
+ /// 2. If networked: PackPayload → Send RPC → Remote: UnpackPayload → RunLocally
+ /// 3. If local: RunLocally directly
+ ///
+ [Serializable]
+ public abstract class RPCInstructionBase : Instruction
+ {
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Network Execution")]
+ [Tooltip("Where should this instruction execute?")]
+ [SerializeField] protected RPCTargetType m_TargetType = RPCTargetType.Local;
+
+ [Tooltip("Who is allowed to trigger this instruction?")]
+ [SerializeField] protected RPCAuthority m_Authority = RPCAuthority.Anyone;
+
+ [Tooltip("Priority for RPC batching")]
+ [SerializeField] protected RPCPriority m_Priority = RPCPriority.Normal;
+
+ [Tooltip("Wait for RPC confirmation before continuing")]
+ [SerializeField] protected bool m_WaitForResponse = false;
+
+ [Tooltip("Timeout for waiting (seconds)")]
+ [SerializeField] protected float m_Timeout = 5f;
+
+ [Header("Targeting")]
+ [Tooltip("Specific client ID when using SpecificClient target")]
+ [SerializeField] protected PropertyGetInteger m_TargetClientId = new PropertyGetInteger();
+
+ [Tooltip("Team ID when using Team target")]
+ [SerializeField] protected PropertyGetInteger m_TeamId = new PropertyGetInteger();
+
+ [Tooltip("Range when using NearbyPlayers target")]
+ [SerializeField] protected PropertyGetDecimal m_Range = new PropertyGetDecimal(10f);
+
+ // ============================================================================================
+ // RUNTIME STATE
+ // ============================================================================================
+
+ protected int m_CachedTypeId = -1;
+ protected bool m_IsRegistered = false;
+
+ // Response tracking
+ protected TaskCompletionSource m_ResponseTCS;
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ ///
+ /// Get the registered type ID for this instruction
+ ///
+ protected int TypeId
+ {
+ get
+ {
+ if (m_CachedTypeId < 0)
+ {
+ EnsureRegistered();
+ }
+ return m_CachedTypeId;
+ }
+ }
+
+ ///
+ /// Is network execution available?
+ ///
+ protected bool IsNetworkAvailable =>
+ NetworkManager.Singleton != null &&
+ NetworkManager.Singleton.IsConnectedClient &&
+ VisualScriptingRPCManager.Instance != null &&
+ VisualScriptingRPCManager.Instance.IsSpawned;
+
+ ///
+ /// Is this the server/host?
+ ///
+ protected bool IsServer => NetworkManager.Singleton?.IsServer ?? false;
+
+ ///
+ /// Is this the owner of the context object?
+ ///
+ protected bool IsOwner(Args args)
+ {
+ if (args?.Self == null) return false;
+ var netObj = args.Self.GetComponent();
+ return netObj != null && netObj.IsOwner;
+ }
+
+ // ============================================================================================
+ // MAIN EXECUTION
+ // ============================================================================================
+
+ protected sealed override async Task Run(Args args)
+ {
+ // Ensure we're registered with the RPC manager
+ EnsureRegistered();
+
+ // Check if we should execute locally or via network
+ if (!IsNetworkAvailable || m_TargetType == RPCTargetType.Local)
+ {
+ // Local execution
+ await RunLocally(args);
+ return;
+ }
+
+ // Check authority
+ if (!HasAuthority(args))
+ {
+ Debug.LogWarning($"[RPC] {GetType().Name}: No authority to execute (required: {m_Authority})");
+ return;
+ }
+
+ // Network execution
+ await ExecuteNetworkRPC(args);
+ }
+
+ ///
+ /// Execute the RPC to remote targets
+ ///
+ private async Task ExecuteNetworkRPC(Args args)
+ {
+ // Pack payload with instruction data
+ var payload = CreatePayload(args);
+ PackPayload(ref payload, args);
+
+ // Determine target client ID if needed
+ ulong targetClientId = 0;
+ if (m_TargetType == RPCTargetType.SpecificClient)
+ {
+ targetClientId = (ulong)m_TargetClientId.Get(args);
+ }
+
+ if (m_WaitForResponse)
+ {
+ // Wait for response
+ m_ResponseTCS = new TaskCompletionSource();
+
+ VisualScriptingRPCManager.Instance.ExecuteRPC(
+ payload,
+ m_TargetType,
+ targetClientId);
+
+ // Wait with timeout
+ var timeoutTask = Task.Delay(TimeSpan.FromSeconds(m_Timeout));
+ var completedTask = await Task.WhenAny(m_ResponseTCS.Task, timeoutTask);
+
+ if (completedTask == timeoutTask)
+ {
+ Debug.LogWarning($"[RPC] {GetType().Name}: Response timeout");
+ OnRPCTimeout(args);
+ }
+ else
+ {
+ var result = await m_ResponseTCS.Task;
+ OnRPCResponse(result, args);
+ }
+ }
+ else
+ {
+ // Fire and forget
+ VisualScriptingRPCManager.Instance.ExecuteRPC(
+ payload,
+ m_TargetType,
+ targetClientId);
+
+ // Also execute locally if targeting AllClients or if we're the server
+ if (ShouldExecuteLocally())
+ {
+ await RunLocally(args);
+ }
+ }
+ }
+
+ ///
+ /// Should we also execute locally when sending network RPC?
+ ///
+ protected virtual bool ShouldExecuteLocally()
+ {
+ switch (m_TargetType)
+ {
+ case RPCTargetType.AllClients:
+ return true; // AllClients includes local
+ case RPCTargetType.Owner:
+ return NetworkManager.Singleton?.LocalClientId == 0; // Check if we're owner
+ case RPCTargetType.Server:
+ return IsServer;
+ case RPCTargetType.HostPlayer:
+ return IsServer;
+ default:
+ return false;
+ }
+ }
+
+ // ============================================================================================
+ // ABSTRACT METHODS - Override in derived classes
+ // ============================================================================================
+
+ ///
+ /// Execute the instruction logic locally.
+ /// This is called on the target machine after RPC delivery.
+ ///
+ protected abstract Task RunLocally(Args args);
+
+ // ============================================================================================
+ // VIRTUAL METHODS - Optionally override
+ // ============================================================================================
+
+ ///
+ /// Pack custom instruction data into the payload.
+ /// Override to add instruction-specific parameters.
+ ///
+ protected virtual void PackPayload(ref VisualScriptingPayload payload, Args args)
+ {
+ // Default: no custom data
+ // Override to pack floatData1-4, intData1-3, stringData1-2, vectorData1-2, etc.
+ }
+
+ ///
+ /// Unpack custom instruction data from the payload.
+ /// Override to extract instruction-specific parameters.
+ ///
+ protected virtual void UnpackPayload(VisualScriptingPayload payload, Args args)
+ {
+ // Default: no custom data
+ // Override to unpack floatData1-4, intData1-3, stringData1-2, vectorData1-2, etc.
+ }
+
+ ///
+ /// Called when RPC response is received (if m_WaitForResponse is true)
+ ///
+ protected virtual void OnRPCResponse(VisualScriptingRPCManager.RPCExecutionResult result, Args args)
+ {
+ // Override to handle response
+ }
+
+ ///
+ /// Called when RPC times out (if m_WaitForResponse is true)
+ ///
+ protected virtual void OnRPCTimeout(Args args)
+ {
+ // Override to handle timeout
+ }
+
+ // ============================================================================================
+ // REGISTRATION
+ // ============================================================================================
+
+ private void EnsureRegistered()
+ {
+ if (m_IsRegistered) return;
+
+ var manager = VisualScriptingRPCManager.Instance;
+ if (manager == null) return;
+
+ // Register type
+ m_CachedTypeId = manager.RegisterInstructionType(GetType());
+
+ // Register handler
+ manager.RegisterHandler(m_CachedTypeId, HandleRemoteExecution);
+
+ m_IsRegistered = true;
+ }
+
+ ///
+ /// Handle RPC execution on the receiving end
+ ///
+ private async Task HandleRemoteExecution(VisualScriptingPayload payload, Args args)
+ {
+ // Unpack custom data
+ UnpackPayload(payload, args);
+
+ // Execute locally
+ await RunLocally(args);
+ }
+
+ // ============================================================================================
+ // AUTHORITY
+ // ============================================================================================
+
+ private bool HasAuthority(Args args)
+ {
+ switch (m_Authority)
+ {
+ case RPCAuthority.Anyone:
+ return true;
+
+ case RPCAuthority.OwnerOnly:
+ return IsOwner(args);
+
+ case RPCAuthority.ServerOnly:
+ return IsServer;
+
+ case RPCAuthority.OwnerOrServer:
+ return IsOwner(args) || IsServer;
+
+ default:
+ return false;
+ }
+ }
+
+ // ============================================================================================
+ // PAYLOAD CREATION
+ // ============================================================================================
+
+ private VisualScriptingPayload CreatePayload(Args args)
+ {
+ return VisualScriptingPayload.CreateFromArgs(TypeId, args, m_Priority);
+ }
+
+ // ============================================================================================
+ // UTILITY METHODS FOR DERIVED CLASSES
+ // ============================================================================================
+
+ ///
+ /// Get a NetworkObject from Args.Self
+ ///
+ protected NetworkObject GetNetworkObject(Args args)
+ {
+ return args?.Self?.GetComponent();
+ }
+
+ ///
+ /// Get a NetworkObject from Args.Target
+ ///
+ protected NetworkObject GetTargetNetworkObject(Args args)
+ {
+ return args?.Target?.GetComponent();
+ }
+
+ ///
+ /// Check if we're connected to a network session
+ ///
+ protected bool IsConnected()
+ {
+ return NetworkManager.Singleton != null && NetworkManager.Singleton.IsConnectedClient;
+ }
+
+ ///
+ /// Get the local client ID
+ ///
+ protected ulong LocalClientId => NetworkManager.Singleton?.LocalClientId ?? 0;
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingPayload.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingPayload.cs
new file mode 100644
index 000000000..02efc983f
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingPayload.cs
@@ -0,0 +1,314 @@
+using System;
+using UnityEngine;
+using Unity.Netcode;
+using Unity.Collections;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// RPC target types for visual scripting execution
+ ///
+ public enum RPCTargetType
+ {
+ /// Execute on server only
+ Server = 0,
+
+ /// Execute on all connected clients (including server if host)
+ AllClients = 1,
+
+ /// Execute on a specific client by ID
+ SpecificClient = 2,
+
+ /// Execute on all clients except the sender
+ OtherClients = 3,
+
+ /// Execute on the network object owner
+ Owner = 4,
+
+ /// Execute locally only (no network)
+ Local = 5,
+
+ /// Execute on players within range
+ NearbyPlayers = 6,
+
+ /// Execute on team members
+ Team = 7,
+
+ /// Execute on host player only
+ HostPlayer = 8
+ }
+
+ ///
+ /// Priority levels for RPC batching
+ ///
+ public enum RPCPriority
+ {
+ /// Can be delayed, batched aggressively (1-2 Hz)
+ Low = 0,
+
+ /// Normal priority, standard batching (5-10 Hz)
+ Normal = 1,
+
+ /// Important, minimal delay (20-30 Hz)
+ High = 2,
+
+ /// Sent immediately, never batched
+ Critical = 3
+ }
+
+ ///
+ /// Authority requirements for RPC execution
+ ///
+ public enum RPCAuthority
+ {
+ /// Anyone can execute
+ Anyone = 0,
+
+ /// Only the network object owner
+ OwnerOnly = 1,
+
+ /// Only the server/host
+ ServerOnly = 2,
+
+ /// Owner or server
+ OwnerOrServer = 3
+ }
+
+ ///
+ /// Serializable payload for visual scripting RPC calls.
+ /// Contains all data needed to execute an Instruction remotely.
+ ///
+ /// DESIGN GOALS:
+ /// - Minimal allocation (fixed-size where possible)
+ /// - Type-safe instruction identification
+ /// - Args context preservation (Self/Target references)
+ /// - Custom data support for instruction parameters
+ ///
+ [Serializable]
+ public struct VisualScriptingPayload : INetworkSerializable
+ {
+ // ============================================================================================
+ // IDENTIFICATION
+ // ============================================================================================
+
+ /// Unique RPC call ID for response matching
+ public uint rpcId;
+
+ /// Registered instruction type ID
+ public int instructionTypeId;
+
+ /// Sender client ID for response routing
+ public ulong senderId;
+
+ /// Client ID to exclude from broadcast (for OtherClients target)
+ public ulong excludeClientId;
+
+ /// Timestamp for latency calculation
+ public float timestamp;
+
+ /// Priority level for batching
+ public RPCPriority priority;
+
+ // ============================================================================================
+ // ARGS CONTEXT
+ // ============================================================================================
+
+ /// Network ID of the "Self" GameObject
+ public ulong selfNetworkId;
+
+ /// Network ID of the "Target" GameObject
+ public ulong targetNetworkId;
+
+ // ============================================================================================
+ // CUSTOM DATA (for instruction parameters)
+ // ============================================================================================
+
+ /// String data slot 1 (max 64 chars)
+ public FixedString64Bytes stringData1;
+
+ /// String data slot 2 (max 64 chars)
+ public FixedString64Bytes stringData2;
+
+ /// Float data slots
+ public float floatData1;
+ public float floatData2;
+ public float floatData3;
+ public float floatData4;
+
+ /// Int data slots
+ public int intData1;
+ public int intData2;
+ public int intData3;
+
+ /// Bool data packed into single int
+ public int boolFlags;
+
+ /// Vector3 data slot
+ public Vector3 vectorData1;
+
+ /// Vector3 data slot 2
+ public Vector3 vectorData2;
+
+ /// Quaternion data slot
+ public Quaternion rotationData;
+
+ /// Additional network object reference
+ public ulong networkObjectId1;
+
+ /// Additional network object reference 2
+ public ulong networkObjectId2;
+
+ // ============================================================================================
+ // BOOL FLAG HELPERS
+ // ============================================================================================
+
+ public bool GetBool(int index)
+ {
+ if (index < 0 || index >= 32) return false;
+ return (boolFlags & (1 << index)) != 0;
+ }
+
+ public void SetBool(int index, bool value)
+ {
+ if (index < 0 || index >= 32) return;
+ if (value)
+ boolFlags |= (1 << index);
+ else
+ boolFlags &= ~(1 << index);
+ }
+
+ // ============================================================================================
+ // STRING HELPERS
+ // ============================================================================================
+
+ public string GetString1() => stringData1.ToString();
+ public string GetString2() => stringData2.ToString();
+
+ public void SetString1(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ stringData1 = default;
+ else
+ stringData1 = new FixedString64Bytes(value.Length > 64 ? value.Substring(0, 64) : value);
+ }
+
+ public void SetString2(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ stringData2 = default;
+ else
+ stringData2 = new FixedString64Bytes(value.Length > 64 ? value.Substring(0, 64) : value);
+ }
+
+ // ============================================================================================
+ // NETWORK SERIALIZATION
+ // ============================================================================================
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ // Identification
+ serializer.SerializeValue(ref rpcId);
+ serializer.SerializeValue(ref instructionTypeId);
+ serializer.SerializeValue(ref senderId);
+ serializer.SerializeValue(ref excludeClientId);
+ serializer.SerializeValue(ref timestamp);
+
+ // Priority (serialize as int)
+ int priorityInt = (int)priority;
+ serializer.SerializeValue(ref priorityInt);
+ priority = (RPCPriority)priorityInt;
+
+ // Args context
+ serializer.SerializeValue(ref selfNetworkId);
+ serializer.SerializeValue(ref targetNetworkId);
+
+ // String data
+ serializer.SerializeValue(ref stringData1);
+ serializer.SerializeValue(ref stringData2);
+
+ // Float data
+ serializer.SerializeValue(ref floatData1);
+ serializer.SerializeValue(ref floatData2);
+ serializer.SerializeValue(ref floatData3);
+ serializer.SerializeValue(ref floatData4);
+
+ // Int data
+ serializer.SerializeValue(ref intData1);
+ serializer.SerializeValue(ref intData2);
+ serializer.SerializeValue(ref intData3);
+ serializer.SerializeValue(ref boolFlags);
+
+ // Vector/Rotation data
+ serializer.SerializeValue(ref vectorData1);
+ serializer.SerializeValue(ref vectorData2);
+ serializer.SerializeValue(ref rotationData);
+
+ // Network object references
+ serializer.SerializeValue(ref networkObjectId1);
+ serializer.SerializeValue(ref networkObjectId2);
+ }
+
+ // ============================================================================================
+ // FACTORY METHODS
+ // ============================================================================================
+
+ ///
+ /// Create a new payload for an instruction type
+ ///
+ public static VisualScriptingPayload Create(int instructionTypeId, RPCPriority priority = RPCPriority.Normal)
+ {
+ return new VisualScriptingPayload
+ {
+ instructionTypeId = instructionTypeId,
+ priority = priority,
+ timestamp = Time.time,
+ rotationData = Quaternion.identity
+ };
+ }
+
+ ///
+ /// Create a payload with Args context
+ ///
+ public static VisualScriptingPayload Create(int instructionTypeId, GameObject self, GameObject target, RPCPriority priority = RPCPriority.Normal)
+ {
+ var payload = Create(instructionTypeId, priority);
+
+ if (self != null)
+ {
+ var selfNetObj = self.GetComponent();
+ if (selfNetObj != null && selfNetObj.IsSpawned)
+ {
+ payload.selfNetworkId = selfNetObj.NetworkObjectId;
+ }
+ }
+
+ if (target != null)
+ {
+ var targetNetObj = target.GetComponent();
+ if (targetNetObj != null && targetNetObj.IsSpawned)
+ {
+ payload.targetNetworkId = targetNetObj.NetworkObjectId;
+ }
+ }
+
+ return payload;
+ }
+
+ ///
+ /// Create a payload from Args
+ ///
+ public static VisualScriptingPayload CreateFromArgs(int instructionTypeId, GameCreator.Runtime.Common.Args args, RPCPriority priority = RPCPriority.Normal)
+ {
+ return Create(instructionTypeId, args?.Self, args?.Target, priority);
+ }
+
+ // ============================================================================================
+ // DEBUG
+ // ============================================================================================
+
+ public override string ToString()
+ {
+ return $"VSPayload[RPC:{rpcId}, Type:{instructionTypeId}, From:{senderId}, Self:{selfNetworkId}, Target:{targetNetworkId}]";
+ }
+ }
+}
diff --git a/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingRPCManager.cs b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingRPCManager.cs
new file mode 100644
index 000000000..ad2d81426
--- /dev/null
+++ b/Assets/Plugins/GameCreator/Packages/GameCreator_Multiplayer/Runtime/VisualScripting/RPC/Core/VisualScriptingRPCManager.cs
@@ -0,0 +1,625 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using UnityEngine;
+using Unity.Netcode;
+using GameCreator.Runtime.Common;
+using GameCreator.Runtime.VisualScripting;
+
+namespace GameCreator.Multiplayer.Runtime.VisualScripting.RPC
+{
+ ///
+ /// Central manager for Visual Scripting RPC integration.
+ /// Bridges GameCreator Instructions/Conditions with Unity Netcode RPCs.
+ ///
+ /// ARCHITECTURE:
+ /// - Type-safe RPC registration (no string-based names)
+ /// - Auto-discovery of network-capable Instructions
+ /// - Unified execution context with Args support
+ /// - Response/callback handling for visual scripting
+ /// - Automatic integration with NetworkRPCBatcher
+ ///
+ /// USAGE:
+ /// 1. Instructions inherit from RPCInstructionBase
+ /// 2. Call ExecuteRPC() with target and payload
+ /// 3. Handle responses via RPCResponseHandler or callbacks
+ ///
+ public class VisualScriptingRPCManager : NetworkBehaviour
+ {
+ // ============================================================================================
+ // SINGLETON
+ // ============================================================================================
+
+ private static VisualScriptingRPCManager s_Instance;
+ public static VisualScriptingRPCManager Instance
+ {
+ get
+ {
+ if (s_Instance == null)
+ {
+ s_Instance = FindAnyObjectByType();
+ if (s_Instance == null)
+ {
+ Debug.LogWarning("[VS-RPC] VisualScriptingRPCManager not found. Creating one.");
+ var go = new GameObject("VisualScriptingRPCManager");
+ s_Instance = go.AddComponent();
+ go.AddComponent();
+ }
+ }
+ return s_Instance;
+ }
+ }
+
+ // ============================================================================================
+ // CONFIGURATION
+ // ============================================================================================
+
+ [Header("Settings")]
+ [Tooltip("Enable automatic batching for high-frequency RPCs")]
+ [SerializeField] private bool m_EnableBatching = true;
+
+ [Tooltip("Default timeout for RPC responses (seconds)")]
+ [SerializeField] private float m_DefaultTimeout = 5f;
+
+ [Tooltip("Maximum concurrent pending RPCs")]
+ [SerializeField] private int m_MaxPendingRPCs = 100;
+
+ [Header("Debug")]
+ [SerializeField] private bool m_DebugMode = false;
+
+ // ============================================================================================
+ // RUNTIME STATE
+ // ============================================================================================
+
+ // Registered RPC handlers by instruction type
+ private Dictionary m_Handlers = new Dictionary();
+
+ // Pending RPC responses waiting for callbacks
+ private Dictionary m_PendingResponses = new Dictionary();
+
+ // RPC ID counter for unique identification
+ private uint m_NextRPCId = 1;
+
+ // Instruction type to ID mapping (for serialization)
+ private Dictionary m_TypeToId = new Dictionary();
+ private Dictionary m_IdToType = new Dictionary();
+ private int m_NextTypeId = 1;
+
+ // Statistics
+ private RPCStatistics m_Stats = new RPCStatistics();
+
+ // ============================================================================================
+ // STRUCTS & ENUMS
+ // ============================================================================================
+
+ public enum RPCExecutionResult
+ {
+ Success,
+ Failed,
+ Timeout,
+ NoAuthority,
+ InvalidTarget,
+ NotConnected
+ }
+
+ public struct PendingRPCResponse
+ {
+ public uint rpcId;
+ public float expirationTime;
+ public Action callback;
+ public int instructionTypeId;
+ }
+
+ public struct RPCStatistics
+ {
+ public int totalSent;
+ public int totalReceived;
+ public int totalSucceeded;
+ public int totalFailed;
+ public int totalTimedOut;
+ public float averageRoundTripMs;
+ }
+
+ public delegate Task RPCInstructionHandler(VisualScriptingPayload payload, Args args);
+
+ // ============================================================================================
+ // PROPERTIES
+ // ============================================================================================
+
+ public bool IsReady => IsSpawned && NetworkManager.Singleton != null && NetworkManager.Singleton.IsConnectedClient;
+ public float DefaultTimeout => m_DefaultTimeout;
+ public RPCStatistics Statistics => m_Stats;
+
+ // ============================================================================================
+ // UNITY LIFECYCLE
+ // ============================================================================================
+
+ private void Awake()
+ {
+ if (s_Instance != null && s_Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ s_Instance = this;
+ DontDestroyOnLoad(gameObject);
+ }
+
+ private void Update()
+ {
+ if (!IsSpawned) return;
+
+ // Check for timed out RPCs
+ CleanupTimedOutRPCs();
+ }
+
+ private void OnDestroy()
+ {
+ if (s_Instance == this)
+ {
+ s_Instance = null;
+ }
+ }
+
+ // ============================================================================================
+ // NETWORK LIFECYCLE
+ // ============================================================================================
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+ LogDebug("VisualScriptingRPCManager spawned");
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ // Cancel all pending RPCs
+ foreach (var pending in m_PendingResponses.Values)
+ {
+ pending.callback?.Invoke(RPCExecutionResult.NotConnected, default);
+ }
+ m_PendingResponses.Clear();
+
+ base.OnNetworkDespawn();
+ }
+
+ // ============================================================================================
+ // TYPE REGISTRATION
+ // ============================================================================================
+
+ ///
+ /// Register an instruction type for RPC execution.
+ /// Called automatically by RPCInstructionBase on first use.
+ ///
+ public int RegisterInstructionType() where T : Instruction
+ {
+ return RegisterInstructionType(typeof(T));
+ }
+
+ public int RegisterInstructionType(Type instructionType)
+ {
+ if (m_TypeToId.TryGetValue(instructionType, out int existingId))
+ {
+ return existingId;
+ }
+
+ int typeId = m_NextTypeId++;
+ m_TypeToId[instructionType] = typeId;
+ m_IdToType[typeId] = instructionType;
+
+ LogDebug($"Registered instruction type: {instructionType.Name} -> ID {typeId}");
+ return typeId;
+ }
+
+ ///
+ /// Get the type ID for an instruction type
+ ///
+ public int GetTypeId() where T : Instruction
+ {
+ return GetTypeId(typeof(T));
+ }
+
+ public int GetTypeId(Type instructionType)
+ {
+ if (m_TypeToId.TryGetValue(instructionType, out int typeId))
+ {
+ return typeId;
+ }
+ return RegisterInstructionType(instructionType);
+ }
+
+ ///
+ /// Get the instruction type from a type ID
+ ///
+ public Type GetInstructionType(int typeId)
+ {
+ m_IdToType.TryGetValue(typeId, out Type type);
+ return type;
+ }
+
+ // ============================================================================================
+ // HANDLER REGISTRATION
+ // ============================================================================================
+
+ ///
+ /// Register a handler for processing received RPCs of a specific instruction type
+ ///
+ public void RegisterHandler(int instructionTypeId, RPCInstructionHandler handler)
+ {
+ m_Handlers[instructionTypeId] = handler;
+ LogDebug($"Registered handler for instruction type ID {instructionTypeId}");
+ }
+
+ public void UnregisterHandler(int instructionTypeId)
+ {
+ m_Handlers.Remove(instructionTypeId);
+ }
+
+ // ============================================================================================
+ // RPC EXECUTION - PUBLIC API
+ // ============================================================================================
+
+ ///
+ /// Execute an RPC with the given payload to a specific target
+ ///
+ public async Task ExecuteRPCAsync(
+ VisualScriptingPayload payload,
+ RPCTargetType targetType,
+ ulong targetClientId = 0,
+ Action responseCallback = null)
+ {
+ if (!IsReady)
+ {
+ LogDebug("RPC failed: Not connected");
+ return RPCExecutionResult.NotConnected;
+ }
+
+ // Assign unique RPC ID
+ payload.rpcId = m_NextRPCId++;
+ payload.senderId = NetworkManager.Singleton.LocalClientId;
+ payload.timestamp = Time.time;
+
+ // Register pending response if callback provided
+ if (responseCallback != null)
+ {
+ RegisterPendingResponse(payload.rpcId, payload.instructionTypeId, responseCallback);
+ }
+
+ // Route to appropriate RPC method
+ try
+ {
+ switch (targetType)
+ {
+ case RPCTargetType.Server:
+ ExecuteOnServerRpc(payload);
+ break;
+
+ case RPCTargetType.AllClients:
+ if (IsServer)
+ {
+ BroadcastToClientsRpc(payload);
+ }
+ else
+ {
+ RequestBroadcastServerRpc(payload);
+ }
+ break;
+
+ case RPCTargetType.SpecificClient:
+ if (IsServer)
+ {
+ SendToClientRpc(payload, RpcTarget.Single(targetClientId, RpcTargetUse.Temp));
+ }
+ else
+ {
+ RequestSendToClientServerRpc(payload, targetClientId);
+ }
+ break;
+
+ case RPCTargetType.OtherClients:
+ if (IsServer)
+ {
+ BroadcastToOthersRpc(payload);
+ }
+ else
+ {
+ RequestBroadcastToOthersServerRpc(payload);
+ }
+ break;
+
+ case RPCTargetType.Owner:
+ SendToOwnerRpc(payload);
+ break;
+
+ default:
+ return RPCExecutionResult.InvalidTarget;
+ }
+
+ m_Stats.totalSent++;
+ LogDebug($"RPC sent: Type={payload.instructionTypeId}, Target={targetType}, ID={payload.rpcId}");
+ return RPCExecutionResult.Success;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"[VS-RPC] RPC execution failed: {e.Message}");
+ m_Stats.totalFailed++;
+ return RPCExecutionResult.Failed;
+ }
+ }
+
+ ///
+ /// Fire-and-forget RPC execution
+ ///
+ public void ExecuteRPC(
+ VisualScriptingPayload payload,
+ RPCTargetType targetType,
+ ulong targetClientId = 0)
+ {
+ _ = ExecuteRPCAsync(payload, targetType, targetClientId);
+ }
+
+ // ============================================================================================
+ // SERVER RPCs
+ // ============================================================================================
+
+ [Rpc(SendTo.Server)]
+ private void ExecuteOnServerRpc(VisualScriptingPayload payload, RpcParams rpcParams = default)
+ {
+ LogDebug($"Server received RPC: Type={payload.instructionTypeId}, From={rpcParams.Receive.SenderClientId}");
+ ProcessPayload(payload);
+ }
+
+ [Rpc(SendTo.Server)]
+ private void RequestBroadcastServerRpc(VisualScriptingPayload payload, RpcParams rpcParams = default)
+ {
+ LogDebug($"Server broadcasting RPC from client {rpcParams.Receive.SenderClientId}");
+ BroadcastToClientsRpc(payload);
+ }
+
+ [Rpc(SendTo.Server)]
+ private void RequestBroadcastToOthersServerRpc(VisualScriptingPayload payload, RpcParams rpcParams = default)
+ {
+ payload.excludeClientId = rpcParams.Receive.SenderClientId;
+ BroadcastToOthersRpc(payload);
+ }
+
+ [Rpc(SendTo.Server)]
+ private void RequestSendToClientServerRpc(VisualScriptingPayload payload, ulong targetClientId, RpcParams rpcParams = default)
+ {
+ SendToClientRpc(payload, RpcTarget.Single(targetClientId, RpcTargetUse.Temp));
+ }
+
+ [Rpc(SendTo.Server)]
+ private void SendResponseServerRpc(uint rpcId, RPCExecutionResult result, VisualScriptingPayload responsePayload, RpcParams rpcParams = default)
+ {
+ // Route response to original sender
+ ulong originalSenderId = responsePayload.senderId;
+ ReceiveResponseClientRpc(rpcId, result, responsePayload, RpcTarget.Single(originalSenderId, RpcTargetUse.Temp));
+ }
+
+ // ============================================================================================
+ // CLIENT RPCs
+ // ============================================================================================
+
+ [Rpc(SendTo.Everyone)]
+ private void BroadcastToClientsRpc(VisualScriptingPayload payload)
+ {
+ LogDebug($"Client received broadcast RPC: Type={payload.instructionTypeId}");
+ m_Stats.totalReceived++;
+ ProcessPayload(payload);
+ }
+
+ [Rpc(SendTo.NotMe)]
+ private void BroadcastToOthersRpc(VisualScriptingPayload payload)
+ {
+ if (payload.excludeClientId == NetworkManager.Singleton.LocalClientId) return;
+
+ LogDebug($"Client received broadcast (others) RPC: Type={payload.instructionTypeId}");
+ m_Stats.totalReceived++;
+ ProcessPayload(payload);
+ }
+
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void SendToClientRpc(VisualScriptingPayload payload, RpcParams rpcParams = default)
+ {
+ LogDebug($"Client received targeted RPC: Type={payload.instructionTypeId}");
+ m_Stats.totalReceived++;
+ ProcessPayload(payload);
+ }
+
+ [Rpc(SendTo.Owner)]
+ private void SendToOwnerRpc(VisualScriptingPayload payload)
+ {
+ LogDebug($"Owner received RPC: Type={payload.instructionTypeId}");
+ m_Stats.totalReceived++;
+ ProcessPayload(payload);
+ }
+
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void ReceiveResponseClientRpc(uint rpcId, RPCExecutionResult result, VisualScriptingPayload responsePayload, RpcParams rpcParams = default)
+ {
+ if (m_PendingResponses.TryGetValue(rpcId, out var pending))
+ {
+ m_PendingResponses.Remove(rpcId);
+ pending.callback?.Invoke(result, responsePayload);
+
+ // Update stats
+ float roundTripMs = (Time.time - responsePayload.timestamp) * 1000f;
+ UpdateRoundTripStats(roundTripMs);
+
+ if (result == RPCExecutionResult.Success)
+ m_Stats.totalSucceeded++;
+ else
+ m_Stats.totalFailed++;
+ }
+ }
+
+ // ============================================================================================
+ // PAYLOAD PROCESSING
+ // ============================================================================================
+
+ private async void ProcessPayload(VisualScriptingPayload payload)
+ {
+ if (!m_Handlers.TryGetValue(payload.instructionTypeId, out var handler))
+ {
+ LogDebug($"No handler registered for instruction type {payload.instructionTypeId}");
+ return;
+ }
+
+ try
+ {
+ // Reconstruct Args from payload
+ Args args = ReconstructArgs(payload);
+
+ // Execute the handler
+ await handler(payload, args);
+
+ m_Stats.totalSucceeded++;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"[VS-RPC] Error processing payload: {e.Message}");
+ m_Stats.totalFailed++;
+ }
+ }
+
+ private Args ReconstructArgs(VisualScriptingPayload payload)
+ {
+ // Find GameObjects by network ID
+ GameObject self = null;
+ GameObject target = null;
+
+ if (payload.selfNetworkId != 0)
+ {
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(payload.selfNetworkId, out var selfNetObj))
+ {
+ self = selfNetObj.gameObject;
+ }
+ }
+
+ if (payload.targetNetworkId != 0)
+ {
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(payload.targetNetworkId, out var targetNetObj))
+ {
+ target = targetNetObj.gameObject;
+ }
+ }
+
+ return new Args(self, target);
+ }
+
+ // ============================================================================================
+ // PENDING RESPONSE MANAGEMENT
+ // ============================================================================================
+
+ private void RegisterPendingResponse(uint rpcId, int instructionTypeId, Action callback)
+ {
+ if (m_PendingResponses.Count >= m_MaxPendingRPCs)
+ {
+ Debug.LogWarning("[VS-RPC] Max pending RPCs reached. Cleaning up oldest.");
+ CleanupOldestPending();
+ }
+
+ m_PendingResponses[rpcId] = new PendingRPCResponse
+ {
+ rpcId = rpcId,
+ expirationTime = Time.time + m_DefaultTimeout,
+ callback = callback,
+ instructionTypeId = instructionTypeId
+ };
+ }
+
+ private void CleanupTimedOutRPCs()
+ {
+ var timedOut = new List