mirror of
https://github.com/wisplite/a-star-go.git
synced 2026-06-27 15:37:07 -05:00
live preview
This commit is contained in:
@@ -7,6 +7,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parentNone marks cells with no predecessor; must not collide with 0–3 (cardinal directions).
|
||||||
|
const parentNone byte = 0xff
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
index int // index of the cell in the grid (y * width + x)
|
index int // index of the cell in the grid (y * width + x)
|
||||||
priority float32 // f = g + h
|
priority float32 // f = g + h
|
||||||
@@ -71,6 +74,9 @@ func (a *AStar) Init(width int, height int) {
|
|||||||
for i := range a.gScores {
|
for i := range a.gScores {
|
||||||
a.gScores[i] = math.MaxFloat32
|
a.gScores[i] = math.MaxFloat32
|
||||||
}
|
}
|
||||||
|
for i := range a.parents {
|
||||||
|
a.parents[i] = parentNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AStar) ResetGrid(withTypes bool) {
|
func (a *AStar) ResetGrid(withTypes bool) {
|
||||||
@@ -79,7 +85,7 @@ func (a *AStar) ResetGrid(withTypes bool) {
|
|||||||
a.gridTypes[i] = 0
|
a.gridTypes[i] = 0
|
||||||
}
|
}
|
||||||
a.gScores[i] = math.MaxFloat32
|
a.gScores[i] = math.MaxFloat32
|
||||||
a.parents[i] = 0
|
a.parents[i] = parentNone
|
||||||
a.closedSet[i] = false
|
a.closedSet[i] = false
|
||||||
}
|
}
|
||||||
a.openSet = a.openSet[:0]
|
a.openSet = a.openSet[:0]
|
||||||
@@ -93,6 +99,9 @@ func (a *AStar) RebuildGrid(width int, height int) {
|
|||||||
a.closedSet = make([]bool, width*height)
|
a.closedSet = make([]bool, width*height)
|
||||||
a.width = width
|
a.width = width
|
||||||
a.height = height
|
a.height = height
|
||||||
|
for i := range a.parents {
|
||||||
|
a.parents[i] = parentNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AStar) SetHeuristic(heuristic int32) {
|
func (a *AStar) SetHeuristic(heuristic int32) {
|
||||||
@@ -162,21 +171,30 @@ func (a *AStar) GetParent(x int, y int) byte {
|
|||||||
return a.parents[y*a.width+x]
|
return a.parents[y*a.width+x]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) {
|
func (a *AStar) GetParents() []byte {
|
||||||
if parent == 0 {
|
return a.parents
|
||||||
return childx - 1, childy // parent left
|
}
|
||||||
} else if parent == 1 {
|
|
||||||
return childx, childy - 1 // parent above
|
func (a *AStar) ParentIndexToXY(childx int, childy int, parent byte) (int, int) {
|
||||||
} else if parent == 2 {
|
switch parent {
|
||||||
return childx + 1, childy // parent right
|
case 0:
|
||||||
} else if parent == 3 {
|
return childx - 1, childy // parent left
|
||||||
return childx, childy + 1 // parent below
|
case 1:
|
||||||
|
return childx, childy - 1 // parent above
|
||||||
|
case 2:
|
||||||
|
return childx + 1, childy // parent right
|
||||||
|
case 3:
|
||||||
|
return childx, childy + 1 // parent below
|
||||||
|
default:
|
||||||
|
return -1, -1
|
||||||
}
|
}
|
||||||
return childx, childy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AStar) ParentIndexToXYIndex(childx int, childy int, parent byte) int {
|
func (a *AStar) ParentIndexToXYIndex(childx int, childy int, parent byte) int {
|
||||||
x, y := a.ParentIndexToXY(childx, childy, parent)
|
x, y := a.ParentIndexToXY(childx, childy, parent)
|
||||||
|
if x < 0 || y < 0 || x >= a.width || y >= a.height {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
return y*a.width + x
|
return y*a.width + x
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +253,15 @@ func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]in
|
|||||||
// We've found the goal!
|
// We've found the goal!
|
||||||
fmt.Println("Found the goal!")
|
fmt.Println("Found the goal!")
|
||||||
path := make([][]int, 0)
|
path := make([][]int, 0)
|
||||||
for currentIndex := current.index; currentIndex != startIndex; currentIndex = a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex]) {
|
for currentIndex := current.index; currentIndex != startIndex; {
|
||||||
x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, a.parents[currentIndex])
|
p := a.parents[currentIndex]
|
||||||
|
next := a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, p)
|
||||||
|
if next < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, p)
|
||||||
path = append(path, []int{x, y})
|
path = append(path, []int{x, y})
|
||||||
|
currentIndex = next
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
@@ -264,3 +288,60 @@ func (a *AStar) CalculatePath(startX int, startY int, endX int, endY int) [][]in
|
|||||||
}
|
}
|
||||||
return make([][]int, 0)
|
return make([][]int, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AStar) CalculatePathLive(startX int, startY int, endX int, endY int, updateChan chan int) [][]int {
|
||||||
|
timer := time.Now()
|
||||||
|
defer func() {
|
||||||
|
a.timeTaken = time.Since(timer)
|
||||||
|
}()
|
||||||
|
startIndex := startY*a.width + startX
|
||||||
|
endIndex := endY*a.width + endX
|
||||||
|
a.gScores[startIndex] = 0
|
||||||
|
startF := a.heuristic(startX, startY, endX, endY)
|
||||||
|
heap.Push(&a.openSet, &Item{index: startIndex, priority: startF, gScore: 0})
|
||||||
|
|
||||||
|
for a.openSet.Len() > 0 {
|
||||||
|
current := heap.Pop(&a.openSet).(*Item)
|
||||||
|
if a.closedSet[current.index] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if current.index == endIndex {
|
||||||
|
// We've found the goal!
|
||||||
|
fmt.Println("Found the goal!")
|
||||||
|
path := make([][]int, 0)
|
||||||
|
for currentIndex := current.index; currentIndex != startIndex; {
|
||||||
|
p := a.parents[currentIndex]
|
||||||
|
next := a.ParentIndexToXYIndex(currentIndex%a.width, currentIndex/a.width, p)
|
||||||
|
if next < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
x, y := a.ParentIndexToXY(currentIndex%a.width, currentIndex/a.width, p)
|
||||||
|
path = append(path, []int{x, y})
|
||||||
|
currentIndex = next
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
a.closedSet[current.index] = true
|
||||||
|
updateChan <- current.index
|
||||||
|
|
||||||
|
for _, neighborIndex := range a.GetNeighbors(current.index%a.width, current.index/a.width) {
|
||||||
|
if a.closedSet[neighborIndex] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a.gridTypes[neighborIndex] == 1 {
|
||||||
|
a.gScores[neighborIndex] = math.MaxFloat32
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
terrainCost := a.GetTerrainCost(neighborIndex%a.width, neighborIndex/a.width)
|
||||||
|
tentativeGScore := a.gScores[current.index] + terrainCost
|
||||||
|
if tentativeGScore < a.gScores[neighborIndex] {
|
||||||
|
a.SetParent(neighborIndex%a.width, neighborIndex/a.width, current.index%a.width, current.index/a.width)
|
||||||
|
a.gScores[neighborIndex] = tentativeGScore
|
||||||
|
priority := tentativeGScore + a.heuristic(neighborIndex%a.width, neighborIndex/a.width, endX, endY)
|
||||||
|
heap.Push(&a.openSet, &Item{index: neighborIndex, priority: priority, gScore: tentativeGScore})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return make([][]int, 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -315,11 +315,16 @@ func main() {
|
|||||||
defer rl.UnloadTexture(mapTexture)
|
defer rl.UnloadTexture(mapTexture)
|
||||||
defer rl.UnloadImage(mapImage)
|
defer rl.UnloadImage(mapImage)
|
||||||
|
|
||||||
|
updateChan := make(chan int, 100000)
|
||||||
|
|
||||||
autoCompute := false
|
autoCompute := false
|
||||||
|
|
||||||
astar := AStar{}
|
astar := AStar{}
|
||||||
astar.Init(width, height)
|
astar.Init(width, height)
|
||||||
|
|
||||||
|
// Above the channel loop
|
||||||
|
lastEvaluatedNode := -1 // Track the "tip of the spear"
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
screenWidth := float32(rl.GetScreenWidth())
|
screenWidth := float32(rl.GetScreenWidth())
|
||||||
screenHeight := float32(rl.GetScreenHeight())
|
screenHeight := float32(rl.GetScreenHeight())
|
||||||
@@ -459,6 +464,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
lastMousePos = rl.NewVector2(-1, -1)
|
lastMousePos = rl.NewVector2(-1, -1)
|
||||||
}
|
}
|
||||||
|
updatesProcessed := 0
|
||||||
|
DrainLoop: // Use a label so we can break out of the infinite 'for' loop
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case nodeIndex := <-updateChan:
|
||||||
|
x := nodeIndex % width
|
||||||
|
y := nodeIndex / width
|
||||||
|
if x != int(startPos.X) || y != int(startPos.Y) {
|
||||||
|
// Bake the blue pixel into the image
|
||||||
|
rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255))
|
||||||
|
tex.markRegion(x, y, x, y, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEvaluatedNode = nodeIndex // Save the absolute latest node
|
||||||
|
updatesProcessed++
|
||||||
|
|
||||||
|
if updatesProcessed > 50000 {
|
||||||
|
break DrainLoop
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Channel is empty
|
||||||
|
break DrainLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- DRAWING ---
|
// --- DRAWING ---
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
@@ -475,6 +504,42 @@ func main() {
|
|||||||
cellSize, // Scale factor
|
cellSize, // Scale factor
|
||||||
rl.White, // Tint (White means no tint)
|
rl.White, // Tint (White means no tint)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 2. Trace parent chain and draw a yellow polyline (cell centers) each frame.
|
||||||
|
if lastEvaluatedNode != -1 {
|
||||||
|
startIndex := int(startPos.Y)*width + int(startPos.X)
|
||||||
|
parents := astar.GetParents()
|
||||||
|
pathThickness := float32(10.0) / (camera.Zoom / 0.75)
|
||||||
|
if pathThickness < 1 {
|
||||||
|
pathThickness = 1
|
||||||
|
}
|
||||||
|
pathColor := rl.NewColor(255, 255, 0, 255)
|
||||||
|
|
||||||
|
var centers []rl.Vector2
|
||||||
|
for idx := lastEvaluatedNode; idx >= 0 && idx < width*height && idx != startIndex; {
|
||||||
|
cx := (float32(idx%width) + 0.5) * cellSize
|
||||||
|
cy := (float32(idx/width) + 0.5) * cellSize
|
||||||
|
centers = append(centers, rl.NewVector2(cx, cy))
|
||||||
|
|
||||||
|
if idx == startIndex {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p := parents[idx]
|
||||||
|
if p == parentNone {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next := astar.ParentIndexToXYIndex(idx%width, idx/width, p)
|
||||||
|
if next < 0 || next == idx {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
idx = next
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(centers)-1; i++ {
|
||||||
|
rl.DrawLineEx(centers[i], centers[i+1], pathThickness, pathColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drawInfiniteGridLines(camera, canvasWidth, screenHeight, cellSize, width, height)
|
drawInfiniteGridLines(camera, canvasWidth, screenHeight, cellSize, width, height)
|
||||||
rl.EndMode2D()
|
rl.EndMode2D()
|
||||||
rl.EndScissorMode()
|
rl.EndScissorMode()
|
||||||
@@ -550,6 +615,7 @@ func main() {
|
|||||||
if !toolDropdownOpen && !heuristicDropdownOpen {
|
if !toolDropdownOpen && !heuristicDropdownOpen {
|
||||||
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") {
|
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") {
|
||||||
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
||||||
|
lastEvaluatedNode = -1
|
||||||
gridTypes := astar.GetGridTypes()
|
gridTypes := astar.GetGridTypes()
|
||||||
for i, gridType := range gridTypes {
|
for i, gridType := range gridTypes {
|
||||||
// reset the map image
|
// reset the map image
|
||||||
@@ -568,6 +634,34 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(80*scale)), (180*scale), (30*scale)), "Calculate Path (Live)") {
|
||||||
|
if int(startPos.X) < 0 || int(startPos.X) >= width || int(startPos.Y) < 0 || int(startPos.Y) >= height || int(endPos.X) < 0 || int(endPos.X) >= width || int(endPos.Y) < 0 || int(endPos.Y) >= height {
|
||||||
|
posError = true
|
||||||
|
} else {
|
||||||
|
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
||||||
|
astar.SetHeuristic(activeHeuristic)
|
||||||
|
lastEvaluatedNode = -1
|
||||||
|
gridTypes := astar.GetGridTypes()
|
||||||
|
for i, gridType := range gridTypes {
|
||||||
|
// reset the map image
|
||||||
|
switch gridType {
|
||||||
|
case 0:
|
||||||
|
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255))
|
||||||
|
case 1:
|
||||||
|
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255))
|
||||||
|
case 2:
|
||||||
|
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255))
|
||||||
|
case 3:
|
||||||
|
rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tex.markFull()
|
||||||
|
go func() {
|
||||||
|
astar.CalculatePathLive(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y), updateChan)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AutoCompute
|
// AutoCompute
|
||||||
autoCompute = rg.CheckBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(40*scale)), (30*scale), (30*scale)), "", autoCompute) // no title because it would overlap the button
|
autoCompute = rg.CheckBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(40*scale)), (30*scale), (30*scale)), "", autoCompute) // no title because it would overlap the button
|
||||||
|
|
||||||
@@ -578,6 +672,7 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating
|
||||||
astar.SetHeuristic(activeHeuristic)
|
astar.SetHeuristic(activeHeuristic)
|
||||||
|
lastEvaluatedNode = -1
|
||||||
gridTypes := astar.GetGridTypes()
|
gridTypes := astar.GetGridTypes()
|
||||||
for i, gridType := range gridTypes {
|
for i, gridType := range gridTypes {
|
||||||
// reset the map image
|
// reset the map image
|
||||||
|
|||||||
Reference in New Issue
Block a user