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);
}