Listing 5.7. Creating the UI Screen Class
///
/// Create a new UI Screen
///
public UiScreen(Device device, int width, int height)
{
// Create the sprite object
renderSprite = new Sprite(device);
// Hook the device events to 'fix' the sprite if needed
device.DeviceLost += new EventHandler(OnDeviceLost);
device.DeviceReset += new EventHandler(OnDeviceReset);
StoreTexture(null, width, height, false);
}
///
/// Store the texture for the background
///
protected void StoreTexture(Texture background, int width, int height,
bool centerTexture)
{
// Store the background texture
backgroundTexture = background;
if (backgroundTexture != null)
{
// Get the background texture
using (Surface s = backgroundTexture.GetSurfaceLevel(0))
{
SurfaceDescription desc = s.Description;
backgroundSource = new Rectangle(0, 0,
desc.Width, desc.Height);
}
}
// Store the width/height
screenWidth = width;
screenHeight = height;
// 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);
}
}
The constructor for the object takes the device you are rendering with as well as the size of the screen. The rendering device is needed to create the sprite object and to hook some of the events. Before the device is lost, and after the device has been reset, you need to call certain methods on the sprite object so the sprite behaves correctly. You can add these event-handling methods to your class now:
private void OnDeviceLost(object sender, EventArgs e)
{
// The device has been lost, make sure the sprite cleans itself up
if (renderSprite != null)
renderSprite.OnLostDevice();
}
private void OnDeviceReset(object sender, EventArgs e)
{
// The device has been reset, make sure the sprite fixes itself up
if (renderSprite != null)
renderSprite.OnResetDevice();
}
These methods are entirely self-explanatory. When the device is lost, you call the OnLostDevice method on the sprite. After the device is reset, you call the OnResetDevice method. If you were allowing Managed DirectX to handle the event handling for you, this would happen automatically. Because you are not, you need to ensure that it happens. Without this code, when you try to switch from full-screen mode or return back to full-screen mode after switching, an exception is thrown because the sprite object is not cleaned up correctly.
The call to StoreTexture in the constructor isn't actually necessary because each derived class needs to call this method on its ownbut I use it so you can visualize the process. Obviously, the first thing you want to do in this method is store the texture. Then (assuming you really do have a texture), you want to calculate the full size of the texture. The default for the background textures of a UI screen is to use the entire texture. Notice here that you get the actual surface the texture is occupying and then use the width and height from the surface description to create a new rectangle. Next, you simply store the remaining variables and calculate the upper-left corner of the screen if you will be centering the background texture. You calculate the center by taking the full size of the screen, subtracting the size of the texture, and dividing that in half.
The last thing you do to finish your UI class is to have a method actually render the screen. Add the code in Listing 5.8 to your class to handle the rendering.
Listing 5.8. Rendering the UI Screen
///
/// Start drawing with this sprite
///
protected void BeginSprite()
{
renderSprite.Begin(SpriteFlags.AlphaBlend);
}
///
/// Stop drawing with this sprite
///
protected void EndSprite()
{
renderSprite.End();
}
///
/// Render the button in the correct state
///
public virtual void Draw()
{
// Render the background if it exists
if (backgroundTexture != null)
{
if (isCentered)
{
// Render to the screen centered
renderSprite.Draw(backgroundTexture, backgroundSource,
ObjectCenter, centerUpper, SpriteColor);
}
else
{
// Scale the background to the right size
renderSprite.Transform = Matrix.Scaling(
(float)screenWidth / (float)backgroundSource.Width,
(float)screenHeight / (float)backgroundSource.Height,
0);
// Render it to the screen
renderSprite.Draw(backgroundTexture, backgroundSource,
ObjectCenter, ObjectCenter, SpriteColor);
// Reset the transform
renderSprite.Transform = Matrix.Identity;
}
}
}
The first thing you'll probably notice here is that the BeginSprite and EndSprite methods are separate and not part of the main Draw call. I did this intentionally because the sprite object is shared with the derived UI screens, as well as the buttons. Rather than let each separate object have its own sprite class and do its own begin and end calls (which isn't overly efficient), each UI screen will share the same sprite and do all the drawing between a single begin/end block. To facilitate this step, you need these protected methods so the derived classes can control the beginning and ending of the sprite drawing. The begin call ensures that the sprites will be rendered with alpha blendingwhich means that the backgrounds of your UI and the buttons will be rendered with transparency. You'll see this effect in action in later chapters.
The Draw call itself isn't overly complicated either. Assuming you have a texture to render, you simply need to draw the sprite onscreen, either centered or not. If the sprite should be centered, it's one simple call to the Draw method on the sprite. The prototype for this method is as follows:
public void Draw ( Microsoft.DirectX.Direct3D.Texture srcTexture ,
System.Drawing.Rectangle srcRectangle ,
Microsoft.DirectX.Vector3 center ,
Microsoft.DirectX.Vector3 position ,
System.Drawing.Color color )
The first parameter is the texture you want to render, in this case the background of the screen. The second parameter is the location of the data inside that texture. As you can see, you are passing in the rectangle that was calculated in the StoreTexture method, and it encompasses the entire texture. If there were multiple items per texture (as you will see with the buttons soon), this rectangle would only cover the data needed for that texture. The third parameter is the "center" of the sprite, and it is only used for calculating rotation, which is why it uses the constant you declared earlier. You won't be rotating your sprites. The position vector is where you want this sprite to be rendered onto the screen, in screen coordinates, and the last parameter is the "color" of the sprite. (I already discussed this parameter when you declared the constants earlier in this chapter.)
For the centered case, you simply use the "default" parameters for the Draw call, and for the position, you use the vector you calculated earlier. The Draw call for the stretched case is virtually identical; the exception is that the position vector you are using is the same constant you used for the center vector. That vector resides at 0,0,0, which is where you want the upper-left corner of the sprite to be rendered for your stretched image. Why create a whole new instance of the same data when you can simply reuse the existing one?
The stretched case has an extra call before the Draw call, howevernamely, setting the transform that should be used when rendering this sprite. Because you want to ensure that the sprite is "stretched" across the entire screen, you want to scale the image. Notice that the calculation takes the size of the texture you will be rendering and divides it by the actual size of the screen to determine the correct scaling factor. Each of these items is first cast to a float before the calculations are performed. The question here is "Do you know why?"
Caution
You do so because the items are originally integers, which can produce strange results. For example, in C#, what do you think the following statement will display?
Console.WriteLine(2/3);
If you didn't guess 0, well, this caution is for you because you would be wrong. Both operands are integers, so the runtime will take 2/3 (0.3333) and then cast it back to an integer (0). To preserve the fraction, you need to ensure that both of the operands are floats, which is why you do the cast.
Also notice that after the draw is done, the transform is set once more back to the default Identity. Because the sprite object is shared, any subsequent calls to Draw would have the same scaling effect without it, which isn't the behavior you desire.
Designing a Button
You can keep the code for your button in the same gui.cs code file you've been using up to this point (which is what the code on the included CD does), or you can put it in its own code file, which you would need to add to your project. Either way, add the class in Listing 5.9 to your code file.
Listing 5.9. The UiButton Class
///
/// Will hold a 'button' that will be rendered via DX
///
public class UiButton
{
private Sprite renderSprite = null;
private Texture buttonTextureOff = null;
private Texture buttonTextureOn = null;
private Rectangle onSource;
private Rectangle offSource;
private Vector3 location;
private bool isButtonOn = false;
private Rectangle buttonRect;
// The click event
public event EventHandler Click;
///
/// Create a new instance of the Button class using an existing sprite
///
public UiButton(Sprite sprite, Texture on, Texture off,
Rectangle rectOn, Rectangle rectOff, Point buttonLocation)
{
// Store the sprite object
renderSprite = sprite;
// Store the textures
buttonTextureOff = off;
buttonTextureOn = on;
// Rectangles
onSource = rectOn;
offSource = rectOff;
// Location
location = new Vector3(buttonLocation.X, buttonLocation.Y, 0);
// Create a rectangle based on the location and size
buttonRect = new Rectangle((int)location.X, (int)location.Y,
onSource.Width, onSource.Height);
}
///
/// Render the button in the correct state
///
public void Draw()
{
if (isButtonOn)
{
renderSprite.Draw(buttonTextureOn, onSource, UiScreen.ObjectCenter,
location, UiScreen.SpriteColor);
}
else
{
renderSprite.Draw(buttonTextureOff, offSource, UiScreen.ObjectCenter,
location, UiScreen.SpriteColor);
}
}
}
You'll notice initially that this class is somewhat similar to the UI abstract class you just wrote. There are some important differences, though. First, you should see that there are two textures to store. One is used to render the button in the "off" state, and the other is used to render the button in the "on" state. It's entirely possible (and in this case, probable) that these textures will be the same file, simply with different source rectangles.
The constructor takes as arguments the sprite used to render the button, the textures for both the on and off states of the button, the rectangle sources of each of these states, and the location onscreen where the button will be rendered. Each of these items is stored for later use in the related class variable. The class itself has three other variables it will need, however. One, it needs to know what state the button is in. Because the button will either be on or off, a Boolean value is the natural selection here, with a default of off. Two, you also need to know the exact rectangle onscreen that the button encompasses. At the end of the constructor, you calculate this rectangle by taking the location onscreen and adding the size of the source rectangle. You'll find out why you need this work in a few moments. Third (and finally), because it is a button, you want an event to be fired when someone clicks the button.
The Draw method is simple. Depending on whether or not the button is on, the appropriate sprite is rendered at the correct location. The only real differences between the two calls are the texture that is passed in and the source rectangle. The last thing you need is a way to actually click the button and have its state change based on the location of the mouse. Add the code in Listing 5.10 to your UiButton class.
Listing 5.10. Handling the Mouse
///
/// Update the button if the mouse is over it
///
public void OnMouseMove(int x, int y)
{
// Determine if the button is on or not
isButtonOn = buttonRect.Contains(x, y);
}
///
/// See if the user clicked the button
///
public void OnMouseClick(int x, int y)
{
// Determine if the button is pressed
if(buttonRect.Contains(x, y))
{
if (Click != null)
Click(this, EventArgs.Empty);
}
}
These methods should be called as the mouse moves around the screen and when the button is clicked. As the mouse moves around, the button state changes depending on whether the current mouse coordinates are within the rectangle onscreen where the button is rendered. This is the reason that you needed to calculate the exact screen rectangle where the button would be rendered. If the mouse button is clicked, you once again check whether the mouse is in the button's rectangle, and if it is, you fire the Click event so that whatever created this button will know about it.
The UiButton class was small and simple. Combining the UI screen classes with the buttons, you can create simple UIs for your game.
|