Selecting a Game Character


Selecting Game Character 

After creating the main menu screen, you are probably an expert at this. The code is simple, and encapsulated, and it should be easy to create a new UI screen. This screen will be at least slightly different than the last one, mainly because not only do you render the UI, but you also need to render the character model. Before you get to that, however, you should add the class in Listing 6.7 to your UiScreen.cs code file.
Listing 6.7. The Character Selection Screen
/// 
/// The screen to select the Loopy character
/// 
public class SelectLoopyScreen : UiScreen, IDisposable
{
    private Texture buttonTextures = null;
    private Texture messageTexture = null;
    private UiButton okButton = null;
    private UiButton leftButton = null;
    private UiButton rightButton = null;

    // Events
    public event EventHandler Selected;
    /// 
    /// Clean up in case dispose isn't called
    /// 
    ~SelectLoopyScreen()
    {
        Dispose();
    }
    /// 
    /// Clean up any resources
    /// 
    public override void Dispose()
    {
        GC.SuppressFinalize(this);
        if (messageTexture != null)
        {
            messageTexture.Dispose();
        }
        messageTexture = null;
        base.Dispose();
    }
}

Obviously, this code looks relatively familiar, so there is no need to go through it again. The only major differences between this and your previous UI screen are the number of buttons (four instead of two) and the fact that the buttons' texture isn't cleaned up in the Dispose implementation. The buttons' texture isn't cleaned up here because the Yes button is shared between this screen and the quit screen. Rather than create two instances of the same texture, which wouldn't be efficient, you create only one, in the game engine. Because the game engine creates the texture, the game engine is responsible for ensuring that it's cleaned up properly.
Before you write the constructor for this class, you should add some more variables, mainly to hold the character mesh. Add these now:
private Mesh loopyMesh = null;
private Texture loopyTexture = null;
private Material loopyMaterial;
private Device storedDevice = null;
private PlayerColor loopyColor = PlayerColor.Min;

The character's representation consists of the mesh (which holds all the geometry data), the texture (which determines the character's color), and the material. You've seen each of these before when you were loading and rendering your sky box, but this part is loaded slightly differently. Because switching to a different version of Loopy will in reality simply update the texture the model is currently using, you need a reference to the rendering device, which is why you need to store one. The last type you haven't even created yet, but it is essentially going to be an enumeration that contains the possible colors of Loopy. Add a new code file to your project called player.cs now, and add the code in Listing 6.8 to this new code file to declare this enumeration.
Listing 6.8. The Player Colors
/// 
/// The color of the player model
/// 
public enum PlayerColor
{
    Blue,
    Green,
    Purple,
    Red,
    Gray,
    Yellow,
    Min = Blue,
    Max = Yellow,
}

Aside from the colors that Loopy can be, you'll also notice Min and Max items in the enumeration. These values are used to determine when you've made it to one "edge" of the enumeration to enable wrapping. For example, if you're currently on Blue and you scroll left, you want to go to Yellow, and scrolling right from there gets you back to Blue. Back in your SelectLoopyScreen class, add the constructor in Listing 6.9.
Listing 6.9. Creating the Select Loopy Screen
public SelectLoopyScreen(Device device, Texture buttons, int width, int height)
    : base(device, width, height)
{
    // Store the device for texture creation
    storedDevice = device;

    // Store the textures for the buttons
    buttonTextures = buttons;

    // Create the loopy mesh
    loopyMesh = Mesh.FromFile(GameEngine.MediaPath + "Loopy.x",
    MeshFlags.Managed, device);

    // Create the loop texture
    CreateTexture(device);

    // Create a material
    loopyMaterial = new Material();
    loopyMaterial.Diffuse = loopyMaterial.Ambient = Color.White;

    // Create the texture for the background
    messageTexture = TextureLoader.FromFile(device, GameEngine.MediaPath +
    "Select.png");
    StoreTexture(messageTexture, width, height / 2, true);

    // Create the ok button
    okButton = new UiButton(renderSprite, buttonTextures, buttonTextures,
        new Rectangle(SmallButtonWidth,SmallButtonHeight * 3,
        SmallButtonWidth,SmallButtonHeight),
        new Rectangle(0,SmallButtonHeight * 3,
        SmallButtonWidth,SmallButtonHeight),
        new Point((width - SmallButtonWidth) / 2,
        height - (SmallButtonHeight * 2)));

    okButton.Click += new EventHandler(OnSelectButton);

    // Create the yes/no buttons
    leftButton = new UiButton(renderSprite, buttonTextures, buttonTextures,
        new Rectangle(SmallButtonWidth,0,SmallButtonWidth,SmallButtonHeight),
        new Rectangle(0,0,SmallButtonWidth,SmallButtonHeight),
        new Point(SmallButtonWidth / 2,
        height - (SmallButtonHeight * 2)));

    leftButton.Click += new EventHandler(OnLeftButton);

    rightButton = new UiButton(renderSprite, buttonTextures, buttonTextures,
        new Rectangle(SmallButtonWidth,SmallButtonHeight * 1,
        SmallButtonWidth,SmallButtonHeight),
        new Rectangle(0,SmallButtonHeight * 1,
        SmallButtonWidth,SmallButtonHeight),
        new Point(width - (SmallButtonWidth + (SmallButtonWidth / 2)),
        height - (SmallButtonHeight * 2)));

    rightButton.Click += new EventHandler(OnRightButton);
}

First, you take the rendering device and the texture for the buttons and store them for later use. Once that's done, you can create the mesh for Loopy. Notice that when you call the method this time, you're not using the overload that returns the materials. Although it is possible to use this method here as well, because you switch the character's texture at will and define your own material, it's really not necessary, so you might as well just skip it.
After you create the model for Loopy, you create a texture that defines his overall color; however, you also use this method for the Next and Previous buttons. You define the CreateTexture method to handle that in a few moments. You also want to ensure that you have a material for Loopy because you need to add lights to this scene before you're done. Materials define how the lights interact with the object. Because you want Loopy to have the light affect him "normally," you set the material to a simple white material.
The background texture for this user screen is slightly different as well. After it is created, notice that the StoreTexture method is called just as before, but there is one major difference in the call. You don't want the background image to interfere with the selection of Loopy, so you scale down the height of the window when you pass it to this method to only the top half of the screen. The majority of the screen "real estate" displays Loopy when the user selects the one he or she wants.
The button creation is similar to the ones you did in the main screen earlier; you just change the source of the buttons and the locations onscreen and use the small button constants instead. Notice that the left button is in the bottom left of the screen and the right button is in the bottom right of the screen. The Yes button appears directly between them in the center of the screen.
Before you can compile this code, you need to add the CreateTexture method from Listing 6.10.
Listing 6.10. Creating the Loopy Texture
private void CreateTexture(Device device)
{
    if (loopyTexture != null)
    {
        // Destroy and re-create a new texture
        loopyTexture.Dispose();
    }
    loopyTexture = TextureLoader.FromFile(device,
        GetTextureFileFromColor(loopyColor));
}
private static string GetTextureFileFromColor(PlayerColor color)
{
    switch(color)
    {
        case PlayerColor.Blue:
            return GameEngine.MediaPath + "LoopyBlue.bmp";
        case PlayerColor.Green:
            return GameEngine.MediaPath + "LoopyGreen.bmp";
        case PlayerColor.Purple:
            return GameEngine.MediaPath + "LoopyPurple.bmp";
        case PlayerColor.Red:
            return GameEngine.MediaPath + "LoopyRed.bmp";
        case PlayerColor.Yellow:
            return GameEngine.MediaPath + "LoopyYellow.bmp";
        case PlayerColor.Gray:
            return GameEngine.MediaPath + "LoopyGray.bmp";
        default:
            throw new ArgumentException("color",
               "An invalid color was passed in.");
    }
}

Because this method will potentially be called numerous times, you want to ensure that any loaded texture is destroyed before you create a new one. Then you can create the texture, but the enumeration alone doesn't tell you which texture file you need to use. Another helper method translates the color from the enumeration to a string representation of the filename the texture resides in. Next, add the rendering code for this screen. You'll find the implementation in Listing 6.11.
Listing 6.11. Rendering the Selection Screen
/// 
/// Render the buttons
/// 
public void Draw(float totalTime)
{
    // Render Loopy Should be "Set the world transform"
    storedDevice.Transform.View = Matrix.LookAtLH(
        CameraPos, CameraTarget, CameraUp);

    storedDevice.Transform.World = Matrix.RotationY(totalTime * 2);

    storedDevice.SetTexture(0, loopyTexture);
    storedDevice.Material = loopyMaterial;
    loopyMesh.DrawSubset(0);

    // Start rendering sprites
    base.BeginSprite();
    // Draw the background
    base.Draw();
    // Now the buttons
    leftButton.Draw();
    rightButton.Draw();
    okButton.Draw();

    // You're done rendering sprites
    base.EndSprite();
}

The first thing you'll probably notice is that you aren't overriding the Draw method from the base class but are instead defining your own. You want to make sure that Loopy spins around as the selection screen appears, and you need to know the time from the timer. Remember from an earlier chapter, the View transform defines where the camera is located, where it is looking, and what direction "up" is. In this case, you are using constants that you haven't defined yet, so add these to this class now:
private static readonly Vector3 CameraPos = new Vector3(0, 20.0f, -85.0f);
private static readonly Vector3 CameraTarget = new Vector3(0,20,0);
private static readonly Vector3 CameraUp = new Vector3(0, 1.0f, 0);

After you set up the camera, you update the World transform to do the rotation of Loopy. Notice that you simply rotate Loopy on the Y axis based on the current time of the application. Multiplying the value by two increases the rotation speed. Next, you can render Loopy just as you rendered the sky box earlier. Simply set the correct texture on the device and the material and call the DrawSubset method on your mesh. Because there is only one subset in the Loopy mesh, there is no need to do this in a loop; you can simply pass in 0 for the attribute ID.
The rest of the rendering method is pretty much the same as the last user screen you did, so there's no need to go through that once more. You do still need to add the event handlers for the buttons you declared earlier, so use Listing 6.12 to add them to your class now.
Listing 6.12. Handling Button Click Events
/// 
/// Fired when the left button is clicked
/// 
private void OnLeftButton(object sender, EventArgs e)
{
    loopyColor--;
    if (loopyColor < PlayerColor.Min)
        loopyColor = PlayerColor.Max;
    // Create the loop texture
    CreateTexture(storedDevice);
}
/// 
/// Fired when the right button is clicked
/// 
private void OnRightButton(object sender, EventArgs e)
{
    loopyColor++;
    if (loopyColor > PlayerColor.Max)
        loopyColor = PlayerColor.Min;
    // Create the loop texture
    CreateTexture(storedDevice);
}
/// 
/// Fired when the ok button is clicked
/// 
private void OnSelectButton(object sender, EventArgs e)
{
    if (Selected != null)
        Selected(this, e);
}

The code here is relatively straightforward. Pressing the left button decrements the current color, and pressing the right increments it. Each of these operations handles the wrapping if you've gone past the bounds set for that button and then calls the CreateTexture method to ensure that the updated texture is loaded. When you click the Yes button, it means you've selected which version of Loopy you want to use, and the Selected event is fired. Obviously, you need to add the handlers for the mouse and keyboard events for this screen as well, so you find them in Listing 6.13.
Listing 6.13. Handling User Input
public void OnMouseMove(int x, int y)
{
    leftButton.OnMouseMove(x, y);
    rightButton.OnMouseMove(x, y);
    okButton.OnMouseMove(x, y);
}
public void OnMouseClick(int x, int y)
{
    leftButton.OnMouseClick(x, y);
    rightButton.OnMouseClick(x, y);
    okButton.OnMouseClick(x, y);
}
public void OnKeyPress(System.Windows.Forms.Keys key)
{
    switch(key)
    {
        case Keys.Enter:
            // Enter is the same as selecting loopy
            OnSelectButton(this, EventArgs.Empty);
            break;
        case Keys.Left:
            // Same as pressing the left arrow
            OnLeftButton(this, EventArgs.Empty);
            break;
        case Keys.Right:
            // Same as pressing the left arrow
            OnRightButton(this, EventArgs.Empty);
            break;
    }
}

The mouse events behave exactly the same as they did last time, but the keystrokes are slightly different. You still use Enter to go to the next step (in this case, actually playing the game), but there are two new keys as well. Clicking the left and right arrows is the same as pressing the left or right user button.

Comments

Popular Posts