312 lines
9.0 KiB
Go
312 lines
9.0 KiB
Go
package precomputed_shade
|
||
|
||
import (
|
||
"bytes"
|
||
"errors"
|
||
"lab.zaar.be/thefish/alchemyst-go/engine/fov/basic"
|
||
"lab.zaar.be/thefish/alchemyst-go/engine/gamemap"
|
||
"lab.zaar.be/thefish/alchemyst-go/engine/types"
|
||
"math"
|
||
"sort"
|
||
)
|
||
|
||
//Реализация алгоритма от chilly_durango
|
||
//https://www.reddit.com/r/roguelikedev/comments/5n1tx3/fov_algorithm_sharencompare/
|
||
//Пока не побеждена засветка стен (или это у меня руки кривые) - сложность почти O(N^2)
|
||
|
||
//Не только у меня:
|
||
//chilly_durango:
|
||
//I turned down the Radio 4 in my car to think about this on the way home - that's how much this question has been
|
||
// bugging me. The two best from all the awful compromises I could consider were:
|
||
// - Only lighting walls with light from the player (cheap trick, dutch)
|
||
// - Match light value for walls with the highest light value in adjacent floor cell visible to player (seems costly)
|
||
|
||
|
||
|
||
/*
|
||
Why am I here? Well, I just don't know what to call it - I'm sure it's an established method, and I'm aware there are
|
||
probably optimisations to be had. I thought if I roughed out the algorithm here, the r/roguelikedev community would
|
||
surely be able to help! I haven't included optimisations here, but if anyone wants them I got 'em :)
|
||
|
||
Method
|
||
|
||
Beforehand
|
||
|
||
- List the cells in your largest-possible FOV, storing X and Y values relative to the center.
|
||
- Store the distance from the center for each cell, and sort the list by this in ascending order.
|
||
- Store the range of angles occludedAngles by each cell in this list, in clockwise order as absolute integers only.
|
||
- Create a 360-char string of 0s called EmptyShade, and a 360-char string of 1s called FullShade
|
||
|
||
Runtime
|
||
|
||
- Store two strings – CurrentShade and NextShade
|
||
- Set CurrentShade to EmptyShade to start.
|
||
- While CurrentShade =/= FullShade: step through the Cell List:
|
||
|
||
- If the distance to the current cell is not equal to the previous distance checked then replace the contents
|
||
of the CurrentShade variable with the contents of the NextShade variable.
|
||
|
||
- If the tested cell is opaque – for each angle in the range occludedAngles by the cell, place a 1 at the position
|
||
determined by angle%360 in the NextShade string.
|
||
|
||
- For each angle in the range occludedAngles by the cell, add 1 to the lit value for that cell for each 0
|
||
encountered at the position determined by angle%360 in the CurrentShade string.
|
||
|
||
Notes
|
||
Benefits
|
||
|
||
No messing around with octants
|
||
|
||
Highly efficient - each cell is only visited once, and checks within that cell are rare. It performs as fast as any
|
||
other LOS I've tried but with more options
|
||
|
||
Human-readable - code and output are highly legible, making it very easy to work with
|
||
|
||
Flexible - I'm using it for FOV, LOS, renderer, and lighting. Each process is calling the same function, within which
|
||
flags control how much data is evaluated and output. It only uses the data it needs to in the context where
|
||
its needed – so monsters that need a list of things they can see only check if a cell is visible or not, and don’t
|
||
bother calculating how much visibility they have there. This cuts processing dramatically.
|
||
|
||
Other links
|
||
|
||
cfov by Konstantin Stupnik on RogueTemple
|
||
|
||
Pre-Computed Visiblity Trees on RogueBasin
|
||
|
||
/r/roguelikedev FAQ Friday on FOV which kicked off this train of thought
|
||
|
||
/u/pnjeffries on his FOV algorithm which inspired this one
|
||
|
||
Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed
|
||
*/
|
||
|
||
var errNotFoundCell = errors.New("Cell not found")
|
||
var errOutOfBounds = errors.New("Cell out of bounds")
|
||
|
||
type Cell struct {
|
||
types.Coords
|
||
distance float64
|
||
occludedAngles []int //angles occluded by this cell
|
||
lit int //light "amount"
|
||
}
|
||
|
||
type CellList []*Cell
|
||
|
||
type DistanceSorter CellList
|
||
|
||
func (a DistanceSorter) Len() int { return len(a) }
|
||
func (a DistanceSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
func (a DistanceSorter) Less(i, j int) bool { return a[i].distance < a[j].distance }
|
||
|
||
type precomputedShade struct {
|
||
originCoords types.Coords
|
||
MaxTorchRadius int
|
||
CellList CellList
|
||
LightWalls bool
|
||
}
|
||
|
||
func NewPrecomputedShade(maxTorchRadius int) *precomputedShade {
|
||
result := &precomputedShade{MaxTorchRadius: maxTorchRadius, LightWalls: true}
|
||
result.PrecomputeFovMap()
|
||
return result
|
||
}
|
||
|
||
func (ps *precomputedShade) FindByCoords(c types.Coords) (int, *Cell, error) {
|
||
for i := range ps.CellList {
|
||
if ps.CellList[i].Coords == c {
|
||
// Found!
|
||
return i, ps.CellList[i], nil
|
||
}
|
||
}
|
||
return 0, &Cell{}, errNotFoundCell
|
||
}
|
||
|
||
func (ps *precomputedShade) IsInFov(coords types.Coords) bool {
|
||
rc := ps.fromLevelCoords(coords)
|
||
_, cell, err := ps.FindByCoords(rc)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return cell.lit > 0
|
||
}
|
||
|
||
func (ps *precomputedShade) SetLightWalls(value bool) {
|
||
ps.LightWalls = value
|
||
}
|
||
|
||
func (ps *precomputedShade) Init() {
|
||
ps.PrecomputeFovMap()
|
||
}
|
||
|
||
func (ps *precomputedShade) PrecomputeFovMap() {
|
||
max := ps.MaxTorchRadius
|
||
minusMax := (-1) * max
|
||
zeroCoords := types.Coords{0, 0}
|
||
var x, y int
|
||
//fill list
|
||
for x = minusMax; x < max+1; x++ {
|
||
for y = minusMax; y < max+1; y++ {
|
||
if x == 0 && y == 0 {
|
||
continue;
|
||
}
|
||
iterCoords := types.Coords{x, y}
|
||
distance := zeroCoords.DistanceTo(iterCoords)
|
||
if distance <= float64(max) {
|
||
ps.CellList = append(ps.CellList, &Cell{iterCoords, distance, nil, 0})
|
||
}
|
||
}
|
||
}
|
||
//Do not change cell order after this!
|
||
sort.Sort(DistanceSorter(ps.CellList))
|
||
//debug
|
||
//for _, cell := range ps.CellList {
|
||
// fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
|
||
//}
|
||
|
||
//Bresanham lines / Raycast
|
||
var lineX, lineY float64
|
||
for i := 0; i < 360; i++ {
|
||
dx := math.Sin(float64(i) / (float64(180) / math.Pi))
|
||
dy := math.Cos(float64(i) / (float64(180) / math.Pi))
|
||
|
||
lineX = 0
|
||
lineY = 0
|
||
for j := 0; j < max; j++ {
|
||
lineX -= dx
|
||
lineY -= dy
|
||
|
||
roundedX := int(basic.Round(lineX))
|
||
roundedY := int(basic.Round(lineY))
|
||
|
||
_, cell, err := ps.FindByCoords(types.Coords{roundedX, roundedY})
|
||
|
||
if err != nil {
|
||
//inexistent coord found
|
||
break;
|
||
}
|
||
cell.occludedAngles = unique(append(cell.occludedAngles, i))
|
||
}
|
||
|
||
}
|
||
|
||
//for _, cell := range ps.CellList {
|
||
// fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
|
||
//}
|
||
}
|
||
|
||
func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords, radius int) {
|
||
for i, _ := range ps.CellList {
|
||
ps.CellList[i].lit = 0
|
||
}
|
||
ps.originCoords = initCoords
|
||
|
||
if radius > ps.MaxTorchRadius {
|
||
radius = ps.MaxTorchRadius //fixme
|
||
}
|
||
|
||
level.GetTile(initCoords).Visible = true
|
||
|
||
var fullShade = make([]byte, 360)
|
||
for i := range fullShade {
|
||
fullShade[i] = 1
|
||
}
|
||
var emptyShade = make([]byte, 360)
|
||
currentShade := emptyShade
|
||
nextShade := emptyShade
|
||
|
||
i := 0
|
||
prevDistance := 0.0
|
||
for !bytes.Equal(currentShade, fullShade) {
|
||
if (i == len(ps.CellList)-1) {
|
||
break
|
||
}
|
||
cell := ps.CellList[i]
|
||
i++
|
||
if cell.distance != prevDistance {
|
||
currentShade = nextShade
|
||
}
|
||
|
||
if cell.distance > float64(radius) {
|
||
break
|
||
}
|
||
|
||
lc, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
//fmt.Printf("\n level coords: %v", lc)
|
||
for _, angle := range cell.occludedAngles {
|
||
|
||
if level.GetTile(lc).BlocksSight {
|
||
nextShade[angle] = 1
|
||
}
|
||
|
||
if currentShade[angle] == 0 {
|
||
cell.lit = cell.lit + 1
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Coords, radius int) {
|
||
|
||
level.SetAllInvisible()
|
||
ps.recalc(level, initCoords, radius)
|
||
|
||
for _, cell := range ps.CellList {
|
||
//fmt.Printf("\n coords: %v, distance: %f, lit: %d", cell.Coords, cell.distance, cell.lit)
|
||
cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
||
if cell.lit > 0 {
|
||
if err != nil {
|
||
continue
|
||
}
|
||
level.GetTile(cs).Visible = true
|
||
level.GetTile(cs).Explored = true
|
||
}
|
||
|
||
//light walls, crutch
|
||
if level.GetTile(cs).BlocksSight && ps.LightWalls {
|
||
if cell.IsAdjacentTo(&types.Coords{0,0}) {
|
||
level.GetTile(cs).Visible = true
|
||
} else {
|
||
for _, maybeNb := range ps.CellList {
|
||
if //int(maybeNb.distance) == int(cell.distance-1) &&
|
||
maybeNb.IsAdjacentTo(&cell.Coords) &&
|
||
(maybeNb.X == cell.X || maybeNb.Y == cell.Y) &&
|
||
maybeNb.lit > 0 { //magic constant!
|
||
level.GetTile(cs).Visible = true
|
||
level.GetTile(cs).Explored = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (ps *precomputedShade) toLevelCoords(level *gamemap.Level, initCoords, relativeCoords types.Coords) (types.Coords, error) {
|
||
realCoords := types.Coords{initCoords.X + relativeCoords.X, initCoords.Y + relativeCoords.Y}
|
||
if !level.InBounds(realCoords) {
|
||
return types.Coords{}, errOutOfBounds
|
||
}
|
||
return realCoords, nil
|
||
}
|
||
|
||
func (ps *precomputedShade) fromLevelCoords(lc types.Coords) types.Coords {
|
||
relativeCoords := types.Coords{lc.X - ps.originCoords.X, lc.Y - ps.originCoords.Y}
|
||
return relativeCoords
|
||
}
|
||
|
||
func unique(intSlice []int) []int {
|
||
keys := make(map[int]bool)
|
||
list := []int{}
|
||
for _, entry := range intSlice {
|
||
if _, value := keys[entry]; !value {
|
||
keys[entry] = true
|
||
list = append(list, entry)
|
||
}
|
||
}
|
||
return list
|
||
}
|