Designing a Vision System
Here's some stuff I'd like to happen under a vision system.
You round a corner and come face-to-face with a snarling ogre!
You cautiously inch through a dark hall, it's walls barely illuminated by your feeble torchlight. A pack of ghouls rushes out of the dark!
You walk by a pile of rubble. From the faint torchlight nothing seems amiss, but when you turn your back a monster jumps out and attacks!
You charge toward the monster, but one of the floor tiles you stepped on was a secrete switch! An arrow shoots out of a bolthole in a nearby wall!
You inspect a section of wall and notice a secret door!
Leery of a nearby pile of rubble, the priest casts a light spell to illuminate the darkened crevices. This reveals a giant spider lying in wait!
The general themes here are risk, surprise, and preparation.
- The dungeon should feel mysterious and menacing.
- Blundering around should be dangerous.
- Preparation, caution, and investigation should be rewarded.
Implementation Goals
Here's the mechanics I need to make the fantasy happen:
- Line of sight - We need line of sight so the level layout can present surprises. This opens the possibility of ambushes or losing track of a monster you're chasing.
- Variable vision - Some units (e.g. thieves) should have better vision so they can see farther in the dark, and detect hidden things at a closer range.
- Seeing a tile != knowing what's in the tile - Terrain should be more visible than entities. That way even a revealed space can present surprises. This allows for ambushes, traps, hidden doors, etc.
- Light and darkness - There should be well-lit spaces where you can see everything you have a line of sight to. There should also be dark spaces that you can't see all of at once.
Line of Sight
Easily done with a ray-casting algorithm based on Bresenham lines. This is standard roguelike stuff.
Variable Vision
Easily done by giving each unit a vision: Int
field that determines how long their line of sight is.
Seeing a Tile != Seeing the Entity at the Tile
Each entity (e.g unit, switch) needs an obscurity: Int
field and we need a formula: visible(distance: Int, vision: Int, obscurity: Int) -> Bool
that determines whether an entity in your line of sight is visible.
Light and Darkness
A lit tile should be visible if you have line of sight, no matter how far away it is. A lit tile should probably penalize the obscurity
value of whatever entity occupies it.
I think for this to work vision
represents how far you can see in the dark, i.e. how many tiles you can see beyond a light source's radius.
Designing Formulas
We need 2 formulas: one to check if a tile is visible and one to check if an entity at a tile is visible.
Tile Visibility
a tile is visible if:
it is in your line of sight AND
it is lit OR
it is within your vision radius OR
there is a bresenham line from a light source to the tile
with length <= vision + light source radius.
That last check is super important but also kind of expensive. I haven't even decided how to model light sources yet.
Entity Visiblity
an entity is visible if:
the tile it's on is visible AND
obscurity == 0 OR
obscurity + distance < vision * light_multiplier (1 if not lit, 2 if lit)
I like this cause:
- Getting closer makes it easier to see hidden things.
- Using a light makes it easier to see hidden things.
Implementation
Light Algorithm
We can simplify the vision checks if we have a matrix of light values.
light_value(coord) -> Int
0 if lit
X if if the closest light source with a bresenham line to coord is X tiles away
tile_visible(from: Coord, to: Coord) -> Bool
check_bresenham(from, to) &&
(distance(from,to) <= vision) || light_value(to) <= vision)
Clean!
entity_visible(from: Coord, to: Coord, obscurity: Int) -> Bool
check_bresenham(from, to) &&
(distance(from,to) + obscurity <= vision || light_value(to) + obscurity <= vision)
Also clean!
Computing the light matrix.
record Light { coord: Coord, radius: Int }
for x in 0..MAP_WIDTH
for y in 0..MAP_HEIGHT
light[x,y] = INT_MAX
for light in lights:
for x in 0..MAP_WIDTH
for y in 0..MAP_HEIGHT
c := Coord(x,y)
if check_bresenham_line(light.coord, c)
value := (distance(light.coord, c) - light.radius).max(0)
light[x,y] = min(value, light[x,y])
let's use N
for map width, map height, number of lights, then the time complexity is about O(N^4)
for N ~= 16
.
The lighting grid will have to be re-created under the following conditions:
- A light is created or destroyed.
- A light moves.
I want to have light spells that create temporary lights, and possibly a light value on each unit to represent a unit carrying a torch or something. I think this algorithm acceptable for the workload I have in mind.
Data model
Lights would be easy to implement as components in an ECS.
Let's say
alias EntityID = Int
record Position { id: EntityID coord: Coord }
record Light { id: EntityID, radius: Int }
record Unit { id: EntityID, glyph: Glyph, vision: Int }
record Entities {
position: Map[EntityID, Position],
...
}
record Tile {
glyph: Glyph
transparent: Boolean
}
alias Map = Tile[MAP_WIDTH][MAP_HEIGHT]
alias LightGrid = Int[MAP_WIDTH][MAP_HEIGHT]
Conclusion
My goal before the next post is to implement the lighting system described above.
Criteria:
- A static 16x16 map some interesting walls to block line of sight.
- A unit with a light source.
- A disembodied light source far away from the unit.
- WASD tile-based movement to demonstrate how the lighting changes.