4

Problem

I am trying to make a city generator for a game that creates blocks that have a type (residential, industrial, commercial). All of my streets will be at 90 degrees and there for I want it to be blocky (instead of zig zagging).

Approaches

My first approach was picking a starting point and then randomly moving around the map for X number of moves. If I hit a dead end I back tracked some random number, kind of like a maze generator but this left me with lots of areas where roads where next to roads and roads that didn't travel in straight lines. I've also looked at using perlin noise, but I believe this two would give me roads that zig zagged to often.

Current Solution

I have come up with an approach that gives me pretty much what I want but I think its more complex than it needs to be or at least less efficient than it can be. Currently if I try to scale it up to a larger map it can take a few seconds to process.

JS Fiddle

https://jsfiddle.net/jrj2211/0exe9jne/

Algorithm

  1. Make a 2D grid and populate with empty cells
  2. Get a list of all possible cells and shuffle it
  3. Loop through each cell in shuffled list
    • Generate a randomly sized rectangle off of the cell and give it a unique ID
    • Give each cell in the rectangle the unique ID
    • Randomly pick its type (Residential, commercial, industrial)
  4. Scale the entire array up by 2
  5. Loop through the scaled up array and replace any tiles that have a neighboring tile with a different unique ID as a ROAD tile.

Scaling the array example (kind of like scaling an image):

[1, 2, 2]     [1, 1, 2, 2, 2, 2]
[1, 3, 3] =>  [1, 1, 3, 3, 3 ,3]
[4, 4, 4]     [4, 4, 4, 4, 4, 4] 

The reason I scale up the grid is if I don't, any tile that was previously an area of 1x1 it would generate a map where two or more roads tiles would be neighbors.

Visualization

enter image description here

Final output

Note: This is not the same output as in the visualization

enter image description here

Summary

So to sum up my question, does anyone have any suggestions on how to make this more efficient or cleaner. I think its hard to write pseudo code for my current process so I think there's room for improvement. I also will have to make it even more complex to do things like removing cells that are 1x1 because no house would be surrounded by streets on all sides. I also don't want the city to be a perfect square (so I'd have to delete random zones along the border and close off their streets).

Joe Jankowiak
  • 684
  • 8
  • 26
  • Do you have any objective requirements on the output? A regular grid of 5x5 blocks and streets between them is certainly not acceptable - but can you explain why? Formulating these rules might actually help you in developing an algorithm. – anatolyg Jan 18 '18 at 10:25
  • My end goal is to actually make more of a town instead of a city with sky scrappers, so having empty space between hasn't been a huge concern for me. I also want to eventually have a park or other things that may need say a 5x5 space but I'm trying to keep it simple for now. Currently when picking the random rectangle I decide if its going to be vertical or horizontal and limit its smaller side to a max of 2. – Joe Jankowiak Jan 18 '18 at 10:29
  • The main part of my code that feels hacky to me is my round about way of adding the road tiles by scaling the entire array up. – Joe Jankowiak Jan 18 '18 at 10:33
  • The idea for your algorithm looks potentially powerful, although it seems it could be hard dealing with corner cases and "weird" shapes. You can also consider using [binary space partitioning](https://en.wikipedia.org/wiki/Binary_space_partitioning) (or something based on that), which is sometimes used to generate different types of maps and should be rather straightforward to apply. For example, see in [this tutorial](https://gamedevelopment.tutsplus.com/tutorials/how-to-use-bsp-trees-to-generate-game-maps--gamedev-12268) the intermediate result at the end of "Creating leafs". – jdehesa Jan 18 '18 at 11:25
  • Very interesting, that does look very similar to what I want and it provides spaces between leafs. I think I'm going to stick with what I have now (with funks recommendation) as I'm just prototyping an idea but I will definitely keep this in my back pocket. – Joe Jankowiak Jan 18 '18 at 20:46
  • Joe, this is such a good idea. I was trying to solve the same problem, and tried so many different approaches that didn't yield satisfactory results. This was it! Thanks a lot! – fs_ Feb 28 '20 at 00:30

1 Answers1

1

One way to circumvent the large array creation is by implementing the desired layout straight away. The key is to scale curTile instead of grid, note the change from ++ to +=2.

// Get all cells as a 1 dimensional array
function GetAllCells() {
  var cells = [];
  for (var i = 0; i < mapSize; i+=2) {
    for (var j = 0; j < mapSize; j+=2) {
      cells.push(grid[i][j]);
    }
  }
  return cells;
}

Back to back

IsInBoundsScaledIsInBounds

newGridgrid

Iterate

To get the same order of blocks, we have to double square sizes (ref minSize, maxSize).

// Get a random order to loop through the cells
var checkOrder = shuffle(GetAllCells());
var minSize = 4;
var maxSize = 10;

for (var id = 1; id < checkOrder.length; id++) {
  var curTile = checkOrder[id];

  if (curTile.type == TYPES.NONE) {
    var direction = (Math.random() > .5 ? 1 : 0);
    var square_width = RandomRange(minSize, (direction ? maxSize : minSize));
    var square_height = RandomRange(minSize, (direction ? minSize : maxSize));

    var zones = [TYPES.RESIDENTIAL, TYPES.COMMERCIAL, TYPES.COMMERCIAL, TYPES.RESIDENTIAL, TYPES.INDUSTRIAL];
    var zone = zones[Math.floor(Math.random() * zones.length)];
    var color = getRandomColor();

    for (var i = 0; i < square_width; i+=2) {
      for (var j = 0; j < square_height; j+=2) {
        if (IsInBounds(curTile.i + i+1, curTile.j + j+1)) {
          grid[curTile.i + i][curTile.j + j].id = id;           // [x] O
          grid[curTile.i + i][curTile.j + j].type = zone;       //  O  O

          grid[curTile.i + i+1][curTile.j + j].id = id;         //  x [O]
          grid[curTile.i + i+1][curTile.j + j].type = zone;     //  O  O

          grid[curTile.i + i][curTile.j + j+1].id = id;         //  x  O
          grid[curTile.i + i][curTile.j + j+1].type = zone;     // [O] O

          grid[curTile.i + i+1][curTile.j + j+1].id = id;       //  x  O 
          grid[curTile.i + i+1][curTile.j + j+1].type = zone;   //  O [O]
        }
      }
    }
  }
}

JS Fiddle Fork

https://jsfiddle.net/7srfrx55/

myTown

Funk
  • 9,043
  • 1
  • 14
  • 30
  • I like this, it looks much cleaner and after the process is complete the map is the same number of tiles as it began. With my original approach it doubled the number of tiles. Thanks! – Joe Jankowiak Jan 18 '18 at 20:42