• 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.

[Solved] Relativistic Missiles Reflect on Terrain, how?

Edit: (unrefined) Solution in last post by me

Using Relativistic Missiles [vJASS][LUA][GUI] for projectiles.
Given a projectile going in the red direction, when hitting a cliff, go in blue direction.
1746977899475.png

There are (at most) 2 ways it can reflect and make sense, but how do I test what's the correct one?

Basically, how do I detect what "normal" to "fake" for the reflection?

I have a test map with the system and this projectile
JASS:
struct BounceTest extends Missiles
    method onFinish takes nothing returns boolean
        call BJDebugMsg("Finished")
        return true
    endmethod

    method onHit takes unit u returns boolean
        if u != source and UnitAlive(u) and not BlzIsUnitInvulnerable(u) and not IsUnitAlly(u, owner) then
            call BJDebugMsg("Hit Enemy")
            return true
        endif
        return false
    endmethod

    method onCliff takes nothing returns boolean
        //call bounce()
        //Want to set angle, aka: this.origin.angle to the reflection angle, but how to test what normal to use?
        call BJDebugMsg("onCliff")
        return false
    endmethod

    static method setupMissile takes unit c, real x, real y, real angle returns nothing
        local real     z = GetUnitFlyHeight(c) + 70.0
        local real     tx = x + 4000. * Cos(angle)
        local real     ty = y + 4000. * Sin(angle)
        local thistype this = thistype.create(x, y, z, tx, ty, 60.0)

        set source    = c
        set model     = "Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl"
        set speed     = 1000.
        set collision = 32.
        set damage = 5.0
        set owner = GetOwningPlayer(c)

        call launch()

        set c = null
    endmethod
endstruct

function FireMissile takes unit u returns nothing
    call BounceTest.setupMissile(u, GetUnitX(u), GetUnitY(u), GetUnitFacing(u) * bj_DEGTORAD)
endfunction

Also, how do I bounce? It seems that I need to specify new point rather than adjusting the angle. In my case, this seems pretty annoying. I kinda just want to let it travel for ~4 seconds or ~4000 units (but deflect resets the "travel" variable, so that's unusable for me) going straight ahead unless hitting a cliff...

Testmap is for 1.31, but I can open latest version too. I use vjass-edition.

Edit: I only care about "straight cliffs". I.E. what's in the test map.
 

Attachments

  • ProjectileBounceTest.w3x
    58.9 KB · Views: 0
Last edited:
Level 3
Joined
Jun 8, 2020
Messages
18
Actually, you have an "OnCliff" event there, just use it to redirect the missile wherever you want to.
Use the right math and calculations to get the desired outcome.
 
It kind of feels like you didn't read my question.
I don't want my projectile to go to a point or unit, I want it to go in an angle and no example really handles this. I don't want it to have a Z angle, I want it to go "flat"
Also as you see in the code in the initial post, I have the onCliff with print, indicating I'm aware of its existance.

I had issues figuring out "normal" on cliffs, but I've got that part covered.
Currently I get the reflection as a 2d vector and need the "warcraft 3 angle" from it. (Unsure if my 2d vector is correct at this point)

I feel like I do something wrong in the Reflect right now.
Also I needed to modify some more internals of the system (how "finished" was handled).
I consider enabling write on angle in the system too, but I currently try to avoid internal modifications of systems...

My current version of the code:
JASS:
globals
    real tr_x
    real tr_y
endglobals
function Dot takes real x1, real y1, real x2, real y2 returns real
    return x1 * x2 + y1 * y2
endfunction
function Normalize takes real x, real y returns nothing
    local real size = SquareRoot(x * x + y * y)
    set tr_x = x / size
    set tr_y = y / size
endfunction
/**
 * rx, ry = ray. nx, ny = normal (must be normalized!!). Returns reflection ray in tr_x, tr_y
 */
function Reflect takes real rx, real ry, real nx, real ny returns nothing
    //nx + ny must be normalized! (I.E. size of it must be 1.0)
    local real dot = Dot(rx, ry, nx, ny)
    set tr_x = rx - 2.0 * dot * nx
    set tr_y = ry - 2.0 * dot * ny
endfunction
struct BounceTest extends Missiles
    real totalTraveled
    method onFinish takes nothing returns boolean
        //call BJDebugMsg("Finished")
        return totalTraveled + this.travel >= 4000.0
    endmethod
    method onHit takes unit u returns boolean
        if u != source and UnitAlive(u) and not BlzIsUnitInvulnerable(u) and not IsUnitAlly(u, owner) then
            call BJDebugMsg("Hit Enemy")
            return true
        endif 
        return false
    endmethod
    method onCliff takes nothing returns boolean
        local integer projectileCliffLevel = GetTerrainCliffLevel(x, y)
        local real testX = Cos(this.origin.angle)
        local real testY = Sin(this.origin.angle)
        local real newAngle
        local real cliffNormal
        local boolean isNS
        local boolean isEW
        local real normalX
        local real normalY
        set totalTraveled = totalTraveled + this.travel //this.travel is reset when deflected, need to track outselves!
        //Just visual indicator that we hit something
        call DestroyEffect(AddSpecialEffect("Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl", x, y))
        if testX > 0 then
            set testX = x + 25.0
            set normalX = -1.0
        else
            set testX = x - 25.0
            set normalX = 1.0
        endif
        if testY > 0 then
            set testY = y + 25.0
            set normalY = -1.0
        else
            set testY = y - 25.0
            set normalY = 1.0
        endif
        set isNS = projectileCliffLevel < GetTerrainCliffLevel(x, testY)
        if isNS then
            set normalX = 0.0
        else
            set normalY = 0.0
        endif
        call Normalize(nextX - x, nextY - y)
        call Reflect(tr_x, tr_y, normalX, normalY)
        set newAngle = Atan2(tr_x, tr_y)
        //call BJDebugMsg("NewAngle=" + R2S(newAngle))
        call this.deflect(x + 500.0 * Cos(newAngle), y + 500.0 * Sin(newAngle), 60.0)
        
        return false
    endmethod
    static method setupMissile takes unit c, real x, real y, real angle returns nothing
        local real     tx = x + 10.0 * Cos(angle)
        local real     ty = y + 10.0 * Sin(angle)
        local thistype this = thistype.create(x, y, 60.0, tx, ty, 60.0)
        set source    = c
        set model     = "Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl"
        set speed     = 1000.
        set collision = 32.
        set damage = 5.0
        set totalTraveled = 0.0
        set owner = GetOwningPlayer(c)
        call launch()
        set c = null
    endmethod
endstruct
function FireMissile takes unit u returns nothing
    call BounceTest.setupMissile(u, GetUnitX(u), GetUnitY(u), GetUnitFacing(u) * bj_DEGTORAD)
endfunction
 

Attachments

  • ProjectileBounceTest.w3x
    62.6 KB · Views: 0
I had a write-up for this a while ago:

You can scroll down to the section that is about "deflecting off cliffs". The issue, as you mentioned, is calculating which normal to use. Most projectile systems I've seen calculate the normal from colliding with an object by using the missile's position relative to the object's center position:
1746992388665.png

That works fine for units/destructables, but doesn't look too realistic for cliffs which have flat, square-ish sides. For those, I used a line intersection algorithm and I included a helper library/method in that sample map^ that you could try using (since you already have the cliff-collision detection and missile movement handled). You just need to provide it the point on the cliff tile where the missile is and the missile's facing:
JASS:
function GetDeflectionAngleOffCliff takes real missileX, real missileY, real missileFacingRadians returns real
    // ...
endfunction
1746992690291.png
1746992695344.png

Once you have the collision edge, you'll know which normal to use (and therefore, the deflection angle):
JASS:
        if intersectsLeft or intersectsRight then
            return bj_PI - angle
        elseif intersectsTop or intersectsBottom then
            return 2*bj_PI - angle
        endif

There are probably a bunch of ways to calculate it as well, but this method worked quite well for me. :)
 
...
JASS:
        if intersectsLeft or intersectsRight then
            return bj_PI - angle
        elseif intersectsTop or intersectsBottom then
            return 2*bj_PI - angle
        endif

There are probably a bunch of ways to calculate it as well, but this method worked quite well for me. :)
Thank you! This was the black magic math that I needed!

Note: This is NOT cleaned up, just proof of concept with a lot of unused code since the reflection part got resolved without 2d vectors :)
JASS:
globals
    real tr_x
    real tr_y
endglobals

function Dot takes real x1, real y1, real x2, real y2 returns real
    return x1 * x2 + y1 * y2
endfunction

function Normalize takes real x, real y returns nothing
    local real size = SquareRoot(x * x + y * y)
    set tr_x = x / size
    set tr_y = y / size
endfunction

/**
 * rx, ry = ray. nx, ny = normal (must be normalized!!). Returns reflection ray in tr_x, tr_y
 */
function Reflect takes real rx, real ry, real nx, real ny returns nothing
    //nx + ny must be normalized! (I.E. size of it must be 1.0)
    local real dot = Dot(rx, ry, nx, ny)
    set tr_x = rx - 2.0 * dot * nx
    set tr_y = ry - 2.0 * dot * ny
endfunction

struct BounceTest extends Missiles
    real totalTraveled

    method onFinish takes nothing returns boolean
        //call BJDebugMsg("Finished")
        return totalTraveled + this.travel >= 4000.0
    endmethod

    method onHit takes unit u returns boolean
        if u != source and UnitAlive(u) and not BlzIsUnitInvulnerable(u) and not IsUnitAlly(u, owner) then
            call BJDebugMsg("Hit Enemy")
            return true
        endif
        return false
    endmethod

    method onCliff takes nothing returns boolean
        local integer projectileCliffLevel = GetTerrainCliffLevel(x, y)
        local real testX = Cos(this.origin.angle)
        local real testY = Sin(this.origin.angle)
        local real newAngle
        local real cliffNormal
        local boolean isNS
        local boolean isEW
        local real normalX
        local real normalY
        local real rayX
        local real rayY

        set totalTraveled = totalTraveled + this.travel //this.travel is reset when deflected, need to track outselves!

        //Just visual indicator that we hit something
        call DestroyEffect(AddSpecialEffect("Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl", x, y))
        if testX > 0 then
            set testX = x + 25.0
            set normalX = -1.0
        else
            set testX = x - 25.0
            set normalX = 1.0
        endif
        if testY > 0 then
            set testY = y + 25.0
            set normalY = -1.0
        else
            set testY = y - 25.0
            set normalY = 1.0
        endif
        set isNS = projectileCliffLevel < GetTerrainCliffLevel(x, testY)
        if isNS then
            set normalX = 0.0
            set newAngle = 2*bj_PI - this.origin.angle
        else
            set normalY = 0.0
            set newAngle = bj_PI - this.origin.angle
        endif
        //call Normalize(nextX - x, nextY - y)
        //set rayX = tr_x
        //set rayY = tr_y
        //call Reflect(tr_x, tr_y, normalX, normalY)
        //set newAngle = Atan2(tr_x, tr_y)
        //set newAngle = Atan2(tr_x, tr_y)
        //call BJDebugMsg("NewAngle=" + R2S(newAngle))

        call this.deflect(x + 500.0 * Cos(newAngle), y + 500.0 * Sin(newAngle), 60.0)
        
        return false
    endmethod

    static method setupMissile takes unit c, real x, real y, real angle returns nothing
        local real     tx = x + 10.0 * Cos(angle)
        local real     ty = y + 10.0 * Sin(angle)
        local thistype this = thistype.create(x, y, 60.0, tx, ty, 60.0)

        set source    = c
        set model     = "Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl"
        set speed     = 1000.
        set collision = 32.
        set damage = 5.0
        set totalTraveled = 0.0
        set owner = GetOwningPlayer(c)

        call launch()

        set c = null
    endmethod
endstruct

function FireMissile takes unit u returns nothing
    call BounceTest.setupMissile(u, GetUnitX(u), GetUnitY(u), GetUnitFacing(u) * bj_DEGTORAD)
endfunction
 

Attachments

  • ProjectileBounceTest.w3x
    62.4 KB · Views: 0

Dr Super Good

Spell Reviewer
Level 65
Joined
Jan 18, 2005
Messages
27,289
The hard part with terrain based deflection is getting the terrain height as GetLocationZ is not safe for this application. Simply using cliff height makes the assumption a cliff is always higher than its surroundings, which might not be the case due to mesh height. Once you have the height of all terran nodes, you can calculate the terrain mesh triangles and their normals.

One approach might be to populate a custom data structure with terrain height, possibly using a custom written tool to convert the w3e file into array or table entries.
 
Top