2

I'm trying to randomly generate blocks on a flat map and make it so that they don't overlap each other. I have made a matrix (c# array) of the size of the map (500x500), the blocks have a scale between 1 and 5. The code works but if a generated block overlaps another one, it is destroyed and not regenerated somewhere else.

Only around 80 of the 1000 blocks I try to generate don't overlap another block.

Here is a picture of the map with around 80 blocks generated, the green squares are blocks

Map

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
    bool elementFound = false;
    for (int i = 0; i < ratio * generationDefault; i++) {
        GameObject el;
        // Randomly generate block size and position
        int size = Random.Range(minScale, maxScale + 1);
        int x = Random.Range(0, mapSizex + 1 - size);
        int y = Random.Range(0, mapSizey + 1 - size);

        // Check if there is already an element 
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)
                if (map[j][k] != null)
                    elementFound = true;
        if (elementFound)
            continue;
        else {
            el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
            el.transform.localScale *= size;
        }
        // Create element on map array
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)  
                if (map[j][k] == null) {
                    map[j][k] = el.GetComponent<ObjectInterface>();
                }
    }
}

I thought of 3 possible fixes

  • I should set the size of the block depending of the place it has.
  • I should use another randomization algorithm.
  • I'm not doing this right.

What do you think is the best idea ?


UPDATE

I got the code working much better. I now try to instantiate the blocks multiple times if needed (maximum 5 for the moment) and I fixed the bugs. If there are already many elements on the map, they will not always be instantiated and that's what I wanted, I just have to find the right amount of times it will try to instantiate the block.

I tried instantiating 1280 elements on a 500x500 map. It takes only about 1.5 second and it instantiated 1278/1280 blocks (99.843%).

enter image description here

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
bool elementFound = false;
int cnt = 0;
// Generate every block
for (int i = 0; i < ratio * generationDefault; i++) {
    GameObject el = null;
    // Randomly generate block size and position
    int size, x, y, tryCnt = 0;

    // Try maximum 5 times to generate the block
    do {
        elementFound = false;
        // Randomly set block size and position
        size = Random.Range(minScale, maxScale + 1);
        x = Random.Range(0, mapSizex + 1 - size);
        y = Random.Range(0, mapSizey + 1 - size);

        // Check if there is already an element 
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)
                if (map[j][k] != null)
                    elementFound = true;
        tryCnt++;
    } while (elementFound && tryCnt < 5);
    if (tryCnt >= 5 && elementFound) continue;

    // Instantiate the block
    el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
    el.transform.localScale *= size;
    // Create element on map array
    for (int j = x; j < x + size; j++)
        for (int k = y; k < y + size; k++)  
            if (map[j][k] == null) {
                map[j][k] = el.GetComponent<ObjectInterface>();
            }
    cnt++;
}
print("Instantiated " + cnt + "/" + ratio * generationDefault);

}

Elbbard
  • 2,064
  • 6
  • 30
  • 53
  • One way I can think of for this (though it'd require some extra math), is to split the map area into a grid of regions, equal to the number of blocks you want to put down. Then, randomly select a position in each region (taking into account the expected scale of the block) and place a block there. Unfortunately, you'll also run into the problem of "regularity" with this approach (especially as block number increases relative to map size)...depending on your needs though, a consistent average population density might not be so bad. =P – Serlite Feb 05 '16 at 16:43
  • Hi Serlite. That is a very well-known approach. Indeed, it's exactly what is in my answer below. Just as you astutely observe, you get a "regular looking" result depending on the sizes involved (sometimes it's fine). A simple solution is to perturb the blocks - code given below. Note! Indeed another approach is this simple .. just lay them all out evenly spaced (don't even randomize the positions) and then perturb. Just try it with 1, 2, 3 or more "perturbs" and see how it goes. – Fattie Feb 05 '16 at 17:02

3 Answers3

6

This is incredibly difficult to do well.

Here's a quick solution you'll maybe like ... depending on your scene.

actualWidth = 500 //or whatever. assume here is square
// your blocks are up to 5 size
chunkWidth = actualWidth / 5
// it goes without saying, everything here is an int
kChunks = chunkWidth*chunkWidth
List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();
howManyWanted = 1000
shuf = shuf.Take(howManyWanted)
foreach( i in shuf )
   x = i % actualWidth
   y = i / actualWidth
   make block at x y
   put block in list allBlocks

HOWEVER ............


...... you'll see that this looks kind of "regular", so do this:

Just randomly perturb all the blocks. Remember, video game programming is about clever tricks!

Ideally, you have to start from the middle and work your way out; in any event you can't just do them in a line. Shuffling is OK. So, do this ..

   harmonic = 3  //for example. TRY DIFFERENT VALUES
   
   function rh = Random.Range(1,harmonic) (that's 1 not 0)
   
   function rhPosNeg
       n = rh
       n = either +n or -n
       return n
   
   function onePerturbation
   {
   allBlocks = allBlocks.OrderBy(r => Random.value) //essential
   foreach b in allBlocks
      newPotentialPosition = Vector2(rhPosNeg,rhPosNeg)
      possible = your function to check if it is possible
           to have a block at newPotentialPosition,
           however be careful not to check "yourself"
      if possible, move block to newPotentialPosition
   }

The simplest approach is just run onePerturbation, say, three times. Have a look at it between each run. Also try different values of the harmonic tuning factor.

There are many ways to perturb fields of differently-sized blocks, above is a KISS solution that hopefully looks good for your situation.


Coding note...

How to get sets of unique random numbers.

Just to explain this line of code...

List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();

If you are new to coding: say you want to do this: "get a hundred random numbers, from 1 to million, but with no repeats".

Fortunately, this is a very well known problem with a very simple solution.

The way you get numbers with no repeats, is simply shuffle all the numbers, and then take how many you want off the top.

For example, say you need a random couple of numbers from 1-10 but with no repeats.

So, here's the numbers 1-10 shuffled: 3,8,6,1,2,7,10,9,4,5

Simply take what you need off the front: so, 3, 8, 6 etc.

So to make an example let's say you want twelve numbers, no repeats, from 1 through 75. So the first problem is, you want a List with all the numbers up to 75, but shuffled. In fact you do that like this ..

List<int> shuf = Enumerable.Range(1,75).OrderBy(r=>Random.value).ToList();

So that list is 75 items long. You can check it by saying foreach(int r in shuf) Debug.Log(r);. Next in the example you only want 12 of those numbers. Fortunately there's a List call that does this:

shuf = shuf.Take(12)

So, that's it - you now have 12 numbers, no repeats, all random between 1 and 75. Again you can check with foreach(int r in shuf) Debug.Log(r);

In short, when you want "n" numbers, no repeats, between 1 and Max, all you have to so is this:

List<int> shuf = Enumerable.Range(1,Max).OrderBy(r=>Random.value).ToList();
shuf = shuf.Take(n);

et voilà, you can check the result with foreach(int r in shuf) Debug.Log(r);

I just explain this at length because the question is often asked "how to get random numbers that are unique". This is an "age-old" programming trick and the answer is simply that you shuffle an array of all the integers involved.

Interestingly, if you google this question ("how to get random numbers that are unique") it's one of those rare occasions where google is not much help, because: whenever this question is asked, you get a plethora of keen new programmers (who have not heard the simple trick to do it properly!!) writing out huge long complicated ideas, leading to further confusion and complication.

So that's how you make random numbers with no repeats, fortunately it is trivial.

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Hello, I like the idea with the chunks but the problem is that I will generate 3-4 different types of blocks but I can probably adapt this code. For the perturbation, my blocks have different sizes (if the scale of a block is two, it will take 4 places in the map array) – Elbbard Feb 07 '16 at 10:16
  • 1
    Hi Serv. in the pseudo code where it says *harmonic = 3*, it will try each individual jiggle up to that max size. Just try different values. – Fattie Feb 07 '16 at 13:18
1

if (elementFound) continue; will skip out this current loop iteration. You need to wrap the int x=Random..; int y=Random()..; part in a while loop with the condition being while(/* position x/y already occupued*/) { /* generate new valid point */} like this for example:

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
    for (int i = 0; i < ratio * generationDefault; i++) {
        GameObject el;
        // Randomly generate block size and position

        bool elementFound = false;
        int size, x, y;
        do
        {
            elementFound = false;
            size = Random.Range(minScale, maxScale + 1);
            x = Random.Range(0, mapSizex + 1 - size);
            y = Random.Range(0, mapSizey + 1 - size);

            // Check if there is already an element 
            for (int j = x; j < x + size; j++)
                for (int k = y; k < y + size; k++)
                    if (map[j][k] != null)
                        elementFound = true;
        } while(elementFound);

        el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
        el.transform.localScale *= size;

        // Create element on map array
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)  
                if (map[j][k] == null) {
                    map[j][k] = el.GetComponent<ObjectInterface>();
                }
    }
}
Maximilian Gerhardt
  • 5,188
  • 3
  • 28
  • 61
  • The number of iterations is indeterminte, but it won't take much iterations for the algorithm to find a valid place, especially not a million iterations. It picks a random size, x and y and checks if that is valid in the map. If not, it picks another few numbers. In case of the map being full at every place, this is non-terminating. I leave it to the reader to add a check for that at the beginning of the algorithm. – Maximilian Gerhardt Feb 05 '16 at 16:18
  • However DO NOTE, just as I said, this IS WHAT YOU DO if the numbers are right .. basically if it's fairly sparse. But you have to be incredibly careful since it is an indeterminate algorithm. In fact, in your loop where you keep looking for one, YOU MUST have a safety count, and only try say 100 times at most (or some safe limit). – Fattie Feb 05 '16 at 16:25
  • Joe, take a look at this tutorialspoint project which I shared. http://goo.gl/llBa4l It simulates a 1000x1000 map and generates 200 random elements of random size on this map and loggs the number of iterations for you. As you can see, it nearly always needs 1 iteration, but you can also see some lines where it needed 2 iterations. You can generate more elements and you'll sometimes see that it needs 3 iterations. As you can see, this disproves your claim that it needs "a million iterations" to complete, the probability that the new random position and size is already full is very low. – Maximilian Gerhardt Feb 05 '16 at 16:36
  • And sure, it misses the safety locks incase the map is full or no object of the requested size can be placed on the map (e.g. maximum 20 iteration-tries), but it's fairly trivial to implement this. This just corrects the error that the `continue` skipped the iterations and didn't retry to generate an new random element. – Maximilian Gerhardt Feb 05 '16 at 16:38
  • You need to add the "loop safety limit" to your code seen on the internet. it's the most basic space-filling algorithm and you can't have it wrong in print! – Fattie Feb 05 '16 at 16:39
  • Still, if you let the algorithm run on the reduced map size of 500x500 and let it generate 10,000 blocks, the worst number of iterations I get is still 35, in the median it should be something like 3. That's appropiate for that map size and the number of elements to be generated. – Maximilian Gerhardt Feb 05 '16 at 16:40
  • I think the OP said 1,000 blocks not 10,000? – Matthew Watson Feb 05 '16 at 16:45
1

You shouldn't be getting that many collisions.

Assuming your blocks were ALL 5 units wide and you're trying to fit them into a grid of 500,500 you would have 100*100 spaces for them at minimum, which gives 10,000 spaces into which to fit 1,000 blocks.

Try playing around with this code:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var result = PlaceNonOverlappingBlocks(1000, 5, 500, 500);
        }

        static List<Block> PlaceNonOverlappingBlocks(int count, int maxBlockSize, int mapX, int mapY)
        {
            var map    = new bool[mapY, mapX];
            var rng    = new Random();
            var result = new List<Block>(count);
            int collisions = 0; 

            while (count > 0)
            {
                int size = rng.Next(1, maxBlockSize + 1);
                int x = rng.Next(0, mapX - size);
                int y = rng.Next(0, mapY - size);

                if (fits(map, x, y, size))
                {
                    result.Add(new Block(x, y, size));
                    addToMap(map, x, y, size);
                    --count;
                }
                else
                {
                    if (++collisions> 100000)
                        throw new InvalidOperationException("Hell has frozen over");
                }
            }

            // This is just for diagnostics, and can be removed.
            Console.WriteLine($"There were {collisions} collisions.");

            return result;
        }

        static void addToMap(bool[,] map, int px, int py, int size)
        {
            for (int x = px; x < px+size; ++x)
                for (int y = py; y < py + size; ++y)
                    map[y, x] = true;
        }

        static bool fits(bool[,] map, int px, int py, int size)
        {
            for (int x = px; x < px + size; ++x)
                for (int y = py; y < py + size; ++y)
                    if (map[y, x])
                        return false;

            return true;
        }

        internal class Block
        {
            public int X    { get; }
            public int Y    { get; }
            public int Size { get; }

            public Block(int x, int y, int size)
            {
                X = x;
                Y = y;
                Size = size;
            }
        }
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276