ESC

Type to search...

Godot + EVOID Game Integration

Build real multiplayer games with Godot (client) and EVOID (server). One plugin, every scenario — desktop, web, embed, UDP.

Architecture

graph TB
    subgraph Godot["Godot Client (GDScript)"]
        App[EvoidApp<br/>State Machine]
        Client[EvoidClient<br/>WebSocket]
        UDP[EvoidUDP<br/>Low-Latency]
        Bus[EvoidBus<br/>Pub/Sub]
        Loader[EvoidWebLoader<br/>WebGL + Embed]
        Config[EvoidConfig<br/>Settings]
    end

    subgraph Server["EVOID Server (Python)"]
        GH[GameHost<br/>Game Hosting]
        Handler[game_intent_handler<br/>Intent Processing]
        Subs[setup_game_hosting<br/>Default Handlers]
        Topics[Topics<br/>Event Constants]
    end

    App --> Client
    App --> UDP
    App --> Bus
    App --> Loader
    App --> Config
    Client <-->|WSS| GH
    UDP <-->|UDP| Server
    Handler --> Subs

The Two Plugins

PluginRuns WhereWhat It Does
evoid_godot (GDScript)Godot clientWebSocket/UDP connection, state machine, event bus, web loading, embed API
evoid-godot (Python)EVOID serverIntent handling, message bus, game hosting, embed mode

Components

ComponentPurpose
EvoidAppState machine + orchestration layer (BOOT → IDLE → CONNECTING → ONLINE → RECOVERING → ERROR)
EvoidClientWebSocket connection (JSON + binary frames)
EvoidUDPLow-latency UDP transport (18-byte binary header, 3 channels)
EvoidBusPub/sub event bus (zero-coupling, O(1) lookup)
EvoidConfigConfiguration resource (Inspector-friendly, UDP, embed, export fields)
EvoidTopicsTopic constants (mirrors Python server)
EvoidWebLoaderWebGL auto-detection + manifest-aware prefetch + embed postMessage API
EvoidExportPluginExport-time optimizations (SW injection, manifest generation)

Setup Scenarios

Scenario 1: Desktop Game

func _ready():
    var config = EvoidConfig.new()
    config.server_url = "wss://your-server.com"
    config.game_id = "my-game"
    EvoidApp.config = config
    EvoidApp.connect_to_server()

Prerequisites: Godot 4.4+, EVOID server (Python 3.12+)

Scenario 2: Web Game (Standalone)

func _ready():
    var config = EvoidConfig.new()
    config.game_id = "my-game"
    EvoidApp.config = config
    EvoidApp.auto_connect()  # auto-detects WebGL, resolves same-origin URL

Prerequisites: Godot 4.4+ with HTML5 export template, EVOID server with GameHost

Scenario 3: Embed in Website (No Borders)

<!-- Parent website -->
<iframe src="/game/tic-tac-toe/" width="400" height="600" frameborder="0"></iframe>
<script>
iframe.addEventListener("message", (e) => {
    if (e.data.type === "evoid:player_joined") {
        showPlayerCount(e.data.player_id);
    }
});
// Send intent to game
iframe.contentWindow.postMessage({type: "evoid:send_intent", name: "pause", metadata: {}}, "*");
</script>
# Game side — receive from parent
func _ready():
    EvoidWebLoader.embed_message.connect(_on_embed_message)

func _on_embed_message(message: Dictionary):
    match message.get("type"):
        "evoid:focus": grab_focus()
        "evoid:resize": resize_canvas(message.get("width"), message.get("height"))

Prerequisites: Server with GameHost(embed_mode=True), parent page supports postMessage

Scenario 4: UDP Transport (Low-Latency)

func _ready():
    var config = EvoidConfig.new()
    config.udp_address = "your-server.com"
    config.udp_port = 9000
    EvoidApp.config = config
    EvoidApp.connect_udp(config.udp_address, config.udp_port, "Player1")

~0.5ms overhead vs ~2-5ms for WebSocket. 3 channels: reliable (game actions), unreliable (positions), chat.

Prerequisites: evoid-transport plugin on server, UDP port open

Scenario 5: Binary Intents (Bandwidth Optimization)

# ~60% smaller than JSON
EvoidClient.send_intent_binary("player_move", {"x": 10, "y": 20})

Scenario 6: Auto-Reconnect

Built-in. If connection drops:

  • State: ONLINE → RECOVERING
  • Exponential backoff: 1s, 2s, 4s, 8s, 16s, 20s (cap)
  • Auto-retry up to max_reconnect_attempts

Project Structure

your-game/
├── addons/
│   └── evoid_godot/           # Godot plugin (client)
│       ├── core/
│       │   ├── app.gd         # State machine + orchestration
│       │   ├── client.gd      # WebSocket connection
│       │   ├── udp_client.gd  # UDP transport
│       │   ├── event_bus.gd   # Pub/sub messaging
│       │   ├── config.gd      # Configuration
│       │   ├── topics.gd      # Topic constants
│       │   └── web_loader.gd  # WebGL + embed API
│       ├── export_plugin.gd   # Export optimizations
│       ├── plugin.cfg
│       └── plugin.gd
├── scenes/
│   ├── main.tscn              # Main game scene
│   ├── player.tscn            # Player prefab
│   └── lobby.tscn             # Matchmaking lobby
└── scripts/
    ├── main.gd                # Game controller
    ├── player.gd              # Player logic
    └── network.gd             # EVOID integration

How It Works

Client (Godot)

# 1. Connect — auto-detects WebGL
func _ready():
    EvoidApp.auto_connect()

# 2. Send player actions
func _on_shot_pressed():
    EvoidApp.send_intent("player_shot", {
        "origin": global_position,
        "direction": aim_direction,
    })

# 3. Receive server events
func _ready():
    EvoidBus.subscribe(EvoidTopics.GAME_EVENT, _on_game_event)

func _on_game_event(payload: Dictionary):
    match payload.get("type"):
        "player_moved": update_player(payload)
        "shot_fired": show_bullet(payload)

Server (EVOID)

# 1. Setup game
from evoid_godot import setup_game_hosting
setup_game_hosting("my-game")

# 2. Serve the game (with embed mode)
from evoid_godot import GameHost, SplashConfig

host = GameHost(embed_mode=True)  # iframe embed, no borders
host.register_build("my-game", "builds/my-game/", title="My Game")

# 3. Handle game intents
from evoid import subscribe

async def on_shot(intent):
    player_id = intent.metadata["player_id"]
    await publish(Intent(
        name="game_event",
        metadata={"type": "shot_fired", "player_id": player_id}
    ))

subscribe("game:my-game:player_shot", on_shot)

How Hosting Works (No Download)

The game doesn’t download — it streams from the server. Here’s what happens:

User clicks link: /game/my-game/

1. HTML splash loads instantly (<100ms)
   └─ Just a div with progress bar, no game code yet

2. Service Worker registers
   └─ Caches everything for instant repeat visits

3. engine.wasm streams in background (~5-10MB)
   └─ Godot engine as WebAssembly, loads once, cached forever

4. game.pck loads in chunks (256KB each)
   └─ Server splits the game data into small pieces
   └─ Client loads chunk 0, 1, 2, ... with progress bar

5. Game starts — splash fades
   └─ All data is in memory, game runs natively in browser

6. WebSocket connects to /ws
   └─ Player joins game, real-time multiplayer begins

What GameHost Does

GameHost is a Python class that turns your Godot HTML5 export into a web-served game:

from evoid_godot import GameHost

host = GameHost()
host.register_build("my-game", "builds/my-game/", title="My Game")

# This creates routes for everything:
# /game/my-game/           → HTML page with splash
# /game/my-game/manifest.json → file list + chunk count
# /game/my-game/sw.js       → Service Worker
# /game/my-game/engine.wasm → Godot engine
# /game/my-game/chunk/0     → First 256KB of game.pck
# /game/my-game/chunk/1     → Next 256KB of game.pck
# ...etc

Why It Feels Instant

MechanismWhat It Does
HTML splashShows progress bar in <100ms, user sees something immediately
Service WorkerCaches engine.wasm + game.pck — second visit loads from cache (0ms)
Chunked PCKGame data loads in 256KB pieces with progress — not one giant download
ManifestTells client exactly how many chunks to expect — no guessing

First Visit vs Repeat Visit

FIRST VISIT:
  HTML (100ms) → engine.wasm (5s) → game.pck chunks (3s) → Game starts
  Total: ~8-10 seconds

REPEAT VISIT (SW cached):
  HTML (100ms) → engine.wasm (0ms, cached) → game.pck (0ms, cached) → Game starts
  Total: <1 second

Client-Side Loading (EvoidWebLoader)

The Godot plugin handles loading automatically:

# EvoidWebLoader — registered as autoload
# 1. Fetches manifest.json to know chunk count
# 2. Prefetches chunks in background
# 3. Listens for postMessage from parent (embed mode)

func _ready():
    EvoidWebLoader.manifest_loaded.connect(_on_manifest)
    EvoidWebLoader.prefetch_chunks()  # starts loading

Server-Side Hosting (GameHost)

# Standalone — full splash screen
host = GameHost()
host.register_build("my-game", "builds/my-game/", title="My Game",
    splash=SplashConfig(bg_color="#1a1a2e", accent_color="#e94560"))

# Embed — minimal loader, transparent background
host_embed = GameHost(embed_mode=True)
host_embed.register_build("my-game", "builds/my-game/")

# Mount both
app = Starlette(routes=[
    Mount("/game", app=host.create_router()),      # standalone
    Mount("/embed", app=host_embed.create_router()), # iframe
])

Export Optimizations

The plugin automatically optimizes web exports:

  • Service Worker injectionEvoidExportPlugin injects SW registration into index.html
  • Manifest generation — auto-generates manifest.json for chunk-aware loading
  • Binary intentssend_intent_binary() uses WebSocket binary frames (~60% smaller)
  • Manifest-aware prefetchEvoidWebLoader.prefetch_chunks() uses server manifest for correct chunk count
# Config for export optimizations
var config = EvoidConfig.new()
config.optimize_web = true        # SW injection, manifest gen
config.binary_intents = false     # binary WebSocket frames
config.pck_compression = "fast"   # none/fast/zip

Cluster Compatibility

EVOID cluster is transparent to the Godot client. The game connects to one node via EvoidApp.connect_to_server(). The server-side cluster plugin handles routing between nodes.

graph LR
    Client[Player<br/>Godot] -->|WSS| Node1[Node 1<br/>ClusterBridge]
    Node1 <-->|WSS| Node2[Node 2<br/>IntentRouter]
    Node1 <-->|WSS| Node3[Node 3]

The client doesn’t know or care how many nodes exist. connect_to_server() works with any node.

Prerequisites (server-side only):

  • evoid-cluster plugin installed
  • WebSocket ports open between nodes
  • evoid-di for service discovery

DI Pattern

This plugin uses Godot’s native autoload singleton pattern — the equivalent of DI in GDScript. All components (EvoidApp, EvoidClient, EvoidUDP, EvoidBus, EvoidWebLoader) are globally accessible singletons.

On the server side, the Python evoid-godot plugin registers with evoid-di:

di.register("godot", create_game_handler, scope="singleton")

If you need testable code, EvoidConfig is the only injectable component — pass it to EvoidApp.config.

Quick Setup

# 1. Install EVOID server plugins
uv add evoid evoid-godot

# 2. Clone the Godot plugin
git clone https://github.com/EvolveBeyond/evoid-godot.git

# 3. Copy to your Godot project
cp -r evoid-godot/evoid_godot your-game/addons/

# 4. Enable in Godot
# Project → Project Settings → Plugins → EVOID → Enable

What You’ll Build

Project 1: Arena Shooter

A Counter-Strike-style top-down shooter. Two players, real-time movement and shooting.

Player 1 (Godot) ←→ EVOID Server ←→ Player 2 (Godot)

What you learn:

  • WebSocket real-time communication
  • Player movement sync
  • Shot detection and broadcasting
  • Game state management
  • Hosting Godot WebGL on EVOID

Project 2: Online Tic-Tac-Toe

A browser-based tic-tac-toe. One player in Godot, one in browser (or both in Godot).

Player 1 (Godot/WebGL) ←→ EVOID Server ←→ Player 2 (Godot/WebGL)

What you learn:

  • Turn-based game logic
  • Server-side validation (no cheating)
  • Room/matchmaking system
  • Instant game loading (no download)
  • Embedding games in websites

Next

Start with the Arena Shooter — it covers real-time movement and shooting.

Or skip to Tic-Tac-Toe — it covers turn-based logic and instant loading.