Ryan Nielson

The personal site and blog of Ryan Nielson.

© 2014. Ryan Nielson All rights reserved.

2D Sprite Outlines in Unity

Unity provides a component to outline UI objects, but it doesn't work on world space sprites. This post will demonstrate a simple way to add outlines to sprites using an extended version of the default sprite shader along with a simple component. This could be used to highlight sprites on mouse over, highlight items in the environment, or just to make sprites stand out from their surroundings.

To begin, create a new shader in your project called Sprite-Outline. This shader provides all the functionality of the default sprite shader, with the additions to allow sprite outlines.

Shader "Sprites/Outline"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0

        // Add values to determine if outlining is enabled and outline color.
        [PerRendererData] _Outline ("Outline", Float) = 0
        [PerRendererData] _OutlineColor("Outline Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ PIXELSNAP_ON
            #pragma shader_feature ETC1_EXTERNAL_ALPHA
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;
            float _Outline;
            fixed4 _OutlineColor;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap (OUT.vertex);
                #endif

                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _AlphaTex;
            float4 _MainTex_TexelSize;

            fixed4 SampleSpriteTexture (float2 uv)
            {
                fixed4 color = tex2D (_MainTex, uv);

                #if ETC1_EXTERNAL_ALPHA
                // get the color from an external texture (usecase: Alpha support for ETC1 on android)
                color.a = tex2D (_AlphaTex, uv).r;
                #endif //ETC1_EXTERNAL_ALPHA

                return color;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;

                // If outline is enabled and there is a pixel, try to draw an outline.
                if (_Outline > 0 && c.a != 0) {
                    // Get the neighbouring four pixels.
                    fixed4 pixelUp = tex2D(_MainTex, IN.texcoord + fixed2(0, _MainTex_TexelSize.y));
                    fixed4 pixelDown = tex2D(_MainTex, IN.texcoord - fixed2(0, _MainTex_TexelSize.y));
                    fixed4 pixelRight = tex2D(_MainTex, IN.texcoord + fixed2(_MainTex_TexelSize.x, 0));
                    fixed4 pixelLeft = tex2D(_MainTex, IN.texcoord - fixed2(_MainTex_TexelSize.x, 0));

                    // If one of the neighbouring pixels is invisible, we render an outline.
                    if (pixelUp.a * pixelDown.a * pixelRight.a * pixelLeft.a == 0) {
                        c.rgba = fixed4(1, 1, 1, 1) * _OutlineColor;
                    }
                }

                c.rgb *= c.a;

                return c;
            }
            ENDCG
        }
    }
}

Now create a new material called SpriteOutline and assign the newly created shader to it in the inspector.

Sprite Outline Material

Next create a new C# script and name it SpriteOutline. This component is going to handle updating our material in the editor and at runtime to toggle the outline off or on and also change the color. This component can also be targetted in an animation to enable or disable outlines for specific animation frames or to change the outline color.

using UnityEngine;

[ExecuteInEditMode]
public class SpriteOutline : MonoBehaviour {
    public Color color = Color.white;

    private SpriteRenderer spriteRenderer;

    void OnEnable() {
        spriteRenderer = GetComponent<SpriteRenderer>();

        UpdateOutline(true);
    }

    void OnDisable() {
        UpdateOutline(false);
    }

    void Update() {
        UpdateOutline(true);
    }

    void UpdateOutline(bool outline) {
        MaterialPropertyBlock mpb = new MaterialPropertyBlock();
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat("_Outline", outline ? 1f : 0);
        mpb.SetColor("_OutlineColor", color);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

Now that the hard work is done, add a few sprites to your scene. Change the material field of the SpriteRenderer component to the SpriteOutline material created above. You'll also want to add the SpriteOutline component to this game object to show a white outline by default. To hide the outline simply disable or remove the component.

Completed Sprite

With all that completed, you should now have a sprite with a white outline. In the inspector you can change the color to anything you'd like, independently from the SpriteRenderer color. The custom shader also maintains all existing functionality of the default sprite shader.

Completed Sprite

Please download the demo project and play around with it to get a better idea of how this technique looks and works. It contains a single scene with three examples of outlined sprites, one of which is animated.

Shaders can be complicated, but they are very powerful and make it quite easy to implement graphical features, even in 2D. If you have any further questions please feel free to message me on Twitter @RyanNielson or comment below.

Update (June 2, 2016)

Some people have been asking about how to change the thickness of the sprite outlines. Please download this new demo project with the changes to add this functionality. Changes were made to both the shader and component. Just adjust the outline size slider on the sprite outline component to change outline size. There is a limited outline size of 16 to prevent issues with shader for loop unrolling. It hasn't been tested throughly, so your results may vary, but it's probably a good place to start.

2D Splatter Effects in Unity Using the Stencil Buffer

While reading Zack Bell's excellent game development blog I was intrigued by the splatter effects used in his game called INK or in other games like Super Meet Boy. As an experiment I decided to try to find an easy way to pull off something similar in Unity. Based on some previous reading I've done regarding the stencil buffer, using it along with some custom shaders seemed like the simplest approach.

This is the kind of effect that's possible using only the stencil buffer and a few sprites.

Surface Material Shader

The stencil buffer is a pixel mask that can be used in shaders to save or discard pixels. It is simply a buffer where an integer is stored for each pixel. In your shader to can change the stencil buffer value, or optionally draw based on the stencil value. For more information on the stencil shader and various stencil operations, check out the Unity documentation. This post will be about achieving a specific effect using the stencil buffer, not a run-down of everything it has to offer.

Our shaders will be slight modifications on Unity's standard sprite shader to add the stencil operations. Luckily you can download all of the built-in shaders here so we don't have to write everything from scratch. These will be used as a base for these tutorials, so the rendering will be very close to standard Unity sprites.

Start by creating two new shaders in your project called Surface and Splatter. For the Surface shader, we want to add a Stencil block that sets the buffer value to 5 for any drawn surface pixel. Below is the Stencil block that we will be adding to the shader in its Pass block. The full shaders will be added further below. We also added an _AlphaCutoff property, which allows the splatter to draw properly on non-rectangular sprites like spikes.

Stencil
{
  Ref 5
  Comp Always
  Pass Replace
}

For the Splatter shader, we only want to draw splatter sprite pixels where the stencil buffer has been set to 5. This ensures that splatter is only drawn on surface pixels.

Stencil
{
  Ref 5
  Comp Equal
}
The full Surface.shader:
Shader "Splatter/Surface"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
        _AlphaCutoff("Alpha Cutoff", Range(0.01, 1.0)) = 0.01
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            Stencil
            {
                Ref 5
                Comp Always
                Pass Replace
            }

        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ PIXELSNAP_ON
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;
            fixed _AlphaCutoff;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap (OUT.vertex);
                #endif

                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _AlphaTex;
            float _AlphaSplitEnabled;

            fixed4 SampleSpriteTexture (float2 uv)
            {
                fixed4 color = tex2D (_MainTex, uv);
                if (_AlphaSplitEnabled)
                    color.a = tex2D (_AlphaTex, uv).r;

                return color;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
                c.rgb *= c.a;

        // Discard pixels below cutoff so that stencil is only updated for visible pixels.
                clip(c.a - _AlphaCutoff);

                return c;
            }
        ENDCG
        }
    }
}
The full Splatter.shader:
Shader "Splatter/Splatter"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            Stencil
            {
                Ref 5
                Comp Equal
            }

        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ PIXELSNAP_ON
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap (OUT.vertex);
                #endif

                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _AlphaTex;
            float _AlphaSplitEnabled;

            fixed4 SampleSpriteTexture (float2 uv)
            {
                fixed4 color = tex2D (_MainTex, uv);
                if (_AlphaSplitEnabled)
                    color.a = tex2D (_AlphaTex, uv).r;

                return color;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
                c.rgb *= c.a;
                return c;
            }
        ENDCG
        }
    }
}

Now we need to make two materials, Surface and Splatter, and assign the new shaders to them using their shader dropdowns.

Surface Material Shader

Splatter Material Shader

Finally, add two objects with sprite renderers to the scene. One named Surface, and the other named Splatter. Assign whatever sprites you want to them, and make sure Splatter is drawn above Surface by increasing its Order in Layer or adding it to a different Sorting Layer. Now, add the corresponding materials that were created earlier to each sprite renderer. If you move the splatter back and forth over the surface, it should only be drawn on the pixels of the surface and masked everywhere else.

Splatter Demo

Please download the demo project and play around with it to get a better idea of how this looks. It contains two scenes, Demo1 and Demo2. Demo1 contains a surface and a splatter sprite. Move the splatter sprite around to give you an idea of how this effect works. Demo2 contains many surfaces and the ability to shoot splatters as shown near the top of this post. Splatter will be shot from the center of the screen towards your mouse cursor when the mouse is clicked, and explode on the first surface that is hit.

The stencil buffer is a great feature that allows for all kinds of interesting effects, and makes it easy to draw simple paint and blood splatter using the approach above. Check out Prime31's post about sprite occlusion for another example of what the stencil buffer makes possible. If you have any further questions please feel free to message me on Twitter @RyanNielson or comment below.

Fixing Gaps Between Sprites - Better 2D in Unity Part 2

The first post in this series focused on the locking objects to the pixel grid. This time around I'll be focusing on issues related to side by side sprite rendering.

When working on 2D games in Unity using sprites, you may notice strange spaces or gaps between sprites. These are most notable when sprites are placed beside one another. This issue may not be noticeable immedately, but becomes most apparent at certain resolutions or when moving the camera as shown below.

Sprites With Gaps

This is quite easy to fix by using sprite packing software, which Unity 5 includes out of the box. The Unity sprite packer combines all sprites together into a single atlas, which can improve performance. Another side effect is that it duplicates the outer pixels of each sprite and adds them as padding in the packed atlas, solving the gap issue.

Enabling sprite packing is quite simple. First make sure the sprite packer is enabled by clicking on Edit > Project Settings > Editor in the toolbar and setting the sprite packer mode to "Always Enabled." Next, click on a sprite or sprite sheet in the project window, which will open the import settings in the inspector, and add a packing tag. In my case I just used the tag "Tiles", but you can use whatever you want. Sprites with identical packing tags will be added to the same atlas.

Sprite Inspector

You can view the atlas that Unity has created by clicking on Window > Sprite Packer in the toolbar. If there's nothing there, just click the pack button in the upper-left corner. Unity doesn't create the atlas until you manually click pack, or you run the game. Unity will regenerate atlases whenever play is pressed, or the game is built.

With an atlas created by setting a packing tag, the gaps between sprites are now gone as shown below. The gaps may still be shown in the game window when editing, but they will disappear when in playing the game. It seems that Unity only uses atlases in play mode.

Sprites With No Gaps

I've found that the Unity sprite packer works perfectly fine for all my needs. If you have any further questions please feel free to message me on Twitter @RyanNielson or comment below.

Controlling Object Visibility and Editability in Unity Using HideFlags

Unity game objects and components have an interesting feature called HideFlags. This allows you to control whether a game object or component is visible, editable, persistent, or any combination thereof.

Both GameObject and MonoBehaviour inherit from the Object class which contains the hideFlags property. This property can be set to any of the valid values in the HideFlags enumeration. By default it is set to HideFlags.None. Below are some examples and explanations of various options.

To run these examples, I've created a script that allows you to choose a HideFlags value and whether to set it on the game object or component. It sets the hideFlags property in OnEnable, so it will run when the game starts, or you can use it in combination with ExecuteInEditMode to run it while in the editor.

using UnityEngine;

public class HideFlagsSetter : MonoBehaviour
{
    public HideFlags customHideFlags;

    public enum Mode
    {
        GameObject,
        Component
    }

    public Mode setOn = Mode.GameObject;

    private void OnEnable()
    {
        if (setOn == Mode.GameObject)
        {
            gameObject.hideFlags = customHideFlags;
        }
        else if (setOn == Mode.Component)
        {
            hideFlags = customHideFlags;
        }
    }
}

HideFlags.HideInHierarchy

This option hides your game object in the hierarchy view, and makes it not selectable in the scene view. This has no effect if set on a component.

HideFlags.HideInHierarchy

HideFlags.HideInInspector

If set on a game object, this option hides all the components in the inspector.

HideFlags.HideInInspector

If set on a component, this option only hides the component in the inspector.

HideFlags.HideInInspector

HideFlags.NotEditable

If set on a game object, no components will be editable. The object will also not be selectable in the scene.

HideFlags.NotEditable

If set on a component, only that component will not be editable.

HideFlags.NotEditable

Other options include HideFlags.DontSaveInEditor, HideFlags.DontUnloadUnusedAsset, HideFlags.DontSaveInBuild, HideFlags.DontSave, and HideFlags.HideAndDontSave that mainly relate to scene and build persistence that I won't go into detail about here. More information about these can be found in the HideFlags Documentation.

You can download the example project to experiment with HideFlags using the script and example scene used in this post.

HideFlags can be a very useful tool in preventing the hierarchy from being cluttered and ensuring specific components are not editable. They're quite useful when working on assets in which the component values or game object structure are imporant. If you have any further questions please feel free to message me on Twitter @RyanNielson or comment below.

Saving Games in Unity

Unity has ways to save game data like PlayerPrefs but it is quite limited and can store information in strange locations on certain platforms. In this post, I'll demonstrate a more object oriented approach that allows you to save almost any kind of data structure to a file. This method is going to be based off of how Unreal Engine 4 handles saving games.

To start, we'll create a SaveGame object that will be used as an abstract base class for all of our saved game objects. This class is marked as [Serializable] so that C# is able to serialize it, and it's also marked as abstract because we won't be using it directly to save games.

using System;

[Serializable]
public abstract class SaveGame
{
}

Next comes the static SaveGameSystem class that does most of the heavy lifting of saving, loading, and deleting save files.

using UnityEngine;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class SaveGameSystem
{
    public static bool SaveGame(SaveGame saveGame, string name)
    {
        BinaryFormatter formatter = new BinaryFormatter();

        using (FileStream stream = new FileStream(GetSavePath(name), FileMode.Create))
        {
            try
            {
                formatter.Serialize(stream, saveGame);
            }
            catch (Exception)
            {
                return false;
            }
        }

        return true;
    }

    public static SaveGame LoadGame(string name)
    {
        if (!DoesSaveGameExist(name))
        {
            return null;
        }

        BinaryFormatter formatter = new BinaryFormatter();

        using (FileStream stream = new FileStream(GetSavePath(name), FileMode.Open))
        {
            try
            {
                return formatter.Deserialize(stream) as SaveGame;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }

    public static bool DeleteSaveGame(string name)
    {
        try
        {
            File.Delete(GetSavePath(name));
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    public static bool DoesSaveGameExist(string name)
    {
        return File.Exists(GetSavePath(name));
    }

    private static string GetSavePath(string name)
    {
        return Path.Combine(Application.persistentDataPath, name + ".sav");
    }
}

The SaveGame method takes a SaveGame object and a name, and saves it to a file. The LoadGame method takes a name and loads the file back into a usable SaveGame object. The DeleteGame method takes a name and deletes the associated save game file. All of these methods use GetSavePath to determine where the save game file should be located. By default, it's saved to the Application.persistantDataPath which is different on each platform. On Windows, it should be located somewhere similar to C:\Users\[username]\AppData\LocalLow\[companyname]\[projectname]. If you want to customize the saved game location, just modify this method.

Finally, create a custom class to store save game data that will be written to a file. This class should be marked as [Serializable] and inherit from the SaveGame class. It should contain any fields you wish to save to a file. By default C#'s BinaryFormatter class serializes all public and private fields, and public and private properties. To prevent a field from being serialized, it should be marked with the [NonSerialized] attribute.

using System;

[Serializable]
public class MySaveGame : SaveGame
{
    public string playerName = "Ryan";

    public int HighScore { get; set; }

    [NonSerialized]
    public string secret = "Nope";
}

Below is an example of how you could use this system to save and load a saved game.

// Saving a saved game.
MySaveGame mySaveGame1 = new MySaveGame();
mySaveGame1.playerName = "Ryan";
mySaveGame1.HighScore = 1000000;
mySaveGame1.secret = Random.Range(0, 1000).ToString();
SaveGameSystem.SaveGame(mySaveGame1, "MySaveGame"); // Saves as MySaveGame.sav

// Loading a saved game.
MySaveGame mySaveGame2 = SaveGameSystem.LoadGame("MySaveGame") as MySaveGame;
Debug.Log(mySaveGame2.playerName); // Will log Ryan
Debug.Log(mySaveGame2.HighScore);  // Will log 1000000
Debug.Log(mySaveGame2.secret);     // Will log null since this field was marked [NonSerialized]

// Deleting a saved game.
SaveGameSystem.DeleteSaveGame("MySaveGame");

I think this approach is much better than using PlayerPrefs because of its added flexibility, and hopefully you do as well. If you have any further questions please feel free to message me on Twitter @RyanNielson or comment below.