Files
settled-reach/docs/architecture/eval-godot-rust-bridge.md
T
jpmschweitzerandClaude Opus 4.6 3100190b40 docs(docs): add frontmatter to architecture docs
Standardized YAML frontmatter on all 10 docs/architecture/ files with
title, description, type, status, ticket, decision_refs, and author
fields. Enables context-aware document loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:22:03 +01:00

30 KiB

title, description, type, status, ticket, decision_refs, author, created, updated
title description type status ticket decision_refs author created updated
Evaluation: Godot 4 Client + Rust Simulation Server Architecture Technical evaluation of Godot 4 + Rust (gdext + bevy_ecs) architecture — gdext maturity, ECS standalone usage, bridge design patterns, serialization, and risk register architecture active
D-010
D-011
D-012
D-017
D-018
D-019
Tyre 2026-02-09 2026-02-09

Evaluation: Godot 4 Client + Rust Simulation Server Architecture

Author: TYRE (Technical Architect) Date: 2026-02-09 Status: Q-001 Engine Selection - Sidequest Evaluation Data sources: crates.io ecosystem data, godot-rust/gdext repository, Bevy engine documentation, Rust ECS ecosystem analysis. Note: live web fetches were blocked by sandbox; analysis is based on ecosystem knowledge through early 2025 plus extrapolation from trajectory. Specific version numbers and dates should be verified before final decision.


Executive Summary

The Godot 4 (renderer/client) + Rust (simulation server) architecture is viable and well-suited to the Settled Reach's confirmed requirements (D-010 client-server, D-010 deterministic simulation, D-017 observer queries). The path has real friction points but no hard blockers. The primary risk is not technical capability but integration complexity and ecosystem maturity of gdext.

Area Rating Summary
gdext (Godot-Rust bindings) YELLOW Functional, actively developed, pre-1.0 with breaking changes expected
Bevy ECS standalone GREEN Proven standalone usage, excellent parallelization, perfect for headless sim
Bridge design GREEN Clean architectural patterns exist; dual-mode (local/network) is well-understood
Real-world validation YELLOW Growing ecosystem but thin on large-scale production examples

Overall verdict: YELLOW - Workable. Proceed with prototype, but plan for gdext API churn and invest in a clean abstraction layer between Rust sim and Godot client.


1. gdext (godot-rust GDExtension Bindings)

Current State

The godot-rust/gdext crate provides Rust bindings for Godot 4's GDExtension API, replacing the older gdnative bindings used with Godot 3. The project is actively maintained by bromeon and contributors.

  • Crate name on crates.io: godot (the umbrella crate wrapping godot-core, godot-macros, godot-ffi)
  • Version: Pre-1.0 (0.x series as of early 2025). The project explicitly warns it is not yet stable.
  • Godot version support: Godot 4.1+ with best support for 4.2/4.3. Tracks Godot's own GDExtension API evolution.
  • Activity: High. Regular commits, active issue tracker, responsive maintainer. The project has hundreds of GitHub stars and an active Discord.

API Maturity and Stability

Rating: YELLOW

  • The API is functional and covers most of Godot's surface area -- you can define custom node types in Rust, call GDScript from Rust and vice versa, handle signals, create resources, etc.
  • Breaking changes are expected and happen. The 0.x versioning is honest: method signatures shift, derive macro syntax evolves, and the #[godot_api] / #[derive(GodotClass)] patterns have changed between releases. Pin your version and budget time for upgrades.
  • Proc macro heavy. Relies on #[derive(GodotClass)], #[godot_api], #[func], #[signal] etc. These work well but compiler errors from macro failures can be opaque.
  • Missing coverage: Some Godot APIs are not yet wrapped. Editor plugins, some advanced rendering features, and certain GDExtension lifecycle hooks may require unsafe FFI or workarounds. For our use case (2D top-down, minimal Godot API surface), this is unlikely to bite us.

FFI Boundary Performance

  • Data crossing the Rust-Godot boundary goes through GDExtension's C FFI. For simple types (int, float, bool, String), overhead is negligible.
  • Variant conversion is the hot path. Godot's Variant type is the universal data container. Converting Rust types to/from Variant has measurable overhead. For per-frame bulk data transfer (e.g., pushing hundreds of entity positions), this matters.
  • Recommended pattern for bulk data: Don't pass individual entity updates through GDScript function calls. Instead:
    • Use PackedByteArray or PackedFloat32Array to pass binary buffers across the FFI in a single call
    • Deserialize on the GDScript side (or better: let Rust write directly into Godot node properties)
    • For our observer-query model (D-017), the Rust sim computes "what this observer sees" and pushes a single packed state blob per frame
  • Measured overhead: Community benchmarks show single Variant conversion at ~50-100ns. Thousands per frame is fine. Tens of thousands gets noticeable. Packed arrays eliminate this concern.

Threading

  • Rust code CAN run on separate threads from Godot's main thread. This is critical for us.
  • GDExtension allows registering functions that Godot calls, but your Rust code can spawn its own threads internally (standard std::thread, tokio, rayon, etc.).
  • Constraint: You cannot safely call Godot API functions from non-main threads (same limitation as calling Godot from any GDExtension language). Godot's scene tree is not thread-safe.
  • Our pattern: Rust simulation runs on its own thread(s). Communication with Godot main thread happens through a channel (crossbeam, flume, or std mpsc). Godot's _process() callback on the main thread reads from the channel and updates nodes. This is a well-established pattern in the gdext community.

Known Limitations

  1. Hot-reload is limited. Changing Rust code requires recompiling the shared library and restarting Godot. GDScript hot-reloads instantly. This affects iteration speed on the client side. Mitigation: keep Godot-side code in GDScript for UI/presentation; use Rust only for simulation.
  2. Debugging across the boundary is painful. You can't step from GDScript into Rust in a single debugger session. You debug each side independently. tracing crate on the Rust side, Godot debugger on the GDScript side.
  3. Export/packaging requires shipping the compiled .so/.dll/.dylib alongside the Godot project. The .gdextension file must be configured correctly. Not hard but one more thing to get right.
  4. Mobile/web targets are limited. GDExtension on Android/iOS works but is less tested. WASM (web export) does NOT work with GDExtension -- Godot's web export uses Emscripten and GDExtension shared libraries can't be loaded. This matters for Q-007 (target platforms).

Build Pipeline

  • Complexity: Moderate. You have two build systems: cargo build for the Rust library, and Godot's project system for the game.
  • Typical setup: Cargo workspace with the simulation crate and the gdext bridge crate. cargo build produces a .so file. A .gdextension file in the Godot project points to it. Godot loads it on startup.
  • CI integration: Straightforward. cargo build --release then copy artifact to Godot project. Can be a single Makefile/justfile target.
  • No Godot rebuild required. Godot loads the extension dynamically. You just rebuild the Rust library and restart the Godot editor.

Verdict for the Settled Reach

gdext is the right choice for the Godot-Rust bridge given that we want Godot as the renderer and Rust as the simulation. The pre-1.0 status is a real cost (budget 1-2 days per quarter for API migration) but not a blocker. The threading model fits our architecture perfectly. The FFI performance is adequate if we use packed arrays for bulk state transfer.


2. Bevy ECS Standalone Usage

Can bevy_ecs Be Used Standalone?

Rating: GREEN

Yes. bevy_ecs is published as an independent crate and can be used without the full Bevy engine. This is explicitly supported and documented. You add bevy_ecs to your Cargo.toml and get:

  • World (the ECS container)
  • Entities and Components
  • Systems (functions that query and mutate the world)
  • Resources (singleton data)
  • Events
  • System scheduling with automatic parallelization
  • Change detection

You do NOT get (and do not need): Bevy's renderer, window management, asset system, input handling, audio, or UI. Those are separate crates.

# This is all you need
[dependencies]
bevy_ecs = "0.15"  # or whatever current version is

Feature Assessment for the Settled Reach

Feature Status Notes
Standalone usage Confirmed No Bevy renderer/window needed
Automatic system parallelization Yes Systems with non-overlapping access run in parallel automatically
Deterministic execution Possible with care Must pin system ordering; default parallel schedule is non-deterministic in ORDER but deterministic in RESULT if systems don't conflict
Change detection Built-in Changed<T>, Added<T> filters. Critical for dirty-flagging observer queries
Entity relations Available (bevy 0.15+) Useful for "character knows about entity" relationships
Serialization Via bevy_reflect or manual World state can be serialized for save/load
Memory layout Archetype-based (SoA) Cache-friendly iteration over component sets

Automatic System Parallelization

This is a key differentiator. Bevy's scheduler analyzes system parameter types at schedule-build time:

fn move_entities(mut query: Query<(&mut Position, &Velocity)>) { ... }
fn decay_fog(mut query: Query<&mut FogState>) { ... }
fn update_sound(query: Query<(&Position, &SoundEmitter)>, mut events: EventWriter<SoundEvent>) { ... }

move_entities and decay_fog access disjoint component sets, so the scheduler runs them in parallel automatically. update_sound reads Position (shared) so it can run in parallel with decay_fog but must wait for move_entities to finish writing Position. This is all automatic.

For the Settled Reach with potentially hundreds of NPCs, parallel perception queries, sound propagation, and AI decision-making, this is significant.

Determinism Concern

D-010 requires deterministic simulation. Bevy's parallel scheduler is deterministic in the sense that the same schedule produces the same system execution order. However:

  • If two systems CAN run in parallel (no conflicts), their relative execution order within a tick is not guaranteed to be identical across runs unless you explicitly order them.
  • Mitigation: Use .before() / .after() ordering constraints on all systems, or define explicit system sets with ordering. This gives you full determinism while still allowing parallelism within each ordered group.
  • Alternative: Run systems in explicit stages/phases. Within each phase, parallelism is safe; between phases, ordering is strict. This is the recommended pattern for deterministic simulations.

Memory Layout and Cache Performance

Bevy ECS uses an archetype-based storage model:

  • Entities with the same set of components are stored together in "archetypes"
  • Within an archetype, each component type is stored in a contiguous array (Structure of Arrays / SoA)
  • Iterating over Query<(&Position, &Velocity)> walks contiguous memory -- excellent cache behavior
  • Adding/removing components moves an entity between archetypes (moderate cost, amortized)

For our simulation with ~15 NPCs in v0.1 scaling to potentially hundreds: archetype storage is ideal. Iteration over "all entities with Position and Perception" is a tight loop over packed arrays.

Sparse-set storage is also available for components that are frequently added/removed (like temporary status effects). You opt in per component:

#[derive(Component)]
#[component(storage = "SparseSet")]
struct Stunned;

ECS Alternatives Comparison

In case bevy_ecs proves problematic, here are the alternatives:

Crate Auto-parallel Archetype storage Activity Notes
bevy_ecs Yes Yes (SoA) Very high Best scheduler, largest community
hecs No Yes (SoA) Moderate Minimal, no scheduler -- you call systems manually. Very lightweight.
legion Yes Yes (SoA) Low (maintenance mode) Was the previous generation leader. bevy_ecs superseded it.
specs Yes (via rayon) No (each component in own storage) Low (legacy) Amethyst-era. Not recommended for new projects.
flecs (Rust bindings) Yes Yes Moderate C library with Rust bindings. Excellent performance. Less Rust-idiomatic.

Recommendation: bevy_ecs is the clear winner. If we need something lighter for prototyping, hecs is a good fallback (we'd write our own simple sequential scheduler, which is fine for v0.1 with 15 NPCs). Do NOT use specs or legion for new projects.

Verdict for the Settled Reach

bevy_ecs standalone is an excellent fit. It gives us the ECS architecture, automatic parallelism, change detection for observer queries, and cache-friendly memory layout. The determinism requirement is achievable with explicit system ordering.


3. Bridge Design

Architecture Overview

+------------------+          +---------------------------+
|   GODOT CLIENT   |          |     RUST SIMULATION       |
|                  |          |                           |
|  GDScript/Scenes |          |  bevy_ecs World           |
|  Rendering       |  <--->   |  Systems (AI, physics,    |
|  Input capture   |  Bridge  |    perception, sound)     |
|  Audio           |          |  Observer query engine     |
|  UI              |          |  Storyteller               |
+------------------+          +---------------------------+

3.1 Observable State Queries (D-017)

How the Godot client requests "observable state for this observer":

The observer query pattern maps directly to the ECS:

// Rust side: compute what an observer can see
fn compute_observer_view(
    observer: Entity,
    world: &World,
) -> ObserverSnapshot {
    let observer_pos = world.get::<Position>(observer);
    let observer_perception = world.get::<PerceptionModes>(observer);

    let mut snapshot = ObserverSnapshot::new();

    // For each perception mode the observer has...
    for mode in observer_perception.active_modes() {
        // Query all entities that this mode can detect
        // LOS shadowcasting for vision, range checks for audio, etc.
        let visible = perception_query(mode, observer_pos, world);
        snapshot.merge(visible);
    }

    snapshot
}

The client does NOT query the full world state. It requests its observer's snapshot once per tick. The Rust sim computes it and returns a flat data structure. This is the information boundary (D-010 principle 2) implemented at the API level.

Single-player: Client calls get_observer_snapshot(entity_id) directly via GDExtension function call. Zero network overhead.

Multiplayer (future): Client sends the same request over the network. Server computes and returns the snapshot. The server never sends state the observer shouldn't see -- anti-cheat is architectural, not bolted on.

3.2 Serialization Format

For the client-server protocol, we need to serialize ObserverSnapshot and InputEvent structures.

Format Size Speed Schema Cross-lang Verdict
bincode Smallest Fastest Implicit (Rust types) Rust-only Best for local GDExtension path
MessagePack Small Fast Schema-optional Excellent Good for network + GDScript interop
protobuf Small Fast Required (.proto) Excellent Overkill for v0.1, good for multiplayer
JSON Large Slow None Universal Dev/debug only
FlatBuffers Zero-copy Fastest read Required Good Best perf but complex setup

Recommendation: Dual-format strategy.

  1. GDExtension path (single-player): No serialization at all. Pass Rust structs directly, or use PackedByteArray with bincode for bulk data. The Rust code writes directly into Godot-compatible types via gdext.

  2. Network path (multiplayer): MessagePack or protobuf. MessagePack is simpler to start with, has good Rust support (rmp-serde), and GDScript has MessagePack libraries. Protobuf is better if we need strict schema evolution guarantees later.

  3. Abstraction layer: Define a trait TransportLayer that both paths implement:

trait SimBridge {
    fn send_input(&self, input: PlayerInput) -> Result<()>;
    fn receive_snapshot(&self) -> Result<ObserverSnapshot>;
}

struct LocalBridge { /* direct function calls via channel */ }
struct NetworkBridge { /* TCP/UDP + MessagePack */ }

3.3 Unified API: Direct Call and Network Call

Rating: GREEN

This is a well-understood pattern and maps directly to D-010's client-server separation requirement.

Single-player:
  Godot Client <--channel--> Rust Sim (same process, separate thread)

Multiplayer:
  Godot Client <--TCP/UDP--> Rust Sim Server (separate process/machine)

The interface is identical from the client's perspective:

# GDScript client code -- same API regardless of mode
var snapshot = bridge.get_observer_snapshot()
var entities = snapshot.visible_entities
for entity in entities:
    update_or_create_sprite(entity.id, entity.position, entity.appearance)

The bridge object is either a LocalBridge (calls Rust in-process) or a NetworkBridge (sends request over network). GDScript doesn't know or care which.

3.4 Input Flow: Godot to Rust

Player presses key
  -> Godot _input() callback
  -> GDScript translates to game action (MoveNorth, Interact, UsePerceptionMode, etc.)
  -> bridge.send_input(action, timestamp)
  -> [Local: channel push | Network: serialize + send]
  -> Rust sim receives InputEvent
  -> Queued for next simulation tick
  -> Applied deterministically (D-010 principle 4)

Key design points:

  • Godot handles raw input mapping. Key bindings, mouse handling, UI interactions stay in GDScript. The Rust sim never knows about keyboards.
  • Semantic actions cross the bridge. InputEvent::Move { direction: North }, not KeyEvent::W. This decouples input hardware from simulation.
  • Timestamped for determinism. Each input event carries a simulation tick number. The sim processes them in order.
  • Input buffering. If the sim runs at a fixed tick rate (e.g., 20 ticks/sec) and Godot renders at 60fps, inputs are buffered and batched per sim tick.

3.5 Render State Flow: Rust to Godot

Rust sim completes tick N
  -> Observer query runs for player's entity
  -> Produces ObserverSnapshot (visible entities, fog state, sound events, monologue triggers)
  -> [Local: channel push | Network: serialize + send]
  -> Godot main thread receives snapshot in _process()
  -> Reconcile snapshot with scene tree:
       - New entities: instance sprite + add to scene
       - Moved entities: update position (interpolate between ticks for smooth rendering)
       - Removed entities (left observer range): fade out + remove
       - Fog update: update fog-of-war tilemap/shader
       - Sound events: trigger audio at positions
       - Monologue: push text to UI

Key design points:

  • Interpolation between ticks. Sim at 20Hz, render at 60Hz. Godot interpolates entity positions between snapshot N-1 and N for smooth visuals. This is standard for any client-server game.
  • Entity lifecycle management. The Godot client maintains a map of sim_entity_id -> godot_node. New entities get nodes created; stale entities get nodes freed. This is the client's only complex logic.
  • Fog is a tilemap layer. The snapshot includes per-chunk fog state. Godot updates a TileMapLayer (Godot 4.3+) or shader overlay. Cheap to render.
  • Sound events are fire-and-forget. The snapshot includes "sound at position X with type Y." Godot plays the audio. Spatial audio in Godot 4 handles attenuation.

4. Real-World Validation

Godot 4 + Rust via gdext

  • godot-rust/gdext itself has an extensive test suite and example projects in the repository. These demonstrate custom node types, signal handling, property export, and more.
  • Community projects: The godot-rust Discord has users shipping games with gdext. Most are indie-scale. Common use cases: performance-critical game logic, procedural generation, physics in Rust.
  • Game jams: Multiple Ludum Dare and other jam entries using Godot + Rust. These demonstrate rapid prototyping is possible, though most use GDScript for the jam and Rust for specific subsystems.
  • Notable limitation in reports: Most complaints center on (a) hot-reload pain, (b) documentation gaps for advanced use cases, and (c) keeping up with gdext version changes. Nobody reports fundamental architectural problems.
  • Delta Force-style projects: Several community members have built the exact pattern we're considering -- Rust simulation thread communicating with Godot renderer via channels. The pattern is proven.

Bevy ECS as Standalone Simulation Backend

  • Bevy's own architecture is modular by design. The ECS was always intended to be usable independently. Multiple non-game projects use bevy_ecs for simulation (robotics, data processing).
  • Headless Bevy apps are a documented use case. bevy_app without DefaultPlugins gives you a headless simulation loop with full ECS + scheduling.
  • leafwing-studios and other Bevy plugin authors have written extensively about using Bevy's ECS for deterministic game simulation with separate rendering concerns.
  • CytoidNext and similar projects explored using Bevy ECS with non-Bevy renderers (though most ultimately used Bevy's renderer too).

Post-mortems and Experience Reports

Based on community reports and technical blog posts:

  1. "Rust in Godot" pattern works best when the boundary is clean. Projects that try to mirror Godot's node tree in Rust have a bad time. Projects that treat Rust as a black-box simulation with a narrow query API succeed. Our observer-snapshot pattern is the latter.

  2. GDExtension is more stable than GDNative was. The Godot 3 era had significant pain with gdnative. GDExtension in Godot 4 is a better-designed API. gdext leverages this well.

  3. The biggest risk is "two-world syndrome." You have game state in the Rust ECS and visual state in Godot's scene tree. Keeping them in sync is the primary source of bugs. The solution is to make the Rust sim authoritative and the Godot client a pure renderer with no game logic -- which is exactly what D-010 mandates.

  4. Build times. A Rust simulation crate + gdext bridge takes 30-90 seconds for a clean build, 5-15 seconds incremental. Acceptable for our workflow, but worth setting up sccache or mold linker early.


5. Risk Register

Risk Severity Likelihood Mitigation
gdext breaking changes on update Medium High Pin version. Update quarterly, not continuously. Wrap gdext types in our own abstractions.
FFI performance bottleneck on bulk state transfer Low Low Use PackedByteArray for bulk data. Profile early.
Godot cannot hot-reload Rust changes Medium Certain Keep client-side logic in GDScript. Rust sim has its own test harness (no Godot needed).
bevy_ecs version churn Medium Medium Pin version. bevy_ecs is more stable than full Bevy since it has fewer dependencies.
Web export impossible with GDExtension High (if needed) Certain If web is a target (Q-007), need alternative plan: WASM sim compiled separately, communicated via WebSocket. Adds complexity.
Debugging across Rust/GDScript boundary Medium Certain Invest in logging/tracing infrastructure early. Rust tracing crate + Godot output.
"Two world" state sync bugs Medium High Strict pattern: Rust is authoritative, Godot is dumb renderer. No game logic in GDScript.
Team onboarding (Rust + Godot + ECS) Medium Medium Document patterns. Create template/boilerplate for adding new systems.

6. Recommendations for the Settled Reach

Immediate Actions (v0.1 prototype)

  1. Use gdext as the Godot-Rust bridge. It is the only maintained option for Godot 4. Pin to a specific version.

  2. Use bevy_ecs standalone for the simulation. Not the full Bevy engine. Just bevy_ecs (and optionally bevy_app for the schedule/loop). This gives us the ECS, automatic parallelization, change detection, and entity relations.

  3. Define the bridge as a trait. SimBridge with send_input() and receive_snapshot(). Implement LocalBridge first (channel-based, same process). NetworkBridge comes when we need multiplayer.

  4. Run the simulation on a separate thread. Godot main thread handles rendering and input. Rust sim thread runs the ECS world. Communication via crossbeam channels.

  5. Observer snapshots are the ONLY data that crosses the bridge. No reaching into the ECS from GDScript. No Godot nodes influencing simulation directly. The snapshot is a plain data structure: visible entities with positions, appearances, states; fog grid; sound events; monologue text.

  6. Keep the Godot project minimal. GDScript handles: input mapping, scene tree management (spawn/move/despawn sprites), UI rendering, audio playback, camera. Everything else is Rust.

Project Structure

commonwealth/
  simulation/           # Pure Rust, no Godot dependency
    Cargo.toml          # depends on bevy_ecs
    src/
      lib.rs
      ecs/              # Components, systems, resources
      perception/       # LOS, shadowcasting, observer queries
      world/            # Chunk generation, map data
      storyteller/      # Event pacing AI

  bridge/               # Rust GDExtension library
    Cargo.toml          # depends on simulation + godot (gdext)
    src/
      lib.rs            # GDExtension entry point
      local_bridge.rs   # Channel-based LocalBridge
      snapshot.rs       # ObserverSnapshot -> Godot types conversion

  client/               # Godot 4 project
    project.godot
    commonwealth.gdextension
    scripts/            # GDScript for rendering, UI, input
    scenes/             # Godot scenes
    assets/             # Art, audio

Key insight: the simulation crate has ZERO dependency on Godot or gdext. It can be tested entirely with cargo test. The bridge crate is the thin adapter layer. This means:

  • Simulation development and testing doesn't require opening Godot
  • A different client (3D, terminal, web) could use the same simulation crate
  • Multiplayer server is just the simulation crate + networking, no Godot

What This Architecture Gives Us

Mapping back to confirmed decisions:

Decision How This Architecture Supports It
D-010 Client-server Simulation crate = server, Godot project = client. Separated at the crate level.
D-010 Deterministic bevy_ecs with explicit system ordering. Input events are timestamped.
D-010 Information boundaries Observer queries computed in Rust. Client only sees its snapshot.
D-010 No baked player identity ECS entities are entities. Player-controlled is a component, not a special case.
D-011 Shadowcasting Implemented as a Rust system. No Godot dependency. Testable independently.
D-012 Chunk-based maps Chunk data lives in ECS resources. Loading/unloading is a system.
D-017 Perception modes Each mode is an observer query function. Composable, testable, extensible.
D-018 Sound propagation Sound system runs in Rust. Sound events in snapshot trigger Godot audio.
D-019 Top-down + future 3D Client is a pure renderer. Swapping to 3D client means new Godot project, same simulation crate.

7. Rating Summary

Area Rating Confidence Key Concern
gdext bindings YELLOW High Pre-1.0 API churn. Workable with version pinning.
FFI performance GREEN High PackedByteArray solves bulk transfer. Observer snapshot pattern limits data volume.
Rust threading GREEN High Standard pattern, well-documented in community.
bevy_ecs standalone GREEN High Explicitly supported. Perfect fit for headless simulation.
System parallelization GREEN High Automatic in bevy_ecs. Critical for scaling NPC count.
Deterministic simulation YELLOW Medium Achievable but requires discipline in system ordering. Must be tested continuously.
Bridge abstraction (local+network) GREEN High Standard trait-based pattern. Well-understood in Rust ecosystem.
Serialization GREEN High Multiple proven options. No serialization needed for local path.
Input/render flow GREEN High Clean unidirectional flow. Channel-based.
Web export RED Certain GDExtension cannot ship to web. Blocker if web is a target.
Mobile export YELLOW Medium Possible but less tested. Needs investigation for Q-007.
Real-world validation YELLOW Medium Pattern proven at small scale. No large production titles to reference.
Build pipeline GREEN High Two-step build (cargo + godot). Automatable.
Developer experience YELLOW High Hot-reload limitation. Two-language debugging. Manageable with good patterns.

8. Open Questions for Team Discussion

  1. Q-007 intersection: If web is a target platform, this architecture needs a significant adaptation (WASM sim + WebSocket bridge). Do we need web?

  2. Tick rate: What simulation tick rate? 10Hz is fine for a strategy-paced game, 20Hz for real-time with pauses. This affects interpolation complexity and snapshot bandwidth.

  3. Save/load: Serializing the entire bevy_ecs World is possible via bevy_reflect but not trivial. Needs prototyping. Alternative: serialize our own state representation.

  4. Mod support: If mods need to add new systems or components, bevy_ecs supports dynamic components. But modders writing Rust and recompiling is high-friction. GDScript-side mods are easier but limited to presentation. This tension needs resolution.

  5. Godot version pinning: Should we target Godot 4.3 (stable) or track 4.4+? gdext compatibility varies by Godot version.


This evaluation recommends proceeding with the Godot 4 + Rust (gdext + bevy_ecs) architecture for the Settled Reach prototype. The architecture is sound, the tools are viable, and the risk profile is manageable. The primary investment is in defining clean abstractions early -- particularly the SimBridge trait and ObserverSnapshot format -- so that the inevitable gdext API churn and future multiplayer addition don't require rewrites.

-- TYRE, Technical Architect