The next user interface component to address is the option screen. Depending on the complexity of a game, it is possible to have multiple option screens that are accessed from a primary option screen. This is where players determine what kind of game they wish to play or if they want to exit from the game. The requirements for the option screens will drive the complexity of the controls used on the screens anywhere from simple buttons all the way up to controls reminiscent of MFC dialog boxes.
For our initial option screen, we will use a button-style interface. To support this, we will define a small class called ImageButton that will hold our button data.
To add a little flash to our buttons, we will define them as having three possible states: Off , Hover , and On . The Off state will have a basic image of the button. The Hover state will show the button highlighted and will be displayed whenever the mouse cursor is over the button. This helps reinforce for players that they are pointing at a button. The On state can be used for items that they can toggle on and off (e.g., an image with a checked check box). We also need to indicate where on the screen the button should be displayed and potentially a method that should be called when the button is selected.
The declaration of the ImageButton class shown in Listing 2 “9 takes these requirements into account.
public delegate void ButtonFunction( ); public class ImageButton : IDisposable { private int m_X = 0 private int m_Y = 0; private Image m_OffImage = null; private Image m_OnImage = null; private Image m_HoverImage = null; private bool m_bOn = false; private bool m_bHoverOn = false; private ButtonFunction m_Function = null; private Rectangle m_Rect; private VertexBuffer m_vb = null; private CustomVertex.TransformedTextured[] data;
The Rectangle value within the class holds the size of the button. One assumption that is made about the three images of the button is that all three are the same size. If we mistakenly initialize the class with different- size images, we will have problems that range from only portions of the image being drawn to assertions generated because we are trying to draw a rectangle that is bigger than the image.
Note | I recommend that you develop the image for the Off button and then use it as the basis for creating the artwork for the other two. |
The constructor for the ImageButton class is a little more involved than the other classes we have dealt with so far. This constructor needs six arguments: the X and Y values for positioning the button on the screen, the filenames for the three images, and the delegate function to be called when the button is selected. In code that should look familiar from the SplashScreen class, we also create a vertex buffer with data to draw the button on the screen as a textured rectangle (see Listing 2 “10).
public ImageButton(int nX, int nY, string sOffFilename, string sOnFilename , string sHoverFilename, ButtonFunction pFunc ) { m_X = nX; m_Y = nY; m_OffImage = new Image( sOffFilename ); m_OnImage = new Image( sOnFilename ); m_HoverImage = new Image( sHoverFilename ); m_Rect = m_OffImage.GetRect(); m_Rect.X += m_X; m_Rect.Y += m_Y; m_Function = pFunc; m_vb = new VertexBuffer( typeof(CustomVertex.TransformedTextured), 4, CGameEngine.Device3D, Usage.WriteOnly, CustomVertex.TransformedTextured.Format, Pool.Default ); }
The Render and Dispose methods are shown in Listing 2 “11. The Render method works very much like the method used in the splash and option screens. The difference is in the coordinates used in setting up the vertex buffer. Instead of filling the screen, the two triangles that form the button fill the rectangle position assigned to the button. The Dispose method simply calls the Dispose method for each of the button images.
public void Render() { try { data = new CustomVertex.TransformedTextured[4]; data[0].X = (float)m_Rect.X; data[0].Y = (float)m_Rect.Y; data[0].Z = 1.0f; data[0].Tu = 0.0f; data[0].Tv = 0.0f; data[1].X = (float)(m_Rect.X+m_Rect.Width); data[1].Y = (float)m_Rect.Y; data[1].Z = 1.0f; data[1].Tu = 1.0f; data[1].Tv = 0.0f; data[2].X = (float)m_Rect.X; data[2].Y = (float)(m_Rect.Y+m_Rect.Height); data[2].Z = 1.0f; data[2].Tu = 0.0f; data[2].Tv = 1.0f; data[3].X = (float)(m_Rect.X+m_Rect.Width); data[3].Y = (float)(m_Rect.Y+m_Rect.Height); data[3].Z = 1.0f; data[3].Tu = 1.0f; data[3].Tv = 1.0f; m_vb.SetData(data, 0, 0); CGameEngine.Device3D.SetStreamSource( 0, m_vb, 0 ); CGameEngine.Device3D.VertexFormat = CustomVertex.TransformedTextured.Format; // Set the texture. CGameEngine.Device3D.SetTexture(0, GetTexture() ); // Render the face. CGameEngine.Device3D.DrawPrimitives( PrimitiveType.TriangleStrip, 0, 2 ); } catch (DirectXException d3de) { Console.AddLine( "Unable to display imagebutton " ); Console.AddLine( d3de.ErrorString ); } catch ( Exception e ) { Console.AddLine( "Unable to display imagebutton " ); Console.AddLine( e.Message ); } } public void Dispose() { m_OffImage.Dispose(); m_OnImage.Dispose(); m_HoverImage.Dispose(); }
Other classes that are rendering the buttons use the remaining methods in the class. The GetTexture method, shown in Listing 2 “12, returns the correct image surface depending on the current hover and activation state of the button.
public Surface GetTexture() { if ( m_bHoverOn ) { return m_HoverImage. GetTexture (); } else if ( m_bOn ) { return m_OnImage. GetTexture (); } else { return m_OffImage. GetTexture (); } }
The GetDestRect and GetSrcRect methods get the destination and source rectangles for the button. The two rectangles are the same size with the destination rectangle offset by the position of the button on the option screen. We also need a GetPoint method so that the rendering class knows where to put the button on the screen (see Listing 2 “13).
public Rect GetDestRect() { return m_Rect; } public Rect GetSrcRect() { return m_OffImage.GetRect(); } public Point GetPoint() { return new Point(m_X, m_Y); }
The last three methods in the class are used in the interaction of the button with the mouse. The first is a private method that checks to see if a supplied position is within the button rectangle. The next method sets the hover state for the button based on whether the supplied position is over the button. The last of the ImageButton methods process mouse button clicks on the button. The calling class calls the ClickTest method if the primary mouse button has been clicked. The current mouse position is checked to see if it is within the area of the button. If so, the state of the button is toggled. Finally, if the button is currently on and a function has been supplied for the button, the function is invoked. The code for these functions is shown in Listing 2 “14.
private bool InRect( Point p ) { return m_Rect. Contains ( p ); } public void HoverTest( Point p ) { m_bHoverOn = InRect( p ); } public void ClickTest( Point p ) { if ( InRect( p ) ) m_bOn = !m_bOn; if ( m_bOn && m_Function != null ) m_Function(); }
Now that we have our ImageButton class for rendering our buttons, it is time to build the option screen itself. The option screen will consist of a background image, an array of buttons that will appear on the screen, and a mouse cursor that will be controlled by mouse movement. The declaration of the OptionScreen class appears in Listing 2 “15.
public class OptionScreen { #region Attributes private Image m_Background = null; private Image m_Cursor = null; public bool m bInitialized = false; private ArrayList m_buttons = new ArrayList (); private Point m_MousePoint = new Point(0,0); private bool m_bMouseIsDown = false; private bool m_bMouseWasDown = false; private VertexBuffer m_vb; #endregion
The constructor for the option screen will require a single argument: the name of the file containing the background image for the screen. The constructor will load the background image and the image for the mouse cursor. The mouse cursor will use the DDS format since that format can include transparency information. The final step is to create the vertex buffer to be used in rendering the screen. Listing 2 “16 shows the constructor for the option screen.
public OptionScreen( string filename) { try { m_Background = new Image( filename ); m_Cursor = new Image ("Cursor.dds"); m_vb = new VertexBuffer( typeof(CustomVertex.TransformedTextured), 4, CGameEngine.Device3D, Usage.WriteOnly , CustomVertex.TransformedTextured.Format, Pool.Default ); } catch (DirectXException d3de) { Console.AddLine("Error creating Option Screen" + filename ); Console.AddLine( d3de.ErrorString ); } catch ( Exception e ) { Console.AddLine("Error creating Option Screen" + filename ); Console.AddLine( e.Message ); } }
Now we have the basic option screen, but it doesn t do much for us without any controls on it. We need a method that allows the game application to add buttons to the option page. This will be our AddButton method, which will take all of the same arguments as the ImageButton constructor. It will create a new ImageButton and add it to the list of buttons for the option screen as shown in Listing 2 “17.
public void AddButton(int nX, int nY, string sOffFilename, string sOnFilename , string sHoverFilename, ImageButton.ButtonFunction pFunc ) { ImageButton button = new ImageButton ( nX, nY, sOffFilename, sOnFilename, sHoverFilename, pFunc ); m_buttons.Add(button); }
We will need another helper method that will be called each frame prior to rendering the option screen. The SetMousePosition method will be used to update the position of the screen s mouse cursor and the primary button status. This is illustrated in Listing 2 “18.
public void SetMousePosition ( int x, int y, bool bDown ) { m_MousePoint.X = x; m_MousePoint.Y = y; m_bMouseIsDown = bDown; }
After creating the option screen and adding some buttons, we are ready to start rendering the screen. Just as in the splash screen, we need to capture the current fog state and disable fogging so that it does not affect the option screen or its buttons. This routine gets a bit involved, so we re going to step through this one a bit at a time. The routine begins by creating and filling an array of vertices for the rectangle used to render the background. The data is then used to render the background image using code similar to that used for the splash screens, as shown in Listing 2 “19.
public void Render() { try { bool fog_state = CgameEngine.Device3D.RenderState.FogEnable; CgameEngine.Device3D.RenderState.FogEnable = false; CustomVertex.TransformedTextured[] data = new CustomVertex.TransformedTextured[4]; data[0].X = 0.0f; data[0].Y = 0.0f; data[0].Z = 0.0f; data[0].Tu = 0.0f; data[0].Tv = 0.0f; data[1].X = CGameEngine.Device3D.Viewport.Width; data[1].Y = 0.0f; data[1].Z = 0.0f; data[1].Tu = 1.0f; data[1].Tv = 0.0f; data[2].X = 0.0f; data[2].Y = CGameEngine.Device3D.Viewport.Height; data[2].Z = 0.0f; data[2].Tu = 0.0f; data[2].Tv = 1.0f; data[3].X = CGameEngine.Device3D.Viewport.Width; data[3].Y = CGameEngine.Device3D.Viewport.Height; data[3].Z = 0.0f; data[3].Tu = 1.0f; data[3].Tv = 1.0f; m_vb.SetData(data, 0, 0); CGameEngine.Device3D.SetStreamSource( 0, m_vb, 0 ); CGameEngine.Device3D.VertexFormat = CustomVertex.TransformedTextured.Format; // Set the texture. CGameEngine.Device3D.SetTexture(0, m_Background.GetTexture() ); // Render the screen background. CGameEngine.Device3D.DrawPrimitive( PrimitiveType.TriangleStrip, 0, 2 );
Now that the background has been rendered, it is time to add the buttons. We will loop through the array of buttons. Before we render the button image onto the screen, we will call the button s HoverTest method with the current mouse position so that the button can determine if it needs to display the hover state image. Then if the primary mouse button is down, and the mouse button was not down last time, we will call the button s ClickTest method so that it may determine if the player has clicked the button and react accordingly . Finally, we have the button render itself to the screen. This code is shown in Listing 2 “20a.
// Copy on the buttons. foreach ( ImageButton button in m_buttons ) { button.HoverTest ( m_MousePoint ); if ( m_bMouseIsDown && !m_bMouseWasDown ) { button.ClickTest( m_MousePoint ); } button.Render(); }
After we have repeated this procedure for each button in the array, we save the status of the mouse button down flag for use on the next pass.
m_bMouseWasDown = m_bMouseIsDown;
Now that all of the buttons have been rendered, it is time to render the image of our mouse cursor. We do this last, of course, so that the mouse cursor appears to move on top of the buttons. The procedure for rendering the mouse cursor follows the same pattern we just saw with the buttons and is shown in Listing 2 “20b. The biggest difference between this code and that used for the buttons is the requirement for portions of the cursor to be transparent. We want the cursor to look like an arrow. To accomplish this, I created the texture image using Microsoft Paint and the Microsoft Texture Tool to include an alpha channel defining the opaque and transparent portions of the image. As you will see in the listing, there are several device settings that control alpha transparency. Once we have finished rendering, we restore the fog state to its previous value.
// Draw cursor. Rectangle mouserect = new Rectangle(m_MousePoint,m_Cursor.GetSize()); try { data[0].X = (float)mouserect.X; data[0].Y = (float)mouserect.Y; data[0].Z = 0.0f; data[0].Tu = 0.0f; data[0].Tv = 0.0f; data[1].X = (float)(mouserect.X+mouserect.Width); data[1].Y = (float)mouserect.Y; data[1].Z = 0.0f; data[1].Tu = 1.0f; data[1].Tv = 0.0f; data[2].X = (float)mouserect.X; data[2].Y = (float)(mouserect.Y+mouserect.Height); data[2].Z = 0.0f; data[2].Tu = 0 .0f; data[2].Tv = 1.0f; data[3].X = (float)(mouserect.X+mouserect.Width); data[3].Y = (float)(mouserect.Y+mouserect.Height); data[3].Z = 0.0f; data[3].Tu = 1.0f; data[3].Tv = 1.0f; m_vb.SetData(data, 0, 0); CGameEngine.Device3D.SetStreamSource( 0, m_vb, 0 ); CGameEngine.Device3D.VertexFormat = CustomVertex.TransformedTextured.Format; // Set the texture. CGameEngine.Device3D.SetTexture(0, m_Cursor.GetTexture() ); // Set diffuse blending for alpha set in vertices. CGameEngine.Device3D.RenderState.AlphaBlendEnable = true; CGameEngine.Device3D.RenderState.SourceBlend = Blend.SourceAlpha; CGameEngine.Device3D.RenderState.DestinationBlend = Blend.InvSourceAlpha; // Enable alpha testing (skip pixels with less than a certain alpha). if( CGameEngine.Device3D.DeviceCaps.AlphaCompareCaps.GreaterEqual ) { CGameEngine.Device3D.RenderState.AlphaTestEnable = true; CGameEngine.Device3D.RenderState.ReferenceAlpha = 008; CGameEngine.Device3D.RenderState.AlphaFunction = Compare.GreaterEqual; } // Render the face. CGameEngine.Device3D.DrawPrimitive( PrimitiveType.TriangleStrip, 0, 2 ); } catch (DirectXException d3de) { Console.AddLine( "Unable to display cursor " ); Console.AddLine( d3de.ErrorString ); } catch ( Exception e ) { Console.AddLine( "Unable to display cursor " ); Console.AddLine( e.Message ); } CgameEngine.Device3D.RenderState.FogEnable = fog_state; }
Like any of our code that has a possibility of failure, we have wrapped the rendering code within a Try block. The corresponding Catch block posts a message to the console and continues on. This allows our code to soft fail by noting the problem and continuing on (see Listing 2 “20c).
catch (DirectXException d3de) { Console.AddLine( "Error rendering Option Screen" ); Console.AddLine( d3de.ErrorString ); } catch ( Exception e ) { Console.AddLine( "Error rendering Option Screen" ); Console.AddLine( e.Message ); }
That concludes our definition of the OptionScreen class. The last section of this chapter will show how our game engine controls the SplashScreen and OptionScreen classes.