Using a rotation transition does not work correctly when going from 180 to -180 degrees.

Currently there is a player object that rotates on a pivot depending on where you tap on the screen. This works correctly and it will choose the correct angle to transition to.

I have set it so the top half of the players rotational value goes from 0 to -180 and the bottom half 0 to 180. This results in the left side having a jump from 180 to -180, therefore the player object moves all the way around its pivot.

I have also tested it with having the bottom half go from 0 - 180 and then it continues on the top half going from 181 - 360 degrees. This results in the same issue but on the right side where the angle goes from 360 to 0 degrees.

I’m wondering if there is a decent way to fix this problem are it’s visually quite annoying. Thanks  :smiley:

I took a gyazo gif screenshot showing a visual example of my problem:

https://gyazo.com/7635a8347758cb4e260be6d4a8021232

Below is my code which is responsible for handling the rotation of the player object. It’s a mixture of my own code and bits that I’ve read from other forum posts.

[lua]local function getImageRotation(x1,y1,x2,y2)
  local PI = 3.14159265358
  local deltaY = y2 - y1
  local deltaX = x2 - x1

  local angleInDegrees = (math.atan2( deltaY, deltaX) * 180 / PI) * -1

  local mult = 10^0

  return math.floor(angleInDegrees * mult + 0.5) / mult
end


local function onTravel(mouseX, mouseY)
  local function Flag()
    travelFlag = true

    --Fires a bullet in the direction of the player facing
    fireBullet()
  end

  if travelFlag then
    local newAngle = (getImageRotation(Player.pGroup.x,Player.pGroup.y,mouseX,mouseY))*-1
    print("Angle = " … newAngle)

    travelFlag = false
    transition.to( Player.pGroup, { time=500, rotation = newAngle, onComplete=Flag } )
  end
end


local function beginRotate( event )
  local phase = event.phase
  local t = event.target

  if phase == “began” then
    onTravel(event.x, event.y)
  end
end[/lua]

The trick to solving this is to ensure that:
 

  1. The transition’s rotation delta < 180 degree in total
  2. The object’s rotation is normalized in such a way to ensure the target angle is left or right such that transition travels the shortest distance.
     
    Tip: I’ve solved this a few times before so it is doable, but I don’t have time right now to dig up my solution.  
    You can solve this  on your own I am sure, or hire a hitman.
     
     
     
    Shortest distance examples
  3. Current angle 0 desired angle 270 --> transition to angle: -90
     
  4. Current angle -90 desired angle 180 --> transition to angle: -180
     
  5. Current angle -180 desired angle 45 --> Normalized object angle to 180, then transition to angle: 45

Hi, I had this issue but I think i solved it by making sure both start and stop number was on the same side of 0.

I cant dig up the code right now but remember angles can be anything, 2540 deg if you want, its all a sping on the 360 scale.

I have come up with a solution to this issue. It might not be the most elegant solution but seems to work quite well.

Basically i check if the angle that has rotated is greater than 180 degrees which means it won’t be taking the shortest route. Then I have an if, else statement which checks if the angle is positive or negative.

If it’s negative I set the current rotation of the object to 360 minus the absolute value of the rotation which mimics the number being a positive value greater than 180.

If it’s positive I do something similar but with -360 plus the object rotation.

Below is the new onTravel function with the checking in place. Also here is a new gyazo gif showing it working.

https://gyazo.com/8760e99da565b79b93b15202aa37f1b3

[lua]local function onTravel(mouseX, mouseY)
  local function Flag()
    travelFlag = true

    --Fires a bullet in the direction of the player facing
    fireBullet()
  end

  if travelFlag then
    local prevAngle = Player.pGroup.rotation
    local newAngle = (getImageRotation(Player.pGroup.x, Player.pGroup.y, mouseX, mouseY)) * -1

    local difference = math.abs(prevAngle - newAngle)

    if difference > 180 then
      if Player.pGroup.rotation <= 0 then
        Player.pGroup.rotation = 360 - math.abs(Player.pGroup.rotation)
      else
        Player.pGroup.rotation = -360 + Player.pGroup.rotation
      end
    end

    print("Angle = " … newAngle)

    travelFlag = false
    transition.to( Player.pGroup, { time=500, rotation = newAngle, onComplete=Flag } )
  end
end[/lua]

Same idea packaged as a function:

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2017/07/shortestPathTransition.zip

local function shortestPathTransition( obj, targetAngle, dps, myEasing ) targetAngle = targetAngle or 0 local dps = dps or 0 dps = dps/1000 -- degrees per second local myEasing = myEasing or easing.linear -- Instant Turn if(dps \< 0 ) then obj.rotation = targetAngle -- Timed Turn else -- Normalize angles while( obj.rotation \< 0 ) do obj.rotation = obj.rotation + 360 end while( obj.rotation \>= 360 ) do obj.rotation = obj.rotation - 360 end while( targetAngle \< 0 ) do targetAngle = targetAngle + 360 end while( targetAngle \>= 360 ) do targetAngle = obj.rotation - 360 end -- Stop any prior transitions on this object transition.cancel( obj ) -- Calc initial tween angle local tweenAngle = obj.rotation - targetAngle -- Adjust to shortest path if(tweenAngle \>= 180) then targetAngle = targetAngle + 360 tweenAngle = targetAngle - obj.rotation elseif(tweenAngle \<= -180) then targetAngle = targetAngle - 360 tweenAngle = targetAngle - obj.rotation end -- Calc rotation time local rotateTime = math.abs(math.floor(tweenAngle / dps)) -- Transition transition.to( obj, { rotation = targetAngle, time = rotateTime, transition = myEasing } ) end obj.targetAngle = targetAngle end -- -- Test it -- local arrow = display.newImageRect( "arrow.png", 80, 80 ) arrow.targetAngle = 0 arrow.x = display.contentCenterX arrow.y = display.contentCenterY local label = display.newText( "TBD", arrow.x, arrow.y + 200 ) function label.enterFrame( self ) self.text = string.format("Arrow Rotation: %d Rotation Target: %d", arrow.rotation, arrow.targetAngle) end Runtime:addEventListener("enterFrame",label) local function test() local targetAngle = math.random( -360, 360 ) shortestPathTransition( arrow, targetAngle, 360 ) timer.performWithDelay( 1500, test ) end test()

The trick to solving this is to ensure that:
 

  1. The transition’s rotation delta < 180 degree in total
  2. The object’s rotation is normalized in such a way to ensure the target angle is left or right such that transition travels the shortest distance.
     
    Tip: I’ve solved this a few times before so it is doable, but I don’t have time right now to dig up my solution.  
    You can solve this  on your own I am sure, or hire a hitman.
     
     
     
    Shortest distance examples
  3. Current angle 0 desired angle 270 --> transition to angle: -90
     
  4. Current angle -90 desired angle 180 --> transition to angle: -180
     
  5. Current angle -180 desired angle 45 --> Normalized object angle to 180, then transition to angle: 45

Hi, I had this issue but I think i solved it by making sure both start and stop number was on the same side of 0.

I cant dig up the code right now but remember angles can be anything, 2540 deg if you want, its all a sping on the 360 scale.

I have come up with a solution to this issue. It might not be the most elegant solution but seems to work quite well.

Basically i check if the angle that has rotated is greater than 180 degrees which means it won’t be taking the shortest route. Then I have an if, else statement which checks if the angle is positive or negative.

If it’s negative I set the current rotation of the object to 360 minus the absolute value of the rotation which mimics the number being a positive value greater than 180.

If it’s positive I do something similar but with -360 plus the object rotation.

Below is the new onTravel function with the checking in place. Also here is a new gyazo gif showing it working.

https://gyazo.com/8760e99da565b79b93b15202aa37f1b3

[lua]local function onTravel(mouseX, mouseY)
  local function Flag()
    travelFlag = true

    --Fires a bullet in the direction of the player facing
    fireBullet()
  end

  if travelFlag then
    local prevAngle = Player.pGroup.rotation
    local newAngle = (getImageRotation(Player.pGroup.x, Player.pGroup.y, mouseX, mouseY)) * -1

    local difference = math.abs(prevAngle - newAngle)

    if difference > 180 then
      if Player.pGroup.rotation <= 0 then
        Player.pGroup.rotation = 360 - math.abs(Player.pGroup.rotation)
      else
        Player.pGroup.rotation = -360 + Player.pGroup.rotation
      end
    end

    print("Angle = " … newAngle)

    travelFlag = false
    transition.to( Player.pGroup, { time=500, rotation = newAngle, onComplete=Flag } )
  end
end[/lua]

Same idea packaged as a function:

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2017/07/shortestPathTransition.zip

local function shortestPathTransition( obj, targetAngle, dps, myEasing ) targetAngle = targetAngle or 0 local dps = dps or 0 dps = dps/1000 -- degrees per second local myEasing = myEasing or easing.linear -- Instant Turn if(dps \< 0 ) then obj.rotation = targetAngle -- Timed Turn else -- Normalize angles while( obj.rotation \< 0 ) do obj.rotation = obj.rotation + 360 end while( obj.rotation \>= 360 ) do obj.rotation = obj.rotation - 360 end while( targetAngle \< 0 ) do targetAngle = targetAngle + 360 end while( targetAngle \>= 360 ) do targetAngle = obj.rotation - 360 end -- Stop any prior transitions on this object transition.cancel( obj ) -- Calc initial tween angle local tweenAngle = obj.rotation - targetAngle -- Adjust to shortest path if(tweenAngle \>= 180) then targetAngle = targetAngle + 360 tweenAngle = targetAngle - obj.rotation elseif(tweenAngle \<= -180) then targetAngle = targetAngle - 360 tweenAngle = targetAngle - obj.rotation end -- Calc rotation time local rotateTime = math.abs(math.floor(tweenAngle / dps)) -- Transition transition.to( obj, { rotation = targetAngle, time = rotateTime, transition = myEasing } ) end obj.targetAngle = targetAngle end -- -- Test it -- local arrow = display.newImageRect( "arrow.png", 80, 80 ) arrow.targetAngle = 0 arrow.x = display.contentCenterX arrow.y = display.contentCenterY local label = display.newText( "TBD", arrow.x, arrow.y + 200 ) function label.enterFrame( self ) self.text = string.format("Arrow Rotation: %d Rotation Target: %d", arrow.rotation, arrow.targetAngle) end Runtime:addEventListener("enterFrame",label) local function test() local targetAngle = math.random( -360, 360 ) shortestPathTransition( arrow, targetAngle, 360 ) timer.performWithDelay( 1500, test ) end test()