Perpetual City - Game
Play it here
Status
2026-10-18 Changes
These are the changes over the past week.
The hero and powers
You now play as a visible, animated hero instead of a floating first-person camera. The hero has a set of superpowers you can switch on and off: a super jump, gliding, flight, super speed, and running up and along walls. Web swinging was rebuilt from scratch. Webs now stick to real building faces, a held web stays taut, and you can let go into a trick or a dive. You can also zip straight to a ledge. Loose street props tip over when you hit them instead of sliding away. A handful of follow-up fixes:
- you no longer zip to a perch hidden under a building;
- webs prefer anchors well above you;
- the hero sits properly on seats;
- the game rescues you if you ever end up stuck inside a wall.
Every control can now be rebound to any key or mouse button in Settings › Controls. If two actions clash, the game tells you and offers to swap them, and you can reset one row or everything. Every on-screen prompt and hint shows the key you actually bound, including the small key labels over armored enemies (“WEB FIRST [Q]”).
Fighting
Fighting now works like the Batman Arkham and Spider-Man games: you flow from enemy to enemy, striking, countering, dodging and stunning. It was built in two passes, the second modelled on the Dockside Freeflow prototype. That pass added enemies with their own kits, a director that decides who attacks when, gunmen, armored enemies and stealth takedowns. Hits flash and spark, and you can yank a distant enemy over to you with a web.
Several fight bugs were fixed:
- An enemy that sometimes showed up as a huge black box turned out to have a pistol five metres wide.
- Hit sparks no longer have dark squares behind them.
- The Brute’s counter now grabs, holds and throws properly.
- Letting go of the zip button keeps your momentum instead of stopping you on a ledge.
- The camera follows you smoothly when you’re knocked down instead of jumping.
Dialogue, voices and sound
You can walk up to people and talk to them (press E). Everyone has their own synthesised voice, and the city has its own made-up “city tongue”. Voices were made easier to hear over the traffic; the background noise dips while someone is talking.
There’s a police scanner with a dispatcher describing suspects, and taxis you can hail. Cars have a radio with three generated stations and a DJ who talks between songs. Every sound in the game now belongs to one of three volume sliders (voices, cars and ambient), checked by recording each one.
The story: The Lesser Key
The story was written up as a story bible and then built, act by act. In Act I you wake up in a stranger’s body and meet Hob, Slate, the collector and Bune, working through the first five quests with cornerstones, pocket spaces, contracts and death. An automated test plays it through from 20 starting points in 3 different cities, and several places where you could get stuck were fixed.
Acts II to IV followed:
- Act II: The Reader, with the Triangle, the Archive, the Ossuary and a way to catch lies.
- Act III: The Missing Name, with the seven Leaves, the Tower and the city’s text starting to corrupt.
- Act IV: The Convocation, with the siege.
There are five possible endings. The powers were also reworked so that each one comes from a deal with a patron.
A living city
The city is now divided into named districts (downtown, residential, uptown, industrial and nightlife), each with its own level of wealth, plus named streets. The names show under the clock and when you enter. Who’s out and what they drive depends on the district and the time of day. Taxis, buses, garbage trucks and ice-cream vans run their own routes.
Cars can be damaged. They dent, smoke, catch fire and explode, sometimes setting off the cars next to them. Fire engines and ambulances come to fires and accidents with sirens, hoses and paramedics, and other traffic steers round the wreck.
If someone sees you commit a crime, you get wanted stars. Police chase you in cars and on foot, and you can be arrested or shake them off. Each crime also nudges a hero-or-criminal score. At higher wanted levels you get a helicopter with a searchlight, roadblocks, SWAT and army vans. A respray shop can make your car disappear from their records.
You can now earn and spend money: taxi fares, courier jobs, street races against rivals, property that pays out, and pickups around the city. Disarmed gunmen drop pistols you can use, motorbikes are parked at the kerb, and you can fly the helicopters parked on tower roofs.
Things in the city now belong to people. If someone sees you take something it counts as stolen, and there are locks to pick, places you’re not supposed to be, an eye that shows when you’ve been noticed, and pockets to pick. Many residents have names, jobs, homes and daily routines, and they gossip about what’s happened. Their rooms fill and empty through the day, so breaking in is all about timing.
Buildings and interiors
Towers have working elevators, exit signs over the way out of every office room, furnished lobbies and canopies over the front doors. Rooms in towers no longer overlap each other, and you can always find your way out of a row house.
Look and art direction
The first rounds of art direction moved the game toward a “painterly urban realism” style:
- new lighting, sky and colour grading, with calmer materials;
- building details like cornices and shopfronts, and glass that reflects the sky;
- smoother-looking people and cars, leafier trees that glow when the sun is behind them, and classic street lamps;
- night scenes you can actually see in, with window lights that match up close and far away;
- offices with ceiling grids and lighter walls, and woods on the hills.
The sky got a full rework, with painted-looking clouds, city glow at night and overcast skies. Distant things fade into the haze naturally, clouds cast moving shadows, and you can fly up among the clouds.
A second art pass worked toward the new reference image, checked colour by colour:
- the sky and haze are brighter and bluer;
- shadows are a light blue instead of near-black, and the sun is warmer;
- trees are a softer green by day and dark at night, so the street lamps light them up;
- office windows glow warm at night;
- office floors are no longer almost black.
None of this cost any frame rate.
Public sculptures now turn up around the city: in parks, on traffic islands, standing in ponds, in tower lobbies and on corners outside towers. There are 14 kinds with 4 variations each, in six colour schemes. You can walk into them, and they cast shadows.
Every tower now belongs to a company and every shop to a business, each with a generated name and logo. Company logos sit near the tops of towers and light up at night. Shop signs show the business’s logo and name instead of a generic word like “DELI”. Every district has its own flag, and it hangs as a banner from about half the street lamps. A bug that made the banners and logos disappear once you left the starting area has been fixed.
World generation
The city now runs all the way to the horizon. Distant parts of the city are generated in a lighter, simplified form, along with the terrain and a skyline beyond that, and they hand over smoothly to the detailed city as you get closer. Stand-in skyline buildings no longer pop up where real buildings about to appear. Generating the city also got about three times faster without changing what it makes.
Each city now has its own rivers, lakes and park ponds, with the ground shaped around them. Roads and keep clear of the water, and there are riverside drives, bridges, quays and stone-rimmed ponds. The water reaches the horizon and shows on the map. If you swim out of your depth you’re pulled back to shore, and a car driven into deep water is lost.
Days and nights now have a more natural rhythm. The sun is up for about 15 game hours and down for 9, and it lingers at dawn and dusk. You can also use the real sun’s timing for a chosen place and date.
Interface
The pause menu has proper Settings and Debug windows. Pressing Tab opens a full map of the generated city, from whole neighbourhoods down to single buildings, with floor plans when you’re indoors. The minimap was restyled to match. A new Save window on the main menu lets you download a backup of your game, load one back in, or start completely fresh. The title screen now reads “The perpetually endless procedural city.”
Performance
The game used to stutter while shaders were prepared mid-play, and the main thread was what held frames back. Shaders are now all prepared while the game loads. Shadows far from the camera update every other frame, and a lot of repeated work each frame was cut. On the test machine:
- a typical street view went from 9.0 to 6.4 ms per frame;
- web-swinging is noticeably smoother, with its worst frames (the slowest 5%) down from about 30 ms to about 23 ms.
The automatic resolution scaling now only lowers the resolution when the graphics card is what’s holding things back. Before, it could blur the picture without making the game any faster.
For the iPhone crashes and the slow laptop:
- Benchmark: it now reports results you can copy and paste, including runs that show whether the graphics card or the processor is the bottleneck.
- Memory: usage is now measured and shown on the debug screen. The biggest user turned out to be the game’s nine background workers.
- Memory fixes: several fixes now apply on every quality setting, with no visible change.
- Potato: a new setting for phones and weak laptops. It runs at a quarter of the pixels, with no effects, a shorter view distance, fewer workers and a 30 fps cap, and uses about half the memory of Low.
- After a crash: the next start asks whether you’d like to load in Potato. It never changes your setting without asking.
Other
2026-09-26: ported to js as a test with Opus 5.5: here
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
- Maybe make PerpCity an off-brand stargate game - it can generate infinite planet/cities instead of one infinite one