[feature request] Rotation tweens should move in the direction of the shortest distance #2494

Closed
ForgeableSum opened this Issue May 21, 2016 · 15 comments

Projects

None yet

4 participants

@ForgeableSum

Suppose I have a tank and I want to turn it to a specific angle using a rotation tween. The problem with the tween system now is that it only adds or subtracts a value to achieve the target value, but that simply logic doesn't work when you are dealing with angles. The function below will tell you wether adding to the rotation is the shortest direction to turn in to achieve a target angle. Given a current and target angle, when returning true, the tween manager should add, when returning false, it should subtract. That way you don't have the tank do an almost 360 spin to turn less than 10 degrees! Hopefully this code can be implemented in core as I think it is intuitive the tweening should work this way.

function addIsShortestDistanceToRotation(rotationAt, rotationGoing) {

    rotationAt = Phaser.Math.normalizeAngle(rotationAt);
    rotationGoing = Phaser.Math.normalizeAngle(rotationGoing);

    if ((rotationAt < rotationGoing) && ((rotationGoing - rotationAt) <= Math.PI)) {
        return true;
    }

    if ((rotationAt < rotationGoing) && ((rotationGoing - rotationAt) > Math.PI)) {
        return false;
    }

    if ((rotationAt > rotationGoing) && ((rotationAt - rotationGoing) <= Math.PI)) {
        return false;
    }

    if ((rotationAt > rotationGoing) && ((rotationAt - rotationGoing) > Math.PI)) {
        return true;
    }

}
@ForgeableSum

I wonder if anyone can assist with a hack or workaround. I'd really like to make use of the tween system instead of having to manually tween all my rotations (with a simple +/- angle in updates), which looks horrible without easing and all the other neat features the tween system has.

Is there an easy way for me to implement this into core? I've scoured the tween code and couldn't find where something like adding/subtracting the properties of the tween target takes place. I'm running 2.4.7 btw.

@ForgeableSum

Just a reminder. I would really really really love to have this. :)

I am currently doing manually tweens on sprites to get the rotation going the shortest distance and it is extremely problematic, especially over a network (now I need to communicate the direction of the rotation over the network). I miss simply tweening rotations/angles. It was so easy and the product was beautiful!

@hilts-vaughan
Contributor
hilts-vaughan commented Jul 25, 2016 edited

You can do this yourself, even if you need to hide it behind some abstraction. Since we know angles can wrap and be negative, we can do ourselves a favour and use some math to solve this problem...

var tween = game.add.tween(sprite).to({rotation: -(3.14*2 - (3.14*1.5))}, 2000, "Linear", true, 0, true)

is the same as

var tween = game.add.tween(sprite).to({rotation: (3.14*2 - (3.14*1.5))}, 2000, "Linear", true, 0, true)

Except the latter takes the longer route. :) Replace 3.14 w/ Pi and the number multiplied by 1.5 with your "target". You can use some basic math to figure out which is shorter. If the absolute distance is better to turn in one direction, use the positive sign. Otherwise, use the negative sign.

Does this help? You can hide this behind a TweenFactory of some sort... not sure if Phaser needs baked in support for something so simple, but if Rich is interested, I can draft a PR that is somewhat generic.

Using your method above BTW, you can simply created a TweenFactory with a createTweenWithShortestRotationTo(currentRotation, targetRotation and then just return the proper tween. Nice and simple. :)

@ForgeableSum

Oh wow, thanks! I will try your solutions and report back.

@hilts-vaughan
Contributor

Great. Let me know if you have issues, I tested it and it works fine.

@ForgeableSum
ForgeableSum commented Jul 28, 2016 edited

I'm having difficulty understanding how to implement your solution.

Suppose I have a sprite with rotation at 0. I want to tween its rotation to 1.5:

var rotation = 1.5; 
        var turnNegative = -(Math.PI * 2 - (Math.PI * rotation));
        var turnPositive = (Math.PI * 2 - (Math.PI * rotation));
        var diffTurnNegative = Math.abs(gameObject.rotation - turnNegative);
        var diffTurnPos = Math.abs(gameObject.rotation - turnPositive);
        if (diffTurnPos > diffTurnNegative) {
            if (rotation > 0) {
                rotation = -rotation;
            }

        } else {
            if (rotation < 0) {

                rotation = rotation * -1;
            }

        }

        var moveTween = game.add.tween(gameObject).to({
            'rotation': rotation
        }, time, Phaser.Easing.Linear.None, true);

What am I doing wrong here? I really just don't understand it. Compare the absolute distance of what? Use positive/negative signs on what, the rotation target? I guess it would make much more sense to me if I saw a complete example. Thanks!

@halilcakarr
halilcakarr commented Jul 28, 2016 edited

i might be thinking wrong but probably your errors coming from here :

   `var rotation = 1.5; 
    var turnNegative = -(Math.PI * 2 - (Math.PI * rotation));
    var turnPositive = (Math.PI * 2 - (Math.PI * rotation));
    var diffTurnNegative = Math.abs(gameObject.rotation - turnNegative);
    var diffTurnPos = Math.abs(gameObject.rotation - turnPositive);
    if (diffTurnPos > diffTurnNegative) {
        if (rotation > 0) {
            rotation = -rotation;
        }
        // there isnt an else statment.. what if rotation < 0 and then  what  is gonna happen? 
    } else {
        if (rotation < 0) {

            rotation = rotation * -1;
        }
        // same thing what if rotation > 0 ..  
    }

    var moveTween = game.add.tween(gameObject).to({
        'rotation': rotation
    }, time, Phaser.Easing.Linear.None, true);

`

Please try to add else statments and give it a try
Hope this will fix your problem

@ForgeableSum
ForgeableSum commented Jul 28, 2016 edited

If the rotation is smaller than 0 then the rotation is already negative; therefore, the sign doesn't need to change. This is all based on my interpretation of hilts-vaughan's logic, but I don't understand the math.

@halilcakarr
halilcakarr commented Jul 28, 2016 edited

Let's deal with the math then and see the result:
`var rotation = 1.5;
var turnNegative = -1.57; -((3.14*2) - (3.14 *1.5)) = -(6.28 - 4.71) = -1.57;
var turnPositive = +1.57 //same math ^-^

var diffTurnNegative = Math.abs(gameObject.rotation - turnNegative);
Okey now here I think at the first place you gameObject.rotation value is prbably zero then

var diffTurnNegative = +1.57; (Math.abs(0 - (-1.57)); which gives you a positive 1.57)
var diffTurnPos = Math.abs(gameObject.rotation - turnPositive);
Okey this one also gives you a positive +1.57 cause of Math.abs() function. So
var diffTurnPos = +1.57

Look at if statemants, you will see that you are going into else statment for these results,
and ckecking your rotation is smaller then 0. Guess what.. It's not.

At the and your moveTween variable give the rotation value of 1.5.

Hope you will figure out rest :)

@ForgeableSum

I've tried writing it like this:


        var turnNegative = -(Math.PI * 2 - (Math.PI * rotation));
        var turnPositive = (Math.PI * 2 - (Math.PI * rotation));
        var diffTurnNegative = Math.abs(gameObject.rotation - turnNegative);
        var diffTurnPos = Math.abs(gameObject.rotation - turnPositive);
        if (diffTurnPos < diffTurnNegative) {
                rotation = turnPositive;

        } else {
                rotation = turnNegative;
        }

        var moveTween = game.add.tween(gameObject).to({
            'rotation': rotation
        }, time, Phaser.Easing.Linear.None, true);

No luck.

@hilts-vaughan
Contributor
hilts-vaughan commented Jul 29, 2016 edited

Hi,

I'll write a Phaser sandbox example. :) I think I explained the "multiply" poorly. See, you do not need to multiply by these values. I only did that because it allowed me to think in terms of "full circles"

0.5 = quarter circle
1 = half circle
1.5 = 3/4 circle

etc... see the Phaser sandbox example below for a corrected algorithm.

@hilts-vaughan
Contributor
hilts-vaughan commented Jul 29, 2016 edited

From one developer to another. <3

I've only tested it from "rotation 0" but it should work fine.. I think? The if might need tweaking if that's the case, but should be O.K. Let me know. There is probably an easier way but it is late and I am tired. Good luck. :)

Pay it forward (http://phaser.io/sandbox/edit/zyzPzRcM)

Don't understand the math and want to? Send me an e-mail, we can talk.

@hilts-vaughan
Contributor

Oh, and of course, hide this ugliness in another class somewhere. ProperRotationTweenFactory might do the trick... or if Rich wants, I can submit a PR w/ the crude math. :)

@photonstorm photonstorm added a commit that referenced this issue Jul 29, 2016
@photonstorm Math.getShortestAngle will return the shortest angle between the two …
…given angles. Angles are in the range -180 to 180, which is what `Sprite.angle` uses. So you can happily feed this method two sprite angles, and get the shortest angle back between them (#2494)
688752c
@photonstorm
Owner

Here's my version :)

    /**
    * Gets the shortest angle between `angle1` and `angle2`.
    * Both angles must be in the range -180 to 180, which is the same clamped
    * range that `sprite.angle` uses, so you can pass in two sprite angles to
    * this method, and get the shortest angle back between the two of them.
    *
    * The angle returned will be in the same range. If the returned angle is
    * less than 0 then it's a counter-clockwise rotation, if >= 0 then it's
    * a clockwise rotation.
    * 
    * @method Phaser.Math#getShortestAngle
    * @param {number} angle1 - The first angle. In the range -180 to 180.
    * @param {number} angle2 - The second angle. In the range -180 to 180.
    * @return {number} The shortest angle, in degrees. If less than zero it's counter-clockwise, otherwise clockwise.
    */
    getShortestAngle: function (angle1, angle2) {

        var difference = angle2 - angle1;
        var times = Math.floor((difference - (-180)) / 360);

        return (difference - (times * 360)) * -1;

    },

Here's a full example (you can find the assets in the Phaser Examples repo)

var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create });

function preload() {

    game.load.image('arrow', 'assets/sprites/longarrow.png');
    game.load.image('lemming', 'assets/sprites/lemming.png');

}

var arrow;
var arrow2;
var lemming;

function create() {

    game.stage.backgroundColor = '#000000';

    arrow = game.add.sprite(game.world.centerX, game.world.centerY, 'arrow');
    arrow.anchor.set(0, 0.5);
    // arrow.tint = 0xff0000;

    arrow2 = game.add.sprite(game.world.centerX, game.world.centerY, 'arrow');
    arrow2.anchor.set(0, 0.5);

    lemming = game.add.sprite(game.world.randomX, game.world.randomY, 'lemming');
    lemming.anchor.set(0.5);

    setNewLocation();

    game.input.onDown.add(setNewLocation, this);

}

function setNewLocation () {

    arrow2.angle = arrow.angle;

    lemming.x = game.world.randomX;
    lemming.y = game.world.randomY;

    var angleTo = Phaser.Math.radToDeg(arrow.position.angle(lemming.position));

    var shortestAngle = game.math.getShortestAngle(angleTo, arrow.angle);

    var newAngle = arrow.angle + shortestAngle;

    game.add.tween(arrow).to({ angle: newAngle }, 3000, 'Linear', true);

}
@ForgeableSum
ForgeableSum commented Jul 29, 2016 edited

Wooooooooooot!

I just tested it now and works like a charm. Here is my example with Richard's method (note that I simply copied his function because it only exists in the dev repo now):

function getShortestAngle(angle1, angle2) {

        var difference = angle2 - angle1;
        var times = Math.floor((difference - (-180)) / 360);

        return (difference - (times * 360)) * -1;

}

        var shortestAngle = getShortestAngle(Phaser.Math.radToDeg(newRotation), gameObject.angle);
        var newAngle = gameObject.angle + shortestAngle; 

        var moveTween = game.add.tween(gameObject).to({
            'angle': newAngle
        }, time, Phaser.Easing.Linear.None, true);

Thanks everyone and I'm so glad to get this issue worked out. It's been plaguing me for a while now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment