Facebook Twitter YouTube Frictional Games | Forum | Privacy Policy | Dev Blog | Dev Wiki | Support | Gametee


Thread Rating:
  • 3 Vote(s) - 3.67 Average
  • 1
  • 2
  • 3
  • 4
  • 5
GUI Tutorial Series
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#11
RE: GUI Tutorial Series

Get to The Back Of The Line

Now that we've got those pesky basics out of the way, we can get into some of the really fun stuff. From here on out, we'll be putting together some of the barebones terminals that in their essence could be published in their own right (though I imagine you'd probably want to pretty them up first).

Table of Contents
The Basics
Getting More Advanced
Using SOMA's Built-In GUI Styles
  • StationGui (WIP)
  • UrbanGui (WIP)
  • Playing Audio (WIP)

Going Beyond Terminals
  • Setting Up a User Module (WIP)
  • Basic Heads-Up Display (WIP)
  • Target Info Module (WIP)
  • Player HUD Menu System (WIP)

Tutorial Requirements

For this tutorial, you will need the following:
  • A map with a prepared terminal, plus all the necessities.
  • A script file with a prepared OnGui terminal callback function.

Tutorial Source Files

Initial File
Completed File

The Tutorial Itself

So far, when we create our TextPanel or Button, the background has been a solid color. While this is perfectly fine for prototyping levels and explaining concepts, it's a bit lack-luster for a published game. What we want is to shape and format our widgets so they aren't just functional - they look good.

Let's start from where we left off, and put together a simple Button and TextPanel in our OnGui function. (For the sake of this tutorial, we'll ignore the Button's interactivity.)

cImGuiTextFrameData textFrameData;
textFrameData.mFont.mvSize = cVector2f(45, 45);
textFrameData.mColorBase = cColor(0, 0, 0, 0);

ImGui_DoTextFrameExt(
    "In this tutorial, we shall learn how to create awesome backgrounds, so that your terminal isn't just a solid white on black, or boring rectangles of color. The amount of awesome in your terminal is only limited to how much you can give it.",
    0,
    10,
    0,
    textFrameData,
    cVector3f(80, 290, 0),
    cVector2f(880, 660));

cImGuiButtonData buttonData;
buttonData.mFont.mvSize = cVector2f(60, 60);
buttonData.mColorBase = cColor(0.35, 0.35, 0.35);
buttonData.mFont.SetFile("sansation_large_bold.fnt");

bool bButtonState = ImGui_DoButtonExt(
    "gui_button",
    "I Don't Do Anything",
    buttonData,
    cVector3f(220, 90, 0),
    cVector2f(600, 95));

Nothing terribly exciting yet, and running this map through the DevBat will confirm that.

[Image: n5D0vT8.jpg]

Given some bells and whistles, this terminal would be perfectly functional. However, it's not terribly impressing to look at right now, so let's change that up.

Inside the widget data classes is a field called mGfxBackground. This field takes a value of the type cImGuiGfx, which you may remember from the Image tutorial. When given an image, the widget will automatically place the image in the background of your widget, handling all the positioning and scaling itself.

In our case, let's replace the boring solid color backgrounds with some graphics that really make our widgets pop. Change up the OnGui code to look like the following:

cImGuiTextFrameData textFrameData;
textFrameData.mFont.mvSize = cVector2f(45, 45);
textFrameData.mbUseBackgroundGfx = true;
textFrameData.mGfxBackground = cImGuiGfx("graphics/imgui/station/filetreeicons/file_icon.tga");

ImGui_DoTextFrameExt(
    "In this tutorial, we shall learn how to create awesome backgrounds, so that your terminal isn't just a solid white on black, or boring rectangles of color. The amount of awesome in your terminal is only limited to how much you can give it.",
    120,
    10,
    0,
    textFrameData,
    cVector3f(40, 200, 0),
    cVector2f(950, 510));

cImGuiButtonData buttonData;
buttonData.mFont.mvSize = cVector2f(60, 60);
buttonData.mbUseBackgroundGfx = true;
buttonData.mGfxBackground = cImGuiGfx("graphics/imgui/station/filetreeicons/file_icon.tga");
buttonData.mFont.SetFile("sansation_large_bold.fnt");

bool bButtonState = ImGui_DoButtonExt(
    "gui_button",
    "I Don't Do Anything",
    buttonData,
    cVector3f(220, 90, 0),
    cVector2f(600, 95));

The changes may be a bit discrete, so let's step through them.

First, we delete the lines of code that sets the mColorBase data for both widgets. Then we replace them with code that sets the widget's mGfxBackground to a particular image. We also want to set the mbUseBackgroundGfx field for both widgets to true, so we don't risk the widgets ignoring the background images we just gave to it.

(Another sneaky change that you may not have noticed is that I changed the second parameter in the call to ImGui_DoTextFrameExt from 0 to 120, which you might recall as being the padding parameter. The reason for this is that when we are just using text, we don't really want any padding, but using no padding when our TextFrame has a background image set makes the text hug the sides of the image, which doesn't look very good. Also, the background image we are using here has distinct borders, so we want to make sure our text stays within those borders.)

One more thing we need to do (that you might remember from the Image tutorial) is that we need to pre-load our images when we load the map. If you forget to add this step, you run the risk of your terminal taking up a lot of resources in trying to load the image on the fly, or the image not showing up at all. Don't forget this step!

void OnEnter()
{
    ImGui_PreloadImage("graphics/imgui/station/filetreeicons/file_icon.tga");
    ImGui_PreloadImage("graphics/imgui/station/thirdparty/pda/pda_background.tga");
}

Alright, now that we've got our images set, let's see how it looks in the DevBat.

[Image: NPbNl5d.jpg]

...Okay, it's not the prettiest solution in the world, but it's the concept that matters, right?

As it is right now, setting the background graphics could let us go a long way. The problem, however, is that the image being used will be scaled to whatever size the widget is, and a lot of times that scaling makes it look weird or downright ugly. You can see this in how we used the same picture for both our Button and our TextFrame, but the differences between them make them look bizarre.

With the Button, there's another way. The ImGui does background images, but another thing it supports is the use of background frames, which are a series of images that the ImGui uses to build a specialized background that will look great no matter what size you give it.

We're going to have to update our buttonData to make use of background frames. (Hold onto something. This next batch of code might be a tad difficult to digest.)

cImGuiButtonData buttonData;
buttonData.mFont.mvSize = cVector2f(60, 60);
buttonData.mFont.SetFile("sansation_large_bold.fnt");

buttonData.mbUseFrame = true;

buttonData.mGfxFrame.mGfxBorderLeft = cImGuiGfx("graphics/imgui/station/button/big_border_left.tga");
buttonData.mGfxFrame.mGfxBorderTop = cImGuiGfx("graphics/imgui/station/button/big_border_top.tga");
buttonData.mGfxFrame.mGfxBorderRight = cImGuiGfx("graphics/imgui/station/button/big_border_right.tga");
buttonData.mGfxFrame.mGfxBorderBottom = cImGuiGfx("graphics/imgui/station/button/big_border_bottom.tga");

buttonData.mGfxFrame.mGfxCornerTopLeft = cImGuiGfx("graphics/imgui/station/button/big_corner_topleft.tga");
buttonData.mGfxFrame.mGfxCornerTopRight = cImGuiGfx("graphics/imgui/station/button/big_corner_topright.tga");
buttonData.mGfxFrame.mGfxCornerBottomLeft = cImGuiGfx("graphics/imgui/station/button/big_corner_bottomleft.tga");
buttonData.mGfxFrame.mGfxCornerBottomRight = cImGuiGfx("graphics/imgui/station/button/big_corner_bottomright.tga");

buttonData.mColorText = cColor(0, 0, 0);

As you can see, we've done away with our mGfxBackground altogether, and instead we are using a frame. When we create our frame, what we are really doing is we are specifying eight different images. Four of these images represent the four sides, and the other four will be the corners.

(Also, for this particular setup of frame images, the result makes the background basically white, so we need to turn our text black in order to be able to read it.)

Of course, we are using eight more images, so we need to make sure those images are pre-loaded.

void OnEnter()
{
    ...
    ImGui_PreloadImage("graphics/imgui/station/button/big_border_left.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_border_top.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_border_right.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_border_bottom.tga");
    
    ImGui_PreloadImage("graphics/imgui/station/button/big_corner_topleft.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_corner_topright.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_corner_bottomleft.tga");
    ImGui_PreloadImage("graphics/imgui/station/button/big_corner_bottomright.tga");
}

Alright, let's send this new code through the DevBat.

[img]http://i.imgur.com/e90Z699.jpg[/img]

That Button looks a lot more crisp now, doesn't it?

(Ironically, the TextFrame widget doesn't support frame graphics. You're stuck with using either a general background image or making the background transparent and handling the frame another way.)

We've got our Button and TextFrame all gussied up, but the rest of the terminal looks pretty bland with its solid black, doesn't it? Let's drop in an Image to make it look interesting. Add the following code after the Button logic:

[code]ImGui_DoImage(cImGuiGfx("graphics/imgui/station/thirdparty/pda/pda_background.tga"), cVector3f(0, 0, -1), cVector2f(1024, 788));

(And of course, the corresponding image pre-loading.)

void OnEnter()
{
    ...
    ImGui_PreloadImage("graphics/imgui/station/thirdparty/pda/pda_background.tga");
}

For the most part, this is the same kind of Image you saw way back before. We just set the origin point at [0, 0] and the size to [1024, 788], so that the Image starts at the very upper-lect corner and covers the entire screen of the terminal.

But one thing you may have been curious about all this time is that confusing cVector3f parameter. We've been using it in every ImGui_DoBlank function call we've ever done, but that third value has always been 0. But here, we set it to -1. What gives, right?

That third value represents the widget's "z-order", which is a value in 3D layout rendering systems that tells the renderer what order it should draw its elements in, from least to greatest. That means that anything with a z-order of 0 will always be drawn behind anything with a z-order of 1, which in turn will be behind anything with a z-order of 2, and so on. If several widgets have the same z-order, then they will generally be drawn in the order which they were created.

Up until now, we've never had overlapping widgets, so the z-order has never mattered much. But in this case, we have an image that we always want drawn behind anything else in the terminal, so we make sure that it's z-order is set to a value less than that of other widgets, which is why it is given a z-order of -1, whereas our other widgets have a z-order of 0;

Alright, now that we've got our Image code in, let's see how things look in the DevBat.

[Image: izEEsWH.jpg]

Our terminal is starting to look pretty snazzy, don't you think? (Albeit in a late '90s website sort of way.)

That more or less wraps up this tutorial. In the next one, we'll take a look into how to give our terminal multiple screens that we can switch between.

Extra Credit

In this tutorial, we removed the field settings for the color base in our widget data... but what happens if you specify a color base and a background image?
(This post was last modified: 09-03-2016, 08:04 PM by Abion47.)
11-10-2015, 05:06 AM
Find
Vale Offline
Member

Posts: 92
Threads: 13
Joined: Mar 2014
Reputation: 7
#12
RE: GUI Tutorial Series

Good tutorials so far. I think you should mention (maybe you did and I missed it?) the ImGui_NrmPos, ImGui_NrmSize, and their group variants, because otherwise users may have difficulty when changing the terminal size, instead of using plain vector input.
11-10-2015, 08:00 AM
Find
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#13
RE: GUI Tutorial Series

(11-10-2015, 08:00 AM)Vale Wrote: Good tutorials so far. I think you should mention (maybe you did and I missed it?) the ImGui_NrmPos, ImGui_NrmSize, and their group variants, because otherwise users may have difficulty when changing the terminal size, instead of using plain vector input.

That topic is going to be covered in the planned article on Grouping and Layout.
(This post was last modified: 11-10-2015, 08:08 AM by Abion47.)
11-10-2015, 08:03 AM
Find
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#14
RE: GUI Tutorial Series

The Well Oiled Machine

What we've done so far can enable some pretty impressive terminals, but there's a snag. Up until now, the terminals we've made have been more or less restricted to a single screen. This can be enough for things like simple interfaces and read-outs, but is limiting when you want to deal with more complex setups. Now it's time to learn how to use a powerful feature of the ImGui system by utilizing it's most common implementation.

Table of Contents
The Basics
Getting More Advanced
Using SOMA's Built-In GUI Styles
  • StationGui (WIP)
  • UrbanGui (WIP)
  • Playing Audio (WIP)

Going Beyond Terminals
  • Setting Up a User Module (WIP)
  • Basic Heads-Up Display (WIP)
  • Target Info Module (WIP)
  • Player HUD Menu System (WIP)

Tutorial Requirements

For this tutorial, you will need the following:
  • A map with a prepared terminal, plus all the necessities.
  • A script file with a prepared OnGui terminal callback function.

Tutorial Source Files

(I'm currently out of state for Thanksgiving. I'll upload the source files when I'm on my main machine again.)

The Tutorial Itself

I assume at this point of the tutorial that you have a pretty good understanding of what variables are within a programming context, but for the sake of this tutorial, here's a brief rundown.

A variable is a virtual construct within a program that allows you to store any value for use later within that program. Through various commands and expressions, this value can be modified, passed around, referenced, or used as a condition. The existence of variables are what allows all but the absolute simplest programs to be possible.

ImGui state variables expand on this principle by allowing you to use variables that are closely linked with the ImGui system itself. Their usage is pretty straightforward, and similar to using a dictionary data structure. The basis of how to interface with state variables works around a getter/setter pair system:

ImGui_SetStateInt("VariableName", lValue);
int lValue = ImGui_GetStateInt("VariableName");

The function in the first line of code, ImGui_SetStateInt, is the setter part of the pair, while the second function, ImGui_GetStateInt, is the getter. These together form the pair of functions that deal with integer state variables. Along with this pair, there are also pairs that deal with booleans (ImGui_GetStateBool, ImGui_SetStateBool), floats (ImGui_GetStateFloat, ImGui_SetStateFloat), colors (ImGui_GetStateColor, ImGui_SetStateColor), and third-degree vectors (ImGui_GetStateVector3f, ImGui_SetStateVector3f).

These state variables have a lot of potential uses, but by far their biggest use is in implementing what is known as a state machine. There are some nuances to what a state machine is, but how it works in a nutshell is that you store the current state of the program in a flag variable (usually an int), and then you branch your logic into a number of different states based on that flag.

State machines are used all over the place in SOMA's terminals. The different branches can be called a number of things, such as states, branches, screens, windows, and so on, but the terminology that FG seems to use is to call them "apps", so that's what I'm going to use as well for the sake of simplicity.

The implementation of a state machine is pretty straightforward. It consists of sending the aforementioned integer flag into a switch statement, having each of the separate apps be processed in its corresponding case block. For organizational reasons, each app is also encapsulated as its own separate function that is called from the case block, leaving the logic of the state machine itself to be clean and simple to follow.

But I think that's enough about the theory. Let's get into the code.

Before we get into the terminal code itself, we need to set up our app flag values. The simplest way to do this is to use an enum structure, with each separate enum value representing a different app for our state machine.

enum eTerminalState
{
    eTerminalState_Main,
    eTerminalState_First,
    eTerminalState_Second,
    eTerminalState_Third
}

As you can see, we have four total apps for our terminal to use - the main app (which will be the default app), and three branching apps. The naming convention SOMA uses for these enum values is to use the letter "e" (for enum), the name of the terminal that will use the enum, then the word "State", followed by an underscore and the name of the app itself. (Our terminal doesn't have a specific name here, so we will just call it "Terminal".)

An important thing to note here is that when you create enums, the enum script needs to be outside any class. The simplest place to put it, then, is just underneath your "#include" section, before your map's class declaration. For example, after creating your enum, the top of your script should look something like the following:

#include "base/Inputhandler_Types.hps"

#include "helpers/helper_map.hps"
#include "helpers/helper_props.hps"
#include "helpers/helper_effects.hps"
#include "helpers/helper_audio.hps"
#include "helpers/helper_imgui.hps"
#include "helpers/helper_sequences.hps"
#include "helpers/helper_game.hps"
#include "helpers/helper_modules.hps"
#include "helpers/helper_ai.hps"

//--------------------------------------------------

enum eTerminalState
{
    eTerminalState_Main,
    eTerminalState_First,
    eTerminalState_Second,
    eTerminalState_Third
}

//--------------------------------------------------

class cScrMap : iScrMap
{ ...

Now that the enum has been created, let's look back at the terminal code. What we want to do is create our state machine inside our terminal's main OnGui function. The basic steps of doing this is to get the value of the current app, then run that value through our switch statement:

int lActiveApp = ImGui_GetStateInt("Terminal_CurrentApp");

switch (lActiveApp)
{
    case eTerminalState_Main:
        break;
    case eTerminalState_First:
        break;
    case eTerminalState_Second:
        break;
    case eTerminalState_Third:
        break;
}

Not too terrible, right? However, there is one problem that hasn't been addressed yet. As you can see, on every update loop of our terminal, we are receiving the current value of the state variable "Terminal_CurrentApp". But what happens the first time this code is run? We need a way to tell SOMA what the default app is, the first time the terminal is run. The initial guess would be to put our initialization code in the map's OnEnter or OnStart functions, but the problem with that is ImGui functions do not work outside of an ImGui context, so any attempt to call ImGui_SetStateInt outside our OnGui function will result in an error. (There are a few exceptions to this rule, such as the ImGui_PreloadImage function that we have used in the past.)

This is where the function ImGui_IsFirstRun comes in. This function will run true only if the current update loop iteration is the first time this terminal's update loop has been processed. Using this function, we can make sure that our crucial ImGui-specific code gets run at the beginning of every terminal's lifespan. Put the following code at the top of your terminal's OnGui function:

if (ImGui_IsFirstRun())
{
    ImGui_SetStateInt("Terminal_CurrentApp", eTerminalState_Main);
}

Now that that little oversight is sorted out, let's return our focus back to the state machine.

What we want to do now is to write the logic for our individual apps. As I've said before, the best way to do this is to put the logic for each app in its own function that we call from the state machine, so let's do that. Add the following functions to your map code, just after the terminal's OnGui function:

void OnGui_Terminal_Main(const tString &in asEntityName, float afTimeStep)
{
    
}

//-------------------------------------------------------

void OnGui_Terminal_First(const tString &in asEntityName, float afTimeStep)
{
    
}

//-------------------------------------------------------

void OnGui_Terminal_Second(const tString &in asEntityName, float afTimeStep)
{
    
}

//-------------------------------------------------------

void OnGui_Terminal_Third(const tString &in asEntityName, float afTimeStep)
{
    
}

These functions will provide a nice isolated environment to handle our individual apps from. From looking at the function signatures, you can see that I included the same parameters in these functions that were present in the main OnGui function. While this isn't required, it does provide a consistent convention across all of our separate GUI functions. This is generally good form to do in your code, as it helps you know at a glance what the purpose of your function is.

Now that we have our functions created, let's change our state machine to call them at the appropriate times:

switch (lActiveApp)
{
    case eTerminalState_Main:
        OnGui_Terminal_Main(asEntityName, afTimeStep);
        break;
    case eTerminalState_First:
        OnGui_Terminal_First(asEntityName, afTimeStep);
        break;
    case eTerminalState_Second:
        OnGui_Terminal_Second(asEntityName, afTimeStep);
        break;
    case eTerminalState_Third:
        OnGui_Terminal_Third(asEntityName, afTimeStep);
        break;
}

Now that we've got our functions getting called from our state machine, it's time to actually put some functions in those functions.

But first, before we put code in the functions themselves, we are going to create a couple more functions into the code for helper reasons. Our apps will each be using buttons and/or labels, but we want to make sure that all our GUI elements look consistent with each other without creating repetitive code. Put the following two helper functions after our GUI functions.

cImGuiButtonData GetDefaultButtonData()
{
    cImGuiButtonData data;
    data.mFont.SetFile("sansation_large_bold.fnt");
    data.mFont.mvSize = cVector2f(75);
    data.mColorBase = cColor(0.4);
    data.mColorInFocus = cColor(0.5);
    data.mFontAlign = eFontAlign_Center;
    
    return data;
}

cImGuiLabelData GetDefaultLabelData()
{
    cImGuiLabelData data;
    data.mFont.mvSize = cVector2f(80);
    
    return data;
}

That should all look like familiar code to you now. All this is doing is creating a single configuration for label data and button data that we can use repeatedly in our code without creating potentially tiresome or error-prone copy-paste code. Now that that's out of the way, let's get back to our GUI code.

Let's start with the main app. From here, we are simply going to create three different buttons that act as our navigation to the other three apps. In order to do this, for each button, we change the flag for the current app when the button is pressed.

void OnGui_Terminal_Main(const tString &in asEntityName, float afTimeStep)
{
    cImGuiButtonData buttonData = GetDefaultButtonData();
        
    if (ImGui_DoButtonExt(
            "App 1 Button",
            "App 1",
            buttonData,
            cVector3f(312, 125, 0),
            cVector2f(400, 110)))
    {
        ImGui_SetStateInt("Terminal_CurrentApp", eTerminalState_First);
    }
    
    if (ImGui_DoButtonExt(
            "App 2 Button",
            "App 2",
            buttonData,
            cVector3f(312, 325, 0),
            cVector2f(400, 110)))
    {
        ImGui_SetStateInt("Terminal_CurrentApp", eTerminalState_Second);
    }
    
    if (ImGui_DoButtonExt(
            "App 3 Button",
            "App 3",
            buttonData,
            cVector3f(312, 525, 0),
            cVector2f(400, 110)))
    {
        ImGui_SetStateInt("Terminal_CurrentApp", eTerminalState_Third);
    }
}

Now for the rest of the apps. For these, we are simply going to show a label that displays the current app.

void OnGui_Terminal_First(const tString &in asEntityName, float afTimeStep)
{
    ImGui_DoLabelExt(
            "This is the first app.",
            GetDefaultLabelData(),
            cVector3f(112, 75, 0),
            cVector2f(400, 110));
}

//-------------------------------------------------------

void OnGui_Terminal_Second(const tString &in asEntityName, float afTimeStep)
{
    ImGui_DoLabelExt(
            "This is the second app.",
            GetDefaultLabelData(),
            cVector3f(112, 75, 0),
            cVector2f(400, 110));
}

//-------------------------------------------------------

void OnGui_Terminal_Third(const tString &in asEntityName, float afTimeStep)
{
    ImGui_DoLabelExt(
            "This is the third app.",
            GetDefaultLabelData(),
            cVector3f(112, 75, 0),
            cVector2f(400, 110));
}

You can try the code as it is now, but if you do, you might see there is a problem. Currently, once you enter one of the other apps, there's no way to get back to the main app. In order to fix this, we need to put in a back button for the player to click to return from their current app. There are a few ways to do this, but the most common way in SOMA that I've seen involves creating a common button for all the apps to use, then enabling or disabling it whenever appropriate. Change the code in the main terminal OnGui function to show the following:

int lState = ImGui_GetStateInt("Terminal_CurrentApp");
int lBackState = eTerminalState_Main;
bool bBackEnabled = false;

switch(lState)
{
    case eTerminalState_Main:
        OnGui_Terminal_Main(asEntityName, afTimeStep);
        break;
    case eTerminalState_First:
        OnGui_Terminal_First(asEntityName, afTimeStep);
        bBackEnabled = true;
        break;
    case eTerminalState_Second:
        OnGui_Terminal_Second(asEntityName, afTimeStep);
        bBackEnabled = true;
        break;
    case eTerminalState_Third:
        OnGui_Terminal_Third(asEntityName, afTimeStep);
        bBackEnabled = true;
        break;
}

if (bBackEnabled)
{
    if (ImGui_DoButtonExt(
            "backButton",
            "Back",
            GetDefaultButtonData(),
            cVector3f(45, 650, 1),
            cVector2f(325, 115)))
    {
        ImGui_SetStateInt("Terminal_CurrentApp", lBackState);
    }
}

There are a few changes here. First, we added a couple new variables called lBackState and bBackEnabled, with lBackState set to eTerminalState_Main and bBackEnabled to false by default. Then in all the apps other than the main app, we set bBackEnabled to true. And finally, we check if bBackEnabled is set to true, and if so, we create a button that will change the current app to the app specified in lBackState (which in this case, is our main app).

And that just about covers the basics of using the ImGui state variable system in implementing a state machine. State machines are very powerful in terms of what they enable the program to do with relative ease and an immense amount of organization. In the next tutorial, we are going to learn about a couple of the less common and more complex ImGui controls.

Extra Credit

In this case, our state machine had a single main app, and a bunch of branching apps that returned to the main one. How else might a state machine be structured?
(This post was last modified: 09-03-2016, 08:04 PM by Abion47.)
11-27-2015, 01:20 AM
Find
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#15
RE: GUI Tutorial Series

A Regular Grab Bag

What we've seen so far is enough to make a good amount of types of terminals, but there may be occasions in which you want to provide the player a bit more control. Perhaps you want to create a puzzle entirely on a terminal? Or maybe you want to layout text with a bit more pizzazz? In either case or others, this tutorial will introduce you to four more controls: the CheckBox, the ToggleButton, the RepeatButton, and the Slider.

Table of Contents
The Basics
Getting More Advanced
Using SOMA's Built-In GUI Styles
  • StationGui (WIP)
  • UrbanGui (WIP)
  • Playing Audio (WIP)

Going Beyond Terminals
  • Setting Up a User Module (WIP)
  • Basic Heads-Up Display (WIP)
  • Target Info Module (WIP)
  • Player HUD Menu System (WIP)

Tutorial Requirements

For this tutorial, you will need the following:
  • A map with a prepared terminal, plus all the necessities. Enable the terminal for interaction.
  • A script file with a prepared OnGui terminal callback function.

Tutorial Source Files

Completed File

The Tutorial Itself

Alright, we have four controls to go through, so we are going to just go ahead and dive in.

The first control I'm going to cover is the CheckBox. For those of you who may not know, a CheckBox is a graphical widget that is just a box with one of two states: checked and unchecked. Clicking on the box will make a check mark appear, and will change the state to checked. Likewise, clicking on the box again will remove the check mark and make the state unchanged. They are useful for cases when you want a value to be toggled between true and false, such as an option that controls whether lights are on or off.

Using a CheckBox widget in code is pretty similar to using a Button. First, though, we need to add a variable to store the state of the CheckBox. When we used a variable before with the Buttons, we just created a temporary variable to store whether the Button was pressed. That was sufficient for a Button, since all we needed to know was pressed on that particular run of the OnGui function. However, with a CheckBox, the value needs to be stored in a variable that doesn't get reset once the run is completed. There are a couple ways to do this, but since we just learned how to use ImGui state variables, why not go ahead and use that?

To start out, let's put an ImGui_IsFirstRun section into the OnGui function to initialize the variables we are going to use.

if (ImGui_IsFirstRun())
{
    ImGui_SetStateBool("CheckBox_IsChecked", false);
}

Now that we have our variable initialized, let's move on to what the code for a CheckBox looks like:

cImGuiCheckBoxData checkBoxData;
checkBoxData.mFont.mvSize = cVector2f(40,40);
checkBoxData.mColorBase = cColor_White;
checkBoxData.mFont.SetFile("sansation_large_bold.fnt");
checkBoxData.mvBoxSize = cVector2f(25,25);
checkBoxData.mGfxBox = cImGuiGfx("imgui_checkbox.tga");
checkBoxData.mvCheckOverlaySize = cVector2f(25,25);
checkBoxData.mGfxCheckOverlay = cImGuiGfx("imgui_checkbox_check.tga");

bool checkBoxResult = ImGui_DoCheckBoxExt(
    "check_box",
    "This is a CheckBox",
    ImGui_GetStateBool("CheckBox_IsChecked", false),
    checkBoxData,
    cVector3f(100, 40, 0),
    cVector2f(350, 95));

ImGui_SetStateBool("CheckBox_IsChecked", checkBoxResult);

Some of the options in the cImGuiCheckBoxData should look familiar from the cImGuiButtonData. The new ones are as follows:
  • mvBoxSize - Sets the size of the box.
  • mGfxBox - Sets the graphic that will represent the box. (The file used above is the default SOMA image for the box.)
  • mvCheckOverlaySize - Sets the size of the check mark.
  • mGfxCheckOverlay - Sets the graphic that will represent the check mark. (The file used above is the default SOMA image for the check mark.)

Next, we use the function ImGui_DoCheckBoxExt to actually create the CheckBox. The parameters should look familiar, as for the most part they are identical to most of the widget functions we have looked at so far. The only new parameter is where we have ImGui_GetStateBool retrieving the value of our state variable. This parameter is what tells ImGui what the actual current state of the CheckBox is in, so it can render or not render the check mark accordingly.

Like the Button function, this function returns a value that reflects whether the state was changed (from checked to unchecked or vice versa), so that is the value we will need to capture and store in order to remember that state outside of this OnGui run. All we do here is capture the value in a temporary variable and then store that value into our state variable.

Running this code will give you something looking like this:

[Image: cVpXzPs.gif]

Next up, we have the ToggleButton. Functionally, the ToggleButton is identical to the CheckBox in that rather than clicking it like a normal Button, it reverts between two states whenever it is clicked, in this case between "pressed" and "released". Because it works with states, we will need to create a state variable for it as well. Add the following line of code to the ImGui_IsFirstRun block:

ImGui_SetStateBool("ToggleButton_IsChecked", false);

Next, let's take a look at the ToggleButton's code:

cImGuiButtonData buttonData;
buttonData.mFont.mvSize = cVector2f(40,40);
buttonData.mColorBase = cColor(0.35, 0.35, 0.35);
buttonData.mbUseTriggeredColor = true;
buttonData.mColorTriggered = cColor(0.1, 0.1, 0.1);
buttonData.mFont.SetFile("sansation_large_bold.fnt");

bool toggleButtonResult = ImGui_DoToggleButtonExt(
    "toggle_button",
    "This is a ToggleButton",
    ImGui_GetStateBool("ToggleButton_IsChecked", false),
    buttonData,
    cVector3f(100, 240, 0),
    cVector2f(380, 95));
    
ImGui_SetStateBool("ToggleButton_IsChecked", toggleButtonResult);

As you can see, the ToggleButton uses the same cImGuiButtonData as regular buttons do, and the ImGui_DoToggleButtonExt function parameters are exactly the same as the function for the CheckBox. There isn't much of anything new to explain here, but I will say that when using ToggleButtons, using the TriggeredColor values in the Button data is a must, as otherwise there would be no visible difference between when the Button is pressed and when it is released.

Running this code turns out like this:

[Image: gqQx3Qs.gif]

The second and last alternate Button type that we will be covering in this tutorial is the RepeatButton. The RepeatButton isn't actually all that different than a regular Button - it uses the same cImGuiButtonData, the appearance is the same, the parameters are the same, and it returns the same bool value that shows whether it had been clicked. The difference, however, is that where the normal Button will only return true on the particular OnGui run that it was clicked, the RepeatButton will return true for as long as the mouse Button is held down.

Normal Button Behavior:
[Image: 77rKfyK.gif]

Repeat Button Behavior:
[Image: 7OSvmoZ.gif]

For the sake of completion, here is the code for doing a RepeatButton:

ImGui_DoRepeatButtonExt(
    "repeat_button",
    "This is a RepeatButton",
    buttonData,
    cVector3f(100, 440, 0),
    cVector2f(380, 95));

(We don't need to create new Button data for the RepeatButton, since we can use the same data that we used for the ToggleButton.)
(This post was last modified: 09-03-2016, 08:03 PM by Abion47.)
07-12-2016, 07:52 PM
Find
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#16
RE: GUI Tutorial Series

The last control that this tutorial will cover is a scroll bar, which ImGui calls "sliders". Unlike all the other widgets that I have shown you, the slider is fairly unique in how it is implemented, and it isn't immediately obvious as to how it works. I'll do my best to explain it, but be prepared to sit down and get your hands dirty with experimenting.

First, we need to create something for the Slider to actually scroll. Perhaps the simplest thing to use for this is a TextFrame, so lets go ahead and use one of those:

cImGuiTextFrameData textFrameData;
textFrameData.mFont.mvSize = cVector2f(35, 35);
textFrameData.mColorBase = cColor(0, 0, 0, 0);
            
ImGui_DoTextFrameExt(
    "This is paragraph 1.\nThis is paragraph 2.\nThis is paragraph 3.\nThis is paragraph 4.\nThis is paragraph 5.\nThis is paragraph 6.\nThis is paragraph 7.\nThis is paragraph 8.\nThis is paragraph 9.\nThis is paragraph 10.\nThis is paragraph 11.\nThis is paragraph 12.\nThis is paragraph 13.\nThis is paragraph 14.\nThis is paragraph 15.\nThis is paragraph 16.\nThis is paragraph 17.\nThis is paragraph 18.\nThis is paragraph 19.\nThis is paragraph 20.",
    0,
    10,
    0,
    textFrameData,
    cVector3f(600, 95, 0),
    cVector2f(300, 550));

This shouldn't be anything new. The text that I put in the TextFrame is just the same text repeated 20 times, other than a number to identify the lines of text from each other.

(If you are wondering, the text "\n" is called an escape character. This particular escape character tells the text that anywhere there is a "\n" in the text, that means it should perform the equivalent of hitting the enter key and start a new paragraph.)

If we were to run our code now, we would see this:

[Image: SMKjnBx.png]

As you can see, the text is too long to properly contain within the TextFrame's size limits. In order to display it properly, we need to add a Slider. In order to do this, we are going to have to get a bit technical.

the first thing we are going to need to do is create another state variable. This is because the Slider uses a value that determines its slide position. Like the CheckBox and the ToggleButton, this value needs to be saved outside of the function so it doesn't get erased after the OnGui run is complete. Add this line to the ImGui_IsFirstRun code block:

ImGui_SetStateFloat("Slider_OffsetValue", 0.0f);

We haven't bothered with it up to this point, but the function ImGui_DoTextFrameExt actually returns a float value. This value represents the number of lines of text that doesn't fit in the TextFrame's view. We are going to need that value, so let's store it.

Next, we will do a quick check to see if a Slider is even necessary. All we need to do is to check the value that we got from the ImGui_DoTextFrameExt function and see if it is greater than 0. If not, then all the text within that frame is visible and we don't need to bother with a Slider.

Now we get to the meat of this thing - the Slider code. First, let's take a look at the function signature for the function that creates a Slider:

float ImGui_DoSliderVerticalExt(
    const tString &in asName,
    float afDefaultValue,
    float afMin,
    float afMax,
    float afStepSize,
    const cImGuiSliderData &in aData,
    const cVector3f &in avPos=cVector3f_Zero,
    const cVector2f &in avSize=cVector2f_MinusOne)

So first off, we have the name of the Slider widget. This is fairly self-explanatory, so we won't bother with it.

Second is afDefaultValue. This is the value that says what the current position for the Slider is (in terms of lines of text).

Third is afMin. This value sets the minimum scroll position of the Slider. This should be 0 to let the Slider be able to scroll to the top of the TextFrame, unless you are doing something specific with your GUI implementation.

Fourth is afMax. This value sets the maximum scroll position of the Slider. This should be set to the value that we got from ImGui_DoTextFrameExt, as that will let it scroll to the bottom of the TextFrame, unless you don't want it to be able to for some reason.

Fifth is afStepSize. This means how far that each scroll of the Slider will advance when it moves. Typically, if you want the Slider to scroll smoothly, set it to a small value like 0.01 . If you want the Slider to snap to each line of text, then use 1. Or you can set it to something else entirely, if you want a particular look and feel to your GUI.

The rest of the parameters are familiar - the cImGuiSliderData that provides the visual ImGui data to the Slider, and the position and size of the Slider.

TheImGui_DoSliderVerticalExt returns a value as well. In this case, it returns its current scroll position. This value is necessary to store as well, as we will need to use it to offset the TextFrame by the appropriate amount.

Now that that is out of the way, let's show what the complete code for the TextFrame-Slider combo looks like:

cImGuiTextFrameData textFrameData;
textFrameData.mFont.mvSize = cVector2f(35, 35);
textFrameData.mColorBase = cColor(0, 0, 0, 0);

float offsetValue = ImGui_GetStateFloat("Slider_OffsetValue");
float overlapLineCount = ImGui_DoTextFrameExt(
    "This is paragraph 1.\nThis is paragraph 2.\nThis is paragraph 3.\nThis is paragraph 4.\nThis is paragraph 5.\nThis is paragraph 6.\nThis is paragraph 7.\nThis is paragraph 8.\nThis is paragraph 9.\nThis is paragraph 10.\nThis is paragraph 11.\nThis is paragraph 12.\nThis is paragraph 13.\nThis is paragraph 14.\nThis is paragraph 15.\nThis is paragraph 16.\nThis is paragraph 17.\nThis is paragraph 18.\nThis is paragraph 19.\nThis is paragraph 20.",
    0,
    10,
    offsetValue,
    textFrameData,
    cVector3f(600, 95, 0),
    cVector2f(300, 550));

if (overlapLineCount > 0)
{
    cImGuiSliderData sliderData = ImGui_GetDefaultSliderVertical();
    
    offsetValue = overlapLineCount - ImGui_DoSliderVerticalExt(
        "slider",
        overlapLineCount - offsetValue,
        0,
        overlapLineCount,
        0.01,
        sliderData,
        cVector3f(900, 95, 0),
        cVector2f(75, 550));
        
    ImGui_SetStateFloat("Slider_OffsetValue", offsetValue);
}

That's quite a mouthful, so let's go through it in chunks.

First is the creation of the cImGuiTextFrameData. This hasn't changed from before, so let's skip it.

Next we retrieve the value from the state variable "Slider_OffsetValue". We need to do this because both the TextFrame and the Slider will need access to this value, so it's better to retrieve it and store it in a temporary variable to avoid having to retrieve it multiple times. (This increases the efficiency of the code as well as making it more readable.)

Next we do our ImGui_DoTextFrameExt. It's mostly the same, but there is one key difference: the use of our offsetValue in the fourth parameter. This parameter is responsible for telling the TextFrame what the text offset of the frame is, which correlates to the position of the Slider. Basically, what this means is if the offsetValue says that the TextFrame should be scrolled down by two lines, then the TextFrame will do so. We also store the return value of the function in another temporary variable called "overlapLineCount", which tells us how many lines are cut off of the top or bottom of the frame.

After our check to make sure a Slider is necessary, we create our Slider data. Unlike what I did with earlier widgets, I am merely resorting to using SOMA's default Slider data rather than create my own. The reason for this is because the cImGuiSliderData contains a fair amount of fields that need to be set, but there is rarely a case in which we would need them to be different than the default data anyway.

Then we have the ImGui_GetDefaultSliderVertical function. As the name suggests, this function creates a vertical Slider widget. We've already covered what the different parameters do, so instead I will just explain the craziness that I am doing with the return value. The offsetValue stores how many lines the TextFrame should be offset by, but the function returns its current position. For whatever reason, the position value is based on the bottom of the Slider being 0, which is inverted from what the TextFrame needs (bottom of the Slider being max offset). So in order to get the value we need, we have to subtract this value from our maximum offset, which happens to be the overlapLineCount that we have stored.

Finally, we store the resulting value into our state variable.

(As you might suspect from the name of the Slider function, there is also a corresponding horizontal Slider widget as well. It acts the same as the vertical Slider, so I won't be covering it in detail in this tutorial.)

Now let's see this code in action:

[Image: jSFkLyD.gif]

This has been quite a mouthful of a tutorial, but hopefully you now know how to use 4 more widgets for making your terminal GUIs more diverse and interesting. In the next tutorial, we make things a bit more intriguing by covering how you can use terminals to affect other objects on a level.
07-12-2016, 10:04 PM
Find
NewPueblo Offline
Junior Member

Posts: 6
Threads: 2
Joined: Jul 2016
Reputation: 0
#17
RE: GUI Tutorial Series

This tutorial is pretty amazing. I love how there's so much detail. I'm definitely going to be using this for my custom story. Big Grin
09-03-2016, 12:09 AM
Find
Abion47 Offline
Senior Member

Posts: 369
Threads: 22
Joined: Oct 2015
Reputation: 46
#18
RE: GUI Tutorial Series

She Goes In And Out, And In, And Out...

So far, we've gone through just about all of the basics of creating beautiful, interactive terminals that can do just about anything we can throw at them. But so far, it's been limited to just that - terminals. In real life, computers can do all sorts of things, and not just confined to their own boxes. What if we wanted to make a terminal that controlled other things? In this tutorial, we are going to cover just that.

Table of Contents
The Basics
Getting More Advanced
Using SOMA's Built-In GUI Styles
  • StationGui (WIP)
  • UrbanGui (WIP)
  • Playing Audio (WIP)

Going Beyond Terminals
  • Setting Up a User Module (WIP)
  • Basic Heads-Up Display (WIP)
  • Target Info Module (WIP)
  • Player HUD Menu System (WIP)

Tutorial Requirements

For this tutorial, you will need the following:
  • A map with a prepared terminal.
  • A button prop named "button".
  • A lamp prop named "lamp".
  • A script file with a prepared OnGui terminal callback function.

Tutorial Source Files

Completed File

The Tutorial Itself

As you can see by the requirements, the terminal we will be making for this tutorial will be interacting with a button and a lamp. This will illustrate the two basic concepts of computer interfacing - input and output. Input consists of anything that gives data to the computer, such as a mouse, keyboard, or microphone. Output consists of anything that receives data from a computer, such as speakers, a printer, or a monitor. (Note that some things, like a USB thumb drive, can be both input and output.)

The first thing we will be focusing on is input. Now, there are two ways to provide input to a terminal - reading the data directly, or saving the data to a dummy variable. Reading data directly can be messy, as it depends entirely on what kind of prop you are receiving data from, and it doesn't lead to very readable code. Using a dummy variable, on the other hand, merely requires some form of callback function (which just about every prop has) and allows you to name the variable to describe its purpose.

For example, take a look at the following code snippets:

Reading Directly
void GUITerminal_OnGui(const tString&in asEntityName, float afTimeStep)
{
    // ... Create Label Data
    
    if (Button_IsSwitchedOn("button"))
    {
        ImGui_DoLabelExt("Button pressed", labelData, cVector3f(220, 260, 0));
    }
    else
    {
        ImGui_DoLabelExt("Button not pressed", labelData, cVector3f(220, 260, 0));
    }
}

Dummy Variable

bool mbButtonPressed;

//-------------------------------------------------------

void button_OnConnectionStateChange(const tString &in asEntity, int alState)
{
    if (alState == 1)
    {
        mbButtonPressed = true;
    }
    else
    {
        mbButtonPressed = false;
    }
}

//-------------------------------------------------------

void GUITerminal_OnGui(const tString&in asEntityName, float afTimeStep)
{
    // ... Create Label Data
    
    if (mbButtonPressed)
    {
        ImGui_DoLabelExt("Button pressed", labelData, cVector3f(220, 260, 0));
    }
    else
    {
        ImGui_DoLabelExt("Button not pressed", labelData, cVector3f(220, 260, 0));
    }
}

The second example is quite a bit more verbose, but there's no question as to what it does when looking at it. The first example, despite being much shorter, is also much more ambiguous as to its apparent meaning. (The difference doesn't seem like much in this example, but trust me. When your mod's code gets to be thousands of lines of code long, you're going to appreciate the extra clarity that the second example provides.)

No matter which method you use, however, you can see that receiving input from other entities in your map is simple. The code within your OnGui function doesn't exist in a vacuum, so you can freely call other variables or resources to get any sort of information you want about different things.

In terms of what the code looks like in the game, the two examples produce the same result:

[Image: fKcuxLZ.gif]

Output is a similar process, but it is simpler in that you don't need to worry about dummy variables or recording data or any of that jazz. All it takes is a direct call to whatever object you are trying to output data to. In this case, we are going to use a terminal button to control the lit state of a lamp.

void GUITerminal_OnGui(const tString&in asEntityName, float afTimeStep)
{
    // ... Create Button Data
    
    if (ImGui_DoButtonExt("lamp_button", "Toggle Lamp", buttonData, cVector3f(220, 260, 0), cVector2f(450, 115)))
    {
        if (Lamp_GetLit("lamp"))
        {
            // Turn lamp off
            Lamp_SetLit("lamp", false, true);
        }
        else
        {
            // Turn lamp on
            Lamp_SetLit("lamp", true, true);
        }
    }
}

(The code should be straight forward, but to be clear, what this script does is, whenever the button is pressed, it checks whether the "lamp" entity is currently lit, and turns it off/on correspondingly.)

Running this code in the game gives you something like the following:

[Image: q5eAVIW.gif]

And that's about all you need to know about input and output for SOMA terminals. Using these basics, you can do all sorts of stuff, like using a terminal to open doors, or as a suppliment to a lever-based puzzle, just to name a couple of possibilities.

I hope you are starting to understand just how powerful a tool that terminals can be in your mods. In the next tutorial, we are going to learn a bit more about how to group and organize our ImGui widgets on the screen.

09-03-2016, 08:02 PM
Find




Users browsing this thread: 1 Guest(s)