How can I make 2D immersive portals in Unity?

Hi, I have developed an idea for a project within the Unity Engine that involves simulating Non-Euclidean geometry within a 2D space using immersive portals. I would like to be able to slice an object when it enters the portal as shown in the image below.

I am not particularly concerned with rendering views through portals, and the portals will be viewed from the side.

I am not entirely sure how to begin going about a project like this, and would appreciate some guidance.

If you can leave a reply, I will be most grateful.

You described what you don’t want to know about (rendering) but you didn’t describe specifically what you do want to know about. It’s an open question and hard to answer.

The simple version is to duplicate the object as it goes through the portal, particularly its visual components, the Sprite Renderer.

From your example, now you have two ladders. Then use a Sprite Mask to and update the SpriteRenderer with the proper mask settings. As the original ladder enters the portal it gets masked out and the second ladder exits the other portal, also exiting a mask.

You may need some extra fx to cover up a hard edge from the mask.

The very first step is to make a rough proof of concept in the editor where you manually move your objects around, coming in and out of the mask.

Yes, this ^ ^ ^ Make the ladder, clone it, make two rectangular sprite masks appropriate to each portal, and get it to at least manually look right. This should take like five minutes TOPS. Grab a blank piece of paper and a pen, scribble out some sprites, photograph them with your phone, and cut them out in Unity.

Once you have that up, save it. That’s your reference. Everything else is just sloggy legwork related to handling the ladder as a dynamic object, cloning it to the other portal, making game design choices to keep the portals from overlapping, etc.

GO!

Thank you all for the replies! I have completed the project. Well, the parts I wanted to complete.

I mainly wanted to see if I could simulate a sort of TARDIS effect in Unity and figured the portal thing would of been a good analogy. The only thing I didn’t really know how to handle was object collision when and object entered the portal.

I didn’t want to use meshes, and using duplicate objects would result in visible discrepancies, so I needed to determine how to mask a 2D collider.

I spent some time researching how I could mask a collider and found Angus Johnson’s clipper library.

I downloaded the library and wrote the following code that allows for masking of the PolygonCollider2D component.

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

public class TheProperPolygonClipper : MonoBehaviour
{
    [Header("Assigning")]
    public PolygonCollider2D subjCol; //The cookie dough
    public PolygonCollider2D clipCol; //The cookie cutter
    public PolygonCollider2D outputCol; //The cut cookie dough
    public enum MaskInteractionOps {VisibleInsideMask, VisibleOutsideMask }
    public MaskInteractionOps MaskInteraction;

    [Header("Debugging")]
    public List<Path> debugSubjPoints = new List<Path>();
    public List<Path> debugClipPoints = new List<Path>();
    public List<Path> debugClippedPoints = new List<Path>();

    float scale = 1000000;

    // Start is called before the first frame update
    void Update()
    {
        //Get tho polygon collider points and place them into lists of vector2's, floats, and scaled integers.
        List<Path> subjPoints = PolygonColliderPointsToPointData(subjCol);
        List<Path> clipPoints = PolygonColliderPointsToPointData(clipCol);

        debugSubjPoints = subjPoints;
        debugClipPoints = clipPoints;

        //Turn the points into paths64 values
        Paths64 subj = new Paths64();
        Paths64 clip = new Paths64();

        foreach (Path P in subjPoints)
        {
            subj.Add(Clipper.MakePath(P.scaledPoints.ToArray()));
        }

        foreach (Path P in clipPoints)
        {
            clip.Add(Clipper.MakePath(P.scaledPoints.ToArray()));
        }

        //Performs a clip
        Paths64 solution = new Paths64();
        if (MaskInteraction == MaskInteractionOps.VisibleOutsideMask)
        {
            solution = Clipper.Difference(subj, clip, FillRule.NonZero);
        }
        else
        {
            solution = Clipper.Intersect(subj, clip, FillRule.NonZero);
        }

        //Puts the cut points onto another collider
        List<List<Vector2>> pointsAfterClip = Paths64ToListListVector2(solution);
  
        AddPointsToPolygonCollider(pointsAfterClip, outputCol);
    }

    private List<Vector2> m_Path = new List<Vector2>();
    List<Path> PolygonColliderPointsToPointData(PolygonCollider2D PC)
    {
        List<Path> ListToReturn = new List<Path>();

        for (int i = 0; i < PC.pathCount; i++)
        {
            m_Path.Clear();

            PC.GetPath(i, m_Path);

            Path currentPath = new Path();

            foreach (var point in m_Path)
            {
                var worldPoint = PC.transform.localToWorldMatrix.MultiplyPoint(point);
                currentPath.PointsAsVector2.Add(worldPoint);
            }

            ListToReturn.Add(currentPath);
        }

        for (int i = 0; i < ListToReturn.Count; i++)
        {
            foreach(Vector2 V in ListToReturn[i].PointsAsVector2)
            {
                ListToReturn[i].pointsAsFloats.Add(V.x);
                ListToReturn[i].pointsAsFloats.Add(V.y);
            }
        }

        for (int i = 0; i < ListToReturn.Count; i++)
        {
            foreach(float f in ListToReturn[i].pointsAsFloats)
            {
                ListToReturn[i].scaledPoints.Add((int)(f * scale));
            }
        }

        return ListToReturn;
    }

    //A class containing the points as vector2's, the points as a list of floats, and a list of scaled ints.
    [System.Serializable]
    public class Path
    {
        public List<Vector2> PointsAsVector2 = new List<Vector2>();
        public List<float> pointsAsFloats = new List<float>();
        public List<int> scaledPoints = new List<int>();
    }

    List<List<Vector2>> Paths64ToListListVector2(Paths64 p)
    {
        List<List<Vector2>> listToReturn = new List<List<Vector2>>();
        for(int i = 0; i < p.Count; i++)
        {
            List<Vector2> pathList = new List<Vector2>();
            Path64 path = p[i];
            foreach(Point64 point in path)
            {
                pathList.Add(new Vector2(point.X / scale, point.Y / scale));
            }
            listToReturn.Add(pathList);
        }
        return listToReturn;
    }

    void AddPointsToPolygonCollider(List<List<Vector2>> points, PolygonCollider2D PC)
    {
        PC.pathCount = 0;

        for (int i = 0; i < points.Count; i++)
        {
            List<Vector2> worldPath = points[i];
            Vector2[] localPath = new Vector2[worldPath.Count];

            for (int j = 0; j < worldPath.Count; j++)
            {
                localPath[j] = transform.InverseTransformPoint(worldPath[j]);
            }

            PC.SetPath(i, localPath);
        }
    }

}


From there, it’s just a matter of assigning a few collision layers, and I can simulate the TARDIS effect rather well.
Majik box :)
Thank you all for the responses nontheless. :slight_smile:

The code you wrote is exactly the same as the code I want to make. Could you explain how you did it in a bit more detail? I’ve been trying to do this for days.

This isn’t really portals, just basic masking with some frills. But, either way its cool to see you figured out something that works for your use case.

It doesn’t matter if it’s a fake portal. I want to do exactly what’s shown in the GIF. Thank you for informing.

Sure!

The code uses an external coding library called Clipper2, which allows you to input two polygons, and get things like intersections. (You could technically make this without Clipper2, but it would lead to strange interactions as the object is passing inside and outside of the box.)

The script takes three different polygon colliders:

  • Subject collider – The ‘normal’ collider of the object. In the example in the GIF, this would be a trigger collider attached to the child.
  • Clipping collider – Another collider that acts as the mask for the subject collider. In the example in the GIF, this would be a trigger collider representing the inside of the box.
  • Output collider – The collider that hosts the masked subject collider. In the example in the GIF, this would be a non-trigger collider attached to the object.

The script takes all the points in the subject collider and the clip collider, converts them into Clipper2 variables, performs a clip, and puts the output back into the output collider

In the example, the output collider uses two different scripts. One for the ‘normal’ collider that interacts with the outside, and another for the collider that interacts with the ‘hidden space’ (interior) of the box.

The hidden space collider has a collision layer assigned to it that only interacts with objects of the hidden space layer.

This allows for the object to smoothly transition colliders as it passes inside and outside of the box.

This is a surface level explanation, but I uploaded a demo project of the script to GitHub if you’d like to have a look around in it.

The script is still rather limited:

  • Having more than one box into the scene can lead to abnormal interactions. (Hidden spaces can intersect with one another)
  • You can only clip the collider with one collider at a time. (Can’t have an object in two boxes at once).
  • If an object inside of the box moves too fast, it can clip outside and begin interacting really weirdly.
  • You need to manually assign whether the object starts in the box or not.
  • The demo uses a sprite mask to hide the object. Any object with a sprite mask will always interact with the box

But it acts as a great starting point. I hope you find it useful!

Note that you can do this for any 2D collider using the CompositeCollider2D without external code but you need Unity 6000.0+ to do it. Also, getting the info from a sprite might be awkward.

It supports CompositeOperations and they look familiar because it uses Clipper to do it and Libtess to decompose it back to polygons or outlines (edges).

At least in the future you’ll be able to do this without limit in Unity 6000.3 because there’s a dedicated PhysicsComposer that even works off the main-thread which doesn’t care about where the geometry is coming from.

Thank you very much for your help and for sharing the project. I couldn’t find many examples on this topic; this was exactly what I needed.