3D maps: surface-graph (z-levels, ramps, overhangs), network sync, feature negotiation - #3
Draft
OddlyDoddly wants to merge 7 commits into
Draft
3D maps: surface-graph (z-levels, ramps, overhangs), network sync, feature negotiation#3OddlyDoddly wants to merge 7 commits into
OddlyDoddly wants to merge 7 commits into
Conversation
…ure negotiation This is PR #1 of the map-system 3D rework. It introduces the surface-graph (2.5D manifold) foundation without changing 2D gameplay: tCoord stays 2D and all existing per-grid physics is untouched. Each drivable surface owns its own planar eGrid, joined by portals, with z derived per-surface for rendering. Engine data model: - eSurface / ePortal (src/engine/eSurface.{h,cpp}): a chart wrapping an eGrid with a z-mapping (flat or fixed-slope ramp), footprint, and seam portals. - eWorld (src/engine/eWorld.{h,cpp}): surface registry. Surface 0 wraps the arena's primary grid, so existing eGrid* code is unchanged; ramps auto-stitch portals to the floors they meet (StitchRamp). - eGameObject gains surfaceId_ (default 0); gPlayerWall gains surfaceId_; gPortalWall (non-massive rim) marks portal seams. Map format 2.0: - resource/proto/map-2.0.dtd adds <Floor>, <Ramp>, <Building> primitives and a basement/ground/air level enum. Encoded as <Map version="200"> to avoid colliding with the existing version="2" zones format. - gParser parses the new primitives into surfaces + portals, gated on map>=200 AND the negotiated 3D feature; legacy maps are byte-identical. - Sample maps: test-2.0-ground (regression) and test-2.0-stacked (overhang). Feature negotiation: - New protocol version 23 ("0.5_3dmaps") and nFeatures (src/network), a named capability layer over nVersion/nVersionFeature. THREE_D is active only when every peer supports it; otherwise the session runs the 2D protocol and never serializes Z/surface data. Tests: src/test/eSurfaceTest.cpp exercises footprint containment, per-level floor lookup, ramp portal stitching, z-interpolation, and portal mapping. https://claude.ai/code/session_01NGicPQEn7UpifzRD2ouSfZ
Cycles now hand off between surfaces at portal seams. Because surface frames are identity (local == world), a crossing keeps (x,y) and only switches the grid, surfaceId, and derived z: - eWall gains a virtual Portal() (NULL by default); gPortalWall returns its ePortal. eGameObject::Move detects a portal at the crossed edge and, since portal walls are non-massive (no death -> "don't die on a slope"), swaps to the neighbour surface's grid, re-finds the face, and resumes the same step. - Parser draws ramp end-seams as non-massive portal walls and mirrors each seam into the adjacent floor's grid, so a cycle can drive on/off the ramp at the shared world seam; the ramp's long sides stay massive guard rails. - eWorld::Connect (public) creates+registers a portal on both surfaces. Legacy 2D maps have no portal walls, so Move() behaviour is unchanged.
Cycles and walls now carry their surface across the wire, but only when the THREE_D feature is negotiated on for everyone; otherwise the new fields are never serialized and the session stays byte-identical to the 2D protocol. - gCycle.proto/CycleSync: optional surface_id (12). Written from eGameObject::SurfaceId() in WriteSync, read back authoritatively in ReadSync (never derived from x,y -- overhangs make that ambiguous). - gWall.proto/PlayerWallSync: optional surface_id (12). gNetPlayerWall carries surfaceId_, set from its cycle at creation and propagated to gPlayerWall, so the renderer can draw each wall at the right height. Trail-wall collision is already per-surface because each wall is drawn into the cycle's current grid; the synced id is the authoritative correction/render hint.
Walls and trails now draw at the height of the surface they belong to: se_SurfaceBaseZ(surfaceId) looks up the surface's BaseZ via the arena's eWorld, and gNetPlayerWall::RenderNormal offsets its vertices by it. 2D walls (surfaceId 0) get z=0, so legacy maps are unchanged. This is applied in the legacy immediate-mode wall path. The modern instanced path (render/gl/gl_wall_instanced) needs a base-z added to WallInstance and its GLSL attribute layout; that, plus multi-height floor rendering, glass floors and building extrusion, is the integration surface for the in-flight Vulkan rewrite (it owns those files and shaders) and is left as a documented hook.
Buildings now carry their footprint, height, level, and style into the world (eWorld::Buildings) so the renderer can extrude them into Tron-Legacy blocks; their footprint is still drawn as a solid collision wall so they block cycles immediately. The stacked sample map gains a block tower, and the unit test covers building storage.
…ting-ptolemy-0g5qdb
… extrusion Now that the Vulkan backend is merged into this branch, complete the 3D render integration: - gl_wall_instanced: add a per-instance zbase (surface base height) to WallInstance, the GLSL vertex shader (location 6), and the attribute setup, so the default modern-GL instanced wall path draws walls/trails at their surface height. gWall.cpp sets inst.zbase from se_SurfaceBaseZ(). The legacy immediate path (used by the Vulkan renderer via the rRenderer Vertex interface) already carries the offset, so multi-level walls render under both backends. - parseBuilding extrudes the footprint side walls to the building's height (scaled by sizeMultiplier) instead of the default rim height, so Buildings render as Tron-Legacy blocks while still acting as solid obstacles. Verified: full autotools build links both with GL (default) and with --with-vulkan (vk_renderer/vk_context compile, SPIR-V generated, binary linked); surface unit test passes against the full client libs.
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.
Context
Armagetron is currently strictly 2D (
tCoordis x,y only; map<Point>is x,y; protobufs carry no Z; the floor renders at z=0). This PR adds a third dimension as a single change set: three z-levels (basement/ground/air) joined by fixed-angle ramps, with true vertical overhangs (tunnels-under-bridges, stacked highways), custom-shaped arenas, and Tron-Legacy building blocks.Architecture: surface-graph (2.5D manifold)
tCoordstays 2D — no engine-wide coordinate refactor. Each drivable surface (floor/ramp/tunnel) is its own planareGridchart plus a z-mapping; surfaces are stitched by portals. Within a surface, all existingeGrid/ movement / rubber / winding / wall code works unchanged.eGridis a planar DCEL and cannot represent overlapping(x,y), so overhangs require multiple grids + portals — which is also what maximizes reuse. Z is derived per-surface; frames are identity (local == world), so a portal crossing keeps(x,y)and only changes surface id and derived height.With the 3D feature off (and for all legacy maps), the game is byte-identical 2D.
What's implemented
Data model (
src/engine/eSurface.*,eWorld.*)eSurface/ePortal: a chart wrapping aneGridwith a z-mapping, footprint, and seam portals (with frame mapping).eWorld: surface registry; surface 0 wraps the arena's primary grid so existingeGrid*code is untouched. Buildings stored for extrusion.eGameObjectcarriessurfaceId_;gPlayerWall/gNetPlayerWallcarry it too;gPortalWallis a non-massive rim marking a seam.Map format 2.0 (
resource/proto/map-2.0.dtd)<Floor>(level + material grid/light/glass),<Ramp>(from/to level + direction),<Building>(footprint + height + style). Declared<Map version="200">to avoid colliding with the existingversion="2"zones format. Parser builds surfaces + auto-stitched portals, gated onmap>=200AND the negotiated 3D feature. Two sample maps (test-2.0-ground,test-2.0-stackedwith a real overhang + ramp + tower).Portal physics (
eGameObject::Move)Feature negotiation (
src/network/nFeatures.*, protocol v23)THREE_Dcapability overnVersion/nVersionFeature. It is active only when every peer supports it; otherwise the session runs the 2D protocol and never serializes Z/surface data. Mandatory features would reject the join.Network sync (feature-gated)
CycleSync.surface_idandPlayerWallSync.surface_idcarry the authoritative surface (never derived from x,y on the client). Trail-wall collision is already per-surface (each wall is drawn into the cycle's current grid).Rendering
se_SurfaceBaseZ). Buildings stored with footprint+height+style for extrusion.Verification
armagetronad_main+ dedicated).src/test/eSurfaceTest.cpp(14 checks) passes: footprint containment, per-level floor lookup at overlapping (x,y), ramp→floor portal stitching, z-interpolation, portal frame mapping, building storage.map-2.0.dtd(xmllint --valid).Boundary with the in-flight Vulkan rewrite
The GPU-side rendering of multi-height floors, glass floors (look-up effect), decals/road-signs, and building extrusion shaders lives in the modern renderer (
src/render/gl/*,se_RenderModernFloor, the instancedWallInstance+ GLSL) that the parallel graphics rewrite owns. This PR provides the data and engine hooks (surface base-z, material enum, building records, per-wall surface id) and applies height in the legacy wall path; the modern-renderer pieces are left as additive integration points (e.g. a base-z field onWallInstance) to avoid conflicting with that work and because they can't be visually verified in a headless build. An in-game map editor remains a separate follow-up; the data model and format are designed to support it.https://claude.ai/code/session_01NGicPQEn7UpifzRD2ouSfZ