Fog of War Optimisation

Started by Terrorcell, Thu 26/01/2012 16:54:32

Previous topic - Next topic

Terrorcell

So I'm creating a game that requires fog of war to shroud the map for the player. So far, the fog of war is made up of an array of a fog of war struct (array size depends on the map size, my test map's FOW array size is 5120, but that's a small map in regards to my bigger plans) that holds variables such as 'bool isVisible' and the sprite slot number that it uses (the sprite is a 15x15 black square).
Basically, everything is drawn onto the background every game loop, and every time a character is drawn the fog of war array is updated, changing its isVisible variable to true if the character has uncovered that tile (pretty much a view distance). Every game loop, the fog of war is also drawn onto the background, tile by tile:

Code: ags

function drawFOW()
{
  roomsurface = Room.GetDrawingSurfaceForBackground();
  
  int y = 0;
  while (y < tilesHeight) // the fog of war array is actually a 2d array, but is represented through the index() function, so tilesHeight is the width of the map (1200) / the tile size (15)
  {
    int x = 0;
    
    while(x < tilesWidth) // same goes for the width here, 960 / 15
    {
      if (!Tiles[index(x, y)].isVisible) // checks to see if the current tile in the loop is already visible, skips the drawing if it is
      {
        roomsurface.DrawImage(x * tileSize, y * tileSize, fog.Graphic); // fog.Graphic is the sprite slot used for each tile, tileSize is 15
      }
      
      x++;
    }
    
    y++;
  }
}


This is very heavy on the games FPS, as you can imagine, and I'm having a bit of trouble thinking of ways to optimise this :(
Iterating through an array that size doesn't hurt the FPS very much at all, but the DrawImage() function being called that many times (5120) every game loop drops it by about 10-12 FPS.
I can't just draw what can be seen in the view screen because the whole background, after the fog of war has been drawn onto it, is then drawn onto a minimap.

So I was wondering if anyone had any ideas? Would be very much appreciated :)

Calin Leafshade

The problem is that the drawing surface functions are not hardware accelerated.

In my game I cache the tiles to an object and only refresh it when needed. In this way i can move the camera around by moving the object without having to redraw the tiles.

You can also have multiple layers in this way by using multiple objects.

Khris

I'd use DynamicSprites; just keep the entire map on global DynamicSprites, then copy, crop and draw them on the background each loop.

Calin Leafshade

Moving an object achieves the same thing but more efficiently.

Radiant

You could try lowering your color depth, or restricting the loop to the part that is actually on-screen. Also, drawing black rectangles may be faster than drawing a sprite.

Also, do I understand correctly that you're first drawing (or restoring) the entire playfield, and then erasing the parts that are fogged? Because that's two draw operations for each fogged tile.

Finally, I would avoid calling a function like index() in the inner loop, just on general principle.

Terrorcell

@Calin Leafshade
Sorry, but could you elaborate on that a little more? Do you mean stitch the tiles together as a single graphic, save them to a room object and then draw them? :S

@Radiant
Drawing black rectangles instead and avoiding the index() function together only increased the FPS by roughly 1. Also, the tiles visibility is updated when the character is drawn, and then the tiles are drawn onto the background depending on their state of visibility. If they have been revealed already then they are not drawn.

mode7

I think what Radiant means is checking if the x/y tile is on screen before actually drawing it. This should make your performance independent from the map size and should give you sufficient performance. What frame rates are you getting? Ags draw image function should be fast enough for resolutions up to 640 (with a 32 tile size or bigger) Dropping the alpha-channel should also give you a performance boost.

Terrorcell

I know what he meant, but the point is that if I check if the tile is on the screen before it gets updated then the rest of the tiles that arent on the screen (as in the viewport I am guessing you mean) won't get updated, and so if a character (there are more than one that the player can control) moves around in the fog of war off screen, then the effected tiles will only stop being drawn once the player moves the viewport to that units position. That could get quite ugly I think.

The way it works now is in the drawing function for the character (the function that draws the character onto the map) a function is called with the characters x, y and range values passed into it. That function updates the tiles within the range of that character, and seperately, in the room script, the function that I posted above goes through all the tiles that are still shrouded and draws them. If the tile has been revealed, then the tile is not drawn.

Ryan Timothy B

#8
You only draw the fog on screen when it's visible to the player, then draw the fog on the minimap only when it changes. You should store the minimap as a dynamic sprite (if you aren't already - I would hope you are, it's the most efficient way) and only change the individual pixels of the minimap when needed.

The best way for the minimap, in my opinion, is to store the colors on an array per pixel along side with the dynamic sprite of the 'current state'. Then whenever the fog is light, medium, dark, you change that single pixel to the appropriate color basing it from the color data in the array. That way you aren't using GetPixel anymore than once when the level loads up. Then you're only using DrawPixel on the minimap dynamic sprite Only whenever the fog actually changes for that single pixel.

Then have a separate dynamic sprite with a Transparent Color background that is layered over top where you draw the colored pixels for where the characters are. This should be done along side with the single While loop for the characters; because you should avoid doing a while loop multiple times wherever possible.

Edit:
And you know what is even faster? Is what Calin said. Because drawing on a large background is much slower than drawing on multiple dynamic sprites that are the size of the screen. But only drawing on these dynamic sprite if that section of it is visible to the player. Then when the 'viewport' changes, you move the sprites around. You don't even need to draw on the background at all either, just on these sprites which can be Objects. Only because using DrawImage on the background is should be slower than AGS displaying the object that is using the dynamic sprite (although I've never benchmarked the speeds on this).

You'd only need 4 objects and have them change to whatever background chunk is needed, then swap the XY of that object to the other side. It's the most efficient way that I've somewhat experimented with.

Edit:
Now that I've thought about this more I really want to run a speed test between the two. Fully doing it Calin's way with a gigantic sprite the size of your map, or multiple sections of your map on individual chunks. Either using 4 objects to display them, or simply drawing it to the room's background; with the room's background being equal size as the game resolution of course. One day when I get a moment, I'll check the speed between the two.

My thoughts are that drawing to a gigantic sprite should be slower than drawing to a smaller sprite, or multiple smaller sprites. That's what I need to run a speed test on.

Calin Leafshade

The important bit of my method is that you only draw (with a drawing surface) the map *once* (unless the map itself changes at some point) and then you just move theobjects position to move around the map (oObject1.x = -camera.x).

The main advantage here is that none of the drawing code (except the inital draw) is done on the CPU. AGS can accelerate object drawing on the GPU and so it is orders of magnitude faster.

Also, if you draw the object with a drawing surface every loop then it essentially gets drawn twice. Once to the DynamicSprite and once by the engine to the screen.

SMF spam blocked by CleanTalk