How can I determine where my player will appear in the next scene?

Using an OnTriggerEnter2D to load a new scene. I want the player to show up in a specific spot, namely the other side of the door… duh.

But when I load the new scene obviously the player is in the same spot it was in the previous scene.

My instinct is to do something like:

OnTriggerEnter2D
{
loadscene (nameOfSceneToLoad)
Player.instance.transform.position = Find “DoorB”
}

But of course once the new scene is loaded this is rendered useless.
Any ideas?

Thanks!

There are a few ways to handle this kind of thing, but here’s a basic way to approach it.

It’s probably a good idea to set up some kind of Scene Change Manager, especially if you’re planning on doing any kind of transition effects, or if you want to do other work when changing scenes. You can go with a singleton, or use a message dispatcher approach, it’s up to you. Here, I’m just grabbing a reference by type in the player’s Awake method. I’m also assuming your player object will persist between scenes.

The idea behind this approach is to create a dictionary of objects and their targets. I’ve structured this so that you can specify the position by Vector3 or the name of the target object in the scene you’re loading. Your player object (or any other game object for that matter) can call the method on your scene change manager and add itself to the dictionary. Then, when the scene changes and OnSceneLoaded is called, the manager will run through every entry in the dictionary, position the objects, then clear out the dictionaries. I’ve used early returns to prevent null reference errors in case an object gets destroyed.

EDIT: I should probably add, this approach is geared towards possibly needing to re-position multiple objects. If you just need to move the player, you could simplify things by ditching the dictionaries and just storing a KeyValuePair or System.Tuple.

Scene Change Manager

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

public class SceneChangeManager : MonoBehaviour
{
    private Dictionary<GameObject, string> gameObjectTargets;
    private Dictionary<GameObject, Vector3> gameObjectPositions;

    private void Awake()
    {
        DontDestroyOnLoad(this);
        gameObjectTargets = new Dictionary<GameObject, string>();
        gameObjectPositions = new Dictionary<GameObject, Vector3>();
    }

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

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

    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        // Place all objects by position
        foreach(KeyValuePair<GameObject, Vector3> objectToPosition in gameObjectPositions)
        {
            if (objectToPosition.Key == null)
                continue;

            objectToPosition.Key.transform.position = objectToPosition.Value;
        }
        gameObjectPositions.Clear();

        // Place all objects by target name
        foreach (KeyValuePair<GameObject, string> objectToPosition in gameObjectTargets)
        {
            GameObject target = GameObject.Find(objectToPosition.Value);

            if (objectToPosition.Key == null || target == null)
                continue;

            objectToPosition.Key.transform.position = target.transform.position;
        }
        gameObjectTargets.Clear();
    }

    public void LoadScene(string scene)
    {
        SceneManager.LoadScene(scene);
    }

    public void SetObjectPositionNextScene(GameObject gameObject, string target)
    {
        gameObjectTargets.Add(gameObject, target);
    }

    public void SetObjectPositionNextScene(GameObject gameObject, Vector3 position)
    {
        gameObjectPositions.Add(gameObject, position);
    }
}

Player

using UnityEngine;

public class Player : MonoBehaviour
{
    private SceneChangeManager sceneChangeManager;

    private void Awake()
    {
        DontDestroyOnLoad(this);
        sceneChangeManager = (SceneChangeManager)FindObjectOfType(typeof(SceneChangeManager));
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z) && sceneChangeManager)
        {
            sceneChangeManager.SetObjectPositionNextScene(gameObject, new Vector3(-2f, 0f, 0f));
            sceneChangeManager.LoadScene("Scene 2");
        }

        if (Input.GetKeyDown(KeyCode.X) && sceneChangeManager)
        {
            sceneChangeManager.SetObjectPositionNextScene(gameObject, "Door 2");
            sceneChangeManager.LoadScene("Scene 2");
        }
    }
}

You could also create a DoorTrigger script, give it some public properties like the Vector3 position or target object’s name in the next scene, and grab those values in the player’s collision method. Again, the above code is designed mainly for positioning persistent objects (the key of the dictionary).

1 Like

Oofh man… I hate to be this guy, but this is way over my head. I’m like 4 months into C# and Unity, so you’re using concepts I’m not yet familiar with.

All I really want to do is load a scene and put the player in a specific position in the next scene based on the door they enter. Is it really all this complex?

Why dont you add your player to a “Level Starter Pack” prefab? So you create an object, call it “Level Starter Pack”, add the player and anything else you think is necessary to keep between levels (like the player’s UI, a Level Manager if you have one, etc. Then drag the whole thing onto a project folder, so Unity converts it into a Prefab.

Then the scene ends, and everything is destroyed, but since you have a prefab of the player (and stuff that accompanies him) you can just drop the prefab on the next scene, where you want him to start at.

To keep the player settings, I’ve seen that you can use PlayerPrefs to keep some values (although I’ve read this is supposed to only keep display settings or things like that, cause players can find the location on your folder and change values to cheat). I believe Serialization help keep these values for save/load so it would be similar.

Actually that’s exactly what I’m doing. The issue is that my player doesn’t load into the correct position. It essentially creates a clone of the player but puts it in a random spot in the screen (presumably the position it was previously in the last scene). I’ve found that adding it’s transform at Start() doesn’t actually do anything to affect this, because technically it’s already had a start.

If this really is bothering you, just create the illusion of a different scene but actually keep it in the same scene. So, they walk through the door, have a screen fade-out then fade-in animation or transition but the player has no idea what actually happened, they actually just changed position within the scene.

Or if you must use different scenes, create a startPosition Transform variable on your character’s script and place it where you want the player to spawn. Then, in Start(), have the player.transform.position = startPosition; If your Start “doesnt actually do anything” then add a Debug.Log() function into it and find out if it runs each scene change. If it doesnt, make sure you dont have any Singleton’s or DontDestroyOnLoad since that will only cause Start() to run that first time.

Here is an forum of someone wanting Start to run every scene change and another of the opposite:

Is your player at the right position under the “Level Starter” prefab object on both Scenes? For example, for my project, I made sure the player was at 0,0,0 inside the prefab, and if I needed to move him I would move the prefab instead of him directly. Then on the next scene, wherever I place my prefab at, that’s where the player would spawn.

Oh, sorry for getting overly technical. I’ve attached a Unity package, you can take a look to see exactly how it works, just start in Scene 1 and press Z or X. Here’s a more thorough explanation (which might still be technical but I’ll try and go through things step by step at least!)

We’ll start with the Player object. In Awake, I’m first making sure the player will persist between scenes - although it sounds like you’re already doing this. The next line gets a reference to the scene change manager, the thing that will help us keep track of where the player should go in the next scene.

// If you change "private" to public, you can assign the reference in the inspector, and get rid of...
private SceneChangeManager sceneChangeManager;

private void Awake()
{
    // Persist the player between scenes
    DontDestroyOnLoad(this);

    // ...this line here
    sceneChangeManager = (SceneChangeManager)FindObjectOfType(typeof(SceneChangeManager));
}

You can ignore what I said about singletons and message dispatchers for now. They are just an improved way to go about dealing with how your game objects communicate. When you’re ready, here’s a pretty good Singleton class you can use and here’s a Message Dispatcher asset.

Ok, in the player’s update, I’ve got two examples of how you could call the method that will change scenes and put your player in the right location. I’ll just look at one, they’re both very similar.

// For the example, we'll change scenes when the player presses X
// We also check that we have a reference to the scene change manager,
// otherwise we'll get an error on the next line
if (Input.GetKeyDown(KeyCode.X) && sceneChangeManager)
{
    // This line calls a method on the scene change manager
    // The two arguments are gameObject (the player)
    // and  "Door 2", the name of the object in the next scene where we want to show up
    sceneChangeManager.SetObjectPositionNextScene(gameObject, "Door 2");

    // This calls a method on the scene change manager that loads the next scene
    // Since the scene change manager is in charge of scene changes, the method
    // to change scenes will go in that script, instead of here in the player script
    sceneChangeManager.LoadScene("Scene 2");
}

Next up is the scene change manager. First, let’s look at those methods we just called.

// This method gets called first, so that our scene change manager has the data it needs...
public void SetObjectPositionNextScene(GameObject gameObject, string target)
{
    gameObjectTargets.Add(gameObject, target);
}

// ... for when this method gets called!
public void LoadScene(string scene)
{
    // This is literally just the standard method for changing scenes in Unity
    // The only reason for putting it in its own function is in case we need
    // to do something more down the line, like show a transition effect / fade out / etc
    SceneManager.LoadScene(scene);
}

Let’s take a look at the rest of the scene change manager. We have two dictionaries. A dictionary contains a bunch of “key-value pairs”, basically any two related things. In this case, we have two of them.

// This holds pairs made up of gameObjects as the keys
// (could be the player or other objects you want to reposition in the new scene)
// and strings (the name of the object we're going to look for later)
private Dictionary<GameObject, string> gameObjectTargets;

// This one contains gameObjects and Vector3s
// (in case you want to just tell the scene change manager where to put your player with a specific position)
private Dictionary<GameObject, Vector3> gameObjectPositions;

private void Awake()
{
    // This next bit keeps the manager object active across scene changes
    DontDestroyOnLoad(this);
 
    // and then initializes the dictionaries
    gameObjectTargets = new Dictionary<GameObject, string>();
    gameObjectPositions = new Dictionary<GameObject, Vector3>();
}

Ok next we need to know when our scene has successfully changed. We’ll use “events” to do this. Basically what we’re doing is saying, when the scene change manager is enabled, have it listen for a message from Unity saying that the scene changed. When the manager is disabled, we will stop listening for messages. We use += and -= to start/stop listening.

private void OnEnable()
{
    // Here, SceneManager refers to the Unity class, not the script we're writing right now
    // SceneManager.sceneLoaded is the event
    // OnSceneLoaded is the name of the method inside *this* script
    // that will get called whenever the scene is loaded
    SceneManager.sceneLoaded += OnSceneLoaded;
}

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

Here’s what a simplified version of the OnSceneLoaded method looks like - we’ll just look at the example that uses gameObject names, not the example that uses Vector3 positions. The logic is the same.

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    // We're going to look at each pair in our dictionary from before.
    // Again, each pair contains an object and the name of a target object where we should place it
    // objectToPosition will be the variable we use to refer to each pair
    // objectToPosition.Key contains the player gameObject
    // objectToPosition.Value contains the name of the target object
    foreach (KeyValuePair<GameObject, string> objectToPosition in gameObjectTargets)
    {
        // We call GameObject.Find to locate the actual gameObject with the given name (e.g. "Door 2")
        // Then we store a reference to it as "target"
        GameObject target = GameObject.Find(objectToPosition.Value);
   
        // If either the player (objectToPosition.Key) gets destroyed somehow
        // or the target object can't be found, our reference will be null
        // so we should stop doing any more work, or we could get into trouble
        // "continue" will skip over this pair and look at the next pair in the dictionary, if there is a next pair
        if (objectToPosition.Key == null || target == null)
            continue;

        // Now we set the player's position to the target's position
        objectToPosition.Key.transform.position = target.transform.position;
    }
    // Finally, we wipe the dictionary clean just to make it obvious that all our work is done
    // That will also prevent weird behaviour if we somehow change scenes without telling the scene manager
    // where to put the player (we only want to use these values once, for the scene they're meant for)
    gameObjectTargets.Clear();
}

I hope that makes a bit more sense now. Sorry for the long read. Also, this is maybe overkill if all you want to do is re-position the player. You can replace the Dictionary<gameObject, string> with a single KeyValuePair<gameObject, string> if you like. OnSceneLoaded would look more like this then:

private KeyValuePair<GameObject, string> playerPosition;

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    GameObject target = GameObject.Find(playerPosition.Value);

    if (playerPosition.Key == null || target == null)
        return;

    playerPosition.Key.transform.position = target.transform.position;
}

4919045–476474–Scene Change Example.unitypackage (5.96 KB)

1 Like

deactivate the player when the scene its changing and when its activated find a tag called initialPoint for example and set the transform of the player the same as the initialPoint transform, easy and clean

I ended up solving this the easy way: adding the scene 2 within the scene 1. That way I’m just changing the position of the player, and not the scene. For my purposes, this should work fine. Thanks for the help!