Skip to content

Third Person Controller in MonoGame Part II

Roman Shapiro edited this page Jun 7, 2026 · 6 revisions

Introduction

This is the second part of the tutorial series where we implement a Third Person Controller in MonoGame.

The first part is available here: Third Person Controller in MonoGame Part I

In this part we will replace the character capsule with an animated model and attach a sword to the character's back.

thirdPersonContollerPartII.mp4

To understand the material, you need to be familiar with what glTF/GLB is and how skeletal animation works. There is plenty of information on this topic online. For example, the article on LearnOpenGL:


Combining FBX Animations into a Single glTF Model (Optional Chapter)

This chapter is optional for the tutorial, since a ready-made glTF model with all the necessary animations will be provided.

However, if you want to create your own animated model based on Mixamo, it will be useful to watch the video where the required animations are first downloaded and then combined in Blender and exported as GLB (the binary version of glTF): https://youtu.be/14G3tAkZYMw?si=kku5mKTtiKiach63

Keep the following points in mind:

  • The first animation from Mixamo (e.g., "Idle") must be downloaded with the "With Skin" parameter; all others — with "Without Skin"
  • Make sure the "In Place" checkbox is enabled. Some animations don't have this option — it can be fixed in Blender using the In Placer add-on
  • In Blender, first import the FBX file downloaded with "With Skin" (i.e., the base model), then import all the others
  • Don't forget to delete all Armatures except the base model's armature after pressing the "Push Down Action" buttons

Starting Point

We will continue from where we left off at the end of the previous part.

If you no longer have the source code from that tutorial, you can get it here:

https://github.com/rds1983/ThirdPersonTutorial/tree/master/Step1-Capsule


Minor Changes

First, let's make some small QoL improvements to the source code:

  • Enable DefaultLights in BasicEffect
  • Split the Update method into smaller parts
  • Extract part of DrawMesh into a separate method

Enabling DefaultLights

Remove the code that initializes DirectionalLight0 in _basicEffect and replace it with this line:

_basicEffect.EnableDefaultLighting();

This will make the scene better lit:

image

Refactoring Update

Add 3 methods: ProcessMouse, ProcessKeyboard, and UpdateJump, and move the corresponding code into them:

// Handle mouse input for camera rotation
private void ProcessMouse()
{
    var mouse = Mouse.GetState();
    if (_oldMouse != null)
    {
        var horizontalRotation = -(int)((mouse.X - _oldMouse.Value.X) * MouseSensitivity);
        _heroYaw += horizontalRotation;

        var verticalRotation = -(int)((mouse.Y - _oldMouse.Value.Y) * MouseSensitivity);
        _cameraMountPitch += verticalRotation;
        _cameraMountPitch = MathHelper.Clamp(_cameraMountPitch, 5, 90);
    }
    _oldMouse = mouse;
}

// Handle keyboard input for movement and jump initiation
private void ProcessKeyboard()
{
    var velocity = Vector3.Zero;
    var heroTransform = ToMatrix(_heroPosition, Vector3.One, _heroYaw, 0, 0);
    var keyboard = Keyboard.GetState();

    if (keyboard.IsKeyDown(Keys.W)) velocity = heroTransform.Forward * -MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.S)) velocity = heroTransform.Forward * MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.A)) velocity = heroTransform.Right * MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.D)) velocity = heroTransform.Right * -MovementSpeed;

    _heroPosition += velocity;

    if (keyboard.IsKeyDown(Keys.Space))
    {
        _jumpStarted = DateTime.Now;
        _jumpMovement = velocity;
    }
}

// Update hero position during jump using projectile motion
private void UpdateJump()
{
    var t = (float)(DateTime.Now - _jumpStarted.Value).TotalSeconds;
    var jumpHeight = DefaultY + JumpForce * t - (0.5f * Gravity * t * t);

    _heroPosition.Y = jumpHeight;
    _heroPosition += _jumpMovement;

    if (_heroPosition.Y <= DefaultY)
    {
        _heroPosition.Y = DefaultY;
        _jumpStarted = null;
    }
}

protected override void Update(GameTime gameTime)
{
    base.Update(gameTime);
    ProcessMouse();

    if (_jumpStarted == null)
        ProcessKeyboard();
    else
        UpdateJump();
}

The code is now simpler and more readable.

Refactoring DrawMesh

Extract the rendering of DrMeshPart into a separate method:

// Draw a single mesh part with the given effect
private void DrawMeshPart(Effect effect, DrMeshPart part)
{
    GraphicsDevice.SetVertexBuffer(part.VertexBuffer);
    GraphicsDevice.Indices = part.IndexBuffer;

    foreach (var pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, part.PrimitiveCount);
    }
}

// Render a mesh with color and texture
private void DrawMesh(DrMesh mesh, Matrix world, Color color, Texture2D texture)
{
    _basicEffect.DiffuseColor = color.ToVector3();
    _basicEffect.TextureEnabled = texture != null;
    _basicEffect.Texture = texture;
    _basicEffect.World = world;

    foreach (var part in mesh.MeshParts)
        DrawMeshPart(_basicEffect, part);
}

Intermediate Result

The result of our refactoring is available here:

https://github.com/rds1983/ThirdPersonTutorial/blob/master/Step2-Refactor/MyGame.cs


Replacing the Capsule with a Model

Download Assets.zip and extract it into the project folder. The archive contains only the glTF character model.

Now let's go to the code.

Remove the _meshHero field and all code that works with it. Replace it with the following fields:

// Hero character model instance
private DrModelInstance _modelHero;

// Effect for rendering skeletal mesh with bone transformations
private SkinnedEffect _skinnedEffect;

// Solid white texture for models without material textures
private Texture2D _textureWhite;
  • SkinnedEffect is the equivalent of BasicEffect, but with skeletal animation support.
  • _textureWhite is a 1×1 white texture needed because our model has no textures.

Add initialization of the new fields to LoadContent:

// Load hero model
DrModel model = assetManager.LoadModel(GraphicsDevice, "Models/mixamo.gltf");
_modelHero = new DrModelInstance(model);

// Effect for rendering skeletal meshes
_skinnedEffect = new SkinnedEffect(GraphicsDevice);
_skinnedEffect.EnableDefaultLighting();

// Create solid white texture for untextured mesh parts
_textureWhite = new Texture2D(GraphicsDevice, 1, 1);
_textureWhite.SetData(new Color[] { Color.White });

A note on model loading: we use two classes — DrModel and DrModelInstance.

  • DrModel contains the model's data: meshes, bone hierarchy, animations, materials, etc. It is effectively immutable.
  • DrModelInstance is an instance of a DrModel and holds mutable data such as bone transforms, etc.

So each DrModel is wrapped in a DrModelInstance. In this tutorial series there is one instance per model, but in real applications a single model can have any number of instances.

The DrawModel Method

Add the following method:

// Render model with material colors and textures, handling both skinned and static meshes
private void DrawModel(DrModelInstance model, Matrix world)
{
    foreach (var mesh in model.Model.Meshes)
    {
        foreach (var part in mesh.MeshParts)
        {
            var color = Color.White;
            var texture = _textureWhite;

            if (part.Material != null)
            {
                color = part.Material.DiffuseColor;
                if (part.Material.DiffuseTexture != null)
                    texture = part.Material.DiffuseTexture;
            }

            if (part.Skin != null)
            {
                // Skinned mesh: bone transforms applied per-vertex in shader
                _skinnedEffect.DiffuseColor = color.ToVector3();
                _skinnedEffect.Texture = texture;
                _skinnedEffect.World = world;
                _skinnedEffect.SetBoneTransforms(model.GetSkinTransforms(part.Skin.SkinIndex));
                DrawMeshPart(_skinnedEffect, part);
            }
            else
            {
                // Static mesh: must include bone transform in World matrix
                _basicEffect.DiffuseColor = color.ToVector3();
                _basicEffect.Texture = texture;
                _basicEffect.World = model.GetBoneGlobalTransform(mesh.ParentBone.Index) * world;
                DrawMeshPart(_basicEffect, part);
            }
        }
    }
}

The most interesting part here is the part.Skin null check and the choice of effect based on its result.

To understand this code, you need to know how a glTF model is structured. Each mesh may have an associated skin — a collection of bones. Calling model.GetSkinTransforms returns an array of transformation matrices for that collection, which we pass to SkinnedEffect.

If no skin is associated with the mesh, we fall back to the regular BasicEffect.

Also note: when a skin is present, we pass only the model's world transformation matrix to the World parameter. When there is no skin, we additionally multiply by the bone's transform — model.GetBoneGlobalTransform(mesh.ParentBone.Index) — because in the skinned case the bone transforms are already encoded in the array.

Updated Draw Method

Rewrite the Draw method to set projection and view matrices for SkinnedEffect and call the new DrawModel:

protected override void Draw(GameTime gameTime)
{
    base.Draw(gameTime);
    var device = GraphicsDevice;
    device.Clear(Color.Black);

    device.DepthStencilState = DepthStencilState.Default;
    device.RasterizerState = RasterizerState.CullCounterClockwise;
    device.BlendState = BlendState.AlphaBlend;
    device.SamplerStates[0] = SamplerState.LinearWrap;

    var projection = Matrix.CreatePerspectiveFieldOfView(
        MathHelper.ToRadians(ViewAngle),
        device.Viewport.AspectRatio,
        NearPlaneDistance,
        FarPlaneDistance);

    _basicEffect.Projection = projection;
    _skinnedEffect.Projection = projection;

    var heroTransform = ToMatrix(_heroPosition, Vector3.One, _heroYaw, 0, 0);
    var cameraMountTransform = ToMatrix(new Vector3(0, 1f, 0), Vector3.One, 0, _cameraMountPitch, 0) * heroTransform;
    var cameraTransform = ToMatrix(new Vector3(0, 0, -5), Vector3.One, 180, 0, 0) * cameraMountTransform;
    var view = Matrix.Invert(cameraTransform);

    _basicEffect.View = view;
    _skinnedEffect.View = view;

    DrawMesh(_meshGround, Matrix.CreateScale(200, 1, 200), Color.White, _textureGround);
    DrawModel(_modelHero, heroTransform);
}

After running the game, the capsule is replaced by the model:

image

However, it floats slightly above the ground because DefaultY is set to 1. Change it to 0:

// Hero ground height
private const float DefaultY = 0;

Now the model stands exactly on the ground:

image

New MyGame.cs:

https://github.com/rds1983/ThirdPersonTutorial/blob/master/Step3-Model/MyGame.cs


Adding Animations

Declare a new field:

// Animation state machine for playing and transitioning clips
private AnimationController _player;

AnimationController — as the name suggests — animates models by computing the necessary transformation matrices.

Add initialization code to LoadContent:

_player = new AnimationController(_modelHero);
_player.StartClip("Idle", AnimationFlags.Looped);

Here we create an AnimationController, bind it to _modelHero, and start the Idle animation with the Looped flag so it restarts when it finishes.

Finally, add one line to the Update method:

_player.Update(gameTime.ElapsedGameTime);

The model will now play the Idle animation on loop:

image

New MyGame.cs:

https://github.com/rds1983/ThirdPersonTutorial/blob/master/Step4-AnimationController/MyGame.cs


Adding Run and Jump Animations

First, add an enum for the list of possible animations:

// Animation states for the hero character
private enum AnimationState
{
    Idle,    // Standing still
    Running, // Moving
    Jumping, // Jumping
    Landing  // Landing after jump
}

Also add a constant (its purpose will be explained below):

// Duration for animation transitions between clips
private static readonly TimeSpan AnimationCrossfadeDelay = TimeSpan.FromSeconds(0.2f);

Add a field for the current animation state:

// Current animation state
private AnimationState _animationState = AnimationState.Idle;

Update ProcessKeyboard with animation-switching logic:

private void ProcessKeyboard()
{
    var velocity = Vector3.Zero;
    var heroTransform = ToMatrix(_heroPosition, Vector3.One, _heroYaw, 0, 0);
    var keyboard = Keyboard.GetState();
    var isRunning = true;

    if (keyboard.IsKeyDown(Keys.W)) velocity = heroTransform.Forward * -MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.S)) velocity = heroTransform.Forward * MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.A)) velocity = heroTransform.Right * MovementSpeed;
    else if (keyboard.IsKeyDown(Keys.D)) velocity = heroTransform.Right * -MovementSpeed;
    else isRunning = false;

    // Transition between Run and Idle animations
    if (_animationState != AnimationState.Running && isRunning)
    {
        _player.CrossfadeToClip("Run", AnimationCrossfadeDelay, AnimationFlags.Looped);
        _animationState = AnimationState.Running;
    }
    else if (_animationState != AnimationState.Idle && !isRunning)
    {
        _player.CrossfadeToClip("Idle", AnimationCrossfadeDelay, AnimationFlags.Looped);
        _animationState = AnimationState.Idle;
    }

    _heroPosition += velocity;

    if (keyboard.IsKeyDown(Keys.Space))
    {
        _jumpStarted = DateTime.Now;
        _animationState = AnimationState.Jumping;
        _jumpMovement = velocity;
        _player.CrossfadeToClip("JumpStart", AnimationCrossfadeDelay);
    }
}

Animation switching is handled by CrossfadeToClip. We could call StartClip as in LoadContent, but that would cause abrupt cuts between animations. CrossfadeToClip transitions smoothly over the specified time — in our case, AnimationCrossfadeDelay (200 ms).

If we run the game now, everything works more or less correctly — except for the landing, since the transition to the Idle animation only begins at the moment of landing:

wrongLanding.mp4

We want the landing animation to start while the character is still falling and approaching the ground. Rewrite UpdateJump:

private void UpdateJump()
{
    var t = (float)(DateTime.Now - _jumpStarted.Value).TotalSeconds;
    var jumpHeight = JumpForce * t - (0.5f * Gravity * t * t);

    _heroPosition.Y = jumpHeight;
    _heroPosition += _jumpMovement;

    // Vertical velocity: positive = upward, negative = falling
    var jumpVelocity = JumpForce - Gravity * t;

    // Start landing animation when falling below height 2
    if (jumpVelocity < 0 && _heroPosition.Y < 2 && _animationState != AnimationState.Landing)
    {
        _player.CrossfadeToClip("JumpEnd", AnimationCrossfadeDelay);
        _animationState = AnimationState.Landing;
    }

    if (_heroPosition.Y <= DefaultY)
    {
        _heroPosition.Y = DefaultY;
        _jumpStarted = null;
    }
}

We compute jumpVelocity to determine whether the character is falling (jumpVelocity < 0). If falling and the character has dropped below height 2, we start the landing animation. Now it looks ok:

correctLanding.mp4

Btw, the running looks ok too:

running.mp4

New MyGame.cs:

https://github.com/rds1983/ThirdPersonTutorial/blob/master/Step5-BasicAnimations/MyGame.cs


Attaching the Sword to the Back

Download the next Assets.zip and extract it into the project folder. It contains the sword model.

The approach is straightforward: we pick one of the character's bones and apply its transformation to the sword model.

Add fields:

// Sword model instance
private DrModelInstance _modelSword;

// Bone where the sword is attached
private DrModelBone _swordAttachBone;

Add the following to LoadContent (make sure it comes after _modelHero is initialized):

// Load sword model
model = assetManager.LoadModel(GraphicsDevice, "Models/sword.gltf");
_modelSword = new DrModelInstance(model);

// Set the bone to attach the sword to
_swordAttachBone = _modelHero.Model.FindBoneByName("mixamorig:Spine");

Here we load the sword model and choose the character's spine bone as the attachment point.

Finally, add the following to Draw:

// Attach the sword to the attachment bone
// Transform chain: local sword offset -> attachment bone transform -> hero world transform
var swordTransform =
    ToMatrix(new Vector3(-12, 0, -20), new Vector3(16), 0, 0, 180)
    * _modelHero.GetBoneGlobalTransform(_swordAttachBone.Index)
    * heroTransform;

DrawModel(_modelSword, swordTransform);

First we apply a local transform to the sword model:

ToMatrix(new Vector3(-12, 0, -20), new Vector3(16), 0, 0, 180)

This was tuned manually for this particular model. Then we apply the spine bone's transform, and then the full character transform. The result: the sword ends up on the character's back:

image

Conclusion

The tutorial is complete. The game should now match the video shown at the beginning of the article.

Final MyGame.cs:

https://github.com/rds1983/ThirdPersonTutorial/blob/master/Step6-SwordOnBack/MyGame.cs

In the next part, we will explore animation blending and add the ability to draw, sheathe, and swing the sword.

Clone this wiki locally