Drawing a Dashed Line for a Path Game

Now I’ll go over the logic to collect points for a path, to be used in a path game like Flight Control. At the end of this tutorial you should be able to have something like the image above: namely, the ability to draw an endless dashed line.

What will be left after this is simply the logic to have a game object follow the path collected. But I’ll go over that in the next tutorial.

Incidentally, I have covered this logic before, in Objective-C and Java.

So what is a path?

In the previous tutorial we drew a line between points A and B. Now we need to draw a line between A, B, C, D, E, F… We traverse the points through interpolation. So in the end the path is a line that interpolates from A to N point, but passing through points B, C, D, E… until N.

If you recall the formula for interpolation, we use a value from 0 to 1 to represent time, in the state change between BEGINNING STATE to END STATE. In this case the states are positions.

So what the Path does is calculate the full length of the line that goes through A, B, C, N… by adding up the length of all the individual segments: AB, BC, CD…

Let’s go over the classes now and see that functionality in place:

The Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
namespace linepath {
 
	public class Path : MonoBehaviour {
 
		[HideInInspector]
		public float angle;
		[HideInInspector]
		public float totalLength;
		[HideInInspector]
		public List<PathPoint> points;
 
		private PathPoint first;
 
		void Awake () {
			points = new List<PathPoint>();
			totalLength = 0f;
			angle = 0f;
		}
 
 
		public void AppendPoint (Vector2 point) {
			InsertPoint(point, points.Count, false);
		}
 
		public void InsertMultiplePoints(List<Vector2> points, int index) {
			int len = points.Count;
			for (int i = 0; i < len; i++) {
				InsertPoint(points[i], index + i, true);
			}
		}
 
		public PathPoint GetPointAtProgress (float progress) {
 
			progress = Mathf.Abs(progress);
 
			PathPoint point = new PathPoint (0, 0);
			if (progress > 1) progress = 1;
			PathPoint pp = first;
			if (pp == null || pp.next == null) return null;
			if (pp.next.progress < 0) return null;
 
			while (pp.next != null && pp.next.progress < progress) {
				pp = pp.next;
			}
 
			if (pp != null) {
				angle = pp.angle;
				if (pp.length == 0) return null;
				float pathProg = (progress - pp.progress) / (pp.length / totalLength);
				point.x = pp.x + pathProg * pp.xChange;
				point.y = pp.y + pathProg * pp.yChange;
				return point;
 
			}
			return null;
		}
 
		public void Dispose () {
			points.Clear();
		}
 
		public void SetPoints(List<Vector2> value) {
 
			points.Clear();
			InsertMultiplePoints(value, 0);
		}
 
 
		private void InsertPoint(Vector2 point, int index, bool skipOrganize)  {
 
			PathPoint p = new PathPoint (0,0);
			p.SetPoint(point);
 
			if (points.Count == 0) {
				points.Add(p);
			} else {
				points.Insert(index, p);
			}
 
			if (!skipOrganize) {
				Organize();
			}
		}
 
		private void Organize() {
 
			totalLength = 0;
			int last = points.Count - 1;
 
			if (last == -1) {
				first = null;
			} else if (last == 0) {
				first = points[0];
				first.progress = first.xChange = first.yChange = first.length = 0;
				return;
			}
			PathPoint pp = null;
			for (int i = 0; i <= last; i++) { 
				if (points[i] != null) {
					pp = points[i];
					if (i == last) {
						pp.length = 0;
						pp.next = null;
					} else {
						pp.next = points[i + 1];
 
						pp.xChange = pp.next.x - pp.x;
						pp.yChange = pp.next.y - pp.y;
						pp.length = (float) Mathf.Sqrt(pp.xChange * pp.xChange + pp.yChange * pp.yChange);
						totalLength += pp.length;
					}
				}
			}
			first = pp = points[0];
			float curTotal = 0f;
			while (pp != null) {
				pp.progress = curTotal / totalLength;
				curTotal += pp.length;
				pp = pp.next;
			}
 
 
			UpdateAngles();
		}
 
		private void UpdateAngles() {
			PathPoint pp = first;
			while (pp != null) {
				pp.angle = (float) Mathf.Atan2(pp.yChange, pp.xChange ) * Mathf.Rad2Deg;
				pp = pp.next;
			}
		}
 
	}
}

The most important method here is the Organize method. It’s here we calculate the full length of the path, and chain its individual points together. So each point holds a reference to the next point, until that reference is null, which represents the end of the path.

Aside from this, the method GetPointAtProgress is used to draw the dashed line. What I do here is interpolate through the whole path with a certain X value for time, this X value represents the distance between each dash. GetPointAtProgress returns the point at which each dash should be drawn.

UpdateAngles calculates and stores the angle between two points in the path.

The PathPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using UnityEngine;
using System.Collections;
 
namespace linepath {
	public class PathPoint  {
 
		public float x;
		public float y;
		public float progress;
		public float xChange;
		public float yChange;
		public Vector2 point;
		public float length;
		public float angle;
		public PathPoint next;
 
		public PathPoint (float x, float y) {
			this.x = x;
			this.y = y;
			point = new Vector2 (x, y);
			progress = -1;
		}
 
		public void SetPoint (Vector2 point) {
			this.x = point.x;
			this.y = point.y;
			point = new Vector2 (x, y);
			progress = -1;
		}
 
		public void Reset () {
			x = y = progress = xChange = yChange = length = angle = 0;
			next = null;
			point = Vector2.zero;
		}
	}
}

Could just as easily be a Struct, but I prefer to pool it which is why I need a Reset method. The PathPoint knows where it is, at what moment in time it exists (progress), the length of its segment, the amount of change between it and the next point and the angle. The Organize method is responsible to filling out most of this information.

The Scene Structure

The Scene is organized with a GameController gameObject which handles input and contains the Path component.

The drawing is done in the PathDrawing GameObject, which adds its own Camera as you’ll see in a moment, so we can later add other game objects (sprites) and render these above the ddashed line.

The GameController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using linepath;
 
[RequireComponent (typeof(Path))]
public class GameView : MonoBehaviour, InputHandler {
 
 
	public Drawing drawingLayer;
 
	private float baseLength = 2f;
	private int dotsInBaseLength = 5;
	private Path pathController;
	private bool collect;
	private float maxDistance = 0.8f;
	private float progress;
	private Vector2 startPoint;
 
	void Awake ()
	{
		pathController = GetComponent<Path> ();
	}
 
	void FixedUpdate () {
 
		if (pathController.points.Count > 0 ) {
 
			drawingLayer.vertices.Clear();
 
			float numPoints = pathController.totalLength/0.5f;
 
			float progressIncrement = 1/numPoints;
 
			float p = 0.0f;
 
			while (p < 1) {
 
				var point = pathController.GetPointAtProgress(p);
 
				if (point != null && p > progress) {
					drawingLayer.vertices.Add (Camera.main.WorldToScreenPoint(new Vector3(point.x, point.y, 0)));
				}
				p += progressIncrement;
 
			}
		}
	}
 
	public void HandleTouchDown (Vector2 touch)
	{
		var point = Camera.main.ScreenToWorldPoint (touch);
		startPoint = point;
		pathController.points.Clear ();
		drawingLayer.vertices.Clear();
		collect = true; 
 
	}
 
	public void HandleTouchMove (Vector2 touch)
	{
 
		var point = Camera.main.ScreenToWorldPoint (touch);
 
 
		if (collect) {
 
			if (pathController.points.Count == 0) {
				progress = 0;
				List<Vector2> ps = new List<Vector2> ();
				ps.Add ( startPoint );
				pathController.InsertMultiplePoints (ps, 0);
			} else {
 
				Vector2 lastPoint = pathController.points [pathController.points.Count - 1].point;
				if (Vector2.Distance (point, lastPoint) > maxDistance) {
					pathController.AppendPoint ( (Vector2) point);
				}
 
			}
		} 
	}
 
 
	public void HandleTouchUp (Vector2 touch)
	{
		collect = false;
	}
 
}

The only important bit of logic here is the check against maxDistance in HandleTouchMove. This is done so we don’t add too many points unnecessarily.

We update the drawing vertices inside a FixedUpdate.

Drawing Behind other GameObjects

As I mentioned before, we need to add a separate camera to the drawing layer so that it’s rendered behind other GameObjects; something we’ll need in the next tutorial. We add the camera at runtime.

Here is the drawing layer logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public class Drawing : MonoBehaviour
{    
	public List<Vector3> vertices;
 
	private float thickness = 0.005f;
 
	private float dashLength = 0.01f;
 
	private int pointsInWidth = 100;
 
	private float screenWidth = 5;
 
	private float progress = 0;
 
	private float progressIncrement = 0;
 
	private Material lineMaterial;
 
 
	void Awake () {
 
		var drawCamera = gameObject.AddComponent<Camera>();
		transform.position = Camera.main.transform.position;
		transform.rotation = Camera.main.transform.rotation;
		drawCamera.orthographicSize = Camera.main.orthographicSize;
		drawCamera.backgroundColor = Camera.main.backgroundColor;
		Camera.main.renderingPath = RenderingPath.Forward;
		Camera.main.clearFlags = CameraClearFlags.Depth;
 
		drawCamera.depth = Camera.main.depth - 1;
		drawCamera.orthographic = true;
		drawCamera.cullingMask = 0;
 
	}
 
	void Start ()
	{
		vertices = new List<Vector3> (1000);
		var shader = Shader.Find ("Unlit/Color");
		lineMaterial = new Material (shader);
		lineMaterial.color = Color.white;
		lineMaterial.hideFlags = HideFlags.HideAndDontSave;
		lineMaterial.shader.hideFlags = HideFlags.HideAndDontSave;
	}
 
	public void OnPostRender ()
	{
		if (vertices.Count > 1) {
 
			lineMaterial.SetPass (0);
 
			GL.PushMatrix ();
			GL.MultMatrix (transform.localToWorldMatrix);
 
			var i = 0;
 
 
			GL.Begin(GL.QUADS);
			GL.LoadOrtho();
 
			while (i < vertices.Count) {
 
				if (i > 0) {
					var point1 = vertices[i-1];
					var point2 = vertices[i];
 
					point1.x /= Screen.width;
					point2.x /= Screen.width;
 
					point1.y /= Screen.height;
					point2.y /= Screen.height;
 
					Vector2 startPoint = Vector2.zero;
					Vector2 endPoint = Vector2.zero;
 
					var diffx = Mathf.Abs(point1.x - point2.x);
					var diffy = Mathf.Abs(point1.y - point2.y);
 
					if (diffx > diffy) {
						if (point1.x <= point2.x) {
							startPoint = point1;	
							endPoint = point2;
						} else {
							startPoint = point2;
							endPoint = point1;
						}
					} else {
						if (point1.y <= point2.y) {
							startPoint = point1;	
							endPoint = point2;
						} else {
							startPoint = point2;
							endPoint = point1;
						}
					}
 
					var angle = Mathf.Atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x);
					var perp = Mathf.Atan2 (endPoint.x - startPoint.x, -(endPoint.y - startPoint.y));
 
					var p1 = Vector3.zero;
					var p2 = Vector3.zero;
					var p3 = Vector3.zero;
					var p4 = Vector3.zero;
 
					var cosAngle = Mathf.Cos (angle);
					var cosPerp = Mathf.Cos (perp);
					var sinAngle = Mathf.Sin (angle);
					var sinPerp = Mathf.Sin (perp);
 
					var distance = Vector2.Distance(startPoint, endPoint);
 
					progress = 0;
					var segments = (distance * pointsInWidth) / screenWidth;
					progressIncrement = 1/segments;
 
					while (progress < 1)
					{
						var newStartPoint = new Vector2(startPoint.x + (endPoint.x - startPoint.x) * progress,
						                                startPoint.y + (endPoint.y - startPoint.y) * progress);
 
						p1.x = newStartPoint.x - (thickness * 0.5f) * cosPerp;
						p1.y = newStartPoint.y - (thickness * 0.5f) * sinPerp;
 
						p2.x = newStartPoint.x + (thickness * 0.5f) * cosPerp;
						p2.y = newStartPoint.y + (thickness * 0.5f) * sinPerp;
 
						p3.x = p2.x + dashLength * cosAngle;
						p3.y = p2.y + dashLength * sinAngle;
 
						p4.x = p1.x + dashLength * cosAngle;
						p4.y = p1.y + dashLength * sinAngle;
 
						GL.Vertex3 (p1.x, p1.y, 0);
						GL.Vertex3 (p2.x, p2.y, 0);
						GL.Vertex3 (p3.x, p3.y, 0);
						GL.Vertex3 (p4.x, p4.y, 0);
 
						progress += progressIncrement;
					}
				}
 
				i++;
			}
			GL.End ();
			GL.PopMatrix ();
		}
	}
}

You can download the source here.