How can I best implement fog of war with Java?

Viewed 359

Hello I am an inexperienced programmer and this is my first question on Stack Overflow!

I am attempting to implement 'fog of war' in my Java game. This means most of my map begins off black and then as one of my characters moves around parts of the map will be revealed. I have searched around including here and found a few suggestions and tried tweaking them myself. Each of my approaches works, however I run into significant runtime issues with each. For comparison, before any of my fog of war attempts I was getting 250-300 FPS.

Here is my basic approach:

  1. Render my background and all objects on my JPanel
  2. Create a black BufferedImage (fogofwarBI)
  3. Work out which areas of my map need to be visible
  4. Set the relevant pixels on my fogofwarBI to be fully transparent
  5. Render my fogofwarBI, thus covering parts of the screen with black and in transparent sections allowing the background and objects to be seen.

For initialising the buffered image I have done the following in my FogOfWar() class:

    private BufferedImage blackBI =  loader.loadImage("/map_black_2160x1620.png");
    private BufferedImage fogofwarBI = new BufferedImage(blackBI.getWidth(), blackBI.getHeight(), BufferedImage.TYPE_INT_ARGB);

    public FogOfWar() {
        fogofwarBI.getGraphics().drawImage(blackBI,0,0,null);
    }

In each of my attempts I start the character in a middle of 'visible' terrain, ie. in a section of my map which has no fog (where my fogofwarBI will have fully transparent pixels).

Attempt 1: setRGB

First I find the 'new' coordinates in my character's field of vision if it has moved. ie. not every pixel within the character's range of sight, but just the pixels at the edge of his range of vision in the direction he is moving. This is done with a for loop, and will go through up to 400 or so pixels.

I feed each of these x and y coordinates into my FogOfWar class.

I check if these x,y coordinates are already visible (in which case I don't bother doing anything to them to save time). I do this check by maintaining a Set of Lists. Where each List contains two elements: an x and y value. And the Set is a unique set of the coordinate Lists. The Set begins empty, and I will add x,y coordinates to represent transparent pixels. I use the Set to keep the collection unique and because I understand the List.contains function is a fast way of doing this check. And I store the coordinates in a List to avoid mixing up x and y.

If a given x,y position on my fogofwarBI is not currently visible I add set the RBG to be transparent using .setRGB, and add it to my transparentPoints Set so that coordinate will not be edited again in future.

    Set<List<Integer>> transparentPoints = new HashSet<List<Integer>>();

    public void editFog(int x, int y) {

        if (transparentPoints.contains(Arrays.asList(x,y)) == false){
            fogofwarBI.setRGB(x,y,0); // 0 is transparent in ARGB
            transparentPoints.add(Arrays.asList(x,y));
        }
    }

I then render it using

    public void render(Graphics g, Camera camera) {
        g.drawImage(fogofwarBI, 0, 0, Game.v_WIDTH, Game.v_HEIGHT, 
                    camera.getX()-Game.v_WIDTH/2, camera.getY()-Game.v_HEIGHT/2, 
                    camera.getX()+Game.v_WIDTH/2, camera.getY()+Game.v_HEIGHT/2, null);
    }

Where I am basically applying the correct part of my fogofwarBI to my JPanel (800*600) based on where my game camera is.

Results: Works correctly. FPS of 20-30 when moving through fog, otherwise normal (250-300). This method is slow due to the .setRGB function, being run up to 400 times each time my game 'ticks'.

Attempt 2: Raster

In this attempt I create a raster of my fogofwarBI to play with the pixels directly in an array format.

    private BufferedImage blackBI =  loader.loadImage("/map_black_2160x1620.png");
    private BufferedImage fogofwarBI = new BufferedImage(blackBI.getWidth(), blackBI.getHeight(), BufferedImage.TYPE_INT_ARGB);

    WritableRaster raster = fogofwarBI.getRaster();
    DataBufferInt dataBuffer = (DataBufferInt)raster.getDataBuffer();
    int[] pixels = dataBuffer.getData();

    public FogOfWar() {
        fogofwarBI.getGraphics().drawImage(blackBI,0,0,null);
    }

My editFog method then looks like this:

    public void editFog(int x, int y) {
        if (transparentPoints.contains(Arrays.asList(x,y)) == false){
            pixels[(x)+((y)*Game.m_WIDTH)] = 0; // 0 is transparent in ARGB
            transparentPoints.add(Arrays.asList(x,y));
        }
    }

My understanding is that the raster is in (constant?) communication with the pixels array, and so I render the BI in the same way as in attempt 1.

Results: Works correctly. A constant FPS of around 15. I believe it is constantly this slow (regardless of whether my character is moving through fog or not) because whilst manipulating the pixels array is quick, the raster is constantly working.

Attempt 3: Smaller Raster

This is a variation on attempt 2.

I read somewhere that constantly resizing a BufferedImage using the 10 input version of .drawImage is slow. I also thought that having a raster for a 2160*1620 BufferedImage might be slow.

Therefore I tried having my 'fog layer' only equal to the size of my view (800*600), and updating every pixel using a for loop, based on whether the current pixel should be black or visible from my standard transparentPoints Set and based on my camera position.

So now my editFog Class just updates the Set of invisible pixels and my render class looks like this:

    public void render(Graphics g, Camera camera) {

        int xOffset = camera.getX() - Game.v_WIDTH/2;
        int yOffset = camera.getY() - Game.v_HEIGHT/2;

        for (int i = 0; i<Game.v_WIDTH; i++) {
            for (int j = 0; j<Game.v_HEIGHT; j++) {
                if ( transparentPoints.contains(Arrays.asList(i+xOffset,j+yOffset)) ) {
                    pixels[i+j*Game.v_WIDTH] = 0;
                } else {
                    pixels[i+j*Game.v_WIDTH] = myBlackARGB;
                }
            }
        }

            g.drawImage(fogofwarBI, 0, 0, null);

    }

So I am no longer resizing my fogofwarBI on the fly, but I am updating every single pixel every time.

Result: Works correctly. FPS: Constantly 1 FPS - worst result yet! I guess that any savings of not resizing my fogofwarBI and having it smaller are massively outweighed by updating 800*600 pixels in the raster rather than around 400.

I have run out of ideas and none of my internet searching is getting me any further in trying to do this in a better way. I think there must be a way to do fog of war effectively, but perhaps I am not yet familiar enough with Java or the available tools.

And pointers as to whether my current attempts could be improved or whether I should be trying something else altogether would be very much appreciated.

Thanks!

1 Answers

This is a good question. I am not familar with the awt/swing type rendering, so I can only try to explain a possible solution for the problem.

From a performance standpoint I think it is a better choice to chunk/raster the FOW in bigger sections of the map rather than using a pixelbased system. That will reduce the amount of checks per tick and updating it will also take less resources, as only a small portion of the window/map needs to update. The larger the grid, the less checks, but there is a visual penalty the bigger you go.

Leaving it like that would make the FOW look blocky/pixelated, but its not something you can't fix.

For the direct surrounding of a player, you can add a circle texture with the player at its center. You can than use blending (I believe the term in awt/swing is composite) to 'override' the alpha where the circle overlaps the FOW texture. This way the pixel-based updating is done by the renderingAPI which usually uses hardware enhanced methods to achieve these things. (for custom pixel-based rendering, something like 'shader scripts' are often used if supported by the rendering API)

This is enough if you only need temporary vission in the FOW (if you don't need to 'remember' the map), you don't even need a texture grid for the FOW than, but I suspect you do want to 'remember' the map. So in that case:

The blocky/pixelated look can be fixed like they do with grid-based terain. Basically add a small additional textures/shapes based on the surroundings to make things look nice. The link below provides good examples and a detailed explanation on how to do the 'terrain-transitions' as they are called.

https://www.gamedev.net/articles/programming/general-and-gameplay-programming/tilemap-based-game-techniques-handling-terrai-r934/

I hope this gives a better result. If you cannot get a better result, I would advise switching over to something like OpenGL for the render engine as it is meant for games, while the awt/swing API is primarely used for UI/application rendering.

Related