Advertisement

Build a 2D Portal Puzzle Game With Unity: More Mechanics and New Levels

by

This is the fourth and final part of the Build a 2D Portal Puzzle Game With Unity tutorial series. In this part we'll finish up the game by adding new mechanics and other miscellaneous stuff. We'll also create a few example levels that make use of our mechanics.


Also available in this series:

  1. Build a 2D Portal Puzzle Game With Unity: Getting Started
  2. Build a 2D Portal Puzzle Game With Unity: Adding the Portals
  3. Build a 2D Portal Puzzle Game With Unity: More Mechanics and New Levels
  4. Build a 2D Portal Puzzle Game With Unity: Portals and Game Mechanics

Step 1: Add Buttons

For now we can only assign a portal's destinations in the inspector, it's time to make that feature available to the player. In the previous part it was explained that we're going to use drag and drop action to connect portals, we need to show visually that two portals are connected and we're going to use lines to do that. Of course it wouldn't look very well if we used plain straight lines and that's why we'll use bezier curves. Our Vectrosity plugin will aid us in doing that.

We already decided that we're going to use portal's vertices as a starting and ending point of the lines. Let's create empty objects and attach a sphere collider for each of them. The colliders will help us handle the input correctly, so we know whether the player clicked one button or another, and whether he connected the portals or not and so on.


Note that we won't use our blocks' colliders for that because they are too small and their size must be corresponding to the block's sprite. Don't forget to check Is Trigger checkbox for each collider. I set their position to be (0.0, 65.0, -5.0) for the top object and (0.0, -65.0, -5.0) for the bottom one, also their radiuses should be quite big, 20 units will do. Name the one at the top Top and the one at the bottom Bottom, we need to differentiate between them.

Now let's create another empty game object and set it as a parent of those two with Collider component attached to them. This object will be managing the lines of this portal so let's call it Line Mgr. Note that you should first set the Line Mgr's position to (0.0, 0.0, 0.0) before you make it into a parent object, because if you won't then you'll have to set the children to their appropiate positions again. Once you finish Apply the changes to the prefab, and let's move on.



Step 2: Create a LineMgr Script

Create a new script and call it LineMgr. In this script we will be managing all the lines for a single portal which the script will be attached to.

 
using UnityEngine; 
using System.Collections; 
 
public class LineMgr : MonoBehaviour 
{ 
 
	void Start () 
	{ 
		 
	} 
 
	void Update () 
	{ 
	 
	} 
}

We need references to our invisible buttons, so let's create them. One for each button plus one additional that will keep a reference of a button that is currently being used, it'll save us copying the code.

 
public class LineMgr : MonoBehaviour 
{ 
	public Collider topBtn; 
	public Collider bottomBtn; 
	Collider curBtn;

Let's assign our references in the Start() function. This time we're going to use FindChild(), which takes a name of a child as an argument and returns its transform.

 
void Start () 
{ 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
}


Step 3: Check for Input

We need to know if the mouse button was pressed on one of our buttons, to get an input we have to call Input.GetMouseButtonDown(), which is true if the mouse button was pressed in the current frame.

 
void Update () 
{ 
	if (Input.GetMouseButtonDown(0)) 
	{ 
	} 
}

To check whether our mouse is above one of our buttons we'll cast a ray from the the pointer's position, and if ray happens to penetrate one of our buttons then we can be sure that our button was pressed. Let's get our pointer's position in world space first, here's how we do it.

 
void Update () 
{ 
	Vector3 curMousePos = Camera.main.ScreenPointToRay(Input.mousePosition).origin; 
	 
	if (Input.GetMouseButtonDown(0)) 
	{ 
	} 
}

As you can see, we needed to use Camera.main.ScreenPointToRay(). Camera.main is a reference to our Main Camera and ScreenPointToRay() takes any position in the screen space and returns a ray placed in the world space. We assign the ray's origin as our curMousePos. The position is accurate on x and y axes, but we'll set the z to suit our needs, because it's dependant on the camera's z position now.


Step 4: Raycast

To use Physics.Raycast() and get a collider that was hit by the ray we need to create an instance of RaycastHit, which basically will hold the data such as where it hit a collider and what collider was it.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
}

Now we can cast a ray and check if it hits something.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
	 
	if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
	{ 
	} 
}

If the ray hits something then Physics.Raycast returns true. In the function, the first argument is the point from which we want to cast the ray, as you can see we want to cast it from the position in front of the scene layer. The second argument is the direction in which the ray will be cast, we want it to move in the direction of the scene layer. The third one is the structure that will hold the info on the ray's hit, and the last one is the length of the ray. Now, if ray hits something, we need to check whether it's one of our buttons, and if it indeed is, then we should set our curBtn.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
{ 
	if (rayInfo.collider == topBtn) 
		curBtn = topBtn; 
	else if (rayInfo.collider == bottomBtn) 
		curBtn = bottomBtn; 
}

Step 5: Check For Other Events

Since we're going to use drag and drop action to connect the portals, we need to know when our mouse is dragged. We know that when our the button is pressed and it hit one of our buttons then we've got curBtn reference set to that button instead of null, we'll use that. We should also distinguish dragging from a simple button press. We'll do that by using the distance between the position when the mouse was pressed and the current position, if that distance is greater than noise then we can say that the mouse is being dragged.

Let's first create a vector that will save the position where the mouse button was first pressed.

 
public Collider topBtn; 
public Collider bottomBtn; 
Collider curBtn; 
 
Vector3 pressPos;

And now let's assign the position to it. Of course we want to do so only if our curBtn is set.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
{ 
	if (rayInfo.collider == topBtn) 
		curBtn = topBtn; 
	else if (rayInfo.collider == bottomBtn) 
		curBtn = bottomBtn; 
	 
	if (curBtn != null) 
	{ 
		pressPos = curMousePos; 
	} 
}

Now let's create our condition to see whether the mouse is being dragged or not.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
 
	if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
	{ 
		if (rayInfo.collider == topBtn) 
			curBtn = topBtn; 
		else if (rayInfo.collider == bottomBtn) 
			curBtn = bottomBtn; 
			 
		if (curBtn != null) 
		{ 
			pressPos = curMousePos; 
		} 
	} 
} 
 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
}

So if our curBtn is set and the mouse was moved further than 5 units away then we assume that the mouse is being dragged.

Now, if the button is up, we need to reset our curBtn. To check that we can use Input.GetMouseButtonUp(0).

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
 
	if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
	{ 
		if (rayInfo.collider == topBtn) 
			curBtn = topBtn; 
		else if (rayInfo.collider == bottomBtn) 
			curBtn = bottomBtn; 
			 
		if (curBtn != null) 
		{ 
			pressPos = curMousePos; 
		} 
	} 
} 
else if (Input.GetMouseButtonUp(0)) 
{ 
	curBtn = null; 
} 
 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
}

And that's it, we have all our events set up. Now we need to start creating lines, connecting them and so on.


Step 6: Create a Wire Class

Now let's create our Wire class. As explained earlier, these won't be just straight lines but curves and will need to hold quite a bit of additional data, since their task is to connect portals, we can call them wires.

 
public class LineMgr : MonoBehaviour 
{ 
	public class Wire 
	{ 
	}

We need two arrays of vectors for each wire, first will hold all the points the line goes through and the second will hold the bezier curve points and handles.

 
public class LineMgr : MonoBehaviour 
{ 
	public class Wire 
	{ 
		public Vector3[] points; 
		public Vector3[] curvePoints; 
	}

Another thing we need to add is a VectorLine which is needed to render the curve with Vectrosity.

 
public class LineMgr : MonoBehaviour 
{ 
	public class Wire 
	{ 
		public Vector3[] points; 
		public Vector3[] curvePoints; 
		 
		public VectorLine line; 
	}

We should also keep a transform of a button that this wire comes from, so we know where the wire has its beginning.

 
public class LineMgr : MonoBehaviour 
{ 
	public class Wire 
	{ 
		public Vector3[] points; 
		public Vector3[] curvePoints; 
		 
		public VectorLine line; 
		 
		public Transform btn; 
	}

To create a wire we also need to decide how many segments should each wire have, 20 should be enough to build a nice curve. We'll also need a material that the line will be rendered with, we'll assign it later from the inspector so for now let's just create a reference for it.

 
Vector3 pressPos; 
public int segments = 20; 
public Material material;

Those are the basics, keep in mind that we didn't add a variable that would point to which portal does a wire lead, that's because we're going to keep our list of wires synchronized with the list of portals, so the first wire in the list of wires will lead to the first portal in the destPortals list. If we'll need anything more for each wire then we can always edit our class later on.


Step 7: Create a Wire

Let's start creating wires. The first thing we need to do is to create a List of Wires for our manager to hold.

 
using UnityEngine; 
using System.Collections; 
using System.Collections.Generic;
 
public Material material; 
public List<Wire> wires = new List<Wire>();

Now let's go ahead and create a wire when a mouse button is pressed.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
{ 
	if (rayInfo.collider == topBtn) 
		curBtn = topBtn; 
	else if (rayInfo.collider == bottomBtn) 
		curBtn = bottomBtn; 
	 
	if (curBtn != null) 
	{ 
		pressPos = curMousePos; 
		 
		wires.Add(new Wire()); 
	}

Now we need to set it appropiately. First, let's create an array for the wire's points.

 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1];

We also need to create an array for curve points.

 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1]; 
wires[wires.Count - 1].curvePoints = new Vector3[4];

Now let's create the VectorLine.

 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1]; 
wires[wires.Count - 1].curvePoints = new Vector3[4]; 
wires[wires.Count - 1].line = new VectorLine("Line", wires[wires.Count - 1].points,  
											material, 8.0f, LineType.Continuous, Joins.None);

The first argument in VectorLine constructor is the line's name, the second is an array with points, the third is the material, the fourth is line's width, the fifth is the line's type and the last one is the method by which the segments are joined. We set the type to Continuous, that means that the line will go through all the points in our points vector. If we chose Discrete instead, then each segment of the line could be separated from the rest and each segment would require two vertices instead of one.

Let's set the wire's button.

 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1]; 
wires[wires.Count - 1].curvePoints = new Vector3[4]; 
wires[wires.Count - 1].line = new VectorLine("Line", wires[wires.Count - 1].points,  
											material, 8.0f, LineType.Continuous, Joins.None); 
wires[wires.Count - 1].btn = curBtn.transform;

Step 8: Render the Wire

Now let's go to our dragging event. We need to set our curvePoints there.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
}

We know that curve starts at the button's position and that it ends on the current mouse position so let's set those vertices first. The format is that first point in the array is the beginning, second point in the array is a handle that shapes the curve coming out of first line, the third point is the end of the curve and the fourth is a handle that shapes the curve as it comes in to the end point.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
	wires[wires.Count - 1].curvePoints[0] = wires[wires.Count - 1].btn.position; 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(curMousePos.x, curMousePos.y, 0.0f); 
}

Now we need to decide where to set the points that shape the curve so it's not too boring.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
	wires[wires.Count - 1].curvePoints[0] = wires[wires.Count - 1].btn.position; 
	wires[wires.Count - 1].curvePoints[1] = new Vector3(wires[wires.Count - 1].btn.position.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(curMousePos.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[3] = new Vector3(curMousePos.x, wires[wires.Count - 1].btn.position.y, 0.0f); 
}

Finally we need to make vectoristy set the points using our curvePoints.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
	wires[wires.Count - 1].curvePoints[0] = wires[wires.Count - 1].btn.position; 
	wires[wires.Count - 1].curvePoints[1] = new Vector3(wires[wires.Count - 1].btn.position.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(curMousePos.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[3] = new Vector3(curMousePos.x, wires[wires.Count - 1].btn.position.y, 0.0f); 
	 
	Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments); 
}

Vector.MakeCurveInLine() takes a VectorLine that needs to have its points set as a first argument, the second argument are the bezier points and handles and the last one is the number of segments of our line.

If we have any lines created then we want to render them right away.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
	wires[wires.Count - 1].curvePoints[0] = wires[wires.Count - 1].btn.position; 
	wires[wires.Count - 1].curvePoints[1] = new Vector3(wires[wires.Count - 1].btn.position.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(curMousePos.x, curMousePos.y, 0.0f); 
	wires[wires.Count - 1].curvePoints[3] = new Vector3(curMousePos.x, wires[wires.Count - 1].btn.position.y, 0.0f); 
	 
	Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments); 
} 
 
if (wires.Count > 0) 
{ 
}

To render a line we need to call Vector.DrawLine3D().

 
if (wires.Count > 0) 
{ 
	Vector.DrawLine3D(wires[wires.Count - 1].line);	 
}

Step 9: Create a Material for Wires

Let's import textures that come with Vectrosity so we can use them with our material. To do that find VectrosityDemos_Unity3.unitpackage in your Vectorisity folder and double-click it to import the package. Once a window in unity pop ups, select only the textures in Vectrosity/Textures/VectorTextures to import.


After you import the textures create a new material in the Materials folder and rename it to Line Material. You can use any shader that supports alpha channel and lets us change the color of the material, I selected Vertex Colored that comes with Sprite Manager 2. Change the material's color to whatever you like, I set it to (0, 0, 255, 128). The texture I chose is called Dot, it's in the VectorTextures folder we imported earlier.


Now assign our LineMgr script to the Line Mgr object and assign a material reference, the buttons are assigned pragmatically in the code. Apply the changes to the prefab.


Let's see if we can drag and drop the lines from the portal's buttons.

Click here to try it out.

As you can see, everything works well. The lines seem a bit too opaque, let's change the material's alpha color to something around 60.


Step 10: Connect the Portals

For now our wires can be placed just anywhere, we need to make them attachable only to another portal's buttons. Let's check whether there is anything under the mouse pointer when the button is released, and if there is, let's check if it's another portal's button. We'll do that by looking up the names of the objects that the ray hit.

 
else if (Input.GetMouseButtonUp(0)) 
{ 
	if (curBtn != null) 
	{ 
		RaycastHit rayInfo; 
		 
		if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)) 
		{ 
		} 
	 
		curBtn = null; 
	} 
}

If our ray hit something, we need to check whether it's another portal's Top or Bottom.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
}

If that's the case we want to align the end of the wire to the center of the button.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
}

In other cases we want to get rid of the wire and remove it from our list.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
} 
else 
{ 
	Vector.DestroyLine(ref wires[wires.Count - 1].line); 
	wires.RemoveAt(wires.Count - 1); 
}

To see the results we have to have at least two portals on the screen, if you've got only one then you can simply duplicate it and then move it to a appropiate position.

Click here to try it out.

Just making lines vanish looks a bit clunky, we should take care of that.


Step 11: Import the iTween

We could simply smooth the erasing of the wire by gradual decrease of its alpha, but we'll go for something a bit fencier, an animation. Since we're dealing with procedurally created lines the animation itself will have to be procedural too, and that's why we'll need some tweening functionality for it. There's a nice free tweening tool named iTween created by Bob Berkebile, we can use to suit our needs. You can download the iTween from its Google Code page. Once you unpack it you'll notice that it's a single C# script. Drag and drop it into our Plugins folder to import it.



Step 12: Create an Animation

To get an idea of what we are trying to do now, try to destroy a wire in the game's demo. The wire deattaches itself from the destination portal it was connected to and goes back to the point where it was first created, then it disappears. To achieve that effect, we simply need to scale the line down so it becomes invisible. We'll use iTween to get the scaling progress the way it looks the best. First, let's create our EraseWire() function.

 
void Start () 
{ 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
} 
	 
public void EraseWire(int index) 
{ 
}

Now we need use the iTween to calculate an inbetweened vector using our line's starting and ending points.

 
public void EraseWire(int index) 
{ 
	iTween.ValueTo(gameObject, iTween.Hash("from", new Vector3(wires[index].curvePoints[2].x, wires[index].curvePoints[2].y, (float) index),  
		"to", new Vector3(wires[index].curvePoints[0].x, wires[index].curvePoints[0].y, (float) index),  
		"time", 0.25f, "onupdate", "EraseUpdate")); 
}

We need to use iTween.ValueTo() to tween between two vectors. The first argument of the function is the object that will have its variables tweened. The second argument contains all of the tween's properties, we use iTween.Hash() to pack them all in a single table all at once. The first property we set is "from", which is basically a vector from which the tweening will start. The next one is "to", which is a vector on which the tweening will end. Another one is "time", which sets how long will the tweening take place and the final one is "onupdate" which is a function that the iTween will call while the tweening takes place. I explained already that we'll be tweening between two end points of the wire, but notice that instead of z value of position there is an index of the wire that gets erased. I'll clear this up when we'll start creating the update function for our inbetween.

 
public void EraseWire(int index) 
{ 
	iTween.ValueTo(gameObject, iTween.Hash("from", new Vector3(wires[index].curvePoints[2].x, wires[index].curvePoints[2].y, (float) index),  
		"to", new Vector3(wires[index].curvePoints[0].x, wires[index].curvePoints[0].y, (float) index),  
		"time", 0.25f, "onupdate", "EraseUpdate")); 
} 
 
public void EraseUpdate(Vector3 v) 
{ 
}

As you can see, our EraseUpdate() has only one argument and that's the inbetweened vector. Since we've got only that information in this function, we don't really know which wire are we deleting and to which line should we apply the changes. At the time of writing this tut there is no way to pass additional arguments with the ValueTo() function, and that's why we needed to use a somewhat hacky solution, to pass the index of the wire that is deleted in the vector's z position. Because we set it to the same value in both, the vector that we're tweening from and the vector we are tweening to, the value will remain unchanged throughout the inbetweening.

The first thing we need to do is to get back our index from the vector so it's named appropiately.

 
public void EraseUpdate(Vector3 v) 
{ 
	int index = Mathf.RoundToInt(v.z); 
}

And now let's change our end vertex of the line so it is equal to the tweened one.

 
public void EraseUpdate(Vector3 v) 
{ 
	int index = Mathf.RoundToInt(v.z); 
	wires[index].curvePoints[2] = new Vector3(v.x, v.y, 0.0f); 
}

Since we want the line to straighten while it's going back, we need to set the anchor points of the curve appropiately.

 
public void EraseUpdate(Vector3 v) 
{ 
	int index = Mathf.RoundToInt(v.z); 
	wires[index].curvePoints[1] = wires[index].btn.position; 
	wires[index].curvePoints[2] = new Vector3(v.x, v.y, 0.0f); 
	wires[index].curvePoints[3] = new Vector3(v.x, v.y, 0.0f); 
}

And let's apply the changes to the line.

 
public void EraseUpdate(Vector3 v) 
{ 
	int index = Mathf.RoundToInt(v.z); 
	wires[index].curvePoints[1] = wires[index].btn.position; 
	wires[index].curvePoints[2] = new Vector3(v.x, v.y, 0.0f); 
	wires[index].curvePoints[3] = new Vector3(v.x, v.y, 0.0f); 
	 
	Vector.MakeCurveInLine(wires[index].line, wires[index].curvePoints, segments); 
	Vector.DrawLine3D(wires[index].line); 
}

Step 13: Erase Wire

Now what's left for us to do is to actually delete the wire when it finishes animating. Let's check in our EraseUpdate() function whether our ending point isn't equal to the starting point of the wire. If it is, then we know that we can safely delete the wire because it's no longer visible.

 
public void EraseUpdate(Vector3 v) 
{ 
	int index = Mathf.RoundToInt(v.z); 
	wires[index].curvePoints[1] = wires[index].btn.position; 
	wires[index].curvePoints[2] = new Vector3(v.x, v.y, 0.0f); 
	wires[index].curvePoints[3] = new Vector3(v.x, v.y, 0.0f); 
	 
	Vector.MakeCurveInLine(wires[index].line, wires[index].curvePoints, segments); 
	Vector.DrawLine3D(wires[index].line); 
	 
	if (v.x == wires[index].curvePoints[0].x && v.y == wires[index].curvePoints[0].y) 
	{ 
	} 
}

We can copy our deletion code from the Update() function and change the last wire to the one we want to delete.

 
if (v.x == wires[index].curvePoints[0].x && v.y == wires[index].curvePoints[0].y) 
{ 
	Vector.DestroyLine(ref wires[index].line); 
	wires.RemoveAt(index); 
}

Now let's EraseWire() in the Update() function.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
} 
else 
	EraseWire(wires.Count - 1);

That's not all, if we simply tap on the button the wire is still created, altough is not really visible. To get rid of it we don't really need to animate it, we just want to destroy it. First, let's EraseWire() only if the wire was actually dragged around. We know when that happens because the start and end point of the wire won't be equal in that case.

 
else if (wires[wires.Count - 1].curvePoints[0] != wires[wires.Count - 1].curvePoints[2]) 
	EraseWire(wires.Count - 1);

Now when the only option left is when the player simply tapped the portal button. Let's delete the wire if that's the case.

 
else if (wires[wires.Count - 1].curvePoints[0] != wires[wires.Count - 1].curvePoints[2]) 
	EraseWire(wires.Count - 1); 
else 
{ 
	Vector.DestroyLine(ref wires[wires.Count - 1].line); 
	wires.RemoveAt(wires.Count - 1); 
}

And that's it, let's see how does it look now when we discard a wire because it wasn't connected to any destination portal.

Click here to try it out.


Step 14: Add a Destination

It's time to add a destination whenever we connect a wire to a portal. For that we need a reference to the portal our wire comes from, so let's create and set it now.

 
Portal portal; 
Vector3 pressPos; 
public int segments = 20; 
public Material material; 
public List<Wire> wires = new List<Wire>();

Since our LineMgr object is a parent of the portal, we can get its Portal component by using transform.parent reference.

 
void Start () 
{ 
	portal = transform.parent.GetComponent<Portal>(); 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
}

And the final step would be to call the AddDest() function when we attach a wire to another portal.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
	portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
}

You'll notice that if you hit play and connect one portal to another, there is a destination added to destPortals list.



Step 15: Set a Destination

Even though we added a new destination, the destPortal wasn't changed from null to the one we just added. We need to edit our AddDest() function so it sets destPortal if it's equal to null. Open our Portal script and let's get to it.

 
public int AddDest(Portal portal) 
{ 
	destPortals.Add(portal); 
	 
	if (destPortal == null) 
	{ 
		destPortal = destPortals[0]; 
		destID = 0; 
	} 
	 
	return destPortals.Count - 1; 
}

Let's check whether the destPortal is set once we connect the portals now.


The inspector shows that everything's fine now.


Step 16: Distinguish the Active Destination

For now all of our lines look the same so the player can't tell to which destination will the ball go if it enters a certain portal and it has more than one connection. We need to create an additional line with a different material so it will be easly distinguishable as the active wire. In our editor duplicate the Line Material and change the duplicate's name to Active Line Material, then change it's texture to dash (it also comes with Vectrosity) and finally change its color to something a bit more opaque and light than the Line Material.


After doing so, let's create a new material reference in our LineMgr script, we'll call it activeMaterial.

 
public Material material; 
public Material activeMaterial;

Now create our activeWire.

 
public List<Wire> wires = new List<Wire>(); 
public Wire activeWire = new Wire();

We'll be using only the wire's points and line so let's initialize them in the Start() function.

 
void Start () 
{	 
	portal = transform.parent.GetComponent<Portal>(); 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
	 
	activeWire.points = new Vector3[segments + 1]; 
	activeWire.line = new VectorLine("ActiveLine", activeWire.points, activeMaterial, 8.0f, LineType.Continuous, Joins.None); 
}

Note that when we create the VectorLine we use activeMaterial as an argument this time. Now let's create a function that will set the activeWire points so they are always on the wire that leads to the current destination.

 
public void SetActiveWire() 
{ 
} 
 
void Update () 
{

We'll start the function by checking whether there is any destination that wires are connected to, if there isn't we can safely assume the active wire won't be needed and we don't need to render it.

 
public void SetActiveWire() 
{ 
	if (portal.destPortals.Count == 0) 
		Vector.Active(activeWire.line, false); 
}

In other case we need to set all of activeWire's points so they match the points of the wire that is connected to the current destination. We'll use a for loop for that.

 
public void SetActiveWire() 
{ 
	if (portal.destPortals.Count == 0) 
		Vector.Active(activeWire.line, false); 
	else 
	{ 
		for (int i = 0; i <= segments; ++i) 
			activeWire.points[i] = wires[portal.destID].points[i]; 
	} 
}

Finally we should make our activeWire active if it got deactiveted before.

 
public void SetActiveWire() 
{ 
	if (wires.Count == 0 || portal.destPortals.Count == 0) 
		Vector.Active(activeWire.line, false); 
	else 
	{ 
		for (int i = 0; i <= segments; ++i) 
			activeWire.points[i] = wires[portal.destID].points[i]; 
		Vector.Active(activeWire.line, true); 
	} 
}

And finally we can render it.

 
public void SetActiveWire() 
{ 
	if (wires.Count == 0 || portal.destPortals.Count == 0) 
		Vector.Active(activeWire.line, false); 
	else 
	{ 
		for (int i = 0; i <= segments; ++i) 
			activeWire.points[i] = wires[portal.destID].points[i]; 
		Vector.Active(activeWire.line, true); 
	} 
	 
	Vector.DrawLine3D(activeWire.line); 
}

We can't forget to use the function when we connect the portal.

 
wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
SetActiveWire();

Step 17: Test the Active Line

Go back to the editor and first thing we need to do here is to set the Active Line Material references for every Line Mgr.


Now hit play and see whether the active line is displayed properly once the portals are connected. There is only one active line per portal because a portal can have only one destination at a time.

Click here to try it out.


Step 18: Create a Destroy Button

We've got adding new lines covered, now we need to let player destroy them whenever he wants to do so. We could make player simply click on the wire he wants to destroy, but checking whether he clicked the wire is a bit cumbersome, and the line itself is pretty thin so it wouldn't be very easy to do that. It'll be better if we create a button on the line and then if the player wants to destroy the wire, he can simply click it.


The button's position will be equal to one of a line's points so we don't really need to define it, but we need a sprite so we can actually render it. Let's add a PackedSprite reference to our Wire class.

 
public class Wire 
{ 
	public Vector3[] points; 
	public Vector3[] curvePoints; 
 
	public VectorLine line; 
	 
	public PackedSprite destroyBtn; 
	 
	public Transform btn; 
}

We also need a reference for the button in the LineMgr, so it can be set in the inspector.

 
public Wire activeWire = new Wire(); 
public PackedSprite destroyBtn;

Let's go back to the editor and create a new sprite. Here's the texture you can use for the button.


Set the sprite to pixel-perfect and attach it to Tiles Material. Don't forget to Build Atlases once you set everything.


Now create a prefab of our new sprite, when you do that you can delete the button from the scene. The last step is to drag and drop the prefab to our Destroy Button reference in every LineMgr.



Step 19: Display a Button

Now let's Instantiate our button when the wire is attached to a portal.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
	|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
	{ 
		wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
		portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
		wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
									wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
		SetActiveWire();

There are a few things to note here, this time we instantiate the prefab at a certain position. This position is equal to the position of the seventh point that the line consists of. Since our lines have constant amount of segments, that means that our button will always be positioned approximately one third from the line's beginning, on the line. We don't want to meddle with rotation so we submit a Quaternion.identity to the rotation argument.

For now our button is white, we want its color to be similar to the wire's color, so let's set it now. For now we can use the same color we use in our material.

 
wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
							wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color"));

Now when the line gets connected, a sprite should show up on the wire.



Step 20: Erase a Connected Wire

Now let's look whether the player has tapped on one of our buttons. We'll simply use a for loop to iterate through all the buttons, and if the mouse position is in the range of our circular button at the moment of tapping that means the player used the button. In that case, our curBtn will not be set when the mouse button is pressed up.

 
else if (Input.GetMouseButtonUp(0)) 
{ 
	if (curBtn != null) 
	{ 
		RaycastHit rayInfo; 
		 
		if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
			&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
				|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
		{ 
			wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
			portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
			wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
										wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
			wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color")); 
			SetActiveWire(); 
		} 
		else if (wires[wires.Count - 1].curvePoints[0] != wires[wires.Count - 1].curvePoints[2]) 
			EraseWire(wires.Count - 1); 
		else 
		{ 
			Vector.DestroyLine(ref wires[wires.Count - 1].line); 
			wires.RemoveAt(wires.Count - 1); 
		} 
		 
		curBtn = null; 
	} 
	else 
	{ 
		for (int i = 0; i < wires.Count; ++i) 
		{ 
		} 
	} 
}

The distance from the mouse to the center of the button must be smaller than the button's radius.

 
for (int i = 0; i < wires.Count; ++i) 
{ 
	if ((wires[i].points[6] - new Vector3(curMousePos.x, curMousePos.y, 0.0f)).magnitude <= wires[i].destroyBtn.width/2) 
	{ 
	} 
}

If the user clicked the button then we need to erase the wire and delete a destination that the wire was connected to.

 
for (int i = 0; i < wires.Count; ++i) 
{ 
	if ((wires[i].points[6] - new Vector3(curMousePos.x, curMousePos.y, 0.0f)).magnitude <= wires[i].destroyBtn.width/2) 
	{ 
		portal.DelDest(i); 
		EraseWire(i); 
		break; 
	} 
}

Step 21: Synchornize the Lists

Now, because our EraseWire() deletes the wire approximately 0.25 seconds after the call of this function, our lists will be out of sync for that time, and that's certainly not something we desire. To solve this problem we need to delete the wire immidiately, but if we do that, the animation would not be played because there is no wire to animate. For that reason we need to create a special wire that will be used solely for the purpose of the animation. The real wire will be deleted immidiately, and the fake wire will replace it simply to play the animation as it goes back to its root.

Let's first define that new wire.

 
public List<Wire> wires = new List<Wire>(); 
public Wire erasedWire = new Wire(); 
public Wire activeWire = new Wire();

We can initialize it in our Start() function.

 
void Start () 
{	 
	portal = transform.parent.GetComponent<Portal>(); 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
	 
	erasedWire.points = new Vector3[segments + 1]; 
	erasedWire.curvePoints = new Vector3[4]; 
	erasedWire.line = new VectorLine("Line", erasedWire.points, material, 8.0f, LineType.Continuous, Joins.None);

Now we need to edit our EraseWire() function. The first thing we should do there is deleting the button. It has been pressed, it served its purpose and we need it no more.

 
void EraseWire(int index) 
{		 
	if (wires[index].destroyBtn != null) 
		wires[index].destroyBtn.Delete();

Now let's copy the essential points of the original wire before it gets deleted.

 
if (wires[index].destroyBtn != null) 
	wires[index].destroyBtn.Delete(); 
 
for (int i = 0; i < 4; ++i) 
	erasedWire.curvePoints[i] = wires[index].curvePoints[i];

You might wonder why are we using a curve to display the fake wire, it's going to be used only for a simple animation that looks as if the line was straight anyway. That's because maybe later in the development we'll want to make this animation a bit fancier, maybe it won't straighten just immidiately but rather gradually and maybe we'll want it to take longer than the 0.25 seconds. It will be really easy to change if we do that this way.

Now that we got our erasedWire set, we can delete the wire that the player wanted to disconnect.

 
for (int i = 0; i < 4; ++i) 
	erasedWire.curvePoints[i] = wires[index].curvePoints[i]; 
		 
Vector.DestroyLine(ref wires[index].line); 
wires.RemoveAt(index);

After that we can immidiately reset the active wire, so it points to the current destination.

 
Vector.DestroyLine(ref wires[index].line); 
wires.RemoveAt(index); 
		 
SetActiveWire();

We need to edit our animation function so it tweens the erasedWire. Note that since we now use a separate line for animation, we no longer need to use the hack that allows us to know the index of the wire we're animating, because we're always animating erasedWire.

 
	SetActiveWire(); 
		 
	iTween.ValueTo(gameObject, iTween.Hash("from", new Vector3(erasedWire.curvePoints[2].x, erasedWire.curvePoints[2].y, erasedWire.curvePoints[2].z),  
				"to", new Vector3(erasedWire.curvePoints[0].x, erasedWire.curvePoints[0].y, erasedWire.curvePoints[2].z),  
				"time", 0.25f, "onupdate", "EraseUpdate")); 
}

Now we need to edit our EraseUpdate function. We need to set the eraseWire's points there.

 
public void EraseUpdate(Vector3 v) 
{	 
	erasedWire.curvePoints[1] = erasedWire.curvePoints[0]; 
	erasedWire.curvePoints[2] = new Vector3(v.x, v.y, v.z); 
	erasedWire.curvePoints[3] = new Vector3(v.x, v.y, v.z);

And finally we need to create the line's curve and render it. We don't need to check whether it's time to delete a wire because we've already deleted it, and we're not going to delete our erasedWire because we want to reuse it every a wire is delete. Once it folds, it will be invisible so there's no need to deactivate it.

 
public void EraseUpdate(Vector3 v) 
{	 
	erasedWire.curvePoints[1] = erasedWire.curvePoints[0]; 
	erasedWire.curvePoints[2] = new Vector3(v.x, v.y, v.z); 
	erasedWire.curvePoints[3] = new Vector3(v.x, v.y, v.z); 
	 
	Vector.MakeCurveInLine(erasedWire.line, erasedWire.curvePoints, segments); 
	Vector.DrawLine3D(erasedWire.line); 
}

And that's it, now our wires are disconnectable.


Step 22: Test Disconnecting

Hit play and test the whole thing, connect and disconnect the wires with various combinations and see if everything works correctly.

Click here to try it out.

It doesn't work well, the order in which the lines become active is incorrect and even more severe problems occur. The cause of that is pretty simple, the DelDest() function in Portal script is incorrect, let's open it up and take a look at the function again.

 
public void DelDest(int index) 
{ 
	destPortals.RemoveAt(index); 
	 
	if (destID == index) 
		NextDest(); 
}

The cause is pretty simple, we leave the destID unchanged after we use the RemoveAt(), which may change the position of our destPortal in the list. We need to take that into account and edit our function. If the destination we're deleting is at a lower index than the destID, then our destination portal's ID will decrease by one. We need to adjust the destID so it matches the current destination correctly.

 
public void DelDest(int index) 
{ 
	destPortals.RemoveAt(index); 
	 
	if (index < destID) 
		--destID; 
}

Now, if the destination we're deleting is our current destination then the similar thing happens, destID is offset by one and therefore pointing to the next portal in the list instead of indexing still the same destination. We really don't want that because we don't even know if the next portal in the list exists and that's why in that case we also need to adjust our destID. So again, we need to decrement the destID and additionally call NextDest(), because our current portal got deleted. By doing that we can avoid using the wrong destID because NextDest() checks whether the next portal exists and depending on the result it sets the current destination.

 
public void DelDest(int index) 
{ 
	destPortals.RemoveAt(index); 
	 
	if (index < destID) 
		--destID; 
	else if (index == destID) 
	{ 
		--destID; 
		NextDest(); 
	} 
}

Let's check if everything's fine now.

Click here to try it out.

Everything works just fine. If you still have any problems, check whether your portals don't have default destinations. If they do, delete them because right now they will cause the destination and wire lists to be unsynced. That's because the number of destinations in that case is bigger than the number of wires, we'll take care of it a bit later.


Step 23: Limit the Number of Connections

We want to have some kind of control over how many wires can come out from one portal, we need to set a limit. Let's open the LineMgr scriptn and create a variable there that will hold a number of wires that can be connected to other portals.

 
public int limit = 0; 
	 
public Collider topBtn; 
public Collider bottomBtn; 
Collider curBtn;

Let's assume that if the limit is equal to -1, then there is no limit at all, when it's equal to 0 then the wires can't be used , when it's equal to 1 then it can have only one connection and so on. We set the initial value to 0, so by default it won't be possible to connect one portal to another. Let's take care of this case first because it's the easiest one.

Let's go to our Update() function. If the limit is equal to 0, then we actually don't have to do anything here, it's as if mechanics didn't exist for this portal. Let's simply return if limit is equal to 0.

 
void Update () 
{ 
	if (limit == 0) 
		return;

Now let's take care of the other cases. The first thing we need to ponder is how will we execute the limit. Are we going to force the player to delete other wires before it can connect another one? That surely doesn't sound too good, especially if there will be a very small limit like one or two connected wires. Instead of that, we can simply delete the oldest wire if a new wire that exceeds the limit is connected. It will still allow the player to delete whichever wire he needs to delete before he creates a new one, but in addition he'll be able to connect a new wire without a hassle of deleting other.

Let's go to the part of the code where a wire gets connected to another portal.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = rayInfo.collider.transform.position; 
	portal.AddDest(rayInfo.collider.transform.parent.parent.GetComponent<Portal>()); 
	wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
								wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
	wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color")); 
	SetActiveWire(); 
}

If after connecting the wire the number of wires is greater than the limit, then we want to delete the oldest wire.

 
wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color")); 
					 
if (limit < wires.Count && limit != -1) 
{ 
} 
 
SetActiveWire();

As you can see, we needed to also check whether the limit is equal to -1, because if that's the case then we'll never have to delete any lines. We'll delete the first wire the same way we erase wires when the destroy button is tapped.

 
if (limit < wires.Count && limit != -1) 
{ 
	portal.DelDest(0); 
	EraseWire(0); 
}

Step 24: Test the Limit

Now let's go to the editor and test the portals' limits by setting it to all kind of values that we handle. Remember that you can find the Limit variable in the LineMgr's inspector. In the first example let's set the portals' limits to 0 and 1 respectively.

Click here to try it out.

Everything seems to work properly. Now let's test for the limits equal two -1 and 2 respectively.

Click here to try it out.

Again, everything seems to work.


Step 25: Color the Wires

Right now if we connect two of our portals with each other we can't visually tell whether it is the first one connected to the second one or the second one connected to the first. To distinguish those cases, let's create two color variables, one for material and another one for activeMaterial.

 
public PackedSprite destroyBtn; 
	 
public Color color; 
public Color activeColor;

Now let's create a copy for our portal so the color change won't affect the main material, because if we changed the main material's color then even wires that don't belong to this LineMgr would have their color changed.

 
void Start () 
{	 
	portal = transform.parent.GetComponent<Portal>(); 
	topBtn = transform.FindChild("Top").collider; 
	bottomBtn = transform.FindChild("Bottom").collider; 
	 
	material = new Material(material); 
	activeMaterial = new Material(activeMaterial);

Finally, let's set the materials' colors to whatever color our color and activeColor are equal to.

 
material = new Material(material); 
material.SetColor("_Color", color); 
activeMaterial = new Material(activeMaterial); 
activeMaterial.SetColor("_Color", activeColor);

Let's test if this works. Go back to the editor and change Color and Active Color values in the LineMgr's inspector to your liking. When you're done, hit play and see whether the wires have the appropiate colors.

Click here to try it out.

Everything seems to work correctly.


Step 26: Create a Glow

When our portal on the left is connected to the one on the right the only indicator of which wire belongs to which portal is the distance between the destroy button and the portal, but that's not really obvious. We need to let player easly know which wires belong to which portals, we'll do that by adding a glow to each portal. The glow will be of the same color the wires of that portal are, so it should be easy to figure out which wires belong to which portal.

We'll use the same sprite we created for destroy buttons, but we'll scale it a bit so it covers the whole portal. First, we need to declare a PackedSprite that will serve us as a reference to the glow.

 
public Color color; 
public Color activeColor; 
	 
PackedSprite glow;

It will also be useful to have a separate color for the glow. Wire's color may be too transparent and activeColor too opaque for the aura or maybe for some reason we would want to have a slightly different hue.

 
public Color color; 
public Color activeColor; 
public Color glowColor; 
	 
PackedSprite glow;

Now let's create our glow sprite in the Start() function, we'll do that pretty much the same way we created wires' buttons.

 
activeWire.points = new Vector3[segments + 1]; 
activeWire.line = new VectorLine("Line", activeWire.points, activeMaterial, 8.0f, LineType.Continuous, Joins.None); 
 
glow = ((GameObject)Instantiate(destroyBtn.gameObject, transform.position, Quaternion.identity)).GetComponent<PackedSprite>();

Let's change the sprite's scale so it actually is big enough to cover a whole portal.

 
glow = ((GameObject)Instantiate(destroyBtn.gameObject, transform.position, Quaternion.identity)).GetComponent<PackedSprite>(); 
glow.transform.localScale = new Vector3(2.0f, 7.0f, 1.0f);

We also need to change the glow's color.

 
glow = ((GameObject)Instantiate(destroyBtn.gameObject, transform.position, Quaternion.identity)).GetComponent<PackedSprite>(); 
glow.transform.localScale = new Vector3(2.0f, 7.0f, 1.0f); 
glow.SetColor(glowColor);

Now we should make the glow a child of the portal that the LineMgr is attached to. If we do that then as the portal is moving, the glow will move with it.

 
glow = ((GameObject)Instantiate(destroyBtn.gameObject, transform.position, Quaternion.identity)).GetComponent<PackedSprite>(); 
glow.transform.localScale = new Vector3(2.0f, 7.0f, 1.0f); 
glow.SetColor(glowColor); 
glow.transform.parent = transform.parent;

Finally, let's set the glow's rotation so it matches the parent's as soon as it's created.

 
glow = ((GameObject)Instantiate(destroyBtn.gameObject, transform.position, Quaternion.identity)).GetComponent<PackedSprite>(); 
glow.transform.localScale = new Vector3(2.0f, 7.0f, 1.0f); 
glow.SetColor(glowColor); 
glow.transform.parent = transform.parent; 
glow.transform.eulerAngles = transform.parent.eulerAngles;

That's it, let's check out how does it look.

Click here to try it out.


Step 27: Make Some Adjustments

For now our wires are in front of both, the portal sprite and portals' blocks. Since they are supposed to go into the blocks, they should be behind them, not in front of. The z position of our portal is 5, the global z position of the blocks is one unit less, 4. If we want to position the wires behind the blocks but in front of the portal sprite, we need to set the z position of any wire between those to, 4.5 will do. To change the position of the wire we need to set all of our curvePoints' z.

In our LineMgr script let's go to the Update() function, where we set the dragged wire's curvePoints.

 
if (curBtn != null && (curMousePos - pressPos).magnitude >= 5.0f) 
{ 
	wires[wires.Count - 1].curvePoints[0] = new Vector3(wires[wires.Count - 1].btn.position.x,		 
														wires[wires.Count - 1].btn.position.y, 4.5f);	 
	wires[wires.Count - 1].curvePoints[1] = new Vector3(wires[wires.Count - 1].btn.position.x, curMousePos.y, 4.5f); 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(curMousePos.x, curMousePos.y, 4.5f); 
	wires[wires.Count - 1].curvePoints[3] = new Vector3(curMousePos.x, wires[wires.Count - 1].btn.position.y, 4.5f); 
	 
	Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments); 
}

Now let's go to the part of the code where we align the connected wire to the block.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].curvePoints[2] = new Vector3(rayInfo.collider.transform.position.x, 
														rayInfo.collider.transform.position.y, 4.5f);

While we are at it, let's fix the alignment. For now we don't apply the changes to the curve, to do that we need to call Vector.MakeCurveInLine().

 
wires[wires.Count - 1].curvePoints[2] = new Vector3(rayInfo.collider.transform.position.x, 
													rayInfo.collider.transform.position.y, 4.5f); 
Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments);

We also need to change the following condition.

 
for (int i = 0; i < wires.Count; ++i) 
{ 
	if ((wires[i].points[6] - new Vector3(curMousePos.x, curMousePos.y, 4.5f)).magnitude <= wires[i].destroyBtn.width/2) 
	{	 
		portal.DelDest(i); 
		EraseWire(i);							 
		break; 
	} 
}

And that's all. Let's see the results.

Click here to try it out.


Step 28: Fix the Colors

If you look at our portals now you can see that the green color seems to be bleeding on the red side. This happens because of our texture format.


To fix that we need to change the parameters of a texture our portal sprite sits in, the one named Tiles Material. It can be found in Sprite Atlases folder.


As you can see, now it's set to Compressed, let's change that to Truecolor. After that we need to press Apply to get the results.


As you can see, now the portals look alright.



Step 29: Set the Active Wire

There's still a little problem with our active line. We don't call SetActiveWire() whenever the destination is changed and that change doesn't concern LineMgr directly. For example, when the ball goes through a portal, it calls NextDest(), so the destination is changed, but the SetActiveWire() isn't called in that case. To solve that we need a reference to LineMgr in our Portal script, and then call SetActiveWire() manually in the NextDest() function. Let's open the Portal script and add this reference.

 
public Vector3 dir; 
 
LineMgr lineMgr;

Let's set it using our GetComponentInChildren() function.

 
void Start () 
{ 
	sprite = GetComponent<PackedSprite>(); 
	 
	lineMgr = GetComponentInChildren<LineMgr>();

Now we just need to use the manager to call SetActiveWire() in both, the NextDest() and SetDest, because those functions can be called without the knowladge of LineMgr. Of course we can do that only if the lineMgr exists.

 
public void NextDest() 
{ 
	++destID; 
	 
	if (destID >= destPortals.Count) 
	{ 
		if (destPortals.Count != 0) 
		{ 
			destID = 0; 
			destPortal = destPortals[0]; 
		} 
		else 
		{ 
			destID = -1; 
			destPortal = null; 
		} 
	} 
	else 
		destPortal = destPortals[destID]; 
		 
	if (lineMgr) 
		lineMgr.SetActiveWire(); 
}

Now for the SetDest().

 
public void SetDest(int index) 
{ 
	if (index < 0 || index >= destPortals.Count) 
	{ 
		destID = -1; 
		destPortal = null; 
	} 
	else 
	{ 
		destID = index; 
		destPortal = destPortals[index]; 
	} 
	 
	if (lineMgr) 
		lineMgr.SetActiveWire(); 
}

That's it.


Step 30: Test Everything Together

Let's create a simple scene that will allow us to test the connecting, disconnecting and going through portals together. You can be as creative as you want, but I'll keep it pretty straight-forward. I'll put three portals to test multiple destinations, destinations and so on.

Click here to try it out.

Everything seems to work just alright, the only concerns is the occasional bug that appears when a ball goes through portal. It happens because the OnTriggerExit() is sometimes called before it should be, just when the clone is created. Because we set lots of stuff in that function, the clone gets all messed up, thankfully we can fix it pretty easly. The bug seems to happen only when framerate is pretty jaggy, so it's fine if you can't reproduce it. Let's open our Portalable script and go where the OnTriggerEnter() function is.

 
void OnTriggerExit(Collider other) 
{ 
	if (portal == null) 
		return; 
	 
	if (other.GetComponent<Portal>() != portal) 
		return; 
	 
	if (clone != null) 
		Destroy(clone.gameObject); 
	 
	clone = null; 
	cloned = false; 
	portal = null; 
	side = 1.0f; 
	 
	if (cols) 
		cols.FinishFiltering(); 
	 
	renderer.material.SetFloat("_Cutoff", float.NegativeInfinity); 
}

The clone at the very beginning has the clone reference set to null. Let's simply return if it exits the portal while it still has clone not set, that will fix our bug because the data won't be reset and the clone will just continue as if nothing happend. If the ball really exits the portal (in that case we don't deal with the bug we just discovered), then it will be destroyed anyway by the original ball, so the data doesn't have to be reset anyway.

 
if (clone != null) 
		Destroy(clone.gameObject); 
else 
	return;

Let's hit play and see if the bug dares to happen again.

Click here to try it out.

Works like a charm.


Step 31: Connect the Default Wires

In this step we'll make it possible for wires to be connected automacially when the level starts. Let's set the first destination of the portal on the left manually in the inscpector to the portal on the right. Once you do that, hit play and see what happens.

Click here to try it out.

As you can see, the ball goes succesfully into the portal on the left and comes our of portal on the right, just as we set in the inspector, but there's no wires connecting those portals. We need to procedurally connect them, it won't be hard, most of the code that's needed is already written after all. Let's open up the LineMgr script and create a new function there. Let's call it ConnectWire().

 
public void ConnectWire(Portal destPortal) 
{ 
} 
 
void Update () 
{

The function requires only one argument, the portal that we need to connect a wire to. We need to get a reference to the destPortal's LineMgr, we'll use it to get the buttons of that portal so we know where precisely should we connect to.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
}

We need to decide whether we are going to connect the wire between two top buttons or between two bottom buttons or between the first portal's top button and second portal's bottom or between the first portal's bottom button and second portal's top button. We'll calculate the distances of all four combinations and then connect the wires between the buttons that are the closest to each other. Let's create an array of four floats and pack the distances between the buttons there.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
	 
	float[] dists = { 
}

Let's start from the distance between the top buttons.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
	 
	float[] dists = {(topBtn.transform.position - mgr.topBtn.transform.position).magnitude,  
}

Then continue to the distance between bottom buttons.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
	 
	float[] dists = {(topBtn.transform.position - mgr.topBtn.transform.position).magnitude,  
					 (bottomBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
}

Now the distance between the first portal's top and second's bottom.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
	 
	float[] dists = {(topBtn.transform.position - mgr.topBtn.transform.position).magnitude,  
					 (bottomBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
					 (topBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
}

And finally the distance between the first portal's bottom and second's top.

 
public void ConnectWire(Portal destPortal) 
{ 
	LineMgr mgr = destPortal.GetComponentInChildren<LineMgr>(); 
	 
	float[] dists = {(topBtn.transform.position - mgr.topBtn.transform.position).magnitude,  
					 (bottomBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
					 (topBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
					 (bottomBtn.transform.position - mgr.topBtn.transform.position).magnitude}; 
}

Now depending on which of the values is the smallest we need set the curBtn to the button from which the wire comes out and also we need to save the position where the wire ends. For that let's create a Vector3.

 
	float[] dists = {(topBtn.transform.position - mgr.topBtn.transform.position).magnitude,  
					 (bottomBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
					 (topBtn.transform.position - mgr.bottomBtn.transform.position).magnitude, 
					 (bottomBtn.transform.position - mgr.topBtn.transform.position).magnitude}; 
					  
	Vector3 destBtn; 
}

Let's use Mathf.Min() to get the smallest value in the array.

 
	Vector3 destBtn; 
	 
	float min = Mathf.Min(dists); 
}

And now we can set our cutBtn and destBtn. If dists[0] is the smallest value, we know that the top buttons must be connected.

 
	float min = Mathf.Min(dists); 
	 
	if (min == dists[0]) 
	{ 
		curBtn = topBtn; 
		destBtn = mgr.topBtn.transform.position; 
	} 
}

If the dists[1] is the smallest value then the bottom buttons must be connected.

 
	float min = Mathf.Min(dists); 
	 
	if (min == dists[0]) 
	{ 
		curBtn = topBtn; 
		destBtn = mgr.topBtn.transform.position; 
	} 
	else if (min == dists[1]) 
	{ 
		curBtn = bottomBtn; 
		destBtn = mgr.bottomBtn.transform.position; 
	} 
}

And analogically we solve the two other cases.

 
	float min = Mathf.Min(dists); 
	 
	if (min == dists[0]) 
	{ 
		curBtn = topBtn; 
		destBtn = mgr.topBtn.transform.position; 
	} 
	else if (min == dists[1]) 
	{ 
		curBtn = bottomBtn; 
		destBtn = mgr.bottomBtn.transform.position; 
	} 
	else if (min == dists[2]) 
	{ 
		curBtn = topBtn; 
		destBtn = mgr.bottomBtn.transform.position; 
	} 
	else 
	{ 
		curBtn = bottomBtn; 
		destBtn = mgr.topBtn.transform.position; 
	} 
}

Now let's create a new wire. We can do it the same way it is done when player clicks one of the buttons.

 
else 
{ 
	curBtn = bottomBtn; 
	destBtn = mgr.topBtn.transform.position; 
} 
 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1]; 
wires[wires.Count - 1].curvePoints = new Vector3[4]; 
wires[wires.Count - 1].line = new VectorLine("Line", wires[wires.Count - 1].points, 
												material, 8.0f, LineType.Continuous, Joins.None); 
wires[wires.Count - 1].btn = curBtn.transform;

Now let's set the curvePoints.

 
wires.Add(new Wire()); 
wires[wires.Count - 1].points = new Vector3[segments + 1]; 
wires[wires.Count - 1].curvePoints = new Vector3[4]; 
wires[wires.Count - 1].line = new VectorLine("Line", wires[wires.Count - 1].points, 
												material, 8.0f, LineType.Continuous, Joins.None); 
wires[wires.Count - 1].btn = curBtn.transform; 
 
wires[wires.Count - 1].curvePoints[0] = new Vector3(wires[wires.Count - 1].btn.position.x,		 
													wires[wires.Count - 1].btn.position.y, 4.5f);	 
wires[wires.Count - 1].curvePoints[1] = new Vector3(wires[wires.Count - 1].btn.position.x, destBtn.y, 4.5f); 
wires[wires.Count - 1].curvePoints[2] = new Vector3(destBtn.x, destBtn.y, 4.5f); 
wires[wires.Count - 1].curvePoints[3] = new Vector3(destBtn.x, wires[wires.Count - 1].btn.position.y, 4.5f);

We also need to call MakeCurveInLine() and the drawing function.

 
wires[wires.Count - 1].curvePoints[3] = new Vector3(destBtn.x, wires[wires.Count - 1].btn.position.y, 4.5f); 
 
Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments); 
Vector.DrawLine3D(wires[wires.Count - 1].line);

We also need to create a destroyBtn, so the lines we create at the beginning are also destructable.

 
Vector.MakeCurveInLine(wires[wires.Count - 1].line, wires[wires.Count - 1].curvePoints, segments); 
Vector.DrawLine3D(wires[wires.Count - 1].line); 
 
wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
										wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color"));

In case we add too many lines at the beginning, we shouldn't skip the part in which we erase the wires.

 
wires[wires.Count - 1].destroyBtn = ((GameObject)Instantiate(destroyBtn.gameObject, 
										wires[wires.Count - 1].points[6], Quaternion.identity)).GetComponent<PackedSprite>(); 
wires[wires.Count - 1].destroyBtn.SetColor(material.GetColor("_Color")); 
 
if (limit < wires.Count && limit != -1) 
{ 
	portal.DelDest(0); 
	EraseWire(0); 
}

Finally we can SetActiveWire() and reset the curBtn to null.

 
	if (limit < wires.Count && limit != -1) 
	{ 
		portal.DelDest(0); 
		EraseWire(0); 
	} 
 
	SetActiveWire(); 
	curBtn = null; 
}

Step 32: Connect the Default Wires Part 2

We've got our ConnectWire() function completed, now let's actually use it in our Start() function of the LineMgr script. For every portal in destPortals we need to connect one wire.

 
glow.transform.eulerAngles = transform.parent.eulerAngles; 
 
for (int i = 0; i < portal.destPortals.Count; ++i) 
{ 
}

We want to use our function only if the portal we want to connect to exists.

 
for (int i = 0; i < portal.destPortals.Count; ++i) 
{ 
	if (portal.destPortals[i] != null) 
		ConnectWire(portal.destPortals[i]); 
}

That's it, now we can go back to the editor and see if the wires are connecting at the beginning. We should also add enough connections to see if multiple default destinations also work.

Click here to try it out.

As you can see, everything works correctly.


Step 33: Drag the Camera

In this step we'll be creating a script that will allow us to drag a camera using our mouse. The first thing we need to do is to create a script in our Game Scripts folder, let's call it CameraMovement.

 
using UnityEngine; 
using System.Collections; 
 
public class CameraMovement : MonoBehaviour 
{ 
	void Start() 
	{ 
	} 
	 
	void Update() 
	{ 
	} 
}

We'll need three vectors for this script. The first one will keep the camera's position from before it started moving, the second one will keep the position of the mouse when the camera started being dragged and the last one for current mouse position. The last two will help us calculate how much to offset the camera's initial position.

 
public class CameraMovement : MonoBehaviour 
{ 
	[HideInInspector] 
	public Vector3 initPos, clickPos, mousePos;

We'll also need a bool that will indicate whether the camera is being dragged or not.

 
[HideInInspector] 
public Vector3 initPos, clickPos, mousePos; 
[HideInInspector] 
public bool btnDown;

To start dragging the camera the player must first press the mouse button. Let's create a condition for that in the Update() function.

 
void Update() 
{ 
	if (Input.GetMouseButtonDown(0)) 
	{ 
	} 
}

We don't want the camera to be dragged if the player clicked one of portal buttons. To make sure we don't do that, we need to cast a ray that will check for a collision. Of course it would be ok to drag the camera if the player clicked the ground, that's why we must place the buttons so it is possible for the ray to hit the buttons' colliders, but it's impossible to hit the ground. To achieve that, let's move the portal buttons z to something like -30 in the editor, remember apply the changes to the prefab after setting Top and Bottom buttons. The next thing we need to do is to change our raycasting in the LineMgr script, so it can hit the buttons now that they are offset. Open LineMgr script and let's change the following line.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
	 
	if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -30.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f))

We need to change the z position of the ray's start vector. Since -30 is already inside the button's collider, we need to change it to something further away, let's say -60.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	RaycastHit rayInfo; 
	 
	if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f))

Of course we also need to change the raycasting after the GetMouseButtonUp().

 
else if (Input.GetMouseButtonUp(0)) 
{ 
	if (curBtn != null) 
	{ 
		RaycastHit rayInfo; 
		 
		if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f)

Let's go back to our CameraMovement script again, to the point where we left off. We need to cast a ray now to see whether there is no button in a way. The first thing we need to do that, is to calculate the mouse's position.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	clickPos = camera.ScreenPointToRay(Input.mousePosition).origin; 
	clickPos = new Vector3(clickPos.x,clickPos.y, -70.0f); 
}

Note that we changed the z position to -70 so we can use it as an origin for the ray. Now we can raycast, we are only interested the boolean result, whether we hit something or not, if not then we can continue with dragging the camera.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	clickPos = camera.ScreenPointToRay(Input.mousePosition).origin; 
	clickPos = new Vector3(clickPos.x,clickPos.y, -70.0f); 
	 
	if (!Physics.Raycast(clickPos, new Vector3(0.0f, 0.0f, 1.0f), 30.0f)) 
	{ 
	} 
}

Length of 30 is enough to detect a portal button but not enough to hit a ground, thanks to that the result will be positive only if we hit a portal button. If the condition is passed, let's set initPos to current camera's position, btnDown to true, and also the clickPos the mouse's position on the screen in the screen space.

 
if (!Physics.Raycast(clickPos, new Vector3(0.0f, 0.0f, 1.0f), 30.0f)) 
{ 
	initPos = transform.position; 
	btnDown = true; 
	clickPos = Input.mousePosition; 
}

If the player stops pressing the button, we have to set the btnDown to false and stop dragging the camera.

 
if (Input.GetMouseButtonDown(0)) 
{ 
	clickPos = camera.ScreenPointToRay(Input.mousePosition).origin; 
	clickPos = new Vector3(clickPos.x,clickPos.y, -70.0f); 
	 
	if (!Physics.Raycast(clickPos, new Vector3(0.0f, 0.0f, 1.0f), 30.0f)) 
	{ 
		initPos = transform.position; 
		btnDown = true; 
		clickPos = Input.mousePosition; 
	} 
} 
else if (Input.GetMouseButtonUp(0)) 
	btnDown = false;

Finally we can change the camera's position. We need only to calculate the difference between the mouse position and clickPos, and add that to the initPos.

 
else if (Input.GetMouseButtonUp(0)) 
	btnDown = false; 
	 
if (btnDown) 
{ 
	mousePos = Input.mousePosition; 
	transform.position = initPos + clickPos - mousePos; 
}

Let's check if the dragging works now. Remember to attach the CameraMovement script to the camera, after that let's hit play and check it out.

Click here to try it out.

Seems that it works alright, the background looks weird because it's moving with the same speed as objects that are technically much closer to the camera, but we'll take care of that a bit later. Another thing is altough we've made dragging the camera possible, we don't really want the user to be able to drag it just anywhere he wants.


Step 34: Limit the Camera

For now the camera can be dragged without any limits, that's certainly not something we want. Let's add a few variables to the CameraMovement script, these will constrain the camera's movement.

 
[HideInInspector] 
public Vector3 initPos, clickPos, mousePos; 
[HideInInspector] 
public bool btnDown; 
 
public float xMin; 
public float xMax; 
public float yMin; 
public float yMax;

Let's go to our Update() function and after the dragging code, let's adjust the camera's position if it went beyound its constraints. Let's start from x axis.

 
if (btnDown) 
{ 
	mousePos = Input.mousePosition; 
	transform.position = initPos + clickPos - mousePos; 
} 
 
if (transform.position.x < xMin) 
	transform.position = new Vector3(xMin, transform.position.y, transform.position.z); 
else if (transform.position.x > xMax) 
	transform.position = new Vector3(xMax, transform.position.y, transform.position.z);

As you can see, if the camera's position on the x axis is less than xMin, it gets offset to xMin so it can't go beyound that value. Analogically we solve for the case when the camera's position on x axis is greater than the xMax. Now we need to do the same for the y axis.

 
if (transform.position.x < xMin) 
	transform.position = new Vector3(xMin, transform.position.y, transform.position.z); 
else if (transform.position.x > xMax) 
	transform.position = new Vector3(xMax, transform.position.y, transform.position.z); 
	 
if (transform.position.y < yMin) 
	transform.position = new Vector3(transform.position.x, yMin, transform.position.z); 
else if (transform.position.y > yMax) 
	transform.position = new Vector3(transform.position.x, yMax, transform.position.z);

That's it, let's go back to the editor and constrain the Main Camera's movement so we don't leave our stage. Remember that floats are equal to 0.0 by default, so if we leave the constraints untouched the camera's position will be glued to (0.0, 0.0) and no draggin will be possible.

Click here to try it out.

Camera's movement is very limited because our stage is very small at the moment and it shouldn't really go beyound it.


Step 35: Create Parallax Scrolling Script

We'll take care of the background with this script. Because our camera is orthographic, the parallax effect isn't applied automatically even if the object is very far from the camera. We need to create a script that will offset an object depending on its distance from the camera. Let's name it Parallax.

 
using UnityEngine; 
using System.Collections; 
 
public class Parallax : MonoBehaviour 
{ 
	void Start () 
	{ 
		 
	} 
 
	void Update () 
	{ 
		 
	} 
}

The first thing I want to note is that we'll not be calculating the parallax the proper way i.e. calculating the offset using the z position of the object. Instead, we'll simply calculate the difference between the camera's position and camera's initial position and we'll scale it and apply to the object so it will appear as if it's moving with different speed than the camera. We need two vectors for that, one for the initial camera's position and another for the offset that we're going to calculate. We'll also need a scalar which will indicate how will we scale the offset.

 
public class Parallax : MonoBehaviour 
{ 
	Vector3 offset, initCamPos; 
	public float factor = 0.8f;

The scale is named factor because scale wouldn't really tell much what it does anyway and factor says that it influences the parallax effect in some way. Its default value is equal to 0.8. Let's set the initCamPos in the Start() function.

 
void Start () 
{ 
	initCamPos = Camera.main.transform.position; 
}

We get our camera's reference by using Camera.main. Now let's start by calculating the difference between the camera's positions and then scaling it. We need to do that in the Update() function.

 
void Update() 
{ 
	offset = (Camera.main.transform.position - initCamPos)*factor; 
}

Now we can add the offset to our object's position.

 
void Update() 
{ 
	offset = (Camera.main.transform.position - initCamPos)*factor; 
	transform.position += offset; 
}

If we left it as it is, the offset would just stack up with the one from the previous frame and the object would just start moving on its own. We need to substract the offset from the position before we apply the calculate the updated offset.

 
void Update() 
{ 
	transform.position -= offset; 
	offset = (Camera.main.transform.position - initCamPos)*factor; 
	transform.position += offset; 
}

Note that in the first Update() call the offset will be equal to (0.0, 0.0, 0.0), because the Vector3 is equal to that when it's created, so thanks to that it won't cause any change in the object's position.

Go back to the editor and attach the Parallax Script to the parent object of all of you backgrouns sprites. Hit play and see the results. I loosened the camera's constraint a bit to make the parallax effect more visible, it may be that you will want to increase the number of rows and or columns in the background, as I did.

Click here to try it out.

As you can see, the parallax doesn't work very well. To make it better, we need to round the offset so we can avoid the distortion in our sprites.

 
void Update() 
{ 
	transform.position -= offset; 
	offset = (Camera.main.transform.position - initCamPos)*factor; 
	offset = new Vector3(Mathf.Round(offset.x), Mathf.Round(offset.y), 0.0f); 
	transform.position += offset; 
}

Hit play and and see how does it work now.

Click here to try it out.

It's still a shaking hell, there are two ways of solving that. We can duplicate the Main Camera and delete the original (when you duplicate, the copy gets automatically selected, so if you want to delete the original Main Camera then you have to delete the unselected one). That will make the camera update after the background and in result the jitter will disappear. The other way is simply calculating and applying our offset in the LateUpdate(), which is called later than the standard Update().

 
void LateUpdate() 
{ 
	transform.position -= offset; 
	offset = (Camera.main.transform.position - initCamPos)*factor; 
	offset = new Vector3(Mathf.Round(offset.x), Mathf.Round(offset.y), 0.0f); 
	transform.position += offset; 
}

Step 36: Create the First Level

Let's create the first level. It'll be a very simple one, we'll introduce the player to the gameplay mechanics. Basically what we need are two portals with no default destinations. The player's task will be to connect the first portal to the second one so the ball can get to the exit, if the player doesn't do so then the ball will roll into a hole. Here's how I setup this level.

Click here to try it out.

Another thing we want to do now is to place a few hints how to navigate around. We'll use a plain text for that now, just for testing purposes, of course you wouldn't want to keep it in the final build, where everything should be pretty. To create a 3D text you need to go to GameObject->Create Other->3D Text. The text will be created in the scene with default Hello World. Change the text and its size to suit your needs, you can also change the text's color if you modify its material. The default font is Arial, it's good enough for test purposes.

Click here to try it out.


Step 37: Reset the Ball

After the ball falls through the hole we should reset it to its original position so there's no instant game over. Let's open the Ball script.

 
using UnityEngine; 
using System.Collections; 
 
public class Ball : MonoBehaviour 
{ 
	public Vector3 initVel; 
	 
	void Start () 
	{ 
		rigidbody.velocity = initVel; 
	} 
 
	void Update () 
	{ 
		 
	} 
}

Let's create a resetHeight variable here, it'll indicate the height at which the ball should be reset. Of course the value must be pretty low, so the ball can reach it only if it falls from the platform.

 
public class Ball : MonoBehaviour 
{ 
	public Vector3 initVel; 
	public float resetHeight = -500.0f;

We also need an initial position so we can reset the ball to the right place.

 
public class Ball : MonoBehaviour 
{ 
	public Vector3 initVel, initPos; 
	public float resetHeight = -500.0f;

Note that we can't set the initPos in our Start() function, we have to do that manually from the inspector. That's because if it was in the Start() function, then if the ball were to fall to resetHeight after cloning itself, the clone would override the initPos when it would be created. We don't want the position of the creation of the ball, we want the position where it initially was placed in the level.

Let's go to our update and create a condition that will be met if the ball will go beyound the resetHeight.

 
void Update () 
{ 
	if (transform.position.y <= resetHeight) 
	{ 
	} 
}

We need to do three things here, the first is to reset the position to the initPos, so everytime it falls it starts from the same position. The second thing is to reset the velocity, so it starts with the same velocity every time it has been reset. The last thing is the angular velocity, we simply need to set it to (0.0, 0.0, 0.0), because initially the ball doesn't rotate around, if we wouldn't do that then the ball would maintain the angular velocity from when it passed the resetHeight. That of course would also change the velocity when the ball would hit any solid object.

 
void Update () 
{ 
	if (transform.position.y <= resetHeight) 
	{ 
		transform.position = initPos; 
		rigidbody.velocity = initVel; 
		rigidbody.angularVelocity = Vector3.zero; 
	} 
}

Let's try it. Remember to set the initPos in the Ball's inspector.

Click here to try it out.

The ball resets correctly.


Step 38: Create the Second Level

Before we'll take care of changing the levels, we need a second one so we actually have another level that we can load. First, let's rename our demo scene to Level 1 in the project view. The next thing we need to do is to duplicate the Level 1, so we don't have to start the second level from scratch. Open up the Level 2 and let's get to editing. This time the scene has a following layout.

Click here to try it out.

As you can see, this time the ball will have to go two portals to get to the last one and fall off to the exit. It's just a tiny bit more complicated than the previous one. But here's a problem that pops out, we want the ball to fall to the first portal. There are currently two ways we can do that, either we spawn the ball above the screen and it will drop with high enough speed to jump above the visible area from the next portal. We don't want that. The other way is to place the ball in a visible spot above the first portal and then it will jump out from the next portal to the same height. We also don't want that because if the ball falls into the hole, it'll just get reset and will magically appear in the initial position. We'll have to create another portal from which the ball will drop down and then enter the first one on the left in the layout above. Since the portal we want to create is the one through which we enter the level (at least it seems so for the player), let's distinguish it from other portals.

Here's a texture for the big portal.


Here's a texture for the big portal's blocks.


Import those and then copy one of the already created portals in the scene, open the Sprite Timeline and change both, the texture that is used for the portal to the bigPortalLine and the texture of the blocks to the bigPortalBlock. Change the portal's height to 196. Finally, delete its LineMgr, we won't use it in big portals. After all that, create a prefab for the new big portal and Build Atlases. After you hit play, the UVs should update and the big portal should like this.


Update the sizes of the bug portal's colliders so blocks' colliders fit the mesh, portal's Box Collider is long enough to cover the whole portal and Check Area collider also covers the whole length of that portal. Rename the portal to Big Portal Enter, because the ball will be entering the stage through this portal. Now duplicate this portal, change the clone's name to Big Portal Start and place it somewhere on the left from the left wall and place the ball just a bit above it.


Don't forget to change the ball's initial velocity to (0.0, 0.0 ,0.0) and initial position. Now set the Big Portal Start's destination to Big Portal Enter.


Finally duplicate one more big portal and place it in the last hole. Name this portal Big Portal Exit. You can make it lead to Big Portal Start so the ball can't be caught by the camera's eye, but since it's in on the buttom of the hole it won't matter much anyway.

Click here to try it out.


Step 39: Move Between the Levels

Now that we've got two levels, we can try to move between them. Before that, let's add the big portals to the first scene too, they won't be techincally taking the ball anywhere, but let's set them up so it's clear where's the enterance and where's the exit. Let's use the same naming conventions here, Big Portal Enter for the portal that the ball is entering the stage and Big Portal Exit for the portal that the ball has to go through to finish the level.

Click here to try it out.

We'll change the level when the ball exits the Big Portal Exit. Let's go to the Ball script to code this functionality. The first thing we need to do is to create our OnTriggerExit() function so we know when we exit the big portal.

 
void OnTriggerExit(Collider other) 
{ 
}

We'll check whether we deal with Big Portal Exit by checking the name of the object we just exited.

 
void OnTriggerExit(Collider other) 
{ 
	if (other.name.Contains("Big Portal Exit")) 
}

We also need to know that there is a next level if we're going to move into it. We can check that using the Application.loadedLevel to get the 0 based ID of the level that is currently loaded and the Application.levelCount.

 
void OnTriggerExit(Collider other) 
{ 
	if (other.name.Contains("Big Portal Exit") && (Application.loadedLevel != Application.levelCount - 1)) 
}

If that condition is met then we can change the scene.

 
void OnTriggerExit(Collider other) 
{ 
	if ((other.name == "Big Portal Exit") && (Application.loadedLevel != Application.levelCount - 1)) 
		Application.LoadLevel(Application.loadedLevel + 1); 
}

This should work just fine. The final step is adding our scenes to the build. Let's go back to the editor and go to File->Build Settings", and then drop both of our scenes in the Scenes In Build window.


Hit play and test whether everything works well.


Step 40: Rotate the Portals

In this step we'll add a new functionality to the portals. Instead of letting the player only connect them, he will also be able to rotate them around. For that we'll need a new button for each of our portals. Before we'll add them though, let's clone one of our stages so we'll be able to work in Level 3 without messing the previous levels. Once you open the Level 3, create a new empty object in it and name it Rotate Btn, attach a box collider to it and change its size to (20.0, 100.0, 30.0). Next thing you need to do is to drag it onto the portal so the button become its child and finally change the buttons z position to -30. Of course the button's Is Trigger checkbox must be set to true.

Now we can create a script. Let's name it Rotate.

 
using UnityEngine; 
using System.Collections; 
 
public class Rotate : MonoBehaviour 
{ 
 
	void Start () 
	{ 
		 
	} 
 
	void Update () 
	{ 
		 
	} 
}

The first thing we need to do is to get a reference to the parent's portal.

 
public class Rotate : MonoBehaviour 
{ 
	Portal portal; 
	 
	void Start () 
	{ 
		portal = transform.parent.GetComponent<Portal>(); 
	}

Another thing is the angle that the portal is going to be rotated by when the button will be pressed.

 
public class Rotate : MonoBehaviour 
{ 
	Portal portal; 
	public float angle = 0.0f;

Now let's go to the Update() function and handle the input there. We'll do that pretty much the same way we handle the input for the LineMgr's buttons. But first, let's simply return if the angle is equal to 0, since there's no point rotating the portal by 0 degress.

 
void Update () 
{ 
	if (angle == 0.0f) 
		return;

Now let's get a mouse input, position and prepare a RaycastHit for the raycasting.

 
void Update () 
{ 
	if (angle == 0.0f) 
		return; 
		 
	if (Input.GetMouseButtonUp(0)) 
	{ 
		Vector3 curMousePos = Camera.main.ScreenPointToRay(Input.mousePosition).origin; 
		RaycastHit rayInfo;

Let's check whether the ray hits our collider.

 
void Update () 
{ 
	if (angle == 0.0f) 
		return; 
		 
	if (Input.GetMouseButtonUp(0)) 
	{ 
		Vector3 curMousePos = Camera.main.ScreenPointToRay(Input.mousePosition).origin; 
		RaycastHit rayInfo; 
		 
		if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
			&& (rayInfo.collider == collider)) 
		{ 
		} 
	} 
}

If it won't, we don't do anything, if it will then we should start rotating the portal. We'll use the iTween for that to get a smooth rotation. We'll need to use RotateBy() function.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
			&& (rayInfo.collider == collider)) 
{ 
	iTween.RotateBy(portal.gameObject, iTween.Hash("z", angle/360.0f, "time", 0.5f, "easetype",  
					iTween.EaseType.easeInOutExpo)); 
}

So the first argument is the object that we're going to rotate, our parent. The second are the parameters for the tween, and here we tell the iTween that we want to rotate on the z axis by angle/360.0f. We had to devide by 360.0f because this parameter takes the number of full rotations, so for example if we want to rotate by 180.0f degrees, that is 180.0f/360.0f = 0.5f full rotations. The next parameter is how much time should the animation take place, and the last one is the ease type.

The animation is set, but rotating the portal will not work as we want it to just yet.


Step 41: Rotate the Portals Part 2

We can't simply get away with only rotating the portal, we also need to update its factors. To do that, let's open the Portal script and separate the portion of the code where we set the factors to another function. Let's call that function Init().

 
public void Init() 
{ 
	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; 
	 
	dir = new Vector3(transform.position.y - facs.top.y, facs.top.x - transform.position.x, 0.0f); 
	dir.Normalize(); 
}

Let's edit the Start() function so it uses Init() instead of calculating the factors itself.

 
void Start () 
{ 
	sprite = GetComponent<PackedSprite>(); 
	lineMgr = GetComponentInChildren<LineMgr>(); 
	height = ((BoxCollider)collider).size.y*transform.localScale.y; 
	 
	Init(); 
	 
	if (destPortals.Count != 0 && destPortals[0] != null) 
	{ 
		destPortal = destPortals[0]; 
		destID = 0; 
	} 
	else 
	{ 
		destPortal = null; 
		destID = -1; 
	} 
}

Now let's go back to the Rotate script and edit it so it uses the Init() function.


Step 42: Rotate the Portals Part 3

Open Rotate script and let's add here a bool that will indicate whether our portal is tweening or not.

 
Portal portal; 
public float angle = 0.0f; 
bool isTweening = false;

In Update() let's use the portal's Init() function if isTweening is true. This will force the portal to update its factors.

 
void Update () 
{ 
	if (angle == 0.0f) 
		return; 
	 
	if (isTweening) 
		portal.Init();

We need to set tweening to true when we start the animation.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
			&& (rayInfo.collider == collider)) 
{ 
	iTween.RotateBy(portal.gameObject, iTween.Hash("z", angle/360.0f, "time", 0.5f, "easetype",  
					iTween.EaseType.easeInOutExpo)); 
	isTweening = true; 
}

We need to reset isTweening to false once the portal stops rotating. Thankfully, iTween has callbacks to make that easy for us, we just need to tell it which object should receive the callback and what's the name of the function that needs to be called.

 
iTween.RotateBy(portal.gameObject, iTween.Hash("z", angle/360.0f, "time", 0.5f, "easetype",  
					iTween.EaseType.easeInOutExpo, "oncompletetarget", gameObject, "oncomplete", "Finish"));

The first additional argument, "oncompletetarget" is a reference to the object that's going to receive "oncomplete" callback. The next one, "oncomplete", is a name of the function that we want to be called in that object once the animation is finished.

Now let's create the Finish() function so the iTween can call it.

 
void Start () 
{ 
	portal = transform.parent.GetComponent<Portal>(); 
} 
	 
void Finish() 
{ 
	 
}

We've got a few things to do here. First, let's reset the isTweening.

 
void Finish() 
{ 
	isTweening = false; 
}

Next thing is rounding the portal's angle. It appears that RotateBy is not very accurate and the portal's angle after the animation is often off by a little, rounding will fix that. After that we need to update the portal's factors the last time and we're done.

 
void Finish() 
{ 
	isTweening = false; 
	portal.transform.eulerAngles = new Vector3(0.0f, 0.0f, Mathf.Round(portal.transform.eulerAngles.z)); 
	portal.Init(); 
}

Now let's go back to the editor, attach new script to the Rotate Btn and change the angle by which the portal is going to be rotated so we can test it. Don't forget to Apply the changes to the prefab.

Click here to try it out.

Portals rotate fine, but there's a problem - wires don't rotate with them. We need to update the wires' positions too, so they don't get deattached.


Step 43: Update the Wires

Let's open our LineMgr script and create a function that will update the wires' position, but before that we need to have more data that we can work on. We need a transform of a button that the wire is connected to, so we need to edit our Wire class to take that into account.

 
public class Wire 
{ 
	public Vector3[] points; 
	public Vector3[] curvePoints; 
	 
	public VectorLine line; 
	 
	public PackedSprite destroyBtn; 
	 
	public Transform btn; 
	public Transform destBtn; 
}

Now we need to set it. First let's go to the ConnectWire() function and set the destBtn for each wire created there.

 
wires[wires.Count - 1].btn = curBtn.transform; 
		 
if (destBtn == mgr.topBtn.transform.position) 
	wires[wires.Count - 1].destBtn = mgr.topBtn.transform; 
else 
	wires[wires.Count - 1].destBtn = mgr.bottomBtn.transform;

In this function destBtn is a Vector3, not a reference to the transform, that's why we've got to compare whether it's the top or the bottom button of the destination portal. Now let's go to our Update() function and set each wire's destBtn there too.

 
if (Physics.Raycast(new Vector3(curMousePos.x, curMousePos.y, -60.0f), new Vector3(0.0f, 0.0f, 1.0f), out rayInfo, 40.0f) 
	&& ((rayInfo.collider.name == "Top" && rayInfo.collider != topBtn) 
		|| (rayInfo.collider.name == "Bottom" && rayInfo.collider != bottomBtn))) 
{ 
	wires[wires.Count - 1].destBtn = rayInfo.collider.transform;

Now we can create UpdateWires() function.

 
void UpdateWires() 
{ 
}

Let's use for loop to go through every wire.

 
void UpdateWires() 
{ 
	for (int i = 0; i < wires.Count; ++i) 
	{ 
	} 
}

Now, if a wire isn't yet connected and doesn't have destBtn set, we want to skip it and continue the loop.

 
void UpdateWires() 
{ 
	for (int i = 0; i < wires.Count; ++i) 
	{ 
		if (wires[i].destBtn == null) 
			continue; 
	} 
}

If that's not the case, we need to set update the wire's curvePoints.

 
void UpdateWires() 
{ 
	for (int i = 0; i < wires.Count; ++i) 
	{ 
		if (wires[i].destBtn == null) 
			continue; 
			 
		wires[i].curvePoints[0] = new Vector3(wires[i].btn.position.x, wires[i].btn.position.y, 4.5f); 
		wires[i].curvePoints[1] = new Vector3(wires[i].btn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[2] = new Vector3(wires[i].destBtn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[3] = new Vector3(wires[i].destBtn.position.x, wires[i].btn.position.y, 4.5f); 
	} 
}

We also need to calculate the curve and draw the line.

 
void UpdateWires() 
{ 
	for (int i = 0; i < wires.Count; ++i) 
	{ 
		if (wires[i].destBtn == null) 
			continue; 
			 
		wires[i].curvePoints[0] = new Vector3(wires[i].btn.position.x, wires[i].btn.position.y, 4.5f); 
		wires[i].curvePoints[1] = new Vector3(wires[i].btn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[2] = new Vector3(wires[i].destBtn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[3] = new Vector3(wires[i].destBtn.position.x, wires[i].btn.position.y, 4.5f); 
		 
		Vector.MakeCurveInLine(wires[i].line, wires[i].curvePoints, segments); 
		Vector.DrawLine3D(wires[i].line);	 
	} 
}

We also need to update the destroyBtn's position and finally, when we're already iterated through the whole list we should also update the active wire.

 
void UpdateWires() 
{ 
	for (int i = 0; i < wires.Count; ++i) 
	{ 
		if (wires[i].destBtn == null) 
			continue; 
			 
		wires[i].curvePoints[0] = new Vector3(wires[i].btn.position.x, wires[i].btn.position.y, 4.5f); 
		wires[i].curvePoints[1] = new Vector3(wires[i].btn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[2] = new Vector3(wires[i].destBtn.position.x, wires[i].destBtn.position.y, 4.5f); 
		wires[i].curvePoints[3] = new Vector3(wires[i].destBtn.position.x, wires[i].btn.position.y, 4.5f); 
		 
		Vector.MakeCurveInLine(wires[i].line, wires[i].curvePoints, segments); 
		Vector.DrawLine3D(wires[i].line);	 
		 
		wires[i].destroyBtn.transform.position = wires[i].points[6]; 
	} 
	 
	SetActiveWire(); 
}

Let's use our function in Update(), so we don't have to create any additional code in Rotate script.

 
if (wires.Count > 0) 
{ 
	UpdateWires(); 
	Vector.DrawLine3D(wires[wires.Count - 1].line);	 
}

Let's go back to the editor and hit play to test everything out.

Click here to try it out.

As you can see, the wires are updated correctly. Allowing the player to rotate portals in this position is very glitch-prone, and generally allowing to let the ball go through a rotating portal is like that, but it's a fun mechanic and it would be a waste to not use both sides of the portals.


Step 44: Other Levels

Now that all elements of the game are finished we can continue creating the levels which make use of our mechanics. First level showed that the portals can be connected, second level showed that portals can be connected into a chain, in the third level we'll show that the connection may be broken.

Click here to try it out.

The portals can't be rotated and there's one default connection, there's also a text which hints the player to tap the green circle.

In the next level we'll make the player use the portals to gain more speed.

Click here to try it out.

And finally let's make player make use of the rotations.

Click here to try it out.

And here's the final build, all levels together.

Click here to try it out.


Conclusion

And that's it for the whole tutorial! It has been a lot of work, but hard work always pays off, I can only hope that you're feeling as satisfied after finishing it as I do. You could have noticed that I didn't put much weight into the game's performance. When I do a one man project I really like to concentrate on getting things done rather than worrying about performance, of course that's true only until things get ugly. There are also no commentaries, I decided to exclude them because the tutorial is one big commentary after all. The game has around one thousand self-written lines of code, which I think shows how much work was done behind our backs, it is really great if you can focus on the game's specifics instead of the generalities. Thanks for your time!