In this tutorial you will learn about overlays - two-dimentional entities in a three-dimentional world. We will also demonstrate how to create the begining of a very simple in-game GUI using overlays. |
If you find any errors in this tutorial please send a private message to amirabiri.
Prerequisites
- This tutorial assumes you have knowledge of C# programming and are able to setup and compile a Mogre application.
- This tutorial also assumes that you have created a project using the Mogre Wiki Tutorial Framework.
- This tutorial builds on the previous tutorials, and it assumes you have already worked through them.
Table of contents
Getting Started
As with the last tutorial, we will be using the Mogre Wiki Tutorial Framework as our starting point.
Overview
Overlays are a very simple concept in essence: they are 2D images loaded like any other texture, but instead of being used as part of a mesh, they are simply "layed over" the camera, hence their name.
In fact, you have already seen them - the tutorial framework provides you with a simple HUD (Heads Up Display) - a display of framerate and other information on the bottom left corner of the render window, and the OGRE logo on the bottom right. These are all overlays:
Actually, to be precise both the framerate box an the logo are Overlay Elements, and both of them are parts of the same Overlay. So the more accurate definition is that an overlay is a collection of 2D elements arranged in some fashion on the render window. At any given point multiple overlays can be defined and loaded in Ogre, all of which, some - or none at all - can be visible.
(If you are familiar with GUI programming under Win32 or X in unix, you might already spot the similarities).
Creating Overlays
Creating an overlay is a very simple process. However, before we do that let's add a simple OgreHead to the scene to give us some orientation:
protected override void CreateScene() { mSceneMgr.AmbientLight = ColourValue.White; var ogreHeadEntity = mSceneMgr.CreateEntity("OgreHead", "ogrehead.mesh"); var ogreHeadNode = mSceneMgr.RootSceneNode.CreateChildSceneNode("OgreHead"); ogreHeadNode.AttachObject(ogreHeadEntity); }
The first thing we must do is create our overlay object. Add the following code to your CreateScene method:
var overlay = OverlayManager.Singleton.Create("TestOverlay");
If you compile and run this code now you will notice that nothing has changed. An overlay is simply a container. To actually see something we must populate it with elements.
Note that we have given our overlay a name: "TestOverlay". You may have guessed that this name is similar to the node and entity names we used in previous tutorials, i.e this is a simple descriptive string that we can use to later retrieve the same object without having to keep a reference to it. We will use this trait later.
Creating Overlay Elements
So let's add a simple overlay element to our overlay. Add the following code to your CreateScene method:
// Create a panel. var panel = (PanelOverlayElement)OverlayManager.Singleton.CreateOverlayElement("Panel", "PanelElement"); // Set panel properties. panel.MaterialName = "Core/StatsBlockCenter"; panel.MetricsMode = GuiMetricsMode.GMM_PIXELS; panel.Top = 200; panel.Left = 300; panel.Width = 250; panel.Height = 150; // Add the panel to the overlay. overlay.Add2D(panel); // Make the overlay visible. overlay.Show();
Compile and run this code. You should see a simple green rectangle appearing in the middle of your render window. However the green rectangle is displayed "on top" of the scene and is always there. You can move around and rotate the camera and the rectangle will always stay where it is. It's that simple.
So what actually happened here, let's analyze the code. In the first line we created a Panel Element. A panel element is a simple rectangle element that draws one material texture. However the panel element has one other impotant characteristic: it is considered a "Container" element. Container elements are overlay elements that can also contain other overlay elements. This distinction is important because only container elements can be attached to the overlay itself.
This is the reason we had to cast the return value of CreateOverlayElement to PanelOverlayElement which we knew we are going to get back. Normally CreateOverlayElement returns a reference to OverlayElement, which is the base class of all overlay elements, both containers and non-containers. Container elements inherit from OverlayContainer, which inherits from OverlayElement. Confused? Hopefully the following class diagram will help clarify matters:
The polymorphic CreateOverlayElement method returns a reference to the base class, so if we know exactly what we are going to get, and we need a subclass reference, we have to use a cast.
In the following lines of code we set the basic properties of our new panel so that Ogre knows how to render it:
panel.MaterialName = "Core/StatsBlockCenter"; panel.MetricsMode = GuiMetricsMode.GMM_PIXELS; panel.Top = 200; panel.Left = 300; panel.Width = 250; panel.Height = 150;
- The material name refers to any previously loaded material name, and the texture defined in this material will be displayed as the background of the panel. The material we are using here is the same one used in the framerate box, hence its name.
- The metrics mode property tells Ogre how we intend to specify the size and location of the element. We chose pixels here.
- The top, left, width and height properties are simple pixel coordinates telling the top left location and the desired dimensions of the element. This is very similar to other windowing systems or the HTML DOM.
Now that we have a panel object fully set, all there is left to do is attach it to the overlay, and then make the overlay visible. Overlays are set to be hidden when they are created. This is intentional - we are responsible for displaying them when they are ready, rather than having them appear prematurely before we are ready to show them. Again, this is similar to windowing APIs.
// Add the panel to the overlay. overlay.Add2D(panel); // Make the overlay visible. overlay.Show();
Note that the Add2D method only accepts a reference to an OverlayContainer object, which relates to what we explained before about container elements and the need to cast. The method accepts our PanelOverlayElement reference because it is a subclass of OverlayContainer.
Your CreateScene method should now look something like this:
protected override void CreateScene() { mSceneMgr.AmbientLight = ColourValue.White; var ogreHeadEntity = mSceneMgr.CreateEntity("OgreHead", "ogrehead.mesh"); var ogreHeadNode = mSceneMgr.RootSceneNode.CreateChildSceneNode("OgreHead"); ogreHeadNode.AttachObject(ogreHeadEntity); var overlay = OverlayManager.Singleton.Create("TestOverlay"); // Create a panel. var panel = (PanelOverlayElement)OverlayManager.Singleton.CreateOverlayElement("Panel", "PanelElement"); // Set panel properties. panel.MaterialName = "Core/StatsBlockCenter"; panel.MetricsMode = GuiMetricsMode.GMM_PIXELS; panel.Top = 200; panel.Left = 300; panel.Width = 250; panel.Height = 150; // Add the panel to the overlay. overlay.Add2D(panel); // Make the overlay visible. overlay.Show(); }
Note: The transparency in this example is a property of the material, not the overlay or the overlay element. In fact the Ogre/Mogre overlay API doesn't not allow us to tinker with the transparency of overlays or overlay elements unfortunately.
Element Nesting
So if overlay elements can contain other elements, what good is that for us? Well it allows us to build clusters of GUI components and then do things with them as a group. In fact they are very similar to scene nodes in this sense. They are also very similar to controls and windows in a windowing API as we've mentioned before. This is simply a pattern that works well, and has been employed here as well.
Let's add another overlay element inside the one we already have. Add the following code to your CreateScene method:
var childPanel = (PanelOverlayElement)OverlayManager.Singleton.CreateOverlayElement("Panel", "ChildPanelElement"); childPanel.MaterialName = "Core/StatsBlockCenter"; childPanel.MetricsMode = GuiMetricsMode.GMM_PIXELS; childPanel.Top = 50; childPanel.Left = 50; childPanel.Width = 150; childPanel.Height = 50; panel.AddChild(childPanel);
If you compile and run this code, you will notice another smaller rectangle inside the previous one. But this still doesn't give us any big advantage. However, if we add the following line:
panel.Left += 200;
And compile and run again the whole panel with the smaller panel inside it now appear more to the right of the render window. This becomes useful when you consider that real overlay GUIs are made of multiple groups of elements. Look at the framerate panel at the bottom left of the render window - it is made of roughly 10 overlay elements.
Overlay Scripts
Although the code above is relatively short and simple, there is an easier way to define the layout and elements of an overlay - overlay scripts. Overlay scripts are simple declarative syntax scripts that Ogre can use to create and set overlays and overlay elements for us. The following overlay script is the equivalent of the code we've written above:
TestScriptOverlay { container Panel(TestScriptOverlay/ScriptPanelElement) { material Core/StatsBlockCenter metrics_mode pixels top 200 left 300 width 250 height 150 element Panel(TestScriptOverlay/ScriptChildPanelElement) { material Core/StatsBlockCenter metrics_mode pixels top 50 left 50 width 150 height 50 } } }
Save this script in a file called "test.overlay" under your Media folder (or any other filename with extension ".overlay" for that matter) Ogre will then parse and load it. Now remove or comment out the code from your CreateScene method that sets up the overlay, and replace it with the following code:
var overlay = OverlayManager.Singleton.GetByName("TestScriptOverlay"); overlay.Show();
Compile and run the application. It should continue running and appear exactly the same. However the overlay script is simpler than the verbose code we used before.
let's analyze the syntax of the overlay script.
[Overlay Name] { [Property 1] [Property Value] [Property 1] [Property Value] ... container [Element Type]([Element Name]) { [Property 1] [Property Value] [Property 1] [Property Value] ... container|element [Element Type]([Element Name]) { [Property 1] [Property Value] [Property 1] [Property Value] ... } ... } ... }
An overlay script starts with an overlay name followed by curly brackets. An overlay script may in fact hold multiple overlays if you wish - each one is distinguished by a different name. The body of the overlay definition is simply any number of property settings or child container element definitions you wish to have. Overlay element definitions start with either the keyword "element" or "container", followed by the element type, followed by its name in brackets and finally followed by the element definition inside curly brackets. The element definition is made of multiple property / value pairs, and optionally more child elements.
It's a very simple declarative, curly-braces based syntax. However there are a couple of small things to look out for:
- Container elements can be declared both as "element" or "container", since they are both. However, if they appear as root elements, or if they contain children they must be declared using the "container" keyword.
- Elements and overlays must have unique names. If you notice in the above example we chose very different names between the code example and the script example to avoid any potential conflicts. If you try to create two overlays or two overlay elements with the same name, you will get an exception. Unfortunately, as of this writing, that exception is not very descriptive and does not appear in the Ogre.log, so it can get hard to trace. For this reason we use a "namespace" notation in our overlay script where we prefix the name of every element with the names of the elements that contain it. This eliminates any potential ambiguity.
Overlays As GUIs
So now that we've learned how to create overlays, let's do something more "useful" with them. In the following example we will use what we learned in previous tutorials and make a popup appear whenever we press the enter key.
Delete the code you've written so far or start fresh with a new copy of the tutorial framework and add the Ogre head. Make sure that the overlay script you've added is removed or otherwise won't interfere with your work either.
protected override void CreateScene() { mSceneMgr.AmbientLight = ColourValue.White; var ogreHeadEntity = mSceneMgr.CreateEntity("OgreHead", "ogrehead.mesh"); var ogreHeadNode = mSceneMgr.RootSceneNode.CreateChildSceneNode("OgreHead"); ogreHeadNode.AttachObject(ogreHeadEntity); }
Now we'll add a keyboard handler as we've seen in previous tutorials:
protected override void InitializeInput() { base.InitializeInput(); mKeyboard.KeyPressed += new MOIS.KeyListener.KeyPressedHandler(KeyPressedHandler); }
But before we move on to writing our handler, let's prepare our overlay using an overlay script. Create an overlay script file in your Media folder with the following contents:
HelloWorldOverlay { container BorderPanel(HelloWorldOverlay/MessageBox) { metrics_mode pixels width 250 height 150 material Core/StatsBlockCenter border_material Core/StatsBlockBorder border_size 1 1 1 1 element TextArea(HelloWorldOverlay/MessageBox/Body) { metrics_mode pixels left 125 top 75 font_name BlueHighway char_height 16 alignment center colour 0.5 0.7 0.5 } } }
This overlay script introduces two new element types:
- BorderPanel: an overlay element similar to the panel you've seen expect it has a border (which should also be part of the material's image).
- TextArea: an overlay element that contains text.
Basically we've defined a simple message box window in this script - a simple panel with a border like a GUI window with some text in the middle. However, there are a couple of things missing here still:
- The message box has no top and left properties defined in the script, which means that if we show the overlay, the box will appear in the top left corner of the render window.
- The text area element has no text defined for it. We could do it in the script using he "caption" property, but we will do it in code instead.
So now let's add the missing key press handler and "pop up" our message box when the user presses the enter button:
protected bool KeyPressedHandler(MOIS.KeyEvent arg) { if (arg.key == MOIS.KeyCode.KC_RETURN) { var messageBox = OverlayManager.Singleton.GetOverlayElement("HelloWorldOverlay/MessageBox"); messageBox.Left = (mWindow.Width - messageBox.Width) / 2; messageBox.Top = (mWindow.Height - messageBox.Height) / 2; var messageBody = OverlayManager.Singleton.GetOverlayElement("HelloWorldOverlay/MessageBox/Body"); messageBody.Caption = "Hello World!"; OverlayManager.Singleton.GetByName("HelloWorldOverlay").Show(); } return true; }
What we are doing here is very simple. When the user presses the enter button, we:
- Acquire the message box border panel element and position it in the middle of the render window.
- Acquire the message box text area element and set its caption property to the message that we would like to display.
- Acquire the overlay and display it by calling it's Show method.
Compile and run this code. If you press the enter button, you should see the message popup in the middle of the render window with the message "Hello World". See how simple it is?
Z Order
We've mentioned previously that many overlays can be loaded and displayed at the same time. This immediately triggers the question of which overlays should appear above others and which should appear "below" or "behind" other overlays. This is where Z-Order comes in.
Again, you might be familiar with this concept from other areas as it is also very common. The idea is very simple: each overlay has a property called "Z Order" Which is a simple numeric value (in Ogre this value limited to the range of 0 to 650). The higher the number, the "closer" the overlay is to the camera. This does not affect the overlay's size or shape, it simply means that an overlay with a Z Order of 500 will appear above an overlay with a Z Order of 400, if they happen to appear in the same part of the render window.
Let's demonstrate this with a simple example. A common practice in first person shooter games is to flash the screen red when the player character is hit. We will create this effect in our program when the user presses the spacebar button using an overlay.
Add the following overlay definition to your project (you can either add it to a new .overlay file in your Media folder, or to an existing one).
Red { zorder 650 container Panel(Red/Panel) { width 1 height 1 material Red } }
This very simple overlay simply covers the whole render window in a red transparent color using the "Red" material which is included in the tutorial framework's Media folder. Note that we've given it a Z order value of 650 - the highest possible Z order value.
Another thing that this script demonstrates is how to use the "relative" metrics mode, which is also the default mode (which is why we have omitted the property altogether). In "relative" mode coordinates are measured in a relative float value between 0 and 1. This value is in turn translated to pixels relative to the current resolution. The result is an overlay element that maintains the same visual size in the render window in all resolutions. Therefore values of 1,1 for width and height mean the full size of the render window.
All we have left to do now is to show this overlay for a short period of time when the user presses the space button. Let's add a simple timer member variable to our tutorial class as we've seen before:
float mHitTimer = 0;
And now let's add some code to our input handler that displays the red overlay if the key is the space key. Add the following code to your KeyPressedHandler method (before the return statement, of course):
if (arg.key == MOIS.KeyCode.KC_SPACE) { OverlayManager.Singleton.GetByName("Red").Show(); mHitTimer = 1; }
We want the red flash to appear briefly for one second, so we need a frame listener that will implement this. As you've seen in previous tutorials, frame logic can be easily defined in the tutorials framework in an overridden UpdateScene method:
protected override void UpdateScene(FrameEvent evt) { if (mHitTimer > 0) { mHitTimer -= evt.timeSinceLastFrame; if (mHitTimer <= 0) OverlayManager.Singleton.GetByName("Red").Hide(); } }
And that's it. Compile and run this code. If you hit the spacebar button, you should see a brief red flash all over the render window:
Now press the enter key to show the message box and hit the spacebar button again:
The red overlay which has the highest possible Z order value, higher than the default which the message box overlay would have, appears above the message box. Therefore the red flash covers the message box as well.
If instead we gave the message box overlay the higher value, what would happen? Modify the overlay script(s) so that the red overlay has a lower value than the message box:
Red { zorder 600 // lower this value from 650 to 600. container Panel(Red/Panel) { width 1 height 1 material Red } }
HelloWorldOverlay { zorder 650 // Add this line container BorderPanel(HelloWorldOverlay/MessageBox) { ...
If you run the code again, this time the message box should appear "above" the red flash. It is as if the Game's GUI is detached from the character's vision, which in turn is covered in red haze by the pain:
Overlay Templates
In all examples so far we created the overlays we needed and used them. However, at some point you will start seeing certain patterns repeat themselves in your overlay definitions. These patterns could represent a "theme" which you may want to swap, or simply represent basic common settings.
One simple option is to repeat these property assignments everywhere. Another is to create methods that generate overlay elements based on some parameters. But there is actually another powerful tool in Ogre/Mogre's overlay system to accomplish this: overlay templates.
Overlay templates, as the name implies, are not real overlays but rather they are templates that can be used to create other elements from, but are not rendered themselves. It is possible to inherit new overlay elements from previously defined templates.
Let's look at an example. Start again from a fresh copy of the code or delete your current changes:
protected override void InitializeInput() { base.InitializeInput(); mKeyboard.KeyPressed += new MOIS.KeyListener.KeyPressedHandler(KeyPressedHandler); } protected override void CreateScene() { mSceneMgr.AmbientLight = ColourValue.White; var ogreHeadEntity = mSceneMgr.CreateEntity("OgreHead", "ogrehead.mesh"); var ogreHeadNode = mSceneMgr.RootSceneNode.CreateChildSceneNode("OgreHead"); ogreHeadNode.AttachObject(ogreHeadEntity); } bool KeyPressedHandler(MOIS.KeyEvent arg) { return true; }
In this example we will create multiple message boxes. Each time the user presses the enter button, we will create a message box with a random number for its contents, in a random location on the render window.
First we need a simple overlay to place all these message boxes on. Add the following code to your CreateScene method:
var overlay = OverlayManager.Singleton.Create("TestOverlay"); overlay.Show();
Next we will create a simple template of a panel which contains a text area, similar to our previous message boxes except without a border. In code, templates are created with the same CreateOverlayElement method of the OverlayManager Singleton, except the last argument we pass is set to true, which tells the method that it is a template and not an overlay element. Later we will see how to define templates in overlay scripts. For now add the following code to your CreateScene method:
var msgBoxTpl = (PanelOverlayElement)OverlayManager.Singleton.CreateOverlayElement("Panel", "Templates/MessageBox", true); msgBoxTpl.MaterialName = "Core/StatsBlockCenter"; msgBoxTpl.MetricsMode = GuiMetricsMode.GMM_PIXELS; msgBoxTpl.Width = 250; msgBoxTpl.Height = 150; var text = (TextAreaOverlayElement)OverlayManager.Singleton.CreateOverlayElement("TextArea", "Templates/MessageBox/Body", true); text.MetricsMode = GuiMetricsMode.GMM_PIXELS; text.Left = 125; text.Top = 75; text.FontName = "BlueHighway"; text.CharHeight = 16; text.SetAlignment(TextAreaOverlayElement.Alignment.Center); text.Colour = new ColourValue(0.5f, 0.7f, 0.5f); msgBoxTpl.AddChild(text);
This code creates a template of a panel element called "Templates/MessageBox", sets its properties, and adds a text area called "Templates/MessageBox/Body" in the middle of it.
We can now define our input handler. If the pressed key is enter, we will generate a random message (a number) and place a new message box in a random location on the screen. Add the following code to your KeyPressedHandler method before the return statement:
if (arg.key == MOIS.KeyCode.KC_RETURN) { var rand = new System.Random(); var id = rand.Next(); var newbox = (BorderPanelOverlayElement)OverlayManager.Singleton.CreateOverlayElementFromTemplate("Templates/MessageBox", "BorderPanel", "MessageBox" + id); newbox.Left = rand.Next((int)mWindow.Width - 250); newbox.Top = rand.Next((int)mWindow.Height - 150); newbox.GetChild("MessageBox" + id + "/Templates/MessageBox/Body").Caption = id.ToString(); OverlayManager.Singleton.GetByName("TestOverlay").Add2D(newbox); }
What this code does is very simple: First, it generates a random ID for the new message box. Then it creates a new overlay element based on the template we defined previously by using the CreateOverlayElementFromTemplate method. It then sets the left and top values of the newly created element randomly, and sets the caption of the text area to the generated ID. Finally it attaches the new element to the overlay we created before so that it is displayed.
Note that CreateOverlayElementFromTemplate requries us to supply a new unique name for the new element that will be created. We simply used the string "MessageBox" followed by the random ID that we have generated. However, note what happens next - every new child element copied from the template gets this same name prefixed to its name. So the text area that is named "Templates/MessageBox/Body" in the template, is named "MessageBoxrandom id/Templates/MessageBox/Body". Ogre's uses the same slash-based notation internally to prefix names of elements created this way, so the notation we used isn't completely arbitrary.
That's all there is to creating overlay templates, as you can see it's pretty simple in essence. As we mentioned previous it is possible to also define templates in overlay scripts, which is preferable of course if we use overlay scripts for our overlays anyway. Here is the overlay script equivalent of the above:
template container Panel(Templates/MessageBox) { metrics_mode pixels width 250 height 150 material Core/StatsBlockCenter element TextArea(Templates/MessageBox/Body) { metrics_mode pixels left 125 top 75 font_name BlueHighway char_height 16 alignment center colour 0.5 0.7 0.5 } }
The differences are very simple:
- The "template" keyword precedes the "container" or "element" keywords.
- Overlay templates don't need to be enclosed in an overlay, they can be defined at the top level of the overlay script.
In addition to using templates from code, we can also use them in the overlay scripts themselves. They can be used as "base classes". Here is an example:
template container BorderPanel(BaseWindow) { metrics_mode pixels width 250 height 150 material Core/StatsBlockCenter border_material Core/StatsBlockBorder border_size 1 1 1 1 } HelloWorldOverlay { container BorderPanel(HelloWorldOverlay/MessageBox) : BaseWindow { element TextArea(HelloWorldOverlay/MessageBox/Body) { metrics_mode pixels left 125 top 75 font_name BlueHighway char_height 16 alignment center colour 0.5 0.7 0.5 } } }
At the top of this overlay script we define a template called "BaseWindow", which as the name implies defines the base properties common to any window we may want to use. Then we create our familiar messagebox. We inherit the BaseWindow properties by using the ":" operator, and then move on to define the unique characteristics of he message box, including additional sub-elements.
Conclusion
In this tutorial we have covered almost all aspects of using overlays in Mogre/Ogre. At this point you should be able to add HUDs to your applications using overlays, and even produce a basic GUI if you wish. However, for a real in-game GUI you should look into dedicated libraries like Myiagi or MyGUI.