Developing the Console


I have referred numerous times to the console for the game. Consoles became popular in recent years through games like Quake and Half-Life that exposed the console to the players. Even if you do not wish your players to have access to a console in your games , you should include a console for your own use. Consoles provide a powerful tool during the development of a game. With a console, we have the ability to display process flow information and statistics from the game to the screen in a logical and controlled manner. We can tweak the game by setting variables on the fly and see immediately what the effect is on game play. If at release time we decide that the player should not have access to the console, we simply unmap the command that displays the console.

The console will be defined as a static class. We want only one console to exist, and we want it existing through the entire game. You ve seen in some of the earlier code examples that the console is the place we post any exception data to. By making the console static, we can access the console from anywhere in the game engine without having to get access to a specific instance of the console class.

To access the console, we will map a function that toggles console visibility to the F12 key on the keyboard. You could select any key on the keyboard, but for this example I chose the F12 key because it is common to every modern keyboard, and it is not likely that it will need to be mapped to any other function.

Defining Console Components

Before we get into the design of the console itself, we need to define a class to hold command information for us. For the console to be more than a glorified list box, we need it to be able to accept and execute commands. The GameCommand class will hold the required information for each command. Remember that console commands are not limited to the needs of the game engine itself. Any game that is employing the game engine will need to add their commands. The GameCommand class will need to hold three pieces of information: the command string itself, a help string for the command that explains the use and syntax of the command, and a delegate of the function to be called when the command is entered. Every command processed by the console will accept a string as its only argument. That way the console can be consistent by passing the text following the command to the command s function (even in the case where the string is null because there was no text after the command). Two read-only properties provide external access to the command and help strings. The code for the class declaration is shown in Listing 2 “21.

Listing 2.21: GameCommand Declaration
start example
 public class GameCommand  {     public delegate void CommandFunction(string sData);     private string           m_sCommand = null;     private string           m_sHelp = null;     private CommandFunction m_Function = null;     public string Command { get { return m_sCommand; } }     public string Help { get { return m_sHelp; } } 
end example
 

The constructor for a GameCommand (shown in Listing 2 “22) will simply accept the three required pieces of information and copy them into its private members .

Listing 2.22: GameCommand Constructor
start example
 public GameCommand(string sCmd, string sHelp, CommandFunction pFunc)  {     m_sCommand = sCmd;     m_sHelp = sHelp;     m_Function = pFunc;  } 
end example
 

The class will have an Execute method that the console will call when it recognizes that this command has been invoked. The argument for the method is the remainder of the entered command string after the command has been extracted. Before calling the function, we will verify that the developer has indeed provided a method delegate. If so, we call the method and give it a string of data. It is the function s responsibility to parse the string to get the information that it expects. See Listing 2 “23 for the C# implementation.

Listing 2.23: GameCommand Execute Method
start example
 public void Execute(string sData)  {     if (m_Function != null)     {        m_Function(sData);     }  } 
end example
 

Now that we have defined how we will handle the console commands, it is time to create the console itself. The console will have an image as the background for the text. This will not only make the console look better, but serves the purpose of ensuring good contrast between the console text and the image behind it. The image for the console therefore should be quite dark, since we are using a light color for our text. Any variations in the background color should be kept over to the right side of the console, because text will rarely extend all the way across the screen.

We could just pop the console onto the screen when the console is opened and make it disappear when it is closed. Even though this would be the easiest solution, it looks better if the console appears to open onto the screen. For this console, we will have the console appear to slide down from the top of the screen when it opens and slide back up when it closes . The amount of screen that the console covers will also be adjustable. We will set the console to open to half the screen by default and supply the means later to adjust the console size .

Defining the Console Class

The attributes of the Console class are shown in Listing 2 “24. You will notice that we are using two different types of container classes for the console. The ArrayList class is a simple dynamic array container that is fine for storing the lines of text that will be displayed on the console. The SortedList class is a little more involved. It stores objects sorted by a key that is associated with each object and can locate the object based on a binary search of the keys. There are two of these SortedList containers, and both will hold instances of the GameCommand class. One set is commands that the console will understand. The other set is parameters that the console will be able to modify. In reality, the parameters will just be more methods that happen to set a variable. Listing 2 “24 shows the declaration of the Console class attributes.

Listing 2.24: Console Class Declaration
start example
 public class Console  {  #region Attributes     private static bool          m_bVisible = false;     private static bool          m_bOpening = false;     private static bool          m_bClosing = false;     private static ArrayList     m_Entries  = new ArrayList();     private static SortedList    m_Commands = new SortedList();     private static SortedList    m_Parameters = new SortedList();     private static GraphicsFont  m_pFont = null;     private static StringBuilder m_Entryline = new StringBuilder();     private static Image         m_Image;     private static float         m_Percent = 0.0f;     private static float         m_MaxPercent = 0.50f;  #endregion 
end example
 

The first thing you should notice about the Console attributes is that they are all declared as static. This supports the design decision that only one copy of the console may exist in an application. You should also notice that the entry line variable is a StringBuilder object rather than a string. Strings in C# may not be modified after they have been created. Any operation that appears to modify a string is actually creating and returning a copy of the original string with the appropriate modifications. The StringBuilder class is designed for strings that will change over time. Since the entry line will be created one character at a time, this is the way we should go.

The constructor for the class will be the only nonstatic method in the class. It will initialize the console with a supplied font and background image. The portions of the initialization of the console that may be repeated later are placed in the Reset method. The constructor and the Reset method appear in Listing 2 “25.

Listing 2.25: Console Constructor and Reset Method
start example
 public Console(GraphicsFont pFont, string sFilename)  {     m_pFont = pFont;     m_Image = new Image(sFilename);     Reset();     AddCommand("SET", "Set a paramter to a value", new CommandFunction(Set));     AddCommand("HELP", "Display command and Parameter help",          new CommandFunction(Help));     AddParameter("CONSOLESIZE",          "The percentage of the screen covered by the console from 1.0 to 100.0",          new CommandFunction(SetScreenSize));         }         public static void Reset()         {            m_Entries.Clear();            AddLine("type 'Help' or 'help command' for command descriptions");            m_Entryline = new StringBuilder(">");  } 
end example
 

The font is stored for later use in rendering the text to the console. The background image is created using the Image class that you should be quite familiar with by now. We then add a line of text to the console informing the player that there is help available. When we get to the command parser, we will implement the help. Typing HELP will cause a complete list of possible commands to be displayed on the console. Typing HELP followed by the name of one of the commands will cause the help string that we associated with the command to be displayed. The entry line is then initialized with an entry prompt character. The last thing to do is to add the commands that are hard coded into the console. The CONSOLESIZE command is used to adjust the console size. The set command is used for setting the parameters.

Rendering the Console

Now we will look at displaying the console with its Render method. This is another long method, so we will take it a bit at a time. The first thing the method does is capture the current fog state and turn off fogging. The method then checks to see if the console is currently being displayed. If the visible flag is not set, the Render method returns without rendering anything. If the console is visible, we must determine how much of the console to display. If the console is opening, we increase the visible percentage of the console. If it is closing, we decrement the visible percentage. Once the console is fully closed, the visibility flag is cleared. The initial part of this method appears in Listing 2 “26a.

Listing 2.26a: Console Render Method
start example
 public static void Render()  {     if (m_bVisible)     {        bool fog_state = CgameEngine.Device3D.RenderState.FogEnable;        CgameEngine.Device3D.RenderState.FogEnable = false;        // Determine how much of the console will be visible based on        // whether it is opening,        // open, or closing.        if (m_bOpening && m_Percent <= m_MaxPercent)        {           m_Percent += 0.05f;        }        else if (m_bClosing && m_Percent >= 0.0f)        {           m_Percent   = 0.05f;           if (m_Percent <= 0.0f)           {              m_bClosing = false;              m_bVisible = false;           }        } 
end example
 

The next step in rendering the console is to render the console background image to that portion of the console that is visible. To get our scrolling effect when the console opens and closes, we will adjust the vertices that define the console rectangle. As the console opens, the rectangle will move down vertically from the top of the screen. Other than adjusting the vertices for the scrolling effect, this is just like rendering the splash screen. This code is shown in Listing 2 “26b.

Note

Remember that with the catch code we have used so far with the other classes, we always send the text of the error message to the console for display. It doesn t make sense to do that if the problem is with the console itself. Instead, we will send the text to the Developer Studio output window using the Debug class that Microsoft supplies in the System.Diagnostics namespace.

Listing 2.26b: Console Render Method
start example
 // Render the console background.  try  {     int line = (int)((m_Percent*CGameEngine.Device3D.Viewport.Height) - 5        m_pFont.LineHeight);     if (line > 5)     {        // Draw the image to the device.        try        {           CustomVertex.TransformedTextured[] data =                   new CustomVertex.TransformedTextured[4];           data[0].X =   CGameEngine.Device3D.Viewport.Width;           data[0].Y =     0.0f - (1.0f-m_Percent)*                   CGameEngine.Device3D.Viewport.Height;           data[0].Z =     0.0f;           data[0].Tu =    1.0f;           data[0].Tv =    0.0f;           data[1].X =     0.0f;           data[1].Y =     0.0f - (1.0f-m_Percent)*                   CGameEngine.Device3D.Viewport.Height;           data[1].Z =     0.0f;           data[1].Tu =    0.0f;           data[1].Tv =    0.0f;           data[2].X =     0.0f;           data[2].Y = CGameEngine.Device3D.Viewport.Height -                     (1.0f-     m_Percent)*CGameEngine.Device3D.Viewport.Height;           data[2].Z =    0.0f;           data[2].Tu =   0.0f;           data [2],TV =  1.0f;           data[3].X =    0.0f;           data[3].Y = CGameEngine.Device3D.Viewport.Height -                     (1.0f-m_Percent)*CGameEngine.Device3D.Viewport.Height;           data[3].Z =    0.0f;           data[3].Tu =   0.0f;           data[3].Tv =   1.0f;           VertexBuffer vb = new VertexBuffer(typeof(CustomVertex.TransformedTextured), 4, CGameEngine.Device3D,                  Usage.WriteOnly, CustomVertex.TransformedTextured.Format,                  Pool.Default);           vb.SetData(data, 0, 0);           CGameEngine.Device3D.SetStreamSource(0, vb, 0);           CGameEngine.Device3D.VertexFormat =                     CustomVertex.TransformedTextured.Format;           CGameEngine.Device3D.RenderState.CullMode =                     Microsoft.DirectX.Direct3D.Cull.Clockwise;           // Set the texture.           CGameEngine.Device3D.SetTexture(0, m_Image.GetTexture());                  // Render the face.           CGameEngine.Device3D.DrawPrimitive(PrimitiveType.TriangleStrip, 0, 2);        }        catch (DirectXException d3de)        {           Console.AddLine("Unable to display SplashScreen ");           Console.AddLine(d3de.ErrorString);        }        catch (Exception e)        {           Console.AddLine("Unable to display SplashScreen ");           Console.AddLine(e.Message);        } 
end example
 

Now that the console background is rendered, it is time to add the text. A variable called line is used to designate the vertical position of where each line of text will be rendered. Since the console is dropping down from the top of the screen, we will render the text in the console from the bottom up until we run out of either room in the console or lines of text to display. The first line of text will be five pixels up from the bottom plus the height of the font. This will give us a small margin area at the bottom of the screen. We will render text only if there are at least five pixels of margin space above the line as well. We will start by rendering the entry line, since it will always be the bottom line of the console. After rendering a line, we decrement the line variable by the height of the font to prepare for rendering the next line. We then repeat this procedure for each entry in the m_Entries array of text lines. Again, we render the line only if there is enough room left in the console to include a five-pixel margin. The code that completes the Render method appears in Listing 2 “26c.

Listing 2.26c: Console Render Method (Conclusion)
start example
 m_pFont.DrawText(2, line, Color.White, m_Entryline.ToString());  line -= (int)m_pFont.LineHeight;  foreach (String entry in m_Entries)  {     if (line > 5)     {         m_pFont.DrawText(2, line, Color.White ,entry);        line -= (int)m_pFont.LineHeight();     }                      }                  }                }                catch (DirectXException d3de)                {                   Debug.WriteLine("unable to render console");                   Debug.WriteLine(d3de.ErrorString);                }                catch (Exception e)                {                   Debug.WriteLine("unable to render console");                   Debug.WriteLine(e.Message);                }                CGameEngine.Device3D.RenderState.FogEnable = fog_state;            }  } 
end example
 

Defining Additional Methods for the Console

Now that we can create and render a console, we need to look at the various methods that we will use to interact with the console. The first two methods are two versions of SetMaxScreenSize . The first version takes a float value as an argument and is used to programmatically set the size of the console. The second version takes a string for an argument and is used as a parameter set function to set the console size through the console itself. In both methods, we need to take care that the screen size is within a valid range of 10 to 100 percent. These methods are shown in Listing 2 “27.

Listing 2.27: Console SetMaxScreenSize and SetScreenSize Methods
start example
 public void SetMaxScreenSize (float fPercent)  {     if (fPercent < 10. of) fPercent = 10.0f;     if (fPercent > 100. 0f) fPercent = 100.0f;     m_MaxPercent = fPercent / 100.0f;  }  public void SetScreenSize(string sPercent)  {     float f;     try     {        f = float.Parse(sPercent);     }     catch     {        f = 50.0f;     }      SetMaxScreenSize (f);  } 
end example
 

We use the next few methods to add text to the console and to process keystrokes made while the console is open. Listing 2 “28 shows the AddLine method used to place strings onto the console display. This method inserts a line of text into the beginning of the m_Entries array of strings. This array will keep only the last 50 lines of text sent to the console. When the 51st line is inserted at the beginning of the array, the last entry in the array is removed.

Listing 2.28: Console AddLine Method
start example
 public static void AddLine(string sNewLine)  {     m_Entries.Insert(0,sNewLine);     if (m_Entries.Count > 50)     {        m_Entries.RemoveAt(50);     }     System.Diagnostics.Debug.WriteLine(sNewLine);  } 
end example
 

The OnKeyDown message handler of the CD3DApplication class calls the next three methods. If a KeyDown message is received, the method calls one of four methods. The first three methods are for data entry and the fourth controls the open state of the console. We will look at that method shortly. The AddCharacterToEntryLine method takes a single character and appends it to the entry line if the console is currently displayed. It is called if the entered character is a letter, number, space, or decimal point. If the player hits the Backspace key, the Backspace method is called to remove the last character in the entry line. If the Enter key is pressed, it is time to process the entry line and execute whatever command is found there. This is done using the ProcessEntry method, which separates the command part of the string from the entry prompt. The command is entered into the console text list and passed to the ParseCommand method for processing. The entry line is reset to just the prompt character and is ready to start receiving text again. These data entry methods appear in Listing 2 “29.

Listing 2.29: Console Data Entry Methods
start example
 public static void AddCharacterToEntryLine(string sNewCharacter)  {     if (m_bVisible) m_Entryline.Append(sNewCharacter);  }  public static void Backspace()  {     if (m_Entryline.Length > 1)     {         m_Entryline.Remove(m_Entryline.Length-1,1);     }  }  public static void ProcessEntry()  {     string sCommand = m_Entryline.ToString().Substring(1,m_Entryline.Length-1);     AddLine(sCommand);     m_Entryline.Remove(1,m_Entryline.Length-1);     ParseCommand(sCommand);  } 
end example
 

The ParseCommand method (shown in Listing 2 “30) divides the command string into two pieces. It begins by using the string class s Trim method to remove any white space at the beginning and end of the string. Any extra spaces could confuse the parser. It then locates the first space in the string. If there is a space, then we know that we need to break the string up into two pieces. If not, the string is a command without any data, and we will pass null to the ProcessCommand method for the data string.

Listing 2.30: Console ParseCommand Method
start example
 private static void ParseCommand(string sCommand)  {      // Remove any extra white space.      sCommand.Trim();      // Find the space between the command and the data (if any),      int nSpace = sCommand.IndexOf(" ");      // Is there any data?      if (nSpace > 0)      {         string sCmd = sCommand.Substring(0,nSpace);         string sData = sCommand.Remove(0,nSpace+1);         ProcessCommand(sCmd, sData);      }      else      {         ProcessCommand(sCommand, null);      }  } 
end example
 

The ProcessCommand method (shown in Listing 2 “31) does a binary search within the m_Commands list to see if the command entered exists in the list. If the command is not found, an Unrecognized Command message is posted to the console. If the command is found, we pull a reference to the GameCommand structure from the list and call its Execute function. The data portion of the entered string is forwarded on to the registered delegate function.

Listing 2.31: Console ProcessCommand Method
start example
 private static void ProcessCommand (string sCmd, string sData)  {      int nIndex = m_Commands.IndexOfKey(sCmd);      if (nIndex < 0) // Not found      {         AddLine("Unrecognized Command");      }      else      {         GameCommand Cmd = (GameCommand)m_Commands.GetByIndex(nIndex);         Cmd.Execute(sData);      }  } 
end example
 

That is all there is to processing console commands. Now we will take a look at how we manage console commands and parameters. The next few methods are used to add and remove commands and parameters. We saw earlier what the Add method calls looked like. Now we will see what they actually do. The AddCommand method takes as arguments the data needed to construct a GameCommand object. Using this information, an object is created and added to the m_Commands list with the command string as the key. Removing the command from the list is equally straightforward. The list is searched for the command using the supplied string as the desired key. If an entry exists on the list with this key (i.e., the returned index is not negative), the entry is removed from the list. The code for adding and removing commands appears in Listing 2 “32.

Listing 2.32: Console Command Methods
start example
 public static void AddCommand(string sCmd, string sHelp,     GameCommand.CommandFunction Func)  {     GameCommand Cmd = new GameCommand (sCmd, sHelp, Func);     m_Commands.Add(sCmd, Cmd);  }  public static void RemoveCommand(string sCmd)  {     int nIndex = m_Commands.IndexOfKey(sCmd);     if (nIndex >= 0)     {        m_Commands.RemoveAt(nIndex);     }  } 
end example
 

The AddParameter and RemoveParameter methods (shown in Listing 2 “33) work the same way. The only significant difference is which list we work with.

Listing 2.33: Console Parameter Methods
start example
 public static void AddParameter(string sParam, string sHelp,     GameCommand.CommandFunction Func)  {     GameCommand Cmd = new GameCommand(sParam, sHelp, Func);     m_Parameters.Add (sParam, Cmd);  }  public static void RemoveParameter(string sParam)  {     int nIndex = m_Parameters.IndexOfKey(sParam);     if (nIndex >= 0)     {        m_Parameters.RemoveAt(nIndex) ;     }  } 
end example
 

The next two methods (shown in Listing 2 “34) are used to change and check the visibility status of the console. ToggleState is called whenever the F12 key is pressed on the keyboard. If the console is currently visible, the Opening/Closing pair of flags is set to cause the window to begin closing. Conversely, if the console is closed, the flags are set to make the console visible and to begin the opening process. The IsVisible property is provided to allow other methods to test whether the console is open. One example is the GameInput class described at the beginning of the chapter. The action map section of the Poll method is wrapped in an if statement that prevents the action map from being processed while the console is open. If we are typing commands into the console, we do not want those keystrokes also being processed as game commands. There s no telling what state the game would be in when we finally close the console. It can also be used to automatically pause the game when the console is opened.

Listing 2.34: Console ToggleState Method and IsVisible Property
start example
 public static void ToggleState()  {     if (m_bVisible)     {        m_bClosing = true;        m_bOpening = false;     }     else     {        m_bOpening = true;        m_bVisible = true;     }  }  public static bool IsVisible()  { get {     return m_bVisible;  } } 
end example
 

Adding Help Capabilities to the Console

We have two methods left to discuss in the console. These are the two command functions that were added to the command list back in the constructor: Help (Listing 2 “35) and Set (Listing 2 “36). The Help method has several different ways that it may respond based on the data supplied with the command. If there is no data, the method will display a list of all known commands. If there is a known command or parameter name passed as the data, it will display the help string for that command or parameter. If it does not recognize the data as either a known command or parameter, it will report that the data is unrecognized. One special case is included at the end of the method. If the command in question is the Set command, the known parameters are displayed in addition to the help string associated with the Set command.

Listing 2.34: Console Help Method
start example
 private void Help(string sData)  {      StringBuilder sTemp = new StringBuilder();      if (sData == null)  {     AddLine("Valid Commands");     foreach (string sCmds in m_Commands.Keys)     {        sTemp.Append(sCmds);        sTemp.Append (" ");        if (sTemp.Length > 40)        {           AddLine(sTemp.ToString());           sTemp.Remove(0,sTemp.Length-1);        }     }     if (sTemp. Length > 0)     {        AddLine(sTemp.ToString());     }  }  else  {     int nIndex = m_Commands.IndexOfKey(sData);     if (nIndex < 0) // Not found     {        nIndex = m_Parameters.IndexOfKey(sData);        if (nIndex < 0) // not found        {     AddLine("Unrecognized Command");        }        else        {           GameCommand Cmd = (GameCommand)m_Parameters.GetByIndex(nIndex);           string sHelpText = sData + "   " + Cmd.GetHelp();           AddLine(sHelpText);        }     }     else     {        GameCommand Cmd = (GameCommand)m_Commands.GetByIndex(nIndex);        string sHelpText = sData + "   " + Cmd.GetHelp();        AddLine(sHelpText);     }  }  if (sData == "SET")     {        AddLine("Valid Parameters");        foreach (string sCmds in m_Parameters.Keys)        {           sTemp.Append(sCmds);           sTemp.Append (" ");           if (sTemp.Length > 40)           {              AddLine(sTemp.ToString());              sTemp.Remove (0, sTemp.Length-1) ;           }        }        if (sTemp.Length > 0)        {           AddLine(sTemp.ToString());        }     }  } 
end example
 
Listing 2.36: Console Set Method
start example
 private void Set(string data)  {     StringBuilder sTemp = new StringBuilder();     int nSpace = data.IndexOf(" ");     if (nSpace > 0)     {        string sCmd = data. Substring(0, nSpace);        string sData = data.Remove(0,nSpace+1);        int nIndex = m_Parameters.IndexOfKey(sCmd);        if (nIndex < 0) // Not found        {           AddLine("Unrecognized Parameter")  ;  }        else        {           GameCommand Cmd = (GameCommand)m_Parameters.GetByIndex(nIndex);           Cmd.Execute(sData);        }     }  } 
end example
 

The Set method (Listing 2 “36) is used to modify a parameter. To accomplish this, it must first identify the parameter to be changed. The data string is parsed to divide into a parameter name and a data string. The parameter name is then looked up in the parameter list. If the parameter is located, its associated function is called with the data portion of the string.




Introduction to 3D Game Engine Design Using DirectX 9 and C#
Introduction to 3D Game Engine Design Using DirectX 9 and C#
ISBN: 1590590813
EAN: 2147483647
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net