286 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			286 lines
		
	
	
		
			7.8 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"
 | ||
| )
 | ||
| 
 | ||
| /*
 | ||
| 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 //indexes of cells in CellList
 | ||
| 	lit            int   //lit value
 | ||
| 	wasOccluded    bool
 | ||
| }
 | ||
| 
 | ||
| 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 {
 | ||
| 	initCoords     types.Coords
 | ||
| 	MaxTorchRadius int
 | ||
| 	CellList       CellList
 | ||
| 	FovMap         [][]int
 | ||
| 	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, false})
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	//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) {
 | ||
| 
 | ||
| 	ps.initCoords = initCoords
 | ||
| 
 | ||
| 	if radius > ps.MaxTorchRadius {
 | ||
| 		radius = ps.MaxTorchRadius //fixme
 | ||
| 	}
 | ||
| 
 | ||
| 	level.Tiles[initCoords.X][initCoords.Y].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.Tiles[lc.X][lc.Y].BlocksSight {
 | ||
| 				nextShade[angle] = 1
 | ||
| 			}
 | ||
| 
 | ||
| 			if currentShade[angle] == 0 {
 | ||
| 				cell.lit = cell.lit + 1
 | ||
| 			}
 | ||
| 
 | ||
| 		}
 | ||
| 
 | ||
| 		if level.Tiles[lc.X][lc.Y].BlocksSight {
 | ||
| 			level.Tiles[lc.X][lc.Y].Visible = true
 | ||
| 		}
 | ||
| 
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Coords, radius int) {
 | ||
| 
 | ||
| 	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)
 | ||
| 		if cell.lit > 0 {
 | ||
| 			cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
 | ||
| 			if err != nil {
 | ||
| 				continue
 | ||
| 			}
 | ||
| 			level.Tiles[cs.X][cs.Y].Visible = 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.initCoords.X, lc.Y - ps.initCoords.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
 | ||
| }
 |