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

UnitMovement

Status
Not open for further replies.

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
This library is intended to deal with common types of triggered unit motions so you don't have to write separate periodic functions per spell/system. This library will handle them internally.

I want to get feedback from you guys what things could still be included in this library. Currently, the library supports:
- Linear motion
- Constantly changing speed (but not direction)
- Circular motion (constantly changing direction)
- Spiral-in motion (like due to a spinning black hole) as well as Spiral-out motion

Perhaps there are many things you have in mind that I haven't considered. If so, please tell them. Feedback is appreciated.

Code
JASS:
library UnitMovement /*


    */uses /*

    */ErrorMessage      /*   https://github.com/nestharus/JASS/blob/master/jass/Systems/ErrorMessage/main.j
    */Vector            /*   http://www.wc3c.net/showthread.php?t=101322
    */TimerUtils        /*   http://www.wc3c.net/showthread.php?t=87027
    */Table             /*   https://www.hiveworkshop.com/threads/snippet-new-table.188084/
    */UnitDex           /*   https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
    */optional AutoFly  /*   https://www.hiveworkshop.com/threads/snippet-autofly.249422/


    *///! novjass

    /*
    Description:

        This system is used to handle if possible, all unit movements, internally. You can use this
        to easily create knockback movements, throws, and jumps whether it be uniform or accelerated
        motion. By default, this system also takes into consideration the units' acceleration due to
        gravity as well as the side effect of gravity, i.e., ground friction. But if you don't need
        these additional features, you can just set their values to 0 in the configuration section
        below.
    */

    |=========|
    | CREDITS |
    |=========|

        Author:
            - AGD

        Dependencies:
            - Anitarf (Vector)
            - Vexorian (TimerUtils)
            - Bribe (Table)
            - TriggerHappy (UnitDex, AutoFly)

    |-----|
    | API |
    |-----|

        struct UnitMovement/*

          */real x                                       /*   The x-component of the UnitMovement
          */real y                                       /*   The y-component of the UnitMovement
          */real z                                       /*   The z-component of the UnitMovement
          */real coefficientOfFriction                   /*   The coefficient of friction of the unit
          */readonly real friction                       /*   The magnitude of the ground friction vector acting on the unit

          */readonly static UnitMovement triggerInstance   /*   Use this to refer to the UnitMovement instance of
                                                              the moving unit inside a handler function

          */static method operator [] takes unit whichUnit returns UnitMovement/*
                - Reftrieves the UnitMovement instance corresponding to the input unit

          */method destroy takes nothing returns nothing/*
                - Destroys a UnitMovement instance

          */method addVelocity takes vector whichVector returns UnitMovement/*
                - Adds a velocity to the UnitMovement instance

          */method addAcceleration takes vector whichVector returns UnitMovement/*
                - Adds a permanent acceleration to the UnitMovement instance
                - You can use this for example to negate the gravity for a specific unit by applying
                  a vector of the same magnitude with the gravity but in the opposite direction like
                  for example: call UnitMovement[unit].addAcceleration(0.00, 0.00, UnitVector.GRAVITY)

          */method addAccelerationTimed takes vector whichVector, real duration returns UnitMovement/*
                - Adds a timed acceleration to the UnitMovement instance

          */method addTorque takes vector axisOfRotation, real magnitude returns UnitMovement/*
          */method addTorqueEx takes vector axisOfRotation, real radius returns UnitMovement/*
                - Adds a rotational acceleration to the UnitMovement instance
                - Positive magnitude causes a clockwise rotation when viewing through the axis head-on
                  while a negative magnitude does the inverse

          */method addTorqueIncrement takes vector axisOfRotation, real magnitude returns UnitMovement/*
                - Adds an increment in the torque's magnitude per period

          */method isFreeFalling takes nothing returns boolean/*
                - Checks if the unit owner of the UnitMovement instance is in free-fall

          */method registerHandler takes code c returns triggercondition/*
          */method unregisterHandler takes triggercondition tc returns nothing/*
                - Registers/Unregisters a code that runs when the unit moves due to a vector applied
                  to the unit using this system

    *///! endnovjass

    private keyword Init

    struct UnitMovement extends array
        /****************************************/
        /*        SYSTEM CONFIGURATION          */
        /****************************************/
        /*
        The number of frames per second in which
        the periodic function operates.
        Default Value: 32                       */
        static constant real FPS = 32.00
        /*
        The 'acceleration' due to gravity.
        Default Value: 981.00                   */
        static constant real GRAVITY = 981.00
        /*
        The coefficient of the ground friction.
        Default Value: 0.70                     */
        static constant real GROUND_FRICTION = 0.70
        /*
        The default value for the coefficient of
        friction for units. This value is applied
        upon UnitMovement creation.
        Default Value: 0.60                     */
        private static constant real DEFAULT_UNIT_FRICTION = 0.60
        /*
        Determines if newly created units are
        automatically registered into the
        system.                                 */
        private static constant boolean AUTO_REGISTER_UNITS = true
        /****************************************/
        /*        END OF CONFIGURATION          */
        /****************************************/
        /*======================================*/
        /*   Do not change anything below this  */
        /*  line if you're not so sure on what  */
        /*            you're doing.             */
        /*======================================*/

        private real deltaFriction
        private boolean flag
        private thistype prev
        private thistype next
        private real unitFriction
        private vector velocity
        private vector acceleration
        private vector torqueAxis
        private vector torqueIncrement
        private vector timerForce
        private integer codeCount
        private trigger handler

        readonly static thistype triggerInstance = 0
        static constant real FRAME_RATE = 1.00/FPS
        private static code periodicCode
        private static triggercondition condition
        private static timer timer = CreateTimer()
        private static group enumerator = CreateGroup()

        private static Table table
        private static TableArray tableArray

        private static method max takes real a, real b returns real
            if a > b then
                return a
            endif
            return b
        endmethod

        static method operator [] takes unit u returns thistype
            local thistype this = GetUnitId(u)
            if not .flag then
                set .flag = true
                set .velocity = vector.create(0.00, 0.00, 0.00)
                set .acceleration = vector.create(0.00, 0.00, -GRAVITY*FRAME_RATE)
                set .torqueAxis = vector.create(0.00, 0.00, 0.00)
                set .torqueIncrement = vector.create(0.00, 0.00, 0.00)
                set .unitFriction = DEFAULT_UNIT_FRICTION
                set .deltaFriction = GRAVITY*GROUND_FRICTION*DEFAULT_UNIT_FRICTION*FRAME_RATE
                static if not LIBRARY_AutoFly then
                    if UnitAddAbility(u, 'Arav') and UnitRemoveAbility(u, 'Arav') then
                    endif
                endif
            endif
            return this
        endmethod

        method destroy takes nothing returns nothing
            if .flag then
                call .velocity.destroy()
                call .acceleration.destroy()
                call .torqueAxis.destroy()
                call .torqueIncrement.destroy()
                call DestroyTrigger(.handler)
                set .handler = null
                set .velocity = 0
                set .acceleration = 0
                set .torqueAxis = 0
                set .torqueIncrement = 0
                set .deltaFriction = 0.00
                set .unitFriction = 0.00
                set .codeCount = 0
                set .flag = false
            endif
        endmethod

        method operator unit takes nothing returns unit
            return GetUnitById(this)
        endmethod

        method operator x= takes real x returns nothing
            set .velocity.x = x
        endmethod
        method operator x takes nothing returns real
            return .velocity.x
        endmethod

        method operator y= takes real y returns nothing
            set .velocity.y = y
        endmethod
        method operator y takes nothing returns real
            return .velocity.y
        endmethod

        method operator z= takes real z returns nothing
            set .velocity.z = z
        endmethod
        method operator z takes nothing returns real
            return .velocity.z
        endmethod

        method operator coefficientOfFriction= takes real frictionFactor returns nothing
            set .unitFriction = frictionFactor
            set .deltaFriction = max(-.acceleration.z*GROUND_FRICTION*frictionFactor, 0.00)
        endmethod
        method operator coefficientOfFriction takes nothing returns real
            return .unitFriction
        endmethod

        method operator friction takes nothing returns real
            return .deltaFriction*FPS
        endmethod

        private method insertNode takes nothing returns nothing
            local thistype last = thistype(0).prev
            set .next = 0
            set .prev = last
            set last.next = this
            set thistype(0).prev = this
            if .prev == 0 then
                call TimerStart(timer, FRAME_RATE, true, periodicCode)
            endif
        endmethod

        private method removeNode takes nothing returns nothing
            local thistype prev = .prev
            local thistype next = .next
            set next.prev = prev
            set prev.next = next
            if thistype(0).next == 0 then
                call PauseTimer(timer)
            endif
        endmethod

        method isFreeFalling takes nothing returns boolean
            local vector acceleration = .acceleration
            return acceleration.x == 0.00 and acceleration.y == 0.00 and acceleration.z == -GRAVITY*FRAME_RATE
        endmethod

        method addVelocity takes vector whichVector returns thistype
            local boolean freeFalling = .isFreeFalling()
            local vector velocity = .velocity
            local boolean prevState = velocity.getLength() == 0.00 and freeFalling
            call velocity.add(whichVector)
            if prevState != (velocity.getLength() == 0.00) then
                if velocity.getLength() > 0.00 then
                    call .insertNode()
                elseif freeFalling then
                    call .removeNode()
                endif
            endif
            return this
        endmethod

        method addAcceleration takes vector whichVector returns thistype
            local vector acceleration = .acceleration
            local boolean prevState = .velocity.getLength() == 0.00 and .isFreeFalling()
            local boolean freeFalling
            call whichVector.scale(FRAME_RATE)
            call acceleration.add(whichVector)
            call whichVector.scale(FPS)
            set .deltaFriction = max(-acceleration.z*GROUND_FRICTION*.unitFriction, 0.00)
            set freeFalling = .isFreeFalling()
            if prevState != freeFalling then
                if not freeFalling then
                    call .insertNode()
                elseif .velocity.getLength() == 0.00 then
                    call .removeNode()
                endif
            endif
            return this
        endmethod

        private static method onExpire takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local vector timerVector = table[GetHandleId(expired)]
            call thistype(GetTimerData(expired)).acceleration.subtract(timerVector)
            call timerVector.destroy()
            call ReleaseTimer(expired)
            set expired = null
        endmethod

        method addAccelerationTimed takes vector whichVector, real duration returns thistype
            local timer newTimer = NewTimerEx(this)
            local vector timerVector = vector.create(0.00, 0.00, 0.00)
            call timerVector.add(whichVector)
            set table[GetHandleId(newTimer)] = timerVector
            call .addAcceleration(timerVector)
            call TimerStart(newTimer, duration, false, function thistype.onExpire)
            set newTimer = null
            return this
        endmethod

        method addTorque takes vector axis, real magnitude returns thistype
            local real prevLength = axis.getLength()
            call axis.setLength(magnitude*FRAME_RATE)
            call .torqueAxis.add(axis)
            call axis.setLength(prevLength)
            return this
        endmethod

        method addTorqueEx takes vector axis, real radius returns thistype
            local real speed = .velocity.getLength()
            return .addTorque(axis, speed*speed/radius)
        endmethod

        method addTorqueIncrement takes vector axis, real magnitude returns thistype
            local real prevLength = axis.getLength()
            call axis.setLength(magnitude*FRAME_RATE)
            call .torqueIncrement.add(axis)
            call axis.setLength(prevLength)
            return this
        endmethod

        method registerHandler takes code c returns triggercondition
            debug call ThrowError(c == null, "UnitVector", "registerHandler", "thistype", this, "Attemped to register a null code")
            if .codeCount == 0 then
                set .handler = CreateTrigger()
            endif
            set .codeCount = .codeCount + 1
            set condition = TriggerAddCondition(.handler, Filter(c))
            set tableArray[this][GetHandleId(condition)] = 1
            return condition
        endmethod

        method unregisterHandler takes triggercondition tc returns nothing
            debug call ThrowError(tc == null, "UnitVector", "unregisterHandler", "thistype", this, "Attemped to unregister a null triggercondition")
            debug call ThrowError(not tableArray[this].has(GetHandleId(tc)), "UnitVector", "unregisterHandler", "thistype", this, "Attemped to unregister a triggercondition twice")
            set .codeCount = .codeCount - 1
            if .codeCount == 0 then
                call tableArray[this].flush()
                call DestroyTrigger(.handler)
                set .handler = null
            else
                call tableArray[this].remove(GetHandleId(tc))
                call TriggerRemoveCondition(.handler, tc)
            endif
        endmethod

        private static method periodic takes nothing returns nothing
            local thistype this = thistype(0).next
            local thistype prev
            local thistype next
            local unit u
            local real xVel
            local real yVel
            local real zVel
            local real unitZ
            local real friction
            local vector velocity
            local vector acceleration
            local vector torqueAxis
            local vector torqueIncrement
            loop
                exitwhen this == 0
                set u = GetUnitById(this)
                set velocity = .velocity
                set acceleration = .acceleration
                set unitZ = GetUnitFlyHeight(u)
                if acceleration.x != 0.00 or acceleration.y != 0.00 or acceleration.z != 0.00 then
                    call velocity.add(acceleration)
                    set .deltaFriction = max(-acceleration.z*GROUND_FRICTION*.unitFriction, 0.00)
                endif
                set friction = .deltaFriction
                if unitZ <= GetUnitDefaultFlyHeight(u) + 0.10 then
                    call SetUnitPropWindow(u, GetUnitDefaultPropWindow(u))
                    set zVel = velocity.z
                    if zVel < 0.00 then
                        set velocity.z = 0.00
                    endif
                    if friction > 0.00 then
                        set xVel = velocity.x
                        set yVel = velocity.y
                        if friction*friction > xVel*xVel + yVel*yVel then
                            set velocity.x = 0.00
                            set velocity.y = 0.00
                            if acceleration.x == 0.00 and acceleration.y == 0.00 and acceleration.z <= 0.00 and zVel <= 0.00 then
                                set prev = .prev
                                set next = .next
                                set next.prev = prev
                                set prev.next = next
                            endif
                        else
                            call velocity.setLength(max(velocity.getLength() - friction, 0.00))
                        endif
                    endif
                else
                    call SetUnitPropWindow(u, 0.00)
                endif
                if velocity.x != 0.00 or velocity.y != 0.00 or velocity.z != 0.00 then
                    set torqueAxis = .torqueAxis
                    set torqueIncrement = .torqueIncrement
                    if torqueIncrement.x != 0.00 or torqueIncrement.y != 0.00 or torqueIncrement.z != 0 then
                        call torqueAxis.add(torqueIncrement)
                    endif
                    if torqueAxis.x != 0.00 or torqueAxis.y != 0.00 or torqueAxis.z != 0 then
                        call velocity.rotate(torqueAxis, torqueAxis.getLength()/velocity.getLength())
                    endif
                endif
                call SetUnitX(u, GetUnitX(u) + velocity.x*FRAME_RATE)
                call SetUnitY(u, GetUnitY(u) + velocity.y*FRAME_RATE)
                call SetUnitFlyHeight(u, unitZ + velocity.z*FRAME_RATE, 0.00)
                set triggerInstance = this
                call TriggerEvaluate(handler)
                set triggerInstance = 0
                set this = .next
            endloop
            set u = null
        endmethod

        static if thistype.AUTO_REGISTER_UNITS then
            private static method onIndex takes nothing returns nothing
                local thistype this = thistype[GetIndexedUnit()]
            endmethod
            private static method onDeindex takes nothing returns nothing
                call thistype[GetIndexedUnit()].destroy()
            endmethod
        endif

        private static method init takes nothing returns nothing
            static if thistype.AUTO_REGISTER_UNITS then
                local code onIndex = function thistype.onIndex
                local code onDeindex = function thistype.onDeindex
                call OnUnitIndex(onIndex)
                call OnUnitDeindex(onDeindex)
            endif
            set periodicCode = function thistype.periodic
            set tableArray = TableArray[0x2000]
            set table = tableArray[0]
        endmethod
        implement Init

    endstruct

    private module Init
        private static method onInit takes nothing returns nothing
            call init()
        endmethod
    endmodule


endlibrary
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
A unit vector is a vector of length 1. This name is very misleading. It's more related to missiles than anything.
Well, I can't think of a better descriptive name. What name do you suggest? preferably with no "missile" in the name =)

Maybe yes, because I'm not sure it will ever suffice for any needs, because movements are often just pretty map specific. But still some default forms could be useful, similar principle like Flux used here [vJASS] - [System] Polygon, one basic polygon lib, and then some specifications in the addonutils lib.
Maybe I'll split it if theres more movement types that'll come to my mind. But now, what I think is that the commonly used movement types can just be derived off these three basic movement types:
1. Constant velocity
2. Constant speed change
3. Constant direction change

Any knockback3Ds for example can just be achieved using type 1, just add an initial velocity then just let the gravity and ground friction do the parabolic movement and the decceleration when it hits the ground, Similar with other jumps. And circular and spiral motions can both be achieved by using type 3 and 2 (I guess). Any other weird movement types for example, like a wave shape motion, can be achieved as a series of circular motions using type 3. So maybe I guess I'll just do these three basic types.

Currently, this lib already supports types 1 and 2 but it still lacks type 3.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
*/ coefficientOfFriction /* The coefficient of friction of the unit
you could also use the variable name 'u' instead which refers to μ to avoid the long name.

JASS:
*/method registerHandler takes code c returns triggercondition/*
*/method unregisterHandler takes triggercondition tc returns nothing/*
It's also possible to make unregisterHandler take a code argument and handle it internally so users don't have to deal with storing triggercondition to their own variable if they want to free them via unregisterHandler later. To achieve this, Filter(code) always return the same handle (with same handle id) for the same code argument
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
It's also possible to make unregisterHandler take a code argument and handle it internally so users don't have to deal with storing triggercondition to their own variable if they want to free them via unregisterHandler later. To achieve this, Filter(code) always return the same handle (with same handle id) for the same code argument
Right, I guess that would be easier for users but I'll edit both to take boolexprs instead because they're easier to use in other cases because they support arrays unlike code.
 
Status
Not open for further replies.
Top