This and the next few blog articles are about the story of a significant game modding project, where my amazing collaborators and I devoted to almost completely overhaul one of the core mechanics in city-builder games, road networks. The specific game is Cities: Skylines, the currently most popular city-builder on the PC platform. If you happen to be a player of it, be sure to check out our project on the Steam Workshop!
Introduction
Cities: Skylines Urban Road, or CSUR in short, is an asset/mod suite for Cities:Skylines providing unprecedented realism in road networks for city-building games. Under its hood is an offline procedural generation system based on a high-level road design API and Blender graphics backend. Therefore, the core of CSUR is a Python package generating game assets (Unity prefabs), and several plugins written in Unity/C# were also developed to modify relevant base game logics and convert asset sources into serialized prefab files. CSUR has enjoyed exceptional reception from the Cities: Skylines community and gained more than 35,000 cumulative users on the Steam Workshop. Below is a highway interchange I built in Cities: Skylines using the road network mechanics delivered by CSUR.
Procedural generation
CSUR is fundamentally a procedural generation framework. In game development, procedural generation refers to the technique of synthesizing game environments using computer algorithms. In a completely static game environment design workflow, all assets involved in the gameplay are modeled by 3D artists. This greatly hinders the extensivity of the gameplay because a more diverse environment would require proportionally more modeling and designing work. This is where procedural generation comes into play – by developing algorithms to generate maps, assets, or other game objects using elementary building blocks which can be readily modeled by artists. Procedually-generated environments can be as large as the earth (Minecraft), or even infinite (Temple Run).
Apart from making massive maps for adventure or generating countless RPG levels, I believe that procedural generation will play an increasingly important role in improving the realism and gameplay of simulation games. This is because
- It can bridge the gap between high-fidelity and user-friendliness. With minimal user input during the gameplay, realistic environments conforming to physical and engineering principles can be generated on the fly. (This will be elaborated in a future blog post using the example of road building.)
- It allows for simulating more extensive and more diverse environments. This is exemplified by the upcoming Microsoft Flight Simulator (2020). Using a combination of 3D reconstruction, procedural generation, and minimal handcrafted modeling, its levels of detail greatly exceeds any vehicle (planes, trains, trucks) simulation games ever published.
Before reborn
CSUR was conceived by a bunch of Cities: Skylines enthusiasts obsessed with roads and interchanges. Back into late 2017, the Road Editor feature was added to Cities: Skylines where community creators can make custom road assets. Not satisfied with the cartoonish appearance of road assets offered by the base game, creators on the Steam Workshop have made a great variety of custom road assets. To create a road asset for Cities: Skylines an artist needs (1) one or several straight road meshes with well-defined subdivisions for the game to bend into curves or create intersections and (2) customize the property values for their customized asset. The latter is particularly difficult for roads because they are strongly tied to the game’s traffic simulation.
In real-world cities, roads come in numerous different configurations including the width of the road, traffic modalities, and number of traffic lanes. Therefore, creating an exhaustive road collection within a unified appearance style requires tremendous amounts of effort from the artist. Before procedural generation kicked in, our extraordinary 3D artist and game designer, AmamIya, have created large amounts of detailed road assets and put them under the name of “Cities Skylines Urban Road”, a.k.a. CSUR. He also creatively addressed the game’s deficiency in simplifying all road transitions (e.g., to split 2 traffic lanes) into plain intersections by “baking” these transitions into separate road assets, which even further multipled the asset creation workload. In fact, the “CSUR” today as a software package was the successor of aforementioned “CSUR” as a collection of custom hand-made assets. This is why this project is entitled “CSUR Reborn” on the Steam Workshop, and it largely driven how the current implementation was designed.
With these background information in road asset creation for Cities: Skylines, our objectives for this project is clear:
- Graphics: to procedurally generate road meshes using elementary building blocks, such as lanes, medians, and barriers
- Mechanics: to synthesize property data for each road asset according to its configuration and intended game mechanics
- In-game: to automate the workload of asset creating using the game’s Asset Editor interface and to change the existing game functionalities for better support of the generated assets
A closer look
The CSUR project mainly consists of the following components:
-
A high-level API
core/
for the configuration of a road asset. The ultimate goal of CSUR is to generate any possible road configuration present in real-world cities. Therefore, it needs to be able to describe the composition of an urban road in its fundamental data structure. Besides, it assigns each road asset a unique name which is both human-readable and can be readily compiled back into road configuration data. -
A 3D graphics library
modeling/
which utilizes the Blender Python backend to procedurally generate road meshes. It can be potentially migrated to other graphics backends (e.g., PyMesh) as long as they support texture mapping and FBX I/O format. -
A sub-package
prefab/
which generates the prefab property data for each road asset based on its configurations. It takes JSON templates encoding common properties among road assets, e.g., traffic lights should be spawn at intersections, and outputs XML files to be further imported through in-game code. It also provides a command-line interface for users to generate their own asset in case the static collection released is not sufficient. -
A 2D graphics library
graphics/
based on PyCairo to create thumbnail images to visualize the configuration and functionality of each road asset in UI sprites. -
Scripts
builder/
to search for valid road configurations and build the list of road assets to be imported into the game. -
In-game Unity code (shipped as DLL binary at
bin/
) to automatically invoke Asset Editor sessions and method calls.
The dependency structure of the system was designed to be as decoupled as possible, so that some components can be run even without Blender or Cairo backends, such as generating the list of road assets for Steam Workshop release can be done straight from the Python shell. The below figure depicts how these components in the CSUR package are organized.
Online or offline?
An obvious question about the implementation of CSUR is: why wasn’t the entire thing done in Unity rather than having two disparate parts for one procedural generation system? Essentially, this is a question about choosing between online and offline in procedural generation. Offline generation means that all necessary game asset data including meshes, property values, and UI sprites for the gameplay are synthesized before the game level is loaded, while online generation requires these data to be computed on the fly. This is also fundamentally a long-standing problem in computer science: the tradeoff between time complexity and space complexity. Typically, an offline generation system will have these advantages and disadvantages (the opposite for online generation):
-
[√] Does not impact the performance when the framerate is mainly CPU bound. (Except for utilizing geometry shaders to generate meshes on the fly, which increase GPU workload instead.)
-
[√] Implementation and debugging is more convenient. Debugging entirely through playtesting can be a headache.
-
[×] Consumes large amounts of memory and storage, especially when large numbers of assets are generated and only a fraction of them are used by players.
-
[×] Incapable of generating stochastic or infinite environments. The result of the procedural generation is deterministic.
The short analysis above led to a retrospective on what we’ve achieved from the CSUR project. After months of development and release (the project was initially conceived in May 2019 and was released in January 2020), could we have done it in a better way?
A retrospective
Albeit being able to produce beautiful end results in city building, CSUR is far from the most popular mods for Cities: Skylines (the most downloaded mod in C:S has more than 1 million subscribers). Offline generation saves runtime computation at the cost of additional memory load, yet Cities: Skylines is among the extremely few games with memory bottlenecks. To create realistic cities, a player may download thousands of custom assets, and the game will attempt to load all of them at once even if the player only use a small fraction of them in a savegame. To make things worse, lots of custom assets lack proper performance optimization and use exceedingly large textures maps. As a result, heavily modded Cities: Skylines may exhaust all 64 GB of RAM which is the typical maximum for desktop CPUs and all 24 GB of VRAM in an NVIDIA Titan RTX. When the amount of assets loaded exceeds the available RAM space and page files, the game will crash at loading the savegame, making hours (true!) of wait at the loading screen in vain. Even though CSUR assets are already carefully optimized and share the same texture set among hundreds of meshes, the inevitable memory cost for storing geometries and level-of-detail (LOD) data still became the final straw of memory overflow. Consequentially, the single most common complaint about CSUR is it crashes the loading screen after one has subscribed to the assets. Nevertheless, some of the memory costs can be easily avoided with little computation – some road configurations have resulted in identical meshes only differing by a displacement on the x-axis! With these nearly duplicate assets loaded in to the RAM, lots of memory resources are wasted, and unfortunately memory is indeed precious in modded Cities: Skylines. If the entire project were to refactored, the emphasis would be undoubtedly be decomposing trivial and computationally intensive parts for the procedural generation and move all of the inexpensive computations to runtime. If a similar procedural road network system were to be implemented for a next-gen city-building game, an entirely online solution may even be the best choice.
Hope you’ve enjoyed reading this article, and if you’re also a game developer I hope what I learned from these experiences can be useful for your projects as well.