Show me the Player!

Back from my sabbatical of not writing game development blog posts for a variety of reasons, todays episode is going to follow the tumultuous world of camera positioning.

imageimageimage

Positioning the camera can go from easy and mundane to may days of effort to tune an effect to just the way you want it. From the screen shots above the effect we will be talking about is chase camera with delayed rotational updating.

The first image could be a still from a static camera, following from a fixed offset, with no delay in camera rotation. Imagine that you had stuck a big stick on to the back of the donkey and attached a camera to it and another, with the carrot:

image

 

The camera’s position can be defined as a relative offset from the player (donkey). This relative offset should be defined in the object space, as we will need the camera to follow any rotation that the player makes. To do this, define a new Vector3 for the offset, slightly behind and slightly above the player. We will need to also store the Camera Position in world space after we calculate it, so we will set that up as well.

// The camera's offset Vector3 cameraOffset = new Vector3(0, 2.0f, 5.5f);

// The world space position of the camera Vector3 cameraPosition = new Vector3();

SunBurn already provides us with a View Matrix setup in the template game project, which is already being handled via Draw, so all we will need to do is override the existing calls updating the View Matrix. Before we do this, replace the existing default Update method in Game.cs with the following, we will then add in the relevant code:

protected override void Update(GameTime gameTime)
{
    // Enables game input / control when not editing the scene (the editor provides its own control). if (!sceneInterface.Editor.EditorAttached)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            this.Exit();
    }

    // Update all contained managers. sceneInterface.Update(gameTime);

    // TODO: Add your update logic here // TODO: Add the camera logic here base.Update(gameTime);
}

It’s important to note (and is an issue I ran in to personally as well) that the last thing that should happen in your update loop is, the updating of the camera. If you don’t update the camera after the objects have moved you will end up drawing the objects in their previous positions, this will introduce lag in to your game (at best 1 frame) and/or sever jerky camera movements (at worst but this is also very bad).

The View Matrix

To update the View Matrix, we need 3 variables. The cameraPosition – where the camera is located in world space. The cameraTarget – the target that the camera will be looking at as a location in world space. The cameraUpVector – the direction that is considered up from the camera’s perspective. In our example, the player (donkey) is our cameraTarget. The cameraPosition is defined from the camera position, which we will work out from our offset (attached to the stick off the back of the donkey). The cameraUpVector in our simple example will be a standard world consideration of up initially (+Y or Vector3(0,1,0) aka Vector3.Up) but because it’s space, we will use the players transformed version.

Let’s update the view matrix with the simple fixed camera.

Because of my fascination with SunBurn components, for this article, I’m assuming that we are moving the player via a component – if you need a component for this, check out my previous post on moving a player with a component which makes use of the internal physics engine in SunBurn – http://dsebj.evolvingsoftware.com/2011/04/23/player-movements-in-3d-sunburn-components/

We will need access to the players position to update the view matrix; assuming that the player object is called player, lets update find the object in the scene.

SceneObject copyBase;
SceneInterface.ActiveSceneInterface.ObjectManager.Find("player", true, out copyBase);
if (copyBase == null)
{
    SystemConsole.AddMessage("Could not find " + "player", 1);
}

Assuming everything is good (as in you have an object accessible in the scene, that is marked as being able to receive updates, is called player and has a component attached to the player to enable movement) copyBase will now contain a reference to the SceneObject called “player”.

We will need to get the rotation of the player, to be able to position the camera correctly in world space, to do this we can decompose the player matrix and split out the component elements.

Quaternion playerQuaternion = new Quaternion();
Vector3 playerTranslation = new Vector3();
Vector3 playerScale = new Vector3();
copyBase.World.Decompose(out playerScale, out playerQuaternion, out playerTranslation);

After decomposing the Players Matrix, we can now use the players rotation, defined in playerQuaternion to move the camera to the world space offset; we do this by Transforming the cameraOffset with the players rotation. We then add the world space location of the player to the transformed camera position, to have the actual camera position that will be used in the view matrix construction.

// Update the camera position to be a relative world offset (calculated from the players rotation in the world) cameraPosition = Vector3.Transform(cameraOffset, Matrix.CreateFromQuaternion(playerQuaternion));

// Update the cameras position to be the cameras position (after calculating the relevant rotation) // to be the offset in world space from the player cameraPosition += playerTranslation;

Finally we can create the View Matrix, as such:

// Create the view Matrix, looking at the player, setting the cameras up based on the player view = Matrix.CreateLookAt(cameraPosition, playerTranslation, copyBase.World.Up);

The other item that is needed now is the projection matrix. We can just use the standard SunBurn projection matrix but it’s up to you to fiddle with.

// Define the standard SunBurn projection projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(70.0f),
    GraphicsDevice.Viewport.AspectRatio, 0.1f, environment.VisibleDistance);

Fixed Camera Update, Engage

For those of you following along at home, your replacement update method should look like this:

protected override void Update(GameTime gameTime)
{
    // Enables game input / control when not editing the scene (the editor provides its own control). if (!sceneInterface.Editor.EditorAttached)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            this.Exit();
    }

    // Update all contained managers. sceneInterface.Update(gameTime);

    // TODO: Add your update logic here // TODO: Add the camera logic here SceneObject copyBase;
    SceneInterface.ActiveSceneInterface.ObjectManager.Find("player", true, out copyBase);
    if (copyBase == null)
    {
        SystemConsole.AddMessage("Could not find " + "player", 1);
    }

    Quaternion playerQuaternion = new Quaternion();
    Vector3 playerTranslation = new Vector3();
    Vector3 playerScale = new Vector3();
    copyBase.World.Decompose(out playerScale, out playerQuaternion, out playerTranslation);

    // Update the camera position to be a relative world offset (calculated from the players rotation in the world) cameraPosition = Vector3.Transform(cameraOffset, Matrix.CreateFromQuaternion(playerQuaternion));

    // Update the cameras position to be the cameras position (after calculating the relevant rotation) // to be the offset in world space from the player cameraPosition += playerTranslation;

    // Create the view Matrix, looking at the player, setting the cameras up based on the player view = Matrix.CreateLookAt(cameraPosition, playerTranslation, copyBase.World.Up);

    // Define the standard SunBurn projection projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(70.0f),
        GraphicsDevice.Viewport.AspectRatio, 0.1f, environment.VisibleDistance);

    base.Update(gameTime);
}

So if you load it up and play around with a ship in space you might get some screen shots like this:

imageimageimage

Amazing asteroids, but they all look like the ship is still. Indeed it feels like the world is moving and the player isn’t when you actually fly the ship around. Which brings us to Lerp.

Lerp joke here please

Lerp is actually an abbreviation for Linear interpolation. Bet you feel informed now. Personally I feel small when ever I try and read an article about something like this on Wikipedia:

image

 

 

 

 

I think about packing my bags and going home..

image

 

There is one important sentence in there for our understanding today; “This formula can also be understood as the weighted average.” Awesome, I understand that bit.

So how can we use a weighted average to help us make our camera more interesting? Well instead of immediately updating the position of the camera offset to that of the world space that our player is in, we can lerp to that position from the cameras current position. To do this, we need to keep track of the cameras current world space rotation as well as the players world space rotation. Handily, we already have the player world space rotation & conveniently it’s already stored as a Quaternion.

We need to be able to store the calculated camera’s position for use in the next update and we will store this as a Quaternion as well:

// Camera rotation that need to be persisted between updates Quaternion cameraQuaternion = Quaternion.Identity;

To use the Lerp function, we pass in the cameraQuaternion, playerQuaternion and an amount; the result will be the updated cammeraQuaternion. You can almost think of the function of taking the weighted average and then using it to add to the current cameraQuaternion value. Calculating this each update will make the camera gradually slide back in to the offset position behind the player.

// Lerp between the current camera rotation and the players rotation, by 10% // This is effectively calculated as an additional amount // It's almost like cameraQuaternion = cameraQuaternion + ((cameraQuaternion - playerQuaternion)/10) cameraQuaternion = Quaternion.Lerp(cameraQuaternion, playerQuaternion, 0.1f);

The updated cameraQuaternion now contains a value between where it was to where the it should be (based on the players world matrix rotation) & as such we can use this for the transform update; replacing the previous cameraPosition calculation.

// Update the camera position to be a relative world offset (calculated from the players rotation in the world) cameraPosition = Vector3.Transform(cameraOffset, Matrix.CreateFromQuaternion(cameraQuaternion));

Because our camera is now using a different rotation from the player, we need to also change the calculation for the camera’s up vector being passed in to the view Matrix. We can create this transform as such.

// Update the cameraUp to reflect the actual up of the camera Vector3 cameraUp = Vector3.Up;
cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromQuaternion(cameraQuaternion));

The view Matrix now needs to be updated to use the up value from the camera, rather than the players Matrix.

// Create the view Matrix, looking at the player, setting the camera's up from it's actual up direction view = Matrix.CreateLookAt(cameraPosition, playerTranslation, cameraUp);

If you add this all together and run it, you too can get some screen shots like this:

image

image

image

 

 

 

 

 

Oh, a moving ship, with freaking awesome shadows! I know, how pro does that look.

Cut to the chase

For those of you who subscribe to the copy and paste school; use the following to replace your standard SunBurn update loop and get a nice chase camera, with some basic camera rotation Lerp’ing.

// The camera's offset Vector3 cameraOffset = new Vector3(0, 2.0f, 5.5f);

// The world space position of the camera Vector3 cameraPosition = new Vector3();

// Camera rotation that need to be persisted between updates Quaternion cameraQuaternion = Quaternion.Identity;

protected override void Update(GameTime gameTime)
{
    // Enables game input / control when not editing the scene (the editor provides its own control). if (!sceneInterface.Editor.EditorAttached)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            this.Exit();
    }

    // Update all contained managers. sceneInterface.Update(gameTime);

    // TODO: Add your update logic here // TODO: Add the camera logic here SceneObject copyBase;
    SceneInterface.ActiveSceneInterface.ObjectManager.Find("player", true, out copyBase);
    if (copyBase == null)
    {
        SystemConsole.AddMessage("Could not find " + "player", 1);
    }

    Quaternion playerQuaternion = new Quaternion();
    Vector3 playerTranslation = new Vector3();
    Vector3 playerScale = new Vector3();
    copyBase.World.Decompose(out playerScale, out playerQuaternion, out playerTranslation);

    // Lerp between the current camera rotation and the players rotation, by 10% // This is effectively calculated as an additional amount // It's almost like cameraQuaternion = cameraQuaternion + ((cameraQuaternion - playerQuaternion)/10) cameraQuaternion = Quaternion.Lerp(cameraQuaternion, playerQuaternion, 0.1f);

    // Update the camera position to be a relative world offset (calculated from the players rotation in the world) cameraPosition = Vector3.Transform(cameraOffset, Matrix.CreateFromQuaternion(cameraQuaternion));

    // Update the cameras position to be the cameras position (after calculating the relevant rotation) // to be the offset in world space from the player cameraPosition += playerTranslation;

    // Update the cameraUp to reflect the actual up of the camera Vector3 cameraUp = Vector3.Up;
    cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromQuaternion(cameraQuaternion));

    // Create the view Matrix, looking at the player, setting the camera's up from it's actual up direction view = Matrix.CreateLookAt(cameraPosition, playerTranslation, cameraUp);

    // Define the standard SunBurn projection projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(70.0f),
        GraphicsDevice.Viewport.AspectRatio, 0.1f, environment.VisibleDistance);

    base.Update(gameTime);
}

An obligatory video please:

 

Big Thanks To

crashover, flashed & Ragath from #XNA on efnet for the assistance and direction on this post. Thanks also to John aka Bob The Builder, for his help with the camera update sequence.

Leave a Reply

Your email address will not be published. Required fields are marked *