Screen going perm-black when transitioning from 'Resume Game' to Gameplay [BUG]

Hi all. Alright so at the moment, I have 2 big bugs in my game that definitely need to be dealt with as soon as possible:

  • The main bug is my ‘Resume’ button leads to a black (black for me) screen and never goes back to the normal alpha value (i.e: doesn’t show me the game). From the debugger and my mouse UI, the game works… just the screen stays black due to a Respawner issue. I’ll paste the stack trace below:
NullReferenceException: Object reference not set to an instance of an object
RPG.Respawnables.RespawnManager.Respawn () (at Assets/Project Backup/Scripts/Respawnables/RespawnManager.cs:103)
RPG.Respawnables.RespawnManager.Start () (at Assets/Project Backup/Scripts/Respawnables/RespawnManager.cs:47)

Here is the RespawnManager Script the Stack Trace is leading to:

using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using RPG.Respawnables; // Test: Delete if failed

namespace RPG.Respawnables
{

    // Since SaveableEntity relies on the ISaveable information of each character to
    // bundle it into our Save file, there are cases when our enemies' state is non-existant
    // hence we need to create a brand new RespawnManager class, as shown below:

    public class RespawnManager : SaveableEntity // in Saving below, we will have to replace the logic, hence we inherit from 'SaveableEntity.cs'
    
    {

        // This class will handle the following:

        // spawning our enemy to the scene
        // listen to death notifications (i.e: Enemy is dead, proceed to keeping him dead for the following steps)
        // hide the body after 'hideTime'
        // deleting the enemy after 'hideTime' is complete
        // Respawn the enemy after 'respawnTime'
    
        [SerializeField] AIController spawnableEnemy;   // the enemy to spawn/respawn
        [HideInInspector] private AIController lastSpawnableEnemy;  // this AI Controller ensures its an enemy we are respawning
        [SerializeField] private float hideTime = 60;   // time before hiding our dead character
        [SerializeField] private float respawnTime = 90;    // time before respawning our hidden dead character, as an alive character
        [SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
        [SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
        [SerializeField] bool hasBeenRestored;  // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not

        /* // TEST Variables: Delete if failed
        [SerializeField] AggroGroup aggroGroup; // the group of enemies we want attacking our player, if things go bad
        [SerializeField] DialogueAggro dialogueAggro; */

        private AIController spawnedEnemy;

        // --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to ensure everything works accordingly --------------

        private void Start()
        {
            // Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
            if (!hasBeenRestored) Respawn(); // LINE 47: Stack trace leads to here

        }

        private void Respawn()
        {

            /* // TEST: Delete if failed
            if (aggroGroup != null) {

                dialogueAggro.SetAggroGroup();
                aggroGroup.AddFighter(spawnedEnemy.GetComponent<Fighter>());

            } */

            if (spawnedEnemy)
            {

                // Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
                spawnedEnemy.GetComponent<Health>().onDie.RemoveListener(OnDeath);

            }

            foreach (Transform child in transform)
            {
                // Start the Respawn by deleting any existing gameObjects
                Destroy(child.gameObject);
            }

            // Respawn the enemy, and parent the enemy to our respawnManagers' transform
            spawnedEnemy = Instantiate(spawnableEnemy, transform);

            // Get the spawned/respawned enemies' health, and listen for death notifications
            spawnedEnemy.GetComponent<Health>().onDie.AddListener(OnDeath);

             if (patrolPath != null)
            
            {
                Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
                spawnedEnemy.AssignPatrolPath(patrolPath);
            }

            else
            
            {
                Debug.Log($"No Patrol Path to assign");
            }

            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------

            if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro) && aggroGroup != null) {

                dialogueAggro.SetAggroGroup(aggroGroup);

            }

            aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>()); // LINE 103 (the Stack Trace leads here)

            // ---------------------------------------------------------------------------------------------------------

        }

        void HideCharacter()
        {
            // Hide the dead character
            foreach (Renderer renderer in spawnedEnemy.GetComponentsInChildren<Renderer>())
            {
                renderer.enabled = false;
            }
        }

        void OnDeath()
        {

            // hide the character after 'hideTime', and then respawn him after 'respawnTime'
            Invoke(nameof(HideCharacter), hideTime);
            Invoke(nameof(Respawn), respawnTime);

            /* // TEST: Delete if failed
            if (aggroGroup != null) {

                aggroGroup.RemoveFighter(spawnedEnemy.GetComponent<Fighter>());

            } */

        }

        public void WaitingPeriod() {

            // Get a little suspicious before patrolling again
            spawnedEnemy.SuspicionBehaviour();

        }

        public override object CaptureState()
        {
            var state = new Dictionary<string, object>();
            foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
                state[saveable.GetType().ToString()] = saveable.CaptureState();
            return state;
        }


        public override void RestoreState(object state)
        {

            // Fixing the FadeIn issue, from both loading and starting the game (because 'RestoreState()' occurs before 'Start()'):
            Respawn();

            var stateDict = (Dictionary<string, object>)state;
            foreach (var saveable in spawnedEnemy.GetComponents<ISaveable>())
            {
                var typeString = saveable.GetType().ToString();
                if (stateDict.ContainsKey(typeString)) saveable.RestoreState(stateDict[typeString]);
            }

            if (spawnedEnemy.GetComponent<Health>().IsDead())
            {
                OnDeath();
            }
        }

        private void OnValidate()
        {

            // This function checks if we changed the Spawnable Character
            // (ensuring any character we attach to the spawnableEnemy is automatically added
            // to the RespawnManager when editing the game, 
            // hence we dont accidentally spawn the wrong dude at the right spot)

            if (spawnableEnemy != lastSpawnableEnemy)
            {
                lastSpawnableEnemy = spawnableEnemy;
                
                foreach (Transform child in transform) {

                    Destroy(child.gameObject);

                }

                Instantiate(spawnableEnemy, transform);

            }
        }
    }
}



// THE ARCHER AND ENEMY 2 ARE BOTH CHILDREN OF THE 'CHARACTER' PREFAB. DISCONNECT THEM FROM THAT, OTHERWISE THEY'LL KEEP INSTANTIATING
// FROM UNDER THE QUEST GIVER, WHO IS ALSO A PREFAB FROM THE CHARACTER PREFAB!!!

is not related to the first bug, so shouldn’t be here…

Fair enough :stuck_out_tongue_winking_eye:
Edit: I added two comments at the lines the stack traces lead to, for simplicity’s sake (Line 103 is the ‘aggroGroup.AddFighterToGroup(…)’ line)

Hehe, I was just working up a full reply going through each line where a statement that could cause an NRE was made in Respawn(), to have you look at each one and ask what was different about one of them… I had a good idea which line the error was on, though…

So take a look at all the other lines within respawn that act on a global variable… are they wrapped with anything in particular?

What is the aggroGroup.AddFighterToGroup() method NOT wrapped in?

NRE is Null Reference Exception… right?

Pulls an Uno Reverse card

Jokes aside, well… there’s a large number of ‘if’ statements and ‘foreach’ loops. Is that something we should include our aggroGroup.AddFighterToGroup into as well?

OK I fixed it lel. Apparently I looked at the top of my respawn function, where I said “Test: Delete if failed”, and found out a very similar-looking function. Curiosity got the best of me, I placed the line in the ‘if’ statement (at the bottom one) and got it to work :stuck_out_tongue_winking_eye: (time to look at the second error I have)

OK umm… there’s a follow up problem:

Placing the ‘aggroGroup.AddFighterToGroup()’ in the ‘if’ statement means the guards in the aggroGroup can now be freely attacked (they’re not restrained by their aggroGroup, which is the effect we wanted back then), making the aggroGroup useless, and placing it outside of the ‘if’ statement causes the black screen issue

Is there a middle point to fix both issues without having them conflict against one another?

Is your aggroGroup assigned in the RespawnManager???

you mean for my enemies in the scene hierarchy? Yes they are

So somehow, at some point, RespawnManager.aggroGroup is getting cleared, or the Aggrogroup is being destroyed… I can’t find anything in the RespawnManager script that is re-assigning the aggrogroup to null or destroying the aggrogroup object itself…

hmm… any other potential solutions you can think of?

If it helps in anyway, my aggroGroup now only includes the dialogue guard and not the other guards for some reason when the game starts

Show me the inspector for the RespawnManager of a guard that should be in the AggroGroup, but is now not in the aggrogroup…

Sure thing, here you go (if I recall correctly, shifting the addFighterToGroup(…) line out of the if statement makes him unattackable unless the group is triggered via dialogue, which is not the case when the line is shifted into the ‘if’ statement)

Show me the if statement…

This is what it looks like (to fix the black screen issue). P.S: I’m using the FIRST One:

if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro) && aggroGroup != null) {

                // if we get a dialogueAggro, and there is an aggroGroup that needs setup too, then:
                // 1. Set the Aggro Group up
                // 2. Add the linked fighters to the group

                dialogueAggro.SetAggroGroup(aggroGroup);    // setting the AggroGroup up
                aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>()); // if this line does not stay in the 'if' statement, then the 'Resume' button in the Main Menu,
                                                                                    // which supposedly leads to the game again, will only lead to a black screen, and give me nightmares

            }

And this is what it looks like to get the aggroGroup guards to act properly:

if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro) && aggroGroup != null) {

                // if we get a dialogueAggro, and there is an aggroGroup that needs setup too, then:
                // 1. Set the Aggro Group up
                // 2. Add the linked fighters to the group

                dialogueAggro.SetAggroGroup(aggroGroup);    // setting the AggroGroup up
                
            }

            aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>()); // if this line does not stay in the 'if' statement, then the 'Resume' button in the Main Menu,
                                                                                // which supposedly leads to the game again, will only lead to a black screen, and give me nightmares

Ok, so this means that only characters with a DialogueAggro would get added to the AggroGroup, but the only characters we want to have a DialogueAggro are the speakers…

Try this:

if(aggroGroup!=null)
{
    aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>());
    if(spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
    {
         dialogueAggro.SetAggroGroup(aggroGroup);
    }
}

So nothing was deleting the Aggrogroup after all, it just was getting blocked when it wasn’t a Dialogue character.

Two birds with one stone… Yup, that script made it work (if there’s time for a short question, what was the difference though? What did re-organizing the block help solve?)

If the Respawn manager has an AggroGroup, then you always want to add the character to the AggroGroup… In the first script, this wouldn’t happen because if there was no DialogueAggro, then we wouldn’t even be checking at all to see if there was an AggroGroup, we’d just say “oh, nevermind then”.

So we start with that premise… if you have an aggrogroup, then you need to add the fighter to the group, regardless of whether or not there was a DialogueAggro.

If you don’t have an aggrogroup, then adding the aggrogroup to a DialogueAggro would be meaningless, so there’s no need do an expensive TryGetComponent if we don’t have one, so putting it
within the if aggrogroup block saves us some time.

You could have stuck with the way it was written and wrapped the AddFighter to group in an if statement

if(spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro) && aggroGroup!=null)
{
    dialogueAggro.SetAggroGroup(aggroGroup);
}
if(aggroGroup!=null)
{
    aggroGroup.AddFighterToGroup(spawnedEnemy.GetComponent<Fighter>());
}

but this would mean that you’re checking the aggroGroup for null twice, and checking for a dialogueAggro in all cases…
In short, the minimum number of if evaluations in the nested if condition is 1, with a maximum if evaluation count of 2.
In the one I just posted as a “you could”, the minimum number of evaluations is 2, and the maximum number is 3.

Jeez… how do you come up with these wild ideas in mind in less than 5 minutes?

So in summary, the previous blocks always formed a group ONLY IF the dialogue guard was involved, but this one integrates the fighters regardless of whether he’s there or not? Sounds solid!

Privacy & Terms