Status
2025-12-03: This is on hold for now. I moved from Windows 11 to Linux and haven’t rebuilt my game-dev setup yet. The big unknown is the Unreal Engine toolchain: I still need to confirm it will build and compile cleanly on Linux for this project.
First, some videos
Basic city gen working, lots of decorations: https://www.youtube.com/watch?v=M50FAsi7-1w
A more playable demo: https://www.youtube.com/watch?v=4u57CENYcI4
- includes flight system 1:47 in and web swinging / super-speed 2:30 in
Game overview
Perpetual City is an open-world game set in a procedural city with no edge. Walk long enough in any direction and new districts keep forming. The vibe shifts as you move, so the city never settles into something you can fully “learn.”
Tonally, I keep thinking about Worm and the uneasy, reality-bending mood of Control. For Control it is not in a copy-the-plot way, more in the “something is off, and it might be watching you back” sense. For Worm, I’m tempted to just copy the plot… or at least how powers interact.
I’m planning for kind of rough like. Death is part of the loop, not rare. When you die, you return to a safe location with what you learned intact. You come back as another super-powered individual.
NPC behavior is intended to feel emergent instead of fully scripted. The world would react across runs: help one faction and another may turn hostile; a small intervention can kick off a bigger conflict; consequences can carry forward even when parts of the environment reshuffle.
I also want this to stay open-ended. It’s a hobby project I can keep growing without a finish line.
Key features
Dynamic exploration
- Explore an unbounded procedural city where new districts appear as you travel.
- Find hidden areas, anomalies, and resources that let you push farther.
Interactive NPC transformations
- Transform into NPCs and gain access to their internal dialogue and perspective.
- Use environment-triggered voice-over and internal narration to carry story context.
Narrative and lore
- A setting shaped by mysterious organizations, eldritch entities, and alternate realities, with touchstones like the SCP Foundation and Worm.
- Learn the world through fragmented clues found in quests, exploration, and NPC interaction.
Respawns and memory threads
- Death returns you to a safe location as a newly empowered NPC.
- Some memories persist via the “Memory Thread” mechanic, so progression can span multiple runs.
Strategic, variable gameplay
- Procedural quests, combat encounters, and moral choices that shift faction relationships and the city’s behavior.
- Powers tied to traumatic events that evolve across incarnations, so progression has both gameplay and narrative weight.
Perpetual City blends procedural exploration, a death-driven progression loop, and an evolving faction ecosystem inside an infinite city.
Attribute system: CREST
Characters are described through CREST. (Yes, I ripped this idea off Fallout.) I wanted a stat set that covers mind, weird power, physical grit, luck, and social pull without turning into a spreadsheet simulator.
- Cognition (C): mental acuity, perception, problem-solving
- Rapture (R): intensity and expression of otherworldly power
- Endurance (E): resilience and stamina
- Serendipity (S): luck and randomness, for better or worse
- Thrall (T): persuasion and the ability to command or influence others
Factions and world dynamics
The city has factions with distinct goals:
- The Watchers: researchers chasing explanations for anomalies and powers
- The Cult of the Eldritch: worshippers trying to appease the city’s creator
- The Resistance: rebels aiming to disrupt and overthrow the controlling entity
- The Technocrats: scientists and engineers attempting to harness the underlying systems
- The Nomads: survivalists who navigate the shifting city well
- The Guardians: protectors focused on order and safety
- The Merchants’ Guild: traders who manage the city’s economic lifelines
Gameplay experience
- Make moral and strategic decisions that carry consequences across runs.
- Build relationships (or grudges) in a world that keeps reacting to what you do.
- Balance planning and improvisation: manage resources, probe the unknown, and piece together why the city won’t end.
Coding details
PerpCity uses the UE5 runtime system: generate terrain and urban structure around the player as they move, with no pre-baked map. The target use case is an always-extending world for the project. Start anywhere, go any direction, and the environment forms on demand.
The system is three layers:
- Infinite terrain streaming with LOD control, so the world stays continuous and performance stays predictable.
- Roads and city blocks that respect terrain constraints (slope matters), so you get structure instead of random splines.
- Buildings with optional interiors, so the city is more than shells and facades.
A guiding idea that kept paying off: treat most layout as 2D geometry work (polygons, offsets, boolean operations), then lift it into 3D meshes only when it’s time to render.
What it produces at runtime
As you play, it generates:
- Terrain tiles around the player, with multiple LODs
- Road networks (currently targeting a specific detail level) grown through a rule-and-score expansion
- Simple city blocks by detecting closed road loops (early-stage for the tile system)
- Buildings spawned from block footprints:
- exterior shell, roofs, windows, doors
- optional floors and room subdivision
- interior decoration with walkability checks
This is what I’ve spent most of my time on.
Editor setup (no code access needed)
The tile-based runtime path is the current approach.
- Enable the PerpCity plugin.
- Place the World Manager actor in the level (this runs the system).
- Assign an auto-material for terrain (optionally with a scalar parameter used for tile fade/opacity).
- Press Play.
Key controls on the manager:
- Tile size (world-space size of each generated tile)
- Spawn radius (how far out tiles are kept alive)
- Per-frame work limits (caps on spawns and LOD changes per frame)
- Update cadence (how often the manager reevaluates tile needs)
Those knobs exist because the system is always balancing two competing goals: react quickly to player movement, and avoid frame spikes from doing too much geometry work at once.
How it works at runtime
1) The manager keeps tiles around the player
On a repeating cadence:
- Read the player position
- Map it onto a tile grid
- Expand outward in rings and decide:
- which tiles should exist
- what LOD each tile should use
Tile work is budgeted. Higher-detail work costs more, so you can keep responsiveness while keeping frame time from going off a cliff.
2) Each tile generates terrain, then evaluates buildability
When a tile initializes or changes LOD:
- Build procedural mesh terrain from multi-octave noise plus extra shaping (not just smooth hills)
- Evaluate local slope
- Group buildable areas into connected regions (reachable without exceeding slope limits)
This is what stops roads from happily marching up cliffs or fragmenting into isolated patches.
3) Roads grow through scored candidates
- Seed candidates from tile edges pointing inward
- Keep a priority queue of candidate segments
- Loop:
- take the best candidate
- rescore it against the current accepted network
- accept only if it passes spacing/collision rules
- spawn follow-up candidates from the endpoint (left, forward, right)
The score mixes things like:
- preference for useful segment length
- preference for connecting into existing structure
- penalties for being too close or running parallel to another road
In practice, this tends to create networks that “organize” instead of spraying random lines.
4) Blocks and buildings come from roads (still early)
Right now, block extraction is simple:
- Treat closed road loops as block polygons
- Feed each block polygon into the building pipeline
5) Buildings start from a 2D footprint polygon
Given a footprint:
- Clean the polygon (consistent winding, remove duplicates and near-collinear points, handle self-intersections as well as possible)
- Derive sidewalk/adjacent geometry around it
- Generate floors, optionally evolving the shape with height:
- scaling
- rotation
- optional shape features (triangular/hex-like variations, smoothing, periodic utility floors)
Each floor can then spawn interior logic: subdivision into rooms, walls/doors, decoration.
6) Rooms place objects while protecting walkable space
Interior decoration is treated like a constraint problem in 2D:
- Start with an “available floor area” polygon
- Each object subtracts an occupied polygon (blocked space)
- Some placements add regions that must remain connected
- After each placement, validate that the remaining navigable space:
- still exists
- stays connected as one polygon
- supports a minimum corridor width
Instead of building a full pathfinding graph for each attempt, the system uses polygon operations and offsets to test connectivity.
The geometry problems and the choices I made
PerpCity leans hard on 2D polygon math. The basic idea is: make layout decisions with robust geometric operations, then translate the result into meshes.
Polygon cleaning and consistency
Procedural inputs are messy: near-duplicates, collinear runs, self-crossings, inconsistent winding order. Utilities exist to:
- normalize orientation
- remove duplicate and near-collinear points
- clip protrusions and attempt to untangle self-crossings
Without this, offsets/booleans/triangulation get unstable.
Boolean ops and offsets that don’t fall apart on concave shapes
Hand-rolled clipping failed quickly. A robust polygon library handles:
- union, difference, intersection
- inset/outset (offset) used for:
- building setbacks
- wall thickness
- keep-out zones for object placement
- corridor-width validation
One practical detail: the library operates on integer coordinates. World units get converted into integer space. That improves robustness in some ways, but it introduces precision/tolerance headaches around intersections.
Holes, partitioning, triangulation
Offsets and booleans often create holes. Rendering wants something simpler.
A partitioning/triangulation library helps:
- represent an outer polygon plus holes with correct winding
- convert a shape with holes into a set of simple polygons
- triangulate when needed for mesh generation
This makes “complex footprint to renderable mesh” workable without reinventing polygon decomposition.
Collision testing for footprints and placement
For frequent checks like “does footprint A collide with B?”, a Separating Axis Theorem (SAT) approach works fast:
- project polygons onto edge normals
- test interval overlap
- allow a small tolerance
It’s fast enough for repeated checks, especially after the cleaning pass makes shapes closer to convex.
Walkability as polygon connectivity
One of my more proud tricks is traversability via polygon math geometry:
- subtract obstacles from the room polygon
- union in “must stay connected” regions
- offset points inward by corridor width
- determining ‘inward’ fast was actually a non-trivial problem on it’s on
- check if the result is one connected area or multiple islands
This answers “is the room still usable?” without constant nav-meshing.
Sliding an object until it hits something
For snug placement against walls without complex constraint solving:
- move the candidate polygon along a direction
- binary search the maximum safe distance using collision checks
Non-geometry issues I ran into and what I did about them
Stable frame time while generating heavy geometry
Procedural mesh creation, road growth, and interior decoration can spike. To address this I have dynamic frame budgets setup:
- cap spawning and LOD changes per frame
- assign higher “cost” to higher-detail operations
- spread expensive work across frames via cadence + throttles
Result: the system stays (mostly) interactive while streaming content.
I’m sure every AAA game has a way of doing this, and that I’m not really doing state-of-the-art… but most AAA games don’t dynamically create their meshes and interiors.
Road intersections: “touch” vs “cross”
Problem: junction logic is full of edge cases:
- collinearity
- endpoint touches
- near-parallel lines
- floating-point tolerance weirdness
Approach:
- separate rules for allowed endpoint touches vs disallowed mid-segment crossings
- when a crossing happens, trim the new segment back to the nearest intersection
- second pass that tries to split roads when an endpoint lands on another segment
Status: better than the initial version, but splitting still gets noisy in edge cases.
LOD blending flicker during transitions
Problem: opacity-based LOD transitions flickered.
Approach:
- keep the transition system in place
- force hard transitions for now while the root cause gets hunted down
This is a boring choice, but stable visuals beat fancy transitions that look broken.
Concave footprints and center-based assumptions
Problem: some footprint operations assume there’s a meaningful “center.” Concave and L-shaped polygons break that.
Approach:
- rely more on offsets and “largest remaining area” methods when possible
- document where center-based logic fails so it can be replaced later
What’s unfinished
Tile-based roads and blocks
- mid-segment intersection splitting is still unreliable
- some “road terminates on another road” cases need better handling
- closed-loop detection and block polygon extraction are still in progress
- blocks are not robustly trimmed against neighbors and tile borders yet
Buildings and interiors
- some footprint transformations behave poorly on concave shapes
- building generation can spike due to heavy per-floor work
- interior streaming transitions are rough
- decoration + walkability checks are expensive and need caching and better workload spreading
Persistence
- ID string conversion for persistent references is stubbed; save/restore is not complete yet
Playability
This isn’t actually a game yet. I have a bunch of game-like abilities (nav-map, NPC AI including shooting, bullet penetration simulation, superpowers)… but these are mostly just to entertain myself between coding.
Third-party and adapted components
PerpCity uses:
- simplex noise for terrain height generation
- a polygon boolean/offset library for 2D operations
- a polygon partition/triangulation library for holes and renderable triangles
- adapted code from an earlier procedural city project (license notes preserved in headers)
To-Do
- Go back into the code to get the names of these libraries