Subsystem: Cities - Adam-Poppenheimer/Civ-Clone GitHub Wiki
Cities are one of the most complex subsystems in the game. The large set of tasks they perform are divided into a number of nested subsystems, some of which are more useful than others. It's also one of the oldest parts of the codebase, and is the most likely to have certain legacy problems from when the architecture of the program was less robust.
In Civilization 5, cities are the most important elements of the game. Cities are where resources are gathered, units are trained, and buildings are constructed. Cities handle the existence, distribution, growth, and decline of a civilization's population through numerous citizens. Cities determine a civ's cultural boundaries and can bring new territory under their control. Cities can be settled, captured, and razed by the actions of enemy civs. And lastly, a city acts as a special type of unit, capable of defending itself but incapable of dying in combat.
The Cities subsystem is centered on the core interface ICity and its standard implementation City. City itself is implemented as a MonoBehaviour to make it more convenient to destroy, but I don't think that's necessary since it uses nothing of that class except the OnDestroy() message. An ICity handles its population, its active project, and its yield focus, as well as its food and culture stockpiles. It reveals information about the next cell it wants to add to its boundaries, and also the IUnit that is uses to engage in combat. ICity also contains a number of public methods that correspond to different game actions a city is expected to do either once a turn or when prompted, prefixed with "Perform".
Cities are constructed via the Factory pattern. The process is quite involved, since cities must meet many conditions for their placement to be valid, some for game design reasons, others for logical ones. Since the entire set of cities often needs to be queried, CityFactory maintains a ReadOnlyCollection of all cities currently in the game.
City makes use of something akin to the Template Method design pattern. It performs a number of operations by calling into more complex helper classes like IYieldGenerationLogic or IWorkerDistributionLogic to deal with the more detailed aspects of all that a city needs to do. This was done both to prevent City from falling into the God Object anti-pattern and to facilitate unit testing. Many of the interfaces in the City subsystem are helpers of this nature and possess a single implementation.
Cities also make use of a generic interface called ICityModifier (and its corresponding CityModifier implementation). There exist a number of game elements which can modify some of the many tasks a city performs. A connection to the capital city, social policies, buildings within the city and in other parts of its owning civ can all change things like growth rate or production. CityModifiers are contained within the CityModifiers data structure. Determining the modifier for a particular city requires passing its interface into the modifier via the GetValueForCity() method. This separation was made to reduce the number of responsibilities the City class had, as the total number of modifiers is both high and subject to change (especially if the design were to be iterated upon). Not to mention that these modifiers were not core to the functionality of City. I'm not particularly satisfied with that modifier structure, however. It might be better modifiers to be data-driven rather than code-defined, so that hypothetical designers could add and make use of modifiers without changing the codebase. It was left as-is because there were higher priority things to work on.
The larger subsystems of City are given dedicated namespaces and are described in more detail below. City also handles a number of smaller tasks less core to the subsystem: Determining whether a city is connected to its capital, determining its net happiness, figuring out line-of-sight, providing local promotions to newly-constructed units, and handling combat. Some of the things assigned to the City subsystem, particularly how combat is handled, might've belonged to the Unit subsystem, or to another subsystem altogether.
Cities have an ICityConfig backed by a single implementation, CityConfig, which is a ScriptableObject with get-only properties backed by private fields with the [SerializeField] attribute.
Buildings
Buildings are objects that exist entirely within cities. They're constructed using the city's production process and can provide bonuses to the city, the units produced from it, and the civilization as a whole. They can also provide specialist slots that citizens can work for yield.
From a programmatic standpoint, Buildings are implemented as generic objects and constructed using the Factory pattern. IBuildingFactory defines a somewhat misleading Destroy method, as well, that sends BuildingDestroyed events that other classes can drop their references to them. The actual memory associated with the building, of course, is not deallocated until garbage collection.
The Building class is actually a bit of a stub, acting as little more than a POD wrapping important information from the much larger IBuildingTemplate, plus a collection for WorkerSlots assigned by BuildingFactory. Buildings are particular instances based around their BuildingTemplates, who then describe the behavior that building exhibits. BuildingTemplate is implemented as a ScriptableObject whose interface exposes read-only fields to ensure the template is not changed during runtime.
Programmatically, buildings can technically be moved from city to city via BuildingPossessionCanon, though the mechanics of the game itself don't support this.
Buildings have lots of complex bonuses and placement restrictions, most of which are handled by various Logic classes within the subsystem. These Logic classes connect to many different parts of the codebase to check for things like world-uniqueness (for wonders), map resource requirements, certain types of terrain, technology prerequisites, adjacent rivers, and the like. These are handled through IBuildingRestriction, an interface that has several implementations to check if an IBuildingTemplate is valid on a given city with a given owner. These are used by the BuildingProductionValidityLogic class to decide if it is game-logically appropriate for a building to exist in a particular city.
The notion of a Specialist, a slot from which a citizen can work, are also defined in buildings since only buildings have specialist slots.
Distribution
Distribution is one of several City subsystems that, while important, perhaps doesn't deserve its own namespace. It was created for organizational purposes very early on in development when it seemed like a more complicated issue and has remained as a legacy decision because it doesn't harm workflow or code cleanliness. Distribution concerns itself with the automatic assignment of citizens to a city's available worker slots. Essentially, a city gains WorkerSlots from the territory it possesses, which can then be worked by the citizens it contains. WorkerDistributionLogic seeks to assign workers to those slots to maximize a particular resource yield without generating too little food and causing starvation, or to maximize the total yield regardless of its type.
Growth
Another tiny namespace separated out early in the project, Growth handles the consumption of food by a city's citizens and whether that consumption leads to growth or starvation. Every citizen, by default, requires a certain amount of food per turn to survive (an amount configurable in CityConfig). If food yield exceeds food consumption, the city will add the excess to their stockpile. When the stockpile gets high enough, it will be cleared and a new citizen will be born, which can then be put to work. PopulationGrowthLogic handles the calculations for how large the stockpile needs to be for growth, how much of the city's excess food turns into proper growth (modified by happiness and buildings), how much food remains in the stockpile following growth (modified by buildings), and how much food is left in the stockpile after starvation. Modifications to City's properties based on these numbers is handled within City itself, as Logic classes are intended to be pure functions.
Production
This subsystem handles the creation of units and buildings from within a city.
Every city has an active production project settable by the player. When PerformProduction() is called, the city makes some amount of progress on that project based on the city's Production yield and its various Production modifiers. When the project's progress is greater than or equal to its cost, the project finishes, its Execute() method is called, and the City's ActiveProject is cleared.
There are two types of production projects: Units and Buildings. They share an IProductionProject interface and, somewhat unexpectedly, share the same implementation, ProductionProject. The processes are fairly similar and the ProductionProject class fairly straightforward, but it seems like a bad architectural decision overall. Future versions would likely want a UnitProductionProject and a BuildingProductionProject.
While it's fairly straightforward to calculate Production yield, figuring out the actual amount of progress added to a given ProductionProject is complex. The producing city possesses generic production modifiers itself. But there are also modifiers that apply only to units or buildings of a specific type, or apply to a specific set of buildings. Defining these modifiers in full and making them configurable without modifying code presented a vexing problem, and the current solution is haphazard. Buildings and Social Policies have a Unity-serializable ProductionModifier member that itself contains a list of Unity-serializable ProductionConditions. To determine whether a modifier applies, its conditions are checked on a given project for a given city. A JoinedTogetherBy field determines whether all the conditions must be true or only one of them must be met. The UI for this is process is, as one might expect, clunky, especially since it lacks a custom editor to make things clearer.
Overall, Cities.Production is a candidate for organizational refactoring, since it's brittle in the face of design changes and is sloppily assembled overall.
Territory
The Territory subsystem handles the acquisition of additional land, both where the next expansion candidate is, how much culture is required to seize it, and which cities control which cells.
When a city is created, it claims, for itself and its controlling civ, the HexCell on which it was placed and the 6 HexCells adjacent to that cell. During play, a city can flag as a target any unowned cell within a certain distance of itself and adjacent to a HexCell it already owns. If that city generates enough Culture in its stockpile, it will spend that stockpile to seize its target, adding it to its own territory and that of its controlling civ. This then gives said city access to the WorkerSlot on that tile, and the civ access to any resources on it.
Yield Generation
The somewhat unintuitively named YieldGeneration subsystem handles the produce of citizens working in cities: Food, Production, Gold, Culture, Science, and various Great Person points. It is named Yield rather than something like Resources because the game already has the notion of Strategic, Luxury, and Bonus Resources. "Resources" is also a special folder name in Unity which tells the editor to add to the build every file within every Resources folder. So after several iterations I settled on Yield.
Yield is produced when citizens occupy a City's WorkerSlots. Every WorkerSlot has a particular yield associated with it that is produced whenever it is occupied by some citizen and its parent city's PerformIncome() method is called.
Yield generation is complicated by two things: modifiers to a city's yield (of which there are many) and the special case of the city center. Modifiers are collected from buildings within the city itself and buildings in other cities. It's also modified by happiness and whether the civ is currently in a golden age.
City centers suppress the WorkerSlot of the HexCell beneath them and instead generate income on their own. The base value is configured at design time. During runtime, it's modified by city and civilization yield modifiers, by whether the city is garrisoned, and also by the population of the city (every person provides a configurable amount of science per turn by default).