0

I'm trying to improve my method of laying out the position of thousands of shapes with an equal footprint over a grid surface from a central point (it has a more organic look). The grid surface means that there's equal spacing between neighbours but I don't want to create a patch-work like pattern by picking a random x and y co-ordinate (or x and z in this case as I'm working in 3 dimensions). What I have now works but is incredibly slow when I start moving up from 2,000 objects. Is there a faster method? I'm avoiding the patch-work effect, like I said, as well as an even circular spread. The distribution right now is very city-like and perfect, but far too slow.

I've commented the function throughout so hopefully it explains everything thoroughly:

ArrayList buildingCoords; // stores the co-ordinates of occupied spaces
int w = 50 // Footprint dimensions. Width and depth.

PVector generateNewBuildingPosition(PVector coord) {
    float sW = 30; // gap between shapes
    // Starting at a coordinate of 0,0 if this is our 1st go
    // (PVector coord initially feeds 0,0)
    // or the last coordinate that was already taken
    // (this loops with the last coordinate if it fails)
    // we check one of the four spaces next to us with
    // the roll of a dice (in a way...)
    float randomX = random(0,15);
    float randomZ = random(0,15);
    if (randomX >= 10) { 
      randomX = w + sW; 
    } else if (randomX >= 5) { 
      randomX = (w + sW) * -1;
    } else {
      randomX = 0;
    }
      if (randomZ >= 10) { 
      randomZ = w + sW; 
    } else if (randomX >= 5) { 
      randomZ = (w + sW) * -1;
    } else {
      randomZ = 0;
    }

    // We've picked our direction.
    // We have a PVector that acts as a marker for where we're
    // placing. Adding or subtracting the movement of each
    // attempt, one at a time, means the shapes spreads out 
    // more organically from the centre rather than trying
    // to distribute each shape as a random patch.
    PVector newDirection = new PVector(randomX, 0, randomZ);
    coord.add(newDirection);
    // Our marker moves to the new spot, we check if it exists.
    // If it doesn't, we confirm it as this shape's anchor spot.
    // If it does, we loop this function again, feeding it where our
    // marker is.
    if(buildingCoords.contains(coord)) {
      generateNewBuildingPosition(coord);
    } else {
      // add this PVector to the arrayList
      buildingCoords.add(coord);
    }
    // Return the coordinates that just succeeded.
    return coord;
  }
biscuitstack
  • 11,591
  • 1
  • 26
  • 41
  • Apologies, I missed that there was one external variable. I've included it in the original now but it's the width and depth of the footprint of each shape. – biscuitstack Mar 17 '15 at 10:35
  • I suppose it slows down because you make a recursive call if `buildingCoords` contains the calculated `coord`. What you could do is change your algorithm so it only considers the empty spaces in it's calculation so you never have to check if the calculated coord is already in use. Ergo: set up a search space of "available" locations (this can grow dynamically to keep things bounded) and select one of them using some logic to make it "semi-random", update the available locations and proceed to the next object. – RDM Mar 17 '15 at 10:39
  • Can we see the code for: buildingCoords.contains I have a general idea of what you issues might be. – Michael Hobbs Mar 17 '15 at 10:39
  • 1
    @Micheal Hobbs: that's just the default code for checking if an ArrayList contains a certain object. This becomes slower the larger the list is. – RDM Mar 17 '15 at 10:40
  • @Warkst. Forgive me for not being too bright on this, but the only way I can think of checking for available spaces is to keep a list of taken spaces. Or do you mean have a single boundary shape? Is that possible? And yes, I assumed the slowdown was due to having to search a growing ArrayList but was stumped thinking of a faster way of accomplishing this. – biscuitstack Mar 17 '15 at 10:44
  • Do you want the shapes to be "radiating out" from a central position? In that case polar coordinates would probably be easier. Basically you just randomise the direction of the next building and drop it in the closest available spot in that direction. – biziclop Mar 17 '15 at 10:45
  • 1
    There are different ways of doing this. If you want your structure to grow from the center outwards, you could use a bounding shape (a bounding box would be easiest), but in fact it comes down to keeping a list of **non-taken** spaces, which you update each time you take a space. As I mentioned, to keep that list from exploding in memory, you could grow it dynamically, meaning you start with some empty spaces and add new ones (e.g. neighbours in a certain radius) when you take a space. – RDM Mar 17 '15 at 10:45
  • @biziclip. I'm not in the best position to be picky when I'm seeking help, am I? ;) The answer is maybe. I'll certainly test it and see if it produces desirable results. Anything that looks too artificially random is not good but the main result is to grow outwards from a central point. e.g. if the method you mention looks elliptical with only some variations to the perimeter it would not be good. – biscuitstack Mar 17 '15 at 10:48
  • @Warkst. That's an interesting idea. In theory, the available space is infinite, providing it a neighbour of a taken space. But I see your point now, it would only contain known neighbours. – biscuitstack Mar 17 '15 at 10:50
  • @biscuitstack: maybe it would help if you defined what you consider and "artificially random" shape. What would be a "natural" shape? This will determine how you construct your set of non-taken spaces and how you select the next one from it. – RDM Mar 17 '15 at 10:51
  • 1
    Here's another idea that may help: a game-of-life-like setup where positions are chosen according to how many neighbours a grid position has. Zero neighbours is not good, too many neighbours is not good either, we always try to pick something in the middle. You can keep track of each empty position that has at least one neighbour (keeping a separate list for those with 1 neighbour, with 2 neighbours and so on might be even better), you update this list when you place a new element. – biziclop Mar 17 '15 at 10:55
  • @Warkst Think of a city sprawl. Or the shape of a splash. They both radiate out from a central spot based on different factors but retain a slightly chaotic perimeter. I was surprised my included method achieved this and doesn't bias itself in one direction. It seems that the PVector that checks for taken spaces quite often traverses back across the area of taken space to a different side, keeping things radiating from the centre. – biscuitstack Mar 17 '15 at 10:57
  • 1
    By tweaking the weights of the probabilities of picking something with `n` neighbours you can adjust the resulting shape: if your "tenants" are very reclusive and prefer a single neighbour to everything else, you'll get a fern-like structure, if they prefer more neighbours, you're more likely to get a blob. You can even tweak the weights during the generation (houses tend to huddle close together when the village is small, and start to spread out above a certain size). – biziclop Mar 17 '15 at 10:58
  • @biziclop. That's a nice idea Though would I be wrong in thinking that it sounds rather processor intensive also? And if it will produce quite an even perimeter? Ah, just saw your follow up - interesting. I'm going to look at that. – biscuitstack Mar 17 '15 at 10:59
  • @biscuitstack If you maintain a separate list for all the places with `n` neighbours, it isn't that processor-intensive. When you place a house, you need to move all its neighbour positions from one list to the other but other than that, everything is straightforward: you use a biased dice roll to pick how many neighbours you want for the next house (this selects a list), then you pick a random element from that list, remove it, place a house there, adjust the neighbours. – biziclop Mar 17 '15 at 11:02

1 Answers1

1

This code handles 200,000 basically instantly. I had to guess at a few details but it should be close to what you are looking for.

public class Test {

    static Map<Integer, Vector3> buildingCoords;
    public static void main(String[] args) {
        buildingCoords = new HashMap();

        Vector3 start = new Vector3(0,0,0);

        for (int i = 0; i < 200000; i++)
            start = generateNewBuildingPosition(start);

        System.out.print("Done");

    }

    static Vector3 generateNewBuildingPosition(Vector3 coord) {
        int w = 50; // Footprint dimensions. Width and depth.
        float sW = 30; // gap between shapes
        // Starting at a coordinate of 0,0 if this is our 1st go
        // (PVector coord initially feeds 0,0)
        // or the last coordinate that was already taken
        // (this loops with the last coordinate if it fails)
        // we check one of the four spaces next to us with
        // the roll of a dice (in a way...)
        float randomX = (float)random() * 15;
        float randomZ = (float)random() * 15;
        if (randomX >= 10) randomX = w + sW;
        else if (randomX >= 5) randomX = (w + sW) * -1;
        else randomX = 0;

        if (randomZ >= 10) randomZ = w + sW;
        else if (randomX >= 5) randomZ = (w + sW) * -1;
        else randomZ = 0;


        // We've picked our direction.
        // We have a PVector that acts as a marker for where we're
        // placing. Adding or subtracting the movement of each
        // attempt, one at a time, means the shapes spreads out
        // more organically from the centre rather than trying
        // to distribute each shape as a random patch.
        Vector3 newDirection = new Vector3(randomX, 0, randomZ);
        coord.add(newDirection);
        // Our marker moves to the new spot, we check if it exists.
        // If it doesn't, we confirm it as this shape's anchor spot.
        // If it does, we loop this function again, feeding it where our
        // marker is.
        if(buildingCoords.containsKey(coord.hashCode())) {
            generateNewBuildingPosition(coord);
        } else {
            // add this PVector to the arrayList
            buildingCoords.put(coord.hashCode(), coord);
        }
        // Return the coordinates that just succeeded.
        return coord;
    }
}

class Vector3 {

    float x, y, z; // package-private variables; nice encapsulation if you place this in a maths package of something

    Vector3(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public Vector3 add(Vector3 vector) {
        x += vector.x;
        y += vector.y;
        z += vector.z;
        return this; // method chaining would be very useful
    }

    @Override
    public int hashCode(){
        return Float.hashCode(x + y + z);
    }

}

Edit: hashCode as shown is not very sound and may cause issues. You should read: Hashing 2D, 3D and nD vectors

Community
  • 1
  • 1
Michael Hobbs
  • 1,663
  • 1
  • 15
  • 26
  • Thank you Michael, that looks fantastic. Please bare with me, I'm not particularly experienced at this and need to adapt what you've written. Particularly 'Map', which I don't have yet. – biscuitstack Mar 17 '15 at 11:30
  • Np, if you need help let me know. I'll be around for another 30 minutes or so. On a side note I am concerned about the hash function I provided. Once I am done eating I will run some test on it. – Michael Hobbs Mar 17 '15 at 11:35
  • I'm going to guess that you're not too familiar with the java framework 'Processing' (judging by your question about 'contains') earlier? It makes things like 'Map' tricky as it's not part of the framework. It is possible to make lower-level calls to java but these are a bit beyond my comprehension. – biscuitstack Mar 17 '15 at 11:47
  • Your original code did not list ArrayList buildingCoords; My thought was that "contains" was some sort of collision detection as you are working in 3d space. No I wasn't aware it even existed until now. – Michael Hobbs Mar 17 '15 at 12:02
  • My fault, yes. Your catch of my omission of a declaration for 'w' led me to edit in my other omission of a declaration for buildingCoords. I thought the early timing of the edit and noting the arrayList existence in the original comments wouldn't cause a problem. Apologies. – biscuitstack Mar 17 '15 at 12:18
  • arrayList has a standard method called 'contains' that searches the arrayList for the existence of a provided element http://docs.oracle.com/javase/1.5.0/docs/api/java/util/ArrayList.html – biscuitstack Mar 17 '15 at 12:32
  • I am aware but it is slow. O(n) http://stackoverflow.com/questions/5771740/time-complexity-of-containsobject-o-in-an-arraylist-of-objects Where as HashMap.containsKey is O(1) http://stackoverflow.com/questions/8923251/what-is-the-time-complexity-of-hashmap-containskey-in-java – Michael Hobbs Mar 17 '15 at 12:59
  • 1
    TLDR: ArrayList.contains is to slow, O(n). In other words when you are adding the 1000 building it must check 1000 items then 1001 items for then next where as the HashMap only ever has to check one item. For what you are doing you are looking at 1/2n(n+1) list.contain calls, where n is the number of buildings you want. If your tried n=200,000 and every 10000 calls took 1 second, it would take you around two years to complete the run. – Michael Hobbs Mar 17 '15 at 13:07
  • Note, when I said I was not aware it existed I meant Processing. – Michael Hobbs Mar 17 '15 at 13:10
  • Gotcha, thanks Michael. Processing includes native calls to HashMap but I'm getting issues about 'The method hashCode() in the the type Float is not applicable for the arguments (float) from your Vector3 class. I'm sure it's a simple change of syntax in Processing itself that I need to tackle. – biscuitstack Mar 17 '15 at 13:11
  • You will likely have to come up with you own hashCode for the PVector class. What are the internal types of PVector for x, y, z? – Michael Hobbs Mar 17 '15 at 13:15
  • Indeed, I'm attempting that now. If I understand your question correctly (I'm straying outside my comfort zone), they're all of type float. – biscuitstack Mar 17 '15 at 13:26
  • I think I'll need to admit defeat on this direction, Michael. I'm not a programmer and any guidance on getting me closer to this would need to be from someone familiar with Processing. Pure Java is proving to be too far over my head. Your help was very much appreciated, thanks for your patience. – biscuitstack Mar 18 '15 at 11:41