Creating an interface

From Serious Sam's Bogus Detour
Jump to: navigation, search

Interfaces is what makes the HUD, Netricsa, etc. This means an interface can either be shown passively (without user input) or actively (with user input).

The gui file

Let's start by creating a gui file. Let's call it hello.gui and we'll put it inside of our scenario in a gui folder. We will discuss its contents later.

<gui>
	<doc>
		<group>
			<text anchor="0.5 0.5" font="gui/fonts/font_agency16.fnt" text="Hello, world!" />
		</group>
	</doc>
</gui>

Drawing our interface

To draw our interface, we need to add it to a place that draws stuff. A prime example of this is the RenderWidgets function of a gamemode. But first, we need to instantiate a Widget Hoster which will do most of the hard work of interfaces for us. Here's how we would load the hello.gui file inside of a gamemode:

	IWidgetHoster@ m_interface;

	BasicVersus(Scene@ scene)
	{
		super(scene);

		// Create a widget hoster
		@m_interface = IWidgetHoster();

		// Load hello.gui into it
		m_interface.LoadWidget(m_guiBuilder, "gui/hello.gui");
	}

In our gamemode constructor, we instantiate a basic IWidgetHoster, put it in m_interface and tell it to load our gui file.

Now, to actually draw our interface to the screen, we override the RenderWidgets function:

	void RenderWidgets(PlayerRecord@ player, int idt, SpriteBatch& sb) override
	{
		// Draw our interface
		m_interface.Draw(sb, idt);

		// Remember to call the base function from the inherited gamemode!
		VersusGameMode::RenderWidgets(player, idt, sb);
	}

And, additionally, to make any sprite animations work, we also update the widget hoster inside of UpdateWidgets:

	void UpdateWidgets(int ms, GameInput& gameInput, MenuInput& menuInput) override
	{
		// Update our interface
		m_interface.Update(ms);

		// Remember to call the base function from the inherited gamemode!
		VersusGameMode::UpdateWidgets(ms, gameInput, menuInput);
	}

The result should look like this:

GuiExample1.png

More about the gui file

Gui files are formatted as basic XML. Here, the gui tag is our root element, and is required for all interfaces. Inside of this, we have the doc tag, which is also required. Inside of that, we have our first widget, which will be the root widget. In this case, we use the GroupWidget by using the group tag. A group widget does not draw anything to the screen. Instead, it is simply a container. It has the special property that it will automatically take the width and height of its parent widget, or if there is no parent widget, the entire screen. This makes it perfect as a container for all other widgets.

Inside of that, we have our first real widget, a TextWidget, which we indicate by using the text tag. This widget is inside of the group tag, which means that this text widget is a child of the group widget, and the group widget is its parent. Also note that we close the text tag immediately instead of opening it, since we don't need to have any child widgets inside of it.

The font and text attributes speak for themselves, however the anchor attribute in the above example is more interesting. This is a vector attribute, meaning it accepts 2 floating point numbers. The first is X, the second is Y. These values defines the scalar position of the widget within its parent's size (based on its own size). For example, if you set the anchor X to 0, the widget will be left-aligned. (0 is the default for both X and Y if you don't define the anchor.) If you se this value to 0.5, it will be aligned in the center of its parent. And if you set it to 1, it will be aligned on the right side. The same counts for Y, but then for the Y axis, where 0 would be the top and 1 would be the bottom. Thus, in our example above, 0.5 0.5 means to put the widget exactly in the center.

Sprites

Any sprites you want to display in your interface need to go into a sprites tag. You can then display them with the sprite widget. Sprites are defined globally in your .gui file, outside of the doc tag, like this:

<gui>
	<sprites>
		<sprite name="globe" texture="gui/icons.png">
			<frame>26 44 13 13</frame>
		</sprite>
	</sprites>

	<doc>
		<group>
			<sprite src="globe" anchor="0.5 0.5" offset="0 -16" />
			<text anchor="0.5 0.5" font="gui/fonts/font_agency16.fnt" text="Hello, world!" />
		</group>
	</doc>
</gui>

Here, we take the "globe" sprite and put it in a sprite widget just above the text we created in the previous section. The globe sprite's frame is inside of the gui/icons.png texture, where it is located at X 26 and Y 44. It also has a width and height of 13. This example looks like this:

GuiExample2.png

We can also define animating sprites, by adding more frames and an additional time:

		<sprite name="block" texture="gui/icons.png">
			<frame time="250">0 57 10 10</frame>
			<frame time="250">10 57 10 10</frame>
			<frame time="250">20 57 10 10</frame>
			<frame time="250">30 57 10 10</frame>
			<frame time="250">40 57 10 10</frame>
			<frame time="250">30 57 10 10</frame>
		</sprite>

Adding interaction

By default, interface interaction is disabled. You have to do the following to be able to interact with an interface:

1. Place an interactable widget, such as a button. 2. Make sure you are calling Update on the widget hoster and that you have added it to the list of widget roots. 3. Additionally, inherit from IWidgetHoster so you can catch events in OnFunc.

Let's go through this step by step.

Placing a button

To place a button, the most convenient way is using the scalebutton widget. An example would be this:

<scalebutton spriteset="scalebutton" anchor="0.5 0.5" offset="0 30" width="90" font="gui/fonts/font_msref10_bold.fnt" text="Click me!" />

Note the spriteset here is set to scalebutton. This means it will load several sprites prefixed by the string scalebutton. A full list of this is:

  • scalebutton-left
  • scalebutton-mid
  • scalebutton-right
  • scalebutton-hover-left
  • scalebutton-hover-mid
  • scalebutton-hover-right
  • scalebutton-down-left
  • scalebutton-down-mid
  • scalebutton-down-right

Fortunately, you don't have to define these sprites yourself by hand. You can use the default %include to automatically add all of these (including a disabled variation) to your .gui file. To use this, add the following line inside of your sprites tag:

%include "gui/main_menu/scalablebutton_sprites.inc"

If you save this and start the game now, you should see a nice blue button like this:

GuiExample3.png

Widget roots

You'll notice that the button can't actually be clicked or hovered over. That's because the widget hoster is not configured to receive any user input, it's only being drawn to the screen. We can solve this by calling the gamemode's AddWidgetRoot function somewhere. For example, directly in our Start function:

	void Start(uint8 peer, SValue@ save, StartMode sMode) override
	{
		// Handle all the main starting logic here
		VersusGameMode::Start(peer, save, sMode);

		// Add our interface as a widget root
		AddWidgetRoot(m_interface);
	}

If you start your level now, you will be able to hover and click the button, but you will also be shooting your weapon and you can still walk around. To work around this, you can override ShouldFreezeControls and return true when your interface has input focus. You'll have to work with some boolean, and do something like:

	bool m_isInterfaceVisible;

	bool ShouldFreezeControls() override
	{
		return m_isInterfaceVisible
		    || VersusGameMode::ShouldFreezeControls();
	}

In the next step we will put this boolean inside of its own class to clean up a bit.

Inheriting from IWidgetHoster

For the sake of simplicity, this guide started by having you instantiate a IWidgetHoster directly. This is however not best practice in most cases, plus doing that won't let you catch events from user interaction on the interface. It also doesn't encapsulate everything very well, see for example that ugly loose m_isInterfaceVisible boolean inside of our gamemode, which is less than ideal.

The way interfaces are generally built in scripts, is by inheriting IWidgetHoster and putting all interface logic inside of your class. The following example encapsulates our gui/hello.gui logic inside of a new HelloGui class that is in itself a widget hoster. For this example I've also added a few helpful things to this compared to the basic example above:

  • Loading the .gui (with filename) directly in the constructor of our class.
  • Convenient Show and Hide functions which will help us to easily toggle the visibility and interactability of the interface.
  • A visibility boolean that we use to stop rendering and updating of the interface when hidden.
class HelloGui : IWidgetHoster
{
	// If visible this is true, otherwise false
	bool m_visible;

	HelloGui(GUIBuilder@ b)
	{
		// Load the widget directly from hello.gui
		LoadWidget(b, "gui/hello.gui");
	}

	void Show()
	{
		// Make visible, and add to widget roots
		m_visible = true;
		g_gameMode.AddWidgetRoot(this);
	}

	void Hide()
	{
		// Make invisible, and remove from widget roots
		m_visible = false;
		g_gameMode.RemoveWidgetRoot(this);
	}

	void Update(int dt) override
	{
		// If not visible, don't bother updating
		if (!m_visible)
			return;
		IWidgetHoster::Update(dt);
	}

	void Draw(SpriteBatch& sb, int idt) override
	{
		// If not visible, don't bother drawing
		if (!m_visible)
			return;
		IWidgetHoster::Draw(sb, idt);
	}
}

Catching events

Events in interfaces are simple strings sent to the OnFunc function of a widget hoster. To get an event when the user clicks the "Click me" button we made in the example above, we will need to add the func attribute to the widget, like: func="click_me_was_clicked"

<scalebutton spriteset="scalebutton" anchor="0.5 0.5" offset="0 30" width="90" font="gui/fonts/font_msref10_bold.fnt" text="Click me!" func="click_me_was_clicked" />

Then, in your class, override OnFunc and look for the event string:

	void OnFunc(Widget@ sender, string name) override
	{
		// If the button was clicked, "close" the interface
		if (name == "click_me_was_clicked")
			Hide();
	}