*//*
With the ability to create customized GUI, we can actually start to do something interesting with the engine. This lesson will be a simulation of heat exchange on a 2 dimensional surface made up of rectangular grid cells. Each grid cell will indicate its temperature with colors, making a nice visual representation of the temperature distribution throughout the material.
The cells will be implemented using a customized subclass of Widget (HeatButton
), and will be formed into a grid using the good old Layout class. Furthermore, we will create a custom container widget (HeatSimulation
) to hold the grid, the other widgets, and to facilitate interaction between the GUI elements.
HeatSimulation
will contain all GUI elements necessary for the operation of the application, so all that's required in the main
function (besides the previously covered initialization/game loop/shutdown stuff) is to create an instance of HeatSimulation and set it as the main widget of Screen.
It should be noted that implementing this heat exchange simulation using a massive grid of GUI widgets is inefficient and a retarded way to go, but it's perfect for the purposes of this lesson. Also note that creating the dozens (or hundreds, if you modify GRID_WIDTH
and GRID_HEIGHT
) of widgets inside the Layout will take a long time due to the (as of August 2006) calculation-intensive Layout resizing code.
Procedural Overview -- Items in bold are additions/changes to the previous lesson.
g_desired_framerate
g_mouse_temperature_change_rate
g_temperature_retention_rate
m_accepts_mouseover
to true
. Comments explaining previously covered material will be made more terse or deleted entirely in each successive lesson. If something is not explained well enough, it was probably already explained in previous lessons.
Code Diving!
*/ #include "xrb.hpp" // Must be included in every source/header file. #include "xrb_containerwidget.hpp" // For use of the ContainerWidget class. #include "xrb_event.hpp" // For use of the Event classes. #include "xrb_eventqueue.hpp" // For use of the EventQueue class. #include "xrb_frameratecalculator.hpp" // For use of the FramerateCalculator class. #include "xrb_inputstate.hpp" // For use of the InputState class (via Singleton::). #include "xrb_label.hpp" // For use of the Label class. #include "xrb_layout.hpp" // For use of the Layout widget class. #include "xrb_render.hpp" // For use of the Render namespace functions. #include "xrb_screen.hpp" // For use of the necessary Screen widget class. #include "xrb_sdlpal.hpp" // For use of the SDLPal platform abstraction layer. #include "xrb_validator.hpp" // For use of various Validator subclasses. #include "xrb_valueedit.hpp" // For use of the ValueEdit<T> template class. #include "xrb_valuelabel.hpp" // For use of the ValueLabel<T> template class. #include "xrb_widget.hpp" // For use of the Widget class. using namespace Xrb; // To avoid having to use Xrb:: everywhere. /*
g_desired_framerate
is the target framerate for the game loop. The actual framerate may not match if the computer is not fast enough. g_mouse_temperature_change_rate
is the number of */
Float g_desired_framerate = 30.0f;
Float g_mouse_temperature_change_rate = 200.0f;
Float g_temperature_retention_rate = 0.001f;
/*
Our widget will behave as a single cell in a grid of heat-sensitive material, so it will have a "temperature" value and an "ambient temperature" value. The temperature value is the current temperature of this grid cell, and will determine what color the cell is. The ambient temperature value is the effective external temperature experienced by this grid cell, and will be used in per-frame calculations to update the temperature value.
The constructor must accept as an argument a pointer to the parent Widget, and optionally a name (which we will assign a default value of "HeatButton"). The constructor can also accept whatever other parameters you want, though no extra are needed here. The convention is to put additional parameters before the superclass' parameters (see Label, Button, ValueLabel, etc).
For this widget, there are 2 Widget methods we will override to implement the custom behavior -- Draw
and HandleFrame
. Widget provides many many overridable virtual methods to facilitate behavior customization.
*/ class HeatButton : public Widget { public: /*
m_accepts_mouseover
to true
here. m_accepts_mouseover
is a protected member of Widget which indicates if mouseover events will be caught and processed by this widget. If we want to use the Widget::IsMouseover accessor, this must be set to true
. If m_accepts_mouseover
remains the default value of false
, this widget will allow mouseover events to fall through to the widget(s) behind it, including its parent widget, which may be desirable if you're writing custom HUD widgets for a game. */ HeatButton (ContainerWidget *parent, std::string const &name = "HeatButton") : Widget(parent, name) { m_temperature = 0.0f; m_ambient_temperature = 0.0f; m_accepts_mouseover = true; } // Accessor for temperature -- used in HeatSimulation::HandleFrame. inline Float Temperature () const { return m_temperature; } // Modifier for ambient temperature -- also used in HeatSimulation::HandleFrame. inline void SetAmbientTemperature (Float ambient_temperature) { m_ambient_temperature = ambient_temperature; } /*
*/ virtual void Draw (RenderContext const &render_context) const { // Normalize the temperature from (-inf, +inf) to [0, 1] so we'll // have a value that can parameterize linear interpolations with to // calculate the color. A temperature of 0 will translate into // a normalized_temperature of 0.5. Float normalized_temperature = 0.5f * (1.0f + Math::Atan(m_temperature) / 90.0f); // Calculate the red, green and blue components for the color. Each // component in the RGBA color value must be within the range [0, 1], // with an alpha value of 0 being completely transparent and an alpha // value of 1 being completely opaque. We'll just use a boring old // greyscale gradient for simplicity. Because of the above // normalization, a temperature of 0 will be 50% grey, a temperature // approaching -infinity will approach pure black, and a temperature // approaching +infinity will approach pure white. Color button_color( normalized_temperature, // red component normalized_temperature, // green component normalized_temperature, // blue component 1.0f); // alpha component (completely opaque) // Draw a rectangle which fills this widget's screen rect using the // calculated color. Render::DrawScreenRect(render_context, button_color, ScreenRect()); } protected: /*
m_temperature
using m_ambient_temperature
and to modify m_temperature
based on mouse input. The HandleFrame method originally comes from FrameHandler (which is inherited by Widget) and there are a few notable provided methods: FrameHandler::FrameTime and FrameHandler::FrameDT which are only available during the execution of HandleFrame (or in a function called by it), and FrameHandler::MostRecentFrameTime which is available at any time. */ virtual void HandleFrame () { ASSERT1(g_temperature_retention_rate > 0.0f); ASSERT1(g_temperature_retention_rate < 1.0f); // Calculate heat transfer. The amount is proportional to the // difference between m_temperature and m_ambient_temperature -- the // fancy looking exponential factor is used to correctly scale the // amount based on time. g_temperature_retention_rate is the ratio of // a grid cell's heat relative to the ambient temperature it retains // over one second. Thus, a high value means the heat spreads slowly, // while a low value causes heat to spread quickly. m_temperature += (m_ambient_temperature - m_temperature) * (1.0f - Math::Pow(g_temperature_retention_rate, FrameDT())); // If the mouse cursor is currently over this widget and the left mouse // button is pressed, increase the temperature. This allows the user // to manually heat up grid cells. Note that this isn't the primary // method of facilitating mouse input -- Xrb::Event based mouse input // will be covered later. if (IsMouseover() && Singleton::InputState().IsKeyPressed(Key::LEFTMOUSE)) m_temperature += g_mouse_temperature_change_rate * FrameDT(); } private: Float m_temperature; Float m_ambient_temperature; }; // end of class HeatButton /*
The constructor will create and initialize all the subordinate GUI elements and make necessary signal connections.
We will only need to override ContainerWidget::HandleFrame for the desired custom behavior, since a container widget is only rendered to screen by proxy through its children's Draw methods.
*/ class HeatSimulation : public ContainerWidget { public: /*
HeatSimulation
is similar to that of HeatButton
-- it accepts the parent widget and a widget name as parameters, and passes them directly to the superclass constructor.
Notice the constructors for the member variables m_desired_framerate_validator
and m_temperature_retention_range_validator
. They are instances of implementations of the Validator interface class which is used to constrain values to preset valid ranges. These are primarily useful in user interface code where the user may enter any retarded value, but must be constrained to a particular range. The motivation to use these seemingly unwieldy objects is to avoid having to add in extra code to manually correct invalid values. The goal is to have all GUI elements, signal connections, value validation (and really everything in general) to be as plug-and-forget (pronounced "hard to fuck up") as possible. The Validator subclasses are templatized for your convenience.
In the game loop, the calculations to determine the value to pass to Singleton::Pal().Sleep involves dividing by g_desired_framerate
, and therefore we must avoid setting g_desired_framerate
to zero. m_desired_framerate_validator
is an instance of GreaterOrEqualValidator<Uint32> -- we will limit the desired framerate to be greater or equal to 1 (the parameter passed into the constructor for m_desired_framerate_validator
).
Note that unlike the constructor for HeatButton
, we do not set m_accepts_mouseover
to true
. This is because ContainerWidgets by themselves should not accept mouseover -- only their children should. This is to prevent ContainerWidgets which contain only m_accepts_mouseover = false
from accepting mouseover instead of perhaps some background widget (such as a game view widget) which may want it. For example, an in-game HUD Layout containing only Labels -- you would not want it or its children accepting mouseover and blocking the game view widget from accepting mouseover.
*/ HeatSimulation (ContainerWidget *parent, std::string const &name = "HeatSimulation") : ContainerWidget(parent, "HeatSimulation"), /*
*/
m_desired_framerate_validator(1),
/*
In the following constructor -- for RangeExclusive<Float> -- the first parameter is the lower bound of the exclusive range. The second is the minimum valid value which will be used if a value-to-be-validated is less than or equal to the lower bound value. The third parameter is the maximum valid value. It is similarly used when a value-to-be-validated is greater or equal to the upper bound value. The final parameter is the upper bound of the exclusive range. See Xrb::RangeExclusiveValidator for more.
*/ m_temperature_retention_range_validator(0.0f, 1e-20f, 0.999999f, 1.0f), m_internal_receiver_set_desired_framerate(&HeatSimulation::SetDesiredFramerate, this), m_internal_receiver_set_temperature_retention_rate(&HeatSimulation::SetTemperatureRetentionRate, this) { Layout *main_layout = new Layout(VERTICAL, this); main_layout->SetIsUsingZeroedLayoutSpacingMargins(true); SetMainWidget(main_layout); Layout *grid_layout = new Layout(ROW, GRID_WIDTH, main_layout, "grid layout"); /*
ContainerWidget::ChildResizeBlocker must be created as a stack variable (no new'ing one up on the heap) so that when it goes out of scope and its destructor is called, the applicable ContainerWidget is unblocked. The rationale is again to provide a plug-and-forget mechanism for ease of use and to reduce the possibility of programmer error.
*/ ContainerWidget::ChildResizeBlocker blocker(grid_layout); grid_layout->SetIsUsingZeroedFrameMargins(true); grid_layout->SetIsUsingZeroedLayoutSpacingMargins(true); // Create the HeatButton grid in a doubly-nested loop, saving pointers // to each grid cell in the 2 dimensional array, m_button_grid for // use in temperature calculations. for (Uint32 y = 0; y < GRID_HEIGHT; ++y) for (Uint32 x = 0; x < GRID_WIDTH; ++x) m_button_grid[y][x] = new HeatButton(grid_layout); // Create a layout for the controls below the grid and enable // the frame margins (which are zeroed-out by default). Layout *sub_layout = new Layout(HORIZONTAL, main_layout); sub_layout->SetIsUsingZeroedFrameMargins(false); // ValueLabel<T> is a templatized subclass of Label which contains a // value instead of a string. It has Value and SetValue methods, // and corresponding SignalSenders and SignalReceivers. It is very // flexible, due to its value-to-text-printf-format and // text-to-value-function constructor parameters. new Label("Actual Framerate:", sub_layout); m_actual_framerate_label = new ValueLabel<Uint32>("%u", Util::TextToUint<Uint32>, sub_layout); m_actual_framerate_label->SetAlignment(Dim::X, LEFT); // ValueEdit<T> is a templatized subclass of LineEdit, analogous to // ValueLabel<T>. You can type into it and it will attempt to use // the specified text-to-value-function to convert it to a value. // A validator may also be specified to enforce valid input values. new Label("Desired Framerate:", sub_layout); ValueEdit<Uint32> *desired_framerate_edit = new ValueEdit<Uint32>("%u", Util::TextToUint<Uint32>, sub_layout); desired_framerate_edit->SetValidator(&m_desired_framerate_validator); desired_framerate_edit->SetValue(static_cast<Uint32>(Math::Round(g_desired_framerate))); // We'll connect this one to this HeatSimulation's SetDesiredFramerate // SignalReceiver. SignalHandler::Connect1( desired_framerate_edit->SenderValueUpdated(), &m_internal_receiver_set_desired_framerate); // Similarly create a ValueEdit for the temperature retention rate // and connect it up. new Label("Temperature Retention Rate:", sub_layout); ValueEdit<Float> *temperature_retention_rate_edit = new ValueEdit<Float>("%g", Util::TextToFloat, sub_layout); temperature_retention_rate_edit->SetValue(g_temperature_retention_rate); temperature_retention_rate_edit->SetValidator(&m_temperature_retention_range_validator); SignalHandler::Connect1( temperature_retention_rate_edit->SenderValueUpdated(), &m_internal_receiver_set_temperature_retention_rate); // Ensure DISTRIBUTION_FUNCTION_WIDTH and DISTRIBUTION_FUNCTION_HEIGHT // are odd, so that there is an exact center in the array. ASSERT0(DISTRIBUTION_FUNCTION_WIDTH % 2 == 1); ASSERT0(DISTRIBUTION_FUNCTION_HEIGHT % 2 == 1); // Make sure the center weight in the distribution function // (representing the target square) is zero. ASSERT0(ms_distribution_function[DISTRIBUTION_FUNCTION_HEIGHT/2][DISTRIBUTION_FUNCTION_WIDTH/2] == 0.0f); // Calculate the total of all weights in the distribution function, // so that later calculations can divide by this value and be ensured // that the adjusted total weight of the distribution function is one. m_distribution_normalization = 0.0f; for (Uint32 y = 0; y < DISTRIBUTION_FUNCTION_HEIGHT; ++y) for (Uint32 x = 0; x < DISTRIBUTION_FUNCTION_WIDTH; ++x) m_distribution_normalization += ms_distribution_function[y][x]; } protected: /*
*/ virtual void HandleFrame () { // must call the superclass' HandleFrame -- this is where // all the child widgets' ProcessFrameOverrides are called. ContainerWidget::HandleFrame(); // Keep track of the framerate and update the "Actual Framerate" label. m_framerate_calculator.AddFrameTime(FrameTime()); m_actual_framerate_label->SetValue( static_cast<Uint32>(Math::Round(m_framerate_calculator.Framerate()))); // Compute and set the ambient temperature for each grid cell, // using the distribution function. for (Sint32 gy = 0; gy < GRID_HEIGHT; ++gy) for (Sint32 gx = 0; gx < GRID_WIDTH; ++gx) { // Calculate the ambient temperature by summing the weighted // temperatures from adjacent grid cells indicated by the // distribution function. Float ambient_temperature = 0.0f; for (Sint32 dy = -DISTRIBUTION_FUNCTION_HEIGHT/2; dy <= DISTRIBUTION_FUNCTION_HEIGHT/2; ++dy) for (Sint32 dx = -DISTRIBUTION_FUNCTION_WIDTH/2; dx <= DISTRIBUTION_FUNCTION_WIDTH/2; ++dx) // If the array indices would go out of bounds, don't // add to the running total ambient temperature. if (gy+dy >= 0 && gy+dy < GRID_HEIGHT && gx+dx >= 0 && gx+dx < GRID_WIDTH) ambient_temperature += ms_distribution_function[dy+DISTRIBUTION_FUNCTION_HEIGHT/2][dx+DISTRIBUTION_FUNCTION_WIDTH/2] / m_distribution_normalization * m_button_grid[gy+dy][gx+dx]->Temperature(); m_button_grid[gy][gx]->SetAmbientTemperature(ambient_temperature); } } private: // Various enums for named constants (array dimension sizes in this case). enum { GRID_WIDTH = 16, GRID_HEIGHT = 12, DISTRIBUTION_FUNCTION_WIDTH = 5, DISTRIBUTION_FUNCTION_HEIGHT = 5 }; /*
m_internal_receiver_set_desired_framerate
-- when said receiver is signaled, it sets g_desired_framerate
with the specified value. Specifically, the receiver will be hooked up to the desired framerate ValueEdit<Uint32>::SignalValueUpdated signal. */ void SetDesiredFramerate (Uint32 desired_framerate) { ASSERT1(desired_framerate > 0); g_desired_framerate = static_cast<Float>(desired_framerate); } /*
m_internal_receiver_set_temperature_retention_rate
-- to set g_temperature_retention_rate
, and will be hooked up to the temperature retention rate ValueEdit<Float>::SignalValueUpdated signal. */ void SetTemperatureRetentionRate (Float temperature_retention_rate) { ASSERT1(temperature_retention_rate > 0.0f); ASSERT1(temperature_retention_rate < 1.0f); g_temperature_retention_rate = temperature_retention_rate; } HeatButton *m_button_grid[GRID_HEIGHT][GRID_WIDTH]; ValueLabel<Uint32> *m_actual_framerate_label; FramerateCalculator m_framerate_calculator; GreaterOrEqualValidator<Uint32> m_desired_framerate_validator; RangeExclusiveValidator<Float> m_temperature_retention_range_validator; Float m_distribution_normalization; SignalReceiver1<Uint32> m_internal_receiver_set_desired_framerate; SignalReceiver1<Float> m_internal_receiver_set_temperature_retention_rate; static Float const ms_distribution_function[DISTRIBUTION_FUNCTION_HEIGHT][DISTRIBUTION_FUNCTION_WIDTH]; }; // end of class HeatSimulation Float const HeatSimulation::ms_distribution_function[DISTRIBUTION_FUNCTION_HEIGHT][DISTRIBUTION_FUNCTION_WIDTH] = { { 0.00f, 0.05f, 0.10f, 0.05f, 0.00f }, { 0.05f, 0.15f, 0.30f, 0.15f, 0.05f }, { 0.10f, 0.30f, 0.00f, 0.30f, 0.10f }, { 0.05f, 0.15f, 0.30f, 0.15f, 0.05f }, { 0.00f, 0.05f, 0.10f, 0.05f, 0.00f } }; void CleanUp () { fprintf(stderr, "CleanUp();\n"); // Shutdown the Pal and singletons. Singleton::Pal().Shutdown(); Singleton::Shutdown(); } int main (int argc, char **argv) { fprintf(stderr, "main();\n"); // Initialize engine singletons. Singleton::Initialize(SDLPal::Create, "none"); // Initialize the Pal. if (Singleton::Pal().Initialize() != Pal::SUCCESS) return 1; // Set the window caption. Singleton::Pal().SetWindowCaption("XuqRijBuh Lesson 03"); // Create Screen object and initialize given video mode. Screen *screen = Screen::Create(800, 600, 32, false); // If the Screen failed to initialize, print an error message and quit. if (screen == NULL) { CleanUp(); return 2; } // At this point, the singletons and the Pal have been initialized, the // video mode has been set, and the engine is ready to go. Here is where // the application-specific code begins. { // this is the only child of Screen HeatSimulation *main_widget = new HeatSimulation(screen); screen->SetMainWidget(main_widget); // These values will be used below in the framerate control code. Float current_real_time = 0.0f; Float next_real_time = 0.0f; // Run the game loop until the Screen no longer has the will to live. while (!screen->IsQuitRequested()) { /*
Singleton::Pal().CurrentTime
to calculate how long to sleep to attempt to achieve the exact desired framerate. */ // Retrieve the current real time in seconds as a Float. current_real_time = 0.001f * Singleton::Pal().CurrentTime(); // figure out how much time to sleep before processing the next frame Sint32 milliseconds_to_sleep = Max(0, static_cast<Sint32>(1000.0f * (next_real_time - current_real_time))); // Delay for the calculated number of milliseconds. Singleton::Pal().Sleep(milliseconds_to_sleep); // Calculate the desired next game loop time (which should never // fall below current_real_time. next_real_time = Max(current_real_time, next_real_time + 1.0f / g_desired_framerate); // Process events until there are no more. Event *event = NULL; while ((event = Singleton::Pal().PollEvent(screen, current_real_time)) != NULL) { // Let the InputState singleton "have a go" at keyboard/mouse events. if (event->IsKeyEvent() || event->IsMouseButtonEvent()) Singleton::InputState().ProcessEvent(event); // Give the GUI hierarchy a chance at the event and then delete it. screen->ProcessEvent(event); Delete(event); } // Turn the EventQueue crank, Perform off-screen GUI processing, // turn the EventQueue crank again, and then draw everything. screen->OwnerEventQueue()->ProcessFrame(current_real_time); screen->ProcessFrame(current_real_time); screen->OwnerEventQueue()->ProcessFrame(current_real_time); screen->Draw(); } } // Delete the Screen (and GUI hierarchy), "SHUT IT DOWN", and return success. Delete(screen); CleanUp(); return 0; } /*
Exercises
grid_layout
's SetIsUsingZeroedFrameMargins and SetIsUsingZeroedLayoutSpacingMargins and observe the visual change. HeatSimulation::m_button_grid
and HeatSimulation::ms_distribution_function
to be 1-dimensional arrays, and perform the row-major array indexing yourself. They are both currently indexed with the Y component first to make this switch easier. HeatSimulation::ms_distribution_function
to cause the temperature changes to propogate up the screen like fire. Along the bottom row of the grid, simulate fluctuating embers via random temperatures. You may need to add additional methods to HeatButton
and/or HeatSimulation
. HeatSimulation::ms_distribution_function
back to its original value and disable the fluctuating ember code before you do this). Thus concludes lesson03. Somehow you're actually dumber now from it.