• 🏆 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!
  • 🏆 Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! 📅 Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece!🔗 Click here to enter!

[Trigger] Math - Angle calculation problems

Status
Not open for further replies.
Level 1
Joined
Mar 30, 2016
Messages
4
I've been struggling with this custom spell I have been working on. It's supposed to let you target a location, and then a series of explosions will happen every half a second towards where I targeted until it has happened 15 times. There's also another trigger to change the target point after the spell has already been cast, so I can make it switch directions to chase a fleeing enemy. The problem comes calculating the angle before it chooses where the next detection point will be. Instead of taking time to change directions, it can turn full 180 on a dime. I added a couple variables so that it can remember the old angle and detection point, and use that to calculate the new one.

I'm awful at math (I failed more than once in highschool :goblin_cry:) and have no idea how to calculate the new angle! I just want it to be a smooth curve that loops back around to where I targeted instead of sharp sudden turns. Any insights would be greatly appreciated!

  • WaterDragon1 Init
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • WD_Init Equal to False
      • (Ability being cast) Equal to Water Dragon
    • Actions
      • Set WD_Init = True
      • Set WD_Caster = (Triggering unit)
      • Set WD_Target_Point = (Target point of ability being cast)
      • Set WD_Old_Angle = (Angle from (Position of WD_Caster) to WD_Target_Point)
      • Set WD_Player = (Owner of WD_Caster)
      • Set WD_Allies = (Units owned by WD_Player)
      • Set WD_Explosion_Count = 0.00
      • Set WD_Detection_Point = ((Position of WD_Caster) offset by WD_Config_JumpLength towards WD_Old_Angle degrees)
      • Set WD_Old_Detection_Point = WD_Detection_Point
      • Special Effect - Create a special effect at WD_Detection_Point using Objects\Spawnmodels\Naga\NagaDeath\NagaDeath.mdl
      • Special Effect - Destroy (Last created special effect)
      • Unit Group - Add all units of (Units within WD_Config_Radius of WD_Detection_Point) to WD_Targets
      • Unit Group - Pick every unit in WD_Allies and do (Unit Group - Remove (Picked unit) from WD_Targets)
      • Unit Group - Pick every unit in WD_Targets and do (Unit - Cause WD_Caster to damage (Picked unit), dealing WD_Config_Damage damage of attack type Spells and damage type Normal)
      • Unit Group - Remove all units from WD_Targets

  • WaterDragon2 Main
    • Events
      • Time - Every 0.50 seconds of game time
    • Conditions
      • WD_Init Equal to True
    • Actions
      • Set WD_Angle = (Angle from WD_Detection_Point to WD_Target_Point)
      • Set WD_Angle = (WD_Angle + WD_Old_Angle)
      • Set WD_Angle = (WD_Angle / 2.00)
      • Set WD_Detection_Point = (WD_Detection_Point offset by WD_Config_JumpLength towards WD_Angle degrees)
      • Special Effect - Create a special effect at WD_Detection_Point using Objects\Spawnmodels\Naga\NagaDeath\NagaDeath.mdl
      • Special Effect - Destroy (Last created special effect)
      • Unit Group - Add all units of (Units within WD_Config_Radius of WD_Detection_Point) to WD_Targets
      • Unit Group - Pick every unit in WD_Allies and do (Unit Group - Remove (Picked unit) from WD_Targets)
      • Unit Group - Pick every unit in WD_Targets and do (Unit - Cause WD_Caster to damage (Picked unit), dealing WD_Config_Damage damage of attack type Spells and damage type Normal)
      • Unit Group - Remove all units from WD_Targets
      • Set WD_Explosion_Count = (WD_Explosion_Count + 1.00)
      • Set WD_Old_Angle = (Angle from WD_Old_Detection_Point to WD_Detection_Point)
      • Set WD_Old_Detection_Point = WD_Detection_Point
      • If (WD_Explosion_Count Greater than or equal to WD_Config_Max_Explosions) then do (Set WD_Init = False) else do (Do nothing)
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
This is what I use to exactly do what you want:
JASS:
        public method applyTargeting takes Missile m, real x, real y returns nothing
            local real targetX = GetUnitX(.target)
            local real targetY = GetUnitY(.target)
            local real angle = DenormalizeAngleRad(AngleBetweenCoordinatesRad(x, y, targetX, targetY) - m.angle)
            
            if angle > m.turnRate then
                set angle = m.turnRate
            elseif angle < -m.turnRate then
                set angle = -m.turnRate
            endif
            call m.setAngle(m.angle + angle)
            
            if m.isFlying then
                //irrelevant
            endif
            
        endmethod

What I basically do is find the location where the missile (explosion) is now (x, y) and find the location where it should be going to (targetX, targetY).
Then I calculate the angle between those two positions (My function is AngleBetweenCoordinates(), but you can use "Angle Between Points" in GUI) and substract the current angle of the missile from it.

Then I, what I call, de-normalize the angle (so it will be between -180 and 180... or -PI and PI or -0.5 and 0.5, etc, etc, etc.).
Then I check if the result to what I want to limit it to (for example, it may not turn faster than 20 degrees per interval which in your case would be 40 degrees per second).
If it is higher, then it should be the limit, if it is lower, then it should be the minus limit.

Then I add the final result to the current angle of the missile, which makes it gradually rotate to the final angle.

In GUI as a complete calculation, you would end up with this:
  • Actions
    • Set MissileLocation = (Position of MISSILE)
    • Set TargetLocation = (Position of TARGET)
    • Set Angle = ((Angle from MissileLocation to TargetLocation) - AngleOfMissile)
    • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
      • If - Conditions
        • Angle Greater than 180.00
      • Then - Actions
        • Set Angle = (Angle - 360.00)
      • Else - Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • Angle Less than -180.00
          • Then - Actions
            • Set Angle = (Angle + 360.00)
          • Else - Actions
    • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
      • If - Conditions
        • Angle Greater than 20.00
      • Then - Actions
        • Set Angle = 20.00
      • Else - Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • Angle Less than -20.00
          • Then - Actions
            • Set Angle = -20.00
          • Else - Actions
    • Set AngleOfMissile = (AngleOfMissile + Angle)
    • Custom script: call RemoveLocation(udg_MissileLocation)
    • Custom script: call RemoveLocation(udg_TargetLocation)
 
Level 1
Joined
Mar 30, 2016
Messages
4
Thanks for the reply! I tried to re-create what you showed me, but I got a little confused again when it didn't work how I had hoped. In your GUI example, what exactly is the difference between AngleOfMissle and Angle? And what are the equivalents (if there are any) between those two variables and the variables in my own trigger?
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Angle is simply a variable which you will use to calculate the new angle with.
AngleOfMissile is like the actual angle of the missile... the direction it should be going to.
WD_Angle would be the equivalent of AngleOfMissile.

Also, you are leaking some data on a 0.5s interval... which is going to hurt your map's performance after some time of playing.
You should search some stuff about group and location leaks.

Also, you should use multiple actions in "If/Then/Else" and "Unit Group - Loop" actions to make your trigger more readable, editable and copyable (to paste in other triggers).
 
Hello @Wietlol
I'v check your code for a move caster to target simple spell and I got errors. Maybe I badly used your snippets

here is my code in your example it is 20 degrees rotations as degerees? radians ? and for what period?

secondly for what reason do you de-normalize diffAngle "here in my code"

JASS:
scope PalladinCharge
  globals
    private constant real INTERVAL = 0.05
    private constant real SPEED = 850

    private constant real MAX_ANGLE_DEG = 40

    private constant real PI = 3.141592

    private real PI2
    private real STEP
    private real MAX_ANGLE_RAD

  endglobals

private function denormalizeAngleRad takes real A returns real
  if A > PI then
    set A = A - PI2
  elseif A < PI then
    set A = A + PI2
  endif
  return A
endfunction

private struct Data
  static Data array D
  static integer DT = 0
  static timer T = null

  unit caster
  unit target
  real x
  real y
  real angle
  boolean done

  private method onDestroy takes nothing returns nothing
    call disableUnit(.caster, false)

    set .caster = null
    set .target = null

  endmethod

  public method applyTargeting takes nothing returns nothing
    local real targetX = GetUnitX(.target)
    local real targetY = GetUnitY(.target)
    local real diffAngle = denormalizeAngleRad(Atan2(targetY-.y, targetX-.x) - .angle)

    if diffAngle > MAX_ANGLE_RAD then
      set diffAngle = MAX_ANGLE_RAD
    elseif diffAngle < -MAX_ANGLE_RAD then
      set diffAngle = -MAX_ANGLE_RAD
    endif

    set .angle = (.angle + diffAngle)

  endmethod

  private method moveCaster takes nothing returns nothing
    local real X = GetUnitX(.target)
    local real Y = GetUnitY(.target)
    // local real A = Atan2(Y-.y, X-.x)

    call .applyTargeting()

    set .x = .x + (Cos(.angle) * STEP)
    set .y = .y + (Sin(.angle) * STEP)

    call SetUnitPosition(.caster, .x, .y)

    if distancePoints(.x, .y, X, Y) <= STEP then
      set .done = true
    endif

  endmethod

  static method update takes nothing returns nothing
    local Data d
    local integer I = 0

    loop
      set I = I + 1
      set d = .D[I]

      call d.moveCaster()

      if d.done then
        call d.destroy()

        set .D[I] = .D[.DT]
        set .DT = .DT - 1
        set I = I - 1
      endif

      exitwhen I >= .DT
    endloop

    if .DT <= 0 then
      call PauseTimer(.T)
      set .DT = 0
    endif

  endmethod

  static method addData takes unit U, unit U1, real X, real Y, real A returns nothing
      local Data d = Data.allocate()

    set d.caster = U
    set d.target = U1
    set d.x = X
    set d.y = Y
    set d.angle = A
    set d.done = false

    call disableUnit(U, true)

    set .DT = .DT + 1
    set .D[.DT] = d

    if .DT == 1 then
      call TimerStart(.T, INTERVAL, true, function Data.update)
    endif

  endmethod
endstruct

function Trig_PalladinCharge_Conditions takes nothing returns boolean
  return GetSpellAbilityId() == 'A000'
endfunction

function Trig_PalladinCharge_Actions takes nothing returns nothing
  local unit U = GetTriggerUnit()
  local unit U1 = GetSpellTargetUnit()
  local real X = GetUnitX(U)
  local real Y = GetUnitY(U)
  local real X1 = GetUnitX(U1)
  local real Y1 = GetUnitY(U1)

  call Data.addData(U, U1, X, Y, Atan2(Y1-Y, X1-X))

  set U = null
  set U1 = null

endfunction

//===========================================================================
function InitTrig_PalladinCharge takes nothing returns nothing
  set gg_trg_PalladinCharge = CreateTrigger(  )
  call TriggerRegisterAnyUnitEventBJ( gg_trg_PalladinCharge, EVENT_PLAYER_UNIT_SPELL_EFFECT )
  call TriggerAddCondition( gg_trg_PalladinCharge, Condition( function Trig_PalladinCharge_Conditions ) )
  call TriggerAddAction( gg_trg_PalladinCharge, function Trig_PalladinCharge_Actions )

  set STEP = SPEED * INTERVAL
  set MAX_ANGLE_RAD = Deg2Rad(MAX_ANGLE_DEG) * INTERVAL
  set Data.T = CreateTimer()
  set PI2 = 2 * PI

endfunction

endscope

Thank you for your future answer
 
Last edited:
Status
Not open for further replies.
Top