• 🏆 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!

Wander

Library Wander


Libary Wander mimics the native Wander ability, while
granting advanced controll over the unit movement.​
Obligatory RequirementsOptional Requirements



Wander

JASS:
native UnitAlive takes unit id returns boolean

library Wander uses Table optional TimerUtils
//==================================================================
// Version: 1.4.2
// Author: BPower
//==================================================================
// Libary Wander mimics the native Wander ability, while
// granting advanced controll over the unit movement.
//==================================================================
// Credits to:
//     Vexorian for TimerUtils    -    wc3c.net/showthread.php?t=101322
//     Bribe for Table            -    hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
//==================================================================
// API:
//
//    Methods:
//
//        static method operator [] takes unit whichUnit returns thistype
//
//        static method create takes unit whichUnit, real range, real cooldown returns thistype
//
//        method destroy takes nothing returns nothing
//
//        method exists takes nothing returns boolean
//
//        method pause takes nothing returns nothing
//
//        method resume takes nothing returns nothing
//
//    Fields:
//
//        readonly unit   source
//                 real   radius
//                 real   centerX
//                 real   centerY
//                 real   timeout ( The timer timeout )
//                 real   random  ( Optionally adds a random interval to the timer timeout )
//                 string order   ( Either "move" or "attack" are recommended )
//
//==================================================================
// User settings:

    globals
        private constant real   DEFAULT_TIMER_TIMEOUT = 1.0
        private constant string DEFAULT_ORDER_STRING  = "move"
    endglobals
    
//==================================================================
// Wander code. Make changes carefully.
//==================================================================
    
    globals
        private Table table
    endglobals
    
    private module Init
        private static method onInit takes nothing returns nothing
            call thistype.init()
        endmethod
    endmodule

    private function Random takes nothing returns real
        return GetRandomReal(0., 1.)
    endfunction
    
    struct Wander// extends array
        // implement Alloc
        //
        // Struct members:
        readonly unit   source
                 real   radius
                 real   centerX
                 real   centerY
                 real   timeout
                 real   random 
                 string order
                 
        private  timer  tmr
        
        method operator exists takes nothing returns boolean
            return tmr != null
        endmethod
        
        static method operator [] takes unit whichUnit returns thistype
            return table[GetHandleId(whichUnit)]
        endmethod
        
        method pause takes nothing returns nothing
            call PauseTimer(tmr)
        endmethod
        
        method resume takes nothing returns nothing
            call ResumeTimer(tmr)
        endmethod
            
        method destroy takes nothing returns nothing
            if not exists then 
                return
            endif
            
            call deallocate()
            call table.remove(GetHandleId(source))
            static if LIBRARY_TimerUtils then
                call ReleaseTimer(tmr)
            else
                call table.remove(GetHandleId(tmr))
                call DestroyTimer(tmr)
            endif
            
            set source = null
            set tmr    = null
        endmethod
        
        method issueWander takes nothing returns boolean
            local real t = Random()*2*bj_PI
            local real r = Random() + Random()
            if r > 1. then 
                set r = (2 - r)*radius
            else
                set r = r*radius
            endif
            
            return IssuePointOrder(source, order, centerX + r*Cos(t), centerY + r*Sin(t))
        endmethod
        
        private static method onPeriodic takes nothing returns nothing
            static if LIBRARY_TimerUtils then
                local thistype this = GetTimerData(GetExpiredTimer())
            else
                local thistype this = table[GetHandleId(GetExpiredTimer())]
            endif
                
            if UnitAlive(source) then
                   // There are a few order ids, which eventually mess with the following unit order comparison.
                   // For example order id 851974. An endless going, undocumented order serving no obvious purpose.
                if GetUnitCurrentOrder(source) == 0 and issueWander() then
                    call TimerStart(tmr, timeout + random*Random(), true, function thistype.onPeriodic)
                else
                    // Update in short intervals until a wander order is issued.
                    call TimerStart(tmr, DEFAULT_TIMER_TIMEOUT, true, function thistype.onPeriodic)
                endif
            else
                call destroy()
            endif
        endmethod
    
        static method create takes unit whichUnit, real range, real cooldown returns thistype
            local thistype this = thistype[whichUnit]
            
            if not exists then
                set this   = thistype.allocate()
                set source = whichUnit
                set order  = DEFAULT_ORDER_STRING
                set table[GetHandleId(whichUnit)] = this
                
                static if not LIBRARY_TimerUtils then
                    set tmr = CreateTimer()
                    set table[GetHandleId(tmr)] = this
                else
                    set tmr = NewTimerEx(this)
                endif
            endif
            
            set radius  = range
            set random  = 0.
            set centerX = GetUnitX(whichUnit)
            set centerY = GetUnitY(whichUnit)
            set timeout = RMaxBJ(0., cooldown)
            
            call TimerStart(tmr, timeout, true, function thistype.onPeriodic)
            
            return this
        endmethod

        private static method init takes nothing returns nothing
            set table = Table.create()
        endmethod
        implement Init
        
    endstruct
endlibrary

Credits:
Bribe for SpellEffectEvent
Vexorian for TimerUtils​

Keywords:
Wander
Contents

Just another Warcraft III map (Map)

Reviews
Wander v1.4.1 | Reviewed by Flux | 3 April 2016 Wander provides an advanced alternative to the default Wander ability allowing to control the timing (with random option), order, search radius and the wandering can be pause/unpause. Units...

Moderator

M

Moderator


Wander v1.4.1 | Reviewed by Flux | 3 April 2016

Concept

Code

Map Demo

Rating


  • Wander provides an advanced alternative to the default Wander
    ability allowing to control the timing (with random option), order,
    search radius and the wandering can be pause/unpause.
  • Units will wander in a random location within a certain radius.
    The randomness is uniform.

  • The code is MUI, leakless and working.
  • Coding is simple and well written. Additionally, it follows the JPAG
    convention and it is very readable.

  • The Map demo demonstrate the system's purpose.
CONCEPTCODEMAP DEMORATINGSTATUS
3/53.5/53/53.17/5APPROVED

 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I was inspired by Chaosy's Wanderer script, which was submitted a few weeks before.

Changelog
Version 1.1
  • Changed timers from one-shot to periodically running.
  • Fixed a mistake with the random distribution. ( [self=hiveworkshop.com/forums/spells-569/wander-277348/#post2805303]See more[/self] )
  • Struct member real random is now reset properly.
Version 1.2
  • TimerUtils is no longer an obligantory requirement.
Version 1.3
  • Changed timer clock from beeing private to readonly.
  • Added method operator exists takes nothing returns boolean
Version 1.3.1
  • Traded speed for safety: method destroy will cancel in good time if the instance is invalid.
Version 1.4 and 1.4.1
  • Recapitulated some math which the old greeks already knew.
  • Added resume() and pause().
  • Dead units are automatically removed from the system.
Version 1.4.2
  • Fixed a mistake concerning static if usage.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Some suggestions:
-
JASS:
local real x = centerX + GetRandomReal(0., radius)*Cos(GetRandomReal(-bj_PI, bj_PI)) 
local real y = centerY + GetRandomReal(0., radius)*Sin(GetRandomReal(-bj_PI, bj_PI))
I think it's enough to randomize the radius and the angle just once.
JASS:
local real d = GetRandomReal(0., radius)
local real a = GetRandomReal(-bj_PI, bj_PI)
local real x = centerX + d*Cos(a) 
local real y = centerY + d*Sin(a)
It's random enough.

- Add constructor/adder method which is able to add every units with certain ability (or certain unit type) to the system automatically. Something like static method createEx takes integer spellid, real range, real cooldown returns nothing. So it will add all units with that ability to the system, with the specified range and cooldown.
I don't like "create" tho, I prefer "start" or "startWander".

- Add static destructor like static method stop takes unit whichUnit returns nothing. So we don't have to keep track of every units added to the system in order to stop the wandering, when needed, just in case. Other static methods like "setRange", "setCooldown", "setCenterLoc" will be very helpful as well I suppose.

- Personally, for such a simple task, I would try to make the system as compact as possible. By not using other libraries.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Some feedback:
  • Your Random Distribution isn't circular nor uniform. I tested it in MATLAB and the result is:

    Code:
    numPts = 3000;
    radius = 10;
    
    %Non-uniform non circular distribution
    for i=1:numPts
        %rand() returns random real from 0 to 1
        x(i) = radius*rand()*cos(2*pi*rand());
        y(i) = radius*rand()*sin(2*pi*rand());
    end
    plot(x, y, '*');


    attachment.php




    I also provided here different kinds of distribution for you to select:

    Code:
    numPts = 3000;
    radius = 10;
    
    % %Non-uniform circular distribution
    for i=1:numPts
        %rand() returns random real from 0 to 1
        r = radius*rand();
        a = 2*pi*rand();
        x(i) = r*cos(a);
        y(i) = r*sin(a);
    end
    plot(x, y, '*');


    attachment.php





    Code:
    numPts = 3000;
    radius = 10;
    
    % Uniform circular distribution
    for i=1:numPts
        %rand() returns random real from 0 to 1
        u = radius*rand() + radius*rand();
        if u > radius
            r = 2*radius - u;
        else
            r = u;
        end
        a = -pi + 2*pi*rand();
        x(i) = r*cos(a);
        y(i) = r*sin(a);
    end
    plot(x, y, '*');


    attachment.php



  • You doesn't reset real random so new instances may inherit real random from old instances.
 

Attachments

  • 1.jpg
    1.jpg
    181.6 KB · Views: 956
  • 2.png
    2.png
    41 KB · Views: 953
  • 3.png
    3.png
    43.2 KB · Views: 978

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
It's random enough.

Not only that, but, as Flux pointed out, the output are is a square instead of a circle. You have to store the angle, first, so that the Cos and Sin are restricted. I don't think the distance matters, but my brain isn't working right at the moment anyway. It should be cached too, like you said.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Thank you Flux for pointing that out and also for the graphics.

You doesn't reset real random so new instances may inherit real random from old instances.
:)

Add constructor/adder method which is able to add every units with certain ability (or certain unit type) to the system automatically. Something like static method createEx takes integer spellid, real range, real cooldown returns nothing . So it will add all units with that ability to the system, with the specified range and cooldown.
You mean when a unit enters the map or on index event?
I don't fully understand what you mean with
static method createEx takes integer spellid, real range, real cooldown returns thistype.

I don't like "create" tho, I prefer "start" or "startWander".
Wander.start instead of Wander.create seems to be a personal preference, however in
the latest JPAG edit we encourage users to use create / destroy. See next quote
Creation and destruction of artificial types should match the naming conventions set by Blizzard and Vexorian. Struct methods should be named "create" or "destroy". Other functions which try to serve the same purpose should begin with Create or Destroy.

----


Add static destructor like static method stop takes unit whichUnit returns nothing . So we don't have to keep track of every units added to the system in order to stop the wandering, when needed, just in case. Other static methods like "setRange", "setCooldown", "setCenterLoc" will be very helpful as well I suppose.
Until now there is no .stop(), but only Wander[whichUnit].destroy(). I will think about it.
range, cooldown, centerX/Y are public variables. You can change them at any time.
For example: Wander[whichUnit].centerX = 1500.

Personally, for such a simple task, I would try to make the system as compact as possible. By not using other libraries.
Table is omnipresent. TimerUtils could be optional though.
Edit: TimerUtils is now optional. Just for you ;)

Edit2: The timer is now readonly. You may stop and restart a unit from wandering via PauseTimer / ResumeTimer.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Not only that, but, as Flux pointed out, the output are is a square instead of a circle. You have to store the angle, first, so that the Cos and Sin are restricted. I don't think the distance matters, but my brain isn't working right at the moment anyway. It should be cached too, like you said.

Hmm.. Never thought of the result..

You mean when a unit enters the map or on index event?
I don't fully understand what you mean with
static method createEx takes integer spellid, real range, real cooldown returns thistype.
I mean when the user calls Wander.startEx('A000', 1, 1) all units in the map that have the ability will be added to the system automatically... But then I think it's completely optional, could be just a feature bloat. Well, you know better..

JASS:
Wander[whichUnit].centerX = 1500.
Ou, that's neat. I didn't know that trick..
 
The struct init can be removed, and the table can be directly initialisized in the module onInit.

onPeriodic you have set tmr = null as last action, which seems wrong. Method exist would logicaly say that the instance does not exist,
and I think you just meant to null local timer t instead. (have not tested it though)

Maybe the table.has would be more intuitive for an existence check, but that's just thinking.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
The struct init can be removed, and the table can be directly initialisized in the module onInit.
^It's a personal preference. In my own resources I have a public module Init,
which I impement into all my libraries when I need a module initializer.
JASS:
library Init
    module Init 
        private static method onInit takes nothing returns nothing
            static if thistype.init.exists then
                call thistype.init()
            endif
        endmethod
    endmodule
endlibrary

onPeriodic you have set tmr = null as last action, which seems wrong. Method exist would logicaly say that the instance does not exist,
and I think you just meant to null local timer t instead. (have not tested it though)
Thanks. That's correct. In the previous version the array timer was named "clock" and the local timers name was "tmr".
I forgot to change it to "t".

Maybe the table.has would be more intuitive for an existence check, but that's just thinking.
The current syntax is Wander[unit].exists, which is Wander(table[GetHandleId(unit)]).clock != null.
What you suggest is a Wander.has(unit), which is a wrapper to table.has(GetHandleId(unit)).
I think both is good. Maybe your idea is a bit more generic.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
These lines are the cause of your problem:

JASS:
static if not LIBRARY_TimerUtils and not LIBRARY_TimerUtilsEx then
    private function GetTimerData takes timer t returns integer
        return table[GetHandleId(t)]
    endfunction
    private function ReleaseTimer takes timer t returns nothing
        call table.remove(GetHandleId(t))
        call DestroyTimer(t)
    endfunction
endif

Delete them.

@BPower: functions can't reside in static ifs. Struct methods, however, can.
 
Level 13
Joined
Mar 19, 2010
Messages
870
Hi, can u tell me how can i extract the part of the random point calculation? I need it for another spell. I just need a function to get a Random Location in a circle...

My approach is like that, but im not sure if its good ...

JASS:
function RandomPointCircle takes real x, real y, real d returns location
		local real cx = GetRandomReal(-d, d)
		local real ty = SquareRoot(d * d - cx * cx)
		
		return Location(x + cx, y + GetRandomReal(-ty, ty))
endfunction
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Take a look into the graphics from Flux's post ( link ).

The one I used the the third one, circular uniform.
What Bribe wrote above is the second graphic, cicular non-uniform.

The code is:

JASS:
    private function Random takes nothing returns real
        return GetRandomReal(0., 1.)
    endfunction
     
    private function GetRandomRange takes real radius returns real
        local real r = Random() + Random()
        if r > 1. then 
            return (2 - r)*radius
        endif
        return r*radius
    endfunction
    
    function RandomPointCircle takes real x, real y, real d return location
        local real theta = Random()*2*bj_PI
        local real range = GetRandomRange(d) 
        return Location(x + range*Cos(theta), y + range*Sin(theta))
    endfunction

Squeezed into one function
JASS:
    function RandomPointCircle takes real x, real y, real d return location
        local real theta  = GetRandomReal(0., 1.)*2*bj_PI
        local real random = GetRandomReal(0., 1.) + GetRandomReal(0., 1.)
        if random > 1. then
            set random = (2 - random)*d
        else
            set random = random*d
        endif
        return Location(x + random*Cos(theta), y + random*Sin(theta))
    endfunction
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
ok, so i'll change it to your squeezed version, thank you. Last question.... is the retrun value a leak problem?
Eventually, but only if you forget to remove the location later.

Btw this one should also work properly ( Disk Point Picking )
JASS:
    function RandomPointCircle takes real x, real y, real d return location
        local real theta = GetRandomReal(0., 1.)*2*bj_PI
        local real range = SquareRoot(GetRandomReal(0., 1.)*d*d)
        return Location(x + range*Cos(theta), y + range*Sin(theta))
    endfunction
 
Top