블로그

  • Curves and Splines, making your own path

    This tutorial will take you from creating a simple line all the way to editing your own Beziér splines. You’ll learn to

    • Create a custom editor;
    • Draw in the scene view;
    • Support editing via the scene view;
    • Create Beziér curves and understand the math behind them;
    • Draw curves and their direction of movement.
    • Build Beziér splines by combining curves;
    • Support free, aligned, and mirrored control points;
    • Support looping splines;
    • Move and place objects along a spline.

    This tutorial builds on the foundation laid by previous tutorials. If you completed the Maze tutorial then you’re good to go.

    This tutorial was made with Unity 4.5.2. It might not work for older versions.

    Fun with splines.

    Lines

    Let’s start simple by creating a line component. It needs two points – p0 and p1 – which define a line segment that goes from the first to the second.

    using UnityEngine;
    
    public class Line : MonoBehaviour {
    
    	public Vector3 p0, p1;
    }
    A simple line.

    While we can now create game objects with line components and adjust the points, we don’t see anything in the scene. Let’s provide some useful visual information when our line is selected. We can do this by creating a custom inspector for our component.

    Editor-related code needs to be placed inside an Editor folder, so create one and put a new LineInspector script in it.

    The inspector needs to extend UnityEditor.Editor. We also have to give it the UnityEditor.CustomEditor attribute. This lets Unity know that it should use our class instead of the default editor for Line components.

    using UnityEditor;
    using UnityEngine;
    
    [CustomEditor(typeof(Line))]
    public class LineInspector : Editor {
    }

    An empty editor does not change anything. We need to add an OnSceneGUI method, which is a special Unity event method. We can use it to draw stuff in the scene view for our component.

    The Editor class has a target variable, which is set to the object to be drawn when OnSceneGUI is called. We can cast this variable to a line and then use the Handles utility class to draw a line between our points.

    	private void OnSceneGUI () {
    		Line line = target as Line;
    
    		Handles.color = Color.white;
    		Handles.DrawLine(line.p0, line.p1);
    	}
    line
    Showing a line.

    We now see the line, but it doesn’t take its transform’s settings into account. Moving, rotating, and scaling does not affect them at all. This is because Handles operates in world space while the points are in the local space of the line. We have to explicitly convert the points into world space points.

    	private void OnSceneGUI () {
    		Line line = target as Line;
    		Transform handleTransform = line.transform;
    		Vector3 p0 = handleTransform.TransformPoint(line.p0);
    		Vector3 p1 = handleTransform.TransformPoint(line.p1);
    
    		Handles.color = Color.white;
    		Handles.DrawLine(p0, p1);
    	}
    untransformed
    Untransformed vs. transformed.

    Besides showing the line, we can also show position handles for our two points. To do this, we also need our transform’s rotation so we can align them correctly.

    	private void OnSceneGUI () {
    		Line line = target as Line;
    		Transform handleTransform = line.transform;
    		Quaternion handleRotation = handleTransform.rotation;
    		Vector3 p0 = handleTransform.TransformPoint(line.p0);
    		Vector3 p1 = handleTransform.TransformPoint(line.p1);
    
    		Handles.color = Color.white;
    		Handles.DrawLine(p0, p1);
    		Handles.DoPositionHandle(p0, handleRotation);
    		Handles.DoPositionHandle(p1, handleRotation);
    	}

    Although we now get handles, they do not honor Unity’s pivot rotation mode. Fortunately, we can use Tools.pivotRotation to determine the current mode and set our rotation accordingly.

    		Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    local handles
    Local vs. global pivot rotation.

    To make the handles actually work, we need to assign their results back to the line. However, as the handle values are in world space we need to convert them back into the line’s local space with the InverseTransformPoint method. Also, we only need to do this when a point has changed. We can use EditorGUI.BeginChangeCheck and EditorGUI.EndChangeCheck for this. The second method tells us whether a change happened after calling the first method.

    		EditorGUI.BeginChangeCheck();
    		p0 = Handles.DoPositionHandle(p0, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			line.p0 = handleTransform.InverseTransformPoint(p0);
    		}
    		EditorGUI.BeginChangeCheck();
    		p1 = Handles.DoPositionHandle(p1, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			line.p1 = handleTransform.InverseTransformPoint(p1);
    		}

    Now we can drag our points in the scene view!

    There are two additional issues that need attention. First, we cannot undo the drag operations. This is fixed by adding a call to Undo.RecordObject before we make any changes. Second, Unity does not know that a change was made, so for example won’t ask the user to save when quitting. This is remedied with a call to EditorUtility.SetDirty.

    		EditorGUI.BeginChangeCheck();
    		p0 = Handles.DoPositionHandle(p0, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(line, "Move Point");
    			EditorUtility.SetDirty(line);
    			line.p0 = handleTransform.InverseTransformPoint(p0);
    		}
    		EditorGUI.BeginChangeCheck();
    		p1 = Handles.DoPositionHandle(p1, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(line, "Move Point");
    			EditorUtility.SetDirty(line);
    			line.p1 = handleTransform.InverseTransformPoint(p1);
    		}

    Curves

    It is time to upgrade to curves. A curve is like a line, but it doesn’t need to be straight. Specifically, we’ll create a Beziér curve.

    A Beziér curve is defined by a sequence of points. It starts at the first point and ends at the last point, but does not need to go through the intermediate points. Instead, those points pull the curve away from being a straight line.

    Create a new BezierCurve component and give it an array of points. Also give it a Reset method that initializes it with three points. This method also functions as a special Unity method, which is called by the editor when the component is created or reset.

    using UnityEngine;
    
    public class BezierCurve : MonoBehaviour {
    
    	public Vector3[] points;
    
    	public void Reset () {
    		points = new Vector3[] {
    			new Vector3(1f, 0f, 0f),
    			new Vector3(2f, 0f, 0f),
    			new Vector3(3f, 0f, 0f)
    		};
    	}
    }

    We also create an inspector for the curve, based on LineInspector. To reduce code repetition, we move the code that shows a point to a separate ShowPoint method that we can call with an index. We also turn curve, handleTransform, and handleRotation into class variables so we don’t need to pass then to ShowPoint.

    While it is a new script, I’ve marked the differences as if we modified LineInspector.

    using UnityEditor;
    using UnityEngine;
    
    [CustomEditor(typeof(BezierCurve))]
    public class BezierCurveInspector : Editor {
    
    	private BezierCurve curve;
    	private Transform handleTransform;
    	private Quaternion handleRotation;
    
    	private void OnSceneGUI () {
    		curve = target as BezierCurve;
    		handleTransform = curve.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    
    		Vector3 p0 = ShowPoint(0);
    		Vector3 p1 = ShowPoint(1);
    		Vector3 p2 = ShowPoint(2);
    
    		Handles.color = Color.white;
    		Handles.DrawLine(p0, p1);
    		Handles.DrawLine(p1, p2);
    	}
    
    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(curve.points[index]);
    		EditorGUI.BeginChangeCheck();
    		point = Handles.DoPositionHandle(point, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(curve, "Move Point");
    			EditorUtility.SetDirty(curve);
    			curve.points[index] = handleTransform.InverseTransformPoint(point);
    		}
    		return point;
    	}
    }
    project
    A 3-point curve.

    The idea of Beziér curves is that they are parametric. If you give it a value – typically named t – between zero and one, you get a point on the curve. As t increases from zero to one, you move from the first point of the curve to the last point.

    To show our curve in the scene, we can approximate it by drawing straight lines between successive steps on the curve. We can do this with a simple loop, assuming our curve has a GetPoint method. We also keep drawing the straight lines between the points, but change their color to gray.

    	private const int lineSteps = 10;
    			
    	private void OnSceneGUI () {
    		curve = target as BezierCurve;
    		handleTransform = curve.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    
    		Vector3 p0 = ShowPoint(0);
    		Vector3 p1 = ShowPoint(1);
    		Vector3 p2 = ShowPoint(2);
    
    		Handles.color = Color.gray;
    		Handles.DrawLine(p0, p1);
    		Handles.DrawLine(p1, p2);
    
    		Handles.color = Color.white;
    		Vector3 lineStart = curve.GetPoint(0f);
    		for (int i = 1; i <= lineSteps; i++) {
    			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
    			Handles.DrawLine(lineStart, lineEnd);
    			lineStart = lineEnd;
    		}
    	}

    Now we have to add the GetPoint method to BezierCurve otherwise it won’t compile. Here we again make an assumption, this time that there’s a utility Beziér class that does the calculation for any sequence of points. We feed it our points and transform the result to world space.

    	public Vector3 GetPoint (float t) {
    		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], t));
    	}

    So we add a static Bezier class with the required method. For now, let’s ignore the middle point and simply linearly interpolate between the first and last point.

    using UnityEngine;
    
    public static class Bezier {
    
    	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
    		return Vector3.Lerp(p0, p2, t);
    	}
    }
    bezier
    Beziér library and linear interpolation.

    Of course, linear interpolation between the end points totally ignores the middle point. So how do we incorporate the middle point? The answer is to interpolate more than once. First, linearly interpolate between the first and middle point, and also between the middle and last point. That gives us two new points. Linearly interpolating between those two gives us the final point on the curve.

    	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
    		return Vector3.Lerp(Vector3.Lerp(p0, p1, t), Vector3.Lerp(p1, p2, t), t);
    	}
    A quadratic Beziér curve.

    This kind of curve is known as a quadratic Beziér curve, because of the polynomial math involved.

    The linear curve can be written as B(t) = (1 – t) P0 + t P1.

    One step deeper you get B(t) = (1 – t) ((1 – t) P0 + t P1) + t ((1 – t) P1 + t P2). This is really just the linear curve with P0 and P1 replaced by two new linear curves. It can also be rewritten into the more compact form B(t) = (1 – t)2 P0 + 2 (1 – t) t P1 + t2 P2.

    So we could use the quadratic formula instead of three calls to Vector3.Lerp.

    	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
    		t = Mathf.Clamp01(t);
    		float oneMinusT = 1f - t;
    		return
    			oneMinusT * oneMinusT * p0 +
    			2f * oneMinusT * t * p1 +
    			t * t * p2;
    	}

    Now that we have a polynomial function, we can also describe its derivatives. The first derivative of our quadratic Beziér curve is B'(t) = 2 (1 – t) (P1 – P0) + 2 t (P2 – P1). Let’s add it.

    	public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, float t) {
    		return
    			2f * (1f - t) * (p1 - p0) +
    			2f * t * (p2 - p1);
    	}

    This function produces lines tangent to the curve, which can be interpreted as the speed with which we move along the curve. So now we can add a GetVelocity method to BezierCurve.

    Because it produces a velocity vector and not a point, it should not be affected by the position of the curve, so we subtract that after transforming.

    	public Vector3 GetVelocity (float t) {
    		return transform.TransformPoint(Bezier.GetFirstDerivative(points[0], points[1], points[2], t)) -
    			transform.position;
    	}

    Now we can visualize the speed along the curve in BezierCurveInspector‘s OnSceneGUI method.

    		Vector3 lineStart = curve.GetPoint(0f);
    		Handles.color = Color.green;
    		Handles.DrawLine(lineStart, lineStart + curve.GetVelocity(0f));
    		for (int i = 1; i <= lineSteps; i++) {
    			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
    			Handles.color = Color.white;
    			Handles.DrawLine(lineStart, lineEnd);
    			Handles.color = Color.green;
    			Handles.DrawLine(lineEnd, lineEnd + curve.GetVelocity(i / (float)lineSteps));
    			lineStart = lineEnd;
    		}
    Showing velocity.

    We can clearly see how the velocity changes along the curve, but those long lines are cluttering the view. Instead of showing the velocity, we can suffice with showing the direction of movement.

    		Vector3 lineStart = curve.GetPoint(0f);
    		Handles.color = Color.green;
    		Handles.DrawLine(lineStart, lineStart + curve.GetDirection(0f));
    		for (int i = 1; i <= lineSteps; i++) {
    			Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
    			Handles.color = Color.white;
    			Handles.DrawLine(lineStart, lineEnd);
    			Handles.color = Color.green;
    			Handles.DrawLine(lineEnd, lineEnd + curve.GetDirection(i / (float)lineSteps));
    			lineStart = lineEnd;
    		}

    Which requires that we add GetDirection to BezierCurve, which simply normalizes the velocity.

    	public Vector3 GetDirection (float t) {
    		return GetVelocity(t).normalized;
    	}
    Showing direction.

    Let’s go a step further and add new methods to Bezier for cubic curves as well! It works just like the quadratic version, except that it needs a fourth point and its formula goes another step deeper, resulting in a combination of six linear interpolations. The consolidated function of that becomes B(t) = (1 – t)3 P0 + 3 (1 – t)2 t P1 + 3 (1 – t) t2 P2 + t3 P3 which has as its first derivative B'(t) = 3 (1 – t)2 (P1 – P0) + 6 (1 – t) t (P2 – P1) + 3 t2 (P3 – P2).

    	public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
    		t = Mathf.Clamp01(t);
    		float oneMinusT = 1f - t;
    		return
    			oneMinusT * oneMinusT * oneMinusT * p0 +
    			3f * oneMinusT * oneMinusT * t * p1 +
    			3f * oneMinusT * t * t * p2 +
    			t * t * t * p3;
    	}
    	
    	public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
    		t = Mathf.Clamp01(t);
    		float oneMinusT = 1f - t;
    		return
    			3f * oneMinusT * oneMinusT * (p1 - p0) +
    			6f * oneMinusT * t * (p2 - p1) +
    			3f * t * t * (p3 - p2);
    	}

    With that, we can upgrade BezierCurve from quadratic to cubic by taking an additional point into consideration. Be sure to add the fourth point to its array either manually or by resetting the component.

    	public Vector3 GetPoint (float t) {
    		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));
    	}
    	
    	public Vector3 GetVelocity (float t) {
    		return transform.TransformPoint(
    			Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;
    	}
    	
    	public void Reset () {
    		points = new Vector3[] {
    			new Vector3(1f, 0f, 0f),
    			new Vector3(2f, 0f, 0f),
    			new Vector3(3f, 0f, 0f),
    			new Vector3(4f, 0f, 0f)
    		};
    	}

    BezierCurveInspector now needs to be updated so it shows the fourth point as well.

    		Vector3 p0 = ShowPoint(0);
    		Vector3 p1 = ShowPoint(1);
    		Vector3 p2 = ShowPoint(2);
    		Vector3 p3 = ShowPoint(3);
    		
    		Handles.color = Color.gray;
    		Handles.DrawLine(p0, p1);
    		Handles.DrawLine(p2, p3);
    A cubic Beziér curve.

    It is probably visually obvious by now that we draw our curve using straight line segments. We could increase the number of steps to improve the visual quality. We could also use an iterative approach to get accurate down to pixel level. But we can also use Unity’s Handles.DrawBezier method, which takes care of drawing nice cubic Beziér curves for us.

    Let’s also show the directions in their own method and scale them to take up less space.

    	private const float directionScale = 0.5f;
    	
    	private void OnSceneGUI () {
    		curve = target as BezierCurve;
    		handleTransform = curve.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    		
    		Vector3 p0 = ShowPoint(0);
    		Vector3 p1 = ShowPoint(1);
    		Vector3 p2 = ShowPoint(2);
    		Vector3 p3 = ShowPoint(3);
    		
    		Handles.color = Color.gray;
    		Handles.DrawLine(p0, p1);
    		Handles.DrawLine(p2, p3);
    		
    		ShowDirections();
    		Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
    	}
    
    	private void ShowDirections () {
    		Handles.color = Color.green;
    		Vector3 point = curve.GetPoint(0f);
    		Handles.DrawLine(point, point + curve.GetDirection(0f) * directionScale);
    		for (int i = 1; i <= lineSteps; i++) {
    			point = curve.GetPoint(i / (float)lineSteps);
    			Handles.DrawLine(point, point + curve.GetDirection(i / (float)lineSteps) * directionScale);
    		}
    	}
    Using Handles.DrawBezier and scaled direction lines.

    Splines

    Having a single curve is nice, but to create complex paths we would need to concatenate multiple curves. Such a construct is known as a spline. Let’s create one by copying the BezierCurve code, changing the type to BezierSpline.

    using UnityEngine;
    
    public class BezierSpline : MonoBehaviour {
    
    	public Vector3[] points;
    	
    	public Vector3 GetPoint (float t) {
    		return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));
    	}
    	
    	public Vector3 GetVelocity (float t) {
    		return transform.TransformPoint(
    			Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;
    	}
    	
    	public Vector3 GetDirection (float t) {
    		return GetVelocity(t).normalized;
    	}
    	
    	public void Reset () {
    		points = new Vector3[] {
    			new Vector3(1f, 0f, 0f),
    			new Vector3(2f, 0f, 0f),
    			new Vector3(3f, 0f, 0f),
    			new Vector3(4f, 0f, 0f)
    		};
    	}
    }

    We also create an editor for it, by copying and tweaking the code from BezierCurveInspector. We can then create a spline object and edit it, just like a curve.

    using UnityEditor;
    using UnityEngine;
    
    [CustomEditor(typeof(BezierSpline))]
    public class BezierSplineInspector : Editor {
    
    	private const int lineSteps = 10;
    	private const float directionScale = 0.5f;
    
    	private BezierSpline spline;
    	private Transform handleTransform;
    	private Quaternion handleRotation;
    
    	private void OnSceneGUI () {
    		spline = target as BezierSpline;
    		handleTransform = spline.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    		
    		Vector3 p0 = ShowPoint(0);
    		Vector3 p1 = ShowPoint(1);
    		Vector3 p2 = ShowPoint(2);
    		Vector3 p3 = ShowPoint(3);
    		
    		Handles.color = Color.gray;
    		Handles.DrawLine(p0, p1);
    		Handles.DrawLine(p2, p3);
    		
    		ShowDirections();
    		Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
    	}
    
    	private void ShowDirections () {
    		Handles.color = Color.green;
    		Vector3 point = spline.GetPoint(0f);
    		Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale);
    		for (int i = 1; i <= lineSteps; i++) {
    			point = spline.GetPoint(i / (float)lineSteps);
    			Handles.DrawLine(point, point + spline.GetDirection(i / (float)lineSteps) * directionScale);
    		}
    	}
    
    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.points[index]);
    		EditorGUI.BeginChangeCheck();
    		point = Handles.DoPositionHandle(point, handleRotation);
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(spline, "Move Point");
    			EditorUtility.SetDirty(spline);
    			spline.points[index] = handleTransform.InverseTransformPoint(point);
    		}
    		return point;
    	}
    }
    A new spline type.

    Let’s add a method to BezierSpline to add another curve to the spline. Because we want the spline to be continuous, the last point of the previous curve is the same as the first point of the next curve. So each extra curve adds three more points.

    	public void AddCurve () {
    		Vector3 point = points[points.Length - 1];
    		Array.Resize(ref points, points.Length + 3);
    		point.x += 1f;
    		points[points.Length - 3] = point;
    		point.x += 1f;
    		points[points.Length - 2] = point;
    		point.x += 1f;
    		points[points.Length - 1] = point;
    	}

    We’re using the Array.Resize method to create a larger array to hold the new points. It’s inside the System namespace, so we should declare that we’re using it at the top of our script.

    using UnityEngine;
    using System;

    To actually be able to add a curve, we have to add a button to our spline’s inspector. We can customize the inspector that Unity uses for our component by overriding the OnInspectorGUI method of BezierSplineInspector. Note that this is not a special Unity method, it relies on inheritance.

    To keep drawing the default inspector, we call the DrawDefaultInspector method. Then we use GUILayout to draw a button, which when clicked adds a curve.

    	public override void OnInspectorGUI () {
    		DrawDefaultInspector();
    		spline = target as BezierSpline;
    		if (GUILayout.Button("Add Curve")) {
    			Undo.RecordObject(spline, "Add Curve");
    			spline.AddCurve();
    			EditorUtility.SetDirty(spline);
    		}
    	}
    Adding a curve.

    Of course we still only see the first curve. So we adjust BezierSplineInspector so it loops over all the curves.

    	private void OnSceneGUI () {
    		spline = target as BezierSpline;
    		handleTransform = spline.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    		
    		Vector3 p0 = ShowPoint(0);
    		for (int i = 1; i < spline.points.Length; i += 3) {
    			Vector3 p1 = ShowPoint(i);
    			Vector3 p2 = ShowPoint(i + 1);
    			Vector3 p3 = ShowPoint(i + 2);
    			
    			Handles.color = Color.gray;
    			Handles.DrawLine(p0, p1);
    			Handles.DrawLine(p2, p3);
    			
    			Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
    			p0 = p3;
    		}
    		ShowDirections();
    	}
    The whole spline.

    Now we can see all the curves, but the direction lines are only added to the first one. This is because BezierSpline‘s method also still only work with the first curve. It’s time to change that.

    To cover the entire spline with a t going from zero to one, we first need to figure out which curve we’re on. We can get the curve’s index by multiplying t by the number of curves and then discarding the fraction. Let’s add a CurveCount property to make that easy.

    	public int CurveCount {
    		get {
    			return (points.Length - 1) / 3;
    		}
    	}

    After that we can reduce t to just the fractional part to get the interpolation value for our curve. To get to the actual points, we have to multiply the curve index by three.

    However, this would fail when then original t equals one. In this case we can just set it to the last curve.

    	public Vector3 GetPoint (float t) {
    		int i;
    		if (t >= 1f) {
    			t = 1f;
    			i = points.Length - 4;
    		}
    		else {
    			t = Mathf.Clamp01(t) * CurveCount;
    			i = (int)t;
    			t -= i;
    			i *= 3;
    		}
    		return transform.TransformPoint(Bezier.GetPoint(
    			points[i], points[i + 1], points[i + 2], points[i + 3], t));
    	}
    	
    	public Vector3 GetVelocity (float t) {
    		int i;
    		if (t >= 1f) {
    			t = 1f;
    			i = points.Length - 4;
    		}
    		else {
    			t = Mathf.Clamp01(t) * CurveCount;
    			i = (int)t;
    			t -= i;
    			i *= 3;
    		}
    		return transform.TransformPoint(Bezier.GetFirstDerivative(
    			points[i], points[i + 1], points[i + 2], points[i + 3], t)) - transform.position;
    	}

    We now see direction lines across the entire spline, but we can improve the visualization by making sure that each curve segment gets the same amount of lines. Fortunately, it is easy to change BezierSplineInspector.ShowDirections so it uses BezierSpline.CurveCount to determine how many lines to draw.

    	private const int stepsPerCurve = 10;
    	
    	private void ShowDirections () {
    		Handles.color = Color.green;
    		Vector3 point = spline.GetPoint(0f);
    		Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale);
    		int steps = stepsPerCurve * spline.CurveCount;
    		for (int i = 1; i <= steps; i++) {
    			point = spline.GetPoint(i / (float)steps);
    			Handles.DrawLine(point, point + spline.GetDirection(i / (float)steps) * directionScale);
    		}
    	}
    Directions along the entire spline.

    It’s rather crowded with all those transform handles. We could only show a handle for the active point. Then then other points can suffice with dots.

    Let’s update ShowPoint so it shows a button instead of a position handle. This button will look like a white dot, which when clicked will turn into the active point. Then we only show the position handle if the point’s index matches the selected index, which we initialize at -1 so nothing is selected by default.

    	private const float handleSize = 0.04f;
    	private const float pickSize = 0.06f;
    	
    	private int selectedIndex = -1;
    	
    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.points[index]);
    		Handles.color = Color.white;
    		if (Handles.Button(point, handleRotation, handleSize, pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    		}
    		if (selectedIndex == index) {
    			EditorGUI.BeginChangeCheck();
    			point = Handles.DoPositionHandle(point, handleRotation);
    			if (EditorGUI.EndChangeCheck()) {
    				Undo.RecordObject(spline, "Move Point");
    				EditorUtility.SetDirty(spline);
    				spline.points[index] = handleTransform.InverseTransformPoint(point);
    			}
    		}
    		return point;
    	}
    Showing dots.

    This works, but it is tough to get a good size for the dots. Depending on the scale you’re working at, they could end up either too large or too small. It would be nice if we could keep the screen size of the dots fixed, just like the position handles always have the same screen size. We can do this by factoring in HandleUtility.GetHandleSize. This method gives us a fixed screen size for any point in world space.

    		float size = HandleUtility.GetHandleSize(point);
    		Handles.color = Color.white;
    		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    		}
    Dots with fixed size.

    Constraining Control Points

    Although our spline is continuous, it sharply changes direction in between curve sections. These sudden changes in direction and speed are possible because the shared control point between two curves has two different velocities associated with it, one for each curve.

    If we want the velocities to be equal, we must ensure that the two control points that define them – the third of the previous curve and the second of the next curve – mirror each other around the shared point. This ensures that the combined first and second derivatives are continuous.

    Alternatively, we could align them but let their distance from the shared point differ. That will result in an abrubt change in velocity, while still keeping the direction continuous. In this case the combined first derivative is continuous, but the second is not.

    The most flexible approach is to decide per curve boundary which contraints should apply, so we’ll do that. Of course, once we have these constraints we can’t just let anyone directly edit BezierSpline‘s points. So let’s make our array private and provide indirect access to it. Make sure to let Unity know that we still want to serialize our points, otherwise they won’t be saved.

    	[SerializeField]
    	private Vector3[] points;
    
    	public int ControlPointCount {
    		get {
    			return points.Length;
    		}
    	}
    
    	public Vector3 GetControlPoint (int index) {
    		return points[index];
    	}
    
    	public void SetControlPoint (int index, Vector3 point) {
    		points[index] = point;
    	}

    Now BezierSplineInspector must use the new methods and property instead of directly accessing the points array.

    	private void OnSceneGUI () {
    		spline = target as BezierSpline;
    		handleTransform = spline.transform;
    		handleRotation = Tools.pivotRotation == PivotRotation.Local ?
    			handleTransform.rotation : Quaternion.identity;
    		
    		Vector3 p0 = ShowPoint(0);
    		for (int i = 1; i < spline.ControlPointCount; i += 3) {
    			Vector3 p1 = ShowPoint(i);
    			Vector3 p2 = ShowPoint(i + 1);
    			Vector3 p3 = ShowPoint(i + 2);
    			
    			Handles.color = Color.gray;
    			Handles.DrawLine(p0, p1);
    			Handles.DrawLine(p2, p3);
    			
    			Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
    			p0 = p3;
    		}
    		ShowDirections();
    	}
    	
    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    		float size = HandleUtility.GetHandleSize(point);
    		Handles.color = Color.white;
    		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    		}
    		if (selectedIndex == index) {
    			EditorGUI.BeginChangeCheck();
    			point = Handles.DoPositionHandle(point, handleRotation);
    			if (EditorGUI.EndChangeCheck()) {
    				Undo.RecordObject(spline, "Move Point");
    				EditorUtility.SetDirty(spline);
    				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
    			}
    		}
    		return point;
    	}

    While we’re at it, we also no longer want to allow direct access to the array in the inspector, so remove the call to DrawDefaultInspector. To still allow changes via typing, let’s show a vector field for the selected point.

    	public override void OnInspectorGUI () {
    		spline = target as BezierSpline;
    		if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
    			DrawSelectedPointInspector();
    		}
    		if (GUILayout.Button("Add Curve")) {
    			Undo.RecordObject(spline, "Add Curve");
    			spline.AddCurve();
    			EditorUtility.SetDirty(spline);
    		}
    	}
    
    	private void DrawSelectedPointInspector() {
    		GUILayout.Label("Selected Point");
    		EditorGUI.BeginChangeCheck();
    		Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(spline, "Move Point");
    			EditorUtility.SetDirty(spline);
    			spline.SetControlPoint(selectedIndex, point);
    		}
    	}

    Unfortunately, it turns out that the inspector doesn’t refresh itself when we select a point in the scene view. We could fix this by calling SetDirty for the spline, but that’s not right because the spline didn’t change. Fortunately, we can issue a repaint request instead.

    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    		float size = HandleUtility.GetHandleSize(point);
    		Handles.color = Color.white;
    		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    			Repaint();
    		}
    		if (selectedIndex == index) {
    			EditorGUI.BeginChangeCheck();
    			point = Handles.DoPositionHandle(point, handleRotation);
    			if (EditorGUI.EndChangeCheck()) {
    				Undo.RecordObject(spline, "Move Point");
    				EditorUtility.SetDirty(spline);
    				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
    			}
    		}
    		return point;
    	}
    Selected point only.

    Let’s define an enumeration type to describe our three modes. Create a new script, remove the default code, and define an enum with the three options.

    public enum BezierControlPointMode {
    	Free,
    	Aligned,
    	Mirrored
    }

    Now we can add these modes to BezierSpline. We only need to store the mode in between curves, so let’s put them in an array with a length equal to the number of curves plus one. You’ll need to reset your spline or create a new one to make sure you have an array of the right size.

    	[SerializeField]
    	private BezierControlPointMode[] modes;
    	
    	public void AddCurve () {
    		Vector3 point = points[points.Length - 1];
    		Array.Resize(ref points, points.Length + 3);
    		point.x += 1f;
    		points[points.Length - 3] = point;
    		point.x += 1f;
    		points[points.Length - 2] = point;
    		point.x += 1f;
    		points[points.Length - 1] = point;
    
    		Array.Resize(ref modes, modes.Length + 1);
    		modes[modes.Length - 1] = modes[modes.Length - 2];
    	}
    	
    	public void Reset () {
    		points = new Vector3[] {
    			new Vector3(1f, 0f, 0f),
    			new Vector3(2f, 0f, 0f),
    			new Vector3(3f, 0f, 0f),
    			new Vector3(4f, 0f, 0f)
    		};
    		modes = new BezierControlPointMode[] {
    			BezierControlPointMode.Free,
    			BezierControlPointMode.Free
    		};
    	}

    While we store the modes in between curves, it is convenient if we could get and set modes per control point. So we need to convert a point index into a mode index because in reality points share modes. As an example, the point index sequence 0, 1, 2, 3, 4, 5, 6 corresponds to the mode index sequence 0, 0, 1, 1, 1, 2, 2. So we need to add one and then divide by three.

    	public BezierControlPointMode GetControlPointMode (int index) {
    		return modes[(index + 1) / 3];
    	}
    
    	public void SetControlPointMode (int index, BezierControlPointMode mode) {
    		modes[(index + 1) / 3] = mode;
    	}

    Now BezierSplineInspector can allow us to change the mode of the selected point. You will notice that changing the mode of one point also appears to change the mode of the points that are linked to it.

    	private void DrawSelectedPointInspector() {
    		GUILayout.Label("Selected Point");
    		EditorGUI.BeginChangeCheck();
    		Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(spline, "Move Point");
    			EditorUtility.SetDirty(spline);
    			spline.SetControlPoint(selectedIndex, point);
    		}
    		EditorGUI.BeginChangeCheck();
    		BezierControlPointMode mode = (BezierControlPointMode)
    			EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(spline, "Change Point Mode");
    			spline.SetControlPointMode(selectedIndex, mode);
    			EditorUtility.SetDirty(spline);
    		}
    	}
    Now with adjustable control point mode.

    It would be useful if we also got some visual feedback about our node types in the scene view. We can easily add this by coloring the dots. I’ll use white for free, yellow for aligned, and cyan for mirrored.

    	private static Color[] modeColors = {
    		Color.white,
    		Color.yellow,
    		Color.cyan
    	};
    	
    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    		float size = HandleUtility.GetHandleSize(point);
    		Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    			Repaint();
    		}
    		if (selectedIndex == index) {
    			EditorGUI.BeginChangeCheck();
    			point = Handles.DoPositionHandle(point, handleRotation);
    			if (EditorGUI.EndChangeCheck()) {
    				Undo.RecordObject(spline, "Move Point");
    				EditorUtility.SetDirty(spline);
    				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
    			}
    		}
    		return point;
    	}
    Now with color-coded points.

    So far we’re just coloring points. It’s time to enforce the constraints. We add a new method to BezierSpline to do so and call it when a point is moved or a mode is changed. It takes a point index and begins by retrieving the relevant mode.

    	public void SetControlPoint (int index, Vector3 point) {
    		points[index] = point;
    		EnforceMode(index);
    	}
    	
    	public void SetControlPointMode (int index, BezierControlPointMode mode) {
    		modes[(index + 1) / 3] = mode;
    		EnforceMode(index);
    	}
    
    	private void EnforceMode (int index) {
    		int modeIndex = (index + 1) / 3;
    	}

    We should check if we actually don’t have to enforce anything. This is the case when the mode is set to free, or when we’re at the end points of the curve. In these cases, we can return without doing anything.

    	private void EnforceMode (int index) {
    		int modeIndex = (index + 1) / 3;
    		BezierControlPointMode mode = modes[modeIndex];
    		if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
    			return;
    		}
    	}

    Now which point should we adjust? When we change a point’s mode, it is either a point in between curves or one of its neighbors. When we have the middle point selected, we can just keep the previous point fixed and enforce the constraints on the point on the opposite side. If we have one of the other points selected, we should keep that one fixed and adjust its opposite. That way our selected point always stays where it is. So let’s define the indices for these points.

    		if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
    			return;
    		}
    		
    		int middleIndex = modeIndex * 3;
    		int fixedIndex, enforcedIndex;
    		if (index <= middleIndex) {
    			fixedIndex = middleIndex - 1;
    			enforcedIndex = middleIndex + 1;
    		}
    		else {
    			fixedIndex = middleIndex + 1;
    			enforcedIndex = middleIndex - 1;
    		}

    Let’s consider the mirrored case first. To mirror around the middle point, we have to take the vector from the middle to the fixed point – which is (fixed – middle) – and invert it. This is the enforced tangent, and adding it to the middle gives us our enforced point.

    		if (index <= middleIndex) {
    			fixedIndex = middleIndex - 1;
    			enforcedIndex = middleIndex + 1;
    		}
    		else {
    			fixedIndex = middleIndex + 1;
    			enforcedIndex = middleIndex - 1;
    		}
    
    		Vector3 middle = points[middleIndex];
    		Vector3 enforcedTangent = middle - points[fixedIndex];
    		points[enforcedIndex] = middle + enforcedTangent;

    For the aligned mode, we also have to make sure that the new tangent has the same length as the old one. So we normalize it and then multiply by the distance between the middle and the old enforced point.

    		Vector3 enforcedTangent = middle - points[fixedIndex];
    		if (mode == BezierControlPointMode.Aligned) {
    			enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
    		}
    		points[enforcedIndex] = middle + enforcedTangent;
    Enforced constraints.

    From now on, whenever you move a point or change a point’s mode, the constraints will be enforced. But when moving a middle point, the previous point always stays fixed and the next point is always enforced. This might be fine, but it’s intuitive if both other points move along with the middle one. So let’s adjust SetControlPoint so it moves them together.

    	public void SetControlPoint (int index, Vector3 point) {
    		if (index % 3 == 0) {
    			Vector3 delta = point - points[index];
    			if (index > 0) {
    				points[index - 1] += delta;
    			}
    			if (index + 1 < points.Length) {
    				points[index + 1] += delta;
    			}
    		}
    		points[index] = point;
    		EnforceMode(index);
    	}

    To wrap things up, we should also make sure that the constraints are enforced when we add a curve. We can do this by simply calling EnforceMode at the point where the new curve was added.

    	public void AddCurve () {
    		Vector3 point = points[points.Length - 1];
    		Array.Resize(ref points, points.Length + 3);
    		point.x += 1f;
    		points[points.Length - 3] = point;
    		point.x += 1f;
    		points[points.Length - 2] = point;
    		point.x += 1f;
    		points[points.Length - 1] = point;
    
    		Array.Resize(ref modes, modes.Length + 1);
    		modes[modes.Length - 1] = modes[modes.Length - 2];
    		EnforceMode(points.Length - 4);
    	}

    There is yet another constraint that we could add. By enforcing that the first and last control points share the same position, we can turn our spline into a loop. Of course, we also have to take modes into consideration as well.

    So let’s add a loop property to BezierSpline. Whenever it is set to true, we make sure the modes of the end points match and we call SetPosition, trusting that it will take care of the position and mode constraints.

    	[SerializeField]
    	private bool loop;
    
    	public bool Loop {
    		get {
    			return loop;
    		}
    		set {
    			loop = value;
    			if (value == true) {
    				modes[modes.Length - 1] = modes[0];
    				SetControlPoint(0, points[0]);
    			}
    		}
    	}

    Now we can add the loop property to BezierSplineInspector.

    	public override void OnInspectorGUI () {
    		spline = target as BezierSpline;
    		EditorGUI.BeginChangeCheck();
    		bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);
    		if (EditorGUI.EndChangeCheck()) {
    			Undo.RecordObject(spline, "Toggle Loop");
    			EditorUtility.SetDirty(spline);
    			spline.Loop = loop;
    		}
    		if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
    			DrawSelectedPointInspector();
    		}
    		if (GUILayout.Button("Add Curve")) {
    			Undo.RecordObject(spline, "Add Curve");
    			spline.AddCurve();
    			EditorUtility.SetDirty(spline);
    		}
    	}
    Optional loop.

    To correctly enforce the loop, we need to make a few more changes to BezierSpline.

    First, SetControlPointMode needs to make sure that the first and last mode remain equal in case of a loop.

    	public void SetControlPointMode (int index, BezierControlPointMode mode) {
    		int modeIndex = (index + 1) / 3;
    		modes[modeIndex] = mode;
    		if (loop) {
    			if (modeIndex == 0) {
    				modes[modes.Length - 1] = mode;
    			}
    			else if (modeIndex == modes.Length - 1) {
    				modes[0] = mode;
    			}
    		}
    		EnforceMode(index);
    	}

    Next, SetControlPoint needs different edge cases when dealing with a loop, because it needs to wrap around the points array.

    	public void SetControlPoint (int index, Vector3 point) {
    		if (index % 3 == 0) {
    			Vector3 delta = point - points[index];
    			if (loop) {
    				if (index == 0) {
    					points[1] += delta;
    					points[points.Length - 2] += delta;
    					points[points.Length - 1] = point;
    				}
    				else if (index == points.Length - 1) {
    					points[0] = point;
    					points[1] += delta;
    					points[index - 1] += delta;
    				}
    				else {
    					points[index - 1] += delta;
    					points[index + 1] += delta;
    				}
    			}
    			else {
    				if (index > 0) {
    					points[index - 1] += delta;
    				}
    				if (index + 1 < points.Length) {
    					points[index + 1] += delta;
    				}
    			}
    		}
    		points[index] = point;
    		EnforceMode(index);
    	}

    Next, EnforceMode can now only bail at the end points when not looping. It also has to check whether the fixed or enforced point wraps around the array.

    	private void EnforceMode (int index) {
    		int modeIndex = (index + 1) / 3;
    		BezierControlPointMode mode = modes[modeIndex];
    		if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) {
    			return;
    		}
    
    		int middleIndex = modeIndex * 3;
    		int fixedIndex, enforcedIndex;
    		if (index <= middleIndex) {
    			fixedIndex = middleIndex - 1;
    			if (fixedIndex < 0) {
    				fixedIndex = points.Length - 2;
    			}
    			enforcedIndex = middleIndex + 1;
    			if (enforcedIndex >= points.Length) {
    				enforcedIndex = 1;
    			}
    		}
    		else {
    			fixedIndex = middleIndex + 1;
    			if (fixedIndex >= points.Length) {
    				fixedIndex = 1;
    			}
    			enforcedIndex = middleIndex - 1;
    			if (enforcedIndex < 0) {
    				enforcedIndex = points.Length - 2;
    			}
    		}
    
    		Vector3 middle = points[middleIndex];
    		Vector3 enforcedTangent = middle - points[fixedIndex];
    		if (mode == BezierControlPointMode.Aligned) {
    			enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
    		}
    		points[enforcedIndex] = middle + enforcedTangent;
    	}

    And finally, we also have to take looping into account when adding a curve to the spline. The result might be a tangle, but it will remain a proper loop.

    	public void AddCurve () {
    		Vector3 point = points[points.Length - 1];
    		Array.Resize(ref points, points.Length + 3);
    		point.x += 1f;
    		points[points.Length - 3] = point;
    		point.x += 1f;
    		points[points.Length - 2] = point;
    		point.x += 1f;
    		points[points.Length - 1] = point;
    
    		Array.Resize(ref modes, modes.Length + 1);
    		modes[modes.Length - 1] = modes[modes.Length - 2];
    		EnforceMode(points.Length - 4);
    
    		if (loop) {
    			points[points.Length - 1] = points[0];
    			modes[modes.Length - 1] = modes[0];
    			EnforceMode(0);
    		}
    	}
    A spline loop.

    It is great that we have loops, but it is inconvenient that we can no longer see where the spline begins. We can make this obvious by letting BezierSplineInspector always double the size of the dot for the first point.

    Note that in case of a loop the last point will be drawn on top of it, so if you clicked the middle of the big dot you’d select the last point, while if you clicked further from the center you’d get the first point.

    	private Vector3 ShowPoint (int index) {
    		Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    		float size = HandleUtility.GetHandleSize(point);
    		if (index == 0) {
    			size *= 2f;
    		}
    		Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    		if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
    			selectedIndex = index;
    			Repaint();
    		}
    		if (selectedIndex == index) {
    			EditorGUI.BeginChangeCheck();
    			point = Handles.DoPositionHandle(point, handleRotation);
    			if (EditorGUI.EndChangeCheck()) {
    				Undo.RecordObject(spline, "Move Point");
    				EditorUtility.SetDirty(spline);
    				spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
    			}
    		}
    		return point;
    	}
    We start big.

    Using Splines

    We have been working with splines for a while now, but we haven’t used them for anything yet. There are uncountable things you can do with splines, for example moving an object alongs its path. Let’s create a SplineWalker component that does just that.

    using UnityEngine;
    
    public class SplineWalker : MonoBehaviour {
    
    	public BezierSpline spline;
    
    	public float duration;
    
    	private float progress;
    
    	private void Update () {
    		progress += Time.deltaTime / duration;
    		if (progress > 1f) {
    			progress = 1f;
    		}
    		transform.localPosition = spline.GetPoint(progress);
    	}
    }

    Now we can create a walker object, assign our spline, set a duration, and watch it move after we enter play mode. I simply used a cube and gave it smaller cubes to resemble eyes, so you can see in what direction it’s looking.

    walker
    Walking the spline.

    The walker now walks, but it’s not looking in the direction that it’s going. We can add an option for that.

    	public bool lookForward;
    
    	private void Update () {
    		progress += Time.deltaTime / duration;
    		if (progress > 1f) {
    			progress = 1f;
    		}
    		Vector3 position = spline.GetPoint(progress);
    		transform.localPosition = position;
    		if (lookForward) {
    			transform.LookAt(position + spline.GetDirection(progress));
    		}
    	}
    looking forward
    Looking where you go.

    Another option is to keep looping the splines, instead of walking it just once. While we’re at it, we could also make the walker move back and forth, ping-ponging across the spline. Let’s create an enumeration to select between these modes.

    public enum SplineWalkerMode {
    	Once,
    	Loop,
    	PingPong
    }

    Now SplineWalker has to remember whether it’s going forward or backward. It also needs to adjust the progress when passing the spline ends depending on its mode.

    	public SplineWalkerMode mode;
    
    	private bool goingForward = true;
    
    	private void Update () {
    		if (goingForward) {
    			progress += Time.deltaTime / duration;
    			if (progress > 1f) {
    				if (mode == SplineWalkerMode.Once) {
    					progress = 1f;
    				}
    				else if (mode == SplineWalkerMode.Loop) {
    					progress -= 1f;
    				}
    				else {
    					progress = 2f - progress;
    					goingForward = false;
    				}
    			}
    		}
    		else {
    			progress -= Time.deltaTime / duration;
    			if (progress < 0f) {
    				progress = -progress;
    				goingForward = true;
    			}
    		}
    
    		Vector3 position = spline.GetPoint(progress);
    		transform.localPosition = position;
    		if (lookForward) {
    			transform.LookAt(position + spline.GetDirection(progress));
    		}
    	}
    Walking in different ways.

    Another thing we could do is create a decorator that instantiates a sequence of items along a spline when it awakens. We also give it a forward-looking option, which applies to the items it spawns. Adding a frequency option to the item sequence allows for repetition. Of course, if either the frequency is zero or there are no items, we do nothing.

    We need some items, so create a few prefabs for that purpose as well.

    using UnityEngine;
    
    public class SplineDecorator : MonoBehaviour {
    
    	public BezierSpline spline;
    
    	public int frequency;
    
    	public bool lookForward;
    
    	public Transform[] items;
    
    	private void Awake () {
    		if (frequency <= 0 || items == null || items.Length == 0) {
    			return;
    		}
    		float stepSize = 1f / (frequency * items.Length);
    		for (int p = 0, f = 0; f < frequency; f++) {
    			for (int i = 0; i < items.Length; i++, p++) {
    				Transform item = Instantiate(items[i]) as Transform;
    				Vector3 position = spline.GetPoint(p * stepSize);
    				item.transform.localPosition = position;
    				if (lookForward) {
    					item.transform.LookAt(position + spline.GetDirection(p * stepSize));
    				}
    				item.transform.parent = transform;
    			}
    		}
    	}
    }
    decorator and prefabs
    Decorating splines.

    This works well for loops, but it doesn’t go all the way to the end of splines that aren’t loops. We can fix this by increasing our step size to cover the entire length of the spline, as long as it’s not a loop and we have more than one item to place.

    		if (frequency <= 0 || items == null || items.Length == 0) {
    			return;
    		}
    		float stepSize = frequency * items.Length;
    		if (spline.Loop || stepSize == 1) {
    			stepSize = 1f / stepSize;
    		}
    		else {
    			stepSize = 1f / (stepSize - 1);
    		}
    Going all the way.

    There are many more ways to use splines, and there’s also more features to add to the splines themselves. Like removing curves, or splitting a curve into two smaller ones, or merging two curves together. There are also other spline types to explore, like Centripetal Catmull-Rom or NURB. If you’re comfortable with Beziér, you should be able to handle those as well. So the tutorial ends here, enjoy walking your own path!

    Enjoyed the tutorial? Help me make more by becoming a patron!

    Downloads

    curves-and-splines-01.unitypackageThe project after Lines.curves-and-splines-02.unitypackageThe project after Curves.curves-and-splines-03.unitypackageThe project after Splines.curves-and-splines-04.unitypackageThe project after Constraining Control Points.curves-and-splines-finished.unitypackageThe finished project.

  • A Look Behind the Scenes of the Creation of ‘Old Man’s Journey’

    The long and winding, ahem, journey of Old Man’s Journey, which we’ve been following for the past year, finally comes to an end when the game hits the App Store this Thursday. This story-driven adventure has you traveling across beautifully-illustrated lands interacting with the environment and recalling the memories of the old man protagonist’s life. While its main purpose is telling an emotional tale about reflection and redemption, the gameplay in Old Man’s Journey involves an interesting mechanic that lets you literally move mountains as you manipulate the elevations in various depths of the environment to make a clear path forward for the old man. In case you aren’t familiar with Old Man’s Journey at all, check out the gameplay trailer below to see it in action.

    That unique landscape moving mechanic really caught my attention when I was first shown a very early version of the game during GDC 2016, and with Old Man’s Journey just about to launch developer Broken Rules has sent along a ton of neat behind the scenes information about the development and evolution of the game. First off is some footage of the first playable prototype of the landscape moving mechanic back when it was just a neat idea with pretty much no visuals or story to go along with it. Despite how crude it may look in this video, it actually looks quite similar to the finished product mechanically and it’s neat to see such a unique idea being realized.

    Next they had to determine what sort of art style to use to build around that cool mechanic they’d developed. Here are three different looks at some concept art of the same level, one in sketchbook style, then the same scene colored in, and finally that same scene again done in a more smoothed out and stylized look.

    The final art style ended up not being exactly like any of these options, but it probably most closely resembles the second style. But another piece of concept art that was sent along shows an additional scene in the smoothed out, ’90s-style 3D graphics of that third example above, and could you imagine if this is how the entirety of Old Man’s Journey looked?

    It has a certain charm, but I’m sure glad they went with the more illustrated look in the end. One other interesting idea they were considering for the game was doing actual 3D visuals for things like the buildings and even the vehicles. This would have given the game sort of a 2.5D look, which could have been quite cool but also still doesn’t seem like as good a fit as the final style they went with.

    It definitely would have been neat to have those 3D elements giving depth to the landscapes, but I also think it would have taken away from the storybook vibe of the game. Talking about depth though brings me to my next set of cool behind the scenes stuff. The landscapes in Old Man’s Journey are made up of many layers which can give a feeling of depth by using parallax scrolling and also is pretty important for the landscape-sliding mechanics. Well, when you zoom out a bit from a finished level and view it in the team’s level editor, you can get a great sense of just how many layers there are that make up each environment. Check these out.

    I especially love that top one where the view zooms out and reveals just how tall and complex the level really is. Next up is several looks at how a level evolves from concept images into a final product. In this example it’s the watermill level which starts out as a sketch, then has the old man’s movement points roughed in, then is painted out before having finishing touches like shadows and details added into the final image. The images are short and wide so feel free to click them for a larger view.

    Here’s a similar look at the hotel level as it goes from sketch, to painted, to detailed, to gorgeous final product. As with the images above, these images are somewhat tiny so feel free to click on any of them to get the larger version. Also, I didn’t include this because it was a bit awkward to actually embed in this post due to its dimensions, but if you want to see this process animated together then check out this gif.

    This next example was a bit of a friendlier size for embedding, so I’ve gone ahead and embedded it below. It’s a really cool look at how all the elements comes together, so check out this animation of the evolution of the level titled The Dive.

    Well, that about wraps it up for our little journey (no I do not get tired of using that joke) into the creation of Old Man’s Journey. I absolutely love this sort of behind the scenes stuff about how a game is made, and especially being able to see very early concept stuff and then be able to compare it to the super-polished finished products. I hope you also enjoyed peeking behind the curtain of this game, and hopefully plenty of people will be picking up Old Man’s Journey when it launches this Thursday and will have an even greater appreciation into the kind of work and techniques that went into its creation.

  • 타일맵 길찾기.

    원래 블로그에는 평소에 공부했던 걸 저장하고 나중에 찾아서 참고하는 용도로 쓰고 있었는데, 최근 바빠서 그럴 시간이 없었다. 특히 NDC2016 발표를 준비하는 시간은 정말 지옥같았기 때문에(결과값이.. 결과값이 나와야 했다), 발표를 준비하면서 공부했던 내용, 그리고 발표 보충 자료를 올린다고 말해놓고 올리지 않은 채 반 년이 지나버렸다. 최근에 타일 맵을 다시 쓸 일이 생겨서 예전 자료를 참조하다가 NDC2016 발표에 썼던 기법이 기억이 나지 않는 걸 깨닫고, 시간이 나는 김에 하나씩 정리해 놓아야겠다고 생각했다.

    타일 맵은 컴퓨터 게임의 초창기부터 지금까지 쓰여온 견고한 기법이다. 보통 균일한 이미지 조각인 타일(Tile) 을 이어붙여서 맵의 형태를 만든다. 보통 이렇게 만든 맵은 보기에 자연스럽고 타일 간의 이음새가 크게 눈에 띄지 않는 것이 좋다. 이렇게 만든 맵은 보통 주인공 캐릭터나 다른 오브젝트들의 활동 무대로 쓰인다.

    타일 맵은 RPG Maker 같은 툴에서도 쉽게 찾아볼 수 있다. 타일을 선택하고 색칠하듯 마우스로 드래그하면 맵이 생성된다. 이렇게 생성된 맵은 자연스럽게 보인다. 즉 타일과 타일 간의 이음새가 분리되는 느낌이 아니라 부드럽게 연결된다. 이렇게 만들려면 어떻게 해야 할까?

    링크

    애초에 이 고민을 했던 것은 고전 게임인 <삼국지 영걸전>의 맵 파일을 누군가 뜯어놓은 것을 살펴볼 때였다. 맵 조각은 아래처럼 구성되어 있었다.

    그에 비해 우리가 보통 알고 있는 맵은 아래와 같다.

    이미지를 보면 금방 둘의 연관성을 알 수 있다. 실제의 맵을 구성하는 타일 조각의 최소 단위는 보통 플레이어가 “한 칸”이라고 인지하는 유닛이나 성채, 보물창고의 크기에 비해 가로, 세로가 각각 1/2 작은, 1/4 크기였다. 영걸전에서는 한 칸이 32 x 32 픽셀, 타일 하나는 16 x 16 픽셀이다.

    예를 들어 위의 맵은 아래처럼 하얀 네모가 하나의 이미지 조각(타일)로, 그것이 모여서 우리가 인지할 수 있는 맵의 형태가 된다.

    영걸전에서는 유닛이 이보다 가로 세로 2배 큰 단위로 배치된다. 물론 게임에 따라 맵 타일과 유닛 타일이 같은 단위일수도 있다.

    이음새가 자연스럽게 보이기 위해서는 우리가 한 칸의 타일이라고 인식하는 것의 절반 정도의 스케일로 타일을 준비해야 한다. 이 페이지에 따르면 보통 RPG에서 많이 사용되는 타일 종류를 2-corner 타일이라고 부른다. 타일의 corner 부분이 변하기 때문이다. 땅과 물처럼 대비되는 두 가지의 지형이 자연스럽게 이어지도록 하는 것이다.

    링크

    영걸전에서는 대각선을 연결할 필요가 없었는지 아래 그림에서 빨간 네모로 표시한 이런 타일들은 쓰이지 않았다.

    대신에 표현을 풍부하게 하기 위해서인지 연결 타일 일부가 2가지로 쓰였다(이음새와 면적이 차지하는 부분에 차이 있음).
    영걸전의 초원 – 물 연결 부분을 정의하는 2-corner 타일은 아래와 같다.

    다음 글에서는 내가 RPG 게임의 타일 맵을 어떻게 만들었는지를 보여줄 예정이다.출처:NDC2016 보충자료 – 1. 타일 맵 구현 기본

    읽어볼만한 좋은 글
    ——————
    1. http://chulin28ho.egloos.com/5097822 – 워크래프트3에서 사용된 타일 구조 연구

    참고자료
    ——–
    2. http://www.codeproject.com/Articles/106884/Implementing-Auto-tiling-Functionality-in-a-Tile-M – 2D 타일 맵의 구현 설명

    3. http://s358455341.websitehome.co.uk/stagecast/wang/2corn.html – 보통 RPG 맵을 만들 때 사용하는 2-corner 타일에 대한 설명신고출처:NDC2016 보충자료 – 1. 타일 맵 구현 기본

    지난 글에 이에 이번에는 실제 NDC2016 발표에서 타일 맵을 어떻게 구현했는지에 대해서 살펴보겠다. 타일 리소스는 게임업계의 저명한 인사인 Daniel Cook 이 무료로 공개한 타일셋을 가져왔다. 게임에 사용하는 무료 리소스가 올라오는 opengameart.org 에는 이 타일셋을 32 x 32 크기로 수정한 버전이 있는데 이것도 참고했다.

    일단 내가 사용할 타일셋을 만들었는데 아래 그림과 같다. 가운데에 있는 벽돌은 이 예제에 사용한 유닛들과 느낌이 어울리지 않아서 결국 사용하지 않았기 때문에 기본 타일 종류는 풀(초원), 길, 물의 3종류다.

    풀과 길은 이전 글에서 설명한 2-corner 타일로 서로 경계면이 부드럽게 연결되는 타일 조각들로 이루어져 있다. 하지만 물은 물과 다른 영역의 경계가 투명하게 처리되어 있는데, 이는 먼저 풀과 길을 정리한 다음에 물을 그 위에 덮어씌우기 위해서다. 풀이든 길이든 물을 그 위에 얹으면 자연스럽게 보인다. 3가지 타일이 만나는 경우의 수를 다 계산하는 것보다 이쪽이 간단하고 편하다.

    간단히 생성해 본 맵은 아래와 같다.

    길을 만드는 과정은 복잡하기 때문에 다음 글에 설명하기로 하고, 여기서는 물을 얹는 방법부터 설명하려고 한다. 맵의 어떤 장소에 물을 배치하기로 결정하면, 상하좌우, 좌상, 좌하, 우상, 우하의 8개 셀에 대해서 랜덤함수를 돌려서 75%의 확률로 물을 배치한다.

    물을 배치한 후에는 Cellular Automata 기법을 이용해서 각 셀의 상하좌우 4방향 이웃을 체크, 물의 모양을 자연스럽게 만들어준다.
    물 타일이 아닐 경우 상하좌우 이웃에 물 타일이 2개 이상 있으면 물 타일이 된다.

    그 다음에는 물의 클러스터가 충분히 가깝지만 이어져 있지 않아서 어색해보이는 경우를 방지하기 위해서 1타일 떨어진 물의 경우 합쳐주는 작업을 한다.

    이제 이 맵을 타일맵으로 바꿔줄 때가 왔다. 영역간의 연결을 생각하지 않는다면 아래와 같은 타일맵도 가능할 것이다.

    하지만 연결을 생각하면 계산은 조금 복잡해진다. 아까의 타일셋을 살펴보면 각 타일은 4개의 corner에 물타일이 있는 경우와 그렇지 않은 경우로 나눌 수 있다(물 = w, 다른 타일 = o 로 표시).

    맵의 타일 결정 방법은, 먼저 물 타일 각각에 대해서 8방향에 어떤 타일이 있는지 검사한다. 그리고 물 타일이 아니면 가중치를 둔다.

    타일이 되는 빨간 타일을 확대해서 살펴보면 4개의 corner로 나눌 수 있는데, 어느 한쪽에서 영향을 받으면 인접한 영역에 1을 더해주게 된다.

    예를 들어서 왼쪽 상단에 물 타일이 아닌 것이 있으면 왼쪽 위 코너에 1이 더해지고, 오른쪽에 물 타일이 아닌 것이 있으면 오른쪽 상단, 오른쪽 하단 코너에 1을 더한다.

    이렇게 계산하면 위 맵의 타일에는 아래처럼 점수가 쌓이고, 그 중 1 이상인 값만 o(other) 타일로 치환하면 알맞은 타일을 찾을 수 있다. 4개의 corner가 모두 o 타일일 때는 이미 물 타일이 아니라고 판단하고 그리지 않는다.

    최종적으로는 아래처럼 타일을 찾게 된다. 위와 비교하면 꽤 큰 차이다.

    이렇게 타일맵의 실제 구현에 대한 내용을 살펴보았다. 다음 글에서는 위에서 약간 두리뭉실하게 넘어갔던 “어떤 장소에 물을 배치할지” 결정하는 문제에 대한 내용, 즉 맵의 노드와 경로를 정하는 기법에 대해 다룰 예정이다.출처:NDC2016 보충자료 – 2. 타일 맵 구현 실제

    이번에는 지난 NDC2016 발표의 “맵 생성 절차”(59~69p.)에 대해서 좀 더 자세한 글을 써보려고 한다. 59페이지를 그대로 가져와보면 다음과 같다.

    이 단계들에 대해 차례대로 설명하려고 한다.

    1. Poisson Disc Sampling

    맵에서 비교적 균일한 간격으로 노드를 선택하기 위한 방법이다. 완전한 랜덤이 아닌 이 방법으로 선택한 노드는 보기에도 좋고 활용도 편리하다. 하나하나의 노드가 도시나 별이라고 하면 서로 너무 멀거나 너무 가깝지 않은 적당한 거리를 유지할 수 있기 때문에 자연스럽기도 하다. 아래 그림에서 가장 오른쪽이 Poisson Disc Sampling 으로 생성한 노드들이다.

    이미지 링크

    Disc Sampling 이라는 이름대로 이 방법은 대기열에 속한 점(=노드) 주변에 있는 원형(Disc)의 영역에 다른 점을 추가할 수 있는지 검사한다.

    새로 추가하는 점 주변에 다른 이웃이 없다면 그 점을 추가하고, 그 점을 대기열에 넣는다.
    미리 정해진 tryCount 만큼 탐색하여 점을 여러 개 추가한다. 나는 tryCount를 30으로 설정했다.

    tryCount 가 끝나면 대기열에 있던 점을 빼고 새로 추가된 점 중 하나를 대기열에 집어넣고 다음 탐색을 반복한다. 이때 주변에 다른 이웃이 없는지 체크해서 이웃이 있다면 그 점은 추가될 수 없다.

    탐색을 빠르게 하기 위해 전체 맵을 그리드로 나누고, 그리드 안에는 하나의 점만 들어갈 수 있도록 제한한다. 따라서 원이 겹치는 영역에 있는 모든 점을 검사할 필요 없이, 원이 걸치는 그리드에 있는 점들만 검사하면 되기 때문에 탐색 시간이 줄어든다.

    이해를 돕기 위해 예전에 만들었던 Poisson Disc Sampling 예제 링크를 올린다. 예제에서는 1프레임당 1개의 점을 탐색하도록 했기 때문에 점이 천천히 검색되는 것이지, 실제 검색 속도는 빠르다.

    http://wonderfl.net/c/MMGa

    2. Minimal Spanning Tree

    트리란 두 개의 노드가 하나의 간선(edge)으로만 연결된 그래프를 말한다.

    신장 트리(Spanning Tree)는 사이클(루프)가 없이 모든 노드를 연결하는 트리이다. 최소 신장 트리(Minimal Spanning Tree)는 이 중 모든 노드를 연결하면서 경로의 합이 가장 짧은 신장 트리를 말한다.

    위에서 만든 노드들을 연결해서 그래프를 만드는데, 복잡한 계산을 줄이기 위해 일정 길이 이하의 간선만 남긴다. 그리고 여기서 최소 신장 트리를 찾는다.

    3. 지형 배치

    이렇게 최소 신장 트리를 찾은 다음에는 간선 연결이 하나뿐인 터미널 노드(terminal node)를 찾는데, 이곳에 물과 절벽을 배치한다. 터미널 노드에는 물이나 절벽 중 하나를 배치한다.

    아래 그림에서 검은색으로 표시된 노드가 터미널 노드이다. 두번째 글에서 설명했던 Cellular Automata 기법으로 인접한 물, 인접한 절벽끼리는 서로 합쳐진 것을 확인할 수 있다.

    이제 각 타일이 어느 지형에 속하는지 결정을 해야 한다. 어떤 타일은 풀이 먼저 그려진 다음 위에 물이 그려지고, 또 절벽까지 겹쳐져 있다. 일단 절벽(검은색)을 최우선으로 찾은 다음 물(파란색)을 찾고, 마지막으로 절벽과 물에 속하지 않은 곳을 이동 가능한 타일(하얀색)로 지정한다.

    또한 경로에는 랜덤하게 길 타일을 배치해서 사실감을 높인다. 길 타일 역시 Cellular Automata 기법으로 너무 뚜렷하지 않게 자연스러운 모양을 유지하도록 했다.

    4. 다시 Poisson Disc Sampling – 메인 경로 추출

    지금까지의 작업이 그럴듯해 보이는 지형을 만들기 위한 것이었다면 이번 단계는 유닛들이 이동할 수 있는 메인 경로를 만드는 작업이다. 다시 Poisson Disc Sampling 을 이동 가능한 타일(하얀색)에 대해서만 수행한다. 그리고 경로는 이동 가능한 타일을 지나는 것만 인정하고, 중간에 절벽이나 물과 겹치는 경로는 제외한다.

    5. 메인 노드 정하기 & 유닛 배치

    그럼 이제 1:1 대결을 상정한 맵이기 때문에 각 팀의 유닛을 배치할 메인 노드를 결정한다. 메인 노드 2개는 서로 연결되어야 하고, 그 거리는 맵의 가로 길이 + 맵의 세로 길이의 40% 에서 100% 사이의 값이 되어야 한다. 적당한 메인 노드 2개를 발견하지 못했을 경우 맵 전체를 다시 생성한다.

    메인 노드에는 각 팀이 지켜야 할 타워를 생성하고 그 주위에 유닛을 배치한다. 이제 양 팀은 서로의 타워를 파괴하기 위해 서로 대결을 펼치게 된다. 참고로 타워가 구석에 있을수록 유리한데, 보병과 기병은 인접해야만 타워에 공격이 가능하기 때문이다. 이 맵에서 Blue Team의 승률은 300번의 전투 시뮬레이션을 돌렸을 때 85.0%가 나왔다.

    약간 길었지만 발표 중에 가장 정리하고 싶었던 부분인 맵 생성 절차를 정리해 보았다. 다음에는 시간이 된다면 딥러닝 실행 부분을 정리해보려고 한다. 양이 많아서 아마 글 하나로는 안될 것 같다.출처:NDC2016 보충자료 – 3. 맵 생성 절차