Missing Reference Exception (For PlayerController Component)

I’m creating a game where there are multiple scenes with some having save points for the player to save their progress. I’ve been following a tutorial on how to save my game and I ran into a very frustrating issue when I tried implementing the save/load system onto my levels. When I start a new game and load into a level that has a save point, I’m able to save and load properly. But when I try to leave that level (via door-to-door style level transition) and then go back and save, I get an error that says:

MissingReferenceException: The object of type ‘PlayerController’ has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
ABADDON_DM_PL.PlayerController.SaveData (ABADDON_DM_PL.GameData& data) (at Assets/Scripts/Player/PlayerController.cs:89)
ABADDON_DM_PL.DataPersistenceManager.SaveGame () (at Assets/Scripts/Save Data/DataPersistence/DataPersistenceManager.cs:115)
ABADDON_DM_PL.FONO.Update () (at Assets/Scripts/NPC/FONO.cs:134)

I’ve tried all I can to figure out why the engine insists that the PlayerController component has been “destroyed”. I’ve looked around online and found not a whole lot that helped my case. I’ve attempted d&d both the player game object from the prefab folder, and even tried hard coding the script to assign the player game object and its PlayerController component in the scripts where I’m having this error. Nothing. I’m completely stumped here and I could really use some advice and feedback on how to properly get around this issue.

Here’s some of the scripts that are relevant to the topic. I’ve indicated where the error has occured (Look for ‘>>’):

PlayerController (Responsible for player movement and camera movement; Naturally attached onto Player game object; Player object is set to not be destroyed on load)

public class PlayerController : MonoBehaviour, IDataPersistence
    {
        [Header("Main Camera")]
        [SerializeField] Transform playerCamera;

        [Header("Movement Settings")]
        [SerializeField] public float walkSpeed = 6f;
        [SerializeField] float gravity = -24f;
        [SerializeField] float jumpHeight = 3f;
        [SerializeField] [Range(0.0f, 0.5f)] float moveSmoothTime = 0.3f;
        float velocityY = 0.0f;
        Vector2 currentDir = Vector2.zero;
        Vector2 currentDirVelocity = Vector2.zero;

        [Header("Dodge Settings")]
        [SerializeField] float dodgeSpeed;
        [SerializeField] float dodgeTime;
        [SerializeField] bool useCameraForward = true;
        [SerializeField] bool allowAllDirections = true;
        [SerializeField] bool disableGravity = false;
        [SerializeField] bool resetVal = true;
        public bool isDodging;

        [Header("Camera Effects")]
        [SerializeField] float dodgeFOV;
        [SerializeField] PlayerCamera camera_;

        [Header("Mouse Settings")]
        [SerializeField] float mouseSensitivity = 3.5f;
        [SerializeField] [Range(0.0f, 0.5f)] float mouseSmoothTime = 0.03f;
        [SerializeField] bool lockCursor = true;
        float cameraPitch = 0.0f;
        Vector2 currentMouseDelta = Vector2.zero;
        Vector2 currentMouseDeltaVelocity = Vector2.zero;

        [Header("Ground Settings")]
        [SerializeField] Transform groundCheck;
        [SerializeField] float groundDistance = 0.4f;
        [SerializeField] public LayerMask groundMask;
        [SerializeField] int jumps_;
        [SerializeField] int jumpLimit;
        [SerializeField] public bool isGrounded;

        [Header("Slope Settings")]
        [SerializeField] private float slopeForce;
        [SerializeField] private float slopeForceRayLength;     

        [Header("Weapon")]
        [SerializeField] public float weaponAnim_Speed;

        [Header("Misc.")]
        public CharacterController controller = null;

        void Start()
        {
            controller = GetComponent<CharacterController>();

            if (!PauseMenu.isPaused)
            {
                if (lockCursor)
                {
                    Cursor.lockState = CursorLockMode.Locked;
                    Cursor.visible = false;
                }
            }
            
        }

        public void LoadData(GameData data)
        {
            this.transform.position = data.playerPosition;
            this.jumpLimit = data.jumpLimit;
        }

        public void SaveData(ref GameData data)
        {
>>            data.playerPosition = this.transform.position; // line where error occurs
            data.jumpLimit = this.jumpLimit;
        }

        void Update()
        {
            isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

            UpdateMouseLook();
            UpdateMovement();
            UpdateWeapon_Anim();
        }

        void UpdateMouseLook()
        {
            if (!PauseMenu.isPaused)
            {
                Vector2 targetmouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));

                currentMouseDelta = Vector2.SmoothDamp(currentMouseDelta, targetmouseDelta, ref currentMouseDeltaVelocity, mouseSmoothTime);

                cameraPitch -= currentMouseDelta.y * mouseSensitivity;
                cameraPitch = Mathf.Clamp(cameraPitch, -90f, 90f);

                playerCamera.localEulerAngles = Vector3.right * cameraPitch;

                transform.Rotate(Vector3.up * currentMouseDelta.x * mouseSensitivity);
            }
        }
        
        void UpdateMovement()
        {
            Vector2 targetDir = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
            targetDir.Normalize();

            // Smooth movement input
            currentDir = Vector2.SmoothDamp(currentDir, targetDir, ref currentDirVelocity, moveSmoothTime);

            // Gravity

            if (controller.isGrounded)
            {
                velocityY = 0f;
                slopeForce = 10;
                jumps_ = 0;
            }

            velocityY += gravity * Time.deltaTime;

            // Jumping
            if (!PauseMenu.isPaused)
            {
                if (Input.GetButtonDown("Jump") && jumps_ < jumpLimit)
                {
                    slopeForce = 0;
                    velocityY = Mathf.Sqrt(jumpHeight * -2f * gravity);
                    jumps_ ++;
                }
            }

            //Dodge
            if (Input.GetKeyDown(KeyCode.LeftShift) && !isDodging)
            {
                StartCoroutine(Dodge());
            }

            if (targetDir.y != 0 || targetDir.x != 0 && OnSlope())
            {
                controller.Move(Vector3.down * controller.height / 2 * slopeForce * Time.deltaTime);
            }

            Vector3 velocity = (transform.forward * currentDir.y + transform.right * currentDir.x) * walkSpeed + Vector3.up * velocityY;
            
            
            
            controller.Move(velocity * Time.deltaTime);
        }

        void UpdateWeapon_Anim()
        {
            weaponAnim_Speed = controller.velocity.magnitude / walkSpeed;

            if (weaponAnim_Speed > 1)
            {
                weaponAnim_Speed = 1;
            }
        }

        private bool OnSlope()
        {
            if (!controller.isGrounded) return false;

            RaycastHit hit;

            if(Physics.Raycast(transform.position, Vector3.down, out hit, controller.height / 2 * slopeForceRayLength))
            {
                if(hit.normal != Vector3.up) return true;
            }
            return false;
        }

        IEnumerator Dodge()
        {
            float startTime = Time.time;
            Transform forwardT;    

            if (useCameraForward)
                forwardT = playerCamera;
            else
                forwardT = transform;
            Vector3 direction = GetDirection(forwardT);

            isDodging = true;
            camera_.DoFov(dodgeFOV);
            while (Time.time < startTime + dodgeTime)
            {   
                controller.Move(direction * dodgeSpeed * Time.deltaTime);
                yield return null;
            }
            isDodging = false;
            camera_.DoFov(90f);
        }

        private Vector3 GetDirection(Transform forwardT)
        {
            float horizontalInput = Input.GetAxisRaw("Horizontal");
            float verticalInput = Input.GetAxisRaw("Vertical");

            Vector3 direction = new Vector3();

            if (allowAllDirections)
            {
                direction = forwardT.forward * verticalInput + forwardT.right * horizontalInput;
            }
            else direction = forwardT.forward;

            if (verticalInput == 0 && horizontalInput == 0)
            {
                direction = forwardT.forward;
            }

            return direction.normalized;
        }
    }

DataPersistenceManager (Responsible for managing Data during game; Has public methods to create a new game, save game, and load saved game; Is attached to an empty Game Object called DataManager; DataManager object also set to not be destroyed on load)

public class DataPersistenceManager : MonoBehaviour
    {
        [Header("File Storage Config")]
        [SerializeField] private string fileName;

        private GameData gameData;
        private List<IDataPersistence> dataPersistenceObjects;
        private FileDataHandler dataHandler;

        public static DataPersistenceManager instance { get; private set; }

        private void Awake()
        {
            if(instance != null)
            {
                Debug.Log("Found more than one Data Persistence Manager in the scene. New one is destroyed");
                Destroy(this.gameObject);
                return;
            }
            instance = this;
            DontDestroyOnLoad(this.gameObject);

            this.dataHandler = new FileDataHandler(Application.persistentDataPath, fileName);
        }

        

        private void OnEnable()
        {
            SceneManager.sceneLoaded += OnSceneLoaded;
            SceneManager.sceneUnloaded += OnSceneUnloaded;
        }

        private void OnDisable()
        {
            SceneManager.sceneLoaded -= OnSceneLoaded;
            SceneManager.sceneUnloaded -= OnSceneUnloaded;
        }

        public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            this.dataPersistenceObjects = FindAllDataPersistenceObjects();
            LoadGame();
        }

        public void OnSceneUnloaded(Scene scene)
        {

        }

        public void NewGame()
        {
            this.gameData = new GameData();
        }

        public void LoadGame()
        {
            // Load any saved data from handler
            this.gameData = dataHandler.Load();

            // if no data to load, initialize new game data
            if (this.gameData == null)
            {
                Debug.Log("No data was found. Initializing data to default value");
                return;
            }

            // Push loaded data to necessary scripts
            foreach (IDataPersistence dataPersistenceObj in dataPersistenceObjects)
            {
                dataPersistenceObj.LoadData(gameData);
            }
        }

        public void SaveGame()
        {
            if (this.gameData == null)
            {
                Debug.LogError("No data was found. A New Game must be created");
                return;
            }

            // Pass the data to other scripts so they can update it
            foreach (IDataPersistence dataPersistenceObj in dataPersistenceObjects)
            {
>>                dataPersistenceObj.SaveData(ref gameData); // line where error occurs
            }

            // Save data to a file using data handler
            dataHandler.Save(gameData);
         
        private List<IDataPersistence> FindAllDataPersistenceObjects()
        {
            IEnumerable<IDataPersistence> dataPersistenceObjects = FindObjectsOfType<MonoBehaviour>()
                .OfType<IDataPersistence>();
            return new List<IDataPersistence>(dataPersistenceObjects);
        }

        public bool HasGameData()
        {
            return gameData != null;
        }
   }

FONO (Handles dialogue and allows you to save the game; Attached to an NPC with the same name)

    public class FONO : MonoBehaviour
    {
        public GameObject player;

        [Header("Name and Dialogue")]
        [SerializeField] string npcName = "FONO";
        [SerializeField] string continueText;
        [SerializeField] float typingSpeed;

        [TextArea(3, 10)]
        [SerializeField] string[] dialogue1;
        [TextArea(3, 10)]
        [SerializeField] string[] dialogue2;
        private Queue<string> sentences;
        int timesSpoken = 0;
        private string sentence;

        private Coroutine typingEffectCoroutine;
        private const string HTML_ALPHA = "<color=#00000000>";

        GameObject normalCrosshair, interactCrosshair, dialogueScreen, crossbracketsObj;
        AudioSource dialogueSFX, savepointSFX;
        AudioSource voice;
        Animator saveScreenAnim;
        Public_Crossbrackets crossbrackets;
        DialogueBox dialogueBox;
        public PlayerController controller;
        PauseMenu pauseMenu;

        public bool isTalking = false;
        public bool isTyping { get; private set; }
        bool canType = false;
        bool canSave = false;
        bool canTalk;


        DataPersistenceManager dataManager;

        void Awake()
        {
            sentences = new Queue<string>();

            GetPlayer();
            GetDialogueBox();
            GetCrosshairs();
            GetAudioSources();
            GetPauseMenu();
            GetDataManager();
            GetAnimator();
        }

        private void GetPlayer()
        {
            player = GameObject.Find("Player");
            controller = player.GetComponent<PlayerController>();
        }

        private void GetDialogueBox()
        {
            dialogueScreen = GameObject.Find("Dialogue Screen");
            dialogueBox = dialogueScreen.GetComponent<DialogueBox>();
        }

        private void GetCrosshairs()
        {
            crossbracketsObj = GameObject.Find("Cross Brackets");
            crossbrackets = crossbracketsObj.GetComponent<Public_Crossbrackets>();
            normalCrosshair = crossbrackets.normal_Cross;
            interactCrosshair = crossbrackets.interact_Cross;
        }

        private void GetAudioSources()
        {
            dialogueSFX = GameObject.Find("UI dialogue SFX").GetComponent<AudioSource>();
            savepointSFX = GameObject.Find("Save Screen").GetComponent<AudioSource>();
            voice = this.GetComponent<AudioSource>();
        }

        private void GetAnimator()
        {
            saveScreenAnim = GameObject.Find("Save Screen").GetComponent<Animator>();
        }

        private void GetPauseMenu()
        {
            pauseMenu = GameObject.Find("Pause Screen").GetComponent<PauseMenu>();
        }

        private void GetDataManager()
        {
            dataManager = GameObject.Find("DataManager").GetComponent<DataPersistenceManager>();
        }

        void Update()
        {
            if (canTalk == true && Input.GetKeyDown(KeyCode.E))
            {
                StartDialogue();
            }
            if (isTalking == true && Input.GetKeyDown(KeyCode.E))
            {
                if (!isTyping && canSave == true)
                {
                    savepointSFX.Play();
>>                    dataManager.SaveGame(); // Line where error occured
                    Debug.Log("Game successfully saved!");
                    dialogueBox.dialogueText.text = "> OH WOW!! IT ACTUALLY WORKED! HOW EXCITING!!!";
                    dialogueBox.pressToContinue.text = "PRESS 'Q' TO CONTINUE>>";
                    saveScreenAnim.SetTrigger("saveMenu_Off");
                }
                else { }
            }
            if (isTalking == true && Input.GetKeyDown(KeyCode.Q))
            {
                DisplayNextSentence();
            }
        }

        private void OnTriggerEnter(Collider other)
        {
            if (other.tag == "Player")
            {
                canTalk = true;
                normalCrosshair.SetActive(false);
                interactCrosshair.SetActive(true);
            }
        }

        private void OnTriggerExit(Collider other)
        {
            if (other.tag == "Player")
            {
                canTalk = false;
                interactCrosshair.SetActive(false);
                normalCrosshair.SetActive(true);
            }
        }

        private void StartDialogue()
        {
            timesSpoken++;
            sentences.Clear();

            if (timesSpoken == 1)
            {
                foreach (string sentence in dialogue1)
                {
                    sentences.Enqueue(sentence);
                }
            }

            else if (timesSpoken == 2)
            {
                foreach (string sentence in dialogue2)
                {
                    sentences.Enqueue(sentence);
                }
                StartCoroutine(SaveWaitTime());

                timesSpoken = 1;
            }

            DisplayNextSentence();

            isTalking = true;
            canTalk = false;
            dialogueSFX.Play();
            controller.enabled = false;
            pauseMenu.enabled = false;
            crossbracketsObj.SetActive(false);
            dialogueBox.dialogueBoxobj.SetActive(true);
            dialogueBox.npcNameText.text = npcName;
            dialogueBox.pressToContinue.text = continueText;
        }

        void DisplayNextSentence()
        {
            if (sentences.Count == 0 && !isTyping)
            {
                EndDialogue();
                return;
            }

            if (!isTyping)
            {
                sentence = sentences.Dequeue();

                typingEffectCoroutine = StartCoroutine(TypeSentence(sentence));
            }
            else
            {
                SkipToEndOfLine();
            }
        }

        private IEnumerator TypeSentence(string sentence)
        {
            isTyping = true;

            dialogueBox.dialogueText.text = "";

            string originalText = sentence;
            string displayText = "";
            int alphaIndex = 0;

            foreach (char letter in sentence.ToCharArray())
            {
                alphaIndex++;
                dialogueBox.dialogueText.text = originalText;

                displayText = dialogueBox.dialogueText.text.Insert(alphaIndex, HTML_ALPHA);
                dialogueBox.dialogueText.text = displayText;
                voice.Play();

                yield return new WaitForSeconds(typingSpeed);
                voice.Stop();
            }

            isTyping = false;
        }

        private void SkipToEndOfLine()
        {
            StopCoroutine(typingEffectCoroutine);
            dialogueBox.dialogueText.text = sentence;
            isTyping = false;
        }

        private void EndDialogue()
        {
            isTalking = false;
            canTalk = true;
            dialogueSFX.Play();
            controller.enabled = true;
            pauseMenu.enabled = true;
            dialogueBox.dialogueBoxobj.SetActive(false);
            crossbracketsObj.SetActive(true);
            canSave = false;
            saveScreenAnim.SetTrigger("saveMenu_idle");
        }

        IEnumerator SaveWaitTime()
        {
            yield return new WaitForSeconds(.5f);
            dialogueBox.pressToContinue.text = "'E' TO SAVE>> 'Q' TO SKIP>>";
            canSave = true;
        }
    }

IDataPersistence (Interface script; basically a middle-man i think)

    public interface IDataPersistence
    {
        void LoadData(GameData data);
        void SaveData(ref GameData data);
    }

GameData (Tracks all player states and assigns its default values)

    [System.Serializable]
    public class GameData
    {
        public int blessedItems;
        public int jumpLimit;
        public bool hasAxe;
        public Vector3 playerPosition;

        public SerializableDictionary<string, bool> roachesCollected;
        public SerializableDictionary<string, bool> blessedItemsCollected;

        // Default values for a new game
        public GameData()
        {
            this.blessedItems = 0;
            this.jumpLimit = 1;
            this.hasAxe = false;
            playerPosition = Vector3.zero;
            roachesCollected = new SerializableDictionary<string, bool>();
            blessedItemsCollected = new SerializableDictionary<string, bool>();
        }
    }

SceneExit (Used for door-to-door scene transitions; Maybe the error could be caused here when you leave and come back?)

public class SceneExit : MonoBehaviour
{
    [Header("Strings")]
    [SerializeField] string sceneToLoad;
    [SerializeField] string exitName;

    [Header("UI")]
    [SerializeField] GameObject normal_crosshair;
    [SerializeField] GameObject interact_crosshair;

    [Header("Access variables from Cross Brackets")]
    [SerializeField] GameObject crossbrack_go;
    [SerializeField] Public_Crossbrackets _Crossbrackets;

    [Header("Access Fade Animations from Fade_Black")]
    [SerializeField] GameObject fadeBlack_go;
    Animation fade_Transition;

    [Header("Access Audio SFX from SFX")]
    [SerializeField] GameObject SFX_go;
    AudioSource door_SFX;
    
    bool canReach;

    void Start ()
    {
        ResetCrosshairs();
        AssignFadeAnim();
        AssignDoorSFX();
    }

    void Update()
    {

        if (Input.GetKeyDown(KeyCode.E) && canReach)
        {
            PlayerPrefs.SetString("LastExitName", exitName);
            LoadScene();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
            //PlayerPrefs.SetString("LastExitName", exitName);

            SwitchTo_InteractCross();
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "Player")
        {
            SwitchBack_NormalCross();
        }
    }

    private void SwitchTo_InteractCross()
    {
        normal_crosshair.SetActive(false);
        interact_crosshair.SetActive(true);

        canReach = true;
    }

    private void SwitchBack_NormalCross()
    {
        interact_crosshair.SetActive(false);
        normal_crosshair.SetActive(true);

        canReach = false;
    }

    void LoadScene()
    {
        StartCoroutine(LevelTransition());
    }

    IEnumerator LevelTransition()
    {
        door_SFX.Play();
        fade_Transition.Play("fade_black");

        yield return new WaitForSeconds(1f);
        SceneManager.LoadSceneAsync(sceneToLoad);
    }

    void ResetCrosshairs()
    {
        crossbrack_go = GameObject.Find("Cross Brackets");
        _Crossbrackets = crossbrack_go.GetComponent<Public_Crossbrackets>();

        normal_crosshair = _Crossbrackets.normal_Cross;
        interact_crosshair = _Crossbrackets.interact_Cross;

        normal_crosshair.SetActive(true);
        interact_crosshair.SetActive(false);     
    }

    void AssignFadeAnim()
    {
        fadeBlack_go = GameObject.Find("Fade_Black");
        fade_Transition = fadeBlack_go.GetComponent<Animation>();
    }

    private void AssignDoorSFX()
    {
        SFX_go = GameObject.Find("door SFX");
        door_SFX = SFX_go.GetComponent<AudioSource>();
    }

}

And here’s the tutorial I mentioned if you want a reference
How to make a Save & Load System work across multiple scenes

Unity isn’t lying when it comes to a missing reference exception.

You should probably investigate when FindAllDataPersistenceObjects is actually running, because from memory the SceneManager.sceneLoaded/sceneUnloaded don’t run at quite the right times that you would expect them to, and I have a feeling you’re getting references to objects from the previous scene.

That said using additive scene loading would prevent the need to keep reloading the player, as they would be persistent throughout the life of a gameplay session.

Yes indeed.

And the DataPersistenceManager is a textbook example of a defective singleton:

It does DontDestroyOnLoad AND it has dragged-in-references.

Here’s more reading, relevant to line 14 above:

If you ever find yourself asking “is this instance null?” then you are working with a defective singleton. Delete it and make one that plays nice with Unity3D.

Some super-simple Singleton examples to take and modify:

Simple Unity3D Singleton (no predefined data):

Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance

Alternately you could start one up with a [RuntimeInitializeOnLoad] attribute.

There is never a reason to drag a GameObject into a scene if it will be DontDestroyOnLoad.

If you do:

  • you may drag it into something else that isn’t DDOL. FAIL
  • you may drag something else into it that isn’t DDOL. FAIL

Just DO NOT drag anything into a scene that will be marked DontDestroyOnLoad. Just Don’t Do It!

Hmm. I didn’t think about checking FindAllDataPersistenceObjects. And I could also experiment with using additive scenes. I would just need to figure out how that would work and how to implement it in my game.

I see. I appreciate you pointing this out. So in order to have the player object (and the better singleton), I would have to Instantiate it in code?

I don’t think that’s what they mean entirely. The pertinent point is that singleton monobehaviours shouldn’t be dragged into scenes, as this opens up the potentially for bugs quite dramatically. You can potentially reference if when you shouldn’t, or it could reference something in the scene that won’t persist for as long as it does, for example.

A singleton should generally only have one instance throughout the lifetime of the application. This works well for things like an audio system or the camera, where you can instantiate and DDoL them, and they sit there on the side during the entire session.

Issues arise with something like the Player character, which might come and go a number of times, and generally not be present at the start of the application. It becomes very easy to write code that inadvertenly tries to access the player when it isn’t present.

I’ve usually handled this by not making the player a singleton, but letting it register itself to a (usually) singleton system that exposes its own API for interfacing with the player, being able to provide meaningful warnings when things are done at the wrong time (or just ignoring them). This is usually just a static class, and I use EditorApplication.playModeStateChanged to clean things up when leaving play mode.