Just because you plan to replace the user interface you wrote for Blockers doesn't mean you need to write the entire thing from scratch again. Instead, you should take the code you already wrote for the base classes in Blockers and use it in this project. As in the last chapter, when you used the sample framework code files for the project, you want to add an existing file and add gui.cs from Blockers as well; only this time, you don't want to link the file, but instead, you just click Open and add it normally. This move has the effect of copying the code into your new project, so modifying it does not break your existing Blockers implementation. You use fonts in this class this time (for rendering the text-box text later), because since you will have references to both System.Drawing.Font and Microsoft.DirectX.Direct3D.Font, you add the following using clause to this file: using Direct3D = Microsoft.DirectX.Direct3D; With that out of the way, you can get right to modifying the class to include the features you need and want for Tankers. You don't want the button sizes defined as part of the base class because they might change depending on the game (and size of your user interface items), so go ahead and remove those four protected constants from the UiScreen class now. One of the things you might have noticed with the UiScreen class from Blockers is that the "background" image was required to be the full texture that was passed in. This requirement was fine with Blockers, but what if you have more than one screen stored per texture? What you need is a way to the rectangle source of the texture that will be used for the screen. The screen width and height member variables are stored in the StoreTexture method, which worked fine during the last game. However, because you'll be adding a second overload to StoreTexture, you don't want that code duplicated, so remove it from that method and put it into the UiScreen class constructor: // Store the width/height screenWidth = width; screenHeight = height; Now, you can replace the single StoreTexture method with the two different overloads in Listing 12.1. Listing 12.1. Storing Textures for User Screens/// <summary> /// Store the texture for the background /// </summary> protected void StoreTexture(Texture background, int width, int height, bool centerTexture) { Rectangle computedSource = Rectangle.Empty; if (background != null) { // Get the background texture using (Surface s = background.GetSurfaceLevel(0)) { SurfaceDescription desc = s.Description; computedSource = new Rectangle(0, 0, desc.Width, desc.Height); } } StoreTexture(background, computedSource, width, height, centerTexture); } /// <summary> /// Store the texture for the background /// </summary> protected void StoreTexture(Texture background, Rectangle source, int width, int height, bool centerTexture) { // Store the background texture backgroundTexture = background; backgroundSource = source; // Store the centered texture isCentered = centerTexture; if (isCentered) { centerUpper = new Vector3((float)(width - backgroundSource.Width) / 2.0f, (float)(height - backgroundSource.Height) / 2.0f, 0.0f); } } As you can see, the first overload is the same as the one that was originally written for the last game; however, in this case, only the first step is performed, getting the full size of the texture. After that texture rectangle is calculated, the code calls the additional overload, which simply stores that texture and the appropriate size and calculates its position if the screen should be centered. Essentially, it's the same code as what you used the last time; it's just now broken into two separate methods to get the deriving classes more flexibility and freedom. That's the only changes you need to make to the UiScreen class; now you can tackle that button class. Remember from the last game, you had to manually call the user input methods for keyboard and mouse on the button classes, which ended up with "spaghetti code"where your game engine user input code called your derived screen classes' user input code, which finally called the actual user input code for the buttons. If you want to talk about too many levels of indirection, that right there is the poster child. It would be significantly more maintainable if the button simply took care of this data itself, and luckily, you can make it do just that. First, you add three new member variables to the UiButton class: private System.Windows.Forms.Keys shortcut; private GameEngine parentOwner = null; private bool enabled = true; // Should we handle user input? The first variable will be the "shortcut" key to this button. For example, the quit button's shortcut key could be Esc or some other variation. The second variable is the "owner" of the game engine whose events the button will want to hook. The last is whether the button is actually enabled. You wouldn't want a disabled button to be handling user input, would you? Now you need some good way to get the events hooked. Add the method in Listing 12.2 to your UiButton class to handle hooking and unhooking the events. Listing 12.2. Hooking User Input Eventsprivate void HookEvents() { if (enabled) { // Hook the parents events parentOwner.MouseClick += new MouseEventHandler(OnMouseClick); parentOwner.MouseMove += new MouseEventHandler(OnMouseMove); parentOwner.KeyDown += new KeyEventHandler(OnKeyDown); } else { // Unhook the parents events parentOwner.MouseClick -= new MouseEventHandler(OnMouseClick); parentOwner.MouseMove -= new MouseEventHandler(OnMouseMove); parentOwner.KeyDown -= new KeyEventHandler(OnKeyDown); } } Here the game owner is used to hook the events we care about (the mouse moving over the button, the mouse clicking on the button, or a keyboard click) when the button is enabled. If the button is disabled, we unhook the event. Construction Cue
You might have noticed that the events you just hooked don't actually exist anywhere in the game engine code. Add the following event declarations to your game engine code file: public event System.Windows.Forms.KeyEventHandler KeyDown; public event System.Windows.Forms.KeyEventHandler KeyUp; public event System.Windows.Forms.KeyPressEventHandler KeyPress; public event System.Windows.Forms.MouseEventHandler MouseMove; public event System.Windows.Forms.MouseEventHandler MouseClick; You also need some code that fires these events at the appropriate time. You already have the methods to handle the mouse and keyboard events, so that is the place to add this code. First, add this code to the OnMouseEvent method: if (MouseMove != null) MouseMove(this, new System.Windows.Forms.MouseEventArgs(0, 0, x, y, wheel)); if (leftDown) if (MouseClick != null) MouseClick(this, new System.Windows.Forms.MouseEventArgs(0, 0, x, y, wheel)); wheel)); And then, add the OnKeyEvent method: if (keyDown) { if (KeyDown != null) KeyDown(this, new System.Windows.Forms.KeyEventArgs(key)); } else { if (KeyUp != null) KeyUp(this, new System.Windows.Forms.KeyEventArgs(key)); if (KeyPress != null) KeyPress(this, new System.Windows.Forms.KeyPressEventArgs((char)key)); } Back in the gui.cs code file, you need something in the button code that actually calls this method. There are two good places to do so; the first is when the object is created. You need to modify the constructor parameters to accept an extra one, namely a GameEngine named parent, and then add these two lines to the end of the constructor's code: parentOwner = parent; HookEvents(); The other area where you want to ensure that this code gets called is when the user changes the state of the button. You need a property to get the state of the button along with the keyboard shortcut anyway, so add the two properties in Listing 12.3 to the class now. Listing 12.3. Button Propertiespublic System.Windows.Forms.Keys ShortcutKey { get { return shortcut; } set { shortcut = value; } } public bool IsEnabled { get { return enabled; } set { enabled = value; HookEvents(); } } The keyboard shortcut is a "normal" property with simple get/set items, but the enabled property is unique in that it calls the HookEvents method on set. This step ensures that no matter whether you enable or disable the button, the appropriate action takes place. You might also notice that the method declarations you had for the mouse before no longer compile because the signature of the method changed now that you're using the event handlers. Replace the mouse code with the code in Listing 12.4 (which also includes the keyboard code as well). Listing 12.4. Handling User Input for Buttonsprivate void OnMouseMove(object sender, MouseEventArgs e) { if (enabled) { // Determine if the button is on or not isButtonOn = buttonRect.Contains(e.X, e.Y); } } private void OnMouseClick(object sender, MouseEventArgs e) { if (enabled) { // Determine if the button is pressed if(buttonRect.Contains(e.X, e.Y)) { if (Click != null) Click(this, EventArgs.Empty); } } } private void OnKeyDown(object sender, KeyEventArgs e) { if (enabled) { // See if the shortcut key was pressed if (e.KeyCode == shortcut) { // It was, raise the event if (Click != null) Click(this, EventArgs.Empty); } } } The check for enabled here probably isn't necessary, given that the events are unhooked when the object is disabled, but as I mentioned earlier, it's possible for a disabled button that was recently enabled to be clicked because of the order of the event messages. It's also possible for a recently disabled button to be clicked as well. This code handles that case. The mouse handlers are pretty much unchanged outside of the arguments passed in, and the keyboard method is self-explanatory. If the user presses the magic shortcut key, the click event is fired just as if he or she clicked the button. |