Drawing the outermost boundaries of set of squares

Viewed 236

enter image description here

I need to draw a border in Unity. I have some randomly generated squares, each of them at least neighbour to another square. I'll use some kind of line renderer, so I need to give line renderer points' coordinates which will create the border.

Figure one is my squares, Figure two is expected red border. In figure three, I tried to explain what I need, points in an order which will generate the red border around those squares.

I know each boxes world position exactly. So what I'm looking is an algorithm to move from point a to b, to create that border.

Any pseudo algorithm will be helpful.

(I will use it in an turn based rpg, where I want to highlight the area which a character can move. I dont want to highlight the whole squares as area, but only the border. Like its in XCOM games)

5 Answers

I've found a way to help you. I've written a function for you that does the following: iterates all the squares through their RectTransforms (easily replaceable with the Transform component), and for each of the squares it checks if its position is equal to another square with the side added up of the square. In this way, squares with at least one side in common are found. If it finds a square with one side in common, it adds the two vertices in common to a list. If at the end 4 vertices are found, the code understands that it is inside the figure and therefore must not have edges, but if three or less are found, they are added to the final list of external vertices.

The problem with the code is that it can be more optimized you have to customize the if () to check if they have a common side. In particular, since they are floats, it is simply not necessary to match them. I tried to round them up, but in particular cases there may be too great a margin of error. You can just change it by knowing the side of the square.

    List<RectTransform> pos = new List<RectTransform>();
    List<Vector2> results = new List<Vector2>();
    int size = pos[0].sizeDelta.x;
    for (int i = 0; i < pos.Count; i++)
    {
        List<Vector2> v = new List<Vector2>();
        for (int o = 0; o < pos.Count; o++)
        {
            if (Mathf.Round(pos[i].position.x) == Mathf.Round(pos[o].position.x + size))
            {
                Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y + size / 2));
                Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y - size / 2));
            }
            else if (Mathf.Round(pos[i].position.x) == Mathf.Round(pos[o].position.x - size))
            {
                Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y + size / 2));
                Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y - size / 2));
            }
            else if (Mathf.Round(pos[i].position.y) == Mathf.Round(pos[o].position.y + size))
            {
                Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y + size / 2));
                Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y + size / 2));
            }
            else if (Mathf.Round(pos[i].position.y) == Mathf.Round(pos[o].position.y - size))
            {
                Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y - size / 2));
                Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y - size / 2));
            }
            if (v.Count == 4)
                break;
        }
        if (v.Count == 4)
            continue;
        for (int o = 0; i < v.Count; o++)
            if (!results.Contains(v[o]))
                results.Add(v[o]);
        void Add(Vector2 _v)
        {
            if (!v.Contains(_v))
                v.Add(_v);
        }
    }

To create the line renderer that joins all these vertices, I suggest you think like this:

  • Choose a vertex to start from. Compare that vertex with all the others and check if the distance between that vertex and the compared vertex is equal to the side of the square. In this case it means that it is above, below, to the right or to the left of the first vertex.
  • You will have a maximum of 4 results, and add them all to a list.
  • Now take a vertex you just found and use it to compare it to all the others, doing the same thing as before. Also this time you will find at most 4 vertices, with the distance from that vetice equal to the side of the square. The difference is that for sure among those vertices you will also find the first vertex analyzed, and then check if it is already present, and if necessary remove it and add the one found. They will have the same value, but the order will be different.
  • Choose another vertex among those exited and start over with the for () loop.

You have to be careful because there are some things you have to think about for it to work that I didn't specify because it would become very long. As mentioned, if you are good with C # you will be able to transform this reasoning into code.

Good work!

I’m going to assume that the movement area is connected. If not, you can do a flood fill and run this algorithm on each of the connected areas.

The first order of business is to find one of the clockwise arcs on the outer boundary. The left edge of the leftmost square, oriented up, will do. Starting with this arc, we trace clockwise around the boundary. There are three cases for each movement, demonstrated in the Python code below. When the boundary is straight, we suppress the interior points.

image = """
  ##
   #
  ##
#### #
 #####
  ##
   #
"""

squares = {
    (x + 0.5, y - 0.5)
    for (y, line) in enumerate(image.splitlines())
    for (x, c) in enumerate(line)
    if c == "#"
}
print("squares =", squares)

leftmost = min(squares)
up = (0, -1)
x, y = leftmost
dx, dy = up
boundary = []
while True:
    point = (x + 0.5 * (dx + dy), y + 0.5 * (dy - dx))
    left = (x + dx + dy, y + dy - dx)
    straight = (x + dx, y + dy)
    if left in squares:  # turn left
        boundary.append(point)
        x, y = left
        dx, dy = dy, -dx
    elif straight in squares:  # go straight
        x, y = straight
    else:  # turn right
        boundary.append(point)
        dx, dy = -dy, dx
    if ((x, y), (dx, dy)) == (leftmost, up):
        break
print("boundary =", boundary)

Output:

squares = {(3.5, 1.5), (5.5, 4.5), (0.5, 3.5), (2.5, 0.5), (1.5, 4.5), (3.5, 0.5), (2.5, 3.5), (3.5, 6.5), (3.5, 3.5), (2.5, 2.5), (5.5, 3.5), (4.5, 4.5), (3.5, 2.5), (2.5, 5.5), (1.5, 3.5), (3.5, 5.5), (2.5, 4.5), (3.5, 4.5)}
boundary = [(0.0, 3.0), (2.0, 3.0), (2.0, 2.0), (3.0, 2.0), (3.0, 1.0), (2.0, 1.0), (2.0, 0.0), (4.0, 0.0), (4.0, 4.0), (5.0, 4.0), (5.0, 3.0), (6.0, 3.0), (6.0, 5.0), (4.0, 5.0), (4.0, 7.0), (3.0, 7.0), (3.0, 6.0), (2.0, 6.0), (2.0, 5.0), (1.0, 5.0), (1.0, 4.0), (0.0, 4.0)]

EDIT :

I didn't realized that an ordered list of coordinates was required, my original answer was making a non oriented graph around the shape.

I updated the answer to add a function that would build an oriented list from this graph


Considering you're using Unity, I made a very simple algorithm.

Knowing only the coordinates of every vertices that is part of the figure you want to outline, and with the assumption that the distance between them is always the same, I came up with this :

Firstly a very crude way of making the squares :

The assumption is that I the coordinates have no concept of what are their edges, only the four points of each square are given.

For the sake of this example I've done this handcrafting a list of list of vertex.

public void CreateCoords()
{
    _squares = new List<List<Vector3>>()
    {
        new List<Vector3>()
        {
            new Vector3(0,0,0),
            new Vector3(0,0,1),
            new Vector3(1,0,0),
            new Vector3(1,0,1),
        },
        new List<Vector3>()
        {
            new Vector3(0,0,1),
            new Vector3(1,0,1),
            new Vector3(1,0,2),
            new Vector3(0,0,2),
        },
        new List<Vector3>()
        {
            new Vector3(1,0,1),
            new Vector3(2,0,1),
            new Vector3(2,0,2),
            new Vector3(1,0,2),
        },
        new List<Vector3>()
        {
            new Vector3(2,0,1),
            new Vector3(2,0,2),
            new Vector3(3,0,1),
            new Vector3(3,0,2),
        },
        new List<Vector3>()
        {
            new Vector3(2,0,1),
            new Vector3(3,0,1),
            new Vector3(2,0,0),
            new Vector3(3,0,0),
        },
        new List<Vector3>()
        {
            new Vector3(4,0,1),
            new Vector3(3,0,1),
            new Vector3(4,0,0),
            new Vector3(3,0,0),
        },
        new List<Vector3>()
        {
            new Vector3(1,0,2),
            new Vector3(1,0,3),
            new Vector3(2,0,2),
            new Vector3(2,0,3),
        },
        new List<Vector3>()
        {
            new Vector3(3,0,2),
            new Vector3(3,0,3),
            new Vector3(2,0,2),
            new Vector3(2,0,3),
        },
        new List<Vector3>()
        {
            new Vector3(3,0,4),
            new Vector3(3,0,3),
            new Vector3(2,0,4),
            new Vector3(2,0,3),
        },
        new List<Vector3>()
        {
            new Vector3(1,0,4),
            new Vector3(1,0,3),
            new Vector3(2,0,4),
            new Vector3(2,0,3),
        },
        new List<Vector3>()
        {
            new Vector3(3,0,4),
            new Vector3(3,0,5),
            new Vector3(2,0,4),
            new Vector3(2,0,5),
        },
        new List<Vector3>()
        {
            new Vector3(3,0,2),
            new Vector3(3,0,3),
            new Vector3(4,0,2),
            new Vector3(4,0,3),
        },
    };
}

Here's the result :

Squares with all edges

Now for the algorithm :

My solution is based on the fact that all edges that are part of the border will only appear once.

It also works on the assumption that the coordinates of the squares are not ordered, the algorithm would be a bit simpler if it was the case.

private void ComputeEdges()
{
    // The edges collection is a set of pair of vertex
    _edges = new HashSet<KeyValuePair<Vector3, Vector3>>();
    foreach (var square in _squares)
    {
        // Iterate over the coordinates to compute the edges
        // Using for loop to skip already processed edges
        var squareCount = square.Count;
        for (var i = 0; i < squareCount; i++)
        {
            // The source vertex
            var src = square[i];

            for (var j = 0; j < squareCount; j++)
            {
                if (i == j) continue;
                // The vertex with whom we want to determine if they form and edge
                var dest = square[j];

                // Check the distance between them to filter out the diagonal edges
                if (!(Math.Abs(Vector3.Distance(src, dest) - edgeDistance) < 0.001)) continue;

                var edge = new KeyValuePair<Vector3, Vector3>(src, dest);

                // _edges is a set, making it viable to use Contains
                // even when the collections contains a lot of elements
                if (_edges.Contains(edge))
                {
                    // If the edge already exists in the set,
                    // it means its not part of the border
                    _edges.Remove(edge);
                }
                else
                {
                    _edges.Add(edge);
                }
            }
        }
    }
}

Here's a simple Gizmos example that will display the borders.

foreach (var (src, dest) in _edges)
{
    Gizmos.DrawLine(src, dest);
}

And here's the final result :

Final result

As I understood later that an ordered list of points would be prefered, I made another algorithm that could make one out of the edges collection.

public void BuildList()
{
    _coordsList = new List<Vector3>();

    // Make a copy of the edges so we can remove items from it
    // without destroying the original collection
    var copy = new HashSet<KeyValuePair<Vector3, Vector3>>(_edges);

    // Add the first pair before starting the loop
    var previousEdge = _edges.First();

    _coordsList.Add(previousEdge.Key);
    _coordsList.Add(previousEdge.Value);

    KeyValuePair<Vector3, Vector3> currentEdge;

    // While there is an edge that follows the previous one
    while (!(currentEdge = copy.FirstOrDefault(pair => pair.Key == previousEdge.Value))
           .Equals(default(KeyValuePair<Vector3, Vector3>)))
    {
        // Our graph is not oriented but we want to ignores edges
        // that go back from where we went
        if (currentEdge.GetHashCode() == previousEdge.GetHashCode())
        {
            copy.Remove(currentEdge);
            continue;
        }

        // Add the vertex to the list and continue
        _coordsList.Add(currentEdge.Value);
        previousEdge = currentEdge;

        // Remove traversed nodes
        copy.Remove(currentEdge);
    }
}

A case might be made about the performance, has my algorithm has first to computes the edges, makes uses of the Vector3.Distance function and frequently uses a Contains() method.

Building the edges iterating over everyone of them using the distance is clearly not optimal performance wise, but I wanted to keep this part simple and I don't think the impact will make it that much different compared to the other algorithm proposed.

As for the Contains() method, keep in mind that it comes from a HashSet, making it of the same complexity as using the indexer operator on a Dictionnary or a List.

For the sake of completeness and ease of use, here's the full class :

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace DrawOuterGraph
{
    public class OuterGraph: MonoBehaviour
    {
        private List<List<Vector3>> _squares;

        private HashSet<KeyValuePair<Vector3, Vector3>> _edges;

        private List<Vector3> _coordsList;

        [SerializeField]
        private float edgeDistance = 1;

        private void Start()
        {
            CreateCoords();
            ComputeEdges();
            BuildList();
        }

        private void ComputeEdges()
        {
            // The edges collection is a set of pair of vertex
            _edges = new HashSet<KeyValuePair<Vector3, Vector3>>();
            foreach (var square in _squares)
            {
                // Iterate over the coordinates to compute the edges
                // Using for loop to skip already processed edges
                var squareCount = square.Count;
                for (var i = 0; i < squareCount; i++)
                {
                    // The source vertex
                    var src = square[i];

                    for (var j = 0; j < squareCount; j++)
                    {
                        if (i == j) continue;
                        // The vertex with whom we want to determine if they form and edge
                        var dest = square[j];

                        // Check the distance between them to filter out the diagonal edges
                        if (!(Math.Abs(Vector3.Distance(src, dest) - edgeDistance) < 0.001)) continue;

                        var edge = new KeyValuePair<Vector3, Vector3>(src, dest);

                        // _edges is a set, making it viable to use Contains
                        // even when the collections contains a lot of elements
                        if (_edges.Contains(edge))
                        {
                            // If the edge already exists in the set,
                            // it means its not part of the border
                            _edges.Remove(edge);
                        }
                        else
                        {
                            _edges.Add(edge);
                        }
                    }
                }
            }
        }

        public void BuildList()
        {
            _coordsList = new List<Vector3>();

            // Make a copy of the edges so we can remove items from it
            // without destroying the original collection
            var copy = new HashSet<KeyValuePair<Vector3, Vector3>>(_edges);

            // Add the first pair before starting the loop
            var previousEdge = _edges.First();

            _coordsList.Add(previousEdge.Key);
            _coordsList.Add(previousEdge.Value);

            KeyValuePair<Vector3, Vector3> currentEdge;

            // While there is an edge that follows the previous one
            while (!(currentEdge = copy.FirstOrDefault(pair => pair.Key == previousEdge.Value))
                   .Equals(default(KeyValuePair<Vector3, Vector3>)))
            {
                // Our graph is not oriented but we want to ignores edges
                // that go back from where we went
                if (currentEdge.GetHashCode() == previousEdge.GetHashCode())
                {
                    copy.Remove(currentEdge);
                    continue;
                }

                // Add the vertex to the list and continue
                _coordsList.Add(currentEdge.Value);
                previousEdge = currentEdge;

                // Remove traversed nodes
                copy.Remove(currentEdge);
            }
        }

        public void CreateCoords()
        {
            _squares = new List<List<Vector3>>()
            {
                new List<Vector3>()
                {
                    new Vector3(0,0,1),
                    new Vector3(1,0,0),
                    new Vector3(0,0,0),
                    new Vector3(1,0,1),
                },
                new List<Vector3>()
                {
                    new Vector3(0,0,1),
                    new Vector3(1,0,1),
                    new Vector3(1,0,2),
                    new Vector3(0,0,2),
                },
                new List<Vector3>()
                {
                    new Vector3(1,0,1),
                    new Vector3(2,0,1),
                    new Vector3(2,0,2),
                    new Vector3(1,0,2),
                },
                new List<Vector3>()
                {
                    new Vector3(2,0,1),
                    new Vector3(2,0,2),
                    new Vector3(3,0,1),
                    new Vector3(3,0,2),
                },
                new List<Vector3>()
                {
                    new Vector3(2,0,1),
                    new Vector3(3,0,1),
                    new Vector3(2,0,0),
                    new Vector3(3,0,0),
                },
                new List<Vector3>()
                {
                    new Vector3(4,0,1),
                    new Vector3(3,0,1),
                    new Vector3(4,0,0),
                    new Vector3(3,0,0),
                },
                new List<Vector3>()
                {
                    new Vector3(1,0,2),
                    new Vector3(1,0,3),
                    new Vector3(2,0,2),
                    new Vector3(2,0,3),
                },
                new List<Vector3>()
                {
                    new Vector3(3,0,2),
                    new Vector3(3,0,3),
                    new Vector3(2,0,2),
                    new Vector3(2,0,3),
                },
                new List<Vector3>()
                {
                    new Vector3(3,0,4),
                    new Vector3(3,0,3),
                    new Vector3(2,0,4),
                    new Vector3(2,0,3),
                },
                new List<Vector3>()
                {
                    new Vector3(1,0,4),
                    new Vector3(1,0,3),
                    new Vector3(2,0,4),
                    new Vector3(2,0,3),
                },
                new List<Vector3>()
                {
                    new Vector3(3,0,4),
                    new Vector3(3,0,5),
                    new Vector3(2,0,4),
                    new Vector3(2,0,5),
                },
                new List<Vector3>()
                {
                    new Vector3(3,0,2),
                    new Vector3(3,0,3),
                    new Vector3(4,0,2),
                    new Vector3(4,0,3),
                },
            };
        }

        private void OnDrawGizmos()
        {
            // Draw using the edges
            if (_edges is not null)
            {
                foreach (var (src, dest) in _edges)
                {
                    Gizmos.DrawLine(src, dest);
                }
            }

            // Draw using the ordered list
            if (_coordsList is null || _coordsList.Count == 0) return;
            var previous = _coordsList[0];
            foreach (var current in _coordsList)
            {
                Gizmos.DrawLine(previous, current);
                previous = current;
            }
        }
    }
}

My approach is as simple as you expect it to be, we just check for the neighborhood and create a line to represent the free side.

Just to contextualize, I'm using an array of bool to represent what is a valid square and what isn't, and a List<(Vector3 from, Vector3 to)> to represent the border, and with some Gizmos' help we ended with something like

private List<(Vector3 from, Vector3 to)> border = new List<(Vector3 from, Vector3 to)>();
public bool[,] ValidPositions = new bool[10, 10];

(the beautiful serialized matrix come from Odin) enter image description here

As I said, simple as it can be, check for neighbor

private bool CheckBox (int i, int j, Side sideToCheck)
    => sideToCheck switch
    {
        Side.Up => j == 9 || !ValidPositions[i, j + 1], // By 9 I mean max range of lines in the array
        Side.Right => i == 9 || !ValidPositions[i + 1, j],
        Side.Down => j == 0 || !ValidPositions[i, j - 1],
        Side.Left => i == 0 || !ValidPositions[i - 1, j],
        _ => false
    };

And add a line

private (Vector3 from, Vector3 to) AddLine (int i, int j, Side side)
    => side switch
    {
        Side.Up => (new Vector3(i, j + 1),
            new Vector3(i + 1, j + 1)),
        Side.Right => (new Vector3(i + 1, j),
            new Vector3(i + 1, j + 1)),
        Side.Down => (new Vector3(i, j),
            new Vector3(i + 1, j)),
        Side.Left => (new Vector3(i, j),
            new Vector3(i, j + 1)),
    };

Call it is just some conditionals

private void RefreshContourPosition()
{
    border.Clear();

    for (var i = 0; i < ValidPositions.GetLength(0); i++)
    {
        for (var j = 0; j < ValidPositions.GetLength(1); j++)
        {
            if (!ValidPositions[i, j])
                continue;

            if (CheckBox(i, j, Side.Up))
                border.Add(AddLine(i, j, Side.Up));

            if (CheckBox(i, j, Side.Right))
                border.Add(AddLine(i, j, Side.Right));

            if (CheckBox(i, j, Side.Down))
                border.Add(AddLine(i, j, Side.Down));

            if (CheckBox(i, j, Side.Left))
                border.Add(AddLine(i, j, Side.Left));
        }
    }
}

After this refresh, your border will have all lines that is part of the desired border.

enter image description here

Hope this helps, N

This answer is a bit borderline Stackoverflow policy violation. :)

I understand that you ask for a specific solution, but I feel that as your question is still on a fairly theoretical design level I just wanted to drop some alternatives. I'm no pro, but my gut feeling is that line renderer will get quite heavy, especially for something like a basic grid.

If you are planning to have an all time visible base grid, I would probably take a look at already existing grid shaders to draw this. This is cheaper and the grid can be huge with no impact. It's also a lot easier to play around with when it comes to aesthetics, like adding glow or blend the lines against the underlying surface etc. For a 2d game, the Unity TileMap is a must to checkout.

https://github.com/ogxd/grid-shader-unity https://assetstore.unity.com/packages/tools/simple-grid-shader-119988

https://blog.devgenius.io/introduction-to-tilemap-in-unity-part-1-beb7b5435c2

When it comes to the outline. You might want to explore using tiles rather than drawing the edges. I think the tiles will be a lot easier to work with, both when it comes to game logic and making it look good. With a tile grid you could do bitmasking to draw nice edges. This will be resource cheap and opens up for endless possibilities to get that outline look really cool (like the curved glowing style xcom uses). If it is a 2d game you can use the Unity tile map, and it might even be possible to use Unity auto tile for the outline. If 3d each tile could just be a 3d mesh with texture. In 3d it would probably be better to generate a (procedural) mesh and outline it with an outline shader.

The short story is that I personally think using shaders/sprites for these kinds of things might be a better approach. :)

https://gamedevelopment.tutsplus.com/tutorials/how-to-use-tile-bitmasking-to-auto-tile-your-level-layouts--cms-25673

https://learn.unity.com/tutorial/using-rule-tiles#5fe9914fedbc2a28d93ce461

https://alexanderameye.github.io/notes/rendering-outlines/

Best of luck!

Related