Blame |
Last modification |
View Log
| RSS feed
#include "Board.h"
#include "DemoWidget.h"
#include "Res.h"
#include "V12DemoApp.h"
#include "..\SexyAppFramework\Font.h"
#include "..\SexyAppFramework\Graphics.h"
#include "..\SexyAppFramework\ButtonWidget.h"
#include "..\SexyAppFramework\WidgetManager.h"
#include "..\SexyAppFramework\Dialog.h"
#include "..\SexyAppFramework\Flags.h"
#include "..\SexyAppFramework\SexyMatrix.h"
#include "..\SexyAppFramework\trivertex.h"
using namespace Sexy;
Board::Board(V12DemoApp* theApp)
{
mApp = theApp;
// Remeber the flag explanation in the 1.2 doc and V12Demo.cpp? Sure you do.
// This is basically another use of it. All widgets by default have the following
// flags set:
// WIDGETFLAGS_UPDATE - The widget gets its Update and UpdateF methods called
// WIDGETFLAGS_DRAW - The widget is allowed to draw
// WIDGETFLAGS_CLIP - Set to clip Graphics passed into Draw to the widget's bounds
// WIDGETFLAGS_ALLOW_MOUSE - Set to allow mouse interaction
// WIDGETFLAGS_ALLOW_FOCUS - Set to allow focus to be maintained
//
// Previously, if you wanted to mark the widget dirty every frame (and thus draw every
// frame), you had to call MarkDirty() all the time. Instead of doing that, we can just
// use the flag system. By ORing WIDGETFLAGS_MARK_DIRTY with mWidgetFlagsMod.mAddFlags,
// when we are added to the widget manager, the manager will then know that we want to
// be marked dirty every frame automatically, instead of manually calling MarkDirty().
// Yes, I know, that's not a huge deal, but as you can see from the other flags,
// it's now easy to set/unset various flags for various widgets and..various reasons.
// If you wanted to remove flags set by default for this widget, you'd just OR the
// flags to remove with mWidgetFlagsMod.mRemoveFlags. You can affect ALL widgets if you
// want to set up widget defaults, by modifying mApp->mWidgetManager's mWidgetFlags
// structure in a similar way.
mWidgetFlagsMod.mAddFlags |= WIDGETFLAGS_MARK_DIRTY;
// Previously, it was annoying trying to place widgets on some sort of parent widget,
// since there was no notion of parent/child relationship. What you had to do was
// override the AddedToManager and RemovedFromManager functions, create and add your
// widgets or remove and nuke them, and in the case of AddedToManager, you also had
// to then place the widget using global coordinates that had no relation to the coordinates
// you set the parent widget at. Not anymore. What we can do now is to dispense with
// overriding those methods altogether! So now, in the parent's constructor, we can
// create our child widgets, resize them, and place them using relative coordinates.
// What this also means is that moving the parent widget around results in moving
// ALL the child widgets, which means you no longer have to manually move everything
// just because the parent moved. In addition, another nice thing is that you no longer
// have to remove your child widgets before deleting them: this is handled automatically
// for you.
mDemoButton = new ButtonWidget(0, this);
mDemoButton->mLabel = _S("Demo Widget");
mDemoButton->SetFont(FONT_DEFAULT);
mDemoButton->Resize(10, 10, 10 + FONT_DEFAULT->StringWidth(mDemoButton->mLabel), 50);
// VERY IMPORTANT: Notice that we're calling THIS CLASS' (or really, it's parent, WidgetContainer's)
// AddWidget method instead of the WidgetManager's method. In order to designate a widget as a child
// widget, you have to call the AddWidget method of the class that will be its parent.
AddWidget(mDemoButton);
mDialogButton = new ButtonWidget(1, this);
mDialogButton->mLabel = _S("Do Dialog");
mDialogButton->SetFont(FONT_DEFAULT);
int w = FONT_DEFAULT->StringWidth(mDialogButton->mLabel);
mDialogButton->Resize(mApp->mWidth - 20 - w, 10, w + 10, 50);
AddWidget(mDialogButton);
mCurtainButton = new ButtonWidget(2, this);
mCurtainButton->mLabel = _S("Do Transition");
mCurtainButton->SetFont(FONT_DEFAULT);
w = FONT_DEFAULT->StringWidth(mCurtainButton->mLabel);
mCurtainButton->Resize(mDemoButton->mX + 20 + mDemoButton->mWidth, 10, w + 10, 50);
AddWidget(mCurtainButton);
mDemoWidget = NULL;
// Position and set up our pulsing rectangle, as well as the location of the lost focus text.
mRect = Rect(mApp->mWidth / 2 - 1, mApp->mHeight / 2 - 1, 2, 2);
mExpanding = true;
mMsgX = Rand() % (mApp->mWidth - 100);
mMsgY = Rand() % (mApp->mHeight - 100);
mLostFocus = false;
mCurtainWidth = 0;
mCurtainMode = CURTAIN_INACTIVE;
// We'll toggle this between 0 and 2 every second when we are unfocussed
mDeferPriority = 0;
}
Board::~Board()
{
// We need to remove child widgets before deleting them.
RemoveAllWidgets();
delete mDemoButton;
delete mDialogButton;
delete mCurtainButton;
if (mDemoWidget != NULL)
mApp->mWidgetManager->RemoveWidget(mDemoWidget);
delete mDemoWidget;
}
void Board::Update()
{
Widget::Update();
if (mLostFocus)
{
if (mUpdateCnt % 100 == 0)
{
mMsgX = Rand() % (mApp->mWidth - 100);
mMsgY = Rand() % (mApp->mHeight - 100);
// Every second, we switch between drawing the overlay above or below
// the mDemoWidget object (assuming of course you have the widget on screen
// when you lose focus).
mDeferPriority = mDeferPriority == 0 ? 2 : 0;
}
}
else
{
// This just makes the rectangle in the middle grow/shrink. Nothing
// too crazy.
if (mExpanding)
{
mRect.mWidth += 2;
mRect.mHeight += 2;
if (mRect.mWidth >= mApp->mWidth)
{
mRect.mWidth = mApp->mWidth;
mExpanding = false;
}
if (mRect.mHeight >= mApp->mHeight)
{
mRect.mHeight = mApp->mHeight;
mExpanding = false;
}
}
else
{
mRect.mWidth -= 2;
mRect.mHeight -= 2;
if (mRect.mHeight <= 0)
{
mRect.mHeight = 0;
mExpanding = true;
}
if (mRect.mWidth <= 0)
{
mRect.mWidth = 0;
mExpanding = true;
}
}
mRect.mX = mApp->mWidth / 2 - mRect.mWidth / 2;
mRect.mY = mApp->mHeight / 2 - mRect.mHeight / 2;
}
}
void Board::Draw(Graphics* g)
{
g->SetColor(Color::Black);
g->FillRect(0, 0, mWidth, mHeight);
// Draw our pulsing rectangle in the middle of the screen.
g->SetColor(Color(255, 0, 0));
g->FillRect(mRect);
if (!mLostFocus)
{
// Previously, any time you changed the
// graphic's state, you had to undo it, otherwise it affected anything
// drawn later. Common examples are SetColorizeImages, SetColor, SetDrawMode, etc.
// In Board::DrawOverlay, you'll see that we can use PushState and PopState to on-demand
// save/restore the graphics state. However, if we're either lazy, or after we draw
// our current stuff we don't have a need to reset the state and change it again, we
// can use the GraphicsAutoState object. This nifty helper class, upon instantiation on
// the stack, will automatically push the graphics state of the graphics object you pass in.
// Note that since stack variables are
// removed (and have their con/destructors called automatically, for classes/structs) when they
// go out of scope, the destructor for a GraphicsAutoState object takes care of popping the graphics
// state and restoring things to their previous..uh...state.
// NOTE: Graphics state is saved between widgets.
GraphicsAutoState auto_state(g);
g->DrawImage(IMAGE_HUNGARR_LOGO, 10, 100);
g->SetDrawMode(Graphics::DRAWMODE_ADDITIVE);
g->SetColorizeImages(true);
g->SetColor(Color(mUpdateCnt % 128, mUpdateCnt % 255, mUpdateCnt % 64));
g->DrawImage(IMAGE_HUNGARR_LOGO, 10, 100);
}
if (mCurtainMode != CURTAIN_INACTIVE)
{
g->SetColor(Color(255, 255, 0));
g->FillRect(0, 0, mCurtainWidth, mHeight);
g->FillRect(mWidth - mCurtainWidth, 0, mCurtainWidth, mHeight);
}
// Instead of using an overlay widget to draw our stuff when focus is lost,
// we can just use DeferOverlay. You can ONLY call this in a Draw routine. What happens
// when you call DeferOverlay is that it schedules with the WidgetManager a call to
// this widget's DrawOverlay method at a later time. You can control the order of the
// overlay layer, and place it above or below other widgets by optionally specifying
// a defer priority in the call to DeferOverlay. The higher the priority, the more toplevel
// and thus the more widgets above it will be drawn. You'll notice that in DemoWidget's constructor,
// that we set its priority to 1. Thus, if we DeferOverlay with a value of 0, the overlay will be
// drawn BELOW the demo widget, but still above all our child widgets and ourself. When mDeferPriority
// is 2, it will be drawn above ALL widgets, including the demo widget, as its priority is only 1, which
// last time I checked, is less than 2. Note that widgets by default have priority 0 and dialogs have priority 1.
if (mLostFocus)
DeferOverlay(mDeferPriority);
}
void Board::DrawOverlay(Graphics* g)
{
// Make sure you've read through Board::Draw before this guy, as it explains
// how and why we get here.
g->SetColor(Color(0, 0, 255, 175));
g->FillRect(0, 0, mWidth, mHeight);
g->SetFont(FONT_DEFAULT);
g->SetColor(Color::White);
g->DrawString(_S("LOST FOCUS"), mMsgX, mMsgY);
// PushState is a new addition. Previously, any time you changed the
// graphic's state, you had to undo it, otherwise it affected anything
// drawn later. Common examples are SetColorizeImages, SetColor, SetDrawMode, etc.
// With PushState, you save the entire previous state of things. Then, you can make
// whatever changes you like, without needing to undo them, as a resulting call to
// PopState restores things back to normal. Note that you can push/pop as much as you want.
g->PushState();
// No need to turn this off! When we PopState the previous, non-colorized state,
// will return.
g->SetColorizeImages(true);
g->SetColor(Color(0, 255, 255));
// MORE NEW STUFF? Yes. Think back to the previous framework version:
// how would you draw in real-time, a rotated and scaled image at a given
// location? Well, you could mess around with destination Rects for scaling,
// but you would have a hard time scaling and rotating all at once. You could
// create images in memory to hold the scaled or rotated versions, but that's annoying.
// BEHOLD: DrawImageTransform/F. All you have to do is create a Transform object,
// and apply various transformations to it, then call DrawImageTransform/F.
// Let's take a look:
Transform t;
// Let's rotate the image between 0 and 360 degrees (note that you could also
// use the radian version of the function). So far, not hard, right?
t.RotateDeg((float)(mUpdateCnt % 360));
float sw = 1.0f;
float sh = 1.0f;
// Don't get scared here. This is just a little trickery to expand/contract
// the image depending on the update count. All it does is expand the image
// to normal size for a second, and then shrink it to almost invisible for
// another second, which makes that pulse effect happen.
int mod = mUpdateCnt % 200;
if (mod < 100)
{
sw = (float)(mod + 1) / 100.0f;
sh = (float)(mod + 1) / 100.0f;
}
else
{
mod = 200 - mod;
sw = (float)mod / 100.0f;
sh = (float)mod / 100.0f;
}
// And now we just tell the transform object, t, that we want to scale.
// The value for the x and y direction should be a %, so 1.0 means no change in scale.
// Let's also mirror and flip the image at the same time too. To mirror and flip, we'll
// just multiply sw and sh by -1. Of course, it'll be hard to tell that it's flipped and mirrored
// since it's rotating at the same time, so if you don't believe me you can try commenting
// out the t.RotateDeg(...) call above and you'll see that it works.
t.Scale(-sw, -sh);
// And now we just pass in our transform object and it works in both 2D and 3D! Note that we can also
// draw the image at a given XY as well. We'll make it move rightward depending on the update count.
// ******************IMPORTANT NOTE:*********************
// DrawImageTransform/F and DrawImageMatrix use the CENTER of the image rather than the top left
// for XY drawing. For images of even size, the non-F form of the function will just truncate
// the the center, but for the F forms, they will be offset by 0.5f since floating point is used.
if (gSexyAppBase->Is3DAccelerated())
g->DrawImageTransformF(IMAGE_HUNGARR_LOGO, t, (float)(mUpdateCnt % (mApp->mWidth + 340)), 200.0f);
else
g->DrawImageTransform(IMAGE_HUNGARR_LOGO, t, (float)(mUpdateCnt % (mApp->mWidth + 340)), 200.0f);
g->PopState();
// You can now draw using matrices. Why the heck would you want to use matrices? Besides doing some
// crazy stuff, you can also do some cool little tricks. For example, previously it used be a pain
// to in real-time flip and mirror an image. While I won't give a tutorial on matrix algebra
// (that would take a loooooong time), I'll explain the essential parts:
SexyTransform2D matrix;
// Multiplying the X coordinate by -1 (which is the 0, 0 element of the matrix) will result in our
// image being mirrored, while multiplying the Y coordinate by -1 (1, 1 in the matrix) will result in our
// image being flipped.
matrix.m[0][0] *= -1;
matrix.m[1][1] *= -1;
// Let's also shear it. While mirroring/flipping could easily be done with the DrawImageTransform methods,
// doing more complex matrix manipulation (like shearing) can only be done with the DrawImageMatrix function.
matrix.m[0][1] = 2;
// And then we just make a call to DrawImageMatrix and give it our specified XY coordinates as well,
// and that's it! This works in both 2D and 3D modes.
g->DrawImageMatrix(IMAGE_HUNGARR_LOGO, matrix, 300, 400);
// IMPORTANT COMPARISON NOTE:
// DrawImageTransform/F will try to use the faster drawing methods if it recognizes certain
// common transforms, like rotating and scaling. This only works though if you are using
// either one operation, or multiple operations of the same type (i.e. you only used scale
// or rotate with the Transform object, or used the same one multiple times). If you mix
// operations, like we do above, the DrawImageTransform method will actually use DrawImageMatrix.
// With DrawImageTransform, you can't directly modify the underlying matrix, so if that's something
// you need to do, then DrawImageMatrix is a better option.
}
void Board::ButtonDepress(int id)
{
if (id == mDemoButton->mId)
{
delete mDemoWidget;
mDemoWidget = new DemoWidget();
mApp->mWidgetManager->AddWidget(mDemoWidget);
// What, more flags? Yup. Since our little DemoWidget isn't a dialog, when we add it,
// it won't change anything about the widgets drawn below it. Which means, unmodified,
// mouse clicks could still be passed down to the board (if the click wasn't in the DemoWidget),
// the board still updates, still draws, etc. Let's turn off mouse clicks for all widgets below the
// DemoWidget. But, let's still allow all widgets below it to update. Note that if we used the form
// of the method that only takes one parameter, then it would use mDefaultBelowModalFlagsMod
// which we modified in our app. By passing in our own flags though, they're used instead.
// Which means the only flag we need to remove from the widgets below it is the allow mouse flag.
// We do that by making a temp FlagsMod object, and setting
// its mRemoveFlags variable to be ORed with WIDGETFLAGS_ALLOW_MOUSE. Upon calling
// the WidgetManager's AddBaseModal method, we then pass in the DemoWidget, and the above flags.
// AddBaseModal will then treat this new widget as a modal object, applying any flags we passed in.
FlagsMod flags;
flags.mRemoveFlags |= WIDGETFLAGS_ALLOW_MOUSE;
mApp->mWidgetManager->AddBaseModal(mDemoWidget, flags);
}
else if (id == mDialogButton->mId)
{
// With the new 1.2 changes, you can create dialogs very easily. Previously, you had to supply
// images in order to skin the dialog and be able to see it in the first place. You also had to
// override the SexyAppBase::NewDialog method, otherwise calls to SexyAppBase::DoDialog wouldn't
// do anything. Not so anymore. You can rapidly prototype with dialogs even without images.
// A default NewDialog implementation now exists, and if a dialog doesn't have an image, it will
// be drawn using colored rectangles. So now, to make a little dialog box appear, it's as simple as
// the single line of code below. No messy functions to write, no images to create, that's a hassel
// when you're starting a new app and don't care about the initial appearance of your UI elements and
// just want to start testing gameplay immediately.
Dialog* d = mApp->DoDialog(100, true, _S("Fun Dialog"), _S("Line 1\nLine 2\nLine 3"), _S("Close!"), Dialog::BUTTONS_FOOTER);
// Using the default font, which is a system font, can sometimes cause problems on older OS's, like
// Windows 95 or 98, in which printing with it appears to produce blank results. Let's set the font
// for the button on the dialog box to be FONT_DEFAULT.
d->SetButtonFont(FONT_DEFAULT);
}
else if (id == mCurtainButton->mId)
{
mCurtainMode = CURTAIN_CLOSING;
mCurtainWidth = 0;
// Here's some more new stuff. Previously, if you wanted to do something like
// a transition after a button is pressed (for example), you had to set a bunch of
// variables in your class, check them, and process/draw differently depending on
// the transition state. That can be both messy and annoying to do. Why not just
// do the logic for the transition right at the site where it has to trigger, in this
// case, right when the button is pressed? If you ignore the UpdateApp() call below for
// a minute, what the loop looks like is just a simple loop that makes a variable expand
// to 1/2 the app width, and then contract back to 0. If you removed the call to UpdateApp,
// obviously the program would be stuck in the loop until it completed, making it look like the
// whole thing is frozen, and not updating the display. By putting UpdateApp in the while loop,
// you enable ALL the other widgets to update, and thus draw, which in turn means we can see
// our little curtain transition effect working as expected. You could even use this method
// to block while waiting for a dialog/widget to return, but to still allow other widgets
// to update/draw in the process. ONE VERY IMPORTANT THING TO NOTE THOUGH:
// If you have code like this in your Update methods, be careful, as you can run into some
// reentrancy problems. If you place this sort of code in your Update methods, make sure you
// protect it (you could just use a simple bool that's true if doing a transition, otherwise false).
// Why? Think about it like this: UpdateApp updates ALL widgets, including the one in which the
// while loop exists. Thus, when going back into Update for said widget, it will once again get
// stuck in the same while loop, which will effectively prevent any drawing from occurring.
// Just remember that UpdateApp will update EVERY widget, and so you will want to avoid having
// your while loop called more often than it should. Of course, an easy way to avoid all that
// is to have your transition code happen elsewhere that isn't updated every frame, like a
// button down method.
//
// What you'll notice is that if the game lost focus, the curtain call still updates, although
// the shrinking/expanding rectangle in the middle does not. That is because when focus is lost,
// we don't update that part of the app, but this while loop is still running, which means the
// curtain effect is still running too.
while ((mCurtainMode != CURTAIN_INACTIVE) && mApp->UpdateApp())
{
if (mCurtainMode == CURTAIN_CLOSING)
{
mCurtainWidth += 4;
if (mCurtainWidth >= mWidth / 2)
{
mCurtainWidth = mWidth / 2;
mCurtainMode = CURTAIN_OPENING;
}
}
else
{
mCurtainWidth -= 4;
if (mCurtainWidth <= 0)
{
mCurtainWidth = 0;
mCurtainMode = CURTAIN_INACTIVE;
}
}
}
}
}