I'm trying to make a turn-based game like XCOM, and I've run into an architectural problem that I haven't been able to solve for quite a long time.
In my game, logic is completely separated from animations - when something happens in the game world, for example, a unit is given a command to move, then all the effects of this action are calculated, and the corresponding animations are just queued for playback.
But I don't understand how to build logic architecture taking into account various side effects properly. The usual depth-first approach, when one effect simply causes another, and so on in a cascade, is not quite suitable.
For example, there is the following scenario:
a unit makes a shot (action points are deducted, ammo is subtracted, trajectory is calculated)
the shot hits an explosive barrel. its health drops to zero and it "dies"
the barrel triggers the "death rattle" effect, and it explodes
all units and objects around the barrel take damage
all tiles around the barrel are engulfed in flames
each affected unit loses morale
some units and objects die from the damage they take
one of the destroyed objects is another barrel, and it also triggers the "death rattle", which leads to a chain reaction
panicked units start to play out their panic actions in turn - escape, shoot, which can also lead to chain reactions
in this scenario, dealing damage does not immediately lead directly to the death of a unit, and its death does not directly lead to the "death rattle", they have some kind of delayed effect. and it seems to me that the system must work in a layered style - first all damage is dealt, then all deaths, then all death rattles, etc.
However, there is another scenario in which this logic should work differently:
a unit makes a series of shots (a machine gun burst)
each of the shots hits some target, this target is immediately damaged, its morale drops, and some of the victims go into the "dead" status.
when all the shots are finished, the remaining effects are played - death rattle, unit panic, chain reactions
It is important here that some of the effects should happen immediately after the bullet "hits", and some after all of the "hits" - this is necessary for the correct animation order:
projectile animation 1
damage animation
death animation
projectile animation 2
damage animation
death animation
death rattle 1
death rattle 2
panic 1
panic 2
etc.
this is fundamentally different from the example with the explosion earlier, because the explosion from the point of view of time happens at one moment, while the line of shots is already stretched out in time.
the rest of the effects should be played as usual - after all the hits. because it would be strange if during the line some unit panicked and ran away.
I understand how to hardcode such a system, but it will result in a very large amount of work, and it seems to me that there is a more elegant approach here, which I can’t get to.
-
\$\begingroup\$ I was just listening to a Game Data Podcast interview with the developers of the recent Solium Infernum remake. They discuss how their "simulation core" handles processing commands and generating the next game state snapshot, along with "receipts" of each executed command and its effects. These receipts can nest, when one action triggers another reaction etc. Then the presentation layer can "play back" those receipts one by one to render the new game state. That sounds applicable to your situation? \$\endgroup\$DMGregory– DMGregory ♦2025年02月21日 14:58:27 +00:00Commented Feb 21 at 14:58
1 Answer 1
it seems to me that the system must work in a layered style - first all damage is dealt, then all deaths, then all death rattles, etc.
I think what you want is for these "layers" (I would call them "phases") to be something that can be manipulated explicitly so they can work differently in different cases.
Imagine you can create, within any effect logic function, a "scope" or "buffer" object which holds a kind of other effect to be processed later. (Its state is just a list/vector.) Functions which trigger effects (e.g. dealing damage, triggering death) accept as a parameter the buffer for those effects.
Then, whenever the side effects should occur, whether that is immediately (as in the machine gun) or at the end of the action (as in the explosion), you tell the buffer to execute all of the buffered effects, just before discarding the buffer. This may then trigger other events in other buffers.
function deal_damage(target: Unit, death_buffer: Buffer<DeathTrigger>) {
target.health -= 1;
if (target.health <= 0) {
death_buffer.add(new DeathTrigger(target));
}
}
function fire_machine_gun(...) {
for bullet in 0..10 {
// ...
let buffer = new Buffer<DeathTrigger>();
deal_damage(target, buffer);
buffer.execute();
}
}
function explode_barrel(...) {
let buffer = new Buffer<DeathTrigger>();
for target in find_units_in_range(...) {
deal_damage(target, buffer);
}
buffer.execute();
}
If you normally want effects to occur in depth-first fashion, then you might make all functions take optional buffers, where the lack of a buffer means to execute the effect immediately. Or, you might decide that providing buffers is mandatory, so that all pieces of the game logic must specify the ordering they intend.
I've suggested that buffers contain a single type of trigger, but you might instead decide to make "untyped" buffers that hold all possible kinds of triggers and sort them into some standard order (your "layers"). This might be less tedious to set up, but gives less enforced organization of how things happen.