1

I'm trying to implement line-of-sight visibility / fog-of-war for my 2D top-down game, and found this article which has a simple and efficient algorithm which involves shooting rays at the edges of rectangles to calculate a list of triangles to brighten.

However, my game uses tiles, so it would be very inefficient to run this against the 150x150 (22,500) tiles surrounding the players every frame. Rather, it would be better to instead convert the tile-map to a list of rectangles and then run the line-of-sight algorithm against that. For example, if this were my tile-map (where 1 is a solid tile and 0 is a free tile):

1 1 1 1 1 1 1 
1 0 0 0 0 0 1 
1 0 0 0 0 0 1 
0 0 1 1 0 0 1  
0 0 1 1 1 0 1 
1 0 0 0 0 0 1 
1 1 1 1 1 1 1 

Then you could convert that into a list of rectangles like so:

1 1 1 1 1 1 1 
5 0 0 0 0 0 2 
5 0 0 0 0 0 2 
0 0 6 6 0 0 2  
0 0 6 6 7 0 2 
4 0 0 0 0 0 2 
3 3 3 3 3 3 3

In this result every 1 refers to the first rectangle, every 2 refers to the second rectangle, etc. This is not the only possible outcome, but one of the many possible ones. Basically, instead of there being 7 x 7 = 49 tiles to check against, the algorithm only has to check against 7 rectangles, which greatly speeds up the field-of-view calculations.

The rectangles would have properties like x, y, width, height. In this case the first rectangle would be something like x: 0, y: 0, w: 7, h: 1. The second rectangle would be x: 6, y: 1, w: 1, h: 5, etc.

Is there an algorithm to generate a list of rectangles from a 2D tile matrix?

Ryan Peschel
  • 11,087
  • 19
  • 74
  • 136
  • Probably looking for a rectangle packing algorithm, or a variation of it. But you could also consider just getting a path shape. – Daniel Apr 09 '21 at 19:00
  • Do you have any recommendations on a specific one that would be ideal for this use-case? – Ryan Peschel Apr 09 '21 at 19:11
  • Also, I think this might be different from a rectangle / bin-packing algorithm in that there is no desire to move the rectangles around, only to generate the smallest list of rectangles that occupies every single filled tile. – Ryan Peschel Apr 09 '21 at 19:12
  • right, usually the packing algorithm tries to fill the space (thus the "variation"). But there may be an overlap in implementation or theory. There is naive way, for example, start top left in a width-first filling process, that would give you similar results (ie 5 would be processed before 2, but the shapes would be the same). But a more complex algorithm, that tries to back the fewest amount of rectangles, might give you better results. If the tiles don't change, the larger up-front cost of processing this data might outweigh doing more calculations per frame. – Daniel Apr 09 '21 at 19:26

1 Answers1

1

There is naive way, for example, start top left in a width-first filling process, that would give you similar results to what you're looking for.

Here's what the width-first algo might look like:

Results in:

1 1 1 1 1 1 1
2 0 0 0 0 0 3
2 0 0 0 0 0 3
0 0 4 4 0 0 3
0 0 4 4 5 0 3
6 0 0 0 0 0 3
6 7 7 7 7 7 7
  • create empty array tileCache for storing which tiles have been processed, prefill it with 0
  • create an array tileRects for storing rectangle info
  • loop through tiles by row and column
  • check if tile equals 1, if not, process to next tile
  • check if tileCache at coordinates does not equal to 0; if != 0 process next tile
  • if it's a novel tile, find tiles to the right that are 1 and get the width of the rectangle
  • use the width of the rectangle (and x,y) to find out how tall we can make the rectangle to get the height
  • then use x,y,width and height to create the rectangle object
  • fill the cache with the data

Note that there is one quirk of this algorithm, that shouldn't be an issue, and can be fixed if it is an issue. It will allow overlapping of rectangles.

ie

let tiles = [
  [1, 1, 1, 1],
  [0, 1, 1, 0],
  [1, 1, 1, 1],
  [0, 1, 1, 0],
];

will generate 3 rectangles

1 1 1 1
0 2 2 0
3 3 3 3
0 2 2 0

let tiles = [
  [1, 1, 1, 1, 1, 1, 1],
  [1, 0, 0, 0, 0, 0, 1],
  [1, 0, 0, 0, 0, 0, 1],
  [0, 0, 1, 1, 0, 0, 1],
  [0, 0, 1, 1, 1, 0, 1],
  [1, 0, 0, 0, 0, 0, 1],
  [1, 1, 1, 1, 1, 1, 1],
];

const tileCache = tiles.map(() => Array(tiles[0].length).fill(0));

const tileRects = [];

function exploreRight(y, x) {
  let w = 1;
  while (tiles[y][x + w] === 1) {
    w++;
  }
  return w;
}

function exploreDown(y, x, w) {
  let pass = true;
  let h = 1;
  while (pass) {
    for (let $x = x; $x < x + w; $x++) {
      if (!tiles[y + h] || tiles[y + h][$x] !== 1) {
        pass = false;
        continue;
      }
    }
    pass && h++;
  }
  return h;
}

function fillCache(y, x, h, w, n) {
  for (let $y = y; $y < y + h; $y++) {
    for (let $x = x; $x < x + w; $x++) {
      tileCache[$y][$x] = n;
    }
  }
}

let n = 1;
for (let y = 0; y < tiles.length; y++) {
  for (let x = 0; x < tiles[y].length; x++) {
    const tile = tiles[y][x];
    const cache = tileCache[y][x];
    if (tile === 0) {
      continue;
    }

    if (cache > 0) {
      continue;
    }

    const w = exploreRight(y, x);
    const h = exploreDown(y, x, w);
    tileRects.push({ y, x, w, h });
    fillCache(y, x, h, w, n);

    if (w > 1) {
      x += w - 1;
    }
    n++;
  }
}

console.log(tileCache.map((r) => r.join(" ")).join("\n"));

console.log(tileRects);

Update

If the overlapping squares are an issue, the only change needed is that the exploreDown method should check against the cache too, not just tiles. The assumption is that fewer rectangles means fewer calculations, but in some cases the overlap might be an issue.

function exploreDown(y, x, w) {
  let pass = true;
  let h = 1;
  while (pass) {
    for (let $x = x; $x < x + w; $x++) {
      if (!tiles[y + h] || tiles[y + h][$x] !== 1) {
        pass = false;
        continue;
      }
      /// change 
      if (!tileCache[y + h] || tileCache[y + h][$x] !== 1) {
        pass = false;
        continue;
      }
      // change 
    }
    pass && h++;
  }
  return h;
}
Daniel
  • 34,125
  • 17
  • 102
  • 150
  • Hey this is great thanks! I'll just have to make a few changes because I'm using a 1D array instead of a 2D array, but I think I can figure that out myself. – Ryan Peschel Apr 09 '21 at 20:35
  • added more info and caveat about overlapping rectangles – Daniel Apr 09 '21 at 20:39
  • Because I'm just using this for a line of sight algorithm, I don't _think_ overlapping rectangles matters, but just in case it does, feel free to add a note on a potential way to fix it. I doubt I'll need the fix but if it turns out a couple days later that I (or perhaps someone else), a few words explaining how one might fix that could be useful to include? Thanks again! – Ryan Peschel Apr 09 '21 at 20:40
  • updated, just need to check the exploreDown for cache too – Daniel Apr 09 '21 at 20:44
  • You also need to check cache in the Explore Right method if you wish to prevent overlapping rectangles during a horizontal scan that were claimed by a previous vertical scan – md5madman Jul 02 '22 at 13:54