So… I have a simple turn based RPG, and after all actions are delegated, it goes to the battle phase (when characters smash each others):
(this is a resumed algorithm, the actual one is more complicated, but I can share it if needed)
IEnumerator BattlePhase (){
for (int i = 0; i < characters.Length; i++){
DamageApplication()
DestroyEnemyIfHPisZero(); //it checks ALL enemies on battle (and destroy if condition is met)
}
}
What should have happened is, supposing I have 4 characters in battle:
character 1 attacks and enemy HP becomes -3
enemy is destroyed
But what really happens is:
character 1 attacks and enemy HP becomes -3
character 2 attacks and enemy HP becomes -10
enemy is destroyed
For some reason, the enemy is being destroyed one loop after the one he indeed died (even if the second attack misses).
The curious thing is, if a put the DestroyEnemyIfHPisZero function inside Update () it works fine, they enemy is destroyed right after the attack!
So, is there a reason for IEnumerators calling the function later instead of calling at the time it was requested?
If the method is being called as a coroutine, I believe that after it yields for the first time, there are specific points where they can resume their execution. This mean like before FixedUpdate or after all Updates. If it is not a coroutine, I think we need to know why it is a IEnumerator to guess more reasons.
On a side note, once I participated in a project that had a bug like this, but the enemies would end up immortal instead of having a late death. The reason was that our procedures to deal the damage to the enemies depended on a state machine, and we were not checking the current state nor fixing the next state in our damage method, meaning that the enemy would only understand their death in the next update loop (by checking and updating the state machine).
I tried putting the destroy function in a lot of different parts of the coroutine, but all of them had the same result. I don’t want to overload Update with unnecessary validations, it would be perfect if I could do the check after each attack.
Guess we’ll need more information on the code then. The only reason I can think is that DestroyEnemyIfHPisZero() fails to detect a death because of a flag or value that will be updated only in the next call to DamageApplication(). Since it is a coroutine, it is more likely related to what you do before and after each yielding.
If the problem is that they die after the current round (not after the next round), right at the start of the next round, then the problem is more likely this:
More exactly, “Actual object destruction is always delayed until after the current Update loop, but will always be done before rendering.”. And then you learn a somewhat common but unusual pitfall.
The current expected behavior (what is actually happening) for your code is:
1 - First unit attacks one alive enemy, and we will assume that it dies
2 - Enemy is scheduled to destruction. However, there is no yield there, so on to the next loop.
3 - Loop until the next attacking player unit. If no more unit attacks, we will have a yield only after all units are evaluated (and then the enemy dies)
3.1 - A unit attack any other enemy, dead or not. Every possible attack result has on yield statement.
3.2 - At least one Update will be called, and after that, the enemy dies.
Is the above correct?
Possible solution: add another yield after the DestroyEnemy. This will put your implicit state machine out of synch with the ActionSelectPhase. Or you can add a “dead flag” to the enemy and make the unit change its target.
So… that could make sense, except for a detail: destroy is not delayed until the end of the loop, it happens DURING the loop, and yet a character attack late. Let me illustrate it:
The battle has 4 characters, and each attack is part of the “for” loop:
First one attacks and kills (but the object is still there), “for” goes to the next
Second one attacks the dead object, then it miraculously disappears
The remaining two attacks continues
As you can see, the destroy doesn’t wait until “for” is over, it simply happens a tick later.
Check the code two posts above, it will show what DamageApplication() is.
I might have written in a confusing way. The “Update loop” is the unity calling Update of your scripts, not a iteration of the for loop. Destroy is not delayed until the end of the iteration. It is delayed until the end of all Updates of all active objects in your running scene. Still, before rendering the screen. However, coroutines are synchronous, so the Unity engine won’t execute any Update call on scripts while your coroutine does not yield or end.
There is no yield between when the enemy is killed and the next attack of a player unit, so the enemy is effectively alive (but flagged for destruction). Since unity is still in your coroutine, it did not reach the point where it can remove the object yet. There is a yield of one second right before you check if the enemy died. You can put your call before that yield and see what happens.