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

Moderator

M

Moderator

19:40, 15th Mar 2016
BPower: Almia mentioned a few important things.
I'll write a review when soon TM takes place.
 
Last edited by a moderator:

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
Nope, I haven't.

Still's useful though since you can't make AI patrol since you can't force them to press shift. Can't make player owned units do it through triggers either.
To my knowledge anyway.

  • Game - Force Player 1 (Red) to press the key A
Not sure if shift works here. If it does, I'll delete this.

edit: I tried
  • Melee Initialization
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Game - Force Player 1 (Red) to press the key SHIFT
      • Unit - Order Footman 0000 <gen> to Patrol To (Center of Region 001 <gen>)
      • Unit - Order Footman 0000 <gen> to Patrol To (Center of Region 002 <gen>)
      • Unit - Order Footman 0000 <gen> to Patrol To (Center of Region 000 <gen>)
And it did not work.
 
Level 33
Joined
Apr 24, 2012
Messages
5,113
may i ask? why 0.03 instead of 0.031250000 on loop speed?

also, just don't Sqrt the dist, instead just compare it like dist < range*range

using OrderById is much better than just using strings. (attack = 851983, Order Ids)

I think that using Table instead of pseudo-2d array(because you know, that's an attribute and you use arrays to it) for dx and dy is much better and it also gives a more waypoints.
JASS:
real cx = GetUnitX(u);
real cy = GetUnitY(u);
real tx = dx[current] - cx;
real ty = dy[current] - cy;
->
JASS:
real tx = dx[current] - GetUnitX(u);
real ty = dy[current] -  GetUnitY(u);

and also a suggestion: how about using just 1 timer and then just use a list to iterate all the units. also how about Unit Indexer :)

and for an optional lib : Alloc libs

[edit]
Some parts of the code should have an access modifier to it. e.g. Update should be private.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
JASS:
real dx [99];
real dy [99];
A big no in my opinion. This will greatly limit the number of parallel instances to 82. Also that means each 'Patrol' is limited to 99 waypoints.
Workarounds would be:
- Use a hashtable's parent key as this and child key as count
- Use TableArray (so you can save a hashtable space)
- Use another struct for the waypoints and access them via linked-list.
EDITED PART:
( - Or follow Almia's suggestion using a Table struct member)


I personally don't like the 1 Timer per instance. Why not use a single static timer where each timeout will pick all instance? (Note: You'll have to use a linked-list to pick all instance, pausing will remove it from the list, resuming/starting add it to the list)


Q: Do I need to null variables in the onDestory method?
No because those are global variables (in an array) and according to Memory Leaks, global agent type leak is negligible. Personally, I still null it because reasons.


You forgot to convert to orderid in method start
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
I am aware of the 99 waypoints. But I got no clue why there would be 82 instances at max.

If I use a hashtable I could make the entire spell based on a hashtable.
I suppose it's fine if I use table though since it does not take up a slot.
Still feels wrong to use both hashtables and structs.

And yes I could use a linked list but I don't like those. Then we're getting into what I hate the most. A resource that requires resources which requires more resources.
Vector<T> for example.
 
Last edited:
Level 22
Joined
Feb 6, 2014
Messages
2,466
I am aware of the 99 waypoints. But I got no clue why there would be 82 instances at max.
http://www.wc3c.net/vexorian/jasshelpermanual.html#arrmemb

Still feels wrong to use both hashtables and structs.
No it's not. I do that all the time.

And yes I could use a linked list but I don't like those. Then we're getting into what I hate the most. A recourse that requires resources which requires more resources.
Vector<T> for example.
If you don't want it to require a linked-list resource, just manually implement an internal linked-list yourself, that's what I do. Also, if you do that, you will no longer need TimerUtils.
 
Level 33
Joined
Apr 24, 2012
Messages
5,113
Please provide an API.

also this:
JASS:
private static method deIndex(integer i)
            {
                thistype this = thistype(i);
                instances[i].deallocate();
                instances[i] = instances[max];
                max -= 1;
                static if(LIBRARY_TimerUtils)
                {
                    ReleaseTimer(t);
                }
                else
                {
                    DestroyTimer(t);
                }
                u = null;
            }
I would like to point out some things:
- On what part of the system do you use this?
- Instead of integer i, why not thistype i? or just use thistype this = i? (integer = struct instances)
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Sorry for the super long pending status. The system is pretty simple, but it can maybe find some use for RPG maps or cinematics. Here are just a few things I would like for you to fix:
  • I think the API should be moved to the top of the actual library code. Having it in a seperate trigger is just... more work. I thought lazy people find the easiest way of doing things!
  • A small description of what detectRange might be useful for some users. I did not know what this was until i read the Update method
  • (dist < detectRange * detectRange) I think this should be less than or equal to; not just less than.
  • All though I am aware that the order id 851983 is attack, random numbers look ugly in code. Could you add it as a configurable? An even better solution is to allow users to decide which order a unit will issue on each waypoint
  • I think method Pause and start can be combined if it takes a boolean as a parameter
  • Why in heavens name is there no destroy method?! The instance count is going to just keep climbing up with no recycling what so ever. You should be deallocating instances if a unit no longer has waypoints, dead, or is no longer in the game. All though the Pause method sort of acts as destroy, it seems really useless if a user wanted to remove all waypoints of a unit's instance. The loop will constantly keep iterating over it when it does not have to anymore
Most of what I listed are really suggestions, but I would really like for you to add the destroy method. The Pause method seems like a really cheap way of doing it. Set to Awaiting Update.
 
Last edited:

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,516
It logically makes more sense to just keep it as <=

I agree, and personally I would use <= since there is no performance penalty - but that doesn't change the fact that it doesn't matter.

The chance of the dist being exactly detectRange^2 is extremely small, maybe not even possible.

Surely it's possible, but it's more interesting that it makes no difference for the player. It also pales in comparison to the fact that radii mean different things in different contexts in warcraft 3. For example, a unit with endurance aura, radius 400 will reach different units than a group enumeration with radius 400.

Nonetheless, it was just a suggestion

I speak from experience when I say that it's a bad habit to mix opinions/suggestions with facts and requirements. Believe me when I say that I have to actively think about it to stop from doing it myself.

As a suggestion, perhaps separate your requirements (that lead the resource to be pending) from your suggestions (that are small quips), for example like so:

Issues:

The script has loads of leaks and it set my PC on fire.

Other suggestions:

It might describe developer intent better in the radius code to use <=.
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
Okay..

1. Separating API and code is simply more clean in my opinion.
2. Fixed
3. Fixed
4. It's supposed to mimic the original patrol, thus attack is the correct way of doing things.
5. Can? Absolutely. Should? probably not. I personally like a more 'logical' API where the function name does what the name describes. calling Start(false) just looks weird to me.
6. There used to be one, I think. I removed it for some reason though.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
I speak from experience when I say that it's a bad habit to mix opinions/suggestions with facts and requirements.
>
Most of what I listed are really suggestions, but I would really like for you to add the destroy method.


4. It's supposed to mimic the original patrol, thus attack is the correct way of doing things.
I was just trying to add some more configurability to the system :D


There used to be one, I think. I removed it for some reason though.
If you're going to be allocating unique indexes for the struct, you should be dealloacting them as well. I would also maybe add an extra check when users use RemoveAt() that automatically deallocates the instance when the count reaches 0.
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
Time to get this shit approved.

I added
JASS:
            method destroy()
            {
                this.deallocate();
            }

That's all I need in a destroy function, yeah?

edit: nope, that ain't gonna work because of me using some retarded indexing.
JASS:
            method destroy()
            {
                if(instances[index] != instances[max])
                {
                    instances[index] = instances[max];
                }
                max -= 1;
                this.deallocate();
            }
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
edit: nope, that ain't gonna work because of me using some retarded indexing.

JASS:
            method destroy()
            {
                if(instances[index] != instances[max])
                {
                    instances[index] = instances[max];
                }
                max -= 1;
                this.deallocate();
            }
The above destroy method still won't work, try making 3 patrols:
1 - footman
2 - grunt
3 - ghoul

Now destroy the footman's patrol and the ghoul's patrol, i.e only leaving the grunt's patrol and see what happens. You should see that the grunt has stopped patrolling but the ghoul hasn't, although we did call .destroy on it's patrol.
 
  • Timers should not run for nothing.
    You could solve this with individual timers, or with a help counter to count active instances.
  • Add and Remove methods can't be used properly. With current structure, the user has no real power which point to remove.
    It was good if the "Add" function returns a Point or so, and the "Remove" function does take this Point as argument and removes it properly from the list.
  • function "Start" is the very same as "Resume". You actually can take "Start", "Stop", and "Resume" together to method enabled takes boolean flag returs nothing
  • Please comment 851983 which order it is or use a descriptive variable.
  • The issue Aniki pointed out needs to be fixed.
  • constant definitons of [99] arrays might work for most cases, but dynamic lists would be superior.
  • A link to used TimerUtils would be useful.
  • Do you know maze maps where units spawn at first Patrol point again when reaching the end? It could be a useful flag to set.
    For example boolean circular -- true would mean it moves by itself, normaly to the first point, so it makes a full "circle".
    And if false, it would be a linear movement only in one direction, from start to end, and then it would be moved back instantly by triggers.
    This would maybe be a cool addition some might want to use.
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
1. Will do
2. What? the add function works just fine, otherwise the system would not work.
3. I will see if I can merge them, I wont promise that I use that exact suggestion though.
4. Will do
5. Will do
6. They would, but it would be insanely rare for them to be used effectively.
7. Will do
8. Hm. Not interested in doing that yet. I'd rather have it approved before I add more features that may contain further errors and thus delay approval.
 

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,182
I think I get what you mean.. that is indeed troublesome.
I got a solution planned, I will remake the system a bit, I think it should fix it.

edit: or so I thought. My new code does not work, sadly and I don't have the will to spend hours on debugging. You got my permission to send this to substandard.
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
integer ORDER_ID = 851986;
The 851986 (move) order id seems to make enemy units not to respond to enemy attacks?

I don't see the point of the public struct PatrolPoint { ... } struct being public.
From a user's point of view calling patrol_path.add_point(x, y) (or equivalent) is better than calling patrol_path.push(PatrolPoint.create(x, y)) because the later introduces artifacts likePatrolPathHardDestroy.
PatrolPath can expose a set_point(point-index, x, y) method for changing a point's x and y coordinates.

JASS:
public struct PatrolState 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.
}
Hm, I think PatrolType might be a better name for the type of patrolling a unit will be doing.
Isn't NORMAL a LOOP-ing patrolling, BACKWARD a BACK_AND_FORTH patrolling and LOOP a TELEPORT_LOOP patrolling?

JASS:
public struct PatrolUnit {
    ...
    private timer clock;
    ...
}
I think you can use a single timer for all patrolling units, this way you won't need the optional TimerUtils library (and not destroy timers when its missing).

It seems to me that the optional UnitDex library is unnecessary because it only saves a single GetUnitTypeId(...) call.

You might want to consider always using curly brackets in the future:
JASS:
static if (!UnitDex)
    deallocate();
    table.integer.remove(GetHandleId(p_u));
 
The 851986 (move) order id seems to make enemy units not to respond to enemy attacks?
I think yes.


PatrolPoint:

One technicaly can use the same PatrolPoints for multiple PatrolPaths, and also change x/y of a point at any time (because it's public).
I thought of it can be good in some cases, and the standard vector destroy method (PatrolPath is a vectorT) would anyways only destroy the vector itself,
and not all data structed that is binded to it. So I just thought adding a new function which, when the user really does not need the PatrolPoints anymore, then he
can use this hard destroy method.
There is also this "_exists" protection so when a user maybe uses a hard destroy function on 2 PatrolPaths which share some same PatrolPoints, so that these PatrolPoints don't get deallocated twice.

Hm, I think
PatrolType
might be a better name for the type of patrolling a unit will be doing.
Isn't NORMAL a LOOP-ing patrolling, BACKWARD a BACK_AND_FORTH patrolling and LOOP a TELEPORT_LOOP patrolling?
Yes, "type" is better, will change.

"NORMAL" is actually a loop, too, yes. Haven't really thought about it. Though, maybe one could argue the very last move order (from FinalPoints to StartPoint)
is not directly defined in the PatrolPath, and so it's maybe not really part of the Patrol anymore. So it is not simple a LOOP of the PatrolPath, but it is a LOOP of
the PatrolPath + MoveFinal-MoveStart. And here, the actual "LOOP" is not a circle, but a 100% loop of the actual PatrolPath. ;D Does it make sense?

I think you can use a single timer for all patrolling units, this way you won't need the optional TimerUtils library (and not destroy timers when its missing).
I would need to write a new data structure, maybe a list, being able to loop through it, which I actually don't really needed.
I also don't need something like list access, or "next" or so, so I only would need it for looping. I thought using timer is a good (but maybe not faster, when many instances) solution.
(it looks a bit cleaner, too)

UnitDex also saves allocate() and deallocate(). ;D I started to make non-optional actually, because I peronaly would force everyone to use used libraries.^^
But Chaosy likes to have less dependency, so they are optional with TimerUtils. Would you personaly make them required or remove them?

You might want to consider always using curly brackets in the future:

JASS:
static if (!UnitDex)
    deallocate();
    table.integer.remove(GetHandleId(p_u));
Good catch, I probably should.^^

Update soon.:)
 
Level 13
Joined
Nov 7, 2014
Messages
571
The 851986 (move) order id seems to make enemy units not to respond to enemy attacks?

I think yes.
A likely use case of patrolling units would be to create guard units that the player would have to kill or pass undetected.
If the move order makes the guards not attack/respond to other units then it sort of defeats the whole purpose?

I think it would be best if the patrolling units would attack and chase for a certain [configurable] distance and then go back to their patrolling.
 
But it's configurable:
JASS:
//=================== --------- Config --------- ====================  
 
    integer ORDER_ID            = 851986;  // The order that the unit will receive to walk to the next point.
    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.
    boolean DESTROY_ON_DEATH    = false;   // When a PatrolUnit dies, the instance will be automaticaly destroyed.
                                           
   
    // move id    = 851986
    // attack id  = 851983
   
//=================== -------------------------- ====================
attack id is commented below.

I think patrols is often used in slide or maze maps, and there, they usually just "walk" and don't attack. But it's no problem if they change it.

I think it would be best if the patrolling units would attack and chase for a certain [configurable] distance and then go back to their patrolling.
When it's set to "attack" they will fight with the enemy, and automaticaly return to their patrol afterwerds when their current order gets "null".
 
Level 13
Joined
Nov 7, 2014
Messages
571
attack id is commented below.
I see.

When it's set to "attack" they will fight with the enemy, and automaticaly return to their patrol afterwerds when their current order gets "null".
It seems when the order is to attack (851983) the patrolling unit will chase an enemy unit forever, i.e the user would have to write extra code to check if a patrolling unit is too far away from its current point and if so order it to go back.

But it's configurable:
Its configurable globaly, although I am not sure it makes sense to have both patrolling units that respond to attacks and chase units and patrolling units that don't respond to attacks and don't chase.
 
You want feature that when a unit has a certain distance or bigger to the PatrolPoint, then the system forces the unit to walk to the patrol point (configurable).

And ^this feature and orderId setable for each unit instead globaly?

edit:

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

edit2:

with using InCombat library it would maybe work though. hm... :s

edit3:

[System] Combat State uses Event (extra requirment) and what is troublesome, an other unit indexer :(
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
It seems when the order is to attack (851983) the patrolling unit will chase an enemy unit forever, i.e the user would have to write extra code to check if a patrolling unit is too far away from its current point and if so order it to go back.
This is not true if the owner of the unit is PLAYER_NEUTRAL_AGGRESSIVE (creeps), the unit will chase until goes beyond the guard return distance (1000, in gameplay constats).

what to do if order is "attack", and the distance between two PatrolPoints is by default bigger than "allowed Max_Distance"?
The distance between patrolling points is arbitrary, my point is to add a "guard return distance" parameter to the system that would also work for units that are not owned by PLAYER_NEUTRAL_AGGRESSIVE player and that distance could be unit specific, not global like the one from the gameplay constants.
Of course you could decide that the built-in "guard return distance" for PLAYER_NEUTRAL_AGGRESSIVE is good enough and simply add that to the documentation =).
 
The distance between patrolling points is arbitrary, my point is to add a "guard return distance" parameter
Have you read my concern where it may get troublesome?

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,
each single callback call, for each registered unit, which is very much operations, or use an extra CombatState library, to check if unit is OutOfRange AND in combat.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Have you read my concern where it may get troublesome?

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,
Not sure I follow, why the next path position and not the current ( private integer p_pathPosition; // Current position in PatrolPath vector)?

Anyway... there is the EVENT_UNIT_ACQUIRED_TARGET event that you might be able to use. 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.

Actually the order should be "attack", and not "move".
If the order is attack and not move the unit could reacquire its target and continue chasing?

I would need to check for the unit distance to the PatrolLine,
each single callback call, for each registered unit, which is very much operations, or use an extra CombatState library, to check if unit is OutOfRange AND in combat.
Like I said, if you don't want to bother, you can piggyback on Blizzard's PLAYER_NEUTRAL_AGGRESSIVE + guard return distance behaviour.
 
Top