So I actually have posted this a couple of places and am going to try my luck here.
First I apologize for the very long post – just wanted to be thorough. There is a TL;DR at the end if you just want to look at that and the code.
A friend and I are working on a game that starts off by randomly spawning some planets into a scene. Further, we would like to spawn each object such that it does not touch or overlap with any other already placed game object (planet). We have scoured the forums and other resources and tried many different things but no matter what, even if it works perfectly several times in a row, there will always eventually be a layout where planets overlap. First, some code:
public class Game_Controller : MonoBehaviour
{
public List<GameObject> planets;
public GameObject planetPrefab;
public float MinY = -120;
public float MinX = -500;
public float MaxX = 600;
public float MaxY = 640;
float x;
float y;
float sizeX;
float sizeY;
float mass;
float rotation;
Vector3 position;
Vector3 size;
private void Start()
{
CreatePlanets();
}
void CreateRanges()
{
//Creates random position for planets, stored in "position"
x = Random.Range(MinX, MaxX);
y = Random.Range(MinY, MaxY);
position = new Vector3(x, y, 0);
rotation = Random.Range(0, 360);
//Creates random sizes for planets transform.scale
sizeX = Random.Range(10, 70);
sizeY = sizeX;
size = new Vector3(sizeX, sizeY, 1);
//Check to see if any planets will collide with this position. This is where we are having issues.
position = CheckPlanetPosition(position, (sizeX*2));
mass = Random.Range(100, 2000);
}
// CreatePlanets() generates and instantiates the planets
void CreatePlanets()
{
for (int i = 1; i <= 15; i++)
{
//Debug.Log(i);
CreateRanges();
GameObject newPlanet = Instantiate(planetPrefab, position, Quaternion.Euler(0, 0, rotation));
newPlanet.name = "Planet " + i;
newPlanet.transform.localScale = size;
newPlanet.GetComponentInChildren<Rigidbody2D>().mass = mass;
planets.Add(newPlanet);
}
}
private Vector3 CheckPlanetPosition(Vector3 oldPos, float sizeX)
{
Vector3 newPos;
//Returns an array of all the colliding planets
Collider2D[] colliders = Physics2D.OverlapCircleAll(oldPos, sizeX);
//if the array isn't empty, choose a new Vector3 and check that position
//Else the position is valid and the planet is placed there
if (colliders.Length > 0)
{
x = Random.Range(MinX, MaxX);
y = Random.Range(MinY, MaxY);
newPos = new Vector3(x, y, 0);
newPos = CheckPlanetPosition(newPos, sizeX);
}
else
{
newPos = oldPos;
}
return newPos;
}
private void OnDrawGizmos()
{
//Gizmos.DrawWireSphere(Vector3.zero, 1);
foreach (GameObject planet in planets)
{
Gizmos.DrawWireSphere(planet.transform.position, planet.transform.localScale.x);
}
}
}
This is the main chunk of code we’re dealing with. The flow is:
- At start run CreatePlanets()
- CreateRanges() is called, which sets the planet spawn points (position) and planet size (sizeX,SizeY,size).
- CheckPlanetPosition is called, which takes the position we just generated, and size of the x component of the transform * 2 (more on this later)
- In CheckPlanetPosition() we pass the position as oldPos and the adjusted size as sizeX.
- We give our oldPos (center point) and sizeX (radius) and check to see if CircleOverlapAll() finds any colliders in the drawn circle. If no, oldPos is fine (move to else and set position). If there are colliders found, we give it a new x and y position, assign it newPos and then recursively call CheckPlanetPosition again with the returned newPos value.
- Note that the Gizmos.DrawWireSphere is set to draw the same bounds we expect to see visually on screen that should be checked in CheckPlanetPosition(). Its for debugging and has no bearing on function.
This code works most of the time. But here will still be overlaps generated. We have also tried:
-
Using Physics2D.OverlapCircle also, testing each position individually
-
Using other Collider2D/Physics2D checks
-
Generating the list of planets with positions and sizes first, then iterating through the list (both using simple foreach and recursive method used above) and running the position check on each item in the list.
-
Changing quite literally every relevant value (range in scene where planets can spawn, size values, scale values, collider values, radius, etc) and testing differences in the behavior.
Yet every time, we still get results like this:
(note again that the white lines are the drawn wire-sphere that represent the bounds of the collider – just for visual debugging)
We believe the issue has to do with the value of the radius. You’ll observe above that we set the radius to be the size of the circles transform x component multiplied by two (sizeX * 2). We did this after reading some articles on how the radius is calculated and got a ton of different answers saying things like"Radius should be planet’s transform.localscale.x * 2" or “Radius should be planet’s scale * 2DCollider.bounds.extents.x”.
Mathematically, the radius of the circle we’re generating should be the planet’s transform.localscale.x / 2 right? The x component is the length from end to end of the x-axis, and the radius of that would be half the size. Yet none of those equations gave us accurate results. They all generate planets that overlap and we cannot figure out why.
We are stuck and would truly appreciate any help. This is a last ditch effort considering we’ve seen 100s of posts/tutorials/docs that cover this material and we figured not many people would want to answer. So please and thank you to anyone who can help us tackle this!
TL;DR
We are trying to spawn circular game objects at start without overlapping. We have tried a ton of solutions but the results stay roughly the same. In the code:
- CreatePlanets() is called to start the process
- CreateRanges() generates random positions and sizes for each circle
- CheckPlanetPosition() is the method used to recursively test to see if there is overlap in the area the next game object (circle) will be placed.
Why do our circles still overlap?