A solution for portal-linking that worked for me

Like others I ran into some headaches with these lectures related to portals and portal linking, and I also tried to implement my own solution for linking portals more flexibly. Here’s what I came up with:

I implemented the Portal prefab as Sam did, with the following changes:

  • Renamed DestinationIdentifier enum to PortalId
  • Renamed sceneToLoad to destinationMap
  • Added another [SerializeField] of type PortalId, named simply portalId (for this Portal)
  • Added an enum named Map, with values assigned based on build indexes of each scene

This allows a few things which I don’t think are possible in the solution presented in the videos:

  • One-way portals
  • Portals connecting to other portals within the same map/level/scene
  • Ability to specify maps in Unity by name rather than by index:
    2023-06-14_16-58-48

A few things to note in the code below:

  • I typed the Map and Portal enums as short and byte respectively; I doubt it would hurt much to leave these as their default type (int)
  • In OnTriggerEnter … If we’ve entered the destination of a one-way portal, return without doing anything
  • In Transition … Compare the build index of the current scene to the (numeric value of) destinationMap. If they’re the same, this is a portal to another within the same map, and therefore we don’t have to [re]load the same scene.
  • In UpdatePlayer … I implemented the fix based on other comments here, and in Sam’s video revision: that is, I’m using calling Warp on the player’s nav mesh agent to move them.
  • In GetOtherPortal … I use FindObjectsByType rather than FindObjectsOfType, since Unity documentation states that FindObjectsOfType is deprecated in current Unity versions.
  • Also in GetOtherPortal … I loop over all active Portal instances, but I check that the scene index of each equals that of the current scene. I found this necessary, because the “source” portal remains active — probably because we usually tell it to DontDestroyOnLoad. If we didn’t do this check, and the source portal’s id matches that of the destination, we’ll try to move to ITS spawn point rather than the one in the new map.

I don’t claim that this is an optimal solution, but it’s been working for me so far. Hope this helps someone!

Here’s my entire Portal.cs listing, copiously commented:

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

namespace RPG.SceneManagement
{
   // NOTE: Keep these enum values (other than None) synced with scene ids defined
   //       in File | Build Settings... | Scenes In Build
   public enum Map:short {
      None = -1,
      Village = 0,
      Mountains = 1
   }

   // Ids you can apply to portals in a single map. Of course you could
   // rename these or add even more, but 26 portals per map gives plenty 
   // of options.
   public enum PortalId:byte { 
      None, A, B, C, D, E, F, G, H, I, J, K, L, 
      M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z 
   };

   public class Portal : MonoBehaviour
   {
      // NOTE: a portal's id should never remain None
      [SerializeField] PortalId portalId = PortalId.None;

      // NOTE: if this portal is just the destination of a one-way portal,
      //       then destinationMap could remain None with further checks
      [SerializeField] Map destinationMap = Map.None;

      // id of the destination portal in the destination scene
      // ...likewise, if this portal is just the destination of a 
      //    one-way portal, destinationPortalId can remain None.
      [SerializeField] PortalId destinationPortalId = PortalId.None; 

      // reference to the portal instance's spawn point, assigned in Prefab editor
      [SerializeField] Transform spawnPoint;

      void OnTriggerEnter(Collider other) {
         // if this is a one-way destination, don't do anything
         if (destinationMap == Map.None) return;

         // otherwise do the transtion if the collider is tagged as Player
         if (other.gameObject.CompareTag("Player"))
         {
            StartCoroutine(Transition());
         }
      }

      private IEnumerator Transition()
      {
         // get the current scene's build index
         int sceneBuildIndex = SceneManager.GetActiveScene().buildIndex;
         
         // get the build index of the destaintion map
         short destMapIndex = (short)destinationMap;

         // determine whether we're moving to a new map
         bool movingToNewMap = sceneBuildIndex != destMapIndex;

         //only load new scene if we're moving to a new map
         if (movingToNewMap)
         {
            // NOTE: DontDestroyOnLoad only works when gameObject is at scene root!
            DontDestroyOnLoad(gameObject);

            // load new scene asynchronously; wait until fully loaded
            AsyncOperation loadingScene = SceneManager.LoadSceneAsync(destMapIndex);
            while (!loadingScene.isDone) yield return null;
         }

         // after new scene loads find the other portal
         Portal otherPortal = GetOtherPortal();
         
         // update player location and rotation based on the portal found
         UpdatePlayer(otherPortal);

         // finally destroy this portal object, but only if we've moved to a new map
         if (movingToNewMap) Destroy(gameObject);
      }

      private void UpdatePlayer(Portal otherPortal)
      {
         // get references to player and their nav mesh agent
         GameObject player = GameObject.FindWithTag("Player");
         NavMeshAgent navMeshAgent = player.GetComponent<NavMeshAgent>();

         // move and rotate player based on the destination portal's spawn point
         navMeshAgent.Warp(otherPortal.spawnPoint.position);
         player.transform.rotation = otherPortal.spawnPoint.rotation;
      }

      private Portal GetOtherPortal()
      {
         // get the current scene's build index
         int sceneBuildIndex = SceneManager.GetActiveScene().buildIndex;

         // loop through active Portal instances
         foreach (Portal portal in FindObjectsByType<Portal>(FindObjectsSortMode.None))
         {
            // if the portal's scene's build index doesn't match the current one,
            // don't consider it. This checkl is necessary because the "source" portal
            // is among the portals returned by FindObjectsByType ... probably because
            // we told it to DontDestroyOnLoad.
            if (sceneBuildIndex != portal.gameObject.scene.buildIndex) continue;
            
            // if the id of the portal we're looping over now matches that of the
            // source portal's destination, then we found the one we want to move to.
            if (portal.portalId == this.destinationPortalId) return portal;
         }
         return null;
      }
   }
}

Have you considered putting your notes on the [SerializeField] variables into [Tooltip]s that will be shown in the inspector? :wink:

1 Like

That’s a great idea … anything to make in-editor usage more straightforward is quite welcome!

Privacy & Terms