Search Unity

Randomly Place Cubes In Viewport Without Overlap

Discussion in 'Scripting' started by poolts, Aug 12, 2013.

  1. poolts

    poolts

    Joined:
    Aug 9, 2012
    Posts:
    113
    Hi all,

    Been racking my brain on this for ages.

    I want to randomly place cubes in the viewport, without them overlapping each other (but not based on pre-defined spawn points).

    So first step was to randomise their position in the viewport which is done here:

    Code (csharp):
    1. Camera.main.ViewportToWorldPoint(new Vector3(Random.value, Random.value, (Camera.main.transform.position.y - t.position.y) - 5f));
    Now I've tried to iterate through the collection of cube transforms and check the a potential random position is close to another others:

    Two ways (both which haven't worked are) to use a Physics.Overlap at the position to check if there are any colliders there (aka other cubes), if there are try and new position, if not rinse and repeat.

    The other way was to check the potential random position and then check the magnitude between all other places buildings to see if you are close, if so do it again, if not place it down.

    Both of these seemed good solutions, but the physics overlap seems a bit heavy and doesn't seem to be returning any colliders (even though the cubes have box colliders) and then other solution, is 2 nested for loops, which (probably just my logic) don't seem to be working.

    Any help would be greatly appreciated.
     
  2. 12doze12

    12doze12

    Joined:
    Mar 17, 2013
    Posts:
    57
    What I think you can do is obviusly randomly generate the 2 coordinates and then add those two coordinates as exceptions when you randomize another 2 coordinates. If the cubes are big you just have to add half of the size of the cube in both coordinates as exception.

    Sorry, no time to code now :D
     
  3. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    Or you could give every cube a script that's checking its OnCollision or OnTriggerEnter and then delete themselves if they match the gameobjects for example tag "Cube"
     
  4. poolts

    poolts

    Joined:
    Aug 9, 2012
    Posts:
    113
    @12doze12 Not sure if that'd work, if the Vector3's are even 2 sf apart then it would place the cube inside the other cube. If you then offset that cube, you may offset it, into another. I think two nested for loops to check the distance between them is the right solution, just can't get my head around the 2nd for loop (think I need some fresh eyes)

    @asd234w4r5 That wouldn't work as you'd end up with less cubes if the colliders overlapped. Also the extra overhead to detect the collisions and delete at run time, wouldn't be needed :)

    Here's my code so far, if someone can set me straight.

    Code (csharp):
    1.  using UnityEngine;
    2.     using System.Collections;
    3.     using System.Collections.Generic;
    4.    
    5.     public class BuildingManager : MonoSingleton<BuildingManager> {
    6.        
    7.         [SerializeField]
    8.         Transform[] m_buildings;
    9.        
    10.         List<Vector3> placedBuildings = new List<Vector3>();
    11.            
    12.         //------------------------------------------------------------------------------------------------------
    13.        
    14.         void Awake()
    15.         {      
    16.             // Place initial building
    17.             m_buildings[0].position = GetRandomTerrainPos(m_buildings[0]);
    18.            
    19.             m_buildings[0].position = new Vector3(m_buildings[0].position.x, 0f, m_buildings[0].position.z);
    20.                            
    21.             placedBuildings.Add(m_buildings[0].position);
    22.            
    23.             // Place the other buildings
    24.             // Start index at 1
    25.             for(int i = 1, n = m_buildings.Length; i < n; i++)
    26.             {  
    27.                 Transform t = m_buildings[i];
    28.                
    29.                 // Get a random position
    30.                 Vector3 randomPos = GetRandomTerrainPos(t);
    31.                    
    32.                 // Check if this position is near any others
    33.                 for(int j = 0, k = placedBuildings.Count; j < k; j++)
    34.                 {  
    35.                     // If the distance between the potential random position and
    36.                     // the placed building is more than 2f (magic number) place
    37.                     // the building there (this is a sutiable location)
    38.                     if((randomPos - placedBuildings[j]).magnitude > 2f)
    39.                     {
    40.                         t.position = randomPos;
    41.                
    42.                         t.position = new Vector3(t.position.x, 0f, t.position.z);
    43.                        
    44.                         placedBuildings.Add(t.position);
    45.                     }
    46.                     // Else look for another location
    47.                     else
    48.                     {
    49.                     }
    50.                 }
    51.             }
    52.         }
    53.        
    54.         //------------------------------------------------------------------------------------------------------
    55.        
    56.         Vector3 GetRandomTerrainPos(Transform t)
    57.         {
    58.             return Camera.main.ViewportToWorldPoint(new Vector3(Random.value, Random.value, (Camera.main.transform.position.y - t.position.y) - 5f));
    59.         }
    60.     }
    61.    
     
  5. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    I am using a script like this to place some object in my scene. Didn't get any performance problems, but i understand what you mean :)
     
  6. lancer

    lancer

    Joined:
    Aug 1, 2013
    Posts:
    231
    Don't you have a Answers post for this too?
     
  7. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    I am placing my cubes on a sphere like this.

    Code (csharp):
    1. function handlePowerUpsDowns()
    2. {
    3.     for ( var k = 0; currentDrops < numberOfBlood; k++ )
    4.     {
    5.         instantiateBloodDrops();
    6.     }
    7. }
    8.  
    9. function instantiateBloodDrops()
    10. {
    11.     for (var i = 0; i < 1; i++)
    12.     {
    13.         var randomPoint = GetPointOnMesh();
    14.        
    15.         if( randomPoint.point.x < xRenderPosition - 2.5  randomPoint.point.x > -xRenderPosition + 2.5  randomPoint.point.y < -24 )
    16.         {
    17.             var gameObject = Instantiate(bloodDrop, randomPoint.point, Quaternion.FromToRotation (Vector3.up, randomPoint.normal));
    18.             gameObject.transform.parent = world.transform ;
    19.             gameObject.transform.rotation.z = 0 ;
    20.            
    21.             currentDrops++;
    22.         }
    23.         else
    24.         {
    25.             i = i - 1 ;
    26.         }
    27.     }
    28. }
    and if one cube is overlapping it has a script that checks and deletes the cube if necessary and counts down currentDrops and again instatiates

    Code (csharp):
    1. function OnTriggerEnter(collider:Collider)
    2. {
    3.     var currentDrops = GameController.GetComponent(LevelCreator_JS).currentDrops ;
    4.     GameController.GetComponent(LevelCreator_JS).currentDrops = currentDrops - 1 ;
    5.     GameController.GetComponent(LevelCreator_JS).instantiateBloodDrops();
    6.    
    7.     Destroy(bloodDropContainer);
    8. }
     
  8. Mister-E2

    Mister-E2

    Joined:
    Jun 6, 2013
    Posts:
    179
    If you are positioning in screenspace, then I would simply fill a List<Vector3> with every position on the screen that the cube could be without overlapping. So, by this I mean start at the bottom-left, get the Screen.Width, determine how many of your cubes can fit into the entire screen width side by side, record those positions, and do the same for the next "row".

    So you basically make a grid of positions on screen. Then, to set a random position, you would use a Random.Range(0,positionList.Count); and the random position will be taken using the random index that is generated....To then make sure the position cannot be reused, just remove that index from the List after you have set the position, meaning the next time you generate the number, the previous position will not be part of it.

    I hope that made sense.
     
  9. poolts

    poolts

    Joined:
    Aug 9, 2012
    Posts:
    113
    @Lancer yep keeping all my bases covered ;)
     
  10. poolts

    poolts

    Joined:
    Aug 9, 2012
    Posts:
    113
    @Mister-E2 Yeah that'd work, although I'd be limited to a Manhattan style structure. I thought about having a grid based system, where each quadrant is a suitable place to to place the cube and then randomly offsetting the building inside that quadrant, so the buildings don't look too "Manhattan" style.
     
  11. poolts

    poolts

    Joined:
    Aug 9, 2012
    Posts:
    113
    @asd234w4r5

    I went with your solution but instead of deleting and removing I just recalculated a random position until the the Trigger didn't have a collider of a another cube in it.

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Building : MonoBehaviour {
    5.    
    6.     Transform m_cachedTransform;
    7.    
    8.     void Awake()
    9.     {
    10.         m_cachedTransform = transform;
    11.     }
    12.    
    13.     void OnTriggerStay(Collider c) 
    14.     {
    15.         if(c.CompareTag("Building"))
    16.         {
    17.             m_cachedTransform.position = BuildingManager.Instance.GetRandomTerrainPos(m_cachedTransform);
    18.         }
    19.     }
    20. }
    21.  
    22.  
    It's not the greatest way to procedurally generate a city, but it serves it's purpose for this little demo. In the future I'd probably go for a quadrant / grid based system where builds are offset in each quadrant to randomise the look. Also I'll cache the transforms for better perform
     
  12. zombie_psy

    zombie_psy

    Joined:
    Oct 9, 2012
    Posts:
    62
    I replied to this answer on another Unity Answers thread, but since this is the same question, I will repeat it here

    I took the time to go through the various solutions, brainstormed, then coded this. This solution requires just one **for** loop and one nested **for** loop:

    Code (csharp):
    1.     var ObjectToInstantiate : GameObject;
    2.     var ObjectToPlotOnGrid : GameObject[];
    3.     var pointThatHasBeenTaken : Vector2[];
    4.    
    5.    
    6.         for(var i=0;i<20;i++) {
    7.             ObjectToPlotOnGrid[i] = Instantiate(ObjectToInstantiate);
    8.             ObjectToPlotOnGrid[i].transform.position.x = Screen.width/20*Random.Range(0,20);
    9.             ObjectToPlotOnGrid[i].transform.position.y = Screen.width/20*Random.Range(0,20);
    10.             pointThatHasBeenTaken[i]= Vector2(ObjectToPlotOnGrid[i].transform.position.x, ObjectToPlotOnGrid[i].transform.position.y);
    11.            
    12.             for(var ii = 0; ii < i; ii++) {
    13.                 if(Vector2(ObjectToPlotOnGrid[i].transform.position.x,ObjectToPlotOnGrid[i].transform.position.y ) == pointThatHasBeenTaken[ii]) {
    14.                     print("Rerolling spawn");
    15.                     ObjectToPlotOnGrid[i].transform.position.x = Screen.width/20*Random.Range(0,20);
    16.                     ObjectToPlotOnGrid[i].transform.position.y = Screen.width/20*Random.Range(0,20);
    17.                     ii--;
    18.                 }
    19.             }
    20.         }
    In this script:

    1) the screen is divided into a 20x20 grid

    2) a for loop is iterated from 0-19 plotting random points in that 20x20 grid

    3) within that for loop, the nested for loop re-iterates all points already taken before it is plotted

    4) If the nested **for** loop finds that the point is already taken, it is re-rolled another random roll, and subtracts from the nested for's count

    Note, since these are Unity Built-In Arrays, right after you declare those arrays, you have to set the Array's length on the Unity Editor.
     
    Last edited: Oct 16, 2013
  13. Toerktumlare

    Toerktumlare

    Joined:
    Sep 15, 2013
    Posts:
    74
    i have one this for level editing my method that i found was good because you dont have to "re randomize" stuff is that lets say we have grid that is 20x20.

    that gives a total of 400 possible locations for an object to be placed.

    to garantee that no object will be placed on the same position as any other is to first generate a List with every possible location. So i created an List() filled it with default values:

    List<int> x = new List<int>(new int[count]);

    after that i took my list and random shuffled it with the fischer yates shuffle so i have a random "deck" of numbers. everytime i pull a number, it will be random, and when that number is pulled it is gone, and you can't pull that number again.

    then i do a check to see when the list is empty, when its empty it automatically fills itself again with new numbers. like a deck of cards but with random numbers.

    its like a random number generator but with only one of each number.