Citizens is my largest project to date, so this page may get a little long!
Citizens is a game in which the player doesn’t necessarily play. The idea is to simply observe, watching a world of NPCs simply existing and living their daily lives. It was built to be a backdrop for simple games which might require such a living world. In its current iteration, it consists of a procedurally generated city, populated by simple AI pedestrian and vehicle agents. Please be aware that this page is still a work-in-progress, and the project itself is still pending a final grade.
The world generation and aspects of the project’s organisation take inspiration from my work with modding Minecraft. Citizens is a 3D game, but for all intents and purposes the world can be considered 2D; consisting of a grid in which each space can contain one “tile”. Tiles are prefabs which are registered into the game and are all uniform in size, and could be something like a small building or a segment of road. The world is divided into chunks, inspired by Minecraft’s system, which are 16×16 tiles each. This allows for more sensible organisation as well as future improvements to world saving, loading, editing and performance improvements. Internally, a prefab chunk consisting of grass tiles is utilised as a base for the world to speed up generation, with one being instantiated to each chunk position for the initial world size. Here’s an image showing a 3×3 world; the yellow Gizmo lines are marking chunk boundaries:
World generation is controlled by a state machine, which iterates through generating chunks, generating roads, a multi-pass building generator with a priority system, creating Unity NavMeshes, and finally placing some initial agents to prepopulate a world.
The next state after the initial chunk generation introduces a simple road network to the world. This utilities a diminishing algorithm, where each road has a certain chance to create a junction and spawn a new road, and each new road spawned has half the chance of the previous road. This gives an overall good starting point – this gif shows road generation on a 3×3 world (A little slowed due to being a gif). Roads are modified later!
After roads are generated, the game iterates through the map and detects all the areas of grass, and saves them as “sections”. These sections are then used in a number of generation passes. First, the game adds any essential buildings, such as the hospital and town hall, and creates roads around them. Next, it finds any areas which are deemed too large and subdivides them into smaller streets. Finally, it places any other smaller buildings and procedurally generated buildings. This is all shown in the next gif:
Finally, the game creates all the navigational data (covered later), and creates some lists of locations to be utilised later. A full 3×3 world generation, including the data storage and placement of some initial vehicle and pedestrians would look like this:
There are two fundamental types of agents in the game; vehicles and pedestrians. Both of these at their core use Unity’s NavMesh system and a state machine to navigate, but beyond that work quite differently. Vehicles specifically are quite complex.
Vehicles utilise a secondary broader A-star mesh alongside their Unity NavMesh. To achieve this, a vehicle creates smaller Unity NavMesh paths between a series of nodes. These paths are precalculated at spawn time and saved to a list, so that when a vehicle reaches the next node, it can immediately transition to the next path with no recalculation time. This image (below left) shows a very simple agent path – the vehicle spawned at the top, and passes through three node controllers before despawning in the bottom-right. The overall A-star path is highlighted in blue (red squares indicate an untraversable location)
The other image (above right) is a close up of the junction node controller. Every junction node controller has input and output nodes for each available direction – in the case of this crossroad, all four directions. Nodes can be marked as give way, but generally the vehicle’s state machine dictates how a node would be handled. In this example, a vehicle approaches from the north and would stop at the give way node, checking both directions, then turning left (right from our perspective) to exit east. If for example a vehicle came in from the east and was turning left to go south, they would not need to stop, but would slow down to approach the turn. A vehicle crossing the east/west axis without turning wouldn’t even need to slow down, as they have right of way.
In addition to the junction nodes, there are also spawning and destination nodes. These again have a controller and often consist of two parts. The below image is from the same example as above; this is the final point where our vehicle will travel to before leaving the world. The vehicle first treats the junction node as normal, following the exit point towards the destination node. When approaching a destination node, the vehicle can perform a pre-check on what will happen next. A destination node can forward to another destination, immediately destroy the vehicle, forward the vehicle to a destruction point (which ideally would be out of sight of the player), or forward the vehicle to a parking space. In this case, the vehicle is forwarded to the end of the bridge where it’ll reach a destruction node – and upon reaching that, it is removed from the world.
Spawning works in a similar but simpler way; vehicles are spawned at a spawner node and then travel to a start point node. During this travel time, their destination is decided and the path is pre-calculated, meaning a vehicle can begin moving as soon as it enters the world and calculates its route while already travelling.
As mentioned above, vehicles themselves are controlled by a finite state machine. Vehicles will switch between around 12 states, controlling aspects such as waiting at a junction and checking if it’s clear to go, or waiting for another vehicle to move. When waiting for other vehicles, they also use a relatively simple fuzzy system to keep a reasonable distance and speed, emulating more realistic traffic where not every vehicle begins to move at once.
Below are a couple of videos showcasing these concepts. In the first video, a vehicle is spawned at a spawn point and travels across the world to a different destruction point. In the second, a vehicle which was pre-generated into the world travels to the hospital,
(please note small buildings are disabled in this video for visibility reasons)
Pedestrians overall work in a similar fashion. They utilise spawning and despawning nodes in the same way (albeit in different locations), and their objective is to navigate to a few shops before going to a house. One unique functionality to pedestrian agents is crossing a road. When an agent needs to cross a road, they will first check if there’s any crossings in the area – if so, they will prioritise these over crossing anywhere else, as they are considered safer. Once they’ve decided where to cross, they’ll look both ways, checking for vehicles before safely crossing the road. This is shown in the below gif, where the agent has moved to the crossing outside Burger Queen instead of just crossing the normal road outside TGI Tuesdays, even though they technically could cross there..
Citizens was created as the core element of my final year development project at De Montfort University. You can download the project to run yourself or browse the source code below:
Project Download: https://drive.google.com/file/d/1WfqP08bFpKZaQD5c00XFhOZK30sKhRtk/view?usp=sharing
Project Source Code: https://github.com/Fureniku/Citizens/tree/RC4 (built with Unity 2020.3.12f1)