-
Notifications
You must be signed in to change notification settings - Fork 1
GUI
The GUI is a fully implemented 3D visualizer for the Zappy game. It connects to the server as a special observer client and renders the live game state — terrain, players, resources, eggs, incantations, and team scores.
It is built as a Godot 4 GDExtension: the logic is compiled C++ (loaded as a .so/.dll shared library) and the scene graph, rendering, and input handling are driven by Godot 4.6 with the GL Compatibility renderer.
./zappy_guiAn in-game Connect dialog lets you enter the server host and port. Once connected, the full game state is rendered immediately from the server's bootstrap messages.
zappy_gui (Godot executable)
└── zappy_gui.pck (packed scene data + project assets)
└── bin/libzappy_gui.linux.release.x86_64.so (C++ extension)
The C++ extension registers custom node types with Godot. The main scene (world.tscn) wires these nodes together via signals:
ZappyWorld (root)
├── ZappyConnection — TCP socket state machine
├── WorldState — authoritative game model (no Godot dependency)
│
├── MapTerrain — procedural noise terrain mesh
├── TileMarkers — 7× MultiMeshInstance3D for resource markers
├── EntityManager — spawns/updates PlayerEntity and EggEntity nodes
├── RtsCamera — orthographic RTS camera
├── DayNightCycle — rotating directional light + sky
│
└── HUD (Control overlay)
├── ConnectDialog — host/port entry
├── InventoryPanel — selected player's stats and inventory
├── TeamPanel — per-team player count and max level
├── WorldStatusPanel — total resources on the ground
├── BroadcastLog — scrolling broadcast/server message feed
├── TimePanel — current server time unit
└── EndGameOverlay — shown on game_over signal
Owns a Godot StreamPeerTCP and runs a state machine polled once per frame by ZappyWorld::_process():
Disconnected
└─ connect_to_host() ─→ Connecting
└─ TCP established ─→ AwaitingWelcome
└─ "WELCOME" received,
"GRAPHIC\n" + bootstrap sent ─→ Ready
└─ any TCP error ─→ Error
Bootstrap: once WELCOME is received, ZappyConnection sends GRAPHIC\n to identify as a GUI client, then immediately sends msz\nmct\ntna\nsgt\n so the server pushes the full initial game state.
Receive pipeline: available bytes are appended to _recvBuf. process_lines() extracts every complete \n-terminated line and passes it to protocol_parser::parse_line(). Each parsed message is pushed to an internal queue.
drain() returns and clears that queue. ZappyWorld::_process() drains it each frame, calls WorldState::apply(msg) for the model update, then calls dispatch_signals(msg) to emit the matching Godot signal.
parse_line(line) maps a raw protocol string to one of the ServerMessage variant alternatives defined in server_message.hpp:
using ServerMessage = std::variant<
MsgMapSize, MsgTileContent, MsgTeamName,
MsgPlayerNew, MsgPlayerPosition, MsgPlayerLevel, MsgPlayerInventory,
MsgPlayerExpulsion, MsgPlayerBroadcast, MsgPlayerDeath,
MsgIncantationStart, MsgIncantationEnd,
MsgEggLaying, MsgResourceDrop, MsgResourceCollect,
MsgEggLaid, MsgEggConnection, MsgEggDeath,
MsgTimeUnit, MsgTimeUnitSet,
MsgEndGame, MsgServerMessage,
MsgUnknownCommand, MsgBadParameter
>;Each struct is plain data with no Godot or rendering dependency — making it easy to unit-test in isolation.
A pure C++ class (zero Godot includes) that is the single source of truth for the live game. It owns:
-
_width,_height— map dimensions set byMsgMapSize. -
_tiles— flat vector ofTilestructs, size = width × height, indexed asy * width + x. EachTileholds a 7-element resource array. -
_players—unordered_map<uint32_t, Player>keyed by player id. -
_eggs—unordered_map<uint32_t, Egg>keyed by egg id. -
_teams— vector of team name strings intnaorder. -
_timeUnit,_gameOver,_winningTeam.
apply(msg) dispatches via std::visit to a private handler for each message type. Invalid data (unknown player ids, out-of-bounds coordinates) is silently ignored — the server is external and messages may arrive out of order during bootstrap.
reset() discards all state so that a reconnect starts clean.
Plain data types used by WorldState and queried by renderers:
struct Tile { std::array<uint32_t, 7> resources{}; };
struct Player { uint32_t id, x, y; Orientation orientation; uint8_t level;
std::string team; std::array<uint32_t, 7> inventory{}; bool incanting; };
struct Egg { uint32_t eggId, playerId, x, y; };
enum class Orientation : uint8_t { North=1, East=2, South=3, West=4 };ZappyWorld emits Godot signals after updating WorldState. Every renderer and HUD panel connects to the relevant signals rather than polling WorldState directly:
| Signal | Parameters | Source message |
|---|---|---|
map_initialized |
width, height | msz |
tile_updated |
x, y, q0..q6 |
bct/mct and any tile change |
team_registered |
name | tna |
player_spawned |
id, x, y, orientation, level, team | pnw |
player_moved |
id, x, y, orientation | ppo |
player_leveled |
id, level | plv |
player_inventory_updated |
id, x, y, q0..q6 | pin |
player_died |
id | pdi |
egg_laid |
egg_id, player_id, x, y | enw |
egg_removed |
egg_id |
ebo or edi
|
incantation_started |
x, y, level, ids[] | pic |
incantation_ended |
x, y, result | pie |
broadcast |
id, message | pbc |
time_updated |
t |
sgt/sst
|
server_message |
message | smg |
game_over |
team | seg |
connection_error |
message | TCP failure |
world_reset |
— | new connection started |
A MeshInstance3D subclass that generates a procedural plane mesh sized to the server's map. Features:
-
Noise displacement: a
FastNoiseLiteresource is sampled per vertex to give the terrain a smooth-hills look. Noise seed can be randomised withrandomize_seed(). -
Subdivisions: the mesh uses
SUBDIVISIONS_PER_TILE = 2subdivisions per tile, clamped toMAX_SUBDIVISIONS = 200for large maps. -
Perimeter walls: four flat "dirt block" walls are generated as a child
MeshInstance3Dthat extend below the camera's reach, hiding the terrain edges. -
tile_to_world(x, y): converts tile coordinates to world-space position on the terrain surface, accounting for the noise height. Used by all entity/resource placement code. -
get_height_at(wx, wz): queries the terrain height at arbitrary world-space coordinates; used by the ripple VFX to conform to hills. -
TILE_SIZE = 2.0world units per tile.
Holds 7 MultiMeshInstance3D children — one per resource type (food, linemate, deraumere, sibur, mendiane, phiras, thystame). Connected to map_initialized and tile_updated signals.
- On
map_initialized: all 7 multimeshes are sized towidth × heightinstances and hidden (scale 0). - On
tile_updated: for each resource, the instance aty * width + xis either shown (with a per-type fixed scale and world-space offset within the tile) or hidden (scale 0) based on the quantity. - Food markers face a deterministic pseudo-random horizontal angle per tile so they don't all point the same way; mineral crystal models are arranged in a hexagonal ring around the tile center.
A DirectionalLight3D that rotates itself every _process() frame to animate a full day-night cycle. The sky shader is parented to this node and reads LIGHT0_DIRECTION automatically, so the sky blends from day → sunset → night → sunrise with no additional code.
A Node3D that owns all live PlayerEntity and EggEntity instances in unordered_map<int, *> keyed by server-assigned id. Connected to every player/egg/incantation/broadcast signal.
Team colors: teams are assigned a stable color from an 8-entry palette in the order they are first seen. on_team_registered pre-seeds the assignment. Players and eggs inherit their team color.
Incantation VFX: on incantation_started, an _incantationVfxScene instance is spawned on the ritual tile and all participating players have set_incanting(true) called. The participating player ids are recorded per-tile in _incantingByTile. On incantation_ended, the VFX node is freed and set_incanting(false) is called for exactly those recorded players.
Broadcast ripple: on broadcast, a BroadcastRippleVfx is spawned on the player's tile (with a per-player cooldown to throttle rapid repeats). The ripple is configured with the MapTerrain reference and the player's team color.
A Node3D wrapping a 3D skeleton player model. It tracks its own tile position and smoothly interpolates to new positions when moved. The model's scale grows slightly with each level. A child Area3D on the "selectable" physics layer enables click-to-select raycasting by SelectionController.
A minimal Node3D wrapping a small 3D egg model, placed at the tile it was laid on.
An orthographic Camera3D with a fixed pitch (default −35°) and a free yaw. Controls:
| Input | Action |
|---|---|
| WASD / Arrow keys | Pan the camera on the ground plane |
| Scroll wheel | Zoom in / out (changes Camera3D.size, not distance) |
| Middle-mouse drag | Pan |
| Q / E (hold) | Continuously rotate yaw left / right |
Pan speed is scaled by size / REFERENCE_SIZE so screen-space speed stays constant across zoom levels. On map_initialized, the camera frames the entire map and clamps pan to the map bounds.
A sibling of RtsCamera that listens for left-click events, raycasts against the "selectable" physics layer, and emits player_selected(id) (or player_selected(-1) on a miss). InventoryPanel connects to this signal to display the clicked player's stats.
All panels are thin C++ classes over .tscn scenes. They hold no direct reference to WorldState; instead they maintain their own minimal local view built from signals.
Shows host/port LineEdit fields and a Connect button. On press, calls ZappyWorld::connect_to_server(). Hides on map_initialized (connection confirmed). Re-shows with an error message on connection_error.
Player picker (ItemList) on the left; position, level, team, and inventory Labels on the right. Keeps a PlayerSummary per live player, updated from player_spawned, player_moved, player_leveled, player_inventory_updated, and player_died. select_player(id) is the public entry point; it is called by SelectionController's player_selected signal.
Per-team scoreboard: player count and max level, kept as a map<String, TeamStats>. Updated from team_registered, player_spawned, player_leveled, and player_died. Teams are displayed sorted by name.
Totals every resource currently on the ground. Maintains a tile cache (unordered_map<int, array<int,7>>) so tile_updated (absolute counts) can be folded into running totals as deltas. Also renders a slowly-spinning 3D preview of each resource model in SubViewports.
A RichTextLabel in auto-scroll mode. broadcast → appends "[Player <id>] <text>". server_message → appends "[Server] <text>".
Displays the current server time unit, updated from time_updated.
A full-screen semi-transparent backdrop with a winner label, hidden by default. Shown by on_game_over(team).
A self-freeing Node3D that expands a team-colored ring from the broadcasting player's tile. The ring geometry is rebuilt every frame from RING_SEGMENTS points whose Y coordinate is sampled from MapTerrain::get_height_at(), so the ring conforms to the terrain hills instead of clipping into them. The material is unshaded + alpha-blended (no bloom required). The node calls queue_free() on itself when the animation is complete.
The GUI uses CMake with godot-cpp (fetched via CMake's FetchContent) to build the GDExtension shared library.
# from the project root:
make # builds all three components; copies zappy_gui + zappy_gui.pck + libzappy_gui.linux.release.x86_64.so
# or inside the Gui directory:
cmake -B build/linux-release -DCMAKE_BUILD_TYPE=Release
cmake --build build/linux-releaseThe top-level Makefile's Gui bin target runs the cmake build and then exports a self-contained zappy_gui PCK + executable pair.