• entries
    503
  • comments
    1888
  • views
    333726

GUI Part 2: A simple example

Sign in to follow this  

236 views

In my last post I spoke about how writing your own game GUI was a long and difficult task a best; and how using different technologies for different needs can save you time.

I'm going to explain my GUI needs for Selenite (notably the Win32 runtime) and show how I've used multiple systems to expedite my development, keep bug potential to a minimum and still produce a quality experience.


GUI Needs for the Selenite Engine are:


  • Main Menu: New Game,Load,Save,Exit,Sound Volume, Language Selection, Help, etc.

  • Save and Load file selector

  • Feedback Sender Dialog

  • An Inventory, with scrollable 'endless' items and item quantity indicators

  • A Dialogue selector, lucas arts adventure style, line for line with scroll bar




These are moderate GUI needs; some games need far less, some need more. Here is how I fulfilled them with only about 16 hours of work.

Main Menu

After I got over the stigma of the idea, I decided to use a native windows menu; always visible when in windowed mode, hidden until mouse-over in full-screen mode.

This made it extremely easy to add lots of options, even checkbox and radio button selectors for options.

Save and Load File Selector

These can be a bear; they seem simple, but with all the associated problems that can come from getting good text input from the user, to dealing with file access ,save location issues and listing previously saved files; it can be a pain. I did it before for Morning's Wrath and Malathedra, and I was in no mood to do it again. I used the Windows Common Dialogs, for saving and opening save files using a custom file extension; wham-bam moving on :)

Feedback Sender Dialog

Using a free resource editor I was able to code up a simple Win32 dialog for sending feedback to us; not extremely easy, but it beats doing it manually.

Custom UI

This is where it gets good; since this was a very important gameplay component (and I already banked loads of time from using Win32 for other elements) I decided this could be custom. I developed a single component called 'UI' which had a robust set of features; but was small enough to insure proper function without a lot of hidden bug potential.

UI.h

#pragma once

#include "Graphics.h"

class UI
{
private:
const static int UP=0;
const static int OVER=1;
const static int DOWN=2;
public:
float x;
float y;
float width;
float height;
//content
std::string text;
unsigned int textFlags;
D3DXCOLOR textColor;
Texture* downImage;
D3DXCOLOR downColor;
Texture* overImage;
D3DXCOLOR overColor;
Texture* upImage;
D3DXCOLOR upColor;
ResourceStub* clickSound;
D3DXCOLOR color;
std::vector children;
UI* parent;
int scrollX;
int scrollY;
bool clipChildren;
bool down;
bool over;
bool enabled;
bool visible;
bool clicked;
void* tag;
UI(void);
virtual ~UI(void);
void getXY(int& x,int& y);
bool hitTest(int px,int py);
bool containerTest(void);
void render(void);
bool mouseInput(float px, float py, float pz, bool primary, bool pressed);
UI* addChild(UI* ui=0);
void removeAllChildren(void);
void scrollTo(int sx,int sy);
void scroll(int sx,int sy);
UI* getClickedChild(void);
};



UI.cpp

#include "UI.h"
#include
#include "Engine.h"
UI::UI(void)
{
text="";
downImage=0;
downColor=0xFFFFFFFF;
overImage=0;
overColor=0xFFFFFFFF;
upImage=0;
upColor=0xFFFFFFFF;
clickSound=0;
color=0xFFFFFFFF;
textColor=0xFFFFFFFF;
textFlags=DTFlags::Center|DTFlags::VCenter|DTFlags::SingleLine;
tag=0;
parent=0;
clipChildren=true;
scrollX=0;
scrollY=0;
enabled=true;
visible=true;
down=false;
over=false;
clicked=false;
x=0;y=0;width=1;height=1;
}

void UI::getXY(int& x,int& y)
{
if(parent)
{
int vx,vy;
parent->getXY(vx,vy);
x=this->x+vx+parent->scrollX;
y=this->y+vy+parent->scrollY;
}
else
{
x=this->x;
y=this->y;
}
}

UI* UI::addChild(UI* ui)
{
if(!ui)ui=new UI();
ui->parent=this;
children.push_back(ui);
return ui;
}

void UI::removeAllChildren(void)
{
std::vector::iterator it=children.begin();
while(it!=children.end())
{
UI* ui=*it;
delete ui;
++it;
}
children.clear();
}

UI::~UI(void)
{
removeAllChildren();
}

bool UI::hitTest(int px,int py)
{
int vx=0;int vy=0;
getXY(vx,vy);
return px>=vx&&py>=vy&&px}

bool UI::containerTest(void)
{
if(parent&&parent->clipChildren)
{
if(parent->scrollX+x+width<=0||parent->scrollY+y+height<=0||parent->scrollX+x+width>parent->width||parent->scrollY+y+height>parent->height)
{
return false;
}
}
return true;
}

void UI::scrollTo(int sx,int sy)
{
scrollX=sx;
scrollY=sy;
}

void UI::scroll(int sx,int sy)
{
scrollX+=sx;
scrollY+=sy;
}

UI* UI::getClickedChild(void)
{
std::vector::iterator it=children.begin();
while(it!=children.end())
{
UI* ui=*it;
if(ui->clicked)return ui;
++it;
}
return 0;
}

void UI::render(void)
{
int vx=0;int vy=0;
if(visible&&containerTest())
{
getXY(vx,vy);
Texture* img=0;
D3DXCOLOR clr;
if(down)
{
if(over){img=downImage;clr=downColor;}
else{img=upImage;clr=upColor;}
}
else
{
if(over){img=overImage;clr=overColor;}
else{img=upImage;clr=upColor;}
}
clr.r*=color.r;clr.g*=color.g;clr.b*=color.b;clr.a*=color.a;
if(img)Graphics::drawImage(img,vx,vy,clr);
clr=textColor;clr.r*=color.r;clr.g*=color.g;clr.b*=color.b;clr.a*=color.a;
if(!text.empty())Graphics::drawText(text,vx,vy,width,height,clr,textFlags);

std::vector::iterator it=children.begin();
while(it!=children.end())
{
UI* ui=*it;
ui->render();
++it;
}
}
}


bool UI::mouseInput(float px, float py, float pz, bool primary, bool pressed)
{
clicked=false;
if(visible&&containerTest())
{
bool oldOver=over;
over=hitTest(px,py);
//if(over&&!oldOver&&overSound)Engine::playSound(overSound);
if(enabled)
{
if(primary&&pressed&&over)down=true;
if(primary&&!pressed)
{
if(down==true&&over==true)
{
clicked=true;
if(clickSound)Engine::playSound(clickSound);
}
down=false;
}
std::vector::iterator it=children.begin();
while(it!=children.end())
{
UI* ui=*it;
ui->mouseInput(px,py,pz,primary,pressed);
++it;
}
}
else
{
down=false;
}
return over;
}
else
{
over=false;
down=false;
}
return false;
}



"...oh my goodness some source code!..." I hear you cry, but before we start splitting hairs about formating and whatnot (though if you've got some burning constructive comments I'd love to hear them) know that this class was coded to fulfill a set of features; it could be/should be more than one class but loading features into this one class for a specific purpose turned out to be very beneficial.

Breaking it down

If you peruse the source a bit, you might notice that the main features the UI class provides are:


  • A proper pushbutton click interface

  • Images to simulate roll-over and up/down buttons states

  • A container for other UI components

  • Coarse child control clipping

  • Child component scrolling



All of these features were developed out of need; this one component fulfills all of my custom UI needs, which isn't exactly trivial.

You'll notice it has render and input methods used to connect it to the input system and the rendering system; it is very ingrained in the selenite engine (using graphics types and such) but since this wasn't going to be a library component there was no reason to needlessly abstract it; porting it to another system would be fast and due to code size, very bug free.

Now we'll talk about how it's used.

Inventory: The ItemMenu

For selenite you can have the option an item inventory, or no inventory; and whether or not to have it at the top or bottom of the screen; rather rigid choices, but for the types of games selenite will make it is sufficient and reduces complexity for both the game developer; and the engine itself.



Seen here, the inventory is a horizontal bar, and contains a number of 'item cells'. As you gain items the list increases to the right, until multiple rows are created. The bar only shows a single row at a time, rows can be scrolled using the up and down arrows at the left (hard to see, bad graphic :D) and arrows are only shown when there is content to be scrolled.

How is it made?

ItemMenu.h

#pragma once

#include "Graphics.h"
#include "ResourceStub.h"
#include "Item.h"
#include "Game.h"
#include "UI.h"

class ResourceStub;
class Item;
class Game;
class UI;

class ItemMenu
{
public:
int index;
int count;
float cellSize;
float cellHalfSize;
int itemsPerPage;
UI upArrow;
UI downArrow;
UI background;
Texture* texCell;
ResourceStub* resScrollUpSound;
ResourceStub* resScrollDownSound;
public:
ItemMenu(Texture* texPanel,Texture* texCell,Texture* texupArrow,Texture* texdownArrow,ResourceStub* resScrollUpSound,ResourceStub* resScrollDownSound);
~ItemMenu(void);
void apply(void);
void nextPage(void);
void prevPage(void);
bool hasPrevPage(void);
bool hasNextPage(void);
void render(void);
void update(float t);
bool mouseInput(float px, float py, float pz, bool primary, bool pressed);

};



ItemMenu.cpp

#include "TopicMenu.h"
#include "Engine.h"
#include
#include
ItemMenu::ItemMenu(Texture* texPanel,Texture* texCell,Texture* texupArrow,Texture* texdownArrow,ResourceStub* resScrollUpSound,ResourceStub* resScrollDownSound)
{
index=0;
cellSize=64;
cellHalfSize=cellSize/2;
background.x=0;
background.width=Engine::game->getClass()->stageWidth;
background.height=cellSize;
background.upImage=texPanel;
background.overImage=texPanel;
background.downImage=texPanel;
background.clipChildren=true;
switch(Engine::game->getClass()->inventoryAlignment)
{
case INVENTORY_ALIGNMENT_NONE:
background.visible=false;
background.enabled=false;
//fallthrough to top
case INVENTORY_ALIGNMENT_TOP:
background.y=0;
break;
case INVENTORY_ALIGNMENT_BOTTOM:
background.y=Engine::game->getClass()->stageHeight-cellSize;
break;
}


this->texCell=texCell;
this->resScrollUpSound=resScrollUpSound;
this->resScrollDownSound=resScrollDownSound;

upArrow.upImage=texupArrow;
upArrow.overImage=texupArrow;
upArrow.downImage=texupArrow;
upArrow.upColor=0x80FFFFFF;
upArrow.overColor=0xFFFFFFFF;
upArrow.downColor=0x80FFFFFF;
upArrow.width=cellHalfSize;
upArrow.height=cellHalfSize;
upArrow.x=0;
upArrow.y=background.y;
upArrow.visible=false;

downArrow.upImage=texdownArrow;
downArrow.overImage=texdownArrow;
downArrow.downImage=texdownArrow;
downArrow.upColor=0x80FFFFFF;
downArrow.overColor=0xFFFFFFFF;
downArrow.downColor=0x80FFFFFF;
downArrow.width=cellHalfSize;
downArrow.height=cellHalfSize;
downArrow.x=0;
downArrow.y=background.y+cellHalfSize;
downArrow.visible=false;
}

ItemMenu::~ItemMenu(void)
{

}

void ItemMenu::apply(void)
{
std::vector items=Engine::game->findVisibleItems();
std::vector::iterator it1=items.begin();
background.removeAllChildren();
int index=0;
int cols=(Engine::game->getClass()->stageWidth-cellHalfSize)/(cellSize+cellHalfSize);
int col=0;
int row=0;
float pages=(float)items.size()/(float)cols;
count=ceil(pages);
while(it1!=items.end())
{
Item* item=*it1;
UI* cell=background.addChild();
cell->x=col*(cellSize+cellHalfSize)+cellHalfSize;
cell->y=row;
cell->width=cellSize;
cell->height=cellSize;
cell->upImage=texCell;
cell->overImage=texCell;
cell->downImage=texCell;
++col;

if(item->image)
{
UI* itemCell=background.addChild();
itemCell->x=cell->x;
itemCell->y=cell->y;
itemCell->width=64;
itemCell->height=64;
itemCell->upImage=item->image;
itemCell->overImage=item->image;
itemCell->downImage=item->image;
itemCell->enabled=false;
cell->tag=item;

}

if(col>=cols)
{
col=0;
row+=cellSize;
}
++it1;
}
//calc current state
hasPrevPage();
hasNextPage();
}

void ItemMenu::update(float t)
{

}

void ItemMenu::render(void)
{
if(Engine::game&&Engine::game->getClass()->inventoryAlignment!=INVENTORY_ALIGNMENT_NONE)
{

background.render();
upArrow.render();
downArrow.render();
}
}


void ItemMenu::nextPage(void)
{
if(hasNextPage())
{
Engine::playSound(resScrollDownSound);
++index;
background.scroll(0,-cellSize);
hasPrevPage();
hasNextPage();
}
}

void ItemMenu::prevPage(void)
{
if(hasPrevPage())
{
Engine::playSound(resScrollUpSound);
--index;
background.scroll(0,cellSize);
hasPrevPage();
hasNextPage();
}
}

bool ItemMenu::hasPrevPage(void)
{
upArrow.visible=index>0;
return upArrow.visible;
}

bool ItemMenu::hasNextPage(void)
{

downArrow.visible=index<(count-1);
return downArrow.visible;
}

bool ItemMenu::mouseInput(float px, float py, float pz, bool primary, bool pressed)
{
if(Engine::game!=0&&Engine::game->getClass()->inventoryAlignment!=INVENTORY_ALIGNMENT_NONE)
{
if(pz!=0)
{
int amt=abs(pz);
for(int i=0;i {
if(pz>0)
{
prevPage();
}
else if(pz<0)
{
nextPage();
}
}
}
if(background.mouseInput(px,py,pz,primary,pressed))
{
UI* child=background.getClickedChild();
if(child)
{
Item* item=(Item*)child->tag;
if(item!=0)
{
if(Engine::selectedItem!=0)
{
hades::message("Item "+Engine::selectedItem->id+" was used with item "+item->id);
//use this item with this other item
lua_State*l;
l=Engine::game->beginRunEvent("combine","item1,item2");
if(l)
{
lua_pushstring(l,Engine::selectedItem->id.c_str());
lua_pushstring(l,item->id.c_str());
Engine::game->endRunEvent(l,2);
}
//backwards
l=Engine::game->beginRunEvent("combine","item1,item2");
if(l)
{
lua_pushstring(l,item->id.c_str());
lua_pushstring(l,Engine::selectedItem->id.c_str());
Engine::game->endRunEvent(l,2);
}
}
else
{
//pickup the item
hades::message("Item "+item->id+" was selected");
Engine::selectedItem=item;
}
}
}
upArrow.mouseInput(px,py,pz,primary,pressed);
if(upArrow.clicked)prevPage();
downArrow.mouseInput(px,py,pz,primary,pressed);
if(downArrow.clicked)nextPage();
return true;
}
}
return false;
}




We see that the item menu background is a container for the item cells, which hold the item cell graphic and an additional component to hold the graphic of the inventory item itself. In addition there is logic for handling putting an item 'in hand' and using an existing item on another item. Engine logic baked into the UI component? it was fast, and ItemMenu is only used for this purpose so I say it's OK.

Conversation - The Topic Menu

Since the topic menu is very similar to the item menu, I wont go into as much detail. Much like the Item Menu the topic menu lists lines of dialogue options, which can be scrolled. It takes advantage of the text label feature of UI component. And also, it contains logic for actually executing dialogue topic selection.

That's all for now!
Sign in to follow this  


1 Comment


Recommended Comments

I don't have too much to comment on - the interface looks pretty, and I applaud the fact that the code was written "as needed." However, I have to say that the fact that a function called hasNextPage() has side effects (and is therefore used without checking the return value, which looks bizarre) gives me shivers.

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now