Sprite Shape gives you the freedom to create rich free-form 2D environments straight in Unity and decorate them as you see fit, with a visual and intuitive workflow. The tool works by dynamically tiling sprites along spline paths based on a given set of angle ranges. Read on to learn how you can work with Sprite Shape, get colliders for your shapes, and use a little bit of scripting to build on top of the features’ functionality.

Sprite Shape is still in preview, with a production release planned at a later date. That means that the workflow and the coding API described in this article may change in the future. However, that doesn’t stop you from trying it now!

Installing Sprite Shape

If you’re using Unity 2018.1 or newer, you can get Sprite Shape for your project by using the Package Manager.

Go to Window > Package Manager, then select the All tab. From there, you’ll be able to find the 2D Sprite Shape package and add it to your project.

Creating a Shape

The Sprite Shape Profile

Sprite Shape works by tiling sprites along spline paths created in the scene as game objects. However, to start creating paths, we must first set up an asset called the Sprite Shape Profile. It is used to define and store information about a particular type of shape. Within this asset, we assign the sprites we wish to use and tell Sprite Shape how they should be rendered. For example, we can configure which sprites are to be displayed based on the direction a part of our shape is facing, whether the shape has a fill texture, and so on.

Let’s create a profile right now! To do so, right-click in the Assets window of your project and go to Create > Sprite Shape Profile. You will see that there are three types of Profiles available to you: Empty, Strip, and Shape. The only difference between these profiles is the number of pre-set angle ranges they come with. Let’s start by making a Strip profile – we’ll get to angle ranges in a second.

We can now edit our Profile in the Inspector window. If you take a look at the Angle Ranges circle, you notice that it’s completely filled, meaning that it has one pre-defined angle range. An angle range determines which sprites are rendered along the path when your curve is facing that particular direction. Having one angle range that covers the full circle means that the same sprites will be displayed at all times.

Under Sprites in the Inspector window, we can add or delete new sprites to the Angle Range by using the ‘+’ and ‘-’ buttons below. Let’s go ahead and add our first sprite to this shape! For this and all future examples, I’m going to be using sprites available for free from our 2D Game Kit.

The Sprite Shape Game Object

Now that we have a profile set up, we can start creating shapes from it. To automatically use the profile in our new shape, make sure you have the profile selected in the Assets window; then right-click in the Hierarchy window and go to 2D Object > Sprite Shape.

Note: if you’ve accidentally created an empty Sprite Shape or you would like to use a different profile, you can assign or change it within the Sprite Shape Controller component:

The other component of the game object is the Sprite Shape Renderer, which acts similarly to a regular Sprite Renderer, allowing you to change the material, color, and layer order of the sprite.

Editing the spline

You can now start editing your shape by clicking Edit Spline in the Sprite Shape Controller options. Once you’ve got that enabled, within your scene you will be able to rearrange, add, and delete nodes on your spline. To add a new node, simply left-click anywhere on the spline. To delete a node, select it and press Delete.

Let’s talk a little bit about Point Modes available in the Sprite Shape Controller. Currently, we are in Linear point mode. This means that no curve is formed through our node. However, if we switch to one of the other two modes, such as the Mirrored mode, with a node selected, we will see that the node now has two tangents, and when we move them around, we can change the shape of the Bezier curve:

The last mode is called Non-Mirrored mode. Enabling it unlinks the two tangents from each other, allowing you to adjust one at a time without affecting the other.

Using the Sprite Editor

Sometimes our sprites will need to be manually adjusted if we want them to be tiled in a particular way. In the examples above, you can see that since we are using a bridge sprite, we’re getting a shape that consists of bridge segments. What if we wanted to make it look like one long bridge strip instead?

Fear not! The Sprite Editor is here to help. This blog post will not cover it in depth, but there is an excellent video tutorial on it on Unity’s YouTube channel. That said, knowing about certain parts of the Sprite Editor will allow you to use Sprite Shape more efficiently, and adjust sprites to suit your needs with ease.

Let’s edit our bridge sprite! With it selected, in the Inspector window, you will be able to find the Sprite Editor button. Click it, and the Sprite Editor window will be brought up.

The main things of interest to us about the sprite editor will be the four green control points around the sprite, as well as the border settings in the Sprite window at the bottom right. By using borders, we can tell Sprite Shape which part of the shape we want to be tiled, and which parts we want to act as our border sprites – which will only be rendered at the start and end nodes of our path, or at angled corners. We can adjust our bridge sprite’s left and right borders to make sure only the middle section of the bridge is tiled.

One other setting that can be of use is the Pivot point of the sprite. The Pivot determines how the sprite is rendered relative to the spline. Currently, our bridge sprite has a pivot in the center, which makes the spline pass it right in the middle. If we set it to be at the top of the bridge, the sprite will render below the spline. This can be useful for adjusting the relative position of auto-generated colliders for your shape which we’ll cover in a bit, as well as for more precision when using Sprite Shape to decorate an environment.

Sprite Variations

On the note of decorating environments, Sprite Shape also allows you to assign more than one sprite per angle range, and switch between them on your shape. This can be used for adding visual variety to your shapes, or in creating prop and decoration ‘brushes’.

I’ve prepared a simple hanging moss profile with two sprites assigned to a single angle range. I can use this profile to add decorative moss to one of the other shapes in my scene.

Once I have a shape I’m happy with, I can now change which of the sprite variants is rendered per each segment of the spline. To do so, I will select a starting node of the segment I wish to change. I can now set the Sprite Index in the Sprite Shape Controller to that of my other sprite.

Adding collision

One awesome feature that Sprite Shape comes with is the ability to auto-generate colliders for your shapes, also allowing you to manually adjust them later if required.

For an open-ended Sprite Shape, which is what we have right now, we can use an Edge Collider 2D component. You can add it to your Sprite Shape game object from the Inspector window. You will notice that a new checkbox called Update Collider has now appeared in the Sprite Shape Controller. Checking it will start adjusting the collider to your shape in real-time:

Once you’ve perfected your shape, you might want to make some manual changes. To do so, make sure you untick Update Collider first – otherwise, it will keep rewriting your changes – and then under Edge Collider 2D, go to Edit Collider. You can now adjust the collider however you want!

We can test our new collider by putting a character in the scene:

Close-ended Shapes

Now that you’re familiar with the basics of Sprite Shape and the Strip profile, let’s take a look at creating close-ended shapes with multiple angle ranges.

Let’s briefly come back to the other two Sprite Shape Profiles we saw earlier. We have the Empty profile which comes with no pre-set angle ranges, and the Shape profile which comes with eight. For my shape, I only need four; so let’s start with an Empty profile and learn how to add our own angle ranges.

Defining Angle Ranges

With the new profile selected, we can create a new angle range by either clicking on an empty spot on the preview circle or pressing the Create Range button below it. Once we have an angle range created, we can select it by clicking on it, and from here we can either define its start and end points numerically (see below) or by dragging the gizmos around the range to the desired position:

I need four angle ranges, so I will make each one cover ninety degrees. Once that’s done, I can assign a sprite to each of the angle ranges separately and attempt to make my shape look like a complete piece of terrain. After that’s done, I can use my profile to create a new shape in the scene:

You will notice that the shape automatically becomes close-ended due to the nature of the angle ranges we have defined. You can change the type of your shape at any point by selecting it in the scene and enabling or disabling the Open Ended checkbox in the Sprite Shape Controller.

You’ll also notice that several things are wrong with our new shape: first of all, it does not have a fill texture; and second, there are no sprites rendering at the corners of the shape. We can fix both of these things very easily back in our Sprite Shape Profile.

Adding a Fill Texture

To fill the inside of our shape we have to include a fill texture in our Profile. This can be done under the Fill options in the Inspector window. We can also change it to the desired resolution. I will be using one of the tiles from the 2D Gamekit as my texture.

There are several things to know about fill textures. First of all, they must be imported as individual files, and cannot be a part of a sprite atlas. Also, in the import settings, you have to make sure that the Wrap Mode is set to Repeat. If you fail to set the Wrap Mode correctly, the texture will create artifacts.

Using corner sprites

Our shape is looking a lot better with a fill texture, but we are still missing sprites at the corners of our shape. Specifically for this purpose, Sprite Shape includes the option to add up to eight individual corner sprites, each of which corresponds to a specific location on the shape.

Corner sprites can be assigned within the Sprite Shape Profile, within the Corners section. For this example, I will assign six out of eight corner sprites. The amount that you use for your shapes may vary depending on the types of paths you plan to create.

Once the corner sprites are assigned, I need to tell the shape where they need to be used. To do so, I can go into Edit Spline mode, and from there select the individual corner nodes on my shape, and set their Corner Mode to Automatic:

Attaching objects to nodes on the spline

We have now covered the majority of the Sprite Shape interface and workflow. You can also easily extend the feature with a little bit of scripting.

Here is an example of a script which will attach an object to a node on a select spline in the scene.

using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.U2D; [ExecuteInEditMode] public class NodeAttach : MonoBehaviour { public SpriteShapeController spriteShapeController; public int index; public bool useNormals = false; public bool runtimeUpdate = false; [Header("Offset")] public float yOffset = 0.0f; public bool localOffset = false; private Spline spline; private int lastSpritePointCount; private bool lastUseNormals; private Vector3 lastPosition; void Awake() { spline = spriteShapeController.spline; } void Update() { if (!EditorApplication.isPlaying || runtimeUpdate) { spline = spriteShapeController.spline; if ((spline.GetPointCount() != 0) && (lastSpritePointCount != 0)) { index = Mathf.Clamp(index, 0, spline.GetPointCount() - 1); if (spline.GetPointCount() != lastSpritePointCount) { if (spline.GetPosition(index) != lastPosition) { index += spline.GetPointCount() - lastSpritePointCount; } } if ((index <= spline.GetPointCount() - 1) && (index >= 0)) { if (useNormals) { if (spline.GetTangentMode(index) != ShapeTangentMode.Linear) { Vector3 lt = Vector3.Normalize(spline.GetLeftTangent(index) - spline.GetRightTangent(index)); Vector3 rt = Vector3.Normalize(spline.GetLeftTangent(index) - spline.GetRightTangent(index)); float a = Angle(Vector3.left, lt); float b = Angle(lt, rt); float c = a + (b * 0.5f); if (b > 0) c = (180 + c); transform.rotation = Quaternion.Euler(0, 0, c); } } else { transform.rotation = Quaternion.Euler(0, 0, 0); } Vector3 offsetVector; if (localOffset) { offsetVector = (Vector3)Rotate(Vector2.up, transform.localEulerAngles.z) * yOffset; } else { offsetVector = Vector2.up * yOffset; } transform.position = spriteShapeController.transform.position + spline.GetPosition(index) + offsetVector; lastPosition = spline.GetPosition(index); } } } lastSpritePointCount = spline.GetPointCount(); } private float Angle(Vector3 a, Vector3 b) { float dot = Vector3.Dot(a, b); float det = (a.x * b.y) - (b.x * a.y); return Mathf.Atan2(det, dot) * Mathf.Rad2Deg; } private Vector2 Rotate(Vector2 v, float degrees) { float radians = degrees * Mathf.Deg2Rad; float sin = Mathf.Sin(radians); float cos = Mathf.Cos(radians); float tx = v.x; float ty = v.y; return new Vector2(cos * tx - sin * ty, sin * tx + cos * ty); } } 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 using System . Collections ; using System . Collections . Generic ; using UnityEditor ; using UnityEngine ; using UnityEngine . U2D ; [ ExecuteInEditMode ] public class NodeAttach : MonoBehaviour { public SpriteShapeController spriteShapeController ; public int index ; public bool useNormals = false ; public bool runtimeUpdate = false ; [ Header ( "Offset" ) ] public float yOffset = 0.0f ; public bool localOffset = false ; private Spline spline ; private int lastSpritePointCount ; private bool lastUseNormals ; private Vector3 lastPosition ; void Awake ( ) { spline = spriteShapeController . spline ; } void Update ( ) { if ( ! EditorApplication . isPlaying || runtimeUpdate ) { spline = spriteShapeController . spline ; if ( ( spline . GetPointCount ( ) != 0 ) && ( lastSpritePointCount != 0 ) ) { index = Mathf . Clamp ( index , 0 , spline . GetPointCount ( ) - 1 ) ; if ( spline . GetPointCount ( ) != lastSpritePointCount ) { if ( spline . GetPosition ( index ) != lastPosition ) { index += spline . GetPointCount ( ) - lastSpritePointCount ; } } if ( ( index <= spline . GetPointCount ( ) - 1 ) && ( index >= 0 ) ) { if ( useNormals ) { if ( spline . GetTangentMode ( index ) != ShapeTangentMode . Linear ) { Vector3 lt = Vector3 . Normalize ( spline . GetLeftTangent ( index ) - spline . GetRightTangent ( index ) ) ; Vector3 rt = Vector3 . Normalize ( spline . GetLeftTangent ( index ) - spline . GetRightTangent ( index ) ) ; float a = Angle ( Vector3 . left , lt ) ; float b = Angle ( lt , rt ) ; float c = a + ( b * 0.5f ) ; if ( b > 0 ) c = ( 180 + c ) ; transform . rotation = Quaternion . Euler ( 0 , 0 , c ) ; } } else { transform . rotation = Quaternion . Euler ( 0 , 0 , 0 ) ; } Vector3 offsetVector ; if ( localOffset ) { offsetVector = ( Vector3 ) Rotate ( Vector2 . up , transform . localEulerAngles . z ) * yOffset ; } else { offsetVector = Vector2 . up * yOffset ; } transform . position = spriteShapeController . transform . position + spline . GetPosition ( index ) + offsetVector ; lastPosition = spline . GetPosition ( index ) ; } } } lastSpritePointCount = spline . GetPointCount ( ) ; } private float Angle ( Vector3 a , Vector3 b ) { float dot = Vector3 . Dot ( a , b ) ; float det = ( a . x * b . y ) - ( b . x * a . y ) ; return Mathf . Atan2 ( det , dot ) * Mathf . Rad2Deg ; } private Vector2 Rotate ( Vector2 v , float degrees ) { float radians = degrees * Mathf . Deg2Rad ; float sin = Mathf . Sin ( radians ) ; float cos = Mathf . Cos ( radians ) ; float tx = v . x ; float ty = v . y ; return new Vector2 ( cos * tx - sin * ty , sin * tx + cos * ty ) ; } }

When the anchor node gets moved or its tangents get rotated, the object’s transform also changes:

A script like this can be used to, for example, create dynamic environments, or make them react to player input. It can also make it easier for you to prototype levels without having to reposition individual elements.

The Sprite Shape scripting API is still under development. However, if you would like to experiment with Sprite Shape, you can access a work in progress version of the API documentation through the Assembly Reference through the Visual Studio Solution Explorer. Most of the API that will be relevant is under Unity.2D.SpriteShape.Runtime, however, please keep in mind that this is subject to change in the future.

Further resources

If you want to learn more about Sprite Shape, there are several resources you can currently make use of.

Basic feature and UI documentation is available on the feature’s GitHub repository.

The scripting API can be accessed from within your project if it has the Sprite Shape package.

Finally, please do share your own Sprite Shape creations and feature feedback on The Sprite Shape forum. We are excited to see the awesome things you come up with!