Skip to content

Latest commit

 

History

History
417 lines (316 loc) · 10.5 KB

File metadata and controls

417 lines (316 loc) · 10.5 KB

🛠️ Development Guide

A comprehensive guide for developing the OpenClaw Godot Plugin.

Table of Contents

  1. Project Structure
  2. Development Environment Setup
  3. Architecture Overview
  4. Adding New Tools
  5. Debugging
  6. Code Style

Project Structure

openclaw-godot-plugin/
├── addons/
│   └── openclaw/
│       ├── plugin.cfg          # Plugin metadata
│       ├── openclaw_plugin.gd  # Main EditorPlugin
│       ├── connection_manager.gd # HTTP connection management
│       └── tools.gd            # Tool execution logic
├── OpenClawPlugin~/            # Gateway extension (TypeScript)
│   ├── index.ts                # Extension entry point
│   ├── package.json
│   └── tsconfig.json
├── Documentation~/             # Documentation (excluded from Godot)
│   ├── DEVELOPMENT.md
│   ├── TESTING.md
│   └── CONTRIBUTING.md
├── README.md
└── LICENSE

Core Files

File Purpose
openclaw_plugin.gd EditorPlugin entry point, UI creation, signal connections
connection_manager.gd Gateway HTTP communication (register, poll, heartbeat)
tools.gd 40 tool execution implementations
OpenClawPlugin~/index.ts Gateway extension, provides godot_execute tool

Development Environment Setup

Requirements

  • Godot 4.x (4.2+ recommended)
  • Node.js 18+ (for building gateway extension)
  • OpenClaw 2026.2.3+

1. Clone the Repository

git clone https://github.com/TomLeeLive/openclaw-godot-plugin.git
cd openclaw-godot-plugin

2. Set Up Development Godot Project

# Create test project
mkdir -p ~/godot-dev-project
cp -r addons ~/godot-dev-project/

Open in Godot:

  1. Project → Project Settings → Plugins
  2. Enable OpenClaw

3. Install Gateway Extension

# Copy extension files
cp -r OpenClawPlugin~/* ~/.openclaw/extensions/godot/

# Restart gateway
openclaw gateway restart

4. Verify Development Mode

Check Godot Output panel:

[OpenClaw] Plugin loading...
[OpenClaw] Plugin loaded!
[OpenClaw] Registering with gateway...
[OpenClaw] Registered! Session: godot_xxxxx

Architecture Overview

Communication Flow

┌──────────────────┐     HTTP      ┌──────────────────┐
│   Godot Editor   │◄────────────►│  OpenClaw Gateway │
│                  │              │   (port 18789)    │
│  ┌────────────┐  │              │                   │
│  │ Connection │  │  /register   │  ┌─────────────┐  │
│  │  Manager   │──┼──────────────┼─►│   Sessions  │  │
│  └────────────┘  │              │  └─────────────┘  │
│        │         │  /poll       │        │          │
│        ▼         │◄─────────────┼────────┘          │
│  ┌────────────┐  │              │                   │
│  │   Tools    │  │  /result     │  ┌─────────────┐  │
│  └────────────┘──┼──────────────┼─►│   Claude    │  │
│                  │              │  └─────────────┘  │
└──────────────────┘              └───────────────────┘

1. Registration

Plugin registers with Gateway on load:

# connection_manager.gd
func _register():
    var body = {
        "project": ProjectSettings.get_setting("application/config/name"),
        "version": Engine.get_version_info().string,
        "platform": "GodotEditor",
        "tools": _get_tool_list()
    }
    _http_post("/godot/register", body, _on_register_complete)

2. Polling

Long polling for commands (30s timeout):

func _poll():
    if is_polling:
        return  # Prevent duplicate requests
    is_polling = true
    _http_get("/godot/poll?sessionId=" + session_id, _on_poll_complete)

3. Command Execution & Result Transmission

func _on_command_received(tool_call_id: String, tool_name: String, args: Dictionary):
    var result = tools.execute(tool_name, args)
    connection_manager.send_result(tool_call_id, result)

Play Mode Persistence

PROCESS_MODE_ALWAYS setting maintains connection during Play mode:

func _ready():
    process_mode = Node.PROCESS_MODE_ALWAYS  # Critical!

Adding New Tools

Example: Adding audio.play Tool

Let's add a tool that plays audio files.

Step 1: Register Tool in tools.gd

# Add to TOOLS array at top of tools.gd

var TOOLS = [
    # ... existing tools ...
    
    # New tool
    {
        "name": "audio.play",
        "description": "Play an audio file in the editor",
        "inputSchema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Resource path to audio file (e.g., res://sounds/bgm.ogg)"
                },
                "volume": {
                    "type": "number",
                    "description": "Volume in dB (default: 0)"
                }
            },
            "required": ["path"]
        }
    }
]

Step 2: Add Handler to execute()

func execute(tool_name: String, args: Dictionary) -> Dictionary:
    match tool_name:
        # ... existing cases ...
        
        "audio.play":
            return _audio_play(args)
        
        _:
            return {"success": false, "error": "Unknown tool: " + tool_name}

Step 3: Implement Handler Function

func _audio_play(args: Dictionary) -> Dictionary:
    var path = args.get("path", "")
    var volume = args.get("volume", 0.0)
    
    # Check resource exists
    if not ResourceLoader.exists(path):
        return {"success": false, "error": "Audio file not found: " + path}
    
    # Load audio stream
    var stream = load(path) as AudioStream
    if stream == null:
        return {"success": false, "error": "Invalid audio file: " + path}
    
    # Create and play AudioStreamPlayer
    var player = AudioStreamPlayer.new()
    player.stream = stream
    player.volume_db = volume
    
    # Add to editor root
    editor_interface.get_base_control().add_child(player)
    player.play()
    
    # Auto-remove when finished
    player.finished.connect(func(): player.queue_free())
    
    return {
        "success": true,
        "path": path,
        "duration": stream.get_length()
    }

Step 4: Update Gateway Extension (Optional)

Tool list is auto-sent from Godot, so no changes needed in OpenClawPlugin~/index.ts. To improve tool description:

// index.ts - add to tools array (optional)
{
  name: "audio.play",
  description: "Play audio file in Godot Editor",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string", description: "Audio resource path" },
      volume: { type: "number", description: "Volume in dB" }
    },
    required: ["path"]
  }
}

Step 5: Test

# Restart gateway
openclaw gateway restart

Test in OpenClaw:

You: Play the background music

OpenClaw:
[Executes audio.play {path: "res://audio/bgm.ogg", volume: -5}]

Playing bgm.ogg (duration: 180.5s)

Debugging

1. Godot Output Panel

Check plugin logs:

[OpenClaw] Plugin loading...
[OpenClaw] Command: scene.getCurrent
[OpenClaw] Error: Node not found

2. Using print_debug()

func _some_function():
    print_debug("Debug: ", some_variable)  # Includes filename/line

3. Gateway Log

# Watch gateway logs in real-time
tail -f ~/.openclaw/logs/gateway.log

4. HTTP Request Debugging

# Add to connection_manager.gd
func _http_post(endpoint: String, body: Dictionary, callback: Callable):
    print("[OpenClaw] POST %s: %s" % [endpoint, JSON.stringify(body)])
    # ... existing code ...

5. Common Issues

Issue Cause Solution
Plugin won't load GDScript syntax error Check Output panel, fix syntax
Connection failed Gateway not running openclaw gateway start
Tool not executing Tool name mismatch Check TOOLS array and match statement
Play mode disconnect PROCESS_MODE not set process_mode = PROCESS_MODE_ALWAYS
HTTP request duplicates Flag not checked Use is_polling flag

Code Style

GDScript Conventions

# Class declaration
class_name MyClass
extends Node

## Doc comment (shown with Ctrl+Shift+D)
## @param value: The value to set
## @return: Success status
func my_function(value: String) -> bool:
    # Constants are UPPER_SNAKE_CASE
    const MAX_RETRIES = 3
    
    # Variables are snake_case
    var retry_count = 0
    
    # Use explicit types
    var result: Dictionary = {}
    
    # Early return pattern
    if value.is_empty():
        return false
    
    return true

# Private functions use _ prefix
func _internal_helper():
    pass

# Signals are past tense
signal connection_changed(connected: bool)
signal command_received(tool_call_id: String, tool_name: String)

Error Handling Pattern

func _safe_operation() -> Dictionary:
    # Always include success field
    if some_error_condition:
        return {"success": false, "error": "Error message"}
    
    return {
        "success": true,
        "data": some_data
    }

Async Handling

# Wait for HTTPRequest completion
func _make_request():
    var http = HTTPRequest.new()
    add_child(http)
    
    http.request_completed.connect(_on_request_completed)
    http.request("http://localhost:18789/endpoint")

func _on_request_completed(result: int, code: int, headers: PackedStringArray, body: PackedByteArray):
    if result != HTTPRequest.RESULT_SUCCESS:
        push_error("Request failed")
        return
    
    var json = JSON.parse_string(body.get_string_from_utf8())
    # Process...

Next Steps


Documentation Updated: 2026-02-08