Blog

DigitalRune Game UI: The GUI Rendering Process

Sep 28

Written by:
Wednesday, September 28, 2011  RssIcon

In the last article about the DigitalRune Game UI we examined how the size and position of a GUI control is determined. Yet we have not discussed where and how the controls are drawn to the screen. This the topic of this article. – This will be another rather dry article, but we are trying to get a lot of information and knowledge across for those who want to extend or learn from our DigitalRune Game UI library. So let’s jump right in…

Rendering-Specific Parts of the UIControl

The UIControl has several properties and methods related to the rendering. The Render() method is obviously one of them. This method is called by the UI system when the control should be rendered. It checks if the control is visible (property IsVisible), if the control is not completely transparent (property Opacity) and if the actual control size is larger than 0 – otherwise there is no need to render the control.

If the control must be rendered, Render() calls OnRender() to do the real work. The OnRender() method can be overridden in derived controls classes. The base implementation simply calls the UI renderer of the current screen:

protected virtual void OnRender(float deltaTime)
{
  Screen.Renderer.Render(this, deltaTime);
}

UIControl_Rendering_MembersVery simple. The purpose of the renderer is discussed below.

It is not necessary that the OnRender() methods of custom controls call the renderer to draw the control. Instead, custom controls can do the drawing right there in this method using, for example, the normal XNA SpriteBatch. This makes it very easy to quickly add new custom controls.

The UIControl further has a few properties that should be used by the renderer or the custom OnRender() implementation: the Background color, the Foreground color, the Font name and the Opacity of the whole control.

The RenderTransform

Each UIControl has a RenderTransform. This is a transformation that is composed of RenderScale, RenderRotation, RenderTranslation and RenderTransformOrigin. Each property can be animated individually to scale, rotate and translate the control to create visual effects and transitions – have a look at the UIAnimationSample of the DigitalRune Game UI samples.

The property HasRenderTransform is true if the control is scaled, rotated or translated. It is false, if the render transform is the identity transform that does nothing.

The RenderTransform is also considered by the control input handling. That means, it is possible to click a rotated and scaled button. But the RenderTransform is not used in the layout process. Therefore, a scaled/rotated/translated control does not influence the layout. For example, a scaled control does not get more space in a stack panel. The rendered control may overlap other controls.

InvalidateVisual, IsVisualValid and RendererInfo

The property RendererInfo can be used by the renderer (or the custom OnRender() method) to cache any information it likes to store with the control. The property IsVisualValid is automatically set to true after the control was rendered (assuming that the renderer has updated the cached information in RendererInfo). IsVisualValid is reset when InvalidateMeasure(), InvalidateArrange() or InvalidateVisual() are called. Controls that influence the control size (e.g. Width), the layout arrangement (e.g. HorizontalAlignment) or the visual appearance (e.g. Foreground) call the invalidate methods automatically. The renderer can check IsVisualValid to determine if it can reuse the cached info in RendererInfo.

The current version of the DigitalRune Game UI is not optimized. Therefore, the current controls do not make intensive use of these properties. But in the future, the renderer can render the control into a texture, cache this texture in RendererInfo and reuse it until IsVisualValid is false. – This is the plan.

VisualState

The VisualState property is a simple string that tells the renderer (or the custom OnRender() method) in which state the control currently is. The base implementation of this property is simple:

public virtual string VisualState
{
  get { return ActualIsEnabled ? "Default" : "Disabled"; }
}

The rendering code can check this property and, for example, draw a disabled button using different button texture.

Other controls can override this property to define more states. For example, here is the VisualState property of a button:

public override string VisualState
{
  get   
  {    
    if (!ActualIsEnabled)      
      return "Disabled";     

    if (IsDown)
      return "Pressed";
 
    if (IsMouseOver)
      return "MouseOver";
 
    if (IsFocused)
      return "Focused";
 
    return "Default";
  }
}

The IUIRenderer Interface

The properties discussed above define the visual appearance of the control. The real rendering is separated from the UIControl code. This is the purpose of the interface IUIRenderer.

The UIScreen is the root of the control hierarchy. When a screen is created it takes the renderer as a parameter in the constructor. For example:

// Load a UI theme, which defines the appearance and default values of UI controls.
ContentManager uiThemeContent = new ContentManager(Game.Services, "WindowsPhone7Theme");
Theme theme = uiThemeContent.Load<Theme>("ThemeDark");

// Create a UI renderer, which uses the theme info to renderer UI controls.
UIRenderer renderer = new UIRenderer(Game, theme);

// Create a UIScreen and add it to the UI service. The screen is the root of the 
// tree of UI controls. Each screen can have its own renderer. 
_screen = new UIScreen("Default", renderer);

// Add the screen to the UI service. 
uiService.Screens.Add(_screen);

 

Where does the rendering start?

IUIRendererThe UIScreen implements IDrawable (the XNA interface) which means it has a Draw() method. This method must be called in each frame to draw the controls. The UIManager does not draw the controls. Therefore, you must not forget to call UIScreen.Draw(). The big advantage is that you can call the method where ever it fits into your render pipeline. As the DigitlaRune Game UI samples show you can also render the whole screen into an off-screen render target.

UIScreen.Draw() calls UIScreen.Render(). UIScreen.Render() calls UIScreen.OnRender() which in turn lets the renderer do the work using IUIRenderer.Render(). When the renderer draws a control, it calls UIControl.Render() of all child controls. This way, Render() of each control in the control tree is executed. Each child control can decide in its OnRender() method if it wants to call IUIRenderer.Render() or do the drawing manually.

IUIRenderer.SpriteBatch

The IUIRenderer manages a single SpriteBatch, which is used to batch all sprite batch draw calls. The UIScreen will call BeginBatch() to start the batch (using SpriteBatch.Begin()). Rendering code can use the sprite batch to draw textures and text.

Please note:Whenever the render state is changed EndBatch() must be called to commit the current batch. (Don’t forget this if you implement custom rendering code.)

The Control Style

In our design, we strived to separate control logic from rendering. The control classes define the control logic (e.g. how a check box behaves), and the exchangeable IUIRenderer defines the visual appearance.

But in order to fully control the visual appearance, the IUIRenderer must be able to supply default values for properties, like Foreground color, Width, etc. And here is how it works:

Each UIControl has a Style property. This simply a string; usually equal to the control name.

The IUIRenderer has a dictionary called Templates. This dictionary stores a ready-initialized UIControl instance for each style name. Templates is like a collection of prototypes; one prototype per style.

When a control is loaded, it locates the prototype for its current style in Templates and sets this prototype in its own UIControl.Template property. Template is a property of our game object system (of the library DigitalRune Game). Whenever a control has not set a local property value, it uses the property value of the template object. This way we can, for example, quickly change the appearance of all Buttons simply by changing the values of the template.

The templates are created on demand: If IUIRenderer.Templates does not contain a template for a given type, then a new empty instance is created. All properties of this instance are initialized with the values provided by the GetAttribute() method of the IUIRenderer. The default IUIRenderer implementation (class UIRenderer) reads the Theme.xml file and returns these values in GetAttribute().

This way, the IUIRenderer can define the default values for control properties. And this mechanism has a bunch of other advantages that we will explain in more detail in the future, when we cover our game object system details.

Here is a concrete example that demonstrate the power of styles. The full code (except comments) for a MenuItem looks like this:

public class MenuItem : ButtonBase
{
  static MenuItem()
  {
    OverrideDefaultValue(typeof(MenuItem), FocusWhenMouseOverPropertyId, true);
  }

  public MenuItem()
  {
    Style = "MenuItem";
  }
}

It is simply a button (base class ButtonBase). Per default it gets input focus when the mouse is over the menu item (the OverrideDefaultValue() call). The style is set to “MenuItem" in the constructor. Since the menu item style is different from the style of a normal button, the control gets very different property values than a normal button, and the renderer will render this control in a totally different way.

Using styles, the control code can focus on the control logic. And the renderer has full control over how the control should appear.

Conclusion

In the DigitalRune Game UI library, we have separated control logic from the rendering. The UI renderer has the ability to change the control properties. But we do not force you to use the IUIRenderer. A custom control can do the drawing directly in the OnRender() method. However, using the renderer has several advantages: Drawing code is separated from the control logic. Developers can exchange the whole UI renderer. And with the default implementation, class UIRenderer, the library provides a very flexible renderer that can be configured using the Theme.xml and can be extended. We will explore the UIRenderer implementation in a future article.

2 comment(s) so far...


Gravatar

Re: DigitalRune Game UI: The GUI Rendering Process

I'm trying to create a new style by using the methods in your previous tutorial. I found an entry in the Neoforce theme's ThemeRed.xml called '' that seems to define the styling for text in the tabs on the tab control.

When I alter the values in the Selected, Disabled, etc. nodes, the renderer seems to ignore the change. I added a node for Default and am now able to change the color, but it ignores all the other states (mouseover, selected, etc.)

I'm also curious as to how I'd go about implementing a tiling method as the default alignment for most control backgrounds. Currently there appears to be only static and stretch methods. I'd like to be able to define a left and right "cap" for the control, and then a background which would tile. This would avoid the stretching of the image, which places serious constraints on image-heavy controls because the stretching degrades the image quality so badly.

[[LeftImage(fadeOut)]--BackgroundImage(tile)--[(fadeIn)RightImage]]

By Furiant on   Thursday, September 29, 2011
Gravatar

Re: DigitalRune Game UI: The GUI Rendering Process

Hi Furiant, there is a bug in our Neoforce theme in the "TabItemTextBlock" style: The state Selected must be marked as IsInherited="true" and all inherited states must be the first in the list (Selected must come be before Default). Have a look at the BlendBlue Theme.xml where it is correct.

Regarding image tiling:
Currently the theme supports images with a non-stretched border and a stretched center using the Border attribute. A tiled center is not implemented. My first idea to add support for a tiled center:
- Use a new attribute for the image tag: StretchMode="Tile"
- In the DigitalRune.Game.UI.Content.Pipeline project: The ThemeProcessor must read the new attribute and store it in ThemeImageContent. ThemeWriter must write the new attribute.
- In the DigitalRune.Game.UI project: The ThemeReader must read the new attribute and store it in ThemeImage. Then you must add the tile rendering code to the method UIRenderer.RenderImage().
I hope I did not forget anything ;-)

If you need more advice, drop us a line in our forum!

By HelmutG on   Friday, September 30, 2011

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Article Collection

A collection of the most useful blog articles can be found here:

Article Collection
(on Documentation page
)