For the last 6 months I've been working on a new (untitled) 2D game project in Unity both as a way to learn C# and also to play around with some game concepts I've been thinking about for quite a while.
Since I'm not much of an artist or a graphic designer I purchased a set of rather nice looking character sprites from https://tokegameart.net/ that also came with animations and ready to use Unity packages.
Since my game has multiple characters on screen at one and each one can be given orders I needed a way to show which one was selected or active. One common way to handle this which felt like a good fit for me is to show an outline around the selected character.
Luckily there's a lot of examples and guides explaining how to do this in Unity (and I based this one on a great article by Daniel Ilett). There was one snag though, my characters consist of multiple sprites (one for reach part of the body) that are drawn and animated separately. This meant that it would not be enough to just draw an outline around each part, otherwise you'd get something like this:
To avoid drawing over the sprites further away from the camera I instead created a separate sprite for each of the existing body parts, setting its sort order to -10. If we for a moment disable the regular sprites it looks like this:
And with the sprite drawn on top:
Since I'm not much of an artist or a graphic designer I purchased a set of rather nice looking character sprites from https://tokegameart.net/ that also came with animations and ready to use Unity packages.
Since my game has multiple characters on screen at one and each one can be given orders I needed a way to show which one was selected or active. One common way to handle this which felt like a good fit for me is to show an outline around the selected character.
Luckily there's a lot of examples and guides explaining how to do this in Unity (and I based this one on a great article by Daniel Ilett). There was one snag though, my characters consist of multiple sprites (one for reach part of the body) that are drawn and animated separately. This meant that it would not be enough to just draw an outline around each part, otherwise you'd get something like this:
To avoid drawing over the sprites further away from the camera I instead created a separate sprite for each of the existing body parts, setting its sort order to -10. If we for a moment disable the regular sprites it looks like this:
And with the sprite drawn on top:
Creating the outline sprites
Instead of manually creating a copy of each sprite and applying the shader from the next section I created a script which attaches to the root object of the character and then creates copies of all children (and grand-children) that has a SpriteRenderer component.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OutlineController : MonoBehaviour
{
// Reference to the shader material defined in the next section
public Material outlineMaterial;
public float outlineSize = 1f;
private List<Material> attachedMaterials = new List<Material>();
void Start()
{
foreach (var s in GetComponentsInChildren<SpriteRenderer>())
{
AddOutline(s);
}
}
void OnMouseEnter()
{
StartCoroutine(Animate(
(m, progress) => m.SetFloat("_Alpha", progress)));
}
void OnMouseExit()
{
StartCoroutine(Animate(
(m, progress) => m.SetFloat("_Alpha", 1 - progress)));
}
private IEnumerator Animate(Action<Material, float> updateAction)
{
for (int i = 0; i < 20; i++)
{
var progress = Mathf.SmoothStep(0f, 1f, (i + 1) / 20f);
foreach (var m in attachedMaterials)
{
updateAction(m, progress);
}
yield return new WaitForSeconds(0.02f);
}
}
private void AddOutline(SpriteRenderer sprite)
{
var width = sprite.bounds.size.x;
var height = sprite.bounds.size.x;
var widthScale = 1 / width;
var heightScale = 1 / height;
// Add child object with sprite renderer
var outline = new GameObject("Outline");
outline.transform.parent = sprite.gameObject.transform;
outline.transform.localScale = new Vector3(1f, 1f, 1f);
outline.transform.localPosition = new Vector3(0f, 0f, 0f);
outline.transform.localRotation = Quaternion.identity;
var outlineSprite = outline.AddComponent<SpriteRenderer>();
outlineSprite.sprite = sprite.sprite;
outlineSprite.material = outlineMaterial;
// The UV coordinates of the texture is always from 0..1 no matter
// what the aspect ratio is so we need to specify both the
// horizontal and vertical size of the outline
outlineSprite.material.SetFloat(
"_HSize", 0.1f * widthScale * outlineSize);
outlineSprite.material.SetFloat(
"_VSize", 0.1f * heightScale * outlineSize);
outlineSprite.sortingOrder = -10;
attachedMaterials.Add(outlineSprite.material);
}
}
The outline shader
When creating the shader I first explored the idea of simply scaling the sprite by a factor (e.g 1.10) and drawing it with a red color for all opaque pixels. I quickly realized that this would not work as it can only handle simple shapes consisting only of convex sides. If the shape has a cutout for example, there would be no outline inside it as the outline drawable has only expanded outwards.
The approach I instead took was the one suggested by Daniel Iletts post, to sample the texture in number of nearby positions (the distance determining how far the outline extends) and use those values to determine if the pixel should be transparent or opaque (part o the outline).
In pseudo code it can be expressed as:
pixel(x, y) = rgba(255, 0, 0,
Max(
SampleAlpha(texture, x - offset, y),
SampleAlpha(texture, x + offset, y),
SampleAlpha(texture, x, y - offset),
SampleAlpha(texture, x, y - offset)
))
Using the maximum value allows us to get a smooth edge for the outline (as long as the source texture has a faded edge).
![]() |
Main shader graph |
![]() |
Sub-shader graph for sampling the alpha value at an offset position |
There are some small mistakes here and there, do you read comments? One of the issues is a paragraph is repeated.
ReplyDeleteAlso a pretty serious typo in your script that results in uneven outlines.
ReplyDeleteThanks for the feedback! I removed the copy pasted paragraph :)
Delete> Also a pretty serious typo in your script that results in uneven outlines.
Nice catch, what's the typo?
I am shader newbie , which node is Sample Alpha ? or any chance download this shader ?
ReplyDelete