Implementing the Level for a 3D game


Implementing the Level

Before you can start implementing the level, you need to add a new code file to your project to hold the level's code. You can call this file level.cs and add it to your project now. The initial implementation of your level class appears in Listing 8.1.
Listing 8.1. The Level Class
public class Level
{
    private const float Spacing = 3.35f;
    private const int SquareSize = 13;
    private const float StartingLocation = (Spacing * SquareSize) / 2.0f;

    // The list of blocks that comprise the level
    private Block[] blocks = null;
    // The final color to win
    private BlockColor finalColor;
    // Store the player
    private Player currentPlayer = null;
    // Block index of the player
    private int playerIndex;
    // Total number of moves made so far
    private int totalMoves = 0;
    // Maximum moves allowed
    private int maxMoves = 0;
    // Maximum time allowed
    private float maxTime = 0.0f;
    // The amount of time that has currently elapsed
    private float elapsedTime = 0.0f;
    // Amount of time remaining
    private TimeSpan timeLeft;
    // Is the game over for any reason?
    private bool isGameOver = false;
    // Did you win the game once it was over?
    private bool isGameWon = false;
    // Is this the first game over?
    private bool isFirstGameOver = true;

    // Number of blocks (total)
    private int numberBlocks = 0;
    // Number of blocks that are currently correct
    private int numberCorrectBlocks = 0;
}

The game uses the constants here when determining the location of the blocks within the level. Each level contains a maximum of 13 rows of blocks and a maximum of 13 columns in each row. Not every block in this square needs to be filled, but this number is used as the maximum size of the game board. Also, the blocks are evenly spaced, 3.35 units apart. Considering that the cubes themselves are 3.0 units in size, this spacing makes sense because the blocks do not touch. The starting location constant marks where the beginning block is in world space (that is, blocks in row or column zero).
This list contains all the class variables you need for the entire level class. You will not need to add any more later in the chapter. Obviously, you need an array of blocks (which was originally going to be a class of itself before) and the "final" color each block must be turned to to win the level. You also need to know the player object for the game and the index of the block the player is currently standing on.
Because you need to know the total number of moves the player has made, as well as the maximum number of moves allowed on this level, you need to store them as well. Along with the moves, each level also has a strict time limit, which you also need to store, just as you do the amount of time left.
Because you want the blocks to explode when the game is over, and you want to notify the player about the game status, you need to know whether the level is overand if it is over, whether the level was won or lost. If the game was lost, the first time you want to assign velocities to the blocks to allow them to explode. Finally, you need to store the number of blocks in the level and the number of those that are correct so you can inform the player.
As mentioned in an earlier chapter, the levels themselves are loaded from a file (or the resources of the executableeither is acceptable). Add the constructor in Listing 8.2 to your level class now.
Listing 8.2. Creating an Instance of the Level Class
public unsafe Level(int level, Device device, Player player)
{
    // Check the params
    if (player == null)
    {
        throw new ArgumentNullException("player");
    }

    // Store the current player object
    currentPlayer = player;

    // First we need to read in the level file. Opening the file will throw an
    // exception if the file doesn't exist.
    using(FileStream levelFile = File.OpenRead(GameEngine.MediaPath +
                string.Format("Level{0}.lvl", level)))
    {

        // With the file open, read in the level data. First, the maximum
        // amount of time you will have to complete the level
        byte[] maxTimeData = new byte[sizeof(float)];
        levelFile.Read(maxTimeData, 0, maxTimeData.Length);
        maxTime = BitConverter.ToSingle(maxTimeData, 0);

        // Now read in the maximum moves item
        byte[] maxMoveData = new byte[sizeof(int)];
        levelFile.Read(maxMoveData, 0, maxMoveData.Length);
        maxMoves = BitConverter.ToInt32(maxMoveData, 0);

        // Determine how many colors will be used.
        byte[] numColorsData = new byte[sizeof(int)];
        levelFile.Read(numColorsData, 0, numColorsData.Length);
        int numColors = BitConverter.ToInt32(numColorsData, 0);

        // Create the block of colors now
        BlockColor[] colors = new BlockColor[numColors];
        for (int i = 0; i < colors.Length; i++)
        {
            colors[i] = (BlockColor)levelFile.ReadByte();
        }s

        // Get the final color now
        finalColor = (BlockColor)levelFile.ReadByte();

        // Determine if the colors will rotate.
        byte[] rotateColorData = new byte[sizeof(bool)];
        levelFile.Read(rotateColorData, 0, rotateColorData.Length);
        bool rotateColor = BitConverter.ToBoolean(rotateColorData, 0);

        // Create the array of blocks now to form the level
        blocks = new Block[SquareSize * SquareSize];

        int blockIndex = 0;

        for (int j = 0; j < SquareSize; j++)
        {
            for (int i = 0; i < SquareSize; i++)
            {
                byte tempBlockColor = (byte)levelFile.ReadByte();

                if (tempBlockColor != 0xff)
                {
                    Block b = new Block(device, colors, tempBlockColor,
                                        rotateColor);
                    b.Position = new Vector3(-StartingLocation +
                        (Spacing * i), -1.5f,
                        -StartingLocation + (Spacing * j));

                    blocks[blockIndex++] = b;
                }
                else
                {
                    blockIndex++;
                }
            }
        }

        // Get the default player starting position
        byte[] playerIndexData = new byte[sizeof(int)];
        levelFile.Read(playerIndexData, 0, playerIndexData.Length);
        playerIndex = BitConverter.ToInt32(playerIndexData, 0);
    }
    UpdatePlayerAndBlock(true, playerIndex);

}s

One of the first things you should notice is the use of the unsafe keyword in the definition of the constructor. This keyword allows you to use things deemed unsafe in C#, such as the sizeof keyword. Soon you will see why this keyword is necessary. After a quick parameter check, and storing the current player object, you actually open a file that will hold the level data, based on the level number. The using keyword here automatically closes the file when you're done with it so you don't need to worry about doing that on your own.
What you're probably thinking now is "What files?" Aside from the fact that numerous levels are included on the accompanying CD, I haven't yet mentioned how these files would be constructed. In Appendix A, "Developing a Level Creator," you will see an application that will act as the level creator for this game. Obviously, level creation is an important aspect of creating a top-notch game, but it isn't central to the 3D game development aspect this book covers, so it resides in the appendix. You might also notice that you never actually check whether the file exists. Add this public method to your class so the game engine can detect whether a given level exists before it tries to load it:
public static bool DoesLevelExist(int level)
{
    return File.Exists(GameEngine.MediaPath +
        string.Format("Level{0}.lvl", level));
}

The file format itself is a relatively simple binary file. The first four bytes of the file are the maximum time (in seconds) you have to complete the level. It is stored in a float variable, and the constructor creates a new byte array of the size of a float variable. This is the code that required the use of the unsafe keyword. After that byte array is filled, the BitConverter class is used to take that array and convert it back to the float value it is intended to be.
Although multiple parameters are read from the file, each is read in the same way described in the last paragraph. Next, the maximum number of moves to complete the level is read in as an integer, followed by how many colors are used for the level. It should always be at least two and a maximum of four. Recall that the BlockColor enumeration from the last chapter was derived from the byte data type, so after creating an array of the appropriate number of colors, you can read in each color individually, a single byte at a time. You do the same with the final color, which is the next piece of data read in.
Next you need to determine whether the colors will wrap or not and read that in as a Boolean value. The block array size is always the same size, and the next 169 bytes of the file tell you everything you need to know about the blocks themselves. Each byte is either the index into the color array you just created or 0xff, signifying that there is no block at this location. First, create your array of blocks of the appropriate size, and then for each block you find that is a real block, create a new Block class, passing in the array of colors, the color index of the current block, and whether the block colors should rotate. You then assign it a position based on a simple formula using the starting location, the spacing constant, and the row or column index.
After each of the blocks is created, you need to store the index of the block the player is currently sitting on. Because the player is actually on a block now, you want to update the block's color to the next available color. The UpdatePlayerAndBlock method moves the player to the correct position as well as updates the block's state. You find the implementation of this method in Listing 8.3.
Listing 8.3. Updating Player and Blocks
private bool UpdatePlayerAndBlock(bool force, int newPlayerIndex)
{
    // Get the block the current player is over
    Block b = blocks[newPlayerIndex] as Block;

    if (force)
    {
        // Move the player to that position
        currentPlayer.Position = new Vector3(b.Position.X, 0, b.Position.Z);
    }
    else
    {
        // Move the player to that position
        if (!currentPlayer.MoveTo(new Vector3(b.Position.X, 0, b.Position.Z)))
        {
            // The player can't move here yet, he's still moving,
            // just exit this method
            return false;
        }
    }

    // Update the color of this block
    b.SelectNextColor();
    return true;
}

This code assumes that the player index is only on a currently valid block. Later, outside of the call to this method in the constructor, you will be checking for valid moves anyway, so the only time it's possible for this index to be invalid is in the constructor. Considering it comes from the file that describes the level (and the level creator does not allow the starting position to be an invalid block), this shouldn't be a problem. The force parameter is only used during the constructor, and it signifies that you want to move the player instantly to the position if true. Otherwise, the MoveTo method on the player is called, and if it is not successful, it returns that "failure" to the caller of this method. At the end of the method, the selected block is moved to the next available color. If the move was invalid, the method will have already returned and will never get to this code. The constructor ignores the return value because there is no way for the method to return any value other than TRue if the force parameter is true.

    Comments

    Popular Posts