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