Successfully implemented a complete remote control system for Unity Editor based on the design specification. The system consists of three components that work together to provide CLI-based control of a running Unity Editor.
Location: UnityCtl.Protocol/
Target: netstandard2.1 (Unity compatible)
Files:
Messages.cs- Protocol message types (Hello, Request, Response, Event)Config.cs- Bridge configuration modelDTOs.cs- Data transfer objects for all commandsConstants.cs- Command and event name constantsJsonHelper.cs- Shared JSON serializationProjectLocator.cs- Project detection and config managementIsExternalInit.cs- Polyfills for C# features in netstandard2.1
Key Features:
- JSON-based message protocol
- Project isolation via projectId (hash of project path)
- Shared types used by all components
- Unity-compatible (netstandard2.1)
Location: UnityCtl.Bridge/
Target: net10.0
Tool Name: unityctl-bridge
Files:
Program.cs- CLI entry point with System.CommandLineBridgeState.cs- State management (connections, logs, requests)BridgeEndpoints.cs- HTTP and WebSocket endpoints
Endpoints:
GET /health- Health check with Unity connection statusGET /logs/tail?lines=N&source=S- Recent logs (source: editor, console, all)POST /logs/clear- Clear log watermarkGET /logs/stream- SSE log streamingPOST /rpc- RPC commands to UnityGET /unity(WebSocket) - Unity Editor connection endpoint
Key Features:
- Automatic port assignment
- Project isolation via
.unityctl/bridge.json - Log ring buffer (1000 entries)
- Request/response matching with timeouts
- Domain reload resilience
Location: UnityCtl.Cli/
Target: net10.0
Tool Name: unityctl
Files:
Program.cs- Main CLI with global optionsBridgeClient.cs- HTTP client for bridge communicationLogsCommand.cs- Unified log viewing commandsSceneCommands.cs- Scene management commandsPlayCommands.cs- Play mode commandsAssetCommands.cs- Asset management commandsBridgeCommands.cs- Bridge management commandsEditorCommands.cs- Editor run/stop commandsBinders.cs- Global option binders
Commands Implemented:
unityctl bridge status # Check bridge status
unityctl bridge start # Start bridge daemon
unityctl logs -n 50 # View logs (editor/console)
unityctl logs -f # Stream logs
unityctl logs clear # Clear log history
unityctl scene list # List scenes
unityctl scene load <path> # Load a scene
unityctl play enter/exit # Control play mode
unityctl play status # Get play mode status
unityctl asset import <path> # Import an asset
unityctl asset refresh # Refresh assets (triggers compilation if needed)Global Options:
--project <path>- Specify project path--agent-id <id>- Set agent ID--json- JSON output mode
Location: UnityCtl.UnityPackage/
Package Name: com.dirtybit.unityctl
Unity Version: 6.0+
Files:
package.json- UPM package manifestEditor/UnityCtl.asmdef- Assembly definitionEditor/UnityCtlBootstrap.cs- Initialization and event handlersEditor/UnityCtlClient.cs- WebSocket client and command handlersPlugins/UnityCtl.Protocol.dll- Protocol library
Commands Handled:
scene.list- List build settings or all scenesscene.load- Load scene (single/additive)play.enter/exit/status- Play mode controlasset.import- Asset importasset.refresh- Refresh assets (triggers compilation)
Events Sent:
log- Console log entries (all levels)playModeChanged- Play mode state changescompilation.started- Compilation startcompilation.finished- Compilation end with success status
Key Features:
- Auto-connect on Editor startup
- Auto-reconnect after domain reload
- Main thread action queue for Unity API calls
- Non-intrusive (no effect if bridge not running)
- WebSocket-based real-time communication
unityctl/
├── .gitignore # Git ignore rules
├── README.md # User documentation
├── IMPLEMENTATION.md # This file
├── bootstrap.md # Original design spec
├── unityctl.sln # Solution file
├── UnityCtl.Protocol/ # Shared protocol library
│ ├── UnityCtl.Protocol.csproj
│ ├── Messages.cs
│ ├── Config.cs
│ ├── DTOs.cs
│ ├── Constants.cs
│ ├── JsonHelper.cs
│ ├── ProjectLocator.cs
│ └── IsExternalInit.cs
├── UnityCtl.Bridge/ # Bridge daemon
│ ├── UnityCtl.Bridge.csproj
│ ├── Program.cs
│ ├── BridgeState.cs
│ └── BridgeEndpoints.cs
├── UnityCtl.Cli/ # CLI tool
│ ├── UnityCtl.Cli.csproj
│ ├── Program.cs
│ ├── BridgeClient.cs
│ ├── Binders.cs
│ ├── LogsCommand.cs
│ ├── SceneCommands.cs
│ ├── PlayCommands.cs
│ ├── AssetCommands.cs
│ ├── EditorCommands.cs
│ └── BridgeCommands.cs
├── UnityCtl.UnityPackage/ # Unity UPM package
│ ├── package.json
│ ├── Editor/
│ │ ├── UnityCtl.asmdef
│ │ ├── UnityCtlBootstrap.cs
│ │ └── UnityCtlClient.cs
│ └── Plugins/
│ └── UnityCtl.Protocol.dll
└── unity-project/ # Test Unity project
├── Packages/manifest.json # (includes UnityCtl package)
└── ...
- Type-based message routing using JSON discriminators
- Request/Response pattern with unique request IDs
- Event streaming for real-time updates
- Status codes (ok/error) with structured error payloads
Each Unity project gets its own bridge instance:
{
"projectId": "proj-a1b2c3d4",
"port": 49521,
"pid": 12345
}Stored in .unityctl/bridge.json at project root.
Unity's domain reload (triggered by compilation) destroys all Editor objects:
- Bridge maintains connection and state
- Unity plugin uses
[InitializeOnLoad]and[DidReloadScripts] - Plugin reconnects after each domain reload
- No commands are lost during reconnection
Bridge:
- Main thread: HTTP/WebSocket server
- Background threads: WebSocket receive loops
- Thread-safe: ConcurrentDictionary for pending requests
Unity Plugin:
- Background thread: WebSocket receive loop
- Main thread: Command execution via action queue
- Pumped every EditorApplication.update
# Build all .NET projects
dotnet build unityctl.sln
# Build Protocol DLL for Unity
dotnet build UnityCtl.Protocol/UnityCtl.Protocol.csproj -c Release
cp UnityCtl.Protocol/bin/Release/netstandard2.1/UnityCtl.Protocol.dll UnityCtl.UnityPackage/Plugins/# Pack tools
dotnet pack UnityCtl.Cli/UnityCtl.Cli.csproj -o ./artifacts
dotnet pack UnityCtl.Bridge/UnityCtl.Bridge.csproj -o ./artifacts
# Install globally
dotnet tool install -g UnityCtl.Cli --add-source ./artifacts
dotnet tool install -g UnityCtl.Bridge --add-source ./artifacts-
Start the bridge:
cd unity-project unityctl bridge start --project .
-
Open Unity Editor with the project
-
Use CLI:
unityctl --project ./unity-project bridge status unityctl --project ./unity-project play enter unityctl --project ./unity-project scene list unityctl --project ./unity-project logs -n 20
The implementation includes a test Unity project at unity-project/ with the UnityCtl package already configured.
To test:
- Start bridge:
dotnet run --project UnityCtl.Bridge/UnityCtl.Bridge.csproj -- --project ./unity-project - Open
unity-project/in Unity Editor 6.0 - Run CLI commands:
dotnet run --project UnityCtl.Cli/UnityCtl.Cli.csproj -- --project ./unity-project bridge status
- Added polyfills for
IsExternalInit,RequiredMemberAttribute, etc. - Used older SHA256 API (Create() instead of HashData())
- Fixed binders to traverse command tree for global options
- Handles nested commands properly
- Added
using UnityEditor.Callbacksfor[DidReloadScripts] - Used CompilationPipeline for script compilation
- Proper main thread marshalling for Unity APIs
-
Why WebSocket for Unity?
- Real-time bidirectional communication
- Event streaming (logs, play mode, compilation)
- Built-in in .NET and Unity
-
Why HTTP for CLI?
- Simple request/response pattern
- No connection management needed
- Easy to debug (curl-friendly)
-
Why separate Bridge process?
- Survives Unity domain reloads
- Independent of Unity lifecycle
- Can buffer logs and state
- Multiple CLIs can connect
-
Why netstandard2.1 for Protocol?
- Compatible with Unity 6.0+
- Shared between .NET 10.0 and Unity
- Single source of truth for protocol
✅ All goals from bootstrap.md achieved:
- Control running Unity 6.0 editor from CLI
- Read console and editor logs
- Trigger asset import and script compilation
- List and load scenes
- Enter / exit play mode
- Stable connection across domain reloads
- Multi-agent support
- Project isolation
- Non-intrusive to teammates
✅ All wire protocol commands implemented:
- asset.import, asset.reimportAll (not exposed in CLI yet), asset.refresh
- scene.list, scene.load
- play.enter, play.exit, play.status
✅ All events implemented:
- log
- playModeChanged
- compilation.started, compilation.finished
- Add
asset.reimportAllCLI command - Add scene unload command
- Add prefab instantiation
- Add GameObject hierarchy inspection
- Add component property get/set
- Add custom command extensibility
- Add authentication/authorization
- Package for NuGet and Unity Asset Store