Coding Adventure - Circles, Cones, and Vertex Mapping

So a while back I began learning C#, and still I struggle with some basic things. At any rate, my latest endeavor is a small UFO game where you abduct cows. I am making progress on that, and got side tracked into a few things.

The original problem was detecting GameObjects within a cone, then calculating their magnitude from the center to select the closest cow inside the cone for abduction. I found that it would be easy if you were on a flat plane, but some weird things happen on realistic terrain.

So during that planning stage, for better or worse, I decided I’d need to map the local Vertices and match the Y value of my base circle to the closest pair of vertices who’s angle is less than a limit on the Y.

I started with making a grid…

public class GridCreator : MonoBehaviour
{
    public Vector2 worldGridSize;
    public float nodeRadius;
    float nodeDiameter;
    int gridSizeX, gridSizeY;

    public Node[,] CreateGrid(Vector3 raycastPoint)
    {
        nodeDiameter = nodeRadius * 2;
        gridSizeX = Mathf.RoundToInt(worldGridSize.x / nodeDiameter);
        gridSizeY = Mathf.RoundToInt(worldGridSize.y / nodeDiameter);
        Node[,] grid = new Node[gridSizeX, gridSizeY];
        Vector3 worldBottomLeft = raycastPoint - (Vector3.right * (worldGridSize.x / 2)) - (Vector3.forward * (worldGridSize.y / 2));

        for (int x = 0; x < gridSizeX; x++)
        {
            for (int y = 0; y < gridSizeY; y++)
            {
                Vector3 nodeLocation = worldBottomLeft + Vector3.right * (x * nodeDiameter + nodeRadius) + Vector3.forward * (y * nodeDiameter + nodeRadius);
                grid[x, y] = new Node(nodeLocation);
            }
        }

        return grid;
    }

Which is a 2D array of Nodes –

public class Node
{
    public Vector3 worldPosition;

    public Node(Vector3 _worldPosition)
    {
        worldPosition = _worldPosition;
    }
}

And that led me to some cool results when I used the gizmos.

    private void OnDrawGizmos()
    {
        if (drawGridGizmos)
        {
            if (drawSquareGrid)
            {
                if (Physics.Raycast(new Ray(raycastOrigin.position, Vector3.down), out RaycastHit _hit, float.MaxValue, layerMask))
                {
                    Gizmos.color = Color.white;
                    Gizmos.DrawWireCube(_hit.point, new Vector3(worldGridSize.x, 1, worldGridSize.y));

                    Node[,] squareGrid = CreateSquareGrid(_hit.point);

                    float squareNodeDiameter = nodeRadius * 2;
                    int squareGridSizeX = Mathf.RoundToInt(worldGridSize.x / squareNodeDiameter);
                    int squareGridSizeY = Mathf.RoundToInt(worldGridSize.y / squareNodeDiameter);

                    Vector3 previousNodePosition = Vector3.positiveInfinity;
                    for (int x = 0; x < squareGrid.GetLength(0); x++)
                    {
                        for (int y = 0; y < squareGrid.GetLength(1); y++)
                        {
                            Vector3 currentNodePosition = squareGrid[x, y].worldPosition;

                            // Draws a black cube at each node location except the first, middle, and last
                            Gizmos.color = Color.black;
                            Gizmos.DrawCube(currentNodePosition, Vector3.one * squareNodeDiameter / 4);
                            if (x == 0 && y == 0)
                            {
                                // First Node  -- Draws a green cube                                                            
                                Gizmos.color = Color.green;
                                Gizmos.DrawCube(currentNodePosition, Vector3.one * squareNodeDiameter / 4);
                            }

                            // Checks if a center node is in the grid
                            //looks for that center node
                            if (currentNodePosition == _hit.point)
                            {
                                // Center Node -- Draws a blue cube                                                            
                                Gizmos.color = Color.blue;
                                Gizmos.DrawCube(currentNodePosition, Vector3.one * squareNodeDiameter / 4);
                            }

                            if (x == squareGridSizeX - 1 && y == squareGridSizeY - 1)
                            {
                                // Last Node -- Draws a red cube
                                Gizmos.color = Color.red;
                                Gizmos.DrawCube(currentNodePosition, Vector3.one * squareNodeDiameter / 4);
                            }

                            // Checks if this is the first node
                            if (previousNodePosition != Vector3.positiveInfinity)
                            {
                                // Draws a line from each node to the next without drawing lines from the last node in a column to the first node of the next column
                                if (y != 0)
                                {
                                    if (currentNodePosition != previousNodePosition)
                                    {
                                        Gizmos.color = Color.white;
                                        Gizmos.DrawLine(previousNodePosition, currentNodePosition);
                                        previousNodePosition = currentNodePosition;
                                    }
                                }
                                // Sets the previous node to the current node in the event we begin a new column
                                else
                                {
                                    previousNodePosition = currentNodePosition;
                                }

                                // Draws a perpendicular line from the last nodes to the first ones
                                if (x == squareGridSizeX - 1)
                                {
                                    Gizmos.color = Color.white;
                                    Gizmos.DrawLine(currentNodePosition, squareGrid[0, y].worldPosition);
                                }

                            }
                            // Sets the first node as the previous node on first iteration
                            else
                            {
                                previousNodePosition = currentNodePosition;
                            }
                        }
                    }
                }
            }

So then I tried turning this concept into a circle –

    public Node[,] CircleGrid(int steps, float radius, Vector3 centerPoint, int ringCount)
    {
        float concentricRadius;
        int concentricSteps;
        Node[,] nodes = new Node[ringCount, steps + 1];

        for (int ring = 0; ring < ringCount; ring++)
        {
            concentricRadius = radius - (radius * ((float)ring / (float)ringCount));
            concentricSteps = steps - (steps * (Mathf.RoundToInt((float)ring)) / Mathf.RoundToInt((float)ringCount));

            for (int currentStep = 0; currentStep < concentricSteps; currentStep++)
            {
                float circumferenceProgress = (float)currentStep / concentricSteps;
                float currentRadian = circumferenceProgress * 2 * Mathf.PI;
                float xScaled = Mathf.Cos(currentRadian);
                float yScaled = Mathf.Sin(currentRadian);
                float x = xScaled * concentricRadius;
                float y = yScaled * concentricRadius;

                Vector3 currentPosition = centerPoint + new Vector3(x, 0, y);
                nodes[ring, currentStep] = new Node(currentPosition);
            }
        }
        if(nodes[nodes.GetLength(0) - 1, nodes.GetLength(1) - 1] == null)
        {
            nodes[nodes.GetLength(0) - 1, nodes.GetLength(1) - 1] = new Node(centerPoint);
        }
        return nodes;
    }

And Likewise drew it with Gizmos –

            if (drawConcentricCircles)
            {
                if (Physics.Raycast(new Ray(raycastOrigin.position, Vector3.down), out RaycastHit hit, float.MaxValue, layerMask))
                {
                    Node[,] circleNodes = CircleGrid(circleGridNodeCount, majorCircleRadius, hit.point, circleGridRingCount);

                    if (circleNodes != null)
                    {
                        for (int x = 0; x < circleNodes.GetLength(0); x++)
                        {
                            int segments = circleGridNodeCount - (circleGridNodeCount * (Mathf.RoundToInt((float)x)) / Mathf.RoundToInt((float)circleGridRingCount));
                            for (int y = 0; y < segments; y++)
                            {
                                Gizmos.color = Color.blue;
                                Gizmos.DrawWireSphere(circleNodes[x, y].worldPosition, .25f);
                            }
                        }
                        Gizmos.color = Color.green;
                        Gizmos.DrawWireSphere(circleNodes[circleNodes.GetLength(0) - 1, circleNodes.GetLength(1) - 1].worldPosition, .30f);
                    }
                }
            }

And now I am at the point where I am trying to find the vertices of the mesh the raycast hits so I can plot my circle and this is what I’ve got –

            if (drawVerts)
            {
                if (Physics.Raycast(new Ray(raycastOrigin.position, Vector3.down), out RaycastHit hit, float.MaxValue, layerMask))
                {
                    Mesh mesh = hit.collider.gameObject.GetComponent<MeshFilter>().sharedMesh;
                    Matrix4x4 localToWorld = hit.transform.localToWorldMatrix;
                    Vector3[] vertices = mesh.vertices;
                    Vector3[] normals = mesh.normals;
                    float closestVerticeDistance = float.MaxValue;
                    int closestVert = int.MinValue;



                    for (var i = 0; i < vertices.Length; i++)
                    {
                        Vector3 world_v = localToWorld.MultiplyPoint3x4(mesh.vertices[i]);
                        vertices[i].Set(world_v.x, world_v.y, world_v.z);
                        Gizmos.color = Color.black;
                        Gizmos.DrawCube(world_v, Vector3.one * .25f);

                        if ((vertices[i] - hit.point).magnitude < closestVerticeDistance)
                        {
                            closestVerticeDistance = (vertices[i] - hit.point).magnitude;
                            closestVert = i;
                        }

                    }

                    for (int i = 0; i < vertices.Length; i++)
                    {
                        if (i == closestVert)
                        {
                            Vector3 world_v = localToWorld.MultiplyPoint3x4(mesh.vertices[i]);
                            Gizmos.color = Color.red;
                            Gizmos.DrawCube(world_v, Vector3.one * .50f);
                        }

                    }

                }
            }

I am just looking for some more experienced programmers to criticize this work, offer insight to the problem, and feedback as to how I can optimize my workflow or utilize other techniques to make cleaner more efficient code.

Thanks for reading, and thanks in advance for any advice!

I love it! So silly.

I’m a little unclear on your question above… I think I lost you right at this early point:

I assume the remainder of your discussion is to get around “some weird things” but I cannot imagine what those might be.

Now, I’ve made a lot of games but never a cow abduction game. But I have picked up things with helicopter lines and tractor beams and all kinds of similar mechanisms. Based on my own study of bovine abduction science, it would seem all you need is a raycast below the saucer and when you hit a cow, consider him “that one is mine” until he moves a certain distance away, so that you’re ready to lift him up as soon as the user says to.

If the saucer and its tractor ray is vertical always, you could also just take the vector different between your ship and each cow, asking if the Y difference is within a certain amount (eg, you’re low enough) and the magnitude of the remaining X/Z vector is small enough to latch on, or choose the cow with the smallest X/Z difference to your position.

1 Like

Thanks man!

So, the “weird things happen” comment wasn’t actually a proven statement, I should have clarified. When I was planning ahead, I tried to think of what I EXPECT to happen in different cases (shown by the crude paint drawing) which ultimately led me to the decision of plotting the heights of the vertices on each mesh so that when I go to draw the Gizmos they draw appropriately, accurately reflecting the zone in which cows are considered targets.

What I’d ultimately like to happen is the blue spheres here –
7975587--1023519--circle_feedback.png

to act like the drawing here -

7975587--1023522--desired_outcome.png
I’m chipping away at it I think…

LOL!

I found that actually hitting the cows with a single raycast is difficult, you have to be very precise, so I was trying to check a cone area.

That is actually an interesting idea though, a two part fix. Not only could I check the distance after “that one is mine” but I could just use a similar approach to gather all gameobjects tagged as cows that have a Vector3.magnitude from the raycast point within a limit. Thanks, man!

Basically this. Yes. This. Seems so much simpler than trying to put those nodes in the correct position then casting a checksphere on each of them…

Added one if statement real quick before I start working on the alternate approach discussed above. I think the effect is pretty cool.

1 Like

Hey @Kurt-Dekker , not to raise the dead, but if you WERE to program the gizmos to respond to the mesh under them, how would YOU go about that?

Anyone else’s opinions?

Raycast down and draw only that far… it’s easier than you think. See enclosed. :slight_smile:

Once you start sprinkling gizmos all over your projects you will wonder how you ever reasoned about your levels without having gizmos all over the place.

7994058--1027401--Screen Shot 2022-03-25 at 8.45.59 AM.png

7994058–1027395–ConicalSensingGizmo.unitypackage (5.13 KB)

1 Like

Ahh, see I was trying to only use a single Raycast… I was coming right down the middle, mapping the vertices of the ground I hit within a specified area, then trying to set the height of the gizmos to those vertex heights.

Your way is so much simpler. :slight_smile:

1 Like

This is the highest compliment you can pay to an engineer. The fewer moving parts the better!

I kinda hacked that script together, let me know if any part of it is mysterious. :slight_smile: