Including the Player within a game


Including the Player

Before you create the player object, do you remember back a few chapters ago when you were first rendering the sky box for the level? Well, the code is probably still there in the OnCreateDevice method of the game engine, but that probably isn't where it should be. You don't need to create the sky box, or the player, or the level until the user selects the character he will be using in the game. You left the event handler for this user interface screen "empty" before, so you should fill it now, using the method in Listing 9.1.
Listing 9.1. Selecting Your Character Event Handler
private void OnLoopySelected(object sender, EventArgs e)
{
    SelectLoopyScreen screen = sender as SelectLoopyScreen;

    System.Diagnostics.Debug.Assert(screen != null, GameName,
        "The selection screen should never be null.");

    // Create the graphics items
    CreateGraphicObjects(screen);

    // And not showing the select screen
    isSelectScreenShowing = false;

    // Fix the lights
    EnableLights();
}

First, you want to get the SelectLoopyScreen object that fired this event, which you can do from the sender variable. Considering this event should only be fired from this object, it should never return null (thus the Assert here).
You might be wondering what an assert is or why you would ever want to use such a thing. An assert is essentially an evaluation of a condition that should never be true, and if that condition is true, there is a big problem. What the Assert method does is, in debug builds, check whether that condition is TRue. If it isn't, execution proceeds normally; however, if it is true, execution halts and a dialog appears with the message you've included.
Using this functionality can help uncover many common coding mistakes.
The "graphics" items are created next (you will implement this method presently), the select screen should no longer be showing, and the lights should now be enabled for the level. If you remember from earlier, this method keys off the isSelectScreenShowing variable, so with it false, it enables the light for the level.
Now, you should remove the code that creates the sky-box mesh from the InitializeGraphics method and add the method you are missing into your game engine code. You will find this method in Listing 9.2.
Listing 9.2. Creating Graphics Objects
private void CreateGraphicObjects(SelectLoopyScreen screen)
{
    // No need to create the level mesh
    if (levelMesh == null)
    {
        // Create the mesh for the level
        ExtendedMaterial[] mtrls;
        levelMesh = Mesh.FromFile(MediaPath + "level.x", MeshFlags.Managed,
                                  device, out mtrls);

        // Store the materials for later use, and create the textures
        if ((mtrls != null) && (mtrls.Length > 0))
        {
            levelTextures = new Texture[mtrls.Length];
            for (int i = 0; i < mtrls.Length; i++)
            {
                // Create the texture
                levelTextures[i] = TextureLoader.FromFile(device, MediaPath +
                    mtrls[i].TextureFilename);
            }
        }
    }

    // Create our player object
    player = new Player(screen.LoopyMesh, screen.LoopyTexture,
                        screen.LoopyMaterial);
}

The code to create the sky-box mesh is the same as it was before, but now it is created only once. Because the mesh is never destroyed until the end of the application, creating it more than once is just a waste of time. When the levelMesh member is null, you can assume that the mesh hasn't been created yet and you can create it the first time. After that, the player object is created using the information from the Select Loopy screen to get the graphical content.
Do you remember earlier when the CleanupLoopy method was created in the Select Loopy screen? You did so when the player object wasn't created yet, and you were told that you would add a call to this method when the player object was added to the game engine. Well, now it's time to oblige. In your OnDestroyDevice overload, add the code from Listing 9.3 directly after your user screen cleanup methods.
Listing 9.3. Cleaning Up Players
// If the player is null, and you are on the select screen, clean up
// those meshes as well
if ((player == null) && (selectScreen != null))
{
    selectScreen.CleanupLoopyMesh();
}
// Clean up the player
if (player != null)
{
    player.Dispose();
}

You've been working on the player for a while now, and you're probably thinking to yourself, "Self, what else do I need to do with the player?" Well, you need to declare it sometime, so how about now?
// The player's object
private Player player = null;

The only things left are drawing the player and updating the player. The update is simple because you already have a method in the game engine that will be called every frame. Add this section of code to your OnFrameMove method:
if ((isQuitMenuShowing) || (isMainMenuShowing) || (isSelectScreenShowing))
{
    return; // Nothing to do
}

// Update the player
player.Update(elapsedTime, timer.TotalTime);

Obviously, if one of the user interface screens is showing, you don't need to update the player. If they are not showing, though, you simply call the player's update method. You probably noticed that you haven't declared the isQuitMenuShowing member variable yet, so go ahead and do that now:
// Is the quit menu showing?
private bool isQuitMenuShowing = false;

You won't actually use this variable until later when you design the user interface screen for quitting, but this code allows you to compile for now. The last thing you need to do with the player is render it onscreen at the appropriate time. You also want to add the code back for rendering the sky box. Go back to the OnFrameRender method in your game engine, and notice that each of the "rendering" methods is encapsulated into a single call depending on the game state; the correct object is rendered depending on which state the game is currently in. Because no single object is used to render the game scene, you want a separate method to render the game scene so that you don't clutter your render method. Add this final else clause to your render method:
else
{
    RenderGameScene();
}

You then need to add the implementation of this method, which you will find in Listing 9.4.
Listing 9.4. Rendering Your Game
private void RenderGameScene(Device device, double appTime)
{
    // First render the level mesh, but before that is done, you need
    // to turn off the zbuffer. This isn't needed for this drawing
    device.RenderState.ZBufferEnable = false;
    device.RenderState.ZBufferWriteEnable = false;

    device.Transform.World = Matrix.Scaling(15,15,15) *
                             Matrix.Translation(0,22,0);
    device.RenderState.Lighting = false;
    for(int i = 0; i < levelTextures.Length; i++)
    {
        device.SetTexture(0, levelTextures[i]);
        levelMesh.DrawSubset(i);
    }

    // Turn the zbuffer back on
    device.RenderState.ZBufferEnable = true;
    device.RenderState.ZBufferWriteEnable = true;

    // Now, turn back on our light
    device.RenderState.Lighting = true;

    // Render the player
    player.Draw(device, (float)appTime);

}

The first part of this method is naturally the replacement code for rendering the sky box that you removed earlier this chapter. The only new code here is the addition of the render call to the player.

Hooking Up the Level

The rest of the interaction with the player has already been implemented inside the code for the Level object, so for now you are done with this object. However, you do need to add the level into the game engine. Go ahead and declare some variables for it:
// The current level
private Level currentLevel = null;
// What level are we currently playing
private int levelNumber = 1;
// Is the level currently being loaded
private bool isLoadingLevel = false;
// Current camera position
private Vector3 currentCameraPosition;
// The amount of time the 'winning' screen is displayed
private float winningLevelTime = 0.0f;

The first two variables are pretty obvious: you need to know which level number you're currently playing as well as the level object itself. You also want to do a quick "flyby" of the level before you actually allow the player to start playing, which is controlled by the Boolean variable and the current camera position. Finally, after a level is finished, you want a message notifying the player to show onscreen for some period of time.
You should create the initial level at the same time you create the player object, directly after the character selection screen. So at the end of the CreateGraphicsObjects method, add a call to the method that will control the creation of the level:
// Load the current level
LoadCurrentLevel();

You can find the implementation of this method in Listing 9.5.
Listing 9.5. Loading the Current Level
private void LoadCurrentLevel()
{
    // Load our level
    currentLevel = new Level(levelNumber, device, player);

    // Create a random camera location
    CreateRandomCamera();

    // Now we're loading the level
    isLoadingLevel = true;
    winningLevelTime = 0.0f;
}

As the name implies, the CreateRandomCamera method creates a random starting location of the camera, which then "flies" into position. You set the loading level variable to TRue, so later you'll know if the user can play the level yet. Because you obviously don't win the level when it is first loaded, you can reset the time here as well. You'll find the implementation for the camera method in Listing 9.6.
Listing 9.6. Creating a Random Camera Location
private void CreateRandomCamera()
{
    Random r = new Random();

    currentCameraPosition = new Vector3(
        (float)r.NextDouble() * 5.0f + 15.0f,
        (float)r.NextDouble() * 10.0f + 120.0f,
        (float)r.NextDouble() * 5.0f + 15.0f
        );
    // Randomly switch
    if (r.Next(100) > 49)
    {
        currentCameraPosition.X *= -1;
    }
    // Randomly switch
    if (r.Next(100) > 49)
    {
        currentCameraPosition.Z *= -1;
    }
}

There's nothing unusual going on in this method. You use the random number generator to generate a random location, always very high above the level. Because the random number generator only returns positive numbers, the X and Z axes are also randomly changed to negative.
As with the player, you also need to ensure that the level and now the camera are updated every frame. You can finish up the OnFrameMove method by including the code in Listing 9.7 at the end of the method.
Listing 9.7. Updating the Level and Camera
// Make sure the level isn't started until the camera moves to position
UpdateCamera(elapsedTime);

// See if we need to keep showing the 'you win' screen
if ((currentLevel.IsGameWon) && (winningLevelTime > ShowWinningTime))
{
    // Make sure the new level exists
    if (Level.DoesLevelExist(levelNumber + 1))
    {
        // Reset the variables
        levelNumber++;
        LoadCurrentLevel();
    }
}

// If the level is won, then increment our timing for showing the screen
if (currentLevel.IsGameWon)
{
    winningLevelTime += elapsedTime;
}

// Update the current level
if (!isLoadingLevel)
{
    currentLevel.Update(elapsedTime);
}

The UpdateCamera method is used to fly the camera into position (if it isn't already there), and you need to implement this method. Skipping that for a moment, what else happens here? For one, you can detect whether the level has been won and, if it has, whether the winning message has been shown for long enough. You need to declare the constant used here as well:
private const float ShowWinningTime = 2.5f;

This value shows the winning message for two and a half seconds; feel free to adjust it to suit your needs. Assuming the level has been won for more than two and a half seconds, you can then check whether the next level exists and, if it does, increment the level counter and load the new level. This part resets all the game states and the winning time.
Otherwise, the only thing you want to do is update the winning time variable if the level has been won and call the Update method on your level object, which updates all the states in the level. You still need to implement the camera method, however, and you find that in Listing 9.8.
Listing 9.8. Implementing a Camera Flyby
private void UpdateCamera(float elapsedTime)
{
    if (currentCameraPosition != CameraDefaultLocation)
    {
        Vector3 diff = CameraDefaultLocation - currentCameraPosition;
        // Are we close enough to just move there?
        if (diff.Length() > (MaximumSpeed * elapsedTime))
        {
            // No we're not, move slowly there
            diff.Normalize();
            diff.Scale(MaximumSpeed * elapsedTime);
            currentCameraPosition += diff;
        }
        else
        {
            // Indeed we are, just move there
            currentCameraPosition = CameraDefaultLocation;
        }

        // Set the view transform now
        device.Transform.View = Matrix.LookAtLH(currentCameraPosition,
            CameraDefaultLookAtLocation, CameraUp);
    }
    else
    {
        isLoadingLevel = false;
    }
}

First, you notice a few constants that haven't been declared yet, so you want to do that now:
// Camera constants
private const float MaximumSpeed = 30.0f;
private static readonly Vector3 CameraDefaultLocation = new Vector3(
    -2.5f, 25.0f, -55.0f);
private static readonly Vector3 CameraDefaultLookAtLocation = new Vector3(
    -2.5f, 0.0f, 0.0f);
private static readonly Vector3 CameraUp = new Vector3(0,1,0);

These constants define where the camera's final destination should be, as well as where the camera will be looking, and the up direction of the camera. (The latter two won't change.) The movement code is similar to what was used to move the player into the correct position. If the camera is not already in the correct position, it is moved directly into position (if is close enough to move there this frame). If it isn't close enough to move there directly, the camera is moved toward the final location at a rate of 30 units per second. After the camera is in place, the isLoadingLevel variable is set to false, and the game play can begin.
Notice that if the camera's position changes, the View transform on the device is updated as well. You originally set this transform in the OnResetDevice method with some default values. Because the camera can be moving when the device is reset, you want to update the camera back to the correct position, not the default values you have there now. Replace the setting of the View transform in the OnResetDevice method with the following:
// Set the view transform now
device.Transform.View = Matrix.LookAtLH(currentCameraPosition,
    CameraDefaultLookAtLocation, CameraUp);

As you see, this is the same transform you set in the UpdateCamera method; it's now used when the device is reset as well. You've almost got everything implemented, but before you finish the rest of the game engine, there's one important thing to take care of. 

Comments

Popular Posts