Designing the Main Menu for the game


Designing the Main Menu

During the end of the last chapter, you wrote the base class for the UI screens you will be creating in this chapter. The first user screen to create is the main menu. Everything in your game spawns from this screen.
Add a new code file to your project called UiScreens.cs, and add the code in Listing 6.1 to the file.
Listing 6.1. The MainUiScreen Class
/// 
/// The main selection screen for the game
/// 
public class MainUiScreen : UiScreen, IDisposable
{
    private Texture buttonTextures = null;
    private Texture messageTexture = null;

    private UiButton newButton = null;
    private UiButton exitButton = null;

    // Events
    public event EventHandler NewGame;
    public event EventHandler Quit;

    #region IDisposable Members
    /// 
    /// Clean up in case dispose isn't called
    /// 
    ~MainUiScreen()
    {
        Dispose();
    }
    /// 
    /// Clean up any resources
    /// 
    public override void Dispose()
    {
        GC.SuppressFinalize(this);
        if (messageTexture != null)
        {
            messageTexture.Dispose();
        }
        if (buttonTextures != null)
        {
            buttonTextures.Dispose();
        }
        buttonTextures = null;
        messageTexture = null;
        base.Dispose();
    }

    #endregion
}

The two textures you are using in this class are for the background and the buttons. Although this menu has two buttons, each of the images for these buttons is stored in a single texture (called MainButtons.png in the media folder). This texture contains four different images, the on and off states of the New Game and Quit buttons. The other texture is the background of the screen, which the buttons will sit on top of.
Notice that you've also declared two instances of the actual button classes you designed during the last chapter. These classes handle the actual rendering of the button but are a "child" of this UI screen. You also have declared two events, which correspond to the user clicking on each of the two buttons.
Finally, you have declared the cleanup code for this UI screen. Because you will be creating each of the two textures in the constructor for this object, you need to ensure that you clean up both of them in this object. Also notice that the base class's Dispose method is also called to ensure that the Sprite object is cleaned up as well.
You might be surprised to find that trying to compile this code will fail; I know I was! There is currently no constructor defined for the MainUiScreen object; the default one that is provided has no parameters. However, derived classes must also call the constructor for the base class, and the base class for this object has no parameter-less constructor. Because the compiler does not know what default parameters to pass on to this method, it simply fails. Add the constructor in Listing 6.2 to your class to fix this compilation error.
Listing 6.2. Creating the MainUiScreen Class
public MainUiScreen(Device device, int width, int height)
    : base(device, width, height)
{
    // Create the texture for the background
    messageTexture = TextureLoader.FromFile(device, GameEngine.MediaPath
                     + "MainMenu.png");

    // Mark the background texture as stretched
    StoreTexture(messageTexture, width, height, false);

    // Create the textures for the buttons
    buttonTextures = TextureLoader.FromFile(device, GameEngine.MediaPath
        + "MainButtons.png");

    // Create the main menu buttons now

    // Create the new game button
    newButton = new UiButton(renderSprite, buttonTextures, buttonTextures,
        new Rectangle(0,LargeButtonHeight * 1,
        LargeButtonWidth, LargeButtonHeight),
        new Rectangle(0,0,
        LargeButtonWidth, LargeButtonHeight),
        new Point((width - LargeButtonWidth) / 2,
        (height - (LargeButtonHeight * 4)) / 2));

    newButton.Click += new EventHandler(OnNewButton);

    // Create the new game button
    exitButton = new UiButton(renderSprite, buttonTextures,
        buttonTextures, new Rectangle(0,LargeButtonHeight * 3,
        LargeButtonWidth, LargeButtonHeight),
        new Rectangle(0,LargeButtonHeight * 2,
        LargeButtonWidth, LargeButtonHeight),
        new Point((width - LargeButtonWidth) / 2,
        (height - (LargeButtonHeight * 2)) / 2));

    exitButton.Click += new EventHandler(OnExitButton);
}

The first thing you should notice is the call to base immediately after the constructor's prototype. I just described this part, informing the compiler about the default parameters to the base class's constructor, which allows this code to be compiled. After that, the textures are loaded, using the media path variable you declared in the main game engine. Notice that the StoreTexture method is called after the background texture is created, and notice that false is passed in as the last parameter to ensure the image is stretched onscreen rather than centered.
After the textures are created, the code creates the two buttons for this screen and hooks the Click event for each. As you see, the protected variable renderSprite is passed in to the constructor for the button, so they share the same sprite object as the UI screen. Quite a bit of fancy math formulas determine where the button will be located onscreen and where the button's texture is located in the main texture. Each button is one of the "large" buttons that you defined in your UI class yesterday, so these constants are used to determine where the buttons should be rendered in the texture. The constants are also used to determine the middle of the screen (width LargeButtonWidth) so they can be centered when they are rendered onscreen.
The image that contains the two buttons (and the two states of each) is a single 256x256 texture. Each button is 256 pixels wide by 64 pixels high. Modern graphics cards work well with textures that are square and that have heights and widths which are a power of 2 (which this texture is) and sometimes will not work at all without these restrictions. Knowing this, it is more efficient to combine the buttons into the single texture that meets the requirements, as you've done here. Common texture sizes range from 128x128 to 1024x1024 and can go even higher for highly detailed models if you want.
You might have noticed that the event handlers you used to hook the button click events used methods that you haven't defined in your class yet. You also need a way to render your UI, so add the code in Listing 6.3 to your class to take care of each of these issues.
Listing 6.3. Rendering and Handling Events
/// 
/// Render the main ui screen
/// 
public override void Draw()
{
    // Start rendering sprites
    base.BeginSprite();
    // Draw the background
    base.Draw();

    // Now the buttons
    newButton.Draw();
    exitButton.Draw();

    // You're done rendering sprites
    base.EndSprite();
}
/// 
/// Fired when the new button is clicked
/// 
private void OnNewButton(object sender, EventArgs e)
{
    if (NewGame != null)
        NewGame(this, e);
}

/// 
/// Fired when the exit button is clicked
/// 
private void OnExitButton(object sender, EventArgs e)
{
    if (Quit != null)
        Quit(this, e);
}

The Draw method encompasses all the rendering that is required for the UI screen. Before any drawing is actually done, the BeginSprite method from the base class is called. In this case, simply calling the Begin method on the renderSprite object itself would have given us the same behavior, but encapsulating the call allows us to easily change the behavior later and let all subsequent derived classes get this new, improved behavior.
The base class was designed to handle rendering the background, so to render that, the Draw method is called on the base class. After that, it is a simple matter to call the Draw method on each of the buttons and mark the sprite as finished rendering.
The actual event-handler methods are amazingly simple. Depending on which button is clicked, you fire the appropriate event. However, if you remember from Chapter 5, "Finishing Up the Support Code," when you were designing the button, the only way to get the button to fire its click event was to call the OnMouseClick method on the button. On top of that, you needed to call the OnMouseMove method for the button to highlight correctly when the mouse moved over it. You also want the user to be able to control the UI without using the mouse. To handle these cases, add the three methods in Listing 6.4 to your class.
Listing 6.4. Handling User Input on Screens
/// 
/// Update the buttons if the mouse is over it
/// 
public void OnMouseMove(int x, int y)
{
    newButton.OnMouseMove(x, y);
    exitButton.OnMouseMove(x, y);
}
/// 
/// See if the user clicked the buttons
/// 
public void OnMouseClick(int x, int y)
{
    newButton.OnMouseClick(x, y);
    exitButton.OnMouseClick(x, y);
}
/// 
/// Called when a key is pressed
/// 
public void OnKeyPress(System.Windows.Forms.Keys key){
    switch(key)
    {
        case Keys.Escape:
            // Escape is the same as pressing quit
            OnExitButton(this, EventArgs.Empty);
            break;
        case Keys.Enter:
            // Enter is the same as starting a new game
            OnNewButton(this, EventArgs.Empty);
            break;
    }
}

Nothing overly complicated here: you simply add new public methods to your UI screen that mirror the methods the buttons have. When they are called, you simply pass the data on to the buttons and allow them to do the work they need to do. The keyboard keys are a different matter, though. In this case, when the user presses a key, the code checks whether it is Esc, which is the same as pressing the Quit button, or Enter, which is the same as pressing the New Game button. This code allows the user to navigate the game without ever needing the mouse.

Plugging into the Game Engine

That's all there is to the main menu screen for your new game. Now all you'll need to do is plug it in to your game engine, and you'll be well on your way to having a fully functional game. Go back to your game engine class, and add the following two variables to your class:
// Is the main menu currently being shown
private bool isMainMenuShowing = true;
// The main UI screen
private MainUiScreen mainScreen = null;

These control the main menu UI screen, as well as determine whether this screen is currently being shown. Because the first thing you see when you start the game is the main menu, obviously you want this Boolean variable to default to true, as it does here. Now you actually create an instance of the main menu UI screen, which you do in the OnCreateDevice method. Add the following code to the end of that method:
// Create the main UI Screen
mainScreen = new MainUiScreen(device, desc.Width, desc.Height);
mainScreen.NewGame += new EventHandler(OnNewGame);
mainScreen.Quit += new EventHandler(OnMainQuit);

You won't be able to compile yet because the event-handler methods haven't been declared yet. For now, you can skip them because you will handle them in a few moments. First, because you've created your main menu screen, you want to ensure that it gets cleaned up when the application shuts down. In the OnDestroyDevice method for your game engine, add the following to the end of the method:
if (mainScreen != null)
{
    mainScreen.Dispose();
}

This code ensures that the textures and sprite used for rendering this UI screen are cleaned up properly. Speaking of rendering, you probably want to ensure that your screen will get rendered as well. For now, go ahead and remove the code that you used to render the sky box from your OnFrameRender method (don't worry, you'll replace it later) because you don't want the sky box to be rendered while the main menu is being displayed. Replace your OnFrameRender method with the one in Listing 6.5.
Listing 6.5. Rendering Your Main Menu
public void OnFrameRender(Device device, double appTime, float elapsedTime)
{
    bool beginSceneCalled = false;

    // Clear the render target and the zbuffer
    device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, 0, 1.0f, 0);
    try
    {
        device.BeginScene();
        beginSceneCalled = true;

        // Decide what to render here
        if (isMainMenuShowing)
        {
            mainScreen.Draw();
        }
        #if (DEBUG)
        // Show debug stats (in debug mode)
        debugFont.DrawText(null, sampleFramework.FrameStats,
            new System.Drawing.Rectangle(2,0,0,0),
            DrawTextFormat.NoClip, unchecked((int)0xffffff00));
        debugFont.DrawText(null, sampleFramework.DeviceStats,
            new System.Drawing.Rectangle(2,15,0,0),
            DrawTextFormat.NoClip, unchecked((int)0xffffff00));
        #endif
    }
    finally
    {
        if (beginSceneCalled)
            device.EndScene();
    }
}

Obviously, your rendering method got a lot less complex with that. Because the UI screen is encapsulated so well, it's only a single method call to ensure that everything gets rendered correctly. You're not entirely finished yet, though. Remember, for the buttons to work correctly, you need to call into the mouse and keyboard methods of the UI screen. You have hooked the mouse and keyboard user input callbacks from the sample framework. Go ahead and add the code from Listing 6.6 to them now to call into the appropriate UI screen.
Listing 6.6. Handling User Input
private void OnMouseEvent(bool leftDown, bool rightDown, bool middleDown,
    bool side1Down, bool side2Down, int wheel, int x, int y)
{
    if (!leftDown)
    {
        if (isMainMenuShowing)
        {
            mainScreen.OnMouseMove(x, y);
        }
    }
    else if (leftDown)
    {
        if (isMainMenuShowing)
        {
            mainScreen.OnMouseClick(x, y);
        }
    }
}
/// Handle keyboard strokes
private void OnKeyEvent(System.Windows.Forms.Keys key,
 bool keyDown, bool altDown)
{
    // Only do this when it's down
    if (keyDown)
    {
        if (isMainMenuShowing)
        {
            mainScreen.OnKeyPress(key);
        }
    }
}

These methods are automatically called by the sample framework whenever the appropriate event occurs. For example, as you move the mouse cursor across the screen, the OnMouseEvent method is called; similarly, the OnKeyEvent method is called if you press a keyboard button. You've now implemented the main menu screen, except for the two event handlers you need to handle the button clicks on the screen. Go ahead and add those now:
private void OnNewGame(object sender, EventArgs e)
{
}
private void OnMainQuit(object sender, EventArgs e)
{
    sampleFramework.CloseWindow();
}

Comments

Popular Posts