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