feat(godot): 3D client with smooth facing, capsule animals, water drought fade & bird roosting#88
Merged
biosynthart merged 25 commits intoJun 21, 2026
Conversation
- WebSocket protocol identical to browser/Python clients - Intent-based agency: 60 Hz local behavior (flee→drink→mate→forage→hunt→pollinate→wander) - Heartbeat sender: 1 Hz position/event reporting upstream - Position reconciliation: gravity well + staggered sync + _ack queue - Isometric voxel renderer: colored blocks with depth sorting, moisture grid, animated water - HUD: stats panel, event log, rain button (R key) - Autoloads: WS (WebSocket + HTTP), World (entity registry + spatial queries) - Simulation layer is renderer-independent for future 3D swap
…rrors - Load world.json from local resources instead of HTTP (websockets server passthrough returns empty body to Godot HTTPRequest) - Send world definition immediately on WebSocket open (was waiting for session_started which creates a chicken-and-egg) - Fix PackedStringArray → Array[String] for pop_front() - Fix hash_val.abs() → absi(hash_val) - Fix HORIZONTAL_ALIGNMENT.CENTER → integer 1 - Fix C-style for loop → while loop - Fix particles.gd class_name + inner class parse error - Add _is_connecting flag to prevent concurrent WS reconnection - Add bbcode_enabled to event log RichTextLabel - Fix death particle position (lookup entity before removal)
Godot's JSON.stringify converts integers to floats, which broke range(dx) in voxel_manager. Coerce dimensions to int at parse time.
Godot JSON.stringify sends floats; randint needs ints.
- Right/middle mouse drag to pan, scroll wheel to zoom (0.2x-4x) - Fix isometric ground and entity rendering to scale with zoom - Add guard for degenerate polygons when zoomed out - Remove duplicate _world_center variable
Three bugs causing entities to jiggle in place and march off-grid: 1. Gravity well missing `* delta` multiplication — nudge was applied at full strength every frame (60x too strong), causing violent overshoot/oscillation around ref_position. Now matches Python/JS: nudge = GRAVITY_WELL_FACTOR * sync_speed * delta, with 0.2 unit proximity threshold to skip when already close. 2. Wander target regenerated every frame — \_evaluate_wandering() picked a new random target each frame instead of reusing an existing one until reached. This caused jitter as the entity chased a moving target. Now tracks has_target/last_action_type to persist wander targets, mirroring Python/browser behavior. 3. Reconciliation missing \_lastReconciledTick — used (tick % 4) == sync_phase instead of tracking ticks since last reconcile. Added queue pruning, last_reconciled_tick tracking, and proper stagger logic to match Python/browser reconciliation. Also added telemetry logging (every 10 ticks) printing entity divergence stats for debugging.
- Add orbit camera (LMB orbit, RMB pan, scroll zoom) - Add directional + ambient 3D lighting - Ground mesh, entity MultiMesh, particle MultiMesh nodes - Add help label with control hints to HUD - Switch main.gd from Node2D to Node3D with @onready bindings
New section documenting the 3D Godot 4.x client: - Project scaffolding, file layout, autoloads (WS + World) - Node tree: Node3D + Camera3D + MultiMesh entities/particles + HUD - WebSocket layer: connect, dispatch, session_started, tick_packet - World model: WorldEntity fields, spatial queries, sync personality - Client-side agency: 60 Hz behavior priority chain, wander target persistence, gravity well, interaction triggers - Reconciliation: staggered sync, queue management, last_reconciled_tick - Heartbeat sender: 1 Hz upstream position/event reporting - Rendering: ground mesh (SurfaceTool), entity cubes, particles - Orbit camera: spherical coordinate orbit, pan, zoom - Constants module: shared with browser/Python clients - Telemetry logging: divergence stats every 10 ticks - Sync bugs fixed: gravity well delta, wander reuse, last_reconciled_tick - Current limitations: no skeletal animation, no water rendering
…rmony Replace single BoxMesh-per-entity with per-type composite meshes built from SurfaceTool primitives (cylinder, sphere, cone, planes). Each entity type gets its own MultiMeshInstance3D with a unique shape: - Deer (ANIMAL): cylinder body + hemispherical head + cone snout + legs - Songbird (BIRD): capsule body + nose cone + tail cone + wing planes - Butterfly (INSECT): small body + 4 wing planes (upper/lower pairs) - Oak (TREE): cylinder trunk + sphere canopy - Grass (PLANT): 3 angled blade planes - Wildflower (PLANT): stem cylinder + bloom sphere - Mushroom (MICROORGANISM): stalk + flat-topped cap State-aware sizing: trees/plants scale by growth, mushrooms by activity, insects float with sine oscillation, birds fly higher. Dormant entities are darkened 55%, wilted plants shift to brown. Harmonize all type/species/particle colors with the browser renderer palette (constants.js). Water sources rendered as flat cylinders with animated alpha pulse. Scene restructuring: - Add Renderer node (composite mesh builder) - Replace single Entities MultiMeshInstance3D with per-type instances - Add WaterSources Node3D parent - Dark background matching browser (#0f100f)
- Fix ground triangle winding (second triangle was CW, normal pointed down) - Replace planar grid with 1x1x1 BoxMesh voxels via MultiMesh - Merge water sources into voxel grid (blend cells toward water color) - Remove circular water mesh rendering (cylinder + standard material) - Drop separate WaterSources scene node
- world_model: use explicit type list instead of PackedStringArray lookup - engine.py: include type/species in entity update packets
- ws_client: add 100ms poll timer (decoupled from render loop) so server pings are serviced even when FPS drops from voxel rebuild - ws_client: persist world definition as raw string, resends on reconnect - ws_client: create fresh WebSocketPeer on reconnect to clear stale state - renderer: build ground MultiMesh once in setup, only update colors each frame (was allocating new BoxMesh+MultiMesh every frame, causing memory pressure)
- Add log() helper to LilaConstants - Replace print() calls in ws_client.gd and main.gd
… sizes - Replace removed ConeMesh with CylinderMesh (top_radius=0) - Drop sphere.rings (not settable in 4.7) - Remove ring_count from CylinderMesh (not settable in 4.7) - Lower fruiting wildflower size multiplier from 2.0 to 1.3 - Remove class_name Renderer (not needed, scene wires it directly) - Drop static from renderer methods (called via instance in main.gd)
- Add TURN_SPEED constant (8.0 rad/s) in constants.gd - _move_toward(): lerp facing_angle toward target direction with wrapf to handle -π/π boundary correctly - _execute_reconcile(): also smooth facing angle during reconciliation spiral movement (was not updating facing_angle at all) - Uses clampf on wrapped angle diff for frame-rate-independent turn speed
- CapsuleMesh (radius=1.0, height=2.5) for a deer-like body shape - Tilt capsule 90° around Z in transform so it lies horizontal (default CapsuleMesh is vertical like a standing pill) - Combine Y-facing rotation with Z-tilt via Basis multiplication, same pattern as bird cone tilt
X-tilt lays capsule horizontal so long axis aligns with travel direction; Z-tilt was spinning around the capsule's own long axis
- Derive max_radius on client (radius / water_level) to know original footprint size - Blend falloff now uses max_radius so cells keep a water tint that fades with level rather than suddenly losing coverage
Deer look less fat with narrower capsule body
- Birds in IDLE/RESTING now seek nearest tree instead of wandering randomly - When near a tree, birds hover nearby with gentle sinusoidal wobble - Mirrors server-side roosting logic from movement_actors.py
Mirrors server Python header style: - līlā project banner - Copyright 2025 BioSynthArt Studios LLC - Apache 2.0 license line - File path and purpose description
Mirrors server header style: - līlā project banner - Copyright 2025 BioSynthArt Studios LLC - Apache 2.0 license line - File path and purpose description
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First PR for the Līlā Godot 4.x 3D Client — a native desktop visualizer for the ecosystem simulation that complements the existing browser-based client.
Genesis
The Godot client started as an isometric 2D renderer and evolved through several milestones:
This PR adds the latest visual polish and behavioral refinements built on top of that foundation.
Changes in this PR
TURN_SPEED=8.0 rad/s) instead of snapping instantly when changing directionroost_affinitylogic)Architecture
The Godot client follows the same intent-based model as the browser:
Files changed
client/godot/scripts/agency.gd— smooth facing interpolation, bird roosting behaviorclient/godot/scripts/renderer.gd— capsule mesh, water blend fadeclient/godot/scripts/constants.gd— TURN_SPEED constantclient/godot/scripts/autoloads/world_model.gd— max_radius tracking for water sourcesCommits ()