Unity RTS – Part 4 – Vision blocking

Its been a while! This time I’ve made a lot of tweaks to the Line of Sight code so that items can correctly and efficiently occlude unit vision and implemented pathfinding using flowfields for smoother unit motion. Both items took quite a bit longer than I had anticipated and are probably still quite slow, but I’m happy with how they are looking. This post will talk about the vision blockers; I’ll leave pathfinding for the next.

Some code existed in the previous build for entities to write their blocking height to a grid covering the game world. This grid tracks the highest unit on each cell and blocks sight for all shorter units. The RevealLOS method has been rewritten to now more accurately read this data and project “shadows” outward to hide areas of terrain. The algorithm has two distinct phases.

Pass 1 – Occlusion angles

The first pass iterates through all tiles within the reveal radius, ordered by their occurrence when sweeping clockwise. Think of this as a radar, which pings for each cell as the detector passes them. This ordering is important to keep the input clean and reduce the amount of work needed later.

Each cells height is compared against the current units height, if the cell is too high, it is added to an ordered list of blocking cells (ordered the same as the iteration, clockwise around the unit). The distance and angles for where the blocked vision starts and stops are also cached. A small optimisation pass is done on this data to remove cells that are eclipsed by other cells nearer to the unit center; these are redundant, since any terrain they occlude will already be occluded by the nearer cells.

Pass 2 – Terrain reveal

The next pass iterates over all tiles within the reveal radius, determines their distance and start/end angles, and looks through the previous list of occlusion cells to determine how much of the tile is occluded. As the occlusion items are ordered, iterating this list forward and shrinking the visible arc is sufficient to correctly calculate occlusion for the left-most angle. This is then repeated in reverse for the right side.

The result of this calculation is four angles; the tiles initial start and end angles relative to the unit, and the unoccluded start and end angles. The visibility of each tile is simply the unoccluded arc size divided by the total tile arc size.

Optimisations

Trigonometry functions are expensive to execute, so “angles” here are approximations. A full circle in this system is 8 degrees, and calculating the angle of a vector is simply determining what quadrant it is in, and then dividing the coordinates.

It is impossible for the arc of a nearer cell to be smaller than a farther cell it is projecting onto. If this were not the case, it could be possible for small arcs to lie entirely within a tiles arc, and be ignored by the system.

losoccluded

View online or Download Project (note: has some unfinished UI code in there too)

Advertisements

Unity RTS – Part 2 – Line Of Sight

There are two common methods for displaying Line of Sight and and Fog of War in an RTS. The geometry hidden from the player can either be modified before being rendered to the screen, or a second pass can be used to paint over the top to cut out areas that should not be seen.

Painting over the top

Painting blackness over the top of the world was a very common approach for older 2D RTS’. It can be seen in Age of Empires 1 by the dithering pattern present in revealed foggy areas. This approach has the benefit of working very well with DirectDraw (and GDI) compatible hardware, but also nicely decouples the fog of war code from the rest of the game rendering. Civilization has another neat version of this for its cloudy fog of war.

Painting on the surface

Warcraft 3

This seems to be the approach used in most modern RTS games. A trivial implementation would require a texture to be generated matching the terrain size, such that each pixel corresponds to one cell of the terrain. Each of these pixels contains visibility data for that cell, with a shader reading that data to darken or remove sections of the terrain. This method forces tight coupling between the geometry rendering and fog of war, but allows more advanced rendering; properly handling transparency, filters, and working well with 3D geometry.

LOS in Unity

Unity has sufficient support to easily handle the shaders necessary for painting line of sight darkening right onto the geometry. I like to implement this with a global shader texture and scale/translation to map from world XZ coordinates to the LOS texture. To work with the indie version of Unity, this texture needs to be generated on CPU and copied over each time it changes; this slower than doing the work directly on the GPU, but for smaller maps, it is sufficiently fast.

Each frame, the LOSManager clears the red channel of each pixel to 0. It then iterates through each entity in the world owned by the player, and paints a white circle onto the texture around the unit (importantly, it sets the red and green channels to 255). This means that every pixel that is currently visible will have a red and green value of 255. Every pixel that was previously visible and is now fogged will have a red value of 0 and green value of 255. Pixels that have not yet been revealed will be black. In the UnityRTS implementation, the blue channel is used to store the amount of time since the area was visible, and the alpha channel stores ambient occlusion, though these are not important features.

The shader needed to render line of sight on objects is quite simple. It can be implemented in a surface shader by adding the following lines:

half4 fow = tex2D(_FOWTex, TRANSFORM_TEX(IN.worldPos.xz, _FOWTex));
o.Albedo = t.rgb * (fow.r + fow.g) / 2;

Where t is the texel from _MainTex, and _FOWTex is the fog of war texture (along with _FOWTex_ST). This code will cause anything in full visibility to be draw as normal, anything revealed but not visible to be half-brightness, and anything hidden to be black.

UnityRTSP2

View online or Download Project (this may be removed)