Skip to content
Alrik Neihouser edited this page Jun 27, 2026 · 2 revisions

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.

Usage

./zappy_gui

An 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.


High-level overview

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

Detailed architecture

Network layer (src/network/)

ZappyConnection

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.

ProtocolParser (src/network/protocol_parser.cpp)

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.


Game model layer (src/world/)

WorldState

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 by MsgMapSize.
  • _tiles — flat vector of Tile structs, size = width × height, indexed as y * width + x. Each Tile holds a 7-element resource array.
  • _playersunordered_map<uint32_t, Player> keyed by player id.
  • _eggsunordered_map<uint32_t, Egg> keyed by egg id.
  • _teams — vector of team name strings in tna order.
  • _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.

world_types.hpp

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 };

Signal graph (ZappyWorld)

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

World rendering (src/world/)

MapTerrain

A MeshInstance3D subclass that generates a procedural plane mesh sized to the server's map. Features:

  • Noise displacement: a FastNoiseLite resource is sampled per vertex to give the terrain a smooth-hills look. Noise seed can be randomised with randomize_seed().
  • Subdivisions: the mesh uses SUBDIVISIONS_PER_TILE = 2 subdivisions per tile, clamped to MAX_SUBDIVISIONS = 200 for large maps.
  • Perimeter walls: four flat "dirt block" walls are generated as a child MeshInstance3D that 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.0 world units per tile.

TileMarkers

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 to width × height instances and hidden (scale 0).
  • On tile_updated: for each resource, the instance at y * width + x is 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.

DayNightCycle

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.


Entity system (src/entities/)

EntityManager

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.

PlayerEntity (src/entities/player_entity.cpp)

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.

EggEntity (src/entities/egg_entity.cpp)

A minimal Node3D wrapping a small 3D egg model, placed at the tile it was laid on.


Camera (src/camera/)

RtsCamera

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.

SelectionController

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.


HUD panels (src/ui/)

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.

ConnectDialog (ServerConnectDialog)

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.

InventoryPanel

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.

TeamPanel

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.

WorldStatusPanel

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.

BroadcastLog

A RichTextLabel in auto-scroll mode. broadcast → appends "[Player <id>] <text>". server_message → appends "[Server] <text>".

TimePanel

Displays the current server time unit, updated from time_updated.

EndGameOverlay

A full-screen semi-transparent backdrop with a winner label, hidden by default. Shown by on_game_over(team).


VFX (src/vfx/)

BroadcastRippleVfx

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.


Build

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-release

The top-level Makefile's Gui bin target runs the cmake build and then exports a self-contained zappy_gui PCK + executable pair.