Assigning object references across several scenes with DontDestroyOnLoad

Hey guys, I have been stumped on this all day, and could really use some input. Basically I have a prototype RPG game in the works, and I am using ScriptableObjects to create my dialogue system as well as book reading system (each NPC or book has its own object created).

My problem is as follows: When my player character needs to leave the scene through a door, I use DontDestroyOnLoad so that the player character continues to exist to the next scene location. To avoid duplicating the player when he returns to the original scene, I am using Singleton pattern, and destroying the original player gameobject. Unfortunately, the NPC objects as well as the book objects require several variable references to the player gameobject as well as the UI elements that I have stored as a child to the player. Originally I used the process of FindGameObjectWithTag(“Tag Name”) to set the variable, but I later learned that it is very heavy to do that (especially because each individual book and NPC was running this process at the start of the scene). I then switched to using public variable references to the player and UI, and just dragged and dropped to set the reference. The thing is, when I switch scenes, there is no way to set these variables to reference an object that doesn’t exist in the current scene. When the player returns to the original scene, after deleting the duplicated gameobject of the player, all of the original books and NPCs no longer recognize the references, and I get a bunch of errors.

After this, I though maybe I should try reverting back to the FndGameObjectsWithTag system, though I am having issues with this now as well. I am trying to assign the variable references in my Start function, and in another script am deleting my duplicate player object in my Awake method (as I believe the Awake is called before Start), though when loading the scene, my variables remain unassigned, resulting in a ton of error messages.

I even tried delaying the assigning of the variables with a coroutine, but still did not work.

I also tried using a “middle man” gameobject that would basically sit in each scene and try to detect when the player entered, and then assign the variables of the scene’s books and NPCs, rather than each individual object searching for the player at once. This also did not work.

Essentially, my preferred method would be assigning all the variables once by making them public, and then simply dragging and dropping each reference, and then doing this for other scenes as well that do not contain the player gameobject (though to my understanding this cannot be done).

I know there must be a better way to do this…

Here are all of my relevant scripts: (the NPC system is very similar to the book, so the logic is the same behind both…)

Script for the book scriptable object:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = “New Book Register”, menuName = “Book Registrar”)]
public class Book : ScriptableObject
{
public string itemID;
public string title;
[TextArea(5,19)]
public string pageOneText;
[TextArea(5, 19)]
public string pageTwoText;
}

Script for calling the book into action:

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.UI;

public class BookManager : MonoBehaviour
{
bool isReading = false;
public Book book;
float distance;
Text bookTextOne;
Text bookTextTwo;
Text bookTitleText;
Text interactiveText;
GameObject player;
GameObject bookUI;

void Start()
{
bookTextOne = GameObject.FindGameObjectWithTag(“Book Text”).GetComponent<UnityEngine.UI.Text>();
bookTextTwo = GameObject.FindGameObjectWithTag(“Book Text 2”).GetComponent<UnityEngine.UI.Text>();
bookTitleText = GameObject.FindGameObjectWithTag(“Book Title Text”).GetComponent<UnityEngine.UI.Text>();
interactiveText = GameObject.FindGameObjectWithTag(“Interactable Text”).GetComponent<UnityEngine.UI.Text>();
player = GameObject.FindGameObjectWithTag(“Player”);
bookUI = GameObject.FindGameObjectWithTag(“Book UI”);
}

void OnMouseOver()
{
distance = Vector3.Distance(player.transform.position, this.transform.position);
if (distance <= 2.5f)
{
interactiveText.text = “Read '” + book.title + “'”;
if (Input.GetKeyDown(KeyCode.E) && isReading == false)
{
bookUI.SetActive(true);
bookTitleText.text = book.title;
bookTextOne.text = book.pageOneText;
bookTextTwo.text = book.pageTwoText;
PlayerMovement.canMove = false;
MouseLook.canLook = false;
PlayerMovement.canRegenStamina = true;
isReading = true;
}
else if(Input.GetKeyDown(KeyCode.E) && isReading == true)
{
bookUI.SetActive(false);
PlayerMovement.canMove = true;
MouseLook.canLook = true;
PlayerMovement.canRegenStamina = false;
isReading = false;
}
}
else
{
interactiveText.text = “”;
}
}
void OnMouseExit()
{
interactiveText.text = “”;
}
}

Script for removing the duplicate player object when returning to original scene:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
static GameManager instance;

void Awake()
{
if(instance != null)
{
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
}

Script for the door scriptable object(to move the player to the new scene)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = “New Door Register”, menuName = “Door Registrar”)]
public class Door : ScriptableObject
{
public string startingScene;
public string destinationScene;
public float destinationX;
public float destinationY;
public float destinationZ;
}

Script for Scene Management (switching the scene using the door variables)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class SceneManager : MonoBehaviour
{
public Door door;
//public Text interactiveText;
GameObject interactiveText;
GameObject player;
float distance;
private static bool created = false;

void Start()
{
player = GameObject.FindGameObjectWithTag(“Player”);
interactiveText = GameObject.FindGameObjectWithTag(“Interactable Text”);
}

void OnMouseOver()
{
distance = Vector3.Distance(player.transform.position, this.transform.position);
if (distance <= 2.5f)
{
interactiveText.GetComponent<UnityEngine.UI.Text>().text = "Open to " + door.destinationScene;
if(Input.GetKeyDown(KeyCode.E))
{
LoadScene();
}
}
else
{
interactiveText.GetComponent<UnityEngine.UI.Text>().text = “”;
}
}

void OnMouseExit()
{
interactiveText.GetComponent<UnityEngine.UI.Text>().text = “”;
}

void LoadScene()
{
player.GetComponent().enabled = false;
player.transform.position = new Vector3 (door.destinationX, door.destinationY, door.destinationZ);
player.GetComponent().enabled = true;
UnityEngine.SceneManagement.SceneManager.LoadScene(door.destinationScene, LoadSceneMode.Single);
}

}

Please use code tags: Using code tags properly

That said, can you put all the stuff that needs to live from scene to scene in a separate scene, load that scene once, then additively load and unload all the other scenes?

Breaking apart your scene into logical subscenes is EXTREMELY powerful in Unity. For instance, one scene for the UI, which may never go away. Another scene for the Inventory, which must come up, then go away. Another scene for all game managers, which stay for the duration of the game, etc. Or maybe your UI scene goes away and becomes the combat UI scene during combat, then back to the “explore UI” for instance. Check out the scene management stuff because once you start chopping your game into sub-scenes, you’ll kinda wonder how you ever didn’t do it before. :slight_smile:

1 Like

Thanks for the quick reply, I will definitely check out the scene management option!

This ^^^

Without refactoring your entire scene system though, you could also consider not destroying the existing DontDestroyOnLoad player. Instantiate the player object once (don’t make the player part of the scene, make it a prefab you instantiate), and only once, and assign a reference to itself to a static variable. Then all your NPC and book objects can just access this static reference in Start, without any performance issues from FindGameObjectWithTag.

This sounds like it could be a simple fix. Is there anyway to assign my variables other than FindGameObjectWithTag after my player travels to a different scene? For example can I access static variables from a different scene?

Yeah if you were just keeping the same player object, instead of destroying and recreating it, and you assign a reference to a static variable, then so long as the static variable isn’t null it will point to the correct player object. You can then access the static reference to the player object from anywhere.

This did the trick! Basically I had my UI as a child of my player, and was trying to access it after switching scenes, which Unity did not like. Instead of this, I made my UI just a constant element of my scenes, made my stats system spawn only once (on the player), and made them all static variables. Now it works great.

Note: I think that the best option would definitely be Kurt-Dekker’s response, but as I had already made all of my scripts in this way, Joe-Censored’s solution worked fine for my project.

Thanks again!

1 Like

Yeah I agree. I only made the other suggestion as a significant redesign when you’re so close to getting things working is probably not what you wanted to hear :wink:

I’d go Kurt’s route in your next project, or if you ever end up significantly redesigning this one anyway. Glad things are working :slight_smile:

You can use this code to make it work
#region Singleton
private static Class _instance;
public static Class Instance
{
get
{
if (_instance == null)
{
_instance = GameObject.FindObjectOfType();
}
return _instance;
}
}
private void Awake()
{
DontDestroyOnLoad(this);
}
#endregion

What I do with most scripts that persist between scenes, most often manager classes like a scene manager or persistence layer, is to create {MangerName}Access scripts that in code refere to a static variable of the manager script that was marked as DoNotDestroy on new scene load. They need to implement the same interface as the manager and simply forward all the function calls if you want to use Unity Events or logic setup in the Editor. Otherwise, you can simply access the static variable of your manager in your custom code directly. But the other way is much more decoupled and allows better code reuse. Ideally you also specify a C# interface which is implemented in the Manager and the Accessor script. Is a bit of effort but is very useful in the long run.