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

Behaviour AI System v1.5

A system to define custom behaviours in certain game events.
Uses StructureDD by Cokemonkey11 (credits must be given to him also)

More about the system can be found inside the code comments.
It's a thing i wrote for a personal project, any bugs / mistakes found and reported will be appreciated a lot since i do not have a lot of time on my hand to proof check everything.

I may include a version using Table instead of hashtables and/or extensions for custom events (ally death, ally takes damage, ally is targeted by spell, etc) but those can be replicated now using the provided interface anyway.

I might add unit variables to the behaviour struct to easier access the units in the events but you can check the documentation.

code:
JASS:
library BAIS uses StructuredDD

/************************************************************************************************/
/*  Behaviour AI system                                                                         */
/*  by Kingz                                                                                    */
/*                                                                                              */
/*  - Requires: StructuredDD by Cokemonkey11                                                    */
/*  Credits to Cokemonkey11 for his nice way of handling damage detection                       */
/*                                                                                              */
/*  Info:                                                                                       */
/*  The system works by defining your own behaviour struct by extending the supplied Behaviour  */
/*  struct and defining wanted methods. After doing so you attach it to the system.             */
/*  You can attach multiple behaviour structs to a single unit / unit type.                     */
/*  Behaviours will fire one after the other, on a principle of first attached first served.    */
/*  Regulating multiple behaviours is left to the end user.                                     */
/*  The system might be provided with an extension to detect allyDamage / allyDeath in the      */
/*  nearby vicinity if needed/requested.                                                        */
/*  Another feature i might add in is global behaviours which get registered to every unit.     */
/*  Reason i didn't implement it in is that i do not see much use for it.                       */
/*  The goal of the system is to allow easy vJass custom AI coding                              */
/*                                                                                              */
/*                                                                                              */
/*  API:                                                                                        */
/*                                                                                              */
/*  Struct BAIS:                                                                                */
/*                                                                                              */
/*  public static method attachBehaviour takes unit u, Behaviour b returns nothing              */
/*                                                                                              */
/*  public static method attachBehaviourType takes integer unitId, Behaviour b returns nothing  */
/*                                                                                              */
/*                                                                                              */
/*  public static method removeBehaviour takes unit u, Behaviour b returns boolean              */
/*                                                                                              */
/*  public static method removeBehaviourType takes integer unitId, Behaviour b returns boolean  */
/*                                                                                              */
/*                                                                                              */
/*  struct Behaviour:                                                                           */
/*
    stub method periodic takes nothing returns nothing
    ^ Use GetEnumUnit() to access the unit
    
    stub method onDamageTaken takes nothing returns nothing
    ^ use GetTriggerUnit() to access the unit, GetEventDamageSource() to access the damage dealer
    
    stub method onDamageDealt takes nothing returns nothing
    ^ use GetEventDamageSource() to access the unit, GetTriggerUnit() to access the damage receiver
    
    stub method castUpon takes nothing returns nothing
    ^ use GetSpellTargetUnit() to access the unit, GetTriggerUnit() to access the caster
    
    stub method onCast takes nothing returns nothing
    ^ use GetTriggerUnit() to access the unit, GetSpellTargetUnit() to access the victim
    
    stub method onDeath takes nothing returns nothing
    ^ use GetTriggerUnit() to access the unit, GetKillingUnit() to access the killer
    
    stub method onKill takes nothing returns nothing
    ^ use GetKillingUnit() to access the unit, GetTriggerUnit() to access the victim
*/
/*                                                                                              */
/*  struct CustomOrder                                                                          */
/*
    static method registerOrder takes unit u, string order, real cooltime returns nothing
    ^Registers a internal cooldown for AI skills for the unit using the order string
    ^BIG NOTE: At the moment you cannot register an order for a unit type, if you want something like that you have to put a call to registerOrder inside the behaviour
    before the registerTargetOrder/registerInstantOrder/registerPointOrder call
    ^BIG NOTE2: All registered orders start on cooldown, it's just the way it works. This can be resolved by setting the TIME_OFFSET to a higher value
    
    static method registerTargetOrder takes unit source, widget target, string orderstr, integer priority returns boolean
    ^Registers a widget target based order, returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
    
    static method registerPointOrder takes unit source, real targetX, real targetY, string orderStr, integer priority returns boolean
    ^Registers a location/point based order. returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
    
    static method registerInstantOrder takes unit source, string orderStr, integer priority returns boolean
    ^Registers an instant unit order, returns false if a higher priority order is in place or if it cannot be issued due to internal cooldown
    
    private static method canOrder takes unit u, string order returns boolean
    ^Called internally before trying to register a target/instant/point order checking the internal cooldown
*/
/*                                                                                              */
/*                                                                                              */
/* Example:                                                                                     */
/*
struct TC_AGRO extends Behaviour
    
    method castUpon takes nothing returns nothing
        local unit target = GetSpellTargetUnit()
        local unit source = GetTriggerUnit()
        local real tx = GetUnitX(target)
        local real ty = GetUnitY(target)
        local real sx = GetUnitX(source)
        local real sy = GetUnitY(source)
        call CustomOrder.registerOrder(target, "berserk", 6.0)
        if(SquareRoot((tx-sx)*(tx-sx) + (ty-sy)*(ty-sy)) < 250) then
            call CustomOrder.registerInstantOrder(target, "berserk", 2)
        endif
        set target = null
        set source = null
    endmethod

    method onDamageTaken takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local unit source = GetEventDamageSource()
        local item i
        if(GetWidgetLife(u) < 180 and GetUnitAbilityLevel(u, 'BSTN') == 0) then
            set i = GetItemOfTypeFromUnitBJ(u, 'phea')
            if(i != null) then
                call UnitUseItem(u, i)
                set i = null
            endif
        endif
        if(GetUnitState(u, UNIT_STATE_MANA) < 180) then
            set i = GetItemOfTypeFromUnitBJ(u, 'pman')
            if(i != null) then
                call UnitUseItem(u, i)
                set i = null
            endif
        endif
        call CustomOrder.registerOrder(u, "shockwave", 8.0)
        call CustomOrder.registerTargetOrder(u, source, "shockwave", 10)
        set u = null
        set source = null
    endmethod
    
    private static method onInit takes nothing returns nothing
        local Behaviour b = TC_AGRO.create()
        call BAIS.attachBehaviourType('Otch', b)
    endmethod

endstruct
*/
/*                                                                                              */
/*                                                                                              */
/************************************************************************************************/
native UnitAlive takes unit u returns boolean

globals
/****************************************************************************************************/
/*  CONFIGURATION                                                                                   */
/****************************************************************************************************/

    private constant real TICK = 0.3         // global timer period for periodic behaviour
    private constant real TIME_OFFSET = 30.0 // if set to 0 all custom orders start on cooldown, set to higher value to reduce initial cooldown
    private constant real CLEANUP_TIME = 60  // after this amount of time a dead unit is unregistered from the AI system unless specified otherwise
    private constant real CLEANUP_PERIOD = 60    // every CLEANUP_PERIOD of time run cleaner
    private constant boolean NEVER_UNREGISTER_HERO = true   // if set to true, hero units never release their AI behaviours
    private constant boolean NEVER_UNREGISTER_UNIT = false   // if set to true, AI behaviours are never released, even if dead
    private constant boolean PERIODIC_SKIPS_DEAD = true  // if the unit is dead, skip periodic behaviour, if false periodic behaviour always fires
    private constant boolean IGNORE_USER_UNITS = true // if set to false AI will be used for user controlled units also
    private constant boolean USE_CUSTOM_ORDER = true // if set to true allows the use of CustomOrder struct, else the struct (and it's methods/members) will not be generated
    
/****************************************************************************************************/
/*  DO NOT TOUCH BELOW UNLESS YOU KNOW WHAT YOU ARE DOING                                           */
/****************************************************************************************************/
    private constant integer ORDER_KEY = 0  // [CustomOrder hashtables]
    
    private constant integer OFFSET_KEY = 1  // [BAIS hashtables] offset used for behaviour storage
    private constant integer DEAD_KEY = 0   // [BAIS hashtables] key used to store amount of seconds the unit is dead
    private constant integer DATA_KEY = 3    // [BAIS hashtables] data key start
endglobals

struct Behaviour

    stub method periodic takes nothing returns nothing
    endmethod
    
    stub method onDamageTaken takes nothing returns nothing
    endmethod
    
    stub method onDamageDealt takes nothing returns nothing
    endmethod
    
    stub method castUpon takes nothing returns nothing
    endmethod
    
    stub method onCast takes nothing returns nothing
    endmethod
    
    stub method onDeath takes nothing returns nothing
    endmethod
    
    stub method onKill takes nothing returns nothing
    endmethod

endstruct

static if USE_CUSTOM_ORDER then
    struct CustomOrder
        private static hashtable orderData = InitHashtable()
        
        private integer pkey
        private unit source
        private widget target
        private string order
        private real tx
        private real ty
        private boolean locBased
        private integer priority
        
        method destroy takes nothing returns nothing
            set target = null
            set source = null
            call this.deallocate()
        endmethod
        
        static method executeOrder takes unit u returns nothing
            local thistype this
            local integer hkey = GetHandleId(u)
            if(HaveSavedInteger(thistype.orderData, hkey, ORDER_KEY) == false or LoadInteger(thistype.orderData, hkey, ORDER_KEY) == -1) then
                return
            endif
            set this = LoadInteger(thistype.orderData, hkey, ORDER_KEY)
            if(target == null) then
                if(locBased == true) then
                    call IssuePointOrder(source, order, tx, ty)
                else
                    call IssueImmediateOrder(source, order)
                endif
            else
                call IssueTargetOrder(source, order, target)
            endif
            call SaveReal(thistype.orderData, pkey, -OrderId(order), BAIS.getElapsedTime())
            call SaveInteger(thistype.orderData, pkey, ORDER_KEY, -1)
            call this.destroy()
        endmethod
        
        static method registerOrder takes unit u, string order, real cooltime returns nothing
            local integer oid = OrderId(order)
            call SaveReal(thistype.orderData, GetHandleId(u), oid+ORDER_KEY+1, cooltime)
            if(HaveSavedReal(thistype.orderData, GetHandleId(u), -oid) == false) then
                call SaveReal(thistype.orderData, GetHandleId(u), -oid, 0)
            endif
        endmethod
        
        private static method canOrder takes unit u, string order returns boolean
            local real pass_time = 0
            local real cool_time = 0
            local integer uHandle = GetHandleId(u)
            local integer oId = OrderId(order)
            if(HaveSavedReal(thistype.orderData, uHandle, oId+ORDER_KEY+1) == false) then
                return false
            endif
            set cool_time = LoadReal(thistype.orderData, uHandle, oId+ORDER_KEY+1)
            set pass_time = BAIS.getElapsedTime() - LoadReal(thistype.orderData, uHandle,-oId)
            return (pass_time >= cool_time)
        endmethod
        
        private static method create takes unit u, string orderstr, widget target, real x, real y, integer prio, boolean isLoc returns thistype
            local thistype this = thistype.allocate()
            set this.source = u
            set this.order = orderstr
            set this.target = target
            set this.tx = x
            set this.ty = y
            set this.priority = prio
            set this.locBased = isLoc
            set this.pkey = GetHandleId(u)
            call SaveInteger(thistype.orderData, this.pkey, ORDER_KEY,this)
            return this
        endmethod
        
        private static method registerGeneralOrder takes unit u, widget target, string orderstr, real x, real y, boolean locOrder, integer priority returns boolean
            local thistype co
            local integer saved_prio
            local integer pkey = GetHandleId(u)
            if(thistype.canOrder(u, orderstr) == false) then
                return false
            endif
            if(HaveSavedInteger(thistype.orderData, pkey, ORDER_KEY) == true and LoadInteger(thistype.orderData, pkey, ORDER_KEY) != -1) then
                set co = LoadInteger(thistype.orderData, pkey, ORDER_KEY)
                if(priority > co.priority) then
                    call co.destroy()
                    set co = CustomOrder.create(u, orderstr, target, x, y, priority, locOrder)
                    return true
                endif
            else
                set co = CustomOrder.create(u, orderstr, target, x, y, priority, locOrder)
                return true
            endif
            return false
        endmethod
        
        static method registerTargetOrder takes unit u, widget target, string orderstr, integer priority returns boolean
            return registerGeneralOrder(u, target, orderstr, 0, 0, false, priority)
        endmethod
        
        static method registerPointOrder takes unit u, real x, real y, string orderstr, integer priority returns boolean
            return registerGeneralOrder(u, null, orderstr, x, y, true, priority)
        endmethod
        
        static method registerInstantOrder takes unit u, string orderstr, integer priority returns boolean
            return registerGeneralOrder(u, null, orderstr, 0, 0, false, priority)
        endmethod

    endstruct
endif

struct BAIS extends array
    private static hashtable unitData = InitHashtable()
    private static hashtable unitIdData = InitHashtable()
    private static group unitPool = CreateGroup()
    private static timer looper = CreateTimer()
    private static timer cleanup = CreateTimer()
    private static real  timeElapsed = TIME_OFFSET
    
    
    
    //! textmacro EVENT_HANDLE takes opName 
    set pkey = GetUnitTypeId(u)
    if(u != null and ((GetPlayerController(GetOwningPlayer(u)) == MAP_CONTROL_USER and IGNORE_USER_UNITS == false) or (GetPlayerController(GetOwningPlayer(u)) != MAP_CONTROL_USER and IGNORE_USER_UNITS == true))) then
        set offset = LoadInteger(thistype.unitIdData, pkey, OFFSET_KEY)
        set i = 0
        loop
            exitwhen i >= offset
            set b = LoadInteger(thistype.unitIdData, pkey, DATA_KEY+i)
            call b.$opName$()
            set i = i+1
        endloop
        
        set pkey = GetHandleId(u)
        set offset = LoadInteger(thistype.unitData, pkey, OFFSET_KEY)
        set i = 0
        loop
            exitwhen i >= offset
            set b = LoadInteger(thistype.unitData, pkey, DATA_KEY+i)
            call b.$opName$()
            set i = i+1
        endloop
        static if(USE_CUSTOM_ORDER) then
            call CustomOrder.executeOrder(u)
        endif
    endif
    //! endtextmacro

    
    public static constant method getTimerPeriod takes nothing returns real
        return TICK
    endmethod
    
    public static constant method getElapsedTime takes nothing returns real
        return thistype.timeElapsed
    endmethod
    
    public static method attachBehaviour takes unit u, Behaviour b returns nothing
        local integer pkey = GetHandleId(u)
        local integer offset = LoadInteger(thistype.unitData, pkey, OFFSET_KEY)
        call SaveInteger(thistype.unitData, pkey, DATA_KEY+offset, b)
        call SaveInteger(thistype.unitData, pkey, OFFSET_KEY, offset+1)
        call GroupAddUnit(thistype.unitPool, u)
    endmethod
    
    public static method attachBehaviourType takes integer ut, Behaviour b returns nothing
        local integer pkey = ut
        local integer offset = LoadInteger(thistype.unitIdData, pkey, OFFSET_KEY)
        call SaveInteger(thistype.unitIdData, pkey, DATA_KEY+offset, b)
        call SaveInteger(thistype.unitIdData, pkey, OFFSET_KEY, offset+1)
    endmethod
    
    private static method removeBehaviourEx takes hashtable h, integer pkey, Behaviour b returns boolean
        local integer offset = LoadInteger(h, pkey, OFFSET_KEY)
        local integer i = 0
        local Behaviour temp = 0
        loop
            exitwhen i >= offset
            set temp = LoadInteger(h, pkey, DATA_KEY+i)
            if(temp == b) then
                set temp = LoadInteger(h, pkey, DATA_KEY+offset-1)
                call SaveInteger(h, pkey, DATA_KEY+i, temp)
                call SaveInteger(h, pkey, OFFSET_KEY, offset-1)
                return true
            endif
            set i = i +1
        endloop
        return false
    endmethod
    
    public static method removeBehaviour takes unit u, Behaviour b returns boolean
        return removeBehaviourEx(thistype.unitData, GetHandleId(u), b)
    endmethod
    
    public static method removeBehaviourType takes integer ut, Behaviour b returns boolean
        return removeBehaviourEx(thistype.unitIdData, ut, b)
    endmethod
    
    private static method deathHandler takes nothing returns boolean
        local unit u = GetTriggerUnit()
        local integer pkey = GetUnitTypeId(u)
        local Behaviour b
        local integer offset = 0
        local integer i = 0
        
        //! runtextmacro EVENT_HANDLE("onDeath")
        set u = GetKillingUnit()
        //! runtextmacro EVENT_HANDLE("onKill")
    
        set u = null
        return false
    endmethod
    
    private static method castHandler takes nothing returns boolean
        local unit u = GetTriggerUnit()
        local integer pkey = GetUnitTypeId(u)
        local Behaviour b
        local integer offset = 0
        local integer i = 0
        
        //! runtextmacro EVENT_HANDLE("onCast")
        set u = GetSpellTargetUnit()
        //! runtextmacro EVENT_HANDLE("castUpon")

        
        set u = null
        return false
    endmethod

    private static method ddHandler takes nothing returns nothing
        local unit u = GetTriggerUnit()
        local integer pkey = GetUnitTypeId(u)
        local integer offset = 0
        local integer i = 0
        local Behaviour b

        //! runtextmacro EVENT_HANDLE("onDamageTaken")
        set u = GetEventDamageSource()
        //! runtextmacro EVENT_HANDLE("onDamageDealt")
        
        set u = null
    endmethod
    
    private static method handleUnit takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer uId = GetUnitTypeId(u)
        local integer pkey = uId
        local integer offset = 0
        local integer i = 0
        local Behaviour b
        
        if(UnitAlive(u) == false and PERIODIC_SKIPS_DEAD) then
            set u = null
        else
            //! runtextmacro EVENT_HANDLE("periodic")
        endif
        
        if(NEVER_UNREGISTER_UNIT == false and (IsHeroUnitId(uId) and NEVER_UNREGISTER_HERO)) then
            set i = LoadInteger(thistype.unitData, pkey, DEAD_KEY)
            call SaveInteger(thistype.unitData, pkey, DEAD_KEY, i +1)
        endif
        
        set u = null
    endmethod
    
    private static method loopHandler takes nothing returns nothing
        call ForGroup(thistype.unitPool, function thistype.handleUnit)
        set thistype.timeElapsed = thistype.timeElapsed + TICK
    endmethod
    
    private static method cleanUnit takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer uHandle = GetHandleId(u)
        local boolean isHero = IsHeroUnitId(GetUnitTypeId(u))
        local real timeDead = LoadInteger(thistype.unitData, uHandle, DEAD_KEY)
        if(NEVER_UNREGISTER_UNIT or (isHero and NEVER_UNREGISTER_HERO)) then 
            set u = null
            return
        endif
        if(timeDead >= CLEANUP_TIME) then
            call GroupRemoveUnit(thistype.unitPool, u)
            call FlushChildHashtable(thistype.unitData, uHandle)
        endif
        set u = null
    endmethod
    
    private static method clean takes nothing returns nothing
        call ForGroup(thistype.unitPool, function thistype.cleanUnit)
    endmethod
    
    private static method onInit takes nothing returns nothing
        local trigger castTracker = CreateTrigger()
        local trigger deathTracker = CreateTrigger()
        
        call TriggerRegisterAnyUnitEventBJ(castTracker, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(castTracker, function thistype.castHandler)
        
        call TriggerRegisterAnyUnitEventBJ(deathTracker, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddCondition(deathTracker, function thistype.deathHandler)
        
        call TimerStart(thistype.looper, TICK, true, function thistype.loopHandler)
        
        call StructuredDD.addHandler(function thistype.ddHandler)
        
        call TimerStart(thistype.cleanup, CLEANUP_PERIOD, true, function thistype.clean) 
    endmethod

endstruct

endlibrary

changelog:
v1.1 removed create method, fixed some typos in documentation, BAIS now extends array because there is no reason not to
v1.2 Added custom order functionality with internal cooldowns, added more documentation, changed the test map a bit
v1.3 code stayed the same, added example with ZTS threat system included, for fun try to beat the enemy forces AI if you can
v1.4 updated code, removed some leaks, less function calls, removed some duplicate variables (thanks to TriggerHappy)
v1.5 updated code with suggestions made by PurgeandFire

Keywords:
ai, wc3, units, behaviour, system, vjass, jass, event, handling
Contents

Behaviour AI System (Map)

Reviews
22:08, 6th Jul 2014 PurgeandFire: (old) Review: http://www.hiveworkshop.com/forums/2552185-post29.html Changes made, approved. Great AI system!

Moderator

M

Moderator

22:08, 6th Jul 2014
PurgeandFire: (old) Review:
http://www.hiveworkshop.com/forums/2552185-post29.html


Changes made, approved. Great AI system!
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
JASS:
private static method create takes nothing returns thistype
        local thistype this = thistype.allocate()
        return this
endmethod
->
JASS:
private static method create takes nothing returns thistype
        return thistype.allocate()
endmethod

Or simply don't write it since JASSHelper will generate it.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Will probably convert the methods into functions since they are all static and scrap the struct model it is using atm.
I am holding it in a struct form only until i figure out if there is any point in doing so since im not really using any of it's feature.
If people throw some ideas at me which would use the struct for anything id probably keep it, maybe for some extensions on the system.

Thanks for the feedback tho.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I wouldn't use stub methods, yet I'm certainly not demand a change here.
You may face criticism, as many people here try to enforce a higher efficency.

I've been working alot on better AI behaviour over the last month for a project of mine.
I turned out to be extremly powerful, but can only used for RPG maps (1-x heroes vs mosters). Admittedly it does only support AI for each UnitTypeId and can't work different for two units of the same type.
My approach is 100% different to yours and I'm really excited how this one will develop over the next time.

I will release my version aswell as soon as it has a better API. :)

Btw 0.1 timeout is a really short interval for AI responses. I mean 600 potential actions per minute is a lot! No player can do so not even half.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
That is quite nice to hear as we lack AI systems on THW.
I am still in the process of modding this AI interface and model and i am mostly looking in for feedback at this point of development.

I have been thinking of developing an OrderQueue which would be able to index unit orders and decided based on their priority what to do, with an aging factor.
So for example you define multiple behaviours which issue orders, you then put them in the queue and as time passes the orders in the queue gain in priority so you start seeing pseudo random acts.
Tho this would require remembering only the recent n orders (where n would be < 10 for common sense sake).

I am not sure if i want to make it that complex yet.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Yes i think it does provide a nice synergy.
Users could implement that system, and then in their handler methods issue orders at the units with most threat.

I will probably add an example to the map and link it for future reference.

Atm i am thinking of doing a CustomOrder struct to allow priority order selection from user methods and internal cooldown handling.
Something like this inside a user method:
JASS:
call CustomOrder.register(orderedUnit, orderString, cooldown) // registers the order inside a table
if(CustomOrder.canIssue(orderedUnit, orderString)) then // checks the order for it's internal cooldown
 call CustomOrder.targetOrder(orderedUnit, orderString, targetWidget, priority) // order is valid, adds it as a contender for execution, if it isn't executed the internal cooldown won't be triggered
endif

Then after going through all behaviours it would execute the order which had the highest priority and ignore the rest or maybe queue them up?
Anyhow after the order would be issued it's internal timer would reset.

I will probably make it optional so users can still use normal orders if they want to.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
ForGroup are so damn slow callbacks :(

How to avoid ForGroup and use FirstOfGroup:
- Make linked lists of size i.e 10.
- Units in one list are considered friendly.
- Once unit is activated the system checks for close allied groups/list, if they are not full the unit is pushed into that list.
- Units beeing pushed into a list by activation event (DDS, EVENT_UNIT_AQUIRE_TARGET, ...), also activate nearby allies. Those will be pushed into the same list ignoring the size limit.
- Each list has its own timer to evenly spread actions. i.e NewTimerEx(list), avoiding the op limit and reducing laggs to zero.
- Units are only part of the list for a specific time i.e 20 seconds. Incoming damage, unit events refresh the time member. When time hits < 0 they are removed from the list. Once the list is empty it gets destroyed and the timer will be stopped /released.
- Clock timeout can be huge for instance 2.5 seconds.

A list size of 10 probably won't have a noticeable impact on the game perfomance at all. Also they allow intelligent group behaviour (surrounding, positioning, helping ...)
The big advantage is that if noone is fighting the system does absolute nothing. No function call, no timers, nothing :D

Some thoughts out of my own Threat system:
- The loop only collects information (Alive, x, y, facing, life%, target, target x, target y, angle, ..) other members are set once the unit get pushed into the list (UnitId, UnitIndex, ranged or meele, ...).
No locals all of these are struct members.
- Use one or two spam protection member (integers). Units which just performed an action should be ignored for x loops.
- User can define pluggins (modules/textmacros) which can be implemented into the loop like HelperPluggin, EngagePluggin, ...
JASS:
            loop
                if UnitAlive(who) then
                    /*
                    *   Each units is only part of the loop for
                    *   THREAT_TIME seconds.
                    */
                    set time = time - CLOCK_TIMEOUT
                    /*
                    *   Spam protection for engage response event.
                    */
                    set spam_protection = spam_protection - 1
                    /*
                    *   Last taken damage is only stored for 3
                    *   clock timeouts
                    */
                    set counter = counter - 1
                    if (0 > counter) then
                        set lastDamage = 0
                    endif
                    /*
                    *   Some actions require more than
                    *   1 clock timeout, hence pause was implemeneted.
                    */
                    if (0 < pause_threat) then
                        set pause_threat = pause_threat - 1
                    elseif NotStunned(who) then
                        /*
                        *   Gathering information for the Threat unit.
                        */
                        set who_x = GetUnitX(who)
                        set who_y = GetUnitY(who)
                        set life  = GetWidgetLife(who)/state
                        set e     = (aim != null) 
                        
                        if e and UnitAlive(aim) then
                            set aim_x = GetUnitX(aim)
                            set aim_y = GetUnitY(aim)
                            set angle = Atan2(who_y - aim_y ,who_x - aim_x)
                        else
                            set e = false
                            set lastDamage = 0
                        endif
                        /*
                        *   The flee pluggin pauses the unit for 2 clock timeout.
                        *   Certainly not finished yet ....
                        */
                        //! runtextmacro optional THREAT_RETREAT_PLUGGIN()
                        /*
                        *   The helper pluggin is checking for injured units.
                        */
                        //! runtextmacro optional THREAT_HELPER_PLUGGIN()
                        /*
                        *   Per loop only 1 unit performs a unique action. d is a local boolean which will become false within then POSITION_PLUGGIN
                        */
                        if d then
                        /*
                        *   The position pluggin checks if the target should be
                        *   surrounded or demands a special assault tactic.
                        */
                        //! runtextmacro optional THREAT_POSITION_PLUGGIN()
                        endif
                        /*
                        *   Action response fires individual unit spells
                        */
                        //! runtextmacro optional THREAT_ACTION_RESPONSE_PLUGGIN()
                    endif
        
                    if (0 > time) and (0>spam_protection) then
                        call display(false)
                        call remove()
                    endif
                else
                    call display(false)
                    call remove()
                endif
        
                set this = next[this]
                exitwhen 0 == this
            endloop

How to fire events, making every unit unique:
Use Table like Bribe did in SpellEffectEvent, based on UnitTypeId.
JASS:
    function RegisterThreatEngageEvent takes integer id, boolexpr expr returns nothing
        if not Threat.ENGAGE.handle.has(id) then
            set Threat.ENGAGE.trigger[id] = CreateTrigger()
        endif
        call TriggerAddCondition(Threat.ENGAGE.trigger[id], expr)
    endfunction

    if Conditions and OtherConditions then
         set Threat.current = this//readonly static thistype current  
         call TriggerEvaluate(Threat.ENGAGE.trigger[this.id])
Allows
JASS:
private function FootmanResponse takes nothing returns boolean
    local Threat threat = Threat.current
    local Charge charge = Charge.create(threat.who, threat.aimX, threat.aimY, ...).
    set charge.damage = 100.
    set charge.duration = 15.
    //
    set threat.spam = 4//method operator spam, this unit is now ignored for the next 4 loop

That's some of the stuff I did in my Threat system.
Furthermore single unit events are better than player_unit_events. Couple UnitIndexer, a onFilter method and a modified version of TriggerRefresh to rebuild triggers for an excellent result.
 
Last edited:
Level 25
Joined
Jun 5, 2008
Messages
2,572
The ForGroups aren't spammed that often, i will push the timer to 0.3 or 0.5 interval and cleanup timer is already running only every 60 seconds. So in one minute there will be about 121 calls to it.
If that however makes a hit on performance i will consider alternatives.
I will probably do some stress tests with a large number of units inside the group and look for the optimal cleanup / tick intervals when i get the time.

I noticed a flaw in my documentation/implementation.
Periodic behaviour for unit types will only trigger for units already inside the UNIT_POOL group.
This could be offseted if i indexed all units always on map entering, or created a trigger which would add the unit to the group when entering the map if there is a behaviour for such a unit defined which is probably gonna be implemented in the next version.

Tho i will still have to add all units of type to UNIT_POOL after calling the attachBehaviourType already present on the map which aren't in the group, or again leave it to the user.

I could switch from ForGroup handling to a GroupEnum condition handling probably, if it would make a big difference.

Edit:

I thought about implementing a Threat system inside the AI but i decided to leave it to the end user, since it's way easier to add one to those who need one than it is to modify one out for those who don't.

So atm i am focusing on the CustomOrder thingy to see how will it play out, will leave it optional tho.

Edit2:

I heard that the trigger destruction errors are now a thing of the past since the return bug is no more, i think i read it on TheHelper.net.
If true it would mean that you wouldn't need to recycle triggers and could just recreate them.

Furthermore single unit events are better than player_unit_events. Couple UnitIndexer, a onFilter method and a modified version of TriggerRefresh to rebuild triggers for an excellent result.

Can i ask you for the source of this discovery? First time i hear about it. (unit specific > player unit specific thingy)
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Can i ask you for the source of this discovery? First time i hear about it. (unit specific > player unit specific thingy)
I'm talking about for exmaple EVENT_PLAYER_UNIT_ATTACKED vs. EVENT_UNIT_ATTACKED
the latter will only fire, if that specific unit is attacked, while the other one is fired if any unit is attacked.

Yes you can rebuild a trigger by simply recreating it and re-register all conditions.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
My initial design did include trigger evaluations and conditions also but i scrapped it because you cannot remove conditions dynamically as far as i remember without recreating and re-registering other conditions.

Unit specific events do work nice when there is trigger recreation, however if you for example wanted to change the AI of a boss by removing some old behaviour and adding new ones at some points of the fight you would have to recreate the trigger.
I opted for a simpler solution.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Bump for update notice:

Users can now (if they wish) add prioritized orders with internal cooldowns to their behaviour structs.
This is limited to specific units at the moment, if you wish to make it work with unit types there is an example showing how to. (do note this causes some unneeded calls as you need to add a CustomOrder.registerOrder() call in behaviour methods before calling a specific registerOrder$Target/Point/Instant$ method)
If you wish to update the cooldowns you can do so at any time by calling the CustomOrder.registerOrder() method for the specific unit, do note, you CANNOT reset an internal cooldown.
User can also indicate whether he wants the AI to apply on user units also or just non human controlled units.

NOTE: CustomOrder struct is meant to be used exclusively with the system, it will NOT be generated at all if you say so in the configuration (static if). Calling any method of the struct while specifying you do not wish to use custom orders will result in compile errors.

Custom orders will always take priority over user issued orders due to them being issued last.

Next update will probably show how to use this with a threat system and will contain quality of life changes.
The BAIS Extension library is an optional library i am thinking of developing which can extend the system to allow ally methods. This is still not confirmed.

As always any reports considering more efficency are fairly welcome.
Edit:

Another plan for next update is prevention of preemption, that is an custom order taking over while a different custom order is executing.
Will have to modify the canOrder method for that.


Can't think of a good way to do this, scrapped the idea for now.

Edit:

v1.3 updated
New test map, ZTS threat system included in the test map to show how would you set it up along something like this.
Both player and AI forces are now equal, try to see if you can outmicro the enemy heroes and win.
I might include proper functions for use with ZTS and custom orders later (since a registered order may not be issued it would be wrong to add threat anyway), but BPower is working on a threat system also so i am gonna wait and see how that works out.
 
Last edited:
Hey, nice idea. I have some code suggestions for now.

  • You should be using thekeyfor your key constants.
  • GetHandleIdandOrderIdcalls should be stored inside a local, to avoid so many unnecessary function calls. The sames goes for the rest of the functions, but those are the ones that stood out.
  • I don't see the point of this.
    JASS:
    local integer pkey = GetUnitTypeId(u)
    local integer uid = GetUnitTypeId(u)
  • You need to null all struct members which are handles, to prevent leaking.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Hey, nice idea. I have some code suggestions for now.

  • You should be using thekeyfor your key constants.
  • GetHandleIdandOrderIdcalls should be stored inside a local, to avoid so many unnecessary function calls. The sames goes for the rest of the functions, but those are the ones that stood out.
  • I don't see the point of this.
    JASS:
    local integer pkey = GetUnitTypeId(u)
    local integer uid = GetUnitTypeId(u)
  • You need to null all struct members which are handles, to prevent leaking.

Thanks for the review, will fix those things asap.
Edit:
v1.4
Updated code.

Using
JASS:
key
throws error in my JNGP for some reason so it stayed a constant integer.
Removed as much redundant calls as i could track down and stored them into locals, didn't bother storing them into locals where there was only one call.
Nulled struct members in onDestroy method, all leaks should now be covered.

Edit:
According to a THW member a "Double free of type: CustomOrder" messages seem to appear.
I will look into this, probably calling a destruction somewhere where it is not needed.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Looks quite nice. You could restructure your code and put methods beeing called from other methods above them.
In structs it is possible to call any method no matter the position, because vJass generates a copy if that method in question is below.
Basically just needless extra code is generated.

if UnitAlive(unit) == false then --> if not UnitAlive(unit)
In general boolean == true --> boolean // boolean == false --> not boolean

Maybe you could run a stress test to see how this system performs for let's say ~50 -100 units.

A few comments in the example code would be a win. Just comment in 3 or 4 lines how you set up the unit/hero.
 
Last edited:
When you update it, here are a few things for you to do:
  • Move clean, castHandler, deathHandler, loopHandler, and ddHandler all above onInit so that jasshelper won't generate a bunch of extra code.
  • Make a separate library with Behaviour spelled as Behavior for the non-Brits. (jk)
  • This:
    JASS:
    set i = LoadInteger(thistype.UNIT_DATA, pkey, DEAD_KEY)
    set i = i + 1
    call SaveInteger(thistype.UNIT_DATA, pkey, DEAD_KEY, i)
    Can be:
    JASS:
    set i = LoadInteger(thistype.UNIT_DATA, pkey, DEAD_KEY)
    call SaveInteger(thistype.UNIT_DATA, pkey, DEAD_KEY, i + 1)
    You can also make it a one-liner, but that hurts readability.
  • Move handleUnit above loopHandler.
  • When you use the textmacros, you always have to redeclare a variable for offset, behavior, i, etc. You may want to consider making globals for them (non-configurable) so that the code becomes simpler. It is good to avoid repeated code, whenever possible. :)
  • Just for future reference, for static variables you can always just use type the name without the thistype. e.g. thistype.UNIT_DATA -> UNIT_DATA. It might help with readability. In general, I would recommend JPAG's convention (all caps for constants, but regular globals are camelCase -> unitData), but that isn't required for the spell section. :)
  • You can consolidate this:
    JASS:
        public static method removeBehaviour takes unit u, Behaviour b returns boolean
            local integer pkey = GetHandleId(u)
            local integer offset = LoadInteger(thistype.UNIT_DATA, pkey, OFFSET_KEY)
            local integer i = 0
            local Behaviour temp = 0
            loop
                exitwhen i >= offset
                set temp = LoadInteger(thistype.UNIT_DATA, pkey, DATA_KEY+i)
                if(temp == b) then
                    set temp = LoadInteger(thistype.UNIT_DATA, pkey, DATA_KEY+offset-1)
                    call SaveInteger(thistype.UNIT_DATA, pkey, DATA_KEY+i, temp)
                    call SaveInteger(thistype.UNIT_DATA, pkey, OFFSET_KEY, offset-1)
                    return true
                endif
                set i = i +1
            endloop
            return false
        endmethod
        
        public static method removeBehaviourType takes integer ut, Behaviour b returns boolean
            local integer pkey = ut
            local integer offset = LoadInteger(thistype.UNIT_ID_DATA, pkey, OFFSET_KEY)
            local integer i = 0
            local Behaviour temp = 0
            loop
                exitwhen i >= offset
                set temp = LoadInteger(thistype.UNIT_ID_DATA, pkey, DATA_KEY+i)
                if(temp == b) then
                    set temp = LoadInteger(thistype.UNIT_ID_DATA, pkey, DATA_KEY+offset-1)
                    call SaveInteger(thistype.UNIT_ID_DATA, pkey, DATA_KEY+i, temp)
                    call SaveInteger(thistype.UNIT_ID_DATA, pkey, OFFSET_KEY, offset-1)
                    return true
                endif
                set i = i +1
            endloop
            return false
        endmethod
    Into:
    JASS:
        private static method removeBehaviourEx takes hashtable hash, integer pkey, Behaviour b returns boolean
            local integer offset = LoadInteger(hash, pkey, OFFSET_KEY)
            local Behaviour temp = 0
            local integer i = 0
            loop
                exitwhen i >= offset
                set temp = LoadInteger(hash, pkey, DATA_KEY + i)
                if (temp == b) then
                    set temp = LoadInteger(hash, pkey, DATA_KEY + offset - 1)
                    call SaveInteger(hash, pkey, DATA_KEY + i, temp)
                    call SaveInteger(hash, pkey, OFFSET_KEY, offset - 1)
                    return true
                endif
                set i = i + 1
            endloop
            return false
        endmethod
    
        public static method removeBehaviour takes unit u, Behaviour b returns boolean
            return removeBehaviourEx(UNIT_DATA, GetHandleId(u), b)
        endmethod
        
        public static method removeBehaviourType takes integer ut, Behaviour b returns boolean
            return removeBehaviourEx(UNIT_ID_DATA, ut, b)
        endmethod
    I wonder if it is possible to somehow consolidate the macros a bit as well. I don't know. Food for thought.
  • This:
    JASS:
            method onDestroy takes nothing returns nothing
                set target = null
                set source = null
            endmethod
    Can become:
    JASS:
    method destroy takes nothing returns nothing
        set target = null
        set source = null
        call this.deallocate()
    endmethod
    The method above rewrites the destroy method. The onDestroy method uses a trigger evaluation to evaluate the code whenever an instance of the struct is destroyed. That is only useful for polymorphism, so it is better to just rewrite the destroy method. Also, you should put it above your other methods to make sure it won't make any extra code.
  • The "register<>Order" functions can be consolidated as well. Simply make a big helper function and then use the other 3 as wrappers to call it:
    JASS:
            private static method registerGeneralOrder takes unit u, widget target, string orderstr, real x, real y, integer priority returns boolean
                local thistype co
                local integer saved_prio
                local integer pkey = GetHandleId(u)
                if(thistype.canOrder(u, orderstr) == false) then
                    return false
                endif
                if(HaveSavedInteger(thistype.ORDER_DATA, pkey, ORDER_KEY) == true and LoadInteger(thistype.ORDER_DATA, pkey, ORDER_KEY) != -1) then
                    set co = LoadInteger(thistype.ORDER_DATA, pkey, ORDER_KEY)
                    if(priority > co.priority) then
                        call co.destroy()
                        set co = CustomOrder.create(u, orderstr, target, x, y, priority, false)
                        return true
                    endif
                else
                    set co = CustomOrder.create(u, orderstr, target, x, y, priority, false)
                endif
                return false
            endmethod
    
            static method registerTargetOrder takes unit u, widget target, string orderstr, integer priority returns boolean
                return thistype.registerGeneralOrder(u, target, orderstr, 0, 0, priority)
            endmethod
            
            static method registerPointOrder takes unit u, real x, real y, string orderstr, integer priority returns boolean
                return thistype.registerGeneralOrder(u, null, orderstr, x, y, priority)
            endmethod
            
            static method registerInstantOrder takes unit u, string orderstr, integer priority returns boolean
                return thistype.registerGeneralOrder(u, null, orderstr, 0, 0, priority)
            endmethod

Feel free to take your time. This looks like a promising system! I might have a bit more to review, but that should be enough for now.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
Code updated with suggestions made by PurgeandFire, excluding:
Just for future reference, for static variables you can always just use type the name without the thistype. e.g.

Reasoning: I prefer clearly defining what is a static variable by putting thistype. in front of them, makes it easier to understand and follow imo.

When you use the textmacros, you always have to redeclare a variable for offset, behavior, i, etc. You may want to consider making globals for them (non-configurable) so that the code becomes simpler. It is good to avoid repeated code, whenever possible. :)

The macros use local variables when expanded, i do not see the need for globals. You wouldn't use a global var for looping for example.
I might have not understood your comment tho, feel free to elaborate.

Anyway, real changes (Ally and Enemy extensions) are under consideration i guess?
I feel like maybe it would stress the system too much and allow for infinite loops if configured badly so it could destabilize the system.

Edit:
Thinking of adding a helper library:
JASS:
library BAISHelper requires BAIS
/************************************************************************************************/
/*  Behaviour AI helper                                                                         */
/*  by Kingz                                                                                    */
/*                                                                                              */
/*                                                                                              */
/*  Info:                                                                                       */
/*  Provides constant methods for getting source/target units for BAIS stub methods             */
/*                                                                                              */
/************************************************************************************************/

struct BAISHelper

    static constant method getDamageTakenSource takes nothing returns unit
        return GetEventDamageSource()
    endmethod
    
    static constant method getDamageTakenTarget takes nothing returns unit
        return GetTriggerUnit()
    endmethod
    
    static constant method getDamageDealtSource takes nothing returns unit
        return GetEventDamageSource()
    endmethod
    
    static constant method getDamageDealtTarget takes nothing returns unit
        return GetTriggerUnit()
    endmethod
    
    static constant method getCastUponSource takes nothing returns unit
        return GetTriggerUnit()
    endmethod
    
    static constant method getCastUponTarget takes nothing returns unit
        return GetSpellTargetUnit()
    endmethod
    
    static constant method getOnDeathSource takes nothing returns unit
        return GetKillingUnit()
    endmethod
    
    static constant method getOnDeathTarget takes nothing returns unit
        return GetTriggerUnit()
    endmethod
    
    static constant method getOnKillSource takes nothing returns unit
        return GetKillingUnit()
    endmethod
    
    static constant method getOnKillTarget takes nothing returns unit
        return GetTriggerUnit()
    endmethod

endstruct

endlibrary

But not sure if needed, i think it might be useful for some people.

Edit2:

I am open to suggestions about adding more functionality, if anyone has any ideas feel free to say.

Edit3:

Forgot to change boolean == false to not boolean. Will make that change in the next version.

Edit4:

Thanks for approval Purge.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
I think that local/global behaviors would be trivial with Trigger. It would improve your API and add some more flexibility too. Give it some thought ;). What's especially cool is that you could inherit behaviors and build on top of them. You could also create behavior plugins, a set of behaviors, and easily add this set to a unit or set of units. There is just a lot ;D.
 
Level 25
Joined
Jun 5, 2008
Messages
2,572
I think that local/global behaviors would be trivial with Trigger. It would improve your API and add some more flexibility too. Give it some thought ;). What's especially cool is that you could inherit behaviors and build on top of them. You could also create behavior plugins, a set of behaviors, and easily add this set to a unit or set of units. There is just a lot ;D.

I will be honest and say that improvements are probably possible (like removal of stub methods) but i feel like the current api is really simple to use.

You can already inherit behaviours by extending an existing behaviour struct i think (didn't test to be honest and haven't looked at this in some time).
If someone wishes to make a more effective AI system based on this code they are free to do so, as we do lack AI systems on THW.

And id rather not complicate the code, it's really easy to use atm if you have any knowledge of jass, adding behaviour plugins, behaviour sets and such things might just make it harder to use (but more modular).

So if you think it deserves an upgrade Nes, you are free to build atop this or write your own AI system, more of them certainly wouldn't hurt.

Thanks for the opinion tho.
 
Top