WildWeazel
Carthago Creanda Est
This is a thought experiment and case study to illustrate how we can structure the game logic using Components and Events.
First, to define terms. An Event is a function call indicating anything that happened that some other code may want to know about. These are built into the C# language, and to Godot as Signals. A Component is a class that implements a game mechanic or some utility functionality (like autosaves) that persists for the whole game session. They are ordinary C# classes stored in the ComponentManager so that they can be accessed from anywhere by type.
Let's consider one of the more complex sequences in the game: a city under siege by a stack of attacking units. The defender is the player as France, with a brand new veteran Musketeer UU and a Longbowman defending a walled town against some Trebuchets (which we'll assume do collateral damage) and Knights. The French fare very well.
The trebuchets bombard the city one by one:
Now the first Knight attacks, triggering a series of events as the CombatComponent resolves the battle (again calling into other components as necessary):
Some of these events may be superfluous, or only subscribed to by other objects outside the scope of this analysis. For example I ignored animations, which could be played in response to events or just based on state polling in the frame updates. In general I feel it's better to have too many events than too few. It doesn't hurt to raise an event that nobody is subscribed to. The choice whether to have components directly update each other or communicate between events is a bit fuzzy. I used events whenever crossing the UI-engine boundary, and when another component is not necessarily involved, but may react; direct function calls when the receiver inherently needs to update, or when return values are required.
First, to define terms. An Event is a function call indicating anything that happened that some other code may want to know about. These are built into the C# language, and to Godot as Signals. A Component is a class that implements a game mechanic or some utility functionality (like autosaves) that persists for the whole game session. They are ordinary C# classes stored in the ComponentManager so that they can be accessed from anywhere by type.
Let's consider one of the more complex sequences in the game: a city under siege by a stack of attacking units. The defender is the player as France, with a brand new veteran Musketeer UU and a Longbowman defending a walled town against some Trebuchets (which we'll assume do collateral damage) and Knights. The French fare very well.
The trebuchets bombard the city one by one:
- Each raises a UnitBombard event and calls into the CombatComponent (which in turn queries CityComponent, TerrainComponent, and UnitComponent for respective combat modifiers) to resolve the outcome.
- The Musketeer loses a total of 2 HP, raising a UnitHPLoss event each time
- The UI layer receives the event and displays the message banners
- On one bombardment, the CombatComponent calculates that collateral damage destroys the city walls, and directly updates the CityComponent which raises a BuildingDestroyed event
- The UI layer receives the event and displays the message banner, and updates the city graphic
Now the first Knight attacks, triggering a series of events as the CombatComponent resolves the battle (again calling into other components as necessary):
- CombatComponent raises UnitBattleBegin
- CombatComponent resolves the Longbowman's defensive bombardment, Knight loses 1HP and raises UnitHPLoss event
- CombatComponent raises UnitBattleRoundBegin
- CombatComponent resolves the round, Knight loses 1 HP and raises UnitHPLoss event
- CombatComponent raises UnitBattleRoundEnd
- Repeat until Knight is at 1HP
- During the last round resolution, Knight is able to retreat and raises UnitRetreat event
- A final UnitBattleRoundEnd, UnitDefeat (for the Knight), UnitVictory (for the Musketeer), and UnitBattleEnd events are raised
- During battle resolution, the Musketeer is promoted to Elite and raises UnitPromotion and UnitHPGain events
- The UI layer receives the UnitPromotion event and displays the message banner
- GoldenAgeComponent receives the UnitVictory event, sees that the unit has the "Triggers Golden Age" ability and the civ has not yet had a golden age, updates the civ's game state to indicate a golden age is in progress, and raises a CivGoldenAgeBegin event
- The UI layer recieves the CivGoldenAgeBegin event, sees that it is for the player's civ, and displays the domestic advisor popup
- During the last round resolution, the Knight is killed, raising a UnitDeath event
- As before, a final UnitBattleRoundEnd, UnitDefeat (for the Knight), UnitVictory (for the Musketeer), and UnitBattleEnd events are raised
- GreatLeaderComponent receives the UnitVictory event and sees that the unit is Elite, calculates that a Great Leader is made, tells UnitComponent to create a new Military Great Leader on the Musketeer's tile, and raises a UnitGreatVictory event
- UnitComponent creates the new MGL and raises a UnitCreated event
- The UI layer receives the UnitGreatVictory event and displays the message banner and military advisor popup
Some of these events may be superfluous, or only subscribed to by other objects outside the scope of this analysis. For example I ignored animations, which could be played in response to events or just based on state polling in the frame updates. In general I feel it's better to have too many events than too few. It doesn't hurt to raise an event that nobody is subscribed to. The choice whether to have components directly update each other or communicate between events is a bit fuzzy. I used events whenever crossing the UI-engine boundary, and when another component is not necessarily involved, but may react; direct function calls when the receiver inherently needs to update, or when return values are required.
Last edited: