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

Patrol System v1.6

Patrol allows units to walk certain patrol paths with certain behaviour. PatrolPaths can be simple routes, but a unit can also run complex routes.
With "complex" patrols is meant that you can combine multiple patrol paths into one bigger, complex patrol path. See demo example.


patrol-gif.261973

Footman - Normal Patrol , and in reverse form

Gryphon
- Backward Patrol, and in reverse form

Knight
- Loop Patrol, and in reverse form

Spellbreaker
- Complex Patrol (2 paths combined)

Whisp
- Normal Patrol

There is a normal, vJass version, and an addon which is made for GUI usage.
The for GUI made library internaly works with the normal vJass version, so you still might need required libraries.
For reading GUI file example you need to open the demo map.

JASS:
native UnitAlive takes unit u returns boolean
library PatrolPath
    //! runtextmacro DEFINE_STRUCT_VECTOR("", "PatrolPath", "PatrolPoint")
endlibrary 
//! zinc
library Patrol  /* v1.5a --hiveworkshop.com/threads/patrol-waypoint-system.276391/

    */ requires VectorT,       /* hiveworkshop.com/threads/containers-vector-t.248942/
    */          UnitDex,       /* wc3c.net/showthread.php?t=101322
    */          TimerUtils,    /* hiveworkshop.com/threads/system-unitdex-unit-indexer.248209
    */          PatrolPath     /*
       
       
    Information
    ¯¯¯¯¯¯¯¯¯¯¯
       
            Patrol allows units to walk certain patrol paths with certain behaviour.
            PatrolPaths can be simple routes, but a unit can also run complex routes.
           
            With "complex" patrols is meant that you can combine multiple patrol
            paths into one bigger, complex patrol path. See demo example.
           
           
    Applied method
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯
           
            1. We create a PatrolPath which is a new vector type from VectorT.
           
            2. We create as many PatrolPoint(s) as we need and add them to a PatrolPath.
                (for adding PatrolPoints to a PatrolPath we use the common vector API, see VectorT)
               
            3. We can apply a unit anytime to a certain Path with using our PatrolUnit API.
           
            You are allowed to define a special PatrolType (defines the behaviour) for each patroling unit.
            You can also register your code which runs when ever a PatrolPoint is reached.
           

*/
//! novjass  
 //=================== --------- API --------- ====================

    struct PatrolPoint
           
        real x
        real y
       
        static method create(real x, real y)
       
        method destroy()
       
//  With this you can create and destroy PatrolPoints.
//  You always can change x/y coordinate of a PatrolPoint at any time.

// PatrolPoint is the type that you need to add to a PatrolPath, example:

        local PatrolPath path = PatrolPath.create()
        call path.push(PatrolPoint(0,0))
   
// In the example we create a PatrolPoint(0/0) and add it to our PatrolPath.
// If you wonder where I took the "push" method from, PatrolPath is actually nothing else
// than a vector type from VectorT library. So you can use all API powers from the
// vector library to manage the PatrolPath.
       
===

    struct PatrolType
   
        static constant integer NORMAL     // After patrol finished, the unit walks back to start Point.
        static constant integer BACKWARD   // After patrol finished, the unit walks all the patrol path backwards.
        static constant integer LOOP       // After patrol finished, the unit will instantly be moved to start.

===

    struct PatrolOrder
   
        static constant integer MOVE     // The unit will "move" to points.
        static constant integer ATTACK   // The unit will "attack" to points.

===

    struct PatrolUnit
   
   
        static method create(unit u, PatrolPath pp, PatrolType pt, PatrolOrder po, real guardDistance, boolean reverse) returns thistype
            // Applies a patrolpath with a patroltype to a unit
            // "reverse" means it would go the PatrolPath in reverse, so start at last position (when set to "true")
           
            // guardDistance is only important if the PatrolOrder is ATTACK.
            // it defines the maximum distance before the unit is forced to return patroling.
           
        method destroy()
            // Make the unit no PatrolUnit anymore
           
        method operator unit returns unit
            // the unit which patrols
           
        method operator pathPosition returns integer
        method operator pathPosition= (integer newPos)
            // This is the current position of the unit's PatrolPath.
            // You ONLY need this actually when you register a code (see later).
            // It's advised only to "read" it (see demo), change it only if you know what you're doing.
            // minimum index (start) = 0
            // maximum index (back)  = PatrolPath.size()-1
           
        method operator order returns PatrolOrder
        method operator order= (PatrolOrder p)
       
        method operator guardDistance returns real
        method operator guardDistance= (real r)
   
        method operator enabled  returns boolean
        method operator enabled= (boolean flag)
            // enables/disable an instance
           
        method operator reverse  returns boolean
        method operator reverse= (boolean flag)
            // "true" means the unit goes the PatrolPath in reverse way.
       
        method operator path  returns PatrolPath
        method operator path= (PatrolPath p)
            // you can apply a new path at any time
            // when newly applied, it will start at StartPosition,
            // or at respectivly at LastPosition, if "reverse" is "true".
       
        method operator patrolType  returns PatrolType
        method operator patrolType= (PatrolType p)
            // you can change patrol type at any time
           
        static method operator enabledAll= (boolean flag)
            // enable or disable all instances
           
        static method operator[] (unit u) returns thistype
            // for example:
                set PatrolUnit[unit].reverse = true
               
        static method register(boolexpr be) returns triggercondition
        static method unregister(triggercondition tc)
            // You can register and unregister boolexrepssions with these functions.
            // Registered code will run when ever a PatrolUnit reaches a PatrolPoint.
           
            // When the registered code returns "true", the system will normaly
            //     contitunue the unit's Patrol behaviour.
            // When the registered code returns "false", the system thinks that
            //     you made custom changes and will skip the current PatrolPoint.
           
        method registerPatrolUnit(boolexpr be) returns triggercondition
        method unregisterPatrolUnit(triggercondition tc)
            // same as above, but specified to your instance
           
// !! Just as info, first the globaly registered code runs, and then the instance-specified one.
           
        static method operator currentPatrolUnit returns thistype
            // use this operator to access the current instance inside registered code.
           
            // e.g: PatrolUnit currentInstance = PatrolUnit.currentPatrolUnit;
       
           
===

    function PatrolPathHardDestroy(PatrolPath p)
        // Of course you always can use the standard destructor .destroy()
        // The major difference to this, is that this "Hard Destroy" will also destroy
        // all to the PatrolPath binded PatrolPoints. With standard .destroy() you solely  
        // destroy the PatrolPath itself, but PatrolPoints would still exist.  
        // The reason for that is, because it is technicaly allowed to use one PatrolPoint
        // for multiple PatrolPaths.
        // You only should use this function if you truely don't need the binded points anymore.
       
       
// Look at demo for examples.
       
//! endnovjass    
{
//=================== --------- Config --------- ====================  
 
    private constant boolean DISABLE_ON_DEATH    = true;    // When a PatrolUnit dies, the instance will be automaticaly disabled
                                                            // You will need to enable it by your own again.
    private constant boolean DESTROY_ON_DEATH    = false;   // When a PatrolUnit dies, the instance will be automaticaly destroyed.
   
    private constant boolean FORMATION_ENABLED   = false;   // Set this to true in case players who control patrol units (can) have the
                                                            // formation enabled in their gameplay options.
                                                            // If you have formation enabled, you can bug patrol units with it,
                                                            // and they would start going crazy when you try to keep some units in format
                                                            // that try to reach the same patrol point. So when you set this to "true", the system
                                                            // will try to fix this with giving patrol orders to a point with random/minimal offset of 
                                                            // the wanted x/ym so 2 units will likely never have the problem in being in formated movement 
                                                            // with also trying to patrol to the very same x/y at the same time.
                                                           
                                                            // but it is just recommended that you leave the formation disabled and set it here to "false".
   
    private constant real    MAX_RANGE           = 20;      // The tolerance distance a unit may have to a patrol point.
   
                                           
//=================== -------------------------- ====================
   
    public struct PatrolPoint{
        real x;
        real y;
        private boolean p_exists;   // private_exists
        static method create(real x, real y) -> thistype{
            thistype this = allocate();
            this.x        = x;
            this.y        = y;
            this.p_exists = true;
            return this;
        }
        method destroy(){
            if (p_exists){
                deallocate();
                p_exists = false;
            }
        }
       
        // user doesn't need to know this exists,
        // but it makes no harm
        method operator exists() -> boolean{
            return p_exists;
        }
    }
   
    public struct PatrolType extends array{
        static constant integer NORMAL   = 1;   // After finished, the unit walks back to start Point.
        static constant integer BACKWARD = 2;   // After finished, the unit walks all the patrol path backwards.
        static constant integer LOOP     = 3;   // After finished, the unit will instantly be moved to start.
       
        static constant integer MAXIMUM_AMOUNT = 3;
    }
   
    public struct PatrolOrder extends array{
        static constant integer MOVE   = 851986;  
        static constant integer ATTACK = 851983;  
    }
   
    private constant real INTERVAL  = 0.031250000;
   
    public struct PatrolUnit extends array{
       
        private static group Group  = CreateGroup();
        private static group Backup = CreateGroup();
        private static group Temp;
       
        private static trigger  handler = CreateTrigger();  // global handler
        private static thistype p_patrolUnit;               // access in registered code
       
//      members
       
        private unit    p_u;               // private_unit
        private timer   clock;
        private integer incrementer;       // Defines if current position is going forward or backward in PatrolPath
        private integer p_pathPosition;    // Current position in PatrolPath vector
        private trigger p_handler;         // instance specific handler
        private boolean p_reverse;         // private_reverse
        private boolean p_enabled;         // private_enabled
       
        private real p_guardDistance;
        private trigger guardHandler;
       
        private PatrolType p_patrolType;
        private PatrolOrder p_order;
        private PatrolPath p_path;
       
        // x/y of the point the unit needs to return, in case it is chasing/fighting some unit
        private real xReturn;
        private real yReturn;
        private boolean isChasing;
       
        method destroy(){
            ReleaseTimer(clock);
           
            if(p_handler != null){
                DestroyTrigger(p_handler);
                p_handler = null;
            }
           
            if(guardHandler != null){
                DestroyTrigger(guardHandler);
                guardHandler = null;
            }
           
            p_enabled = false;
            GroupRemoveUnit(Group, p_u);
            p_u = null;
            clock = null;
        }
       
        private static method callback(){
            thistype this = GetTimerData(GetExpiredTimer());
            real distance, x, y;
           
            static if(DESTROY_ON_DEATH){
                if (!UnitAlive(p_u)){
                    destroy();
                    return;
                }
            }
            static if(DISABLE_ON_DEATH){
                if (!UnitAlive(p_u)){
                    PauseTimer(clock);
                    p_enabled = false;
                    return;
                }
            }
           
            if(p_path.empty()){
                debug BJDebugMsg(thistype.callback.name + ": instance " + I2S(this) + " has an invalid or empty PatrolPath.");
                return;
            }
            else if(!p_path[p_pathPosition].exists){
                debug BJDebugMsg(thistype.callback.name + ": instance " + I2S(this) + " tries to acces a removed/invalid PatrolPoint.");
                return;
            }
           
            x = GetUnitX(p_u); y = GetUnitY(p_u);
            distance = SquareRoot((p_path[p_pathPosition].x-x)*(p_path[p_pathPosition].x-x) + (p_path[p_pathPosition].y-y)*(p_path[p_pathPosition].y-y));
           
            if (distance <= MAX_RANGE) {
               
                p_patrolUnit = this;
                TriggerEvaluate(handler);
                if(p_handler != null)
                    TriggerEvaluate(p_handler);

                if (p_patrolType == PatrolType.NORMAL){
                    if(p_path[p_pathPosition] == p_path.back() && !reverse){
                        p_pathPosition = 0;
                    }
                    else if(reverse && p_path[p_pathPosition] == p_path.front()){
                        p_pathPosition = p_path.size()-1;
                    }
                    else {
                        p_pathPosition = p_pathPosition + incrementer;
                    }
                }
                else if(p_patrolType == PatrolType.BACKWARD){
                    if(p_path[p_pathPosition] == p_path.back()){
                        incrementer = -1;
                    }
                    else if (p_path[p_pathPosition] == p_path.front()){
                        incrementer = 1;
                    }
                    p_pathPosition = p_pathPosition + incrementer;
                }
                else if(p_patrolType == PatrolType.LOOP){
                    if(p_path[p_pathPosition] == p_path.back() && !reverse){
                        SetUnitX(p_u, p_path.front().x);
                        SetUnitY(p_u, p_path.front().y);
                        p_pathPosition = 1;
                       
                    }
                    else if(reverse && p_path[p_pathPosition] == p_path.front()){
                        SetUnitX(p_u, p_path.back().x);
                        SetUnitY(p_u, p_path.back().y);
                        p_pathPosition = p_path.size()-2;
                    }
                    else {
                        p_pathPosition = p_pathPosition + incrementer;
                    }
                }
               
                if (FORMATION_ENABLED) {
                    IssuePointOrderById(p_u, p_order, p_path[p_pathPosition].x + GetRandomReal(0, 0.1), p_path[p_pathPosition].y + GetRandomReal(0, 0.1));
                }
                else{
                    IssuePointOrderById(p_u, p_order, p_path[p_pathPosition].x , p_path[p_pathPosition].y);
                }
            }
           
            // when the unit got off the path for some reason (maybe after a fight with enemies),
            // and is being detected without a current order, we re-order it to continue patroling.
            if (GetUnitCurrentOrder(p_u) == null){
           
               if (FORMATION_ENABLED) {
                    IssuePointOrderById(p_u, p_order, p_path[p_pathPosition].x + GetRandomReal(0, 0.1), p_path[p_pathPosition].y + GetRandomReal(0, 0.1));
                }
                else{
                    IssuePointOrderById(p_u, p_order, p_path[p_pathPosition].x , p_path[p_pathPosition].y);
                }
               
            }
        }
       
       
        // callback for when a unit is chasing an other unit
        private static method callback_2(){
            thistype this = GetTimerData(GetExpiredTimer());
            real distance, x, y;
            static if(DESTROY_ON_DEATH){
                if (!UnitAlive(p_u)){
                    destroy();
                    return;
                }
            }
            static if(DISABLE_ON_DEATH){
                if (!UnitAlive(p_u)){
                    PauseTimer(clock);
                    p_enabled = false;
                    return;
                }
            }
           
            x = GetUnitX(p_u); y = GetUnitY(p_u);
            distance = SquareRoot((xReturn-x)*(xReturn-x) + (yReturn-y)*(yReturn-y));
           
            // unit returned to path, it can continue patroling
            if(!isChasing && distance <= MAX_RANGE){
                TimerStart(clock, INTERVAL, true, function thistype.callback);
            }
           
            // unit is out of max range, it gets ordered moving back
            if(distance > p_guardDistance){
                isChasing = false;
                IssuePointOrderById(p_u, PatrolOrder.MOVE, xReturn, yReturn);
            }
           
            // unit is not chasing anymore, so return
            if (GetUnitCurrentOrder(p_u) == null){
                isChasing = false;
                IssuePointOrderById(p_u, PatrolOrder.MOVE, xReturn, yReturn);
            }
        }
       
        private static method onTargetAquired() -> boolean{
            thistype this = GetUnitId(GetTriggerUnit());
            if (!isChasing){
                xReturn = GetUnitX(p_u);
                yReturn = GetUnitY(p_u);
            }
            isChasing = true;
            TimerStart(clock, INTERVAL, true, function thistype.callback_2);
            return false;
        }
       
        static method create(unit u, PatrolPath pp, PatrolType pt, PatrolOrder po, real gd, boolean r) -> thistype{
            thistype this;
           
            if(integer(pt) < 1 || integer(pt) > 3){
                debug BJDebugMsg(thistype.create.name + ": unit with id " + I2S(GetHandleId(u)) + " tries to apply an invalid PatrolType.");
                return 0;
            }
            if(!(po == PatrolOrder.MOVE || po == PatrolOrder.ATTACK)){
                debug BJDebugMsg(thistype.create.name + ": unit with id" + I2S(GetHandleId(u)) + " tries to apply an invalid PatrolOrder.");
                return 0;
            }
            if(IsUnitInGroup(u, Group)){
                debug BJDebugMsg(thistype.create.name + ": unit with id" + I2S(GetHandleId(u)) + " already is a PatrolUnit.");
                return 0;
            }
           
            this            = GetUnitId(u);
            clock           = NewTimerEx(this);
            p_u             = u;
            p_order         = po;
            p_path          = pp;
            p_patrolType    = pt;
            p_reverse       = r;
            p_enabled       = true;
            p_handler       = null;
            isChasing       = false;
            p_guardDistance = gd;
            GroupAddUnit(Group, p_u);
           
            if(po == PatrolOrder.ATTACK){
                guardHandler = CreateTrigger();
                TriggerAddCondition(guardHandler, Condition(function thistype.onTargetAquired));
                TriggerRegisterUnitEvent(guardHandler, u, EVENT_UNIT_ACQUIRED_TARGET);
            }
           
            if(r){
                p_pathPosition = p_path.size()-1;
                incrementer    = -1;
            }
            else{
                p_pathPosition = 0;
                incrementer    = 1;
            }
            TimerStart(clock, INTERVAL, true, function thistype.callback);
            return this;
        }
       
       
        method operator patrolType() -> PatrolType{
            return p_patrolType;
        }
        method operator patrolType=(PatrolType p){
            if(integer(p) > 0 && integer(p) <= PatrolType.MAXIMUM_AMOUNT){
                p_patrolType = p;
            }
            else{
                debug BJDebugMsg("Patrol: operator patrolType=: " + ": instance " + I2S(this) + " tries to apply an invalid PatrolType.");
            }
        }
       
        method operator reverse() -> boolean{
            return p_reverse;
        }
        method operator reverse=(boolean flag){
            if(flag != p_reverse){
                incrementer   = -incrementer;
                p_reverse = flag;
            }
        }
       
        method operator path() -> PatrolPath{
            return p_path;
        }
        method operator path=(PatrolPath p){
            p_path = p;
            if(reverse){
                p_pathPosition = p_path.size()-1;
                incrementer    = -1;
            }
            else{
                p_pathPosition = 0;
                incrementer    = 1;
            }
        }
       
        // this exists so the user has no chance to "null" the
        // unit variable in public access.
        method operator unit()-> unit{
            return p_u;
        }
       
        method operator pathPosition()-> integer{
            return p_pathPosition;
        }
        method operator pathPosition=(integer i){
            if (0 <= i && i <= (p_path.size()-1))
                p_pathPosition = i;
            else
                debug BJDebugMsg("Patrol: operator pathPosition=: " + ": instance " + I2S(this) + " tries to apply an invalid pathPosition.");
        }
       
        method operator guardDistance() -> real{
            return p_guardDistance;
        }
        method operator guardDistance=(real r){
            p_guardDistance = r;
        }
       
        method operator order()-> PatrolOrder{
            return p_order;
        }
        method operator order=(PatrolOrder p){
            if (p == PatrolOrder.MOVE || p == PatrolOrder.ATTACK){
                if(p == PatrolOrder.ATTACK && p_order != p){
                    guardHandler = CreateTrigger();
                    TriggerAddCondition(guardHandler, Condition(function thistype.onTargetAquired));
                    TriggerRegisterUnitEvent(guardHandler, p_u, EVENT_UNIT_ACQUIRED_TARGET);
                }
                else if(p_order == PatrolOrder.ATTACK && p == PatrolOrder.MOVE){
                    DestroyTrigger(p_handler);
                    p_handler = null;
                }
                p_order = p;
            }
            else{
                debug BJDebugMsg("Patrol: operator order=: " + ": instance " + I2S(this) + " tries to apply an invalid PatrolOrder.");
            }
        }
       
        method registerPatrolUnit(boolexpr be) -> triggercondition{
            if(p_handler == null){
                p_handler = CreateTrigger();
            }
            return TriggerAddCondition(p_handler, be);
        }
        method unregisterPatrolUnit(triggercondition tc){
            TriggerRemoveCondition(p_handler, tc);
        }
       
        method operator enabled() -> boolean{
            return p_enabled;
        }
        method operator enabled=(boolean flag){
            if(flag && !(p_enabled)){
                TimerStart(clock, INTERVAL, true, function thistype.callback);
                if(p_order == PatrolOrder.ATTACK){
                    EnableTrigger(guardHandler);
                }
            }
            else if(!(flag) && p_enabled){
                PauseTimer(clock);
                if(p_order == PatrolOrder.ATTACK){
                    DisableTrigger(guardHandler);
                }
            }
            p_enabled = flag;
        }
       
        static method operator enabledAll=(boolean flag){
            unit fog;
            fog = FirstOfGroup(Group);
            while(fog != null){
                thistype(GetUnitId(fog)).enabled = flag;
                GroupAddUnit(Backup,fog);
                GroupRemoveUnit(Group,fog);
                fog = FirstOfGroup(Group);
            }
            Temp = Group;
            Group = Backup;
            Backup = Temp;
        }
       
        static method operator[](unit u)-> thistype{
            if(!IsUnitInGroup(u, Group)){
                debug BJDebugMsg("Patrol: operator []: " + ": unit with id " + I2S(GetHandleId(u)) + " is not a PatrolUnit yet.");
                return 0;
            }
            return GetUnitId(u);
        }
       
        static method register(boolexpr be) -> triggercondition{
            return TriggerAddCondition(thistype.handler, be);
        }
        static method unregister(triggercondition tc){
            TriggerRemoveCondition(thistype.handler, tc);
        }
       
        static method operator currentPatrolUnit()->thistype{
            return p_patrolUnit;
        }
       
        // wow, static if's aren't allowed globaly in Zinc :(
        private static method onDeindex()->boolean{
            if (IsUnitInGroup(GetIndexedUnit(), Group)) {
                thistype(GetIndexedUnitId()).destroy();
            }
            return false;
        }
       
        // wow, static if's still aren't allowed globaly in Zinc :(
        private static method onInit(){
            OnUnitDeindex(function thistype.onDeindex);
        }
   
    } // end of struct
   
    public function PatrolPathHardDestroy (PatrolPath p){
        integer i = p.size() - 1;
        while(i >= 0){
            p[i].destroy();
            i = i - 1;
        }
        p.destroy();
    }
}
//! endzinc

JASS:
//! zinc
library PatrolGUI requires Patrol /* v1.1 -- hiveworkshop.com/threads/patrol-waypoint-system.276391/ */{

    PatrolPath tempPath = 0;
    Table table;
 
    function onRegister(){
        if(GetHandleId(udg_PATROL_Point) == 0){
            debug BJDebugMsg("ERROR: PatrolGUI - Register - Point is invalid or does not exist.");
            return;
        }
        if (tempPath == 0)
            tempPath = PatrolPath.create();
        tempPath.push(PatrolPoint.create(GetLocationX(udg_PATROL_Point), GetLocationY(udg_PATROL_Point)));
    }

    function onCreate(){
        if(udg_PATROL_Order != udg_PATROL_ORDER_MOVE && udg_PATROL_Order != udg_PATROL_ORDER_ATTACK){
            debug BJDebugMsg("ERROR: PatrolGUI - Create - Invalid PatrolOrder");
            return;
        }
        if(udg_PATROL_PatrolType < 1 || udg_PATROL_PatrolType > 3){
            debug BJDebugMsg("ERROR: PatrolGUI - Create - Invalid PatrolType.");
            return;
        }
        if(tempPath == 0){
            debug BJDebugMsg("ERROR: PatrolGUI - Create - No Points are registered.");
            return;
        }
 
        PatrolUnit.create(udg_PATROL_Unit, tempPath, udg_PATROL_PatrolType, OrderId(udg_PATROL_Order), udg_PATROL_GuardDistance, false).enabled = udg_PATROL_Enabled;
        table.integer[GetHandleId(udg_PATROL_Unit)] = tempPath;
        tempPath = 0;
    }
 
    function onDestroy(){
        integer id = GetHandleId(udg_PATROL_Unit);
        if(table.integer.has(id)){
            PatrolPathHardDestroy(PatrolPath(table.integer[id]));
            table.integer.remove(id);
        }
        else
            debug BJDebugMsg("ERROR: PatrolGUI - Destroy - Unit is no PatrolUnit.");
    }
 
    function onGet(){
        PatrolUnit this = PatrolUnit[udg_PATROL_Unit];
        udg_PATROL_Enabled       = this.enabled;
        udg_PATROL_PatrolType    = this.patrolType;
        udg_PATROL_Order         = OrderId2String(this.order);
        udg_PATROL_GuardDistance = this.guardDistance;
    }
 
    function onSet(){
        PatrolUnit this;
 
        if(udg_PATROL_Order != udg_PATROL_ORDER_MOVE && udg_PATROL_Order != udg_PATROL_ORDER_ATTACK){
            debug BJDebugMsg("ERROR: PatrolGUI - SetData - Invalid PatrolOrder");
            return;
        }
        if(udg_PATROL_PatrolType < 1 || udg_PATROL_PatrolType > 3){
            debug BJDebugMsg("ERROR: PatrolGUI - SetData - Invalid PatrolType.");
            return;
        }
 
        this = PatrolUnit[udg_PATROL_Unit];
        this.enabled       = udg_PATROL_Enabled;
        this.patrolType    = udg_PATROL_PatrolType;
        this.order         = OrderId(udg_PATROL_Order);
        this.guardDistance = udg_PATROL_GuardDistance;
    }
 
    function init(){
        udg_PATROL_TYPE_NORMAL   = PatrolType.NORMAL;
        udg_PATROL_TYPE_BACKWARD = PatrolType.BACKWARD;
        udg_PATROL_TYPE_LOOP     = PatrolType.LOOP;
 
        udg_PATROL_ORDER_MOVE   = "move";
        udg_PATROL_ORDER_ATTACK = "attack";
 
        DestroyTimer(GetExpiredTimer());
    }
 
    function onInit(){
        table = Table.create();
        TimerStart(CreateTimer(), 0, false, function init);
        udg_PATROL_Create        = CreateTrigger();
        udg_PATROL_Destroy       = CreateTrigger();
        udg_PATROL_RegisterPoint = CreateTrigger();
        udg_PATROL_GetData       = CreateTrigger();
        udg_PATROL_SetData       = CreateTrigger();
 
        TriggerAddAction(udg_PATROL_Create, function onCreate);
        TriggerAddAction(udg_PATROL_RegisterPoint, function onRegister);
        TriggerAddAction(udg_PATROL_Destroy, function onDestroy);
        TriggerAddAction(udg_PATROL_GetData, function onGet);
        TriggerAddAction(udg_PATROL_SetData, function onSet);
    }
}
//! endzinc


v1.4 to v1.5:
- unit collision is no longer considered
- formated movement is now allowed


Keywords:
Patrol, Waypoint, MUI, system, jass, vjass, point, walk
Previews
Contents

Patrol System (Map)

Reviews
19:40, 15th Mar 2016 BPower: Almia mentioned a few important things. I'll write a review when soon TM takes place.
Wietlol
API Madness:static method register(boolexpr be) returns triggercondition static method unregister(triggercondition tc) register can take in a "code" variable instead (as long as the functions arent inlined) Optimization:private timer clock; clock...
Wietlol
I suppose we can trust the user to not mess up :D Status: Approved
Not sure I follow, why the next path position and not the current
next PatrolPoint is the point where the unit currently goes/aims for. I mean that.

The system will force the unit to return to its current (or nearest, or the position that it was when it acquired the target) path position only if its too far away and is chasing an enemy unit.
The problem is to distinguish if it is currently chasing a unit or not.

If the order is attack and not move the unit could reacquire its target and continue chasing?
In my example there no enemy in range at all. So there is nothing to chase. But the user wants having "attack" order for the case.
But the system would think unit is out of range -> force "move" back to route and not "attack".

Like I said, if you don't want to bother, you can piggyback on Blizzard's PLAYER_NEUTRAL_AGGRESSIVE + guard return distance behaviour
But I honestly don't very understand how it should help the unit to stay on a patrolPath/certain line. The return behaviour for aggresive passives relates to one certain point, only, or?

Please let's continue discussing, if I get it wrong, I also see a return-feature can be useful, but I really don't understand atm how to optimaly implement good.
For now I added the possiblity to define orderId per instance. attack/move. And I wrote an addon library for GUI usage.

Updated.
 
Level 13
Joined
Nov 7, 2014
Messages
571
The problem is to distinguish if it is currently chasing a unit or not.
A patrolling unit goes into a chasing state when it EVENT_UNIT_ACQUIRED_TARGET, if it goes too far away from "its current path point (or nearest path point, or the position that it was when it acquired the target)" it goes into a "returning back state", after it reaches the point from which the chase started, it goes into a patrolling state again, etc.

Please let's continue discussing,
I don't have anything else to add.
 
Level 13
Joined
Nov 7, 2014
Messages
571
what to do if order is "attack", and the distance between two PatrolPoints is by default bigger than "allowed Max_Distance"?

then I wold force the unit to "walk" to next point, even it should use "attack", and is just normaly patroling

Scenario:

Distance between two patrolpoints = 2000
MAX_ALLOWED_DISTANCE = 1000
Order = "attack"

now the system will think the unit is out of range, and so it will force the unit to return ("move") to next PatrolPoint,
even it is normaly walking on the path. Actually the order should be "attack", and not "move". I would need to check for the unit distance to the PatrolLine,

In my example there no enemy in range at all. So there is nothing to chase. But the user wants having "attack" order for the case.
But the system would think unit is out of range -> force "move" back to route and not "attack".

You keep repeating without countering mentioned concerns.
Hm...

A patrolling unit goes from one point of the path to the next via a patrolling order.(IssuePointOrderById(p_u, p_order, p_path[p_pathPosition].x, p_path[p_pathPosition].y);).

The patrolling unit starts its patrolling in a state called, say patrolling-state, if its patrolling order is not move but attack or smart and we have registered for it a EVENT_UNIT_ACQUIRED_TARGET (this event doesn't fire for the move order, how lucky =))
we can detect when it has acquared an enemy target unit and put the patrolling unit into a chasing state.

While being in a chasing state we check if its distance from some point (I guess it has to be either the nearest point of the patrolling path [which would be somewhat expensive to compute] or the point from which the patrolling unit started the chase [when the EVENT_UNIT_ACQUIRED_TARGET happend], now I understand your concern about the point being the current patrolling point, because the patrolling unit could immediately go into a "returning back state")
is greater than our "guard return distance" parameter, and if so, we set the patrolling unit's state to that of "returning back state" and order it to move back with a move order regardless of the patrolling order.

When the patrolling unit gets back to the point where the chasing started it goes again into a patrolling state and we order it via the patrolling order to go to its current patrol path point and continue its patrolling.
 
Tried to implement it as you said, thanks. I was not very sure to which point to return exactly, so I did this:
or the point from which the patrolling unit started the chase

I tried to experiment a bit and changed code here and there (though changed back to normal again), and so it became hard to maintain all changes with all optional libraries.
I made all libraries as "required" then to stop the mess. Sorry, Chaosy.

So updated to what Aniki suggested.
 
Updated.

@CHA_Owner , unit's collision should no longer be a thing, can you try if the issue is gone for you? If there is still a issue you can change the max distance in the config now.

Also, there was a bug that @Tank-Commander found, which could appear with having "formated movement" enabled. The user can also set it in config now, if the units may have formated movement, or not.
 

Attachments

  • Patrol.gif
    Patrol.gif
    8 MB · Views: 2,233
Level 12
Joined
Feb 11, 2008
Messages
809
Okay ill be home by 6 then ill test the new version and let you know if any bugs occur! Nice response time on the update though!

EDIT*

Units are now patrolling correctly without stopping so my issue is completely fixed thanks again for all the help!
 
Last edited:
Level 24
Joined
Aug 1, 2013
Messages
4,657
API Madness:
  • static method register(boolexpr be) returns triggercondition
    static method unregister(triggercondition tc)
    register can take in a "code" variable instead (as long as the functions arent inlined)

Optimization:
  • private timer clock;
    clock could be global with a foreach loop in the callback
    On a side note, the interval could be 0.03 instead of 0.03125000
  • distance = SquareRoot((p_path[p_pathPosition].x-x)*(p_path[p_pathPosition].x-x) + (p_path[p_pathPosition].y-y)*(p_path[p_pathPosition].y-y));
    istance check should be done with squared values and only ever be rooted whenever it requires to be so... for example for visuals or whatever which you dont
  • PatrolPath ìs placed below everything, thus it creates trigger executions for every function it cannot inline... better place it at the top.

Readability:
  • if(integer(p) >= 1 && integer(p) <= 3){
    Typecasts are unnecessary... but I suppose its preference.
    the "3" should be a private constant as "PatrolType.COUNT" (or something)
  • JASS:
    if(flag != p_reverse)
        incrementer   = -incrementer;
    p_reverse = flag;
    ->
    JASS:
    if(flag != p_reverse)
    {
        incrementer   = -incrementer;
        p_reverse = flag;
    }

Bugs: (Required to be fixed)
  • p_pathPosition = p_path.size()-1;
    ??? possible negative p_pathPosition ???
    why would this one use "method operator pathPosition=(integer i)"
    but "p_pathPosition = i;" (inside that operator) doesnt
    either way, it is either allowing a negative p_pathPosition or it is placed in the wrong order thus creating a trigger evaluation to reference the function ahead.

Status: Requires Update

 
clock could be global with a foreach loop in the callback
I don't think this is better. It's also cleaner this way, and I don't need an extra data struture but the timer utils.

On a side note, the interval could be 0.03 instead of 0.03125000
Why?

PatrolPath ìs placed below everything, thus it creates trigger executions for every function it cannot inline... better place it at the top.
Are you sure, have you checked?

Bugs: (Required to be fixed)
  • p_pathPosition = p_path.size()-1;
    ??? possible negative p_pathPosition ???
    why would this one use "method operator pathPosition=(integer i)"
    but "p_pathPosition = i;" (inside that operator) doesnt
    either way, it is either allowing a negative p_pathPosition or it is placed in the wrong order thus creating a trigger evaluation to reference the function ahead.
Where is a bug? It's wrong input somewhere, if it results in a negative value, and will throw a debug message to user that path is invalid.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Using one timer, one callback and no "get id" stuff is a big optimization though.
However, it wont break anything so it is not required to do so... just optimizations.

I had to mention the 0.03s thingy.
Simply because there is no readon (apart from a 1.333 frame optimization) to use 0.03125s.
As a drawback, 0.03125s is slightly less accurate.

//! runtextmacro DEFINE_STRUCT_VECTOR("", "PatrolPath", "PatrolPoint")
Doesnt that create the whole struct at the position of that textmacro?
If so, then it is either placed after this library, and there is no requirement/usage of it defined or the code in the thread isnt the actual code.

What happens when the path given is empty?
 
Using one timer, one callback and no "get id" stuff is a big optimization though.
As said I don't agree, and explained why. For PatrolSystem I don't need an extra data structure, no doubly linked list, or what ever, which would mean more code logics.
Now I simply can use the internaly logics from Timer Utils to store my data, so it's more efficient -- maybe sligthly less performant, in case I use 20+ more instances. Still, less complex, and a win for me.

I had to mention the 0.03s thingy.
Simply because there is no readon (apart from a 1.333 frame optimization) to use 0.03125s.
As a drawback, 0.03125s is slightly less accurate.
That's not true, and by the way, there is a difference between 0.03125, and 0,031250000. Explicitily defining the 4x"0", it was tested to be the most accurate value, more sligthly accurate than 0.03125. (which is not really important anyways)

so, then it is either placed after this library, and there is no requirement/usage of it defined or the code in the thread isnt the actual code.
You stated something, you say. : ) But I think you might be surprised at 1-2 points might be different at the TriggerExecution point.

What happens when the path given is empty?
PatrolPaths, and PatrolUnits are treated seperatly - and what you create an empty patrol path first nothing happens. But in case you try to apply a unit to move to an empty patrol path, and in our example in 'reverse' form, then it will try to reach the "size - 1" point (which is just the first checkpoint), the system will understand that this point doesn't exist yet, and throw error(s).

edit:

Made the few changes, and use a second library, now, to avoid an evaluation.
For size()-1 thing, though, nothing was changed.
 
Last edited:
So, I noticed that if a patrolling unit is obstructed, it will weirdly abort its path and return into a loop till that obstructed point.

In the test map, the teal knight will obstruct the path and watch what happen to the next knights. Is this behavior even intentional?
 

Attachments

  • SpawnPatrolSystem.w3x
    84 KB · Views: 9
Top