• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • It's time for the first HD Modeling Contest of 2025. Join the theme discussion for Hive's HD Modeling Contest #7! Click here to post your idea!

[JASS] How to determine angle of reflection?

Rheiko

Spell Reviewer
Level 25
Joined
Aug 27, 2013
Messages
4,122
I'm working on a spell that fires a projectile that travels for X distance at target location's angle, if it hits an obstacle on its way (cliff and units), it should be reflected with the correct angle.
I've been following this tutorial so far (Deflection Angle) although I don't get the angle of reflection correctly still for cliffs.
Also, I'm pretty bad at math.

This is the code:
JASS:
function GetDeflectionAngle takes real mx, real my, real tx, real ty, real mface returns real
    return 2 * Atan2(ty - my, tx - mx) + bj_PI - mface
endfunction

function FLRE_Loop takes nothing returns nothing
    //Locals
    local integer Node = 0
    local integer order
    local real x
    local real x2
    local real y
    local real y2
    local real Angle
    local real Angle2
    local real tempCos
    local real tempSin
 
    loop
        set Node = udg_FLRE_NextNode[Node]
        exitwhen Node == 0
    
        set order = GetUnitCurrentOrder(udg_FLRE_Caster[Node])
        if (order == OrderId(FLRE_OrderId())) then
        if (udg_FLRE_Interval[Node] < FLRE_DurationBase(udg_FLRE_Level[Node])) then
        set udg_FLRE_Counter[Node] = udg_FLRE_Counter[Node] + FLRE_TimerSpeed()
        if (udg_FLRE_Counter[Node] >= 1) then
            set x = GetUnitX(udg_FLRE_Dummy[Node])
                    set y = GetUnitY(udg_FLRE_Dummy[Node])
            set udg_FLRE_Interval[Node] = udg_FLRE_Interval[Node] + 1
            set udg_FLRE_Counter[Node] = 0.00
            set udg_FLRE_CurrentSize[Node] = udg_FLRE_CurrentSize[Node] + 0.1
            call SetUnitScale( udg_FLRE_Dummy[Node], udg_FLRE_CurrentSize[Node], udg_FLRE_CurrentSize[Node], udg_FLRE_CurrentSize[Node] )
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Other\\Doom\\DoomDeath.mdl", x, y))
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl", x, y))
            set udg_FLRE_BounceDmg[Node] = udg_FLRE_BounceDmg[Node] + FLRE_OnHitDamageBase(udg_FLRE_Level[Node])
        endif
    
        endif
        else
        set x = GetUnitX(udg_FLRE_Dummy[Node])
            set y = GetUnitY(udg_FLRE_Dummy[Node])
        
            if (udg_FLRE_ReachedDistance[Node] < udg_FLRE_MaxDistance[Node]) then                   
                set x2 = x + udg_FLRE_CurrentSpeed[Node] * Cos(udg_FLRE_CurrentAngle[Node])
                set y2 = y + udg_FLRE_CurrentSpeed[Node] * Sin(udg_FLRE_CurrentAngle[Node])
                set udg_CP_Point = Location(x2, y2)
        call TriggerExecute( gg_trg_Check_Walkability )
        if not (udg_CP_PointIsWalkable) then
            set udg_FLRE_ReachedDistance[Node] = udg_FLRE_ReachedDistance[Node] + udg_FLRE_CurrentSpeed[Node]
            set udg_FLRE_CurrentAngle[Node] = GetDeflectionAngle(y2, y, x2, x, (udg_FLRE_CurrentAngle[Node]))
            set x2 = x + udg_FLRE_CurrentSpeed[Node] * Cos(udg_FLRE_CurrentAngle[Node])
                    set y2 = y + udg_FLRE_CurrentSpeed[Node] * Sin(udg_FLRE_CurrentAngle[Node])        
            call BJDebugMsg("Found unpathable!")
            call BJDebugMsg("NewAngle: " + R2S(udg_FLRE_CurrentAngle[Node] * bj_RADTODEG))      
        endif
        // Move the projectile
        call SetUnitX(udg_FLRE_Dummy[Node], x2)
                call SetUnitY(udg_FLRE_Dummy[Node], y2)
        set udg_FLRE_ReachedDistance[Node] = udg_FLRE_ReachedDistance[Node] + udg_FLRE_CurrentSpeed[Node]
        call RemoveLocation(udg_CP_Point)         
            
            else
                call KillUnit(udg_FLRE_Dummy[Node])     
            endif
        endif
    
    
    
    endloop
endfunction

function FLRE_Start takes nothing returns boolean
    local integer SpellId = GetSpellAbilityId()
    local integer Node
    local real x1
    local real x2
    local real x3
    local real y1
    local real y2
    local real y3
 
 
    if ( SpellId == FLRE_AbilityId() ) then
    
        // Set up Node
        if (udg_FLRE_RecycledSize == 0) then
            set udg_FLRE_MaxIndex = udg_FLRE_MaxIndex + 1
            set Node = udg_FLRE_MaxIndex
        else
            set udg_FLRE_RecycledSize = udg_FLRE_RecycledSize - 1
            set Node = udg_FLRE_RecycledStack[udg_FLRE_RecycledSize]
        endif
    
        set udg_FLRE_NextNode[Node] = 0
        set udg_FLRE_NextNode[udg_FLRE_PrevNode[0]] = Node
        set udg_FLRE_PrevNode[Node] = udg_FLRE_PrevNode[0]
        set udg_FLRE_PrevNode[0] = Node
    
        // Set up Ability Data
        set udg_FLRE_Caster[Node] = GetTriggerUnit()
        set x1 = GetUnitX(udg_FLRE_Caster[Node])
        set y1 = GetUnitY(udg_FLRE_Caster[Node])
        set x2 = GetSpellTargetX()
        set y2 = GetSpellTargetY()
        set udg_FLRE_CurrentAngle[Node] = Atan2((y2-y1), (x2-x1))
        set udg_FLRE_Owner[Node] = GetTriggerPlayer()
        set udg_FLRE_Level[Node] = GetUnitAbilityLevel(udg_FLRE_Caster[Node], FLRE_AbilityId())
        set udg_FLRE_Duration[Node] = FLRE_DurationBase(udg_FLRE_Level[Node])
        set udg_FLRE_MissileAoe[Node] = FLRE_MissileAoeBase(udg_FLRE_Level[Node])
        set udg_FLRE_FinalAoe[Node] = FLRE_FinalAoeBase(udg_FLRE_Level[Node])
        set udg_FLRE_MaxDistance[Node] = FLRE_MaxDistanceBase(udg_FLRE_Level[Node])
        set udg_FLRE_Speed[Node] = FLRE_SpeedBase(udg_FLRE_Level[Node])
        set udg_FLRE_SpeedFactor[Node] = FLRE_SpeedFactorBase(udg_FLRE_Level[Node])
        set udg_FLRE_MaxBounce[Node] = FLRE_MaxBounceBase(udg_FLRE_Level[Node])
    set udg_FLRE_BounceDmg[Node] = FLRE_InitialDamage(udg_FLRE_Level[Node])
        set udg_FLRE_Bounce[Node] = 0
    set udg_FLRE_Interval[Node] = 0
        set udg_FLRE_ReachedDistance[Node] = 0.
    set udg_FLRE_Counter[Node] = 0.00
    set udg_FLRE_CurrentSize[Node] = 1.
        set udg_FLRE_CurrentSpeed[Node] = udg_FLRE_Speed[Node] * FLRE_TimerSpeed()
        call BJDebugMsg("CurrentAngle: " + R2S(udg_FLRE_CurrentAngle[Node] * bj_RADTODEG))

    // Create dummy unit as projectile
        set x3 = x1 + FLRE_InitDistance() * Cos(udg_FLRE_CurrentAngle[Node])
        set y3 = y1 + FLRE_InitDistance() * Sin(udg_FLRE_CurrentAngle[Node])
        set udg_FLRE_Dummy[Node] = CreateUnit(udg_FLRE_Owner[Node], FLRE_DummyUnit(), x3, y3, udg_FLRE_CurrentAngle[Node] * bj_RADTODEG)    
    call SetUnitScale( udg_FLRE_Dummy[Node], udg_FLRE_CurrentSize[Node], udg_FLRE_CurrentSize[Node], udg_FLRE_CurrentSize[Node] )
    call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Other\\Charm\\CharmTarget.mdl", x3, y3))
        call DestroyEffect(AddSpecialEffect("Abilities\\Weapons\\Bolt\\BoltImpact.mdl", x3, y3))

    // Start timer if this is the only instance
        if (udg_FLRE_PrevNode[Node] == 0) then
            call TimerStart(udg_FLRE_Timer, FLRE_TimerSpeed(), true, function FLRE_Loop)
        endif  
    
    
    endif
    return false
endfunction

function Trig_Flame_Rebound_Actions takes nothing returns nothing
endfunction

//===========================================================================
function InitTrig_Flame_Rebound takes nothing returns nothing
    local trigger FLRE_Trigger = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( FLRE_Trigger, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( FLRE_Trigger, Condition( function FLRE_Start ) )
 
    set udg_FLRE_DummyCaster = CreateUnit(FLRE_DummyOwner(), FLRE_DummyCaster(), 0., 0., 0.)
    call UnitAddAbility(udg_FLRE_DummyCaster, FLRE_DummyAbility())
    call ShowUnit(udg_FLRE_DummyCaster, false)
endfunction

It's not finished yet as I want to deal with the angle of reflection first before the other things. But if there's any feedback regarding the code aside from the main problem, greatly appreciated, as this is my first project using plain jass. Thanks in advance.

Edit: Forgot to mention, I'm using purgeandfire's walkability system to do pathing check.
 

Attachments

  • Flame Rebound.w3x
    42.1 KB · Views: 3
Last edited:
Bad mathers unite! This'll be a bit of a long post, so I'll break it down into sections.

Shifting to vJASS

First things first, if you're starting to write code for wc3 (which is awesome)--I'd recommend either using vJass or Lua. To enable vJass on the editor for the latest patch, just open the Trigger Editor > JassHelper > Enable JassHelper and Enable vJass. It'll add a lot of features that save a lot of time and make things a lot more readable. Even if you don't want to jump into the complexities of structs and such yet, the two main features that'll be useful right away are:
  • Globals
  • Scopes
The globals feature allows you to define variables in code. It makes things a lot easier, more readable, and reduces a lot of clutter.
JASS:
// e.g. the equivalent of udg_FLRE_CurrentSize and udg_FLRE_Interval
globals
     real array CurrentSize
     real array Interval
endglobals

Next, Scopes enable encapsulation, which essentially solves the issue where we need to put prefixes on every variable (e.g. FLRE). The reason we have to do that in GUI is because all the variables are in the global "namespace", and you can't have two variables with the same name. But it makes our triggers look a lot more complicated and less readable. Thankfully, with scopes you can make variables "private" to the scope--this means they can only be accessed within that scope/endscope section--and it allows you to freely name the variable without worrying about it conflicting with another variable name (they also let you do that for functions too!).

To take advantage of it, you just need to wrap your code in a "scope" with a name, and then put "private" in front of your variables:
JASS:
scope FlameRebound
    globals
        private real array Interval
    endglobals
 
    // ... spell code
endscope

scope MyOtherSpell
     globals
         private real array Interval // this is fine, this name won't conflict with FlameRebound since they are private and in different scopes
     endglobals

     // ... other spell code
endscope

The one other feature I'll mention for scopes is that they allow you to specify a function to run on map initialization. Normally wc3 has one initializer function per trigger, prefixed by "InitTrig_". Scopes give you more flexibility:
JASS:
scope FlameRebound initializer Init
    globals
        // ... any globals you want
    endglobals
    private function Init takes nothing returns nothing
        // ... create a trigger, register the event, add an action, etc.
    endfunction
endscope

Main takeaway is that this'll make your life a lot easier as you start to learn JASS, even if you just use these two features. Your code will end up being a LOT more readable too, which is a big win!

Deflecting off Units
Back to your spell. I'll discuss deflection for three different cases as they each have some subtle details that are useful to consider:
  • Units
  • Destructables
  • Cliffs
The first thing I noticed with the code was that you accidentally swapped the parameters around:
JASS:
// This is the function signature (missile X, missile Y, target X, target Y, missile facing)
function GetDeflectionAngle takes real mx, real my, real tx, real ty, real mface returns real

// You accidentally put (y2, y, x2, x, facing), it should probably be (x, y, x2, y2, facing)
set udg_FLRE_CurrentAngle[Node] = GetDeflectionAngle(y2, y, x2, x, (udg_FLRE_CurrentAngle[Node]))

However, if you fix it--you might notice that the projectile reflects, but it just comes back the way it came. The issue is that we're only considering the details of the projectile: it's initial point (x, y), its next point (x2, y2), and its facing.

Consider this example. We have two balls being thrown at a 45 degree angle--one is against a vertical wall and one is against a horizontal wall. Even though the ball has the same properties: initial point, next point, and facing--the deflection angle is different:
Deflect1.png
Deflect2.png


So this brings up an important point: to calculate the deflection angle, the missile information alone is not enough. You need "some" information from the thing you're colliding with. Which makes sense. If I throw a ball in real life, it'll bounce differently based on the thing I'm hitting, right?

If you look up info on deflection/reflection online, you'll get a bunch of things about the law of reflection--the angle of incidence equals the angle of reflection with respect to the surface normal, yadda yadda:
DeflectLaw.png

The "surface normal" sounds complicated, but it really is just like drawing an imaginary line from the surface you're hitting outwards. It essentially just says "hey, I am facing this direction". As for the angles: incidence = incoming angle, reflection = outgoing angle. That's why deflection feels so simple to us in our heads. If you drew something on paper, you could pretty accurately guess how it is going to deflect off that surface (especially if you draw that imaginary "surface normal" line).

However, that also illustrates the key question for us in Warcraft III: how do we get the surface normal that we're colliding with? The short answer is: you can't. But you can use some clever tricks to work around it! We'll just need a bit of information from the thing we're colliding with.

In your spell, you currently compute the deflection angle when you hit a non-walkable point. The issue is we don't know what we're hitting--it could be a unit, it could be a destructable, or a cliff, or something else. So we'll need to make some adjustments. We'll start by detecting when we collide with a unit.

For this post, I'll make a custom spell "glacial spike". It'll just fire a projectile that bounces off units/destructables/walls--just to keep things simple. Also, instead of using a dummy unit--I'll just use special effects and the new special effect natives. They allow you to rotate effects and move them around, which basically means we don't have to use dummy units for projectiles anymore.

The initial boilerplate is the same: you set up some variables, start a timer, and every tick you move the projectile towards the target point. To detect if you collide with a unit, we can use unit group enumeration. Basically, every time we move the projectile, you can "pick every unit within 64.0 of the projectile"--if the unit group is not empty, then we've collided with a unit!

Here is an excerpt of what the code could roughly look like:
JASS:
scope GlacialSpike
    globals
        // Controls how close a projectile needs to be to collide with a unit
        private constant real PROJECTILE_UNIT_COLLISION_RADIUS = 64.0

        // Re-used for picking units in range
        private group g = CreateGroup()
    endglobals

    // Modify this to control which units are allowed to be collided with
    private function UnitCollisionFilter takes unit unitToCheck, unit caster returns boolean
        return GetWidgetLife(unitToCheck) > 0.405 and unitToCheck != caster
    endfunction

    // Given the projectile (x, y) and the caster...
    // Search for any units in range. Check if they are alive and not the caster.
    // If so, return that unit.
    private function SearchForCollidingUnit takes real fx, real fy, unit caster returns unit
        local unit match = null
        call GroupEnumUnitsInRange(g, fx, fy, PROJECTILE_UNIT_COLLISION_RADIUS, null)
        loop
            set match = FirstOfGroup(g)
            exitwhen match == null
           
            // If this unit passes our criteria, return it
            if UnitCollisionFilter(match, caster) then
                return match
            endif
           
            call GroupRemoveUnit(g, match)
        endloop
        return null
    endfunction

    private function ProjectileTick takes nothing returns nothing
        // ... some boilerplate code omitted ...
        local real newX = effectX + PROJECTILE_SPEED * Cos(angle)
        local real newY = effectY + PROJECTILE_SPEED * Sin(angle)
        local unit collisionUnit = null
       
        // ... some logic to move the effect to (newX, newY) ...
       
        // Check if our projectile is colliding with a unit
        set collisionUnit = SearchForCollidingUnit(newX, newY, caster)
        if collisionUnit != null then
            set angle = GetDeflectionAngleOffUnit(newX, newY, angle, collisionUnit)
            // ... rotate the effect to the new angle, add a special effect, etc. ...
        endif
    endfunction
endscope

To recap:
  • Every time we move the projectile, we search to see if we've collided with a unit
  • If we collided with a unit that passes our criteria, call GetDeflectionAngleOffUnit to compute the new angle
  • Rotate the effect and use that new angle for the projectile movement going forward
So what is GetDeflectionAngleOffUnit? I've defined it in a separate trigger, and it is basically the same as in emjlr3's tutorial:
JASS:
    // Credits to emjlr3: https://www.hiveworkshop.com/threads/deflection-angle.194703/
    function GetDeflectionAngleOffUnit takes real missileX, real missileY, real missileFacingRadians, unit target returns real
        local real angleBetween = Atan2(GetUnitY(target) - missileY, GetUnitX(target) - missileX)
        return 2.*angleBetween + bj_PI - missileFacingRadians
    endfunction

It might be a little confusing to understand why this is different from what you had in your code. The main difference is that we now know we're colliding with a unit, and we're incorporating its information into our calculation (GetUnitX and GetUnitY). But why does this work? Well, when we "collide" with a unit, we're really just getting "close enough" to it. You can picture an imaginary circle around the unit, and when the projectile gets in range, we can find the angle between the missile and the center of the unit and use that to "approximate" the normal for our calculations:
DeflectUnit1Fix.png
DeflectUnit2.png
DeflectUnit3.png


(for step 2 to 3, the normal line can be found using the angle in step 2, and flipping it [i.e. add 180 degrees]).

If you implement it this way, it should work reasonably well now!
giphy.gif


Some notes for improvement:
  • You may want to expand the enum range a bit, and then when you're looping through the units, use IsUnitInRangeXY(...). That will consider the unit's collision size better. i.e. if you have a really fat ogre with a large collision radius, you might want the projectile to collide with him based on that rather than whether the projectile is within 64 points of his origin.
  • If the unit is moving, the projectile might deflect multiple times off the same unit. Some spells will track which units have been deflected off of, or will limit the number of bounces, so the projectile doesn't flip flop a million times.
Deflecting off Destructables

Now that we know how to deflect off units, let's focus on destructables. Fortunately, the principle is exactly the same. You'll want to search for nearby destructables, and if you find one close enough, then compute the angle between the missile and the coordinates of the destructable to approximate the normal.

The main difference is that destructables can only be enumerated in rects. So the code will be slightly different:
JASS:
scope GlacialSpike initializer Init
    globals
        private constant real PROJECTILE_DESTRUCTABLE_COLLISION_RADIUS = 128.0

        private rect enumRect = null
        private destructable matchingDestructable = null
    endglobals

    // Modify this to control which destructables are allowed to be collided with
    private function DestructableCollisionFilter takes destructable d returns boolean
        return GetDestructableLife(d) > 0.0
    endfunction

    private function DestructableEnumHandler takes nothing returns nothing
        local destructable d = GetEnumDestructable()
        if DestructableCollisionFilter(d) then
            set matchingDestructable = d
        endif
        set d = null
    endfunction
   
    // Given the projectile (x, y)...
    // Search for any destructables in range. Check if they are alive.
    // If so, return that destructable.
    private function SearchForCollidingDestructable takes real fx, real fy returns destructable
        set matchingDestructable = null
        call MoveRectTo(enumRect, fx, fy)
        call EnumDestructablesInRect(enumRect, null, function DestructableEnumHandler)
        return matchingDestructable
    endfunction

    private function ProjectileTick takes nothing returns nothing
        // ... other variables omitted ...
        local destructable collisionDestructable = null

        // Check if our projectile is colliding with a unit
        set collisionUnit = SearchForCollidingUnit(newX, newY, caster)
        if collisionUnit != null then
            set angle = GetDeflectionAngleOffUnit(newX, newY, angle, collisionUnit)
            // ... rotate effect and handle unit collision ...
        else
            // Check if our projectile is colliding with a destructable
            set collisionDestructable = SearchForCollidingDestructable(newX, newY)
            if collisionDestructable != null then
                set angle = GetDeflectionAngleOffDestructable(newX, newY, angle, collisionDestructable)
                // ... rotate effect and handle destructable collision ...
            endif
        endif
    endfunction
   
    private function Init takes nothing returns nothing
        // ... other init trigger code ...
        set enumRect = Rect(0, 0, PROJECTILE_DESTRUCTABLE_COLLISION_RADIUS, PROJECTILE_DESTRUCTABLE_COLLISION_RADIUS)
    endfunction
endscope

The code for GetDeflectionAngleOffDestructable is basically the same. It just takes in a destructable instead. You could combine the two, but it is sometimes nice to have them separate in case you want to customize the behavior between the two types of objects you can collide with.
JASS:
    function GetDeflectionAngleOffDestructable takes real missileX, real missileY, real missileFacingRadians, destructable target returns real
        local real angleBetween = Atan2(GetDestructableY(target) - missileY, GetDestructableX(target) - missileX)
        return 2.*angleBetween + bj_PI - missileFacingRadians
    endfunction

And hooray, you should be able to deflect off destructables now!
giphy.gif


Notes for improvement:
  • Destructables tend to be static (they don't usually move), so there may be more efficient ways to detect their collision compared to enumerating every tick.

Deflecting off Cliffs

Cool, so deflecting off units and destructables are working well using basically the same technique. Cliffs should be easy, right? Well, not exactly. 😢

The two main problems to solve with cliffs:
  • How do we detect a collision with a cliff?
  • Our previous deflections have treated the objects as "spheres" essentially. But most cliffs are square. How do we accurately deflect off that?
Cliff.png


First, let's stare at a cliff. A cliff takes up one full "tile" (128x128 units). For simplicity, we'll treat each cliff tile as a square to deflect off of.

For the first problem (how do we detect a collision with a cliff)--when we're moving the projectile, we can't really "enumerate" cliffs. But you do have some options:
  • You could use GetLocationZ(...). However, you may not know if something is a cliff or just raised terrain. However, this can be a useful strategy if you want to have your projectile deflect off terrain too. Just be wary that GetLocationZ(...) has had some inconsistencies between reforged <-> classic graphics, so it opens up potential for desyncs if it is used for stuff that can affect the game (i.e. if the Z check is used to deflect the projectile towards a unit, and then that unit takes damage as a result).
  • The other option is GetTerrainCliffLevel(x, y), which returns a number indicating the cliff level at that coordinate.
For this example, we'll use GetTerrainCliffLevel. If we're moving a projectile from (x1, y1) to (x2, y2), we just need to check the terrain level at (x1, y1) and (x2, y2)--and see if it goes up.

This works for most cases, but one issue you might notice is that it'll sometimes go "through" the corner of a cliff.
CliffPass.png

This often happens if the projectile is too fast. There are a couple of ways to fix this:
  1. You could make the timer tick faster (and reduce the projectile speed accordingly) to reduce the likelihood that we skip through the coordinates of a cliff tile. But this can be pretty performance intensive.
  2. You could "enumerate" nearby cliffs by grabbing the tile points nearby, checking for cliffs, and then checking if the projectile line intersects the cliff edges. This would be the most accurate, but gets complicated to write.
  3. (Option I chose) If the projectile moves onto a new "tile", then we can do a "line trace" and see if it encounters a cliff along the way.
By "line trace", I essentially mean you'll divide your (x1, y1) -> (x2, y2) line into points, run through it, and see if you hit a cliff along the way. Some other games will use this for dashes/projectiles/etc. In unreal, for example, they have something similar called "capsule traces" which use a similar concept to check a few points along your path and see if your going to collide with something.
CapsuleTracing.png
So in our case, we'd go through a loop like this and ideally we'd detect that we hit a cliff:
LineTrace.png


The code for this looks roughly like this:
JASS:
    // This just contains data about any potential cliff collision
    private struct CliffCollisionInfo
        static thistype noCollision

        boolean hasCollision
        real collisionX
        real collisionY
        real repositionX
        real repositionY

        static method create takes nothing returns thistype
            local thistype this = thistype.allocate()
            set this.hasCollision = false
            set this.collisionX = 0
            set this.collisionY = 0
            set this.repositionX = 0
            set this.repositionY = 0
            return this
        endmethod

        static method onInit takes nothing returns nothing 
           set CliffCollisionInfo.noCollision = thistype.allocate()
        endmethod
    endstruct

    // These methods return the center coordinates of a tile given an arbitrary (x, y)
    // Basically we can use this to see if we're on a new tile or not, to avoid unnecessary calculations
    // (we know we won't encounter a cliff if we're on the same physical tile)
    function GetTileCenterX takes real x returns real
        local integer tileIndexX = MathRound(x / 128.)
        return tileIndexX * 128.
    endfunction

    function GetTileCenterY takes real y returns real
        local integer tileIndexY = MathRound(y / 128.)
        return tileIndexY * 128.
    endfunction

    // "baseCliffLevel" is the cliff level to compare against
    // "increment" controls how precise our trace is (e.g. if it is "8", we'll create a point every 8 units from start -> end)
    private function SearchForCollidingCliff takes real startX, real startY, real endX, real endY, real angle, integer baseCliffLevel, real increment returns CliffCollisionInfo
        local real startTileX = R2I(GetTileCenterX(startX))
        local real startTileY = R2I(GetTileCenterY(startY))
        local real endTileX = R2I(GetTileCenterX(endX))
        local real endTileY = R2I(GetTileCenterY(endY))

        // For looping
        local real currX = startX
        local real currY = startY
        local integer currCliffLevel = 0
        local real traversed = 0
        local real distance = 0

        // To return multiple data items
        local CliffCollisionInfo info = 0
       
        // If we start and end on the same tile, then we won't collide with a cliff
        if startTileX == endTileX and startTileY == endTileY then
            return CliffCollisionInfo.noCollision
        endif

        set distance = SquareRoot((endX-startX)*(endX-startX) + (endY-startY)*(endY-startY))
        loop
            exitwhen traversed > distance
            set currX = currX + increment*Cos(angle)
            set currY = currY + increment*Sin(angle)
            if GetTerrainCliffLevel(currX, currY) > baseCliffLevel then
                set info = CliffCollisionInfo.create()
                set info.hasCollision = true
                set info.collisionX = currX
                set info.collisionY = currY
                set info.repositionX = currX - increment*Cos(angle)
                set info.repositionY = currY - increment*Sin(angle)
                return info
            endif
            set traversed = traversed + increment
        endloop

        return CliffCollisionInfo.noCollision   
    endfunction

The algorithm basically simulates our projectile movement every "increment" units from "start" to "end" to see if we hit a cliff.

For our purposes, the "struct CliffCollisionInfo" is essentially a group of variables. I'm just using it so we can return multiple properties from a function. In this case, we'll track a few variables:
  • hasCollision - true if a cliff has been detected
  • collisionX/Y - the point on the cliff tile where the projectile hits a cliff
  • repositionX/Y - the point just before we enter the cliff tile. This allows us to reposition the effect a little so it doesn't get stuck "inside" the cliff and reflect within it. Just a lot of trial & error.
Okay, so now we have a way to "search" for a cliff collision. And we can use it like so:
JASS:
set cliffCollisionInfo = SearchForCollidingCliff(effectX, effectY, newX, newY, angle, baseCliffLevel, PROJECTILE_CLIFF_HIT_TEST_INCREMENT) 
if cliffCollisionInfo.hasCollision then
    // Move the effect just prior to colliding with the cliff
    // This avoids the projectile reflecting within the cliff
    set newX = cliffCollisionInfo.repositionX
    set newY = cliffCollisionInfo.repositionY
    // ... move the effect to newX, newY ...

    // Use the actual cliff coordinates to compute the deflection angle
    set angle = GetDeflectionAngleOffCliff(cliffCollisionInfo.collisionX, cliffCollisionInfo.collisionY, angle)
    // ... rotate effect and handle collision ...

    call cliffCollisionInfo.destroy()
endif

Okay, so now we have a similar format to our other objects. The last thing is to compute the deflection angle off the cliff. If we're okay with the cliff being "spherical", we could technically just use the same method as the others--find the center of the tilepoint and use that angle to approximate the normal. This works okayish.

However, I wanted to have it deflect off flat edges. This is pretty straightforward to visualize, as the surface normals are fixed.

CliffNormals.png


But then the question becomes: which edge is the projectile deflecting off of?

There might be some creative ways to find this, but I ended up doing a line intersection test for this. It is a bit expensive, but you're not going to be colliding with a cliff often so IMO it is fine and very accurate.

So the info we have is: the point on the cliff tile where we collided (collisionX, collisionY), and the missile facing.
CliffCollisionInfo1.png

From here, we can create a rough line to a point one tile away to estimate the projectile's path based off of missileFacingRadians. (we could've also just made our function take in our actual startX/startY since we have that, but I figured it wasn't necessary to add those parameters) This "line" will just be used to check which edge we intersect with.

CliffDeflection1.png
CliffIntersection.png


Once we know which edge intersects, we can compute the deflection angle. The code gets quite complicated at this point, as the line intersection algorithm is just one I grabbed off google and ported to JASS--and then the actual deflection angles you can derive using a few examples on pen and paper and simplifying the terms. Here is what the GetDeflectionAngle code looks like:
JASS:
     // Puts an angle into the 0 to 2*bj_PI range
    private function NormalizeAngleRadians takes real angle returns real
        return ModuloReal(angle, 2*bj_PI)
    endfunction

    private function GetDeflectionAngleOffCliffHelper takes real x, real y, real angle returns real
        // Pretend the missile is starting from one tile prior
        // This will serve as the starting point for our line intersection tests
        local real sx = x - 128*Cos(angle)
        local real sy = y - 128*Sin(angle)

        // (tx, ty) is the center of the tile
        // the rest are the four corners of the tile
        local real tx = GetTileCenterX(x)
        local real ty = GetTileCenterY(y)
        local real tileMinX = tx - 64
        local real tileMinY = ty - 64
        local real tileMaxX = tx + 64
        local real tileMaxY = ty + 64

        // Create lines for each edge of the tile
        local LineSegment left = LineSegment.create(tileMinX, tileMinY, tileMinX, tileMaxY)
        local LineSegment top = LineSegment.create(tileMinX, tileMaxY, tileMaxX, tileMaxY)
        local LineSegment right = LineSegment.create(tileMaxX, tileMaxY, tileMaxX, tileMinY)
        local LineSegment bottom = LineSegment.create(tileMinX, tileMinY, tileMaxX, tileMinY)

        // Our missile projectile line to test against
        local LineSegment missileLine = LineSegment.create(sx, sy, x, y)
        
        // Perform intersection tests
        local boolean intersectsLeft = DoLineSegmentsIntersect(missileLine, left)
        local boolean intersectsTop = DoLineSegmentsIntersect(missileLine, top)
        local boolean intersectsRight = DoLineSegmentsIntersect(missileLine, right)
        local boolean intersectsBottom = DoLineSegmentsIntersect(missileLine, bottom)

        call left.destroy()
        call top.destroy()
        call right.destroy()
        call bottom.destroy()
        call missileLine.destroy()

        if intersectsLeft or intersectsRight then
            return bj_PI - angle
        elseif intersectsTop or intersectsBottom then
            return 2*bj_PI - angle
        endif

        return angle
    endfunction

    function GetDeflectionAngleOffCliff takes real missileX, real missileY, real missileFacingRadians returns real
        local real angle = NormalizeAngleRadians(missileFacingRadians)
        return GetDeflectionAngleOffCliffHelper(missileX, missileY, angle)
    endfunction

When you put it all together, you get deflections off cliffs! And everything else! :)
giphy.gif


Some areas to improve:
  • Deflection off diagonal cliffs
  • Sometimes the projectile can still go through a cliff diagonal, since it technically isn't going through a tile with a cliff. This can be improved but would require a few tweaks to enumerate surrounding tiles.
I'll attach the map below (GlacialSpike.w3m). Credits to Vinz for the frostbolt model: Frost Bolt. A lot of parts can be made more efficient/accurate, but I did it mostly for fun/the example--since I haven't found too many useful examples online of how to handle cliff deflection accurately. Hopefully the explanation helps!
 

Attachments

  • GlacialSpike.w3m
    138.7 KB · Views: 6
Last edited:

Rheiko

Spell Reviewer
Level 25
Joined
Aug 27, 2013
Messages
4,122
First of all, that is a very very thorough explanation which I did not expect, at all. And I appreciate it very much. I can't thank you enough for all the effort, I am very grateful. THANK YOU SO MUCH, PURGE. You are the best ; )

I did expect it will get increasingly complicated when deflection angle is involved. I was so lost when I had to handle cliff.
I guess I just unknowingly chose a rather complicated project for my first JASS project, lol.
But this only makes me even eager to work on it!

And yeah, I can really see the reason to move from GUI to JASS and from JASS to vJASS now. Guess, it's time to move to vJASS then.
This is a lot of information to take in, I will take my sweet time to swallow it whole and come back when I have further questions.

since I haven't found too many useful examples online of how to handle cliff deflection accurately.
I swear, I was looking around for some examples but could barely find any. Your post is a really good material for a tutorial. :xxd:
 
Top