Portals multiply and are incorrectly identified

@sampattuzzi @Rick_Davidson I implement access to the spawn point differently. However, I find that using this == portal simply doesn’t return the proper reference, since apparently 3 portals exist when only 2 should after having triggered either portal at least once, so I temporarily make it work by comparing the names. However, this breaks when I add the destination enumeration.

I’m following the course in Unity 2019.1, though, a little evil on my part.

private Transform SpawnPoint => transform.GetChild(0);

private Portal OtherPortal
{
    get
    {
        foreach (var portal in FindObjectsOfType<Portal>())
        {
            if (name == portal.name)
            {
                continue;
            }
            if (destination != portal.destination)
            {
                continue;
            }
            return portal;
        }
        return null;
    }
}

I’m not entirely clear what the problem is. Could you answer the questions:

  1. What do you expect to see?
  2. What actually happens?
  3. What version of the engine are you using?
  4. What steps did you take to reproduce this?

If possible include some screenshots or videos to help explain.

1 Like

Apologies for the unclear problem statement.

  1. I expect to replicate the behaviour accomplished in the lecture.
  2. The player teleports back to the default location. The error is
NullReferenceException: Object reference not set to an instance of an object
TL7.SceneManagement.Portal.TeleportPlayer () (at Assets/Scripts/SceneManagement/Portal.cs:71)
TL7.SceneManagement.Portal+<Transit>d__9.MoveNext () (at Assets/Scripts/SceneManagement/Portal.cs:58)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Scripting/Coroutines.cs:17)
  1. 2019.1.4f1
  2. I followed through the code and kept all functionality near exact, except for a few changes, and then proceeded as per the video
    i. I hit play.
    ii. I make the player run towards a block.

The entire code for Portal.cs:

using System.Collections;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Assertions;
using UnityEngine.SceneManagement;

namespace TL7.SceneManagement
{
    public class Portal : MonoBehaviour
    {
        enum Destination { A, B, C, D, E };

        [SerializeField, Range(-1, 100)] int sceneIndex = -1;
        [SerializeField] Destination destination = Destination.A;

        private Transform SpawnPoint => transform.GetChild(0);

        private Portal OtherPortal
        {
            get
            {
                foreach (var portal in FindObjectsOfType<Portal>())
                {
                    if (name == portal.name)
                    {
                        continue;
                    }
                    if (destination != portal.destination)
                    {
                        continue;
                    }
                    return portal;
                }
                return null;
            }
        }

        void Start()
        {
            bool spawnPointInsidePortalBounds = GetComponent<BoxCollider>().bounds.Contains(SpawnPoint.position);
            Assert.IsFalse(spawnPointInsidePortalBounds, $"{name}: Spawn point must be outside the portal's bounds!");
        }

        void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Player"))
            {
                StartCoroutine(Transit());
            }
        }

        private IEnumerator Transit()
        {
            if (sceneIndex >= 0)
            {
                DontDestroyOnLoad(gameObject);
                yield return SceneManager.LoadSceneAsync(sceneIndex);
                TeleportPlayer();
                Destroy(gameObject);
            }
            else
            {
                Debug.LogError($"{sceneIndex} is not allowed as a scene index.");
                yield break;
            }
        }

        private void TeleportPlayer()
        {
            var player = GameObject.FindWithTag("Player");
            print(OtherPortal.name);
            player.GetComponent<NavMeshAgent>().Warp(OtherPortal.SpawnPoint.position);
            player.transform.rotation = OtherPortal.SpawnPoint.rotation;
        }
    }
}

Upon further inspection, when the scene loads asynchronously once the collider is triggered, the number of Portals (FindObjectsByType<Portal>().Length) becomes 3 instead of 2, and causes trouble with object references.

Although I never figured out why this particular code wasn’t working, I switched to a different solution, for now, involving string references to portals I’d want to travel to.

private void TeleportPlayerToMarkedPortal()
{
    var player = GameObject.FindWithTag("Player");
    var portal = GameObject.Find(portalToSpawnAt).GetComponent<Portal>();
    if (portal != null)
    {
        player.GetComponent<NavMeshAgent>().Warp(portal.SpawnPoint.position);
        player.transform.rotation = portal.SpawnPoint.rotation;
    }
}

How many portals do you have in each scene?

Does this print the right thing?

Could the SpawnPoint be null?

Two portals per scene. The Portal name would be the correct one if we were comparing by name (name == portal.name) inside the OtherPortal property. Otherwise, only the first portal would be chosen. This broke when we added the comparison by destination, as it returned null.

The SpawnPoint is never null since all portals share a Prefab, and there’s an assertion check to attest the spawn point’s position in Start.

A quirk of DontDestroyOnLoad in the newer versions?

So we would expect there to be 3 Portals if you have 2 in each scene. This is because the current Portal is DontDestroyOnLoad. So you should have 1 portal from the previous scene + 2 from the new scene. You are probably seeing that the portal from the current scene is getting selected in your for loop. You need to check that the portal isn’t the current portal as we did in the lectures.

This was done in “Player Spawn Point”.

Yes, I saw the portal existing beyond the scene load. Should get destroyed once done, of course.

As I said, doing that check didn’t work.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

I wonder why. Did you trying logging both the names being compared and the boolean value for the comparison?

After spending the afternoon doing tests with 6 portals I got it to work. :slight_smile:
I have added some checks and it has also helped me to use the WarpTo method suggested by a partner in the forum.

private Portal getOtherPortal()
        {
            foreach(Portal tmpPortal in FindObjectsOfType<Portal>())
            {
                if (tmpPortal.destino == destino && name != tmpPortal.name)
                {
                    if (tmpPortal.transform == this.transform) continue;
                    return tmpPortal;
                }
                continue;
            }
            return null;            
        }

private void UpdatePlayer(Portal otherPortal)
        {
            if (otherPortal == null) return;
            GameObject player = GameObject.FindGameObjectWithTag("Player");

            player.GetComponent<NavMeshAgent>().Warp(otherPortal.spawnPoint.position);
            //player.transform.position = otherPortal.spawnPoint.position;
            player.transform.rotation = otherPortal.spawnPoint.rotation;
        }

Just curious for this lecture why we don’t give each portal a specific identifier and load the destination via that? Eg:

public class Portal : MonoBehaviour
{
enum DestinationIdentifier
{
A, B
}

  [SerializeField] private Transform spawnPoint;
  [SerializeField] private DestinationIdentifier identifier;
  [SerializeField] private DestinationIdentifier destination;
  [SerializeField] private int sceneToLoad;
  
  private void OnTriggerEnter(Collider other)
  {
  	if (other.CompareTag("Player"))
  	{
  		StartCoroutine(Transition());
  	}
  }

  private IEnumerator Transition()
  {
  	DontDestroyOnLoad(gameObject);
  	yield return SceneManager.LoadSceneAsync(sceneToLoad);

  	Portal otherPortal = GetOtherPortal();
  	UpdatePlayer(otherPortal);
  	
  	Destroy(gameObject);
  }

  private void UpdatePlayer(Portal otherPortal)
  {
  	Transform portalWaypoint = otherPortal.transform.GetChild(0);
  	GameObject player = GameObject.FindWithTag("Player");
  	player.transform.position = portalWaypoint.position;
  }

  private Portal GetOtherPortal()
  {
  	foreach (Portal portal in FindObjectsOfType<Portal>())
  	{
  		if (portal.identifier == destination)
  			return portal;
  	}

  	return null;
  }

}

2 Likes

Yeah, that’s what I’m thinking. I don’t see this scaling well. In the actual game you’d want to make you’d probably have specific areas lead to other specific areas and link them manually based on that. I think that’s how I’m going to implement it. Sorry Sam.

Honestly, I’m really not happy with how this implemented in the course. I can see why you’re doing it this way but it seems like more trouble than it’s worth. This is the weakest part of the course so far. Everything else has been really good material.

Many have chosen to have both a identifier and a destination. It’s actually good thinking on their part. Not every factor of the course is fleshed out entirely and there is plenty of room in the course for you to find areas to improve and implement your own ideas. This is by design.

Hello this probably sounds crazy, but im running your code above and i put in print statments for debugging. I have the same problem as you were my player will seemingly load were i want at spawnpoint then after .5 sec revert to defualt. ive found that i get the correct portal with print statments. im also trying to do 4 portals.