Creating a Spline Tool

Checked with version: 2018.1

-

Difficulty: Intermediate

How SerializedProperty and SerializedObject instances are used to manipulate components

How to implement a custom Inspector GUI

How to respond to, intercept and use GUI events

How to query internal Editor state to customise tool behaviour

Creating the Component

If we create an interface which specifies the API of our spline tool, we can use this interface instead of a concrete class, which allows us to switch between implementations, and integrate with any other systems which might arrive in the future, as long as they also use the interface.

This interface specification contains the general methods which are applicable across most spline algorithms. It contains methods for creating and adjusting the spline, and methods for querying the spline for different information.

The Data

We need some fields to store that data used by our Interpolate function. The closed field specifies if the spline should form a closed loop or not, the points list will contain our control points which specify the shape of the spline, and finally the length is a nullable float where we can store the length of the spline once it has been calculated? Why nullable? You will find out soon!

How to get Uniform points along a spline? (SplineIndex.cs)

If we look at the interface documentation, you will notice that almost all the query methods are expected to return a uniform position along the spline. This is not straightforward, as our spline is composed of arbitrary control points which could be any distance from each other. In addition to this, the nature of our interpolation algorithm means we cannot simply store the distance between control points and use that to modify the t parameter.

Therefore, we create an index of discrete, uniform positions along the spline. This index is then used to provide the uniform positions assumed by the interface.

Add lazy indexing to Spline

The index we have created is expensive to create, and takes (relatively speaking) quite a lot of memory. If the user does not need this index, we should avoid creating it. This is achieved by using a private property which will only create an index when required, then re-use that index. We also provide a method to reset the index, so that the index will be rebuilt when control points or other parameters are changed.

The index now allows us to add a body to the GetPoint method required by the interface, and return a uniform position along the spline.

For the same reasons we need to construct an index, we also need to iterate along the spline to get an estimate of the total length. The step paramter controls how accurate the estimate will be. It defaults to 0.001f, which is acceptable for most cases.

The FindClosest method returns the approximate closest position on the spline to a world point. Due to the nature of splines, this solution cannot be analytical and we must create a numerical solution to solve the problem. The spline is divided into 1024 points and we choose the closest by comparing square of the distance to the world point.

Add editor helper methods

The editor provides the Reset method, which is used to set default values on the component when it is first added to a gameobject. Add 4 default points as that is the minimum required for our spline implementation.

Code snippet

OnValidate is called by the editor whenever values on the component have been changed. If we have an active index on our component, we reindex the spline so that the index will be built on the changed values.

Code snippet

Creating the Editor

The SplineComponent works nicely, but to use it effectively inside the Unity Editor, we are going to need to make it much more user friendly.

A Custom Inspector (Editor/SplineComponentEditor.cs)

The first step is a custom inspector. This is created inside an Editor class via the OnInspectorGUI method. The method below sets up widgets for the component fields, and adds some buttons for some useful utility methods we will create later.

Draw Gizmos

Gizmos are the visual inside the scene view that helps us identify the component, especially since it has no renderable geometry. There are 3 functions, the main drawing function (DrawGizmo) and 2 other functions which have the DrawGizmo attribute. This allows us to draw a high resolution gizmo when the spline component is selected in the hierarchy, and a low resolution gizmo at other times.

Scene View Controls

You will notice that we didn’t create inspector fields for the spline control points. That is because we are going to manage the control points through the scene view.

These two fields store the index of the currently selected control point, and if we choose to remove a control point, we store the index of that control point too. Why? Stay tuned, this will be answered below.

Code snippet

int hotIndex = -1;
int removeIndex = -1;

The OnSceneGUI method allows us to draw widgets inside the scene view when the component is selected in the hierarchy. If the mouse cursor is not over the scene view, we early exit the method to avoid the potentially expensive drawing which can really slow down the Editor when in play mode.

If the user is holding down the shift key, we perform some special visualisation as we are going to use shift + left click events to add control points.

Loop over the serialized property

When modifying control points, a SerializedProperty is used instead of directly modifying the points list, or using the appropriate methods on the component. This is done so that Undo/Redo functionality is automatically applied to the entire point list, including position value. To use the control point in the scene view, it must be converted into world space using the TransformPoint method.

Allow selection of control points

How does the user select which control point to edit? The Handles.Button method works just like a regular IMGUI Button method, however it allows us to use a sphere as the button visual instead of a GUI button. This is perfect for visualising and selecting points in the scene view. We use the GetHandleSize method so that the button-spheres are drawn at a consistent size across the scene, regardless of the camera position.

Code snippet

Perform deletion last

Remember the removeIndex field we created? This is where we use the value of that field to remove a control point. This happens right at the end of the OnSceneGUI method, so that next time the method is called it will have a correct list of control points. It also avoids modifying the list of points during other method calls, which can cause problems when iterating over the changed list.

Code snippet

Remember to set removeIndex to -1, otherwise we will delete a point every frame!

Also, to persist the changes we must must call ApplyModifiedProperties.

Code snippet

removeIndex = -1;
serializedObject.ApplyModifiedProperties();
}

Intercept and Handle Keyboard Commands

This is the method mentioned previously for handling commands which are intended for the hot control point. The first command is ‘FrameSelected’, which occurs when you press the F key in the scene view. We intercept the command here, so that instead of framing the game object which the spline component is attached to, we frame the hot control point.

The second command catches the Backspace keypress, allowing the hot control point to be scheduled for deletion, by assign it’s index to the removeIndex field.

Allow adding and inserting control points

These are the two functions which are called from OnSceneGUI when the user has the shift key pressed. They have slightly different behaviour depending on whether the spline is closed or open, so for clarity this is split into two different methods.

Both methods have similar functionality. They draw a line from the mouse cursor to the intersection point on the spline where the new control point will be inserted. In the case of an open spline, they also show a line when extending the spline from one of the end points.

They then check for the left click of the mouse button and if clicked use the SerializedProperty API to insert an item into the list of points, and then set it’s value to the new control point position.

As both methods have the common function of searching for a closest point, this function is split out into a separate method.

Add Utility Methods

The final task is to create the utility methods which are called by the custom inspector buttons. The first method flattens the y position of all the control points. The second repositions all the control points, so that the GameObjects’s transform is at the center of all the control points.

Oops...

"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.

Got it

We use cookies to ensure that we give you the best experience on our website. Click here for more information.