Sgt. Conker We are "absolutely fine"

6Sep/100

Article: Character Movement

by David Kendall

When development started on The Casino, a lot of things were implemented quickly to get the game to a playable state as soon as possible with the intention of coming back to these parts at a later point. In order to achieve character movement quickly, character movement that can be found in most XNA tutorials were added. This involved the character strafing and moving based upon the left thumbstick and rotation of the character based upon the right thumbstick.

This control scheme may seem familiar to some of you, and it should as it is reminiscent of the controls for a First Person Shooter (FPS). However, The Casino is in no way a FPS. In fact I don't know what you would classify it as. I also had an aim when designing the controls...to keep it simple. This was so players of all ages and skills could pick up and play the game as quickly as possible. This meant that the FPS control scheme had to go.


When the player isn't playing one of the many mini games in The Casino, they are traversing the casino room, moving from table to table. The set up of the game can be seen below (Alpha screen shot).

As you can see, the set up is reminiscent of Third Person Action games. Because of this, I thought I would "test" some third person games for inspiration on how to move my character. I tried Grand Theft Auto IV, Alan Wake, Splinter Cell and countless others. After much pondering, I thought that Alan Wake's set up would best suit my game.

For those of you that haven't played Alan Wake, the controls are simple. Movement and rotation of the character is linked to the left thumbstick, while the camera is linked to the right thumbstick. Each direction of the thumbstick represents an angle in which the characters body is rotated, between 0 and 360 degrees. When a direction is pressed, the character rotates his body in the shorted direction until they are facing the direction which is being pressed. For example, if the thumbstick was being pressed to the right, the character would rotate towards the right until he is facing to the right. Once the character is facing in the correct direction, he starts moving in said direction. This set up was perfect, as the rotation would help provide realistic movement of the character, while being so simple the player doesn't have to worry about it. So how did I translate this into The Casino.

The first thing was to translate the rotation of my character based upon the direction of the thumbstick. As my character can only move in 2 dimensions, a solution was found that was designed for a 2 dimensional game. However, a little tweaking needed to be done. This solution assumes that the player will only have one perspective. However, as the player can rotate and can move the camera, the thumbstick input has to be relative to the current camera angle. This lead to the following code, where my walkable plane is along the X and Z axis:

Vector3 translatedThumbStick = Vector3.Transform(new Vector3(-gamepad.ThumbSticks.Left.X,
                                                             0.0f,
                                                             gamepad.ThumbSticks.Left.Y),
                                                 Matrix.CreateFromYawPitchRoll(camera.Rotation.X,
                                                                               0.0f,
                                                                               0.0f));
Vector2 leftstick = new Vector2(translatedThumbStick.X, translatedThumbStick.Z);

leftstick.Normalize();
newAngle = (float)Math.Acos(leftstick.Y);
if (leftstick.X <= 0.0f)
newAngle = -newAngle; player.Avatar.Rotation = new Vector3(newAngle, 0.0f, 0.0f);

By transforming the left thumbstick by the camera angle first, the vector will be in relation to the current camera angle. This means that no matter which camera angle you are currently viewing your character at, when you press up on the thumbstick your character will always face forwards. The camera used is a modified version of the Third Person Camera from the MSDN website. You may have also noticed I only transform the thumbstick from the X co-ordinate of the camera angle. This is because I don't want the translation to be influenced by the Y or Z plane.

With this implemented, the character rotates based upon our input from the left thumbstick. However, the character doesn't move from one rotation to another very smoothly. My first solution to this was to interpolate between the old angle and the new angle retrieved from the thumbstick. This however wasn't effective as the more you moved the thumbstick, the more the character lagged behind. The solution was to have the character rotate in the direction with the shortest angle between the characters current rotation and the angle calculated (stored in the newAngle variable). This solution worked well, until a certain point on the thumbstick was selected which came down to the way rotations are stored.

Rotations from the thumbstick are stored between -Pi to Pi, in the form of radians. If the thumbstick is moved to the bottom-left, this would be stored as a 45 degree angle. However, if the user then presses bottom-right then this would be stored as a 315 degree angle. Because of this, instead of the character rotating in the logical sense (45 degrees to 0 degrees to -45 degrees), the character moves from 45 degrees to 180 degrees to 315 degrees. This does not look good.

The way I got round this was by storing a new version of newAngle. This version is called targetAngle and is used to determine the newAngle in relation to the current angle. This is done, by checking the distance between the player's current angle and three different versions of new angle and using the new version that has the smallest distance. These are:

  • newAngle - this is the unaltered version that is calculated in the code above.
  • newAngle + (2 x Pi) - This represents the value of new angle with an addition of 2Pi. This represents the same visual angle as newAngle, but with an additional 360 degree rotation in the clockwise direction.
  • newAngle - (2 x Pi) - This represents the value of new angle with 2Pi taken away. This represents the same visual angle as newAngle, but with an additional 360 degree rotation in the anti-clockwise direction.

But why these three versions? Well, let’s take the example that we had before; bottom-left (45 degrees) to bottom-right (315 degrees). By calculating the smallest distance by using the three different versions, we would arrive at newAngle (315 degrees) - (2 x Pi) as being the angle with the smallest distance. We then use this angle to determine the direction in which the character moves, which gives us the following code:

Vector3 translatedThumbStick = Vector3.Transform(new Vector3(-gamepad.ThumbSticks.Left.X,
                                                             0.0f,
                                                             gamepad.ThumbSticks.Left.Y),
                                                  Matrix.CreateFromYawPitchRoll(camera.Rotation.X,
                                                                                0.0f,
                                                                                0.0f));
Vector2 leftstick = new Vector2(translatedThumbStick.X, translatedThumbStick.Z);

leftstick.Normalize();
newAngle = (float)Math.Acos(leftstick.Y);

if (!float.IsNaN(newAngle))
{
        // confine newAngle to bewtween -Pi and Pi
        if (newAngle > Math.PI)
                newAngle = (float)(-Math.PI + Math.Abs(newAngle - Math.PI));
        else if (newAngle < -Math.PI)
                newAngle = (float)(Math.PI - Math.Abs(-Math.PI - newAngle));

        if (newAngle != player.Avatar.Rotation.X)
        {
                float targetAngle = newAngle;
                float distance = MathHelper.Distance(newAngle, player.Avatar.Rotation.X);
                if (MathHelper.Distance((newAngle - MathHelper.TwoPi),player.Avatar.Rotation.X)
                {
                        targetAngle = (newAngle - MathHelper.TwoPi);
                        distance = MathHelper.Distance((newAngle - MathHelper.TwoPi),
                                                        player.Avatar.Rotation.X);
                }
                if (MathHelper.Distance(player.Avatar.Rotation.X, newAngle + MathHelper.TwoPi)
                {
                        targetAngle = newAngle + MathHelper.TwoPi;
                        distance = MathHelper.Distance((newAngle + MathHelper.TwoPi),
                                                        player.Avatar.Rotation.X);
                }

                double angleDirection = targetAngle - player.Avatar.Rotation.X;

                if (MathHelper.Distance(player.Avatar.Rotation.X, targetAngle) > MathHelper.ToRadians(10.0f))
                                player.Avatar.Rotation += new Vector3(MathHelper.ToRadians(10.0f) *
                                                                      Math.Sign(angleDirection),
                                                                      0.0f,
                                                                      0.0f);
                else
                        player.Avatar.Rotation = new Vector3(newAngle,0.0f,0.0f);
                }
        }

There are two important things to note about this code that will probably only make sense now the code is in front of you. The first thing is that in order for this to work, the angle has to be clamped to between -Pi and Pi. This is due to the calculation of the three variations of newAngle. This is present in the code and has been commented. The way I have implemented it is to take away or add 2Pi depending on which end of the -Pi/Pi scale the newAngle has gone over. This is also the reason why I make the rotation equal to newAngle when the distance between the two angles gets to small.

The second thing to note is the check at first to see if the newAngle is actually an angle (in the form of if (!float.IsNaN(newAngle))). The reason for this is because as this is calculated every frame, if the user is not pushing down anywhere on the thumbstick, the angle which is created is not a number. This then a) makes the following calculations unnecessary and b) can cause an error when applying the rotation.

The last thing to add to this code is allow the player to move when they are facing in the correct direction. I check to see if the angle of the thumbstick is within 5 degrees of the player’s current rotation (another reason to clamp the rotation). This leniency is because some controllers have incorrect callibration (due to general wear and tear), and this helps to overcome this issue, without having the player move while still not facing in the correct direction. You can play with this threshold yourself. The finished code can be found below:

Vector3 translatedThumbStick = Vector3.Transform(new Vector3(-gamepad.ThumbSticks.Left.X,
                                                              0.0f,
                                                               gamepad.ThumbSticks.Left.Y),
                                                  Matrix.CreateFromYawPitchRoll(camera.Rotation.X,
                                                                                0.0f,
                                                                                0.0f));

Vector2 leftstick = new Vector2(translatedThumbStick.X, translatedThumbStick.Z);
leftstick.Normalize();
newAngle = (float)Math.Acos(leftstick.Y);

if (newAngle != player.Avatar.Rotation.X)
{
        if (!float.IsNaN(newAngle))
        {
                // confine newAngle to bewtween -Pi and Pi
                if (newAngle > Math.PI)
                        newAngle = (float)(-Math.PI + Math.Abs(newAngle - Math.PI));
                else if (newAngle < -Math.PI)
                        newAngle = (float)(Math.PI - Math.Abs(-Math.PI - newAngle));

                if (newAngle != player.Avatar.Rotation.X)
                {
                        float targetAngle = newAngle;
                        float distance = MathHelper.Distance(newAngle, player.Avatar.Rotation.X);
                        if (MathHelper.Distance((newAngle - MathHelper.TwoPi),player.Avatar.Rotation.X)
                        {
                                targetAngle = (newAngle - MathHelper.TwoPi);
                                distance = MathHelper.Distance((newAngle - MathHelper.TwoPi),
                                                                player.Avatar.Rotation.X);
                        }
                if (MathHelper.Distance(player.Avatar.Rotation.X, newAngle + MathHelper.TwoPi)
                {
                        targetAngle = newAngle + MathHelper.TwoPi;
                        distance = MathHelper.Distance((newAngle + MathHelper.TwoPi),
                                                                player.Avatar.Rotation.X);
                }
                double angleDirection = targetAngle - player.Avatar.Rotation.X;
                if (MathHelper.Distance(player.Avatar.Rotation.X, targetAngle) >
                                                        MathHelper.ToRadians(10.0f))
                        player.Avatar.Rotation += new Vector3(MathHelper.ToRadians(10.0f) *
                                                                                Math.Sign(angleDirection),
                                                                                0.0f,
                                                                                0.0f);
                 else
                        player.Avatar.Rotation = new Vector3(newAngle,0.0f,0.0f);

                if ((player.Avatar.Rotation.X >= (newAngle - MathHelper.ToRadians(5.0f))) &&
                                (player.Avatar.Rotation.X <= (newAngle + MathHelper.ToRadians(5.0f))))
                {
                        player.Avatar.Position += Vector3.Transform(new Vector3(gamepad.ThumbSticks.Left.X *
                                                                                (gameTime.ElapsedGameTime.Milliseconds *
                                                                                 velocity),
                                                                                 0.0f,
                                                                                 -gamepad.ThumbSticks.Left.Y *
                                                                                 (gameTime.ElapsedGameTime.Milliseconds *
                                                                                  velocity)),
                                                                      Matrix.CreateFromYawPitchRoll(camera.Rotation.X,
                                                                                                    0.0f,
                                                                                                    0.0f));
                }
        }
}
}

The finished result of this code can be found at the beginning of my recently released alpha footage of The Casino, which can be found on my YouTube account. I hope this has helped you towards creating a professional control scheme for your character and if you have any suggestions or there are any sections I have failed to make clear, then don't hestitate to comment below.

About Absolutely Fine Tutorial Contest

Look out for the complete list of entries in our tutorial contest! Coming soon (it's being built step by step)!
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


*

No trackbacks yet.