Arranging Objects/Spheres Along A Circle Overlapping Issue

Hi, after some researching on the internet, I got some C# code that lets me generate a dynamic circle formation based on the units selected, circles come in different sizes as there are different units (tanks, infantry) with different sizes, the code works, but units with larger sizes seems seems to overlap and when there are few units like around 4 or 6 active, the generated circles/spheres starts overlapping alot, can’t seem to find a way to pass this >_<, but I think I already kinda understand what I’m doing wrong, its generating the circle formation radius, maybe I’m doing it wrong?

script/code:


[System.Serializable]
public class UnitFormationPoint
{
    public Vector3 point;
    public float radius;
    public float angle;
}
[System.Serializable]
public class UnitCirclePoint
{
    public Vector3 point;
    public float radius;
    public float angle;
}

public class TestFormationGeneratorV3 : MonoBehaviour
{

    public List<UnitFormationPoint> formationPoints = new List<UnitFormationPoint>();

    // thinginfo is the base monobehaviour script which all units has, and AIThingInfo stores the nav mesh agent
    public List<ThingInfo> expThings = new  List<ThingInfo>();
    public float mainRadius;

    public float PIMultiply = 2;

    public List<GameObject> testGameObjects = new List<GameObject>();
    // Start is called before the first frame update
    void Start()
    {
        

        GenCircles();
    }
    void GenCircles()
    {
        expThings.Clear();

        
        foreach (AIThingInfo _aiThing in GameObject.FindObjectsOfType<AIThingInfo>())
        {
            if (_aiThing.gameObject.activeSelf)
            {
                expThings.Add(_aiThing.GetComponent<ThingInfo>());
            }

        }


        foreach (GameObject _go in testGameObjects) { GameObject.Destroy(_go); } testGameObjects.Clear();
        formationPoints.Clear();


        float _radius = 0;
        foreach (ThingInfo _expThing in expThings)
        {
            _radius += _expThing.aiThingInfo.agent.radius;
        }
        GenerateCircles(expThings, transform.position, _radius / (Mathf.PI * PIMultiply), 0, expThings.Count);
    }
    // Update is called once per frame
    void Update()
    {
        PlayerController player = PlayerController.Get; 

        //formationPoints = AIDefs.GenerateFormationRectangle(player.selectedThings, transform.position, transform.forward);
        //formationPoints = AIDefs.GenerateFormationCircle(player.selectedThings, transform.position, transform.forward);

        //Debug.Log($"PI Value: {Mathf.PI * 1}");
        if (Input.GetKeyDown(KeyCode.T))
        {
            //CircledCircle(transform.position.x, transform.position.z, 100, 0.1f, 100, 1, 10);
            
        }
        GenCircles();
    }

    //-----------------------------------------------------------------------------------------------------------------
    void GenerateCircles(List<ThingInfo> _things, Vector3 position, float radius, float margin, int circleCount)
    {
        List<ThingInfo> things = new List<ThingInfo>();
        things.AddRange(_things);
        List<ThingInfo> things_assigned = new List<ThingInfo>();

        // to store circles
        List<UnitCirclePoint> circles = new List<UnitCirclePoint>();

        // circle half count
        int halfCount = Mathf.FloorToInt(circleCount / 2);

        // current angle & total angle
        float current_angle = 0;
        float total_angle = 0;


        // add circles() - top
        for (int i = 0; i < halfCount; i++)
        {
            if(things.Count > 0)
            {
                ThingInfo thing = things[0];

                // get thing radius & do a calculation
                float thing_radius = thing.aiThingInfo.agent.radius;
                float thing_angle = Mathf.Atan2(2 * thing_radius + margin, radius);

                // add circle
                circles.Add(new UnitCirclePoint
                {
                    radius = thing_radius,
                    angle = current_angle + thing_angle / 2
                });
                current_angle += thing_angle;

                // add thing to assigned things & remove it from the default list
                things_assigned.Add(things[0]);
                things.RemoveAt(0);
            }
        }
        total_angle = current_angle;

        // re-center top circles
        float correction = total_angle / 2 + Mathf.PI / 2;
        for (int i = 0; i < halfCount; i++)
        {
            circles[i].angle -= correction;
        }




        current_angle = 0;
        total_angle = 0;


        // add circles() - bottom
        for (int i = 0; i < (circleCount - halfCount); i++)
        {
            if (things.Count > 0)
            {
                ThingInfo thing = things[0];

                // get thing radius & do a calculation
                float thing_radius = thing.aiThingInfo.agent.radius;
                float thing_angle = Mathf.Atan2(2 * thing_radius + margin, radius);

                // add circle
                circles.Add(new UnitCirclePoint
                {
                    radius = thing_radius,
                    angle = current_angle + thing_angle / 2
                });
                current_angle += thing_angle;

                // add thing to assigned things & remove it from the default list
                things_assigned.Add(things[0]);
                things.RemoveAt(0);
            }
        }
        total_angle = current_angle;

        correction = total_angle / 2 - Mathf.PI / 2;
        for (int i = halfCount; i < circleCount; i++)
        {
            circles[i].angle -= correction;
        }


        // create formation points
        formationPoints.Clear();
        foreach(UnitCirclePoint _circle in circles)
        {
            float circleX = position.x + radius * Mathf.Cos(_circle.angle);
            float circleZ = position.z + radius * Mathf.Sin(_circle.angle);

            formationPoints.Add(new UnitFormationPoint
            {
                point = new Vector3(circleX, position.y, circleZ),
                radius = _circle.radius
            });

            GameObject testSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            testSphere.transform.position = new Vector3(circleX, position.y, circleZ);
            testSphere.transform.localScale = new Vector3(_circle.radius * 2, _circle.radius * 2, _circle.radius * 2);
            testGameObjects.Add(testSphere);
        }
    }

    private void OnDrawGizmos()
    {
        if(formationPoints.Count == 0) { return; }

        foreach(UnitFormationPoint _circle in formationPoints)
        {
            Gizmos.DrawSphere(_circle.point, _circle.radius);
        }
    }
}

topics explored:

https://stackoverflow.com/questions/25470730/position-different-size-circles-around-a-circular-path-with-no-gaps
https://discussions.unity.com/t/how-to-calculate-required-radius-of-a-circle/586886/5
etc...

Your thingAngle is for a given thing and you advance around the circle by that.

This would only happen to work if all thingAngles were the same angle.

When each is differnt, you need to advance by half of the previous thingAngle and half of the current thingAngle, right?

woah it worked, added tons of units and one with a larger noticeable radius and it doesn’t seem to overlap at all, dang, thanks but there is still a issue, and this might be because of the algorithm that I took from this post: javascript - Position different size circles around a circular path with no gaps - Stack Overflow, because how the circle halfs are generated

the issue is when there is a few amount of units with larger radius, they again overlaps :pensive:, but tested and only happens when there is a few amount of units, I’m pretty sure its because of the algorithm but I’m not really a math expert and would be cool if ur throwing another bonus tip, again thx :slight_smile:

few units:
image

EDIT: just noticed its still having some issues

EDIT: works fine when there is tons of units

EDIT: I guess this is because I’m separately generating top half and bottom half in two loops, gonna try out something new tomm

It’s a little bit tricky: Remember that you’re actually doing an approximation here:

By using the radius of each sphere and a single arctan, you are pretending that the section of big circle you are on is actually a straight line between the two spheres. Of course it is actually curving around.

That’s likely why your larger spheres are “eating” some of the adjacent spheres, as the curvature of your big circle is still really a thing, so they get closer together and overlap.

Because the more units you have, the closer the big circle becomes to actually being a line (from the local perspective of the spheres), so the error gets smaller.

To get it “perfect,” you need to map the sum of the two different sphere radii to the amount of angle you must subtend going around the big circle (two slender back-to-back right triangles), given that distance mapping not to a circumferential distance but rather the chord between those two points, something like this:

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

// @kurtdekker
//
// randomly-sized spheres going around a circle, each sphere just "kissing" adjacent spheres
//
// to use; slap this on a GameObject and press PLAY

public class SpheresAroundACircle : MonoBehaviour
{
	float min_random_radius = 0.1f;
	float max_random_radius = 6.0f;

	float big_circle_radius = 20;

	void Start ()
	{
		// degrees
		float heading = 0;

		// we'll spin it around +Y with heading 0 mapped to +Z

		float radius1 = 0;

		while(true)
		{
			// pick a random radius
			float radius2 = Random.Range( min_random_radius, max_random_radius);

			// the full chord between two different-radii spheres:
			float combined_radius = radius1 + radius2;

			// half of the chord (this will be the "opposite" of one of our two triangles)
			float half_combined = combined_radius / 2;

			// we have the big circle radius (hypotenuse), we have opposite, that gives us the sine:
			float sine = half_combined / big_circle_radius;

			// bad arcsin domain, sphere radii too big!
			if (sine < -1) break;
			if (sine > +1) break;

			// so now we use arcsin to get one half of the angle
			float radians = Mathf.Asin( sine);

			// double it because we have two slender triangles back to back
			radians = radians * 2;

			// back to degrees
			float degrees = radians * Mathf.Rad2Deg;

			// advance full chord distance for both spheres
			heading += degrees;

			// stop!
			if (heading >= 360 - degrees / 2) break;

			// make our sphere (which will be 0.5f radius!)
			GameObject sphere = GameObject.CreatePrimitive( PrimitiveType.Sphere);

			// since sphere primitive is 0.5 radius, we must apply radius x 2 for scale
			sphere.transform.localScale = Vector3.one * radius2 * 2;

			// raw unrotated radius of large circle (what we consider "heading zero")
			Vector3 position = Vector3.forward * big_circle_radius;

			// rotate it around +Y axis
			position = Quaternion.Euler( 0, heading, 0) * position;

			// TODO add a center offset in here if you like
//			position = position + center;

			sphere.transform.position = position;

			// age our radius
			radius1 = radius2;
		}
	}
}
3 Likes

lol at first I wondered why you put a infinite loop in the start method until I saw those breaks xD, many thanks again! :slight_smile: for whipping up such a nice clean code with comments, I’m not much of a math guy but I’m working on my math to learn Computer Science, and… don’t mind me modifying your code to dynamically assign the big circle radius :eyes:

P.S: also would have honestly donated :dollar: if there was an option in this site since this is one of those obligatory features in my wip rts game

1 Like