I’m working on a realtime roguelike-ish game, where I want all NPC actions to occur in “ticks”, such that everyone gets a single tick (and thus, a chance to act) every “turn”. If it were a turn-based game I would just retain a list of everything registered with Tick, and iterate through it one-by-one, but since it occurs in real time, using a coroutine that fires every second would result in one giant lag spike every time it executed.
One alternative would be to keep track of an int index for every list I’m iterating through, then every update, just execute myList[currentIndex].Tick if the index is in range, and reset it to 0 if it’s not.
The obvious problem there is that this ties my timestep to the number of items in my biggest list; a game with two NPCs and five turrets will complete one full “turn” every 5 updates, but the same game with 20 NPCs would only complete a turn every 20 updates. That sort of variability leads to tons of weird edge cases and balance issues that I’d rather avoid, so using some kind of a fixed timestep, like “Every turn takes one second, during that second every entity will tick once”.
So is there an obvious way to pull this off that I’m missing?
So you want to fire the event on a bunch of enemies once per {time period}, but you don’t want them all to be fired on the same frame?
A pair of coroutines might serve you well here. You have your major loop that executes once per second, which call your minor loop which loops through your enemies and executes them one per frame or enough per frame so that they’re all executed within 1 second.
I think the following code will do this:
public float turnTime = 1f;
public List<Enemy> enemies; //populate as needed
void Start() {
StartCoroutine(MainTurnLoop() );
}
IEnumerator MainTurnLoop() {
while (true) {
StartCoroutine(ExecuteTurns() );
yield return new WaitForSeconds(turnTime);
}
}
IEnumerator ExecuteTurn() {
Enemy[] thisTurnEnemies = enemies.ToArray(); //cache the enemy list at the moment the turn starts, because changes to this list can make this algorithm unpredictable
float turnStartTime = Time.time;
for (int e=0;e<thisTurnEnemies.Length;e++) {
if (thisTurnEnemies[e] != null) //just in case one gets destroyed earlier in the turn
thisTurnEnemies[e].ExecuteTurn();
float turnProgressTime = (Time.time - turnStartTime) / turnTime; //normalized 0-to-1 across the turn
float turnProgressEnemies = ((float)e / (float)thisTurnEnemies.Length);
if (turnProgressEnemies >= turnProgressTime) {
yield return 0; //wait a frame
}
}
}
That works absolutely perfectly, thank you so much for taking the time to create an example! The core idea is great, but I’m pretty sure it still would’ve taken me an embarrassingly long time to iron out without the accompanying code
Assuming I scale this later to work with multiple lists of entities (e.g. List, List, List and so forth), does it make any performance difference at all whether I use multiple coroutines (one secondary loop per list), or a single coroutine with multiple loops? My gut says that fewer coroutines is preferable just in terms of cleanliness, but I’m sure I’m prematurely optimizing either way.
I would probably suggest making all your enemies, environmental, and cheese class derive from a single TurnTakingObject class, and pool them all into a single List. It’s simpler and cleaner, and guarantees that all their updates are spread across frames evenly.
Ex: if you had 4 Enemies, 4 Environmental, and 4 Cheese, with separate lists you’d run the risk that 3x objects would be executed on the same frame 4 times per second, as opposed to spreading them across 12 different frames.
Having multiple coroutines is slightly less efficient (coroutines create a bit of garbage), but on the scale we’re talking about it’s hardly enough to be worth mentioning.