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

RigidBody Physics v1.3

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
Description:
The system applies some physics to the native Warcraft 3 movements making it behaves very realistically. Useful not only for sliding maps, it can also be used for survival or RPG maps to keep everything as realistic as possible. Can be adjusted to create racing maps too (with or without drifting).

Features:
(Demo Vids)
  1. Realistic sliding mechanism
  2. Take off from ramps
  3. Sloppy ground speed bonus & reduction
  4. Automatic unwalkable ground tiltness detection
  5. On landing collision & speed reduction/increment
  6. Native control & combat mechanism compatibility

Code:
JASS:
library RigidBody requires UnitZ, TimerUtils, UnitIndexer, Table
   
    /*
        ~ RigidBody v1.3 ~
       
        Applies highly realistic physics to the native Warcraft 3 movements.
       
        I. Requirements:
            • TimerUtils  : www.wc3c.net/showthread.php?t=101322
            • UnitIndexer : www.hiveworkshop.com/forums/spells-569/unit-indexer-v5-3-0-1-a-260859/
            • UnitZ       : www.hiveworkshop.com/forums/jass-resources-412/snippet-getterrainz-unitz-236942/
            • Table       : www.hiveworkshop.com/threads/snippet-new-table.188084/
           
        II. How to install:
            • Install and configure the unit indexer properly
            • Copy the RigidBody trigger group to your map
            • Adjust the system configuration below if neccesary
            • Define frictions for each tile in your map
            • Input unit data to the data reference for better accuracy
           
        III. User API
           
            1. Applies some physics to unit body
            struct RigidBody
                real acceleration
                    - Unit's acceleration rate
                real deacceleration
                    - Unit's deacceleration rate
                real turnRate
                    - Unit's turn rate
                real angle
                    - Unit's current motion direction
                real speed
                    - Unit's current motion velocity
                real zVel
                    - Unit's current gravitational velocity
                readonly boolean isEnabled
                    - True if unit's movement is currrently enabled
                readonly boolean isAirborne
                    - True if the unit is currently mid-air
                readonly unit object
                    - The unit agent
               
                static method RegisterUnit takes unit whichUnit returns thistype
                    - Applies movement physics to a unit
                static method IsUnitRegistered takes unit u returns boolean
                    - Check if the unit is registered to the system or not
                method accelerate takes real power, real maxspeed, real direction, real turnRate returns nothing
                    - Accelerate with certain power toward given direction
                method move takes real x, real y, real z returns nothing
                    - Move the unit to certain coordinate
                method jump takes real power returns nothing
                    - Jump with certain velocity
                method enable takes boolean b returns nothing
                    - If disabled, physics will be disabled for the unit
                method destroy takes nothing returns nothing
                    - Unregister the unit
                static method operator [] takes unit u returns thistype
                    - Get unit's index
           
            2. Unit's object data reference
            public struct UnitDataRef
                static method IsRegistered takes unit whichUnit returns boolean
                    - True if the unit data has been defined
               
                static method GetAttackRange takes unit whichUnit returns real
                static method GetBackswingPoint takes unit whichUnit returns real
                    - Get unit data
               
                static method Add takes integer unitId, real attackRange, real backswingPoint returns nothing
                    - Define unit data
           
            3. Terrain type specifications
            public struct TerrainUtils
                static method GetTerrainSteepness takes real x1, real y1, real x2, real y2 returns real
                    - Get tiltness between two points
               
                static method GetTerrainFriction takes real x, real y returns real
                    - Get terrain friction at given coordinate
               
                static method SetTerrainTypeFriction takes integer terrainId, real friction returns nothing
                    - Define tile friction
       
        IV. Credits:
            • Nestharus
            • Garfield1337
            • Vexorian
            • Bribe
    */                      ////////////////////
                            // Configurations //
                            ////////////////////
                           
        // Warning! Every tiny bit of modification to these settings may
        // causes significant impact to the motion behavior!
        // So be careful when you are modifying it if you have "deeply"
        // implement the system to your map.
       
    globals
        // 1. Physic calculations interval
        private constant real INTERVAL = 0.0312500
       
        // 2. Unit position update rate
        //  - Higher values will significantly improve performance
        //  - But it causes the movement looks rough
        private constant real RELOCATION_RATE = 0.01
       
        // 3. Gravitational force of your world (map)
        private constant real GRAVITY = 9.8*INTERVAL
       
        // 4. Max friction of the ground to be identified as slippery
        private constant real SLIPPERY_TILE_MAX_FRICTION = 1.0
       
        // 5. Offset accuracy of tiltness detection
        //  - Different value may result in different motion behavior
        private constant real SLOPE_DETECTION_RADIUS = 0.1
       
        // 6. Number of tiltness detection iterations
        //  - Higher value means higher precision (more realistic theoritically)
        //  - Different value may result in different motion behavior
        private constant integer SLOPE_DETECTION_ACCURACY = 5
       
        // 7. Ground tiltness speed factors
        //  - Downhill movement speed bonus
        private constant real SLOPE_SPEED_INCREMENT_STRENGTH = 0.05
        //  - Maximum speed bonus on downhill movement
        private constant real MAXIMUM_SLOPE_SPEED_BONUS_RATE = 1.5
        //  - Uphill movement speed penalty
        private constant real SLOPE_SPEED_REDUCTION_STRENGTH = 0.05
       
        // 8. Ground tiltness strength
        //  - Tiltness affects movement exponentially
        //  - Higher value causes slopier ground to have stronger impact to the motion
        private constant real SLOPE_STRENGTH_FACTOR = 1.25
       
        // 9. Maximum walktable ground tiltness (in radian)
        //  - Units can't move on ground tiltness beyond this value
        private constant real MAX_WALKABLE_SLOPE = 85.0*bj_DEGTORAD
       
        // 10. Maximum speed of unit slipping
        //  - Units will be passively slipped when standing on sloppy ground
        //  - The effect can be overcame by ground friction so it will be seemingly idle
        private constant real SLIP_MAXIMUM_SPEED = 522.0*INTERVAL
       
        // 11. Turn rate of unit slipping
        //  - Higher value causes motion to direct downward more quickly when slipping
        private constant real SLIP_TURN_RATE = 15.0*bj_DEGTORAD
       
        // 12. Absolute motion speed limit
        //  - Nothing can move faster than following specified value
        private constant real ABSOLUTE_SPEED_LIMIT = 1566.0*INTERVAL
       
        // 13. If true, units will try to deaccelerate themselves when idle
        //  - Acts like additional friction
        //  - Ideally turned on for battle-based maps
        private constant boolean IDLE_DEACCELERATION = false
       
        // 14. If true, every classified units will be registered to the system on creation
        private constant boolean AUTO_REGISTER = true
       
        // 15. Units can't take off if the ground has lower tiltness than following specified value
        //  - A unit can take off (for example from ramps) if it has enough velocity to escape the gravitation
        private constant real MINIMUM_TAKE_OFF_TILTNESS = 45.0*bj_DEGTORAD
       
        // 16. General unit attack range (used for units without pre-registered data)
        //  - For range attackers
        private constant real RANGE_UNIT_ATTACK_RANGE = 600.0
        //  - For melee attackers
        private constant real MELEE_UNIT_ATTACK_RANGE = 128.0
       
        // 17. General unit backswing point (used for units without pre-defined data)
        private constant real GENERAL_UNIT_BACKSWING_POINT = 0.800
       
        // 18. General unit turn rate
        //  - Turn rate on slippery grounds
        private constant real GENERAL_UNIT_TURN_RATE = 5.0*bj_DEGTORAD
        //  - Turn rate on non-slippery grounds
        private constant real GENERAL_UNIT_TURN_RATE_2 = 90.0*bj_DEGTORAD
       
        // 19. General unit acceleration
        private constant real GENERAL_UNIT_ACCELERATION = 25.0*INTERVAL
       
        // 20. General unit deacceleration
        //  - Only works if IDLE_DEACCELERATION is set to "true"
        private constant real GENERAL_UNIT_DEACCELERATION = 5.0*INTERVAL
       
        // 21. General unit mass
        //  - Acts like additional gravitational force
        private constant real GENERAL_UNIT_MASS = 5.0*INTERVAL
       
        // 22. Detection trigger will be recycled if have more wasted events than the following value
        private constant integer TRIGGER_RECYCLE_THRESHOLD = 15
       
        // 23. Target adjustment range
        private constant real TARGET_ADJUSTMENT_RANGE = 64.0
       
        // 24. Target adjustment movespeed factor
        //  - Factor applied to movespeed when adjusting target
        private constant real TARGET_ADJUSTMENT_SPEED = 0.5
       
        // 25. Speed reduction on collision
        private constant real COLLISION_BOUNCE_FACTOR = 0.5
       
    endglobals
   
    native UnitAlive takes unit id returns boolean
   
    private function FilterUnit takes unit u returns boolean
        return not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitType(u, UNIT_TYPE_FLYING) and GetUnitAbilityLevel(u, 'Aloc') == 0
    endfunction

    globals
        private real WorldMaxX = 0
        private real WorldMaxY = 0
        private real WorldMinX = 0
        private real WorldMinY = 0
        private integer EventWasted = 0
        private constant real TAU = bj_PI*2
        private constant real HP  = bj_PI/2
    endglobals
   
    private module Init
        private static method onInit takes nothing returns nothing
            call Initialize()
        endmethod
    endmodule
   
    public struct UnitDataRef
       
        private static Table AttackRangeTable
        private static Table BackswingPointTable
       
        static method IsRegistered takes unit whichUnit returns boolean
            return BackswingPointTable.boolean[GetUnitTypeId(whichUnit)]
        endmethod
       
        static method GetAttackRange takes unit whichUnit returns real
            return AttackRangeTable.real[GetUnitTypeId(whichUnit)]
        endmethod
       
        static method GetBackswingPoint takes unit whichUnit returns real
            return BackswingPointTable.real[GetUnitTypeId(whichUnit)]
        endmethod
       
        static method Add takes integer unitId, real attackRange, real backswingPoint returns nothing
            set AttackRangeTable.real[unitId]       = attackRange
            set BackswingPointTable.real[unitId]    = backswingPoint
            set BackswingPointTable.boolean[unitId] = true
        endmethod
       
        private static method Initialize takes nothing returns nothing
            set AttackRangeTable    = Table.create()
            set BackswingPointTable = Table.create()
        endmethod
       
        implement Init
       
    endstruct
   
    public struct TerrainUtils extends array
   
        private static Table TerrainUtils
   
        static method GetTerrainSteepness takes real x1, real y1, real x2, real y2 returns real
            return Sin(GetTerrainZ(x1, y1)-GetTerrainZ(x2, y2))/SLOPE_DETECTION_RADIUS
        endmethod
       
        static method GetTerrainFriction takes real x, real y returns real
            return TerrainUtils.real[GetTerrainType(x, y)]
        endmethod
       
        static method SetTerrainTypeFriction takes integer id, real friction returns nothing
            set TerrainUtils.real[id] = friction
        endmethod
       
        private static method Initialize takes nothing returns nothing
            set TerrainUtils = Table.create()
        endmethod
       
        implement Init
       
    endstruct
   
    struct RigidBody extends array

        real acceleration
        real deacceleration
        real turnRate
        real angle // Unit's motion direction
        real speed // Unit's motion velocity
        real zVel  // Unit's gravitational velocity
       
        private  boolean isMoving
        private  boolean isPatroling
        readonly boolean isEnabled
        readonly boolean isAirborne
       
        readonly unit  object
        private  unit  target
        private  real  locX
        private  real  locY
        private  real  locZ
        private  timer tick
        private  timer locator
        private  timer disabler
       
        private real orderX
        private real orderY
        private widget orderTarget
        private integer orderId
       
        private static real LowestA = 0
        private static real LowestX = 0
        private static real LowestY = 0
        private static real LowestZ = 0
        private static group Objects = CreateGroup()
        private static trigger DetectTrigger = CreateTrigger()
        private static conditionfunc DetectFunc
       
        static method operator [] takes unit u returns thistype
            return GetUnitUserData(u)
        endmethod
       
        static method IsUnitRegistered takes unit u returns boolean
            return thistype(GetUnitUserData(u)).object != null
        endmethod
   
        private static method AngularDifferenceAbs takes real a, real b returns real
            return RAbsBJ(Atan2(Sin(a-b), Cos(a-b)))
        endmethod
       
        private static method Exponent takes real b returns real
            return Pow(SLOPE_STRENGTH_FACTOR, b)
        endmethod
       
        method destroy takes nothing returns nothing
            call ReleaseTimer(.tick)
            call ReleaseTimer(.locator)
            call ReleaseTimer(.disabler)
            set .object = null
            set .target = null
            set .tick = null
            set .locator = null
            set .disabler = null
        endmethod
       
        // Detecting lowest ground with constant time complexity of O(n) = N
        // depending on specified SLOPE_DETECTION_ACCURACY
        private method getLowestPoint takes nothing returns nothing
           
            local real x
            local real y
            local real z
            local real x2
            local real y2
            local real z2
            local real inc   = bj_PI
            local real angle = GetRandomReal(0, bj_PI-.1) // Significantly reduces no-slope detection error possibility
            local integer i
           
            set LowestZ = 99999999
            loop
                exitwhen angle >= TAU
                set x  = .locX+SLOPE_DETECTION_RADIUS*Cos(.angle+angle)
                set y  = .locY+SLOPE_DETECTION_RADIUS*Sin(.angle+angle)
                set z  = GetTerrainZ(x, y)
                if z < LowestZ then
                    set LowestX = x
                    set LowestY = y
                    set LowestZ = z
                    set LowestA =.angle+ angle
                elseif z == LowestZ then
                    set LowestX = .locX
                    set LowestY = .locY
                    set LowestA = .angle
                    return
                endif
                set angle = angle+inc
            endloop
            set i = 0
            loop
                set i   = i + 1
                set inc = inc/2
                set x   = .locX+SLOPE_DETECTION_RADIUS*Cos(LowestA+inc)
                set y   = .locY+SLOPE_DETECTION_RADIUS*Sin(LowestA+inc)
                set z   = GetTerrainZ(x, y)
                set x2  = .locX+SLOPE_DETECTION_RADIUS*Cos(LowestA-inc)
                set y2  = .locY+SLOPE_DETECTION_RADIUS*Sin(LowestA-inc)
                set z2  = GetTerrainZ(x2, y2)
                if z < LowestZ or z2 < LowestZ then
                    if z < z2 then
                        set LowestX = x
                        set LowestY = y
                        set LowestZ = z
                        set LowestA = LowestA+inc
                    else
                        set LowestX = x2
                        set LowestY = y2
                        set LowestZ = z2
                        set LowestA = LowestA-inc
                    endif
                endif
                exitwhen i == SLOPE_DETECTION_ACCURACY
            endloop
           
        endmethod
       
        private method turn takes real targetAngle, real turnRate returns nothing
            if Cos(.angle-targetAngle) <= Cos(turnRate) then
                if Sin(targetAngle-.angle) >= 0 then
                    set .angle = .angle+turnRate
                else
                    set .angle = .angle-turnRate
                endif
            else
                set .angle = targetAngle
            endif
        endmethod
       
        // Apply gravitational forces
        private method slip takes real power, real direction, real turnRate returns nothing
           
            local real diff   = AngularDifferenceAbs(direction, .angle)
            local real spdPow = 1-diff*2/bj_PI
            local boolean b
           
            call turn(direction, turnRate*(1-AngularDifferenceAbs(diff, HP)/HP))
            set b = .speed < SLIP_MAXIMUM_SPEED
            if spdPow < 0 or b then
                set .speed = .speed+power*spdPow
                if .speed > SLIP_MAXIMUM_SPEED and b then
                    set .speed = SLIP_MAXIMUM_SPEED
                elseif .speed < 0 then
                    set .angle = direction
                    set .speed = 0.0
                endif
            endif
           
        endmethod
       
        method accelerate takes real power, real maxspeed, real direction, real turnRate returns nothing
           
            local real diff
            local real spdPow
           
            if .speed == 0 then
                set .angle = direction
                if .speed < power then
                    set .speed = power
                endif
                set diff = 0.0
            else
                set diff = AngularDifferenceAbs(direction, .angle)
                call turn(direction, turnRate*(1-AngularDifferenceAbs(diff, HP)/HP))
            endif
            set spdPow = 1-diff*2/bj_PI
            if spdPow < 0 or .speed < maxspeed then
                set .speed = .speed+power*spdPow
                if .speed > maxspeed and spdPow >= 0 then
                    set .speed = maxspeed
                elseif .speed < 0 then
                    set .angle = direction
                    set .speed = -.speed
                endif
            elseif .speed > maxspeed then
                set .speed = .speed-.deacceleration
            endif
           
        endmethod
       
        method enable takes boolean b returns nothing
            set .isEnabled = b
            if b then
                set .locX = GetUnitX(.object)
                set .locY = GetUnitY(.object)
                set .locZ = GetUnitZ(.object)
                set .isAirborne = true
            endif
        endmethod
       
        method jump takes real power returns nothing
            set .zVel = power
            set .isAirborne = true
        endmethod
       
        method move takes real x, real y, real z returns nothing
            set .locX = x
            set .locY = y
            set .locZ = GetTerrainZ(x, y)+z
            call SetUnitX(.object, .locX)
            call SetUnitY(.object, .locY)
            call SetUnitZ(.object, .locZ)
            if z > 0 then
                set .isAirborne = true
            else
                set .isAirborne = false
                call SetUnitFlyHeight(.object, 0, 0)
            endif
        endmethod
       
        private static method Relocate takes nothing returns nothing
       
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
           
            if .isEnabled and (.speed > 0 or .isMoving) then
                // Detect teleportation
                if not IsUnitInRangeXY(.object, .locX, .locY, .speed) then
                    set .locX = GetUnitX(.object)
                    set .locY = GetUnitY(.object)
                else
                    call SetUnitX(.object, .locX)
                    call SetUnitY(.object, .locY)
                endif
                if .isAirborne then
                    call SetUnitZ(.object, .locZ)
                endif
            endif
           
        endmethod
       
        private static method OnPeriodic takes nothing returns nothing
           
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            local real slope
            local real slopeC
            local real slopeS
            local real slope2
            local real reduction
            local real friction
            local real diff
            local real a
            local real r
            local real x
            local real y
            local real z
            local real x2
            local real y2
            local real ms
            local integer order
            local boolean moving
            local boolean slippy
           
            if .isEnabled and UnitAlive(.object) then
                set x  = .locX
                set y  = .locY
                set a  = GetUnitFacing(object)*bj_DEGTORAD
                set ms = GetUnitMoveSpeed(object)*INTERVAL
                if not .isAirborne then
                    call getLowestPoint()
                    if LowestX != .locX or LowestY != .locY then
                        set slope = TerrainUtils.GetTerrainSteepness(x, y, LowestX, LowestY)
                        if slope > HP then
                            set slope = HP
                        endif
                        set slopeC = Cos(slope)
                        set slopeS = Sin(slope)
                    else
                        set slope  = 0
                        set slopeS = 0
                        set slopeC = 1
                    endif
                    set friction = TerrainUtils.GetTerrainFriction(x, y)
                    set slippy = friction <= SLIPPERY_TILE_MAX_FRICTION
                    set order  = GetUnitCurrentOrder(.object)
                    set moving = .isMoving and order != 0 or .isPatroling and not IsUnitPaused(.object)
                    if UnitAlive(.target) then
                        if TimerGetRemaining(.disabler) == 0 then
                            if UnitDataRef.IsRegistered(.object) then
                                set r = UnitDataRef.GetAttackRange(.object)
                            elseif IsUnitType(.object, UNIT_TYPE_RANGED_ATTACKER) then
                                set r = RANGE_UNIT_ATTACK_RANGE
                            else
                                set r = MELEE_UNIT_ATTACK_RANGE
                            endif
                            set x2 = x-GetUnitX(.target)
                            set y2 = y-GetUnitY(.target)
                            set moving = moving or x2*x2+y2*y2 > r*r
                        endif
                    elseif .target != null then
                        set .isMoving = order != 0
                        if not .isMoving then
                            set .target = null
                        endif
                        set moving = not IsUnitPaused(.object)
                    endif
                    if moving then
                        if .orderTarget == null then
                            if IsUnitInRangeXY(.object, .orderX, .orderY, TARGET_ADJUSTMENT_RANGE) then
                                set ms = ms * TARGET_ADJUSTMENT_SPEED
                            endif
                        endif
                        if slippy then
                            call accelerate(.acceleration, ms, a, .turnRate*slopeC)
                        elseif slope < MAX_WALKABLE_SLOPE then
                            call accelerate(.acceleration, ms, a, GENERAL_UNIT_TURN_RATE_2*slopeC)
                        endif
                    endif
                    set diff = AngularDifferenceAbs(LowestA, .angle)
                    if not slippy then
                        set r = .speed*slopeS*SLOPE_SPEED_INCREMENT_STRENGTH*(1-diff*2/bj_PI)
                        if r < 0 then
                            if slope > MAX_WALKABLE_SLOPE then
                                set .speed = 0
                            elseif .speed >= ms*.5 then
                                set .speed = .speed+r
                            endif
                        else
                            set .speed = .speed+r
                        endif
                        if .speed > ms*MAXIMUM_SLOPE_SPEED_BONUS_RATE then
                            set .speed = ms*MAXIMUM_SLOPE_SPEED_BONUS_RATE
                        endif
                    endif
                    if diff >= HP and slope > MAX_WALKABLE_SLOPE then
                        set .speed = 0
                    endif
                    if slope != 0 then
                        call slip((GRAVITY+GENERAL_UNIT_MASS)*Exponent(slopeS+1), LowestA, SLIP_TURN_RATE*slopeS)
                    endif
                    if moving then
                        if slippy then
                            set .speed = .speed-friction*slopeC
                        endif
                    else
                        if slope > MAX_WALKABLE_SLOPE then
                            set .speed = .speed-friction*slopeC
                        else
                            set .speed = .speed-friction
                        endif
                        static if IDLE_DEACCELERATION then
                            set .speed = .speed-.deacceleration
                        endif
                    endif
                    if .speed > ABSOLUTE_SPEED_LIMIT then
                        set .speed = ABSOLUTE_SPEED_LIMIT
                    elseif .speed < 0 then
                        set .speed = 0
                    endif
                    if moving then
                        if .orderTarget == null then
                            if AngularDifferenceAbs(Atan2(.orderY-.locY, .orderX-.locX), GetUnitFacing(.object)*bj_DEGTORAD) > HP / 2 then
                                call IssuePointOrderById(.object, .orderId, .orderX, .orderY)
                            endif
                        else
                            if AngularDifferenceAbs(Atan2(GetWidgetY(.orderTarget)-.locY, GetWidgetX(.orderTarget)-.locX), GetUnitFacing(.object)*bj_DEGTORAD) > HP / 2 then
                                call IssueTargetOrderById(.object, .orderId, .orderTarget)
                            endif
                        endif
                    endif
                endif
                if x < WorldMaxX and x > WorldMinX and y < WorldMaxY and y > WorldMinY then
                    set x2 = x+SLOPE_DETECTION_RADIUS*Cos(.angle)
                    set y2 = y+SLOPE_DETECTION_RADIUS*Sin(.angle)
                    set x  = x+.speed*Cos(.angle)
                    set y  = y+.speed*Sin(.angle)
                    set slope = -TerrainUtils.GetTerrainSteepness(.locX, .locY, x2, y2)
                    if .isAirborne then
                        set .locZ = .locZ+.zVel
                        set .zVel = .zVel-(GRAVITY+GENERAL_UNIT_MASS)
                        set z       = GetTerrainZ(x, y)
                        if .locZ <= z+.01 then
                            set .locZ = z
                            set .isAirborne = false
                            // Calculate speed change when colliding with the ground
                            set reduction = Atan((z-GetTerrainZ(.locX, .locY))/.speed)
                            set .speed = .speed*((bj_PI-Atan(RAbsBJ(.zVel)/.speed)-reduction)/HP-1)
                            if .speed > ABSOLUTE_SPEED_LIMIT then
                                set .speed = ABSOLUTE_SPEED_LIMIT
                            elseif .speed < 0 then
                                set .speed = 0
                            endif
                            call SetUnitFlyHeight(.object, 0, 0)
                        endif
                    else
                        set x2 = x-SLOPE_DETECTION_RADIUS*Cos(.angle)
                        set y2 = y-SLOPE_DETECTION_RADIUS*Sin(.angle)
                        set slope2 = -TerrainUtils.GetTerrainSteepness(x2, y2, x, y)
                        if slope2 > HP then
                            set slope2 = HP
                        endif
                        if slope > slope2 and slope-slope2 > MINIMUM_TAKE_OFF_TILTNESS then
                            // Check if the velocity is enough to escape the gravitational force
                            set .zVel = .speed*Sin(slope)
                            if .zVel > ((GetTerrainZ(x, y)-GetTerrainZ(x2, y2))+(GRAVITY+GENERAL_UNIT_MASS)) then
                                set .isAirborne = true
                                set .locZ = GetTerrainZ(.locX, .locY)+.zVel
                            endif
                        endif
                    endif
                    call SetUnitX(.object, WorldBounds.maxX)
                    call SetUnitY(.object, WorldBounds.maxY)
                    if IsTerrainWalkable(x, y) or .isAirborne then
                        set .locX = x
                        set .locY = y
                    else
                        set .angle = angle + bj_PI
                        set .speed = .speed * COLLISION_BOUNCE_FACTOR
                    endif
                    call SetUnitX(.object, .locX)
                    call SetUnitY(.object, .locY)
                endif
            endif
            set t = null
           
        endmethod
       
        static method RegisterUnit takes unit whichUnit returns thistype
           
            local thistype this = GetUnitUserData(whichUnit)
           
            if .object == null then
                set .object       = whichUnit
                set .angle        = GetUnitFacing(whichUnit)*bj_DEGTORAD
                set .isAirborne   = false
                set .isEnabled       = true
                set .isPatroling  = false
                set .locX           = GetUnitX(whichUnit)
                set .locY           = GetUnitY(whichUnit)
                set .locZ           = 0.0
                set .speed        = 0
                set .turnRate       = GENERAL_UNIT_TURN_RATE
                set .tick             = NewTimerEx(this)
                set .locator       = NewTimerEx(this)
                set .disabler       = NewTimerEx(this)
                set .acceleration = GENERAL_UNIT_ACCELERATION
                set .deacceleration = GENERAL_UNIT_DEACCELERATION
                static if not LIBRARY_AutoFly then
                    if UnitAddAbility(.object, 'Amrf') and UnitRemoveAbility(.object, 'Amrf') then
                    endif
                endif
                call GroupAddUnit(Objects, .object)
                call TriggerRegisterUnitEvent(DetectTrigger, .object, EVENT_UNIT_ACQUIRED_TARGET)
                call TimerStart(.locator, RELOCATION_RATE, true, function thistype.Relocate)
                call TimerStart(.tick, INTERVAL, true, function thistype.OnPeriodic)
            endif
           
            return this
        endmethod
       
        private static method OnImmediateOrder takes nothing returns boolean
           
            local unit u = GetTriggerUnit()
            local thistype this = GetUnitUserData(u)
            local integer order = GetIssuedOrderId()
           
            if .object != null and .isEnabled and GetUnitCurrentOrder(u) == order then
                set .isMoving = false
                set .isPatroling = false
                set .orderId = order
                set .orderTarget = null
            endif
            set u = null
           
            return false
        endmethod
       
        private static method OnOrder takes nothing returns boolean
           
            local unit u = GetTriggerUnit()
            local thistype this = GetUnitUserData(u)
            local integer order = GetIssuedOrderId()
           
            if .object != null and .isEnabled and GetUnitCurrentOrder(u) == order then
                set .isPatroling = false
                set .target = null
                set .orderId = order
                set .orderTarget = GetOrderTarget()
                if order == 851973 then // If stun order
                    set .isMoving = false
                else
                    if .orderTarget == null then
                        set .orderX = GetOrderPointX()
                        set .orderY = GetOrderPointY()
                        set .isMoving = true
                    else
                        set .isMoving = true
                    endif
                endif
            endif
            set u = null
 
            return false
        endmethod
       
        private static method OnAttack takes nothing returns boolean
           
            local unit u = GetAttacker()
            local thistype this = GetUnitUserData(u)
           
            if .object != null and .isEnabled then
                set .isMoving = false
                set .target = GetTriggerUnit()
                if UnitDataRef.IsRegistered(u) then
                    call TimerStart(.disabler, UnitDataRef.GetBackswingPoint(u), false, null)
                else
                    call TimerStart(.disabler, GENERAL_UNIT_BACKSWING_POINT, false, null)
                endif
            endif
            set u = null
           
            return false
        endmethod
       
        private static method OnDetect takes nothing returns boolean
       
            local thistype this = GetUnitUserData(GetTriggerUnit())
           
            if .object != null and .isEnabled then
                set .isMoving = true
                set .isPatroling = true
            endif
           
            return false
        endmethod
       
        private static method OnCast takes nothing returns boolean
 
            local thistype this = GetUnitUserData(GetTriggerUnit())
           
            if .object != null and .isEnabled and (GetSpellTargetX() != 0 or GetSpellTargetY() != 0) then
                set .isMoving = false
                set .target = null
            endif
 
            return false
        endmethod
       
        static if AUTO_REGISTER then
            private static method OnIndex takes nothing returns boolean
               
                local unit u = GetIndexedUnit()
               
                if FilterUnit(u) then
                    call RegisterUnit(u)
                endif
                set u = null
               
                return false
            endmethod
        endif
       
        private static method OnDeindex takes nothing returns boolean
           
            local thistype this = thistype[GetIndexedUnit()]
            local group tempG
            local unit u
           
            if .object != null then
                call GroupRemoveUnit(Objects, .object)
                call destroy()
                set EventWasted = EventWasted+1
                if EventWasted == TRIGGER_RECYCLE_THRESHOLD then
                    set EventWasted = 0
                    call DestroyTrigger(DetectTrigger)
                    set DetectTrigger = CreateTrigger()
                    call TriggerAddCondition(DetectTrigger, DetectFunc)
                    set tempG = CreateGroup()
                    loop
                        set u = FirstOfGroup(Objects)
                        exitwhen u == null
                        call GroupRemoveUnit(Objects, u)
                        call GroupAddUnit(tempG, u)
                        call TriggerRegisterUnitEvent(DetectTrigger, u, EVENT_UNIT_ACQUIRED_TARGET)
                    endloop
                    call DestroyGroup(Objects)
                    set Objects = tempG
                    set tempG = null
                endif
            endif
           
            return false
        endmethod
       
        private static method onInit takes nothing returns nothing
           
            local player  p
            local integer i  = 0
            local trigger t1 = CreateTrigger()
            local trigger t2 = CreateTrigger()
            local trigger t3 = CreateTrigger()
            local trigger t4 = CreateTrigger()

            set WorldMaxX = GetRectMaxX(bj_mapInitialPlayableArea)
            set WorldMaxY = GetRectMaxY(bj_mapInitialPlayableArea)
            set WorldMinX = GetRectMinX(bj_mapInitialPlayableArea)
            set WorldMinY = GetRectMinX(bj_mapInitialPlayableArea)
            loop
                exitwhen i == bj_MAX_PLAYER_SLOTS
                set p = Player(i)
                call TriggerRegisterPlayerUnitEvent(t4, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t1, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t1, p, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(t2, p, EVENT_PLAYER_UNIT_SPELL_CAST, null)
                call TriggerRegisterPlayerUnitEvent(t3, p, EVENT_PLAYER_UNIT_ATTACKED, null)
                set i = i + 1
            endloop
            set DetectFunc = Condition(function thistype.OnDetect)
            call TriggerAddCondition(DetectTrigger, DetectFunc)
            call TriggerAddCondition(t1, Condition(function thistype.OnOrder))
            call TriggerAddCondition(t2, Condition(function thistype.OnCast))
            call TriggerAddCondition(t3, Condition(function thistype.OnAttack))
            call TriggerAddCondition(t4, Condition(function thistype.OnImmediateOrder))
            static if AUTO_REGISTER then
                call RegisterUnitIndexEvent(Condition(function thistype.OnIndex), UnitIndexer.INDEX)
            endif
            call RegisterUnitIndexEvent(Condition(function thistype.OnDeindex), UnitIndexer.DEINDEX)
           
        endmethod
       
    endstruct
   
endlibrary

Credits:
- Nestharus
- Garfield1337
- Vexorian
- Bribe​
Contents

Rigidbody Physics (Map)

Reviews
IcemanBo
// Input unit data to the data reference for better accuracy ^it's not very clear at that point. Later in docu, actually not very, too. Can you provide information what it is about with the unit data, and why one needs even to define attack range, or...
MyPad
Suggestions and Optimizations: A minor optimization for static method OnOrder in RigidBody: if .orderTarget == null then set .orderX = GetOrderPointX() set .orderY = GetOrderPointY() set .isMoving = true else set .isMoving = true...
Level 22
Joined
Feb 6, 2014
Messages
2,466
It's quite not clear what the system does in my opinion. Where's the API? Does this system doesn't need API and automatically integrates upon import?

Quilnez said:
Does this mean some terrain has higher coefficient of friction (e.g. grass) that units moving there will be slower compared to dirt terrain that has lesser coefficient of friction?
Quilnez said:
gravitational force
I'm guessing moving to higher grounds through ramps is slower while moving to lower grounds through ramp is faster? What about moving from higher ground to lower ground without ramp (through cliffs)? Is that supported?
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
It's quite not clear what the system does in my opinion. Where's the API? Does this system doesn't need API and automatically integrates upon import?
There are some APIs, I just haven't made proper documentation.

Does this mean some terrain has higher coefficient of friction (e.g. grass) that units moving there will be slower compared to dirt terrain that has lesser coefficient of friction?
Yes, you can specify each tile's friction. Keep in mind that higher friction doesn't necessarily mean slower movement, because it's not for "slider" only. If a terrain has enough friction, it will cause units to move normally upon that terrain.

I'm guessing moving to higher grounds through ramps is slower while moving to lower grounds through ramp is faster? What about moving from higher ground to lower ground without ramp (through cliffs)? Is that supported?
Yes. It supports take off the ground too if your velocity is enough to escape the gravitation force.

EDIT:
I uploaded some vids demonstrating each feature.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
v1.1:
  • Added in-depth documentations
  • Improved compatibility with native movements
    • No longer interrupt target detection behavior
    • Added compatibility with non order-breaking spells (such as divine shield)
  • Sloppy ground speed bonus/penalty is calculated more properly
  • Added new methods:
    • destroy
    • jump
    • move
    • enable
  • Detection trigger is now recycled properly
  • Some adjustments on default configuration
  • Removed unit will be automatically unregistered
  • Added auto register option
  • Tons of optimizations
  • Improved demonstration map
Known issues:
  • Not yet handles collision and terrain pathing
  • Not yet handles widget following
  • Bugged with attack index changing items

v1.1b:
  • Fixed a simple auto attacking bug
    Sorry for this careless update. It introduces even worse bug. I will fix it later.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Update v1.1c (minor)
- Renamed to RigidBody
- Fixed target detection bug

Sorry @Paultaker I haven't added Bribe's UnitIndexer yet. I will make sure to do so in the next update.
In the next update, I'm planning to add compatibility with native terrain pathing and shortest path detection by modifying unit's movespeed instead of simulating it.
 
// Input unit data to the data reference for better accuracy
^it's not very clear at that point. Later in docu, actually not very, too. Can you provide information what it is about with the unit data, and why one needs even to define attack range, or backswing.

JASS:
        private static thistype array UnitIndex
    
        static method operator [] takes unit u returns thistype
            return UnitIndex[GetUnitUserData(u)]
        endmethod
^Instead, directly using a unit's userdata for this can be used. And then no allocation is needed.

It is worthy to note that it works with disabling unit's pathing.

Why is the data update logics using it's own interval? Is it required to have it run more often, other than always updating data on INTERVAL? I mean this: private constant real RELOCATION_RATE = 0.01

JASS:
call ReleaseTimer(.tick)
call ReleaseTimer(.locator)
call ReleaseTimer(.disabler)
^it seems missing in destroy() method.

Currently it might happen that a unit circulates for ever, in case it's ordered to move to a very close position, which it will technically never reach because of the turn rate.

What are the bugs you were talking about in earlier posts? Were they fixed?

It seems like a great resource. Probably worth a 5 rating on approval.
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Why isn't the data update logics using it's own interval? Is it required to have it run more often, other than always updating data on INTERVAL?
Hm, because we need to refresh unit's position as often as possible to override units' native movement, 0.03 is not fast enough.

Currently it might happen that a unit circulates for ever, in case it's ordered to move to a very close position, which it will technically never reach because of the turn rate.
Indeed.

What are the bugs you were talking about in earlier posts? Were they fixed?
It was fixed.

I'm not sure to update this resource tho. It's your call to approve it or not, either is fine :)
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
I take it you're inspired from Unity's RigidBody? :p
Nope. This is a self-invented physic made for my Glideon map, formerly named Movement Physic system, made way before I know Unity. But later my head got a little big and I renamed it because I found a little resemblance from Unity's rb, only to find that this is nothing close to Unity's rb. Still too lazy to rename it again tho, but will definitely do later.
 
Hey, Dalvengyr. This is quite the resurrection spell cast on this thread.

I will add some feedback on this.

Notes:

  • JASS:
    static method operator [] takes unit u returns thistype
    [/LIST]
                return UnitIndex[GetUnitUserData(u)]
            endmethod

    I think IcemanBo already mentioned that, so I'll add some more.
    I think registering a unit twice will cause unintended behavior, since it does not check if an instance is already registered, given the fact that it is public.
  • Some method names are not camelCase. Looking at the handler methods and such.
  • The keyword public can be removed from the struct, since structs are public by default.
  • For versions 1.29 and above, you can use the native BlzSetUnitZ (or BlzSetLocalUnitZ, I think that's it's name) inside SetUnitZ.
  • Structs UnitDataRef and TerrainUtils do not use a module initializer. This could break resources that will depend on the public methods found therein (using them in a module).
  • GetTerrainFriction and SetTerrainFriction have different function arguments. Is this intended?
  • Edit: Why not update Table to it's current version?
  • Method destroy does not dereference the value of UnitIndex at that specific location. If not Auto registered, this could cause another unit with the same unit index to point to an instance which is already destroyed.

Nitpicks:

  • How about renaming SetTerrainFriction to SetTerrainTypeFriction?
 
Last edited:

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Updated:
- Now the system works well enough along with the path finding
- Collision is now enabled. Collision detection is "bugged" at times that's because the PathingLib detection accuracy isn't configurable just yet. But this shouldn't be game breaking at all.
- Units will now slow down upon approaching target point

@MyPad:
I think registering a unit twice will cause unintended behavior, since it does not check if an instance is already registered, given the fact that it is public.
Fixed. Now it uses UnitUserData instead of allocation.

Some method names are not camelCase. Looking at the handler methods and such.
I prefer to use PascalCase for static members. But over times, I realize that using camelCase in programming is old and uncool.

The keyword public can be removed from the struct, since structs are public by default.
By using public keyword user must use the library's name as prefix. I did that in purpose since I think those struct names are quite generic that can easily collide with other systems.

For versions 1.29 and above, you can use the native
BlzSetUnitZ
(or BlzSetLocalUnitZ, I think that's it's name) inside SetUnitZ.
I don't use 1.29 : p And I prefer to let this system support all patch versions.

GetTerrainFriction and SetTerrainFriction have different function arguments. Is this intended?
I take your last suggestion and change its name to avoid misunderstanding.

Edit: Why not update Table to it's current version?
I didn't see the edit. And I didn't know Table get an update.

Thanks @IcemanBo and @MyPad for the great reviews!

EDIT:
I will make another small update to improve the collision behavior, soon.
 
Last edited:

Suggestions and Optimizations:

  • A minor optimization for static method OnOrder in RigidBody:
JASS:
if .orderTarget == null then
    set .orderX = GetOrderPointX()
    set .orderY = GetOrderPointY()
    set .isMoving = true
else
    set .isMoving = true
endif

can be made into:

JASS:
if .orderTarget == null then
    set .orderX = GetOrderPointX()
    set .orderY = GetOrderPointY()
endif
set .isMoving = true
  • Why not handle the onPeriodic callback method globally (One timer fits all approach)? Only add active instances to the list and remove them when no longer active.

Issues:

  • It appears that Shift-queue orders will break with this system. (The system will forcibly reset the unit's orders).
For now, the most important thing to resolve is the Shift-queue issue with the system.

Set to Awaiting Update.
 
Last edited:
Its wrong for this system to be awaiting update when asking for a wc3 bug that can not be fixed by default wc3 though the following would be a better reason.

Units that go on the top right arrows fly out of map with and without boots of speed crashing the game even though I see a worldbounds in here... Collision eventually bugs and so does basic movement bugs by getting stuck and teleporting in place.
 
Last edited:
Top