Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm developing a 2D sprite-based game, and I'm finding that I'm having trouble with making the sprites rotate correctly. In a nutshell, I've got spritesheets for each of 5 directions (the other 3 come from just flipping the sprite horizontally), and I need to clamp the velocity/rotation of the sprite to one of those directions. My sprite class has a pre-computed list of radians corresponding to the cardinal directions like this:

protected readonly List<float> CardinalDirections = new List<float>
                           {
                               MathHelper.PiOver4,
                               MathHelper.PiOver2,
                               MathHelper.PiOver2 + MathHelper.PiOver4,
                               MathHelper.Pi, 
                               -MathHelper.PiOver4,
                               -MathHelper.PiOver2,
                               -MathHelper.PiOver2 + -MathHelper.PiOver4,
                               -MathHelper.Pi,
                           };

Here's the positional update code:

if (velocity == Vector2.Zero)
            return;

        var rot = ((float)Math.Atan2(velocity.Y, velocity.X));
        TurretRotation = SnapPositionToGrid(rot);

        var snappedX = (float)Math.Cos(TurretRotation);
        var snappedY = (float)Math.Sin(TurretRotation);
        var rotVector = new Vector2(snappedX, snappedY);

        velocity *= rotVector;
        //...snip

private float SnapPositionToGrid(float rotationToSnap)
    {
        if (rotationToSnap == 0)
            return 0.0f;

        var targetRotation = CardinalDirections.First(x => 
                (x - rotationToSnap >= -0.01 && x - rotationToSnap <= 0.01));
        return (float)Math.Round(targetRotation, 3);

    }

What am I doing wrong here? I know that the SnapPositionToGrid method is far from what it needs to be - the .First(..) call is on purpose so that it throws on no match, but I have no idea how I would go about accomplishing this, and unfortunately, Google hasn't helped too much either. Am I thinking about this the wrong way, or is the answer staring at me in the face?

EDIT: Thanks Gregory for the answer. Only thing I really had to modify from the answer was to re-construct the velocity vector to the new snapped rotation (I'm sure there are further optimizations that I can do, but for now it's enough that it works!):

        var snappedX = (float)Math.Round(Math.Cos(TurretRotation), 3) * 
                           velocity.Length();
        var snappedY = (float)Math.Round(Math.Sin(TurretRotation), 3) *
                           velocity.Length();
        var rotVector = new Vector2(snappedX, snappedY);

FOLLOW-UP EDIT: The initial answer worked very well as a naive implementation, but suffered from inaccuracies of up to 45 degrees when dealing with negative angles; -3.14 snapped to -2.356 for instance. To correct that, I added a check to account for those situations. Below is a complete implementation of a SnapToGrid method. Some optimizations have been made, like pre-computing the various divisors of Pi to avoid the division. Further optimization could possibly be achieved by looking for short-circuit scenarios, i.e. if rotationToSnap ~=0, but if anyone has further optimizations, I'd be curious to see them!

public static float SnapPositionToGrid(float rotationToSnap)
{
    if (rotationToSnap == 0)
        return 0.0f;

    var modRot = rotationToSnap  % MathHelper.PiOver4;

    float finalRot; 

    if (modRot < RoundedPiOver8)
    {
        if (modRot < -RoundedPiOver8)
            finalRot = rotationToSnap + -MathHelper.PiOver4 - modRot;
        else
            finalRot = rotationToSnap - modRot;
    }
    else
        finalRot = rotationToSnap + MathHelper.PiOver4 - modRot;

    return (float)Math.Round(finalRot, 3);
}
share|improve this question
add comment

2 Answers

up vote 6 down vote accepted

I'd do this mathematically, assuming I understand your question. You want your rotation to be an integer multiple of PiOver4, right? So divide by your direction increment, round to the nearest integer, and multiply by the increment again. An unoptimized version would be:

return PiOver4 * (float) Math.Round(rotationToSnap / PiOver4);

I'm not too familiar with optimization in XNA; if modulos and comparisons are cheaper than multiplications and divisions (as I suspect), it'll be faster to do something like:

var modRot = rotationToSnap % PiOver4;
if(modRot < PiOver8) return rotationToSnap - modRot;
else return rotationToSnap + PiOver4 - modRot;

I haven't tested that code or anything, but it or a similar approach should work.

share|improve this answer
    
Thanks Gregory - your answer worked beautifully! –  Josh E Jan 12 '11 at 20:51
2  
Anyone reading this answer later - Gregory's answer is a great approach, but can be very inaccurate (up to 45d/0.785r!) when dealing with negative angles. Please see my edit for a corrected, semi-optimized answer –  Josh E Jan 27 '11 at 15:46
    
You're totally right; modulos don't work as you'd intuitively expect when dealing with negative numbers. Sorry for the omission, and I'm glad you found it! –  Gregory Avery-Weir Jan 27 '11 at 21:33
1  
If you're calling Math.Round anyway, I would expect the first algorithm to be faster, and you wouldn't have to deal with the negative modulo crap at all. –  user744 Jan 27 '11 at 22:45
add comment

Gregory's answer can be amended to handle negative rotation by just wrapping and unwrapping by a full rotation:

return PiOver4 * (float) (Math.Round(TwoPi + rotationToSnap / PiOver4) - TwoPi);
share|improve this answer
    
That won't work right at all, you're adding an unscaled 2pi to the scaled rotation. For example, it will round pi/4 to 0.56. (In fact, I'm not sure what you think in Gregory's answer won't work for negative rotations.) –  user744 Jan 28 '11 at 23:55
    
Ah, you're right. For some reason I thought modulo's negative number behavior was involved, but of course it isn't. This is what I get for posting before coffee. –  munificent Jan 31 '11 at 18:26
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.