• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[JASS] Trying to implement breadth first search

Status
Not open for further replies.
Level 4
Joined
Jan 19, 2008
Messages
69
I have a grid of cells (see attachment 1) and I need find the shortest path between any two of these cells.
So I tried to create something like a breath first search system. This is what my code looks like:

Creating the cells:
JASS:
scope GenerateDungeon initializer init

globals
    integer DungeonRows = 15
    integer DungeonColumns = 16
    integer DungeonSize = DungeonRows * DungeonColumns
    unit array DungeonCell
    boolean array CellOnWBorder
    boolean array CellOnEBorder
    boolean array CellOnSBorder
    boolean array CellOnNBorder
    integer array BFSQueue
    group BFSCellVisited = CreateGroup()
endglobals

private function Generate_Dungeon_Actions takes nothing returns boolean
    local real x = GetUnitX(gg_unit_n000_0001)
    local real y = GetUnitY(gg_unit_n000_0001)
    local real x2
    local real y2
    local real d = 128.
    local integer A
    local integer B = 1
    local integer i = 1

    call RemoveUnit(gg_unit_n000_0001)
    loop
        set A = 1
        loop
            set x2 = x + d * A - d
            set y2 = y + d * B - d
            set DungeonCell[i] = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), 'n000', x2, y2, 270)

            if A == 1 then
                set CellOnWBorder[i] = true
            endif
            if A == DungeonRows then
                set CellOnEBorder[i] = true
            endif
            if B == 1 then
                set CellOnSBorder[i] = true
            endif
            if B == DungeonColumns then
                set CellOnNBorder[i] = true
            endif

            set i = i + 1
            set A = A + 1
            exitwhen A > DungeonRows
        endloop
        set B = B + 1
        exitwhen B > DungeonColumns
    endloop

    return false
endfunction

//===========================================================================
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterTimerEventSingle(t, 0.00 )
    call TriggerAddCondition(t, function Generate_Dungeon_Actions)
    set t = null
endfunction
endscope

Breadth first search:
JASS:
scope BreadthFirstSearch initializer init

private function GetCellNeighbourN takes integer i returns integer
    if CellOnNBorder[i] == true then
        return 0
    else
        return i + DungeonRows
    endif
endfunction

private function GetCellNeighbourS takes integer i returns integer
    if CellOnSBorder[i] == true then
        return 0
    else
        return i - DungeonRows
    endif
endfunction

private function GetCellNeighbourE takes integer i returns integer
    if CellOnEBorder[i] == true then
        return 0
    else
        return i + 1
    endif
endfunction

private function GetCellNeighbourW takes integer i returns integer
    if CellOnWBorder[i] == true then
        return 0
    else
        return i - 1
    endif
endfunction

private function BreadthFirstSearch takes integer i returns nothing
    local integer N
    local integer S
    local integer E
    local integer W
    local integer count = 1
    local integer count2 = 1

    loop
        set N = GetCellNeighbourN(BFSQueue[count])
        if N != 0 and IsUnitInGroup(DungeonCell[N], BFSCellVisited) == false then
            set count2 = count2 + 1
            set BFSQueue[count2] = N
            call GroupAddUnitSimple(DungeonCell[N], BFSCellVisited)
            call SetUnitColor(DungeonCell[N], PLAYER_COLOR_RED)
        endif

        set S = GetCellNeighbourS(BFSQueue[count])
        if S != 0 and IsUnitInGroup(DungeonCell[S], BFSCellVisited) == false then
            set count2 = count2 + 1
            set BFSQueue[count2] = S
            call GroupAddUnitSimple(DungeonCell[S], BFSCellVisited)
            call SetUnitColor(DungeonCell[S], PLAYER_COLOR_BLUE)
        endif

        set E = GetCellNeighbourE(BFSQueue[count])
        if E != 0 and IsUnitInGroup(DungeonCell[E], BFSCellVisited) == false then
            set count2 = count2 + 1
            set BFSQueue[count2] = E
            call GroupAddUnitSimple(DungeonCell[E], BFSCellVisited)
            call SetUnitColor(DungeonCell[E], PLAYER_COLOR_YELLOW)
        endif

        set W = GetCellNeighbourW(BFSQueue[count])
        if W != 0 and IsUnitInGroup(DungeonCell[W], BFSCellVisited) == false then
            set count2 = count2 + 1
            set BFSQueue[count2] = W
            call GroupAddUnitSimple(DungeonCell[W], BFSCellVisited)
            call SetUnitColor(DungeonCell[W], PLAYER_COLOR_PURPLE)
        endif

        set count = count + 1
        exitwhen CountUnitsInGroup(BFSCellVisited) == DungeonSize
    endloop
endfunction

private function StartSearch takes nothing returns boolean
    local unit u = GetTriggerUnit()
    local integer i = 1

    loop
        if DungeonCell[i] == u then
            set BFSQueue[1] = i
            call GroupAddUnitSimple(DungeonCell[i], BFSCellVisited)
            call BreadthFirstSearch(1)
            call SetUnitColor(u, PLAYER_COLOR_GREEN)
        endif
        exitwhen i == DungeonSize or DungeonCell[i] == u
        set i = i + 1
    endloop

    set u = null
    return false
endfunction


//===========================================================================
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerSelectionEventBJ(t, Player(0), true)
    call TriggerAddCondition(t, function StartSearch)
    set t = null
endfunction
endscope

The result is not quite what I'm looking for (see attachment 2). I've been loosely following a tutorial on Red Blob Games: Introduction to A* and I'd like my search result to look like his (see attachment 3). Red circles in attachment 2 correspond to a down arrow in attachment 3, blue circle to an up arrow etc.

I can tell this happens because the order in which cells are queued is fixed (first north, then south, then east, then west). But I can't figure out how to get around this. Any ideas?

Thanks : )

BFS1.png BFS2.pngBFS3.png
 
Last edited:
Level 18
Joined
Jan 1, 2018
Messages
728
Instead of determining the colour of a cell when you enqueue it, instead do this when you dequeue it.
You will have to choose between one of the neighbouring visited cells, using a heuristic.
The most suitable heuristic for your purpose would probably be euclidean distance.

As an example, if you want to know the direction of the cell that is 2 left and 1 up from the center, its neighbouring visited nodes will be the ones below and right of it.
The right one will have a heuristic value of sqrt(2), the one below will be 2. Therefore, you select the node to the right.
Similarly, the cell 1 left and 2 up will favour the node below it.
 
Level 4
Joined
Jan 19, 2008
Messages
69
It works! My only concern right now is that the game freezes up for a second when I start the search.
I ended up using a snippet to get the closest cell rather than a heuristic ([Snippet] GetClosestWidget). Would it be faster to use a heuristic?

I'm also curious if there's anything else I could do to improve the speed of my code to reduce the freezing.

JASS:
scope BreadthFirstSearch initializer init

private function GetCellNeighbourN takes integer i returns integer
    if CellOnNBorder[i] == true then
        return 0
    else
        return i + DungeonRows
    endif
endfunction

private function GetCellNeighbourS takes integer i returns integer
    if CellOnSBorder[i] == true then
        return 0
    else
        return i - DungeonRows
    endif
endfunction

private function GetCellNeighbourE takes integer i returns integer
    if CellOnEBorder[i] == true then
        return 0
    else
        return i + 1
    endif
endfunction

private function GetCellNeighbourW takes integer i returns integer
    if CellOnWBorder[i] == true then
        return 0
    else
        return i - 1
    endif
endfunction

private function BreadthFirstSearch takes nothing returns nothing
    local integer N
    local integer S
    local integer E
    local integer W
    local integer i = 1
    local integer i2 = 1
    local real x = GetUnitX(DungeonCell[BFSNextCell[i]])
    local real y = GetUnitY(DungeonCell[BFSNextCell[i]])
    local unit u

    loop
        set N = GetCellNeighbourN(BFSNextCell[i])
        set E = GetCellNeighbourE(BFSNextCell[i])
        set S = GetCellNeighbourS(BFSNextCell[i])
        set W = GetCellNeighbourW(BFSNextCell[i])

        if N != 0 and IsUnitInGroup(DungeonCell[N], BFSCellVisited) == false then
            call GroupAddUnitSimple(DungeonCell[N], BFSCellQueue)
            call GroupAddUnitSimple(DungeonCell[N], BFSCellVisited)
        endif

        if E != 0 and IsUnitInGroup(DungeonCell[E], BFSCellVisited) == false then
            call GroupAddUnitSimple(DungeonCell[E], BFSCellQueue)
            call GroupAddUnitSimple(DungeonCell[E], BFSCellVisited)
        endif

        if S != 0 and IsUnitInGroup(DungeonCell[S], BFSCellVisited) == false then
            call GroupAddUnitSimple(DungeonCell[S], BFSCellQueue)
            call GroupAddUnitSimple(DungeonCell[S], BFSCellVisited)
        endif

        if W != 0 and IsUnitInGroup(DungeonCell[W], BFSCellVisited) == false then
            call GroupAddUnitSimple(DungeonCell[W], BFSCellQueue)
            call GroupAddUnitSimple(DungeonCell[W], BFSCellVisited)
        endif

        if CountUnitsInGroup(BFSCellQueue) != 0 then
            loop
                set u = GetClosestUnitInGroup(x, y, BFSCellQueue)
                call GroupRemoveUnitSimple(u, BFSCellQueue)

                if u == DungeonCell[N] then
                    set i2 = i2 + 1
                    set BFSNextCell[i2] = N
                    call SetUnitColor(DungeonCell[N], PLAYER_COLOR_RED)
                endif
                if u == DungeonCell[E] then
                    set i2 = i2 + 1
                    set BFSNextCell[i2] = E
                    call SetUnitColor(DungeonCell[E], PLAYER_COLOR_YELLOW)
                endif
                if u == DungeonCell[S] then
                    set i2 = i2 + 1
                    set BFSNextCell[i2] = S
                    call SetUnitColor(DungeonCell[S], PLAYER_COLOR_BLUE)
                endif
                if u == DungeonCell[W] then
                    set i2 = i2 + 1
                    set BFSNextCell[i2] = W
                    call SetUnitColor(DungeonCell[W], PLAYER_COLOR_PURPLE)
                endif

                set u = null
                exitwhen CountUnitsInGroup(BFSCellQueue) == 0
            endloop
        endif
          
        exitwhen CountUnitsInGroup(BFSCellVisited) == DungeonSize
        set i = i + 1
    endloop
endfunction

private function StartSearch takes nothing returns boolean
    local unit u = GetTriggerUnit()
    local integer i = 1

    loop
        if DungeonCell[i] == u then
            set BFSNextCell[1] = i
            call GroupAddUnitSimple(DungeonCell[i], BFSCellVisited)
            call BreadthFirstSearch()
            call SetUnitColor(u, PLAYER_COLOR_GREEN)
        endif
        exitwhen i == DungeonSize or DungeonCell[i] == u
        set i = i + 1
    endloop

    set u = null
    return false
endfunction


//===========================================================================
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerSelectionEventBJ(t, Player(0), true)
    call TriggerAddCondition(t, function StartSearch)
    set t = null
endfunction
endscope

BFS4.png
 
Last edited:
Level 18
Joined
Jan 1, 2018
Messages
728
I'm not entirely sure what you're trying to do here.
You are now 'dequeueing' a unit by using set u = GetClosestUnitInGroup(x, y, BFSCellQueue), so technically you're not using a queue at all.

I tried implementing the solution I gave you myself, but since I don't have your map, I can't test to see if I made any mistakes.
Hopefully it's faster and still works as intended:

JASS:
scope BreadthFirstSearch initializer init

    globals
        // Queue
        private integer queuePointer
        private integer queueSize
        private integer array queue

        // Heuristic
        private real bestHeuristic
        private integer bestDirection
    endglobals

    private function GetCellNeighbourN takes integer i returns integer
        if CellOnNBorder[i] then
            return 0
        endif
        return i + DungeonRows
    endfunction
    private function GetCellNeighbourS takes integer i returns integer
        if CellOnSBorder[i] then
            return 0
        endif
        return i - DungeonRows
    endfunction
    private function GetCellNeighbourE takes integer i returns integer
        if CellOnEBorder[i] then
            return 0
        endif
        return i + 1
    endfunction
    private function GetCellNeighbourW takes integer i returns integer
        if CellOnWBorder[i] then
            return 0
        endif
        return i - 1
    endfunction

    private function Enqueue takes integer cell returns nothing
        set queueSize = queueSize + 1
        set queue[queueSize] = cell
    endfunction
    private function Dequeue takes nothing returns integer
        set queuePointer = queuePointer + 1
        return queue[queuePointer]
    endfunction

    private function TryAddSuccessor takes integer cell returns boolean
        if cell != 0 then
            if IsUnitInGroup( DungeonCell[cell], BFSCellVisited ) then
                return true
            endif
            call GroupAddUnitSimple( DungeonCell[cell], BFSCellVisited )
            call Enqueue( cell )
        endif
        return false
    endfunction

    private function ResetBestHeuristic takes nothing returns nothing
        set bestHeuristic = 2100000000.
        set bestDirection = -1
    endfunction
    private function CompareBestHeuristic takes real heuristic, integer index returns nothing
        if heuristic < bestHeuristic then
            set bestHeuristic = heuristic
            set bestDirection = index
        endif
    endfunction

    private function GetSquaredEuclideanDistance takes real x1, real y1, real x2, real y2 returns real
        local real dx = x1 - x2
        local real dy = y1 - y2
        return dx * dx + dy * dy
    endfunction

    private function BreadthFirstSearch takes integer startCell returns nothing
        local real startX = GetUnitX( DungeonCell[startCell] )
        local real startY = GetUnitY( DungeonCell[startCell] )
        local integer expandedCell
        local integer neighbourCell
        local real heuristic
        local real dx
        local real dy

        set queuePointer = 0
        set queueSize = 0

        call Enqueue( startCell )
        loop
            exitwhen queuePointer >= queueSize // queue empty

            set expandedCell = Dequeue()

            call ResetBestHeuristic()

            set neighbourCell = GetCellNeighbourN(expandedCell)
            if TryAddSuccessor( neighbourCell ) then
                call CompareBestHeuristic( GetSquaredEuclideanDistance( GetUnitX( DungeonCell[neighbourCell] ), GetUnitY( DungeonCell[neighbourCell] ), startX, startY ), 0 )
            endif

            set neighbourCell = GetCellNeighbourE(expandedCell)
            if TryAddSuccessor( neighbourCell ) then
                call CompareBestHeuristic( GetSquaredEuclideanDistance( GetUnitX( DungeonCell[neighbourCell] ), GetUnitY( DungeonCell[neighbourCell] ), startX, startY ), 1 )
            endif

            set neighbourCell = GetCellNeighbourS(expandedCell)
            if TryAddSuccessor( neighbourCell ) then
                call CompareBestHeuristic( GetSquaredEuclideanDistance( GetUnitX( DungeonCell[neighbourCell] ), GetUnitY( DungeonCell[neighbourCell] ), startX, startY ), 2 )
            endif

            set neighbourCell = GetCellNeighbourW(expandedCell)
            if TryAddSuccessor( neighbourCell ) then
                call CompareBestHeuristic( GetSquaredEuclideanDistance( GetUnitX( DungeonCell[neighbourCell] ), GetUnitY( DungeonCell[neighbourCell] ), startX, startY ), 3 )
            endif

            if bestDirection == 0 then
                call SetUnitColor( DungeonCell[expandedCell], PLAYER_COLOR_RED )
            elseif bestDirection == 1 then
                call SetUnitColor( DungeonCell[expandedCell], PLAYER_COLOR_YELLOW )
            elseif bestDirection == 2 then
                call SetUnitColor( DungeonCell[expandedCell], PLAYER_COLOR_BLUE )
            elseif bestDirection == 3 then
                call SetUnitColor( DungeonCell[expandedCell], PLAYER_COLOR_PURPLE )
            else//if bestDirection == -1 then
                call SetUnitColor( DungeonCell[expandedCell], PLAYER_COLOR_GREEN )
            endif
        endloop
    endfunction

    private function StartSearch takes nothing returns boolean
        local unit u = GetTriggerUnit()
        local integer i = 1

        call GroupClear( BFSCellVisited )
        loop
            if DungeonCell[i] == u then
                call GroupAddUnitSimple(DungeonCell[i], BFSCellVisited)
                call BreadthFirstSearch( i )
                exitwhen true
            endif
            exitwhen i == DungeonSize
            set i = i + 1
        endloop

        set u = null
        return false
    endfunction


    //===========================================================================
    private function init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerSelectionEventBJ(t, Player(0), true)
        call TriggerAddCondition(t, function StartSearch)
        set t = null
    endfunction

endscope
 
Last edited:
Level 4
Joined
Jan 19, 2008
Messages
69
Hi Drake. I tried your code and the results were a little strange (see attachment 1).
So instead I tried using your queue- and dequeue functions and your CompareBestHeuristic function to enqueue new cells based on their distance to the center. Doing this I got the result I wanted (see attachment 2) and the game no longer freezes up!

Again -- thank you so much for your help!

If you're curious, this is what the code looks like:
JASS:
scope BreadthFirstSearch initializer init

    globals
    // Queue
    private integer queuePointer
    private integer queueSize
    private integer array queue

    // Heuristic
    private real bestHeuristic
    private integer bestDirection
    endglobals

    private function GetCellNeighbourN takes integer i returns integer
    if CellOnNBorder[i] then
        return 0
        endif

        return i + DungeonRows
    endfunction

    private function GetCellNeighbourS takes integer i returns integer
    if CellOnSBorder[i] then
        return 0
    endif

        return i - DungeonRows
    endfunction

    private function GetCellNeighbourE takes integer i returns integer
    if CellOnEBorder[i] then
        return 0
        endif

    return i + 1
    endfunction

    private function GetCellNeighbourW takes integer i returns integer
        if CellOnWBorder[i] then
        return 0
        endif

        return i - 1
    endfunction

    private function Enqueue takes integer cell returns nothing
        set queueSize = queueSize + 1
        set queue[queueSize] = cell
    endfunction

    private function Dequeue takes nothing returns integer
        set queuePointer = queuePointer + 1
        return queue[queuePointer]
    endfunction

    private function ResetBestHeuristic takes nothing returns nothing
        set bestHeuristic = 9999999
        set bestDirection = -1
    endfunction

    private function CompareBestHeuristic takes real heuristic, integer index returns nothing
    if heuristic < bestHeuristic then
        set bestHeuristic = heuristic
        set bestDirection = index
    endif
    endfunction

    private function GetSquaredEuclideanDistance takes real x1, real y1, real x2, real y2 returns real
        local real dx = x1 - x2
        local real dy = y1 - y2
        return dx * dx + dy * dy
    endfunction

    private function BreadthFirstSearch takes integer startCell returns nothing
    local real startX = GetUnitX( DungeonCell[startCell] )
    local real startY = GetUnitY( DungeonCell[startCell] )
    local integer expandedCell
    local real heuristic
    local real dx
    local real dy
    local integer N
    local integer E
    local integer S
    local integer W

    set queuePointer = 0
    set queueSize = 0

    call Enqueue( startCell )
    loop
        exitwhen queuePointer >= queueSize // queue empty

        set expandedCell = Dequeue()

        set N = GetCellNeighbourN(expandedCell)
        set E = GetCellNeighbourE(expandedCell)
        set S = GetCellNeighbourS(expandedCell)
        set W = GetCellNeighbourW(expandedCell)

        loop
            call ResetBestHeuristic()
            if N != 0 and IsUnitInGroup(DungeonCell[N], BFSCellVisited) == false then
                call CompareBestHeuristic(GetSquaredEuclideanDistance(GetUnitX( DungeonCell[N]), GetUnitY(DungeonCell[N]), startX, startY), 0)
            endif

            if E != 0 and IsUnitInGroup(DungeonCell[E], BFSCellVisited) == false then
                call CompareBestHeuristic(GetSquaredEuclideanDistance(GetUnitX( DungeonCell[E]), GetUnitY(DungeonCell[E]), startX, startY), 1)
            endif

            if S != 0 and IsUnitInGroup(DungeonCell[S], BFSCellVisited) == false then
                call CompareBestHeuristic(GetSquaredEuclideanDistance(GetUnitX( DungeonCell[S]), GetUnitY(DungeonCell[S]), startX, startY), 2)
            endif

            if W != 0 and IsUnitInGroup(DungeonCell[W], BFSCellVisited) == false then
                call CompareBestHeuristic(GetSquaredEuclideanDistance(GetUnitX( DungeonCell[W]), GetUnitY(DungeonCell[W]), startX, startY), 3)
            endif

            if bestDirection == 0 then
                call GroupAddUnitSimple(DungeonCell[N], BFSCellVisited)
                call Enqueue(N)
                        call SetUnitColor( DungeonCell[N], PLAYER_COLOR_RED )
            elseif bestDirection == 1 then
                call GroupAddUnitSimple(DungeonCell[E], BFSCellVisited)
                call Enqueue(E)
                call SetUnitColor( DungeonCell[E], PLAYER_COLOR_YELLOW )
            elseif bestDirection == 2 then
                call GroupAddUnitSimple(DungeonCell[S], BFSCellVisited)
                call Enqueue(S)
                call SetUnitColor( DungeonCell[S], PLAYER_COLOR_BLUE )
            elseif bestDirection == 3 then
                call GroupAddUnitSimple(DungeonCell[W], BFSCellVisited)
                call Enqueue(W)
                call SetUnitColor( DungeonCell[W], PLAYER_COLOR_PURPLE )
            endif
       
            exitwhen bestDirection == -1
        endloop
        endloop
    endfunction

    private function StartSearch takes nothing returns boolean
    local unit u = GetTriggerUnit()
    local integer i = 1

        loop
        if DungeonCell[i] == u then
            call GroupAddUnitSimple(DungeonCell[i], BFSCellVisited)
            call BreadthFirstSearch( i )
            exitwhen true
                endif
        exitwhen i == DungeonSize
        set i = i + 1
        endloop

        set u = null
        return false
    endfunction


    //===========================================================================
    private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
        call TriggerRegisterPlayerSelectionEventBJ(t, Player(0), true)
        call TriggerAddCondition(t, function StartSearch)
        set t = null
    endfunction

endscope


BFS5.png BFS6.png
 
Last edited:
Level 18
Joined
Jan 1, 2018
Messages
728
Nice to hear you got it working without freezes now.

I took another look at my code to see why it bugged like that, and found there was an error in function TryAddSuccessor.
Also worth mentioning, since the heuristic is squared distance, 999999 is only 1000 distance, but I see you already added an additional 9 to fix that.

I updated my previous post with these changes (tested with world editor this time).
Glad I could help you with your problem ^^.
 
Status
Not open for further replies.
Top