Tutorial Details
- Difficulty: Intermediate
- Platform: Unity3D
- Language: C#
- Software Used: Sprite Manager 2, Vectrosity
- Estimated Completion Time: Two hours
- Build a 2D Portal Puzzle Game With Unity: Getting Started
- Build a 2D Portal Puzzle Game With Unity: Adding the Portals
- Build a 2D Portal Puzzle Game With Unity: Portals and Game Mechanics – Active Premium
- Build a 2D Portal Puzzle Game With Unity: More Mechanics and New Levels – Active Premium
At the end of the first part of this series, we had just created a new shader, which will allow us to hide part of the main character’s sprite when it is part-way concealed by a portal. In this part, we’ll put that to use as we create a portal prototype.
Final Result Preview
Let’s take a look at the final result we will be working towards, across the whole of this multi-part tutorial:
Hit the number keys 1-8 to try different levels. The aim is to get the little yellow Roly character to the designated end point, using portal mechanics. The demo shows off a few of the different mechanics we’ll introduce.
It’s going to take us a while to get to that point, though! In this second part of the tutorial, we’ll start work on adding the actual portal objects. Click here to see what we’ll have built by the end of this part.
Step 1: Create a Portal
It’s time to create an early portal prototype. Here’s a texture we’ll use to build it.

It’s very small, but we will stretch so the portal will look OK. Create a sprite the way you did before, but this time instead of selecting Pixel Perfect, select only Auto Resize. Then change the height of the sprite to 128 and width to 12. Also, you may assign Tiles Material to it.
Now add Box Collider to our portal. Do it the same way we did before, but this time let’s check Is Trigger, because we don’t want our collider to be solid, we want the ball to be able to go through it. Also remember to set it’s z size to 30, so all objects on the ground can detect the collision with it.

The next thing to do is to create a script and name it Portal. This will be the script we attach to the portal, it will store portal related data, such as a linear equation of an axis, its height and what kind of portal does it lead to. Of course you should place this script in the Game Scripts folder.
Step 2: Set the Portal’s Height
First thing we need to do is to clean up the script. Change the class’s name to Portal and delete comments.
using UnityEngine;
using System.Collections;
public class Portal : MonoBehaviour
{
void Start ()
{
}
void Update ()
{
}
}
First thing that we need to know is height of the portal. We don’t really want to force using sprite’s height as portal’s height, because that will force a correlation between them. What we can use, is our collider’s height, because it can be changed without any visual influence. We need to create a variable that will hold the height for us.
using UnityEngine;
using System.Collections;
public class Portal : MonoBehaviour
{
private float height = 0.0f;
Before we set our height, let’s create a variable that will hold a reference to our PackedSprite component. Since it’s not a unity class, we can’t simply use packedSprite, because it’s not existant. First thing we need to do is to create the variable that we will assign our PackedSprite component to.
private float height = 0.0f; private PackedSprite sprite;
To assign the reference, we need to use GetComponent() function. It will find the any component we need thats attached to our object, and return a reference to it.
void Start ()
{
sprite = GetComponent<PackedSprite>();
Now we can set our height.
void Start ()
{
sprite = GetComponent<PackedSprite>();
height = ((BoxCollider)collider).size.y*transform.localScale.y;
}
Notice that first we need to cast our collider to BoxCollider. That’s because BoxCollider is a specific collider, and the collider reference points only to the class our BoxCollider inherits from. Notice that we can acces size only if we have a reference to BoxCollider, that’s why we need to cast. Of course we need to multiply the height by the scale of our object, because even when we scale the object, the size of the box collider remains the same.
Now, how do we figure out the axis the portal goes through? We need to have two points, one at the bottom and second at the top of the portal. If we’ve got that, we can easly figure out portal’s parameters.
Step 3: Calculate the Portal’s Vertices
Let’s calculate top and bottom vertices now. There is a usefull Bounds class in collider, that has the data on the volume of our collider in the world space, but we won’t use it, because it would be a bit tricky to calculate the top and bottom vertices using the volume of our object. Instead, let’s first create vertices as if there was no rotation applied to our portal, and then rotate them by the same amount the portal is rotated.

Let’s start from creating two Vector3s.
private float height = 0.0f; private PackedSprite sprite; private Vector3 top; private Vector3 bottom;
Let’s take care of the top vertex first. If we’ve got that, then we can easly calculate the bottom because it’s basically a mirror image of the top.
height = ((BoxCollider)collider).size.y*transform.localScale.y; top = new Vector3(transform.position.x, transform.position.y + height, transform.position.z);
We simply set the top to the top of our unrotated portal. Now, if portal is rotated we need to rotate it by the same amount around the center. That means we need a function that will rotate a point around another point by the angle we want, and return the rotated result.
public Vector2 RotatePoint(Vector3 basePoint, Vector3 sourcePoint, float rotationAngle)
{
float s = Mathf.Sin(rotationAngle*Mathf.Deg2Rad);
float c = Mathf.Cos(rotationAngle*Mathf.Deg2Rad);
// translate point back to origin
sourcePoint.x -= basePoint.x;
sourcePoint.y -= basePoint.y;
// rotate point
float xnew = sourcePoint.x * c - sourcePoint.y * s;
float ynew = sourcePoint.x * s + sourcePoint.y * c;
// translate point back:
sourcePoint.x = xnew + basePoint.x;
sourcePoint.y = ynew + basePoint.y;
return sourcePoint;
}
You must have met with this kind of function many times now. Since rotating around the center of the coordinate system is much simpler than rotating around a specific point, that’s why we first offset both points (the point we rotate and the point we rotate around) by the point we rotate around. This way, the point we rotate around is at the center of the coordinate system, so we can simply rotate around it the simple way, remembering that later we need to move back our point by the same amount we did offset it earlier. To calculate the Sin and Cos of our angle we need to use our Mathf class again. We also need to remember that both functions take radians as the input, so we need to multiply them by Mathf.Deg2Rad constant. Having all that, it’s very simple to use rotation around the origin formula. Again, after we use it we need to move the returned point back by the same amount we moved it in the first place.
Now let’s rotate our top vertex.
height = ((BoxCollider)collider).size.y*transform.localScale.y; top = new Vector3(transform.position.x, transform.position.y + height/2.0f, transform.position.z); top = RotatePoint(transform.position, top, transform.eulerAngles.z);
As you can see, to get the angles of our portal we need to access transform.eulerAngles. Of course since we work in 2D, we only allow rotation around the z axis. Since bottom is a mirror image of the top, it is equal to the transform.position minus the distance between the top and transform.position.
height = ((BoxCollider)collider).size.y*transform.localScale.y; top = new Vector3(transform.position.x, transform.position.y + height/2.0f, transform.position.z); top = RotatePoint(transform.position, top, transform.eulerAngles.z); bottom = transform.position - (top - transform.position);
And that’s it, we’ve got our vertices.
Step 4: Calculate the Portal’s Factors, Part 3
Let’s create additional variables that will hold our equation’s factor.
private float height = 0.0f; private PackedSprite sprite; private Vector3 top; private Vector3 bottom; [HideInInspector] public float a; [HideInInspector] public float b;
As you can see, the new varriables are public. That’s because we may want to access them from other scripts, technically we should make them private and then provide functions that return their values, but that’s a lot of hassle for a one-man project. We set those variables in script, so we don’t want to expose them in the inspector, that’s why before declaring each of variables we need to put [HideInInspector] attribute. You can read more on attributes at this unity docs page. Now we can calculate our factors. If you played with the shader, you should have noticed that we can use it well only if _A + _B is equal to 1. That’s because texture UVs are from 0 to 1, and if the sum of the factors is higher than 1.0, then the scale is too big to handle for our _Cutoff, which ranges only from -1 to 1. That’s why we need the factors to sum up to 1. First things first, let’s calculate the first factor, a. It simply dictates how fast does the slope rise in the y axis. If the a factor is really high, and in comparison the b factor is very low, then you may be sure that the slope will be very steep. When the situation is reversed, the slope will be very flat instead. First Step NaNwould be to set the factors so they represent the slope nicely, we already calculated top and bottom vertices, and we know that the segment they create is part of the line we want to have an equation of. That will do.
bottom = transform.position - (top - transform.position); b = top.x - bottom.x; a = bottom.y - top.y;
Now let’s go on with the translation. I explained that we need the sum of first two factors to be one, but that isn’t entirely true. Because factors can be positive and negative values, they just can’t always be equal to one. It’s their absolute values that always must be equal to one in our case. Let’s translate the a factor first.
bottom = transform.position - (top - transform.position); b = top.x - bottom.x; a = bottom.y - top.y; a = a / (Mathf.Abs(a) + Mathf.Abs(b));
It’s very simple, we just see what part is a of a sum of both factors. Now to figure out the second factor. We should use the very same way to do that, but our a has already been translated, therefore it’s no longer usable for this method. We need to create a temporary variable that will hold the value of a, before it’s translated.
bottom = transform.position - (top - transform.position); b = top.x - bottom.x; a = bottom.y - top.y; float tmpA = a; a = a / (Mathf.Abs(a) + Mathf.Abs(b));
And now we can calculate our b, using tmpA instead of already translated a.
float tmpA = a; a = a / (Mathf.Abs(a) + Mathf.Abs(b)); b = b / (Mathf.Abs(tmpA) + Mathf.Abs(b));
Step 5: Going Through Portal
Now it’s the time to create a class which will automatically cut off part of the sprite according to it’s distance to portal, rotation and so on. We’ll first get it to work for non-rotated sprites, and then extend its capabilities. For ease of testing, let’s make our ball Kinematic for now. It will be easier to move it around in the scene window. The Is Kinematic checkbox is in the rigidbody component.

Now let’s create a script for the objects which can use portals. Again, clean it up and let’s start working on it right away.
using UnityEngine;
using System.Collections;
public class Portalable : MonoBehaviour
{
void Start ()
{
}
void Update ()
{
}
}
Now let’s create our cutoff variable. Let’s call it gCutoff.
public class Portalable : MonoBehaviour
{
private float gCutoff = 0.0f;
Of course we’ll also need a reference to our sprite.
private float gCutoff = 0.0f;
private PackedSprite sprite;
void Start ()
{
sprite = GetComponent<PackedSprite>();
Another reference we will need, is to the portal our sprite currently goes through.
private float gCutoff = 0.0f; private PackedSprite sprite; private Portal portal;
We can set this reference when our sprite collides with the portal. How do we know it does? It’s pretty simple, there’s a callback called OnTriggerEnter(), which is called whenever our objects enters a trigger. Since our portal object has a collider that is a trigger, we can use this function. But how do we know that we entered the portal object, not some other object that also has a trigger collider? That’s also very simple, we just need to check whether the object we entered has a Portal script component attached to it.
void OnTriggerEnter(Collider other)
{
}
The function takes Collider other as its argument, it is a reference to the collider we intersected with. Now let’s check whether it has a Portal script attached or not. If it does, let’s set our portal to it, if it doesn’t let’s simply return, because in this script we will be handling only intersections with portals. Intersections with other objects will be the job of other scripts.
void OnTriggerEnter(Collider other)
{
if (other.GetComponent<Portal>() != null)
portal = other.GetComponent<Portal>();
else
return;
}
Now let’s create our SetCutoff() function. It should automatically set the amount of sprite it needs to cut. For that, we need to know what is our distance from the portal, or rather, what is our distance from the portal’s axis.
void SetCutoff()
{
if (portal == null)
return;
}
In case we try to SetCutoff() when we are not even intersecting with a portal, we simply ignore the request. Now to calculate our gCutoff, we need the distance mentioned earlier. The best place to create a function which returns it seems to be the Portal class, so let’s go back to the Portal script.
Step 6: Calculate the Distance
To start off, we need to get a formula that will help us calculate the linear equation. We’ve got the factors and the point we need to go through, so this won’t be hard at all. Keep in mind that our original axis equation is Ax + By = C throughout this step. Here’s the formula I found, y = a*(x - x0) + y0. The x0, y0 are the coordinates of the point we want the axis to go through. It assumes that our b factor is equal to 1, but that’s not a problem at all. We need to find the C factor, so by messing with equation a bit we’ll get that C = a*x0 + y0. Of course the a from this formula isn’t the same us our a from portal axis, because the a from the formula is a factor when b is equal to 1. Taking that into consideration, we can simply calculate that the formula’s a factor is equal to a/b, where a and b are our portal’s factors. Now we can calculate the c factor. First, let’s create a variable for it.
[HideInInspector] public float a; [HideInInspector] public float b; [HideInInspector] public float c;
And now calculate it.
a = a / (Mathf.Abs(a) + Mathf.Abs(b)); b = b / (Mathf.Abs(tmpA) + Mathf.Abs(b)); c = a/b*transform.position.x + transform.position.y;
And that’s it. Now let’s create our Dist() function, it will return the distance from the point to our portal’s axis.
public float Dist(Vector3 p)
{
}
To calculate the distance we need to use Mathf.Abs(Ax + By + C)/Mathf.Sqrt(A*A + B*B) formula, but we need to tweak it here and there so it’s adequate to our original equation, because this formula assumes that Ax + Bx + C = 0. If we go with a directional linear equation, like the one we just used, we need to make B factor be equal to 1. Then, the A will be equal to a/b, because it’s the same a we used with the previous formula. This way, our factors for the distance formula will be: A = a/b, B = 1.0 and C = c. That’s it, now we only need to use the formula.
public float Dist(Vector3 p)
{
return (a/b*p.x + p.y - c)/Mathf.Sqrt((a/b)*(a/b) + 1.0f);
}
Notice that we skipped the Math.Abs(), so it may give us a negative distance result. We did it because we want the distance to help us differentiate from which side of the portal the object enters. It will be essential later on, because the object going through portal must exit another portal through adequate side, for example, if we’ve got green and red sides and if the object enters through the green one, it should exit from the green one too. Another thing we need to be careful of is division by zero. In our case, if the b factor is equal to 0, then the division by zero will occur, because c factor is equal to a/b. To prevent returning a wrong distance in this case, we need to handle this case separately.
public float Dist(Vector3 p)
{
if (b == 0.0f)
return (p.x - transform.position.x);
else
return (a/b*p.x + p.y - c)/Mathf.Sqrt((a/b)*(a/b) + 1.0f);
}
b is equal to zero only if the portal is completely unrotated, or rotated by 180 degrees. The distance in these cases will be equal to the difference in position on the x axis. Again, we don’t want to use Mathf.Abs() here, because we want to differentiate between the portal’s sides.
Step 7: Going Through Portal Part 2
Now since we’ve got our distance, it’s time to test the function out. Let’s go back to our Portalable script, and use it to calculate the gCutoff. First, let’s save the distance to a variable named dist.
void SetCutoff()
{
if (portal == null)
return;
float dist = portal.Dist(transform.position);
}
Now let’s make our dist appear in our console
void SetCutoff()
{
if (portal == null)
return;
float dist = portal.Dist(transform.position);
Debug.Log(dist);
}
Now we need to call the SetCutoff() function so the Debug.Log(dist) will be executed. We will do so in the Update() function.
void Update ()
{
SetCutoff();
}
Let’s go back to the editor and hit play. In the debug console we’ll be able to see the distance from the portal’s axis. You can rotate the portal around to test whether other stuff we calculated is also correct. If the distance appears to be correct, we can move on with our function. Remember that our sprite need a Portalable script attached, and portal needs Portal script attached to it. Also, the reference to the portal is set upon an intersection with it, so first we need to move our sprite to intersect with the portal.


It works. You can use Debug.Log() whenever you want to check if something works or not. If you know it’s not working and want to know why, I recommend using classic debugging. Now you can delete the Debug.Log(dist) line.
Now we need to know how much of a sprite should we need to cut off along the portal’s axis.

As you can see, in this case the issue isn’t very difficult to solve, partly because we’ve got our signed distance. In the first case, where the smaller part of a sprite is invisible, we can see that the pink length, the one we need to calculate is equal to half of the green length minus the blue length, the distance between our sprite and the portal. In the second case, it’s pretty much the same but instead of substracting the distance, we add it up. Our distance is signed, that means we can simply add the distance in both cases, because then in the first case it will add a negative value, which is the same as substracting, and in the second case it will add a positive value. Of course we will need to be careful to not do the both sides the wrong way, that is substracting when we need to add and adding when we need to substract, because it depends from which side does our sprite start out. We’ll take care of it soon enough. Now for the more general case.

As you can see, it looks like we need to calculate here things the same way, but there appears another issue. This time, the length of the sprite isn’t equal to simple width of the sprite, this time it’s a diagonal. You can imagine that this length will be different, and it depends on how the portal is rotated. Fortunately, the calculations of the right length is not that hard. We need to use projections. Metanet has a good, interactive basic geometry appendix, so if you have no idea how do we use projections, you should check it out.
Step 8: Going Through Portal Part 3
The first thing we need to do is to create a unit vector that will represent our portal’s axis. Here’s a little catch, we don’t really need our unit vector to reasemble the portal’s axis, we just need it to project our sprite’s diagonal on it and get the length of the projected vector. Since we don’t really want to use the world-space diagonal of our sprite, we also don’t want to use accurate portal’s axis. That’s why we should simply use absolute values of our portal’s factors instead of real values. If we wouldn’t do that, the projected length would be messed up more or less, depending on what kind of axis would we throw our diagonal on.
void SetCutoff()
{
if (portal == null)
return;
float dist = portal.Dist(transform.position);
Vector2 dir = new Vector2(Mathf.Abs(portal.a), Mathf.Abs(portal.b));
}
Now we want to convert our dir into a unit vector
Vector2 dir = new Vector2(Mathf.Abs(portal.a), Mathf.Abs(portal.b)); dir.Normalize();
Now let’s create a vector that will represent the size of our sprite.
Vector2 dir = new Vector2(Mathf.Abs(portal.a), Mathf.Abs(portal.b)); dir.Normalize(); Vector2 size = new Vector2(sprite.width, sprite.height);
And finally, it’s time to project our size vector onto the unit vector. Of course, before we do that, we need to do is to create the projection vector.
Vector2 dir = new Vector2(Mathf.Abs(portal.a), Mathf.Abs(portal.b)); dir.Normalize(); Vector2 size = new Vector2(sprite.width, sprite.height); Vector2 proj = new Vector2();
And now apply the formula to calculate it.
Vector2 proj = new Vector2(); proj.x = Vector2.Dot(size, dir)*dir.x; proj.y = Vector2.Dot(size, dir)*dir.y;
And that’s it, we’ve got our projection. Now we need to calculate our gCutoff, and we’re done!
Vector2 proj = new Vector2(); proj.x = Vector2.Dot(size, dir)*dir.x; proj.y = Vector2.Dot(size, dir)*dir.y; gCutoff = dist/proj.magnitude + 0.5f;
We just calculate it the way I explained before. We don’t need additional conditionals because our dist can carry a negative value. All in all, it sums up to this, it’s just a question of how much of the sprite is between its center and the portal. Then of course we need to cut additional half, so we cut along the portal’s axis, not somewhere else.
Step 9: Get the UVs
We still need to do a couple of things before we can see any interesting results. We need to calculate the minimum and maximum cutoff value. You know that the texture coordinates range from 0 to 1. What we calculated in the previous step, is the amount of the cutoff we need to apply to a single frame, not the whole texture, in which many frames can sit. That’s why we need to calculate where on the texture should we start cutting the frame, and where we finish doing so. First of all, we need UVs of our current frame, and that’s the first thing we should do, get them. Let’s not continue the previous function, because it already finished doing what was it supposed to do, let’s create another and call it UpdateCutoff.
void UpdateCutoff()
{
}
Now let’s create a Rect that will hold our sprite’s UVs.
void UpdateCutoff()
{
Rect uvs = new Rect();
}
We need to set it to the current animation frame UVs or if we’re not playing any animation, to static frame UVs.
void UpdateCutoff()
{
Rect uvs = new Rect();
if (sprite.GetCurAnim() != null)
uvs = sprite.GetCurAnim().GetCurrentFrame().uvs;
else
uvs = sprite.DefaultFrame.uvs;
}
As you can imagine, GetCurAnim() returns a null if no animation is played, and the UVAnimation class when we do play any. To get current frame we need to call GetCurrentFrame() function, and from the frame we can access uvs. When we don’t play anything we need to access our sprite’s DefaultFrame, and then we can get uvs from it.
Step 10: Going Through Portal Part 4
Now let’s create the variables that will hold our extreme cutoff values.
void UpdateCutoff()
{
Rect uvs = new Rect();
if (sprite.GetCurAnim() != null)
uvs = sprite.GetCurAnim().GetCurrentFrame().uvs;
else
uvs = sprite.DefaultFrame.uvs;
float cutoffMin, cutoffMax;
}
First, let’s study how are we going to calculate those extremes. Let’s go with an example.

In this example our sprite is currently displaying the fourth frame and it’s going through portal. We need to know how much of cutoff to apply if we want to cut the texture up to the point where it touches the frame but doesn’t cut anything and to the point that it ideally cuts the whole frame and doesn’t cut any more of the texture.

As you can see, we simply need to find where the given portal axis would intersect with our sprite’s UVs. There’s a catch here, depending on how the portal axis looks, we need to know where it intersect with different UVs. In this example the axis would first intersect with the bottom-left UV, and the last one it would touch would be the top-right. Now let’s see another example, where portal axis looks a bit differently.

As you can see, this time when we start cutting the frame, the texture is cut up to the top-left UV of our frame. When it finishes cutting the whole frame, it’s at the bottom-right UV.
Those are the only options we have, so we need two cases to prepare. First one, when the extremes will be the bottom-left and top-right UVs, and the second one when the extremes will be the bottom-right and top-left UVs. So how will our condition look? We need to visualise what kind of angles will result with which extreme points.

Portal’s rotation will help us to make a good condition. As you may have noticed, the extremes are bottom-right and top-left when angles range from 90 to 180 and from 270 to 360. The other cases have bottom-left and top-right extremes. Let’s start creating our condition. Of course using the angle values doesn’t seem a very good idea, portal could be at angle equal to 1000 or more. That’s why we should use trigonometric functions. As you know, the Tan() function returns negative values only for the 2nd and 4th quarters of the coordinate system. That’s exactly when we need to calculate the bottom-left and top-right extremes.
float cutoffMin, cutoffMax;
if (Mathf.Tan(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
{
}
else
{
}
Always remember to use Mathf.Deg2Rad constant when you’re using trigonometric functions and deal with euler angles. Alright, so how do we calculate the cutoffMin? It’s pretty simple, since the cutoff is basically a C factor in a linear equation, we know that it is equal to x*A + y*B. That’s because our equation is in this form: x*A + y*B = C. If we want to calculate the cutoffMin, that mean we need to substitute the x with the first extreme’s x value and y with first extereme’s y value.
Before we’ll get right to the meat, here’s a quick reminder how the UVs positions work. Here’s how UVs on the texture look like.

Notice that the top is actually 0, and the bottom is 1. The lower the point on the texture, the higher y value it has. Now let’s see how does it look if we’re dealing with a frame, not a whole texture.

As you can see, it’s pretty similar. The only difference is we don’t deal with whole texture but rather with a specific quad in the texture.
Step 11: Going Through Portal Part 5
Now we can calculate our minCutoff.
if (Mathf.Tan(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
{
cutoffMin = uvs.xMin*portal.a + (uvs.yMin + uvs.height)*portal.b;
}
else
{
}
As you can see, we simply use our linear equation to see where does our line intersects with the point we specified. We’re going to do the same to calculate cutoffMax, but this time we’re going to substitute the last extreme.
if (Mathf.Tan(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
{
cutoffMin = uvs.xMin*portal.a + (uvs.yMin + uvs.height)*portal.b;
cutoffMax = (uvs.xMin + uvs.width)*portal.a + (uvs.yMin)*portal.b;
}
else
{
}
For the second case, where the extremes are different we simply substitute those different extreme’s into our equation
if (Mathf.Tan(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
{
cutoffMin = uvs.xMin*portal.a + (uvs.yMin + uvs.height)*portal.b;
cutoffMax = (uvs.xMin + uvs.width)*portal.a + (uvs.yMin)*portal.b;
}
else
{
cutoffMin = (uvs.xMin + uvs.width)*portal.a + (uvs.yMin + uvs.height)*portal.b;
cutoffMax = uvs.xMin*portal.a + uvs.yMin*portal.b;
}
And that’s it. We need to do only one more thing to see the results. We need to submit the values we have to the shader, so it can work correctly.
Step 12: Feed the Shader
As you probably remember, the shader we created had a few specific Properties, the linear equation’s factors. How do we set those? It’s very simple, we only need to use our Renderer component to access the material we are currently using. Once we have that, we can change any properties that are defined in the shader which our material uses. Here’s how we do it.
renderer.material.SetFloat("_A", portal.a);
renderer.material.SetFloat("_B", portal.b);
One other thing you should be aware of. If we tweak the material in the way we just did, the unity will automatically copy it for the object that modified the properties. If it didn’t do that, then two sprites would use the very same material, and they would both tweak it to their custom needs resulting in one sprite’s shader properties being overwritten by another. Just a note that you don’t actually have to worry about this because it’s handled automatically.
Now we need to submit our _Cutoff property to the shader. How to calculate that? Well, we’ve got our extreme cutoff values, and also we know how much of a sprite we need to cutoff, because that’s what gCutoff represents. So for example, if we want to cutoff half of the sprite, what would have to be the value of _Cutoff? Of course it would have to be the number exactly inbetween of cutoffMin and cutoffMax. If we wouldn’t want to cut the sprite at all, we would want the _Cutoff to be equal to cutoffMin, and if we wanted to cut the whole sprite then we would need _Cutoff to be equal to cutoffMax. We basically need to use linear interpolation here.
renderer.material.SetFloat("_A", portal.a);
renderer.material.SetFloat("_B", portal.b);
renderer.material.SetFloat("_Cutoff", Mathf.Lerp(cutoffMin, cutoffMax, gCutoff));
Mathf.Lerp() simply finds the number between cutoffMin and cutoffMax, basing on the step, which in our case is gCutoff. The Step NaNindicates the distance between two extreme values. So for example, if we use a Mathf.Lerp() on the values 25 and 50 with the Step NaNequal to 0.5, then the returned value will be equal to 37.5, exactly in between the two given numbers.
Let's not forget to call our function after we set the gCutoff.
gCutoff = dist/proj.magnitude + 0.5f; UpdateCutoff(); }
Step 13: Going Through Portal Part 6
Let's test out our portal now.

Looks pretty good behind the thick portal. Let's move our sprite in front of it so we can see if the cutoff is as accurate as it should be.

It looks alright. Since our code is angle dependant, we should check if everything's alright for different angles too.

It looks like everything works just fine. Of course we're far from finishing the portals, but that's one huge Step NaNforward. Note that for now we only let the green side of the portal to be the 'visible side', for now we shouldn't really care which side is visible or not, but we'll have to do so later on. From now on, we'll try to make a rotated sprites work with our shader.
Step 14: Rotated Sprites
First, let's visualize how do we need to cut our texture if the sprite is rotated. A few sketches will surely help with that.
As you can see, there's some kind of pattern here. If you haven't noticed it by looking at this sketch, another picture one should make everything clear.

The portal's rotation is equal to -45 euler angles. As you can see, the most interesting thing now is that the portal at -45 degrees cuts off the same part of sprite as the second case from the previous image. Now, let's consider yet another example.
As you can see, if we want to properly display rotated sprite, we need to cut the texture as if the portal angle would be equal to portal's z euler angles minus sprite's z euler angles.
Step 15: Factors Class
We want many objects to be able to use one portal, that's why we can't calculate the factor's for a specific sprite's rotation. Every sprite will need to keep its own, custom portal factors that are adequate to its rotation. It would be a good idea to gather all specific factors in one class for clarity. Let's start from creating a new class inside of Portal script. Let's name it Factors.
public class Portal : MonoBehaviour
{
public class Factors
{
}
Why do we create a class instead of a struct? Because classes are passed on as references, and that seems much more efficient than copying the whole struct. We need all data specific for the portal's rotation, that means we also need to include top and bottom vertices besides a, b, c factors.
public class Factors
{
public Vector3 top = new Vector3();
public Vector3 bottom = new Vector3();
public float a = 0.0f;
public float b = 0.0f;
public float c = 0.0f;
}
It will also be useful to have the rotation in a separate variable so we won't have to calculate it every time.
public class Factors
{
public Vector3 top = new Vector3();
public Vector3 bottom = new Vector3();
public float a = 0.0f;
public float b = 0.0f;
public float c = 0.0f;
public float rot = 0.0f;
}
Now let's substitute all of our loose factors in the class with one Factors instance.
public class Factors
{
public Vector3 top = new Vector3();
public Vector3 bottom = new Vector3();
public float a = 0.0f;
public float b = 0.0f;
public float c = 0.0f;
public float rot = 0.0f;
}
private float height = 0.0f;
private PackedSprite sprite;
public Factors facs = new Factors();
void Start ()
{
Now we need to edit our Start() function, so it calculates the variables inside facs, instead of old, deleted variables.
void Start ()
{
sprite = GetComponent<PackedSprite>();
height = ((BoxCollider)collider).size.y*transform.localScale.y;
facs.top = new Vector3(transform.position.x, transform.position.y + height/2.0f, transform.position.z);
facs.top = RotatePoint(transform.position, facs.top, transform.eulerAngles.z);
facs.bottom = transform.position - (facs.top - transform.position);
facs.b = facs.top.x - facs.bottom.x;
facs.a = facs.bottom.y - facs.top.y;
float tmpA = facs.a;
facs.a = facs.a / (Mathf.Abs(facs.a) + Mathf.Abs(facs.b));
facs.b = facs.b / (Mathf.Abs(tmpA) + Mathf.Abs(facs.b));
facs.c = facs.a/facs.b*transform.position.x + transform.position.y;
}
Step 16: Return New Factors
Now let's create another function. This one will calculate the factors for rotated sprite. It will return an instance of Factors, containing all the factors that sprite may need. Also, we need to know what is the sprite's rotation, so the function must have one argument. Let's call the function CalcFacs().
public Factors CalcFacs(float angle)
{
}
The first thing we want to do is to create our return Factors.
public Factors CalcFacs(float angle)
{
Factors f = new Factors();
}
Now let's calculate the rotation according to the formula we thought up earlier.
public Factors CalcFacs(float angle)
{
Factors f = new Factors();
f.rot = transform.eulerAngles.z - angle;
}
It's portal rotation minus sprite rotation. Now let's calculate the rest of the factors, we can simply copy-paste it from the Start() function and then substitute transform.eulerAngles.z with f.rot. We would also need to substitute facs with the Factors we're going to return, that is f.
public Factors CalcFacs(float angle)
{
Factors f = new Factors();
f.rot = transform.eulerAngles.z - angle;
f.top = new Vector3(transform.position.x, transform.position.y + height/2.0f, transform.position.z);
f.top = RotatePoint(transform.position, f.top, f.rot);
f.bottom = transform.position - (f.top - transform.position);
f.b = f.top.x - f.bottom.x;
f.a = f.bottom.y - f.top.y;
float tmpA = f.a;
f.a = f.a / (Mathf.Abs(f.a) + Mathf.Abs(f.b));
f.b = f.b / (Mathf.Abs(tmpA) + Mathf.Abs(f.b));
f.c = f.a/f.b*transform.position.x + transform.position.y;
}
And now we can return our f, so the Portalable object will be able to use it.
public Factors CalcFacs(float angle)
{
Factors f = new Factors();
f.rot = transform.eulerAngles.z - angle;
f.top = new Vector3(transform.position.x, transform.position.y + height/2.0f, transform.position.z);
f.top = RotatePoint(transform.position, f.top, f.rot);
f.bottom = transform.position - (f.top - transform.position);
f.b = f.top.x - f.bottom.x;
f.a = f.bottom.y - f.top.y;
float tmpA = f.a;
f.a = f.a / (Mathf.Abs(f.a) + Mathf.Abs(f.b));
f.b = f.b / (Mathf.Abs(tmpA) + Mathf.Abs(f.b));
f.c = f.b/f.a*transform.position.x + transform.position.y;
return f;
}
Don't forget to adjust our Dist() function, so it uses our facs class.
public float Dist(Vector3 p)
{
if (facs.b == 0.0f)
return (p.x - transform.position.x);
else
return (facs.a/facs.b*p.x + p.y - facs.c)/Mathf.Sqrt((facs.a/facs.b)*(facs.a/facs.b) + 1.0f);
}
That's it, now we need to edit appropiately our Portalable script.
Step 17: Edit Portalable Script
First thing we should do is to define a Factors instance in our class.
public class Portalable : MonoBehaviour
{
private float gCutoff = 0.0f;
private PackedSprite sprite;
public Portal portal;
private Portal.Factors facs;
Now let's calculate appropiate factors. Good place to do that is our SetCutoff() function.
void SetCutoff()
{
if (portal == null)
return;
facs = portal.CalcFacs(transform.eulerAngles.z);
We also need to change our dir. It needs to use our own factors now.
void SetCutoff()
{
if (portal == null)
return;
facs = portal.CalcFacs(transform.eulerAngles.z);
float dist = portal.Dist(transform.position);
Vector2 dir = new Vector2(Mathf.Abs(facs.a), Mathf.Abs(facs.b));
dir.Normalize();
Vector2 size = new Vector2(sprite.width, sprite.height);
Vector2 proj = new Vector2();
proj.x = Vector2.Dot(size, dir)*dir.x;
proj.y = Vector2.Dot(size, dir)*dir.y;
gCutoff = dist/proj.magnitude + 0.5f;
UpdateCutoff();
}
Finally, let's update our UpdateCutoff() function. Again, instead of portal's factors, we need to use the ones calculated for our angle. We also need to use our calculated rotation instead of portal.transform.eulerAngles.z.
void UpdateCutoff()
{
Rect uvs = new Rect();
if (sprite.GetCurAnim() != null)
uvs = sprite.GetCurAnim().GetCurrentFrame().uvs;
else
uvs = sprite.DefaultFrame.uvs;
float cutoffMin, cutoffMax;
if (Mathf.Tan(Mathf.Deg2Rad*facs.rot) < 0.0f)
{
cutoffMin = uvs.xMin*facs.a + (uvs.yMin + uvs.height)*facs.b;
cutoffMax = (uvs.xMin + uvs.width)*facs.a + (uvs.yMin)*facs.b;
}
else
{
cutoffMin = (uvs.xMin + uvs.width)*facs.a + (uvs.yMin + uvs.height)*facs.b;
cutoffMax = uvs.xMin*facs.a + uvs.yMin*facs.b;
}
renderer.material.SetFloat("_A", facs.a);
renderer.material.SetFloat("_B", facs.b);
renderer.material.SetFloat("_Cutoff", Mathf.Lerp(cutoffMin, cutoffMax, gCutoff));
}
Now we should test our rotations. You can play with the angle of the sprite at the runtime, because it's updating the factors all the time, but the portal does intitialization only at the beginning, so rotating it won't change much, you'll have to restart the game.
As you can see, the rotations doesn't work too good, but the issue doesn't seem too complicated to fix. As you can notice when you move the rotated sprite, the cutoff simply progresses in the wrong direction. To change the direction our cutoff progresses, we need to swap cutoffMin and cutoffMax variables. So why does it happen, why our extremes are calculated incorrectly? Remember when I said that we will need to be careful so the sign of our distance isn't exactly the opposite of what it neds to be? This is the time we needed to be careful of it. So instead of swapping the cutoffMin with cutoffMax we could multiply our calculated dist by -1, and the result would be the same.

We cutoff the texture from the different side we should. So when do we do so wrongly? When our facs.rot is in 3rd or 4th quarters and if the portal is in its 1st or 2nd quarter or when our facs.rot is in 1st or 2nd and our portal is in 3rd or 4th quarter.
if ((Mathf.Sin(Mathf.Deg2Rad*facs.rot) < 0.0f != Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
&& (Mathf.Sin(Mathf.Deg2Rad*facs.rot) < 0.0f || Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f))
{
}
renderer.material.SetFloat("_A", facs.a);
renderer.material.SetFloat("_B", facs.b);
As you can see, we used hand-mande exclusive or statement here. The idea is, we need to swap the extremes either when Mathf.Sin(Mathf.Deg2Rad*facs.rot) < 0.0f or Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f, but if both of them are true, they would need to be swapped twice and that we result with nothing changed so why should we do it in the first place? We shouldn't. When both are false, we don't want to swap anything. For swapping we'll use a temoporary variable.
if ((Mathf.Sin(Mathf.Deg2Rad*facs.rot) < 0.0f != Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f)
&& (Mathf.Sin(Mathf.Deg2Rad*facs.rot) < 0.0f || Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f))
{
float tmp = cutoffMin;
cutoffMin = cutoffMax;
cutoffMax = tmp;
}
And that's it. We can now test whether the cutoff is calculated properly even for rotated sprites.
Step 18: Adjust Visibility
For now our sprite is visible on the green side of the portal and it's invisible on the red one. Let's get a bit of control over where the sprite is visible and where it's not. Let's say we want the sprite to be always visible as it enters the portal, and then if it crosses the portal axis, it disappears. So no matter if we start from the red side or the green side, the sprite should be visible as it enters it. The first question that need to be asked is, how do we reverse the current order? That's pretty simple, if all we want is to be visible on the red side instead of the green one, we need to make the portal's factors into their opposites. Of course we need to change only a and b factors, because c is a kind of an offset and have no share in shaping the axis.
void SetCutoff()
{
if (portal == null)
return;
facs = portal.CalcFacs(transform.eulerAngles.z);
facs.a *= -1.0f;
facs.b *= -1.0f;
Let's check if it really works.

As you can see, now the sprite is visible on the red side of the portal, proves that making the factors into their opposites works. Now we need to think how should we manage those sides. One variable should be enough, it could indicate whether we are on one side of the portal, or another. How do we differentiate between the sides? Remember that our Dist() function can return the distance with a sign, depending on which side of an axis is our sprite on. That's all we need. Delete the test lines in which we multiply the factors and let's get to work.
Step 19: Handle Stepping Out of Portal
Before we do anything else, let's create a OnTriggerExit() callback. This function is called when we finished intersecting with a trigger. When our sprite finishes intersecting with our current portal, we need it no more so we can delete the reference to it.
void OnTriggerExit(Collider other)
{
}
If we finished intersecting with a trigger and yet our portal reference is equal to null that means we couldn't have stepped out of the portal, but from some other trigger that we don't care in this script. If that's the case, let's simply return.
void OnTriggerExit(Collider other)
{
if (portal == null)
return;
}
If we have our portal reference set, but it is not equal to the Portal component attached to the trigger we Step NaNout of, that means that either the trigger doesn't have a Portal script component attached and it is equal to null, or it has a Portal component, but it's not the portal our sprite is currently using, though that's very unlikely. If any of those happens, we also need not to handle the case and therefore may safely return.
void OnTriggerExit(Collider other)
{
if (portal == null)
return;
if (other.GetComponent<Portal>() != portal)
return;
}
Finally, if we exit our portal, we simply set the portal reference to null, because we don't need it anymore.
void OnTriggerExit(Collider other)
{
if (portal == null)
return;
if (other.GetComponent<Portal>() != portal)
return;
portal = null;
}
Step 20: Handle Visible Sides
Let's create our variable which will hold the info on which side are we. Let's call it side.
public class Portalable : MonoBehaviour
{
private float gCutoff = 0.0f;
private PackedSprite sprite;
public Portal portal;
private Portal.Factors facs;
public float side = 1.0f;
Now let's set our side variable. The place to do that is in OnTriggerEnter() function, because we want to set the side from which sprite enters the portal. Now how do we know which side is which? Well, we've got a useful info from our distance from portal. We know that if we are above the portal axis, our distance is positive, and if we are below it, it's negative. Of course we can't speak about above and below when our axis is completely vertical, but this applies to all other cases. If you play with the portal a bit, rotating it from 0 to 180 degrees, you'll notice that the red side is always on the top. When the rotation ranges from 180 to 360 degrees, the green side is always on top. That's the info we will use to differentiate the green side and the red side. So, if our distance is positive and the portal's angle ranges from 0 to 180, we know that we are on the red side. The same applied if the situation is reversed, our distance is negative and the portal's angle ranges from 180 to 360. In other cases, we enter from the green side so we don't have to multiply our factors by -1. First thing we need to do is to calculate the distance.
void OnTriggerEnter(Collider other)
{
if (other.GetComponent<Portal>() != null)
portal = other.GetComponent<Portal>();
else
return;
float dist = portal.Dist(transform.position);
}
Now we need to create a condition and then multiply our side by -1.
void OnTriggerEnter(Collider other)
{
if (other.GetComponent<Portal>() != null)
portal = other.GetComponent<Portal>();
else
return;
float dist = portal.Dist(transform.position);
if ((Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) >= 0.0f && portal.Dist(transform.position) >= 0.0f)
|| (Mathf.Sin(Mathf.Deg2Rad*portal.transform.eulerAngles.z) < 0.0f && portal.Dist(transform.position) < 0.0f))
side *= -1.0f;
}
Note that we multiply instead of assigning -1. That's for the case when we would like to start invisible or other special cases. If we wanted to start invisible now, would simply need to set the initial side to -1, and then the multiplying would do the rest of the work for us. Finally, we need to multiply our factors with the side, so the changes may take place.
void SetCutoff()
{
if (portal == null)
return;
facs = portal.CalcFacs(transform.eulerAngles.z);
facs.a *= side;
facs.b *= side;
We also should reset the side to 1 when we finish intersecting with the portal. That's because we're only multiplying it at the beginning, if we wanted the results to be reapeated each time we enter a portal, we must do so. We can reset the side in OnTriggerExit() funtion.
void OnTriggerExit(Collider other)
{
if (portal == null)
return;
if (other.GetComponent<Portal>() != portal)
return;
portal = null;
side = 1.0f;
}
Let's test if this works. The best way to show that would be duplicating the ball and then placing each one on each side of the portal. Again, you can duplicate any object by going to Edit->Duplicate or pressing Ctrl+D while having it selected. Remember that our sprites must intersect with the portal if we want to see whether everything is working well, if they don't then of course they will be visible by default because there will be no cutoff taking place.
As you can see, both are visible from the beginning. If you move them so they cross the portal, they'll disappear, but if you again move the ball while it's invisible it will appear when it touches the portal. That's because we reset the side when our ball exits the portal trigger. It's nothing to worry about, it's just as we made it to be. Now let's change initial side to -1.0. Both sprites should be invisble at the beginning in this case.

We need to change our side in inspector. That's because the default value in script is assigned to the public variable only when the script is attached to the object. That means even if we changed the default value of side in the script to -1, then it would be overridden by the value of side in inspector, which is 1, because it was set this way when we attached the script to the ball for the first time. Note that with private variables there is no way to change them in inspector so unity doesn't override the default value assigned script. Remember to change the side in both sprites, and then hit play.
Both sprites are invisible upon touch with the portal. We don't really want it to stay that way so let's change our side in inspector back to 1.
Step 21: Create the Ball Class
To test what we have done up until now properly, we should let the rolling ball go through portal without us meddling with it in the editor window. For that we need to set the velocity of the ball, so it can move in a direction we want it to. If we want to do that, we need to create a script for our ball. Let's name it Ball. As usual, clean up the contents so we can start work on the script right away.
using UnityEngine;
using System.Collections;
public class Ball : MonoBehaviour
{
void Start ()
{
}
void Update ()
{
}
}
We don't need any precise controls over the ball. It would be OK if we simply could make it go right or left with the speed we want it to do so. The easy way to do that would be to create a Vector3 which would set the ball's initial velocity. We'll do just that.
public class Ball : MonoBehaviour
{
public Vector3 initVel;
Now, in our Start() function let's change our ball's velocity to the initVel.
public class Ball : MonoBehaviour
{
public Vector3 initVel;
void Start ()
{
rigidbody.velocity = initVel;
}
Step 22: Setup the Scene
Before we set our velocities, let's duplicate our portal. Let's place one on the left side of the level, and another on the right side.

Now let's attach the Ball script and set the initVel for each of the ball. Let's make the ball on the left go to the left, and the ball on the right go to the right.

The last thing we need to do is to uncheck the IsKinematic checkbox for each of the ball in their Rigidbody component. After that, let's hit play and see how the balls roll to their portals.
Conclusion
That's the end of the second part of the tutorial. So far, we've added sprites and made them useful by attaching components and custom scripts, but the most exhausting part was creating a portal that would hide part of the sprite crossing it. The game is far from done, but I hope you learned something new by following this tutorial. Feel free to continue the project, either by yourself or following the next parts of this tutorial. Thanks for your time.
