Bug - repeated respawns when being attacked by multiple enemies

So I don’t know why no one else is encountering this. The closest example was this bug but it was only for projectiles.

So what happens is

  1. Enemy 1’s Fighter.Hit is called via the animation event
  2. This calls Health.TakeDamage() on the player
  3. The player is not dead yet so the player takes damage then dies. The die event triggers
  4. Respawner picks this up and starts the respawn routine which heals the player and begins fading out
  5. The respawn routine ends
  6. The next enemy’s animation event triggers. It seems because this was triggered before the frame ended it takes place as if everyone is still in their non-prespawned location. Because the player was healed that means the player can take damage again, which then triggers the die event again which triggers respawner again which includes another fade out, heal, etc
  7. The next enemy’s animation event triggers… you see where this is going :slight_smile:

So what it seems we need is some way to mark the character as being in a “respawning” state so that the next Hit() events from the enemies have no effect.

Or maybe we have to delay the player’s heal with a coroutine so that it happens in a future frame thus making an upcoming Health.TakeDamage() terminate early.

I have to admit the mix of coroutines and animation events creates a code flow that is a little tough to fully diagnose.

I tried the following trick (see first line of RespawnRoutine) to cancel all the enemy attacks but it seems the update loop then gives them another target again. So I’m thinking we need some sort of two stage action to entirely suspend the enemy AIControllers and only reenable them once we’re sure the respawn routine has completely concluded.

But I’m also more than willing to bet I have some other bug somewhere.

private IEnumerator RespawnRoutine()
        {
            CancelEnemyActions();
            SavingWrapper savingWrapper = FindObjectOfType<SavingWrapper>();
            savingWrapper.Save();
            yield return new WaitForSeconds(respawnDelay);
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeTime);
            RespawnPlayer();
            ResetEnemies();
            savingWrapper.Save();
            yield return fader.FadeIn(fadeTime);
        }

        private void CancelEnemyActions()
        {
            foreach (AIController enemyController in FindObjectsOfType<AIController>())
            {
                Health health = enemyController.GetComponent<Health>();
                if (health && !health.IsDead())
                {
                    health.GetComponent<ActionScheduler>().CancelCurrentAction();
                }
            }
        }

Just imagine how this would play out in the cinema… John Wick takes a fatal shot respawning in front fo the next enemy who kills him again until the bad guys run out of bullets… (ok, maybe not a great movie plot after all).

I encountered something similar to this when I was working on my procedurally generated game. My solution was far less complicated, I simply made it so that the initial spawn location for each procedural level was far enough from the zone where enemies appeared that the respawning player was safe from harms way.

That’s not always an option…

I think you’re on the right track here, Maybe add something to AIController to cause it to stop thinking for a brief period of time.

        public void Reset()
        {
            NavMeshAgent navMeshAgent = GetComponent<NavMeshAgent>();
            navMeshAgent.Warp(guardPosition.value);
            timeSinceLastSawPlayer = Mathf.Infinity;
            timeSinceArrivedAtWaypoint = Mathf.Infinity;
            timeSinceAggrevated = Mathf.Infinity;
            currentWaypointIndex = 0;
            frozen = true;
            GetComponent<Health>().takeDamage.AddListener(UnFreezeController);
            Invoke(nameof(UnFreezeController), 1f);
        }

        void UnFreezeController(float _)
        {
            frozen = false;
            GetComponent<Health>().takeDamage.RemoveListener(UnFreezeController);
        }

Then start Update() with

if(frozen) return;

Thanks. OK here’s how I solved it. I made a small change to my prior code and then I had to make a change to your code. Invoke() wasn’t finding the UnFreezeController method since the method signature was different. Not even void UnFreezeController(float _ = 0) worked so I had to explicitly define the method.

It is curious why no one else encountered this since I’m pretty sure I just used the course code.

Respawner:

        private IEnumerator RespawnRoutine()
        {
            FreezeEnemyAI();
            SavingWrapper savingWrapper = FindObjectOfType<SavingWrapper>();
            savingWrapper.Save();
            yield return new WaitForSeconds(respawnDelay);
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeTime);
            RespawnPlayer();
            ResetEnemies();
            savingWrapper.Save();
            yield return fader.FadeIn(fadeTime);
        }

        private void FreezeEnemyAI()
        {
            foreach (AIController enemyController in FindObjectsOfType<AIController>())
            {
                Health health = enemyController.GetComponent<Health>();
                if (health && !health.IsDead())
                {
                    enemyController.Freeze();
                }
            }
        }

AIController

        private void Update()
        {
            if(frozen) return;
            //rest same as before
        }        

        public void Freeze()
        {
            SuspicionBehaviour();
            timeSinceLastSawPlayer = Mathf.Infinity;
            timeSinceArrivedAtWaypoint = Mathf.Infinity;
            timeSinceAggravated = Mathf.Infinity;
            currentWaypointIndex = 0;
            frozen = true;
        }

        public void Reset()
        {
            NavMeshAgent navMeshAgent = GetComponent<NavMeshAgent>();
            navMeshAgent.Warp(guardPosition.value);
            Freeze();
            GetComponent<Health>().CharacterTookDamage += UnFreezeController;
            Invoke(nameof(UnFreezeController), 1f);
        }

        private void UnFreezeController()
        {
            UnFreezeController(0);
        }

        private void UnFreezeController(float _)
        {
            frozen = false;
            GetComponent<Health>().CharacterTookDamage -= UnFreezeController;
        }

No one else mentioning it and noone else encountering it are two entirely different things. Up to 1000 students a month sign up for the RPG series (actually sometimes many more!) but the vast majority never ask for help or comment on the forums.

I’m wondering if switching the order of these two statements might make a difference…

I feel so dumb. I found what the issue is. My player character has health regeneration in update and I forgot to stop health regeneration if the character is already dead. It’s a really tiny amount at lower levels but enough to wreck havoc with some of the checks

Swapping the order of those two has no effect. It’s all about freezing health regeneration.

So you’re saying that your player is a vampire, and you forgot to stake him in the heart?

1 Like

I was thinking maybe closer to Wolverine. :slight_smile:

In all seriousness, I was looking to keep the gameplay very simple on a mobile device so I wanted to dispense with unnecessary controls like potions and keep the skill element more closely tied to maneuvering and dodging.

But I’m sure you know this well - simpler for user often means harder for the developer.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms