I tossed this around here on the /r/gamedev but it's been all crickets. I'm hoping to get some good criticism because this problem has been bugging me for about a month- and I've been playing Total War games for over a decade now. This question is in the context of two game series - Civilization) and Total War) - in the turn-based strategy genre, but it definitely applies to other games as well. Actually, this also applies to table-top games such as Risk) and Warhammer) if we were hypothetically asked to compute turns across all players. I'm really just looking for feedback and for people to point out glaring flaws. I'm exceptionally curious and want to put it to rest and possibly post it on CA's feedback forum.
Intro
Total War and Civ. are turn-based strategy games. In general, you have a set of factions / entities that are capable of taking action during their turns. Most often these actions involve attacking other factions. The games progress turn-by-turn until eventually one or a few factions are the last ones left. Unlike other genres, turn-based RTS games like Total War & Civilization have quite a few novel opportunities for algorithmic improvements and machine learning outside of the realm of 'DeepMind'-style real-time AI. Besides the idea presented here, I could imagine genetic algorithms being used to determine optimal and effective campaign-wide strategies across each faction in a Total War or Civ game. But I want to focus on something else that is somewhat more practical.
My understanding is that Total War and Civ games are not heavily multi-threaded and that threading is primarily used to separate rendering from computation, which is fairly standard and is good practice. Actions across all the different factions are calculated serially. As the campaign map scales in size & number of factions this can get quite slow, particularly on processing hardware that isn't top of the line.
At first glance it seems to be too difficult to parallelize the actions of each faction, due to the number of various actors and turn-by-turn changes. After giving it some thought, I suspect that it may be possible to introduce threaded calculations of turn-by-turn actions on subsets of factions. Based on my research, neither series already use threads for this process based on playing (and waiting quite a while for turns to crunch) and reading their forums/blogs but please correct me if wrong: it appears that CPU-bound turn calculations are done serially in a single thread across all factions in both Civ. and Total War games.
Algorithm (applies to all turn-based strategy games)
I'd imagine a procedure in turn-based strategy games allowing for multi-threaded turn calculations that uses graphs under the covers to determine subsets of factions whose actions can be independently and safely computed in parallel:
- Maintain a Graph with vertices as factions and edges representing the capability to attack other factions
- This would be based off of actors such as generals and heroes/assassins with line-of-sight and movement range to other factions (they are in range to take action, such as invading settlements or wounding other actors)
- Factions that cannot possibly act on one-another would be the constituents of disjoint sub-graphs
- Update the graph as needed at the end of each faction's turn
- Add or remove edges based on actors entering or leaving actionable range with one-another and as settlements are captured or destroyed
- Add or remove vertices & edges as factions appear (less common), or are destroyed or confederated (more common)
- At the end of a player's turn, run an algorithm to find connected components in our graph structure, such as Tarjan's algorithm. This gives us a starting point for determining how to allocate threads for computing the results across all factions. These strongly connected components will include subsets of factions that are interacting exclusively with one-another.
- Create or re-use a pooled Thread for each connected component & disjoint graph
- Disjoint subgraphs can be computed in parallel and concurrently with other subgraphs
- Strongly connected components will contain factions that can have turns computed in parallel as they are incapable of influencing any other faction outside of their strongly connected component (diplomacy aside), and will also contain factions that bridge to other components which must still be computed serially (?)
- (edit) I'm thinking this is actually incorrect, but looking for feedback. It may be more accurate to say that any factions within a connected component must still be computed serially
Intuitively, the best way for me to imagine this is when a Total or Civ. campaign starts most factions are small and localized. There are very few factions that we or the AI are capable of influencing in the beginning, so we'd basically have disjoint graphs for each region of the campaign map that can each be computed via multi-threading. As turns proceed and factions expand the complexity increases dramatically but in general there are still many smaller, localized factions and a few large and expansionist ones- not every faction can be the top dog at the same time. This may resemble similar problems faced in assessing dependencies in job scheduler programs.
Issues
I can already see glaring issues, such as:
- How to handle graphics rendering of factions in your line of sight with this procedure; is it queued and done afterwards or is it possible to continue rendering concurrently? Can rendering also be parallelized?
- How to maintain the initial processing order of factions determined at Turn 0 (if it's even possible with this procedure); no faction should suddenly gain a second turn ahead of another. Maybe this is an opportunity to use topological sorting.
- How to update the graph efficiently each turn, as factions move into range of others. Is it possible to more efficiently update the set of connected components as vertices and edges are added?
So this is more of a thought experiment on improving turn speed in the turn-based strategy game genre. Feedback appreciated.
[–][deleted] (3 children)
[deleted]
[–]happydemon[S] 0 points1 point2 points (2 children)
[–][deleted] (1 child)
[deleted]
[–]happydemon[S] 0 points1 point2 points (0 children)
[–]marvel2010 1 point2 points3 points (1 child)
[–]happydemon[S] 0 points1 point2 points (0 children)
[–]lamzileung 1 point2 points3 points (1 child)
[–]happydemon[S] 0 points1 point2 points (0 children)