SDL - drawing 'negative' circles (Fog of War)

后端 未结 1 799
清歌不尽
清歌不尽 2021-02-03 13:30

I have this 800x600square I want to draw to the screen. I want to \'cut\' circles in it (where alpha would be 0). Basically I\'m drawing this whole rectangle over a map so in

相关标签:
1条回答
  • 2021-02-03 14:09

    So, I assume you're trying to add fog of war to one of you game?

    I had a small demo I made for a local University a few weeks ago to show A* pathfinding, so I thought I could add fog of war to it for you. Here's the results:

    Initial map

    First, you start with a complete map, totally visible

    Full Map

    Fog

    Then, I added a surface to cover the entire screen (take note that my map is smaller than the screen, so for this case I just added fog of war on the screen, but if you have scrolling, make sure it covers each map pixel 1:1)

    mFogOfWar = SDL_CreateRGBSurface(SDL_HWSURFACE, in_Width, in_Height, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
    SDL_Rect screenRect = {0, 0, in_Width, in_Height};
    SDL_FillRect(mFogOfWar, &screenRect, 0xFF202020);
    

    Then, you need to draw it... I added this call after drawing the game objects and before drawing the UI

    DrawSurface(mFogOfWar, 0, 0);
    

    Where

    void RenderingManager::DrawSurface(SDL_Surface* in_Surface, int in_X, int in_Y)
    {
        SDL_Rect Dest = { in_X, in_Y, 0, 0 };
        SDL_BlitSurface(in_Surface, NULL, mScreen, &Dest);
    }
    

    Which should give you the following result:

    Everything Fogged

    "Punch Surface"

    I then created a 32 bits .png that looks like this (checkerboard shows alpha)

    Punch

    When rendering my main character, I added this call:

    gRenderingManager.RemoveFogOfWar(int(mX) + SPRITE_X_OFFSET, int(mY) + SPRITE_Y_OFFSET);
    

    The offset is only there to center the punch with the sprite, basically, what I'm passing to RemoveFogOfWar is the center of my sprite.

    Remove Fog Of War

    Now the meat of the fog of war. I did two versions, one where Fog of War is removed permanently and one where the fog of war is reset. My fog of war reset relies on my punch surface to have a contour where the alpha is reset to 0 and the fact that my character moves of less pixels than the contour contains per frame, otherwise I would keep the Rect where my punch was applied and I would refill it before drawing again the new punch.

    Since I couldn't find a "multiply" blend with SDL, I decided to write a simple function that iterates on the punch surface and updates the alpha on the fog of war surface. The most important part is to make sure you stay within the bounds of your surfaces, so it takes up most of the code... there might be some crop functions but I didn't bother checking:

    void RenderingManager::RemoveFogOfWar(int in_X, int in_Y)
    {
        const int halfWidth = mFogOfWarPunch->w / 2;
        const int halfHeight = mFogOfWarPunch->h / 2;
    
        SDL_Rect sourceRect = { 0, 0, mFogOfWarPunch->w, mFogOfWarPunch->h };
        SDL_Rect destRect = { in_X - halfWidth, in_Y - halfHeight, mFogOfWarPunch->w, mFogOfWarPunch->h };
    
        // Make sure our rects stays within bounds
        if(destRect.x < 0)
        {
            sourceRect.x -= destRect.x; // remove the pixels outside of the surface
            sourceRect.w -= sourceRect.x; // shrink to the surface, not to offset fog
            destRect.x = 0;
            destRect.w -= sourceRect.x; // shrink the width to stay within bounds
        }
        if(destRect.y < 0)
        {
            sourceRect.y -= destRect.y; // remove the pixels outside
            sourceRect.h -= sourceRect.y; // shrink to the surface, not to offset fog
            destRect.y = 0;
            destRect.h -= sourceRect.y; // shrink the height to stay within bounds
        }
    
        int xDistanceFromEdge = (destRect.x + destRect.w) - mFogOfWar->w;
        if(xDistanceFromEdge > 0) // we're busting
        {
            sourceRect.w -= xDistanceFromEdge;
            destRect.w -= xDistanceFromEdge;
        }
        int yDistanceFromEdge = (destRect.y + destRect.h) - mFogOfWar->h;
        if(yDistanceFromEdge > 0) // we're busting
        {
            sourceRect.h -= yDistanceFromEdge;
            destRect.h -= yDistanceFromEdge;
        }
    
        SDL_LockSurface(mFogOfWar);
    
        Uint32* destPixels = (Uint32*)mFogOfWar->pixels;
        Uint32* srcPixels = (Uint32*)mFogOfWarPunch->pixels;
    
        static bool keepFogRemoved = false;
    
        for(int x = 0; x < destRect.w; ++x)
        {
            for(int y = 0; y < destRect.h; ++y)
            {
                Uint32* destPixel = destPixels + (y + destRect.y) * mFogOfWar->w + destRect.x + x;
                Uint32* srcPixel = srcPixels + (y + sourceRect.y) * mFogOfWarPunch->w + sourceRect.x + x;
    
                unsigned char* destAlpha = (unsigned char*)destPixel + 3; // fetch alpha channel
                unsigned char* srcAlpha = (unsigned char*)srcPixel + 3; // fetch alpha channel
                if(keepFogRemoved == true && *srcAlpha > 0)
                {
                    continue; // skip this pixel
                }
    
                *destAlpha = *srcAlpha;
            }
        }
    
        SDL_UnlockSurface(mFogOfWar);
    }
    

    Which then gave me this with keepFogRemoved = false even after the character had moved around

    fog of war

    And this with keepFogRemoved = true

    fog of war permanent

    Validation

    The important part is really to make sure you don't write outside of your pixel buffer, so watch out with negative offsets or offsets that would bring you out of the width or height. To validate my code, I added a simple call to RemoveFogOfWar when the mouse is clicked and tried corners and edges to make sure I didn't have a "off by one" problem

    case SDL_MOUSEBUTTONDOWN:
        {
            if(Event.button.button == SDL_BUTTON_LEFT)
            {
                gRenderingManager.RemoveFogOfWar(Event.button.x, Event.button.y);
            }
            break;
        }
    

    Notes

    Obviously, you don't need a 32 bits texture for the "punch", but it was the clearest way I could think of to show you how to do it. It could be done using as little as 1 bit per pixel (on / off). You can also add some gradient, and change the

    if(keepFogRemoved == true && *srcAlpha > 0)
    {
        continue; // skip this pixel
    }
    

    To something like

    if(*srcAlpha > *destAlpha)
    {
        continue;
    }
    

    To keep a smooth blend like this:

    enter image description here

    3 State Fog of War

    I thought I should add this... I added a way to create a 3 state fog of war: visible, seen and fogged.

    To do this, I simply keep the SDL_Rect of where I last "punched" the fog of war, and if the alpha is lower than a certain value, I clamp it at that value.

    So, by simply adding

    for(int x = 0; x < mLastFogOfWarPunchPosition.w; ++x)
    {
        for(int y = 0; y < mLastFogOfWarPunchPosition.h; ++y)
        {
            Uint32* destPixel = destPixels + (y + mLastFogOfWarPunchPosition.y) * mFogOfWar->w + mLastFogOfWarPunchPosition.x + x;
            unsigned char* destAlpha = (unsigned char*)destPixel + 3;
    
            if(*destAlpha < 0x60)
            {
                *destAlpha = 0x60;
            }
        }
    }
    mLastFogOfWarPunchPosition = destRect;
    

    right before the loop where the fog of war is "punched", I get a fog of war similar to what you could have in games like StarCraft:

    3 state

    Now, since the "seen" fog of war is semi transparent, you will need to tweak your rendering method to properly clip "enemies" that would be in the fog, so you don't see them but you still see the terrain.

    Hope this helps!

    0 讨论(0)
提交回复
热议问题