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

[vJASS] Projectile Extension: ProjectileCollision

Level 19
Joined
Mar 18, 2012
Messages
1,716
Projectile Collision
Code

Library ProjectileCollision is a system that extends
library Projectile by collision detection in 2D and 3D space.
ProjectileCollision is required to configurate free flying projectiles
without predetermined target widget.



~~~~~~~~~~~~~~~~~~~~~~~~~~
Color_balance.png
Import Instructions



~~~~~~~~~~~~~~~~~~~~~~~~~~
Copy.png
Library ProjectileCollision

JASS:
library ProjectileCollision /* Version 1.0 by BPower
*************************************************************************************
*
*   Collision detection for library Projectile
*   
************************************************************************************* 
*
*   Import instructions:
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       • Copy library ProjectileCollision into to your map. 
*       • Initialize custom unit heights in function InitBodySize. ( Optional ) 
*
*************************************************************************************
*
*   */ initializer InitBodySize     /* ( See function below the API documentation )
*   */ requires    Projectile       /* 
*   */ optional    GetUnitCollision /* github.com/nestharus/JASS/tree/master/jass/Systems/GetUnitCollision
*
*************************************************************************************
*
*   API:   
*   ¯¯¯¯
*       function SetUnitBodySize takes integer unitId, real size returns nothing
*       function GetUnitBodySize takes unit whichUnit returns real
*
*       struct Projectile
*
*           public boolean collision3D = true 
*               • Set this.collision3D to false if you want 2D collision detection.
*
*           method hitWidget takes widget w returns nothing
*           method hasHitWidget takes widget w returns boolean
*           method removeHitWidget takes widget w returns nothing
*           method flushHitWidgets takes nothing returns nothing
*
*           method cacheUnitPos takes nothing returns nothing
*               • Must always be called before using one of the methods below.
*
*                   --> readonly real unitX
*                   --> readonly real unitY     
*                   --> readonly real unitZ // Combined terrain z and unit fly height.
*
*           method isUnitInRange takes unit whichUnit, real range returns boolean
*               • Returns true for units in 3D collision range. 
*        
*           method isItemInRange takes item whichItem, real range returns boolean
*               • Returns true for items in 3D collision range.
*        
*           method isDestructableInRange takes destructable d, real range returns boolean
*               • Returns true for destructables in 3D collision range.
*
*           method isUnitInRect takes unit whichUnit, real range returns boolean    
*           method isItemInRect takes item whichItem, real range returns boolean
*           method isDestructableInRect takes destructable d, real range returns boolean 
*
*************************************************************************************/

// User settings:
// ==============
    
    // Initialize custom body size values per unit type id.
    // By default any unit uses UNIT_BODY_SIZE from the Projectile library constants. 
    private function InitBodySize takes nothing returns nothing
        call SetUnitBodySize('hfoo', 100.00)
        // ...
    endfunction
    
//=============================================================================
// Projectile Collision Code.
//=============================================================================
    
    // Doesn't consider unit scaling, because of the difficulty 
    // of properly catching Avatar, Bloodlust and SetUnitScale effects.
    //! textmacro PROJECTILE_COLLISION_UTILTIY_CODE
    
    function GetUnitBodySize takes unit whichUnit returns real
        if HaveSavedReal(HASH, KEY_BODY_SIZE, GetUnitTypeId(whichUnit)) then
            return LoadReal(HASH, KEY_BODY_SIZE, GetUnitTypeId(whichUnit))
        endif
        return UNIT_BODY_SIZE        
    endfunction
    
    function SetUnitBodySize takes integer unitId, real size returns nothing
        call SaveReal(HASH, KEY_BODY_SIZE, unitId, size) 
    endfunction
    
    // An avarage finalizer should be able to inline this function.
    private function GetUnitPathing takes unit whichUnit returns real
        static if LIBRARY_GetUnitCollision then
            return GetUnitCollision(whichUnit)
        else
            return UNIT_PATHING_RADIUS
        endif
    endfunction
    
    //! endtextmacro
    
    //! textmacro PROJECTILE_COLLISION_CODE
        
        //===================================================================
        // Collision filters
        
        // Readonly variables for the code inside the ProjectileStruct module.
        readonly static boolexpr unitFilter = null
        readonly static boolexpr itemFilter = null
        readonly static boolexpr destFilter = null
        
        // Filter function array
        private static boolexpr array filter
        
        // Filter constants
        private static constant integer UNIT_FILTER_2D   = 0
        private static constant integer ITEM_FILTER_2D   = 1
        private static constant integer DEST_FILTER_2D   = 2
        private static constant integer UNIT_FILTER_3D   = 3
        private static constant integer ITEM_FILTER_3D   = 4
        private static constant integer DEST_FILTER_3D   = 5
        private static constant integer UNIT_FILTER_RECT = 6
        private static constant integer ITEM_FILTER_RECT = 7
        private static constant integer DEST_FILTER_RECT = 8
                      
        //===================================================================
        // Collision type
              
        public   boolean collision3D = true 
        
        //===================================================================
        // Last cached projectile position.
        
        readonly real unitX
        readonly real unitY     
        readonly real unitZ // Combined terrain z and unit fly height. 
        method cacheUnitPos takes nothing returns nothing
            set unitX = GetUnitX(dummy)
            set unitY = GetUnitY(dummy)
            set unitZ = GetUnitFlyHeight(dummy) + GetTerrainZ(unitX, unitY)            
        endmethod
        
        method prepareCollision takes nothing returns nothing
            local real r = collision + Projectile_MAX_COLLISION_SIZE
        
            call cacheUnitPos()
    
            if speed <= collision then
                if collision3D then
                    set unitFilter = filter[UNIT_FILTER_3D]
                    set destFilter = filter[DEST_FILTER_3D]
                    set itemFilter = filter[ITEM_FILTER_3D]
                else
                    set unitFilter = filter[UNIT_FILTER_2D]
                    set destFilter = filter[DEST_FILTER_2D]
                    set itemFilter = filter[ITEM_FILTER_2D]                
                endif
                call SetRect(ENUM_RECT, unitX - r, unitY - r, unitX + r, unitY + r)
                return
            endif
            
            set unitFilter = filter[UNIT_FILTER_RECT]
            set destFilter = filter[DEST_FILTER_RECT]
            set itemFilter = filter[ITEM_FILTER_RECT]
            if prevX < x then
                if prevY < y then
                    call SetRect(ENUM_RECT, prevX - r, prevY - r, x + r, y + r)
                else
                    call SetRect(ENUM_RECT, prevX - r, y - r, x + r, prevY + r)
                endif
            elseif prevY < y then
                call SetRect(ENUM_RECT, x - r, prevY - r, prevX + r, y + r)
            else
                call SetRect(ENUM_RECT, x - r, y - r, prevX + r, prevY + r)
            endif
        endmethod
        
        //===================================================================
        // Projectile collision API
        
        // Hashtable wrappers
        method hitWidget takes widget whichWidget returns nothing
            call SaveWidgetHandle(HASH, this, GetHandleId(whichWidget), whichWidget)
        endmethod
        
        method hasHitWidget takes widget whichWidget returns boolean
            return HaveSavedHandle(HASH, this, GetHandleId(whichWidget))
        endmethod
        
        method removeHitWidget takes widget whichWidget returns nothing
            call RemoveSavedHandle(HASH, this, GetHandleId(whichWidget))
        endmethod
        
        method flushHitWidgets takes nothing returns nothing
            call FlushChildHashtable(HASH, this)
            call SaveWidgetHandle(HASH, this, GetHandleId(dummy), dummy)
        endmethod
        
        method isUnitInRange takes unit whichUnit, real range returns boolean
            call MoveLocation(LOC, GetUnitX(whichUnit), GetUnitY(whichUnit))
            set tempZ = unitZ - GetUnitFlyHeight(whichUnit) - GetLocationZ(LOC)
            
            if GetUnitTypeId(dummy) == Projectile_DUMMY_UNIT_TYPE_ID then
                return IsUnitInRange(whichUnit, dummy, range) and tempZ - range < GetUnitBodySize(whichUnit) and tempZ > -range
            endif
            return IsUnitInRange(whichUnit, dummy, range) and tempZ < GetUnitBodySize(whichUnit) and tempZ > -GetUnitBodySize(dummy)
        endmethod
        
        method isItemInRange takes item whichItem, real range returns boolean
            set tempX = GetItemX(whichItem)
            set tempY = GetItemY(whichItem)
            call MoveLocation(LOC, tempX, tempY)
            set tempX = unitX - tempX
            set tempY = unitY - tempY
            set tempZ = unitZ - GetLocationZ(LOC) - ITEM_PATHING_RADIUS 
        
            return tempX*tempX + tempY*tempY + tempZ*tempZ < range*range + ITEM_PATHING_RADIUS*ITEM_PATHING_RADIUS 
        endmethod
        
        method isDestructableInRange takes destructable d, real range returns boolean
            set tempX = GetDestructableX(d)
            set tempY = GetDestructableY(d)
            call MoveLocation(LOC, tempX, tempY)
            set tempZ = unitZ - GetLocationZ(LOC)
    
            if GetUnitTypeId(dummy) == Projectile_DUMMY_UNIT_TYPE_ID then
                return IsUnitInRangeXY(dummy, tempX, tempY, range + DEST_PATHING_RADIUS)/*
                */ and tempZ - range < GetDestructableOccluderHeight(d) and tempZ > -range 
            endif
            return IsUnitInRangeXY(dummy, tempX, tempY, range + DEST_PATHING_RADIUS)/*
            */ and tempZ < GetDestructableOccluderHeight(d) and tempZ > -GetUnitBodySize(dummy) 
        endmethod

        // Rect collision methods for fast moving projectiles. 2D and 3D.
        method isUnitInRect takes unit whichUnit, real range returns boolean
            local real uX = GetUnitX(whichUnit)
            local real uY = GetUnitY(whichUnit)
            local real uZ = GetUnitFlyHeight(whichUnit) + GetTerrainZ(uX, uY)
            local real dX = x - prevX
            local real dY = y - prevY
            local real dZ = z + GetTerrainZ(x, y) - prevZ 
            local real s  = (dX*(uX - prevX) + dY*(uY - prevY) + dZ*(uZ - prevZ))/(dX*dX + dY*dY + dZ*dZ) 
            local real mZ
            
            if s < 0.00 then
                set s = 0.00
            elseif s > 1.00 then
                set s = 1.00
            endif 
            set mZ = prevZ + s*dZ
            set dZ = dZ - uZ - range 
            
            return IsUnitInRangeXY(whichUnit, prevX + s*dX, prevY + s*dY, range + GetUnitPathing(whichUnit))/*
                */ and (dZ < mZ and dZ + GetUnitBodySize(whichUnit) < mZ or not collision3D)
        endmethod
        
        method isDestructableInRect takes destructable d, real range returns boolean
            local real wX = GetDestructableX(d)
            local real wY = GetDestructableY(d)
            local real wZ = GetTerrainZ(wX, wY)
            local real dX = x - prevX
            local real dY = y - prevY
            local real dZ = z + GetTerrainZ(x, y) - prevZ
            local real s  = (dX*(wX - prevX) + dY*(wY - prevY) + dZ*(wZ - prevZ))/(dX*dX + dY*dY + dZ*dZ)  
            
            if s < 0.00 then
                set s = 0.00
            elseif s > 1.00 then
                set s = 1.00
            endif
            set dX = prevX + s*dX - wX
            set dY = prevY + s*dY - wY
            set dZ = prevZ + s*dZ  
            set wZ = dZ - wZ - range
            
            return dX*dX + dY*dY < range*range + DEST_PATHING_RADIUS*DEST_PATHING_RADIUS/*
                */ and (wZ < dZ and wZ + GetDestructableOccluderHeight(d) > dZ or not collision3D)
        endmethod
        
        method isItemInRect takes item whichItem, real range returns boolean
            local real iX = GetItemX(whichItem)
            local real iY = GetItemY(whichItem)
            local real iZ = GetTerrainZ(iX, iY) + ITEM_PATHING_RADIUS
            local real dX = x - prevX
            local real dY = y - prevY
            local real dZ = z + GetTerrainZ(x, y) - prevZ
            local real s  = (dX*(iX - prevX) + dY*(iY - prevY) + dZ*(iZ - prevZ))/(dX*dX + dY*dY + dZ*dZ)
            
            if s < 0.00 then
                set s = 0.00
            elseif s > 1.00 then
                set s = 1.00
            endif
            set dX = prevX + s*dX - iX
            set dY = prevY + s*dY - iY
            
            if collision3D then
                set dZ = prevZ + s*dZ - iZ
                return dX*dX +dY*dY + dZ*dZ < range*range + ITEM_PATHING_RADIUS*ITEM_PATHING_RADIUS
            endif
            return dX*dX + dY*dY < range*range + ITEM_PATHING_RADIUS*ITEM_PATHING_RADIUS
        endmethod
        
        //===================================================================
        // Projectile projectile collision.
        
        private static method enumProjectiles takes nothing returns nothing
            local thistype this = loopIndex
            local thistype node = thistype.first
            local boolean  fast = speed > collision
            
            loop
                exitwhen node == 0
                
                if not HaveSavedHandle(HASH, this, GetHandleId(node.dummy)) then
             
                    if fast then
                        // Actually an incorrect calculation. I'll fix it somewhen.
                        if isUnitInRect(node.dummy, collision) then
                            call GroupAddUnit(FILTER_GROUP, node.dummy)
                        endif
                    elseif collision3D then
                        set tempX = unitX - node.x
                        set tempY = unitY - node.y
                        call MoveLocation(LOC, node.x, node.y)
                        set tempZ = unitZ - node.z - GetLocationZ(LOC)
                        if GetUnitTypeId(node.dummy) == Projectile_DUMMY_UNIT_TYPE_ID then
                            if tempX*tempX + tempY*tempY + tempZ*tempZ < collision*collision + node.collision*node.collision then
                                call GroupAddUnit(FILTER_GROUP, node.dummy)
                            endif
                        elseif isUnitInRange(node.dummy, collision) then
                            call GroupAddUnit(FILTER_GROUP, node.dummy)
                        endif
                    elseif IsUnitInRange(dummy, node.dummy, collision + node.collision) then
                        call GroupAddUnit(FILTER_GROUP, node.dummy)
                    endif
                endif
                set node = node.next
            endloop
        endmethod
        
        method enumForOnProjectile takes code actionFunc returns nothing
            call cacheUnitPos()
            call GroupClear(FILTER_GROUP)
            call ForForce(bj_FORCE_PLAYER[0], function thistype.enumProjectiles)
            call ForGroup(FILTER_GROUP, actionFunc)
        endmethod
        
        //===================================================================
        // Filter conditions

        private static method filterUnit2D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterUnit()))/*
                */ and IsUnitInRange(GetFilterUnit(), loopIndex.dummy, loopIndex.collision) 
        endmethod

        private static method filterItem2D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterItem()))/*
                */ and IsUnitInRangeXY(loopIndex.dummy, GetItemX(GetFilterItem()), GetItemY(GetFilterItem()), loopIndex.collision + ITEM_PATHING_RADIUS) 
        endmethod
        
        private static method filterDest2D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterDestructable()))/*
                */ and IsUnitInRangeXY(loopIndex.dummy, GetDestructableX(GetFilterDestructable()), GetDestructableY(GetFilterDestructable()), loopIndex.collision + DEST_PATHING_RADIUS) 
        endmethod
        
        private static method filterUnit3D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterUnit()))/*
                */ and loopIndex.isUnitInRange(GetFilterUnit(), loopIndex.collision) 
        endmethod

        private static method filterItem3D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterItem()))/*
                */ and loopIndex.isItemInRange(GetFilterItem(), loopIndex.collision) 
        endmethod
        
        private static method filterDest3D takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterDestructable()))/*
                */ and loopIndex.isDestructableInRange(GetFilterDestructable(), loopIndex.collision) 
        endmethod
        
        private static method filterUnitRect takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterUnit()))/*
                */ and loopIndex.isUnitInRect(GetFilterUnit(), loopIndex.collision) 
        endmethod
        
        private static method filterItemRect takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterItem()))/*
                */ and loopIndex.isItemInRect(GetFilterItem(), loopIndex.collision) 
        endmethod
        
        private static method filterDestRect takes nothing returns boolean
            return not HaveSavedHandle(HASH, loopIndex, GetHandleId(GetFilterDestructable()))/*
                */ and loopIndex.isDestructableInRect(GetFilterDestructable(), loopIndex.collision) 
        endmethod
                
    //! endtextmacro

//===================================================================
// Collision init
    
    //! textmacro PROJECTILE_COLLISION_ON_INIT
            set filter[UNIT_FILTER_2D] = Filter(function thistype.filterUnit2D)
            set filter[ITEM_FILTER_2D] = Filter(function thistype.filterItem2D)
            set filter[DEST_FILTER_2D] = Filter(function thistype.filterDest2D)
            set filter[UNIT_FILTER_3D] = Filter(function thistype.filterUnit3D)
            set filter[ITEM_FILTER_3D] = Filter(function thistype.filterItem3D)
            set filter[DEST_FILTER_3D] = Filter(function thistype.filterDest3D)
            set filter[UNIT_FILTER_RECT] = Filter(function thistype.filterUnitRect)
            set filter[ITEM_FILTER_RECT] = Filter(function thistype.filterItemRect)
            set filter[DEST_FILTER_RECT] = Filter(function thistype.filterDestRect)
    //! endtextmacro
    
endlibrary



~~~~~~~~~~~~~~~~~~~~~~~~~~
Flow_block.png
Requirements


~~~~~~~~~~~~~~~~~~~~~~~~~~
Scenario.png
Changelog
  • -
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Does this detect Missile-Missile collision? If so, which method did you end up with?
Yes, but it's a bit basic. It assumes that missiles are spherical objects and checks if a sphere collides with another sphere.

Avoid SquareRoot native for performance gain:
dX*dX + dY*dY + dZ*dZ < collision*collision + other.collision*other.collision.

It would be best to compare if the collision box of missile A intersects collision box of missile B,
but I didn't find the time to look up the correct algorithm for it.


From the MissileStruct module placed in a missile struct.

JASS:
    static if thistype.onMissile.exists then
        private static method onMissileM takes nothing returns nothing
            local Missile missile = Missile[GetEnumUnit()]
            local integer id = GetHandleId(missile.dummy)
            
            if loopIndex.exists and missile.exists then
                call SaveUnitHandle(HASH, loopIndex, id, missile.dummy) 
                call SaveInteger(HASH, loopIndex, id, LoadInteger(HASH, KEY_UNIQUE_MISSILE_ID, id)) 
                if thistype.onMissile(loopIndex, missile) then
                    call terminateM(loopIndex)
                endif
            endif
        endmethod
    endif


.......
      static if thistype.onMissile.exists then
                    if this.exists and this.collision > 0.00 then
                        call this.enumForOnMissile(function thistype.onMissileM)
                    endif
                endif
JASS:
        private static method enumMissiles takes nothing returns nothing
            local thistype this = loopIndex
            local thistype node = thistype.first
            local boolean  fast = speed > collision
            
            loop
                exitwhen node == 0
                
                if not HaveSavedHandle(HASH, this, GetHandleId(node.dummy)) then
             
                    if fast then
                        // Actually an incorrect calculation. I'll fix it somewhen.
                        if isUnitInRect(node.dummy, collision) then
                            call GroupAddUnit(FILTER_GROUP, node.dummy)
                        endif
                    elseif collision3D then
                        set tempX = unitX - node.x
                        set tempY = unitY - node.y
                        call MoveLocation(LOC, node.x, node.y)
                        set tempZ = unitZ - node.z - GetLocationZ(LOC)
                        if GetUnitTypeId(node.dummy) == Missile_DUMMY_UNIT_TYPE_ID then
                            if tempX*tempX + tempY*tempY + tempZ*tempZ < collision*collision + node.collision*node.collision then
                                call GroupAddUnit(FILTER_GROUP, node.dummy)
                            endif
                        elseif isUnitInRange(node.dummy, collision) then
                            call GroupAddUnit(FILTER_GROUP, node.dummy)
                        endif
                    elseif IsUnitInRange(dummy, node.dummy, collision + node.collision) then
                        call GroupAddUnit(FILTER_GROUP, node.dummy)
                    endif
                endif
                set node = node.next
            endloop
        endmethod
        
        method enumForOnMissile takes code actionFunc returns nothing
            call cacheUnitPos()
            call GroupClear(FILTER_GROUP)
            call ForForce(bj_FORCE_PLAYER[0], function thistype.enumMissiles)
            call ForGroup(FILTER_GROUP, actionFunc)
        endmethod
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
I think we can either assume that a projectile is spherical object,
which results into the following collision check:
dX*dX + dY*dY + dZ*dZ < collision1*collision1 + collision2*collision2

Eventually assuming projectiles are AABBs is easier, but probably less accurate.

I'm not sure if using a grid comes along with obvious benefits.
The total projectile count normally doesn't and shouldn't go above ~150.

The other assumption is that missiles are OBBs.
Projectile speed would determine the length of the box between two projectile game states.

The latter calculation is always required if the collision size is lower than the displacement.
I guess there is a better approach than my isUnitInRect method, but I don't know how :/
 
I once wrote a Boid system in Warcraft and let 30 boids fly.
I never knew about kNN algorithms so I used Brute Forcing to enum boids that collide, it dropped my fps from 60 to 20 :/ (because brute forcing results to O(n^2))

I am now rewriting my Boid system to use Spatial Hash Grids, after all I have tested this in a minigame I have made using Java.
 
Top