Render a Circular Perimeter in Unity3D

If you want to make a circle perimeter line in Unity3D to display under a selected character, for example, you may come across some issues if you use a simple texture like the green circle below. If the circle has to support different size for different sizes of characters, the line will be bolder for larger character. That's why I came up with a simple solution for this.

Using a simple circle texture like this may not be the perfect solution.

Using a simple circle texture like this may not be the perfect solution.

CircleLineRenderer.cs (full code at the end of this post) is a simple script that uses a Line Renderer to draw a circle with the specified width. This way, the line is always the same width even when it is animated to different radius.

You can adjust the radius of the perimeter, the width of the line and if it's a dotted line using the property fields on the component in the inspector or by calling the SetRadius(), SetWidth() or SetIsDotted() methods at runtime. The LineRenderer renders in local space so you can rotate the Transform to change the plane of the circle.

You also have to assign a material for the plain line field and another one for the dotted line type, if you want to us it. Here's a texture for the dotted line material, note that it has to be a circle with empty padding space so the tiling material alternates between a dot an empty space.

This solution based on a Line Renderer might not be what you need if you have a bumpy terrain because it will clip through it. In that case, you may want to use a projector to make the texture fit the curve of the terrain perfectly.


CircleLineRenderer.cs

(to use with Unity 5.5 or newer since the LineRenderer component changed in 5.5)

using UnityEngine;

[RequireComponent(typeof(LineRenderer))]
public class CircleLineRenderer : MonoBehaviour {

    private const float DOTTED_LINE_TICKNESS_RATIO = 2.5f;
    private const int NUM_CAP_VERTICES = 6;

    [Tooltip("The radius of the circle perimeter")]
    public float radius = 2;

    [Tooltip("The width of the line for the circle perimeter")]
    public float lineWidth = 0.05f;

    [Tooltip("Check this to use the dotted line material for the circle perimeter line")]
    public bool isDotted = false;

    [Tooltip("The material for the plain line")]
    public Material plainMaterial;

    [Tooltip("The material for the dotted line")]
    public Material dottedMaterial;

    private LineRenderer lineRenderer;

    void Start ()
    {
        lineRenderer = GetComponent<LineRenderer>();
        lineRenderer.useWorldSpace = false;
        lineRenderer.numCapVertices = NUM_CAP_VERTICES;
        SetRadius(radius);
    }
    
    void Update ()
    {
        //While testing in-editor, refresh the circle each frame so we can test the circle by changing the fields in the inspector.
        if (Application.isEditor) SetRadius(radius);
    }

    //Call this method from other scripts to adjust the radius at runtime
    public void SetRadius(float pRadius)
    {
        radius = pRadius;

        if (radius <= 0.1f)
        {
            lineRenderer.numPositions = 0;
            return;
        }

        float tickness = lineWidth;
        if(isDotted) tickness *= DOTTED_LINE_TICKNESS_RATIO;
        lineRenderer.startWidth = tickness;
        lineRenderer.endWidth = tickness;

        //Calculate the number of vertices needed depending on radius so it always looks round.
        //For instance, huge circles need proportionaly less vertices than smaller ones to look good.
        //Source : http://stackoverflow.com/questions/11774038/how-to-render-a-circle-with-as-few-vertices-as-possible
        float e = 0.01f; //constant ratio to adjust, reduce this value for more vertices
        float th = Mathf.Acos(2 * Mathf.Pow(1 - e / radius, 2) - 1); //th is in radian
        int numberOfVertices = Mathf.CeilToInt(2 * Mathf.PI / th);

        lineRenderer.numPositions = numberOfVertices + 1;
        for (int i = 0; i < numberOfVertices + 1; i++)
        {
            float angle = (360f / (float)numberOfVertices) * (float)i;
            lineRenderer.SetPosition(i, radius * new Vector3(Mathf.Cos(Mathf.Deg2Rad * angle), Mathf.Sin(Mathf.Deg2Rad * angle), 0));
        }

        //Update material depending if it's a dotted line or a plain one.
        if (isDotted)
        {
            lineRenderer.material = dottedMaterial;
            lineRenderer.materials[0].mainTextureScale = new Vector3(2 * Mathf.PI * radius * (1 / tickness), 1, 1);
        }
        else
        {
            lineRenderer.material = plainMaterial;
            lineRenderer.materials[0].mainTextureScale = Vector3.one;
        }
    }

    //Call this method from other scripts to adjust the width of the line at runtime
    public void SetWidth(float pWidth)
    {
        lineWidth = pWidth;
        SetRadius(radius);
    }

    //Call this method from other scripts to switch between plain and dotted line at runtime
    public void SetIsDotted(bool pIsDotted)
    {
        if (isDotted != pIsDotted)
        {
            isDotted = pIsDotted;
            SetRadius(radius);
        }
    }    
}