Designing a game-orientated GUI library

Started by
20 comments, last by Bob Janova 18 years ago
I'd like to hear people's thoughts on game-orientated GUI libraries - more specifically I'm thinking of writing my own set of GUI controls for use in my games. Background I tend to write lots of smaller games and experiments rather than single big projects. Recently I've had several on the go at the same time - this works better than expected, when I get tired or stuck with one I can work on another for a while. Also improvements and common code get added which benifit the whole lot. Writing the GUI is always a tedious process, so I'd like to share as much of the code as possible. Ideas Simple, minimal selection of widgets. I'm not doing an RPG here, I just need the basics. So far I'm thinking: - Static text - Static images - Push button - Checkboxes, sliders (for options screens) - Simple text boxes (for high scores) What I don't need: - Multiple floating windows - Tables - Combo boxes - Drop menus - Right clicking - Any sort of automatic layout wizardry - Scroll panes Undecided: - Keyboard navigation - Popup boxes Easily skinable. As I'd like this to be used in lots of different games, I't got to be able to completely change it's look. Probably don't need anything fancy here other than being able to set the textures used for the various controls. Flexible usage. Again, with lots of different games possibly using it I don't want to enforce any kind of rigid usage pattern. Anyone any comments or ideas on this? I think as long as I make it possible to add custom components quite painlessly anything fancy and game specific can easily be added in just for that game (fancy level select screens etc.). And by keeping the actual widgets and the range of interactions simple it shouldn't take me too long to get something workable. Previously I've found that automatic layout managers are one of the most tricky points to get right, so I'm slinging this whole area out on it's arse. [grin] The problem is that even the simplest case is tricky to code with multiple widgets interacting, and even then it's often wrong lots of the time.
Advertisement
Writing your own is always a good idea. This way you can customise it for all those little quirks and kwizzles that you want. Take 'Play Online' for instance, that is very customized. Getting a pre-made GUI would allow for a quicker start though, as most GUIs are based on the same principles.
Quote:Original post by rpg_code_master
Getting a pre-made GUI would allow for a quicker start though, as most GUIs are based on the same principles.

Given my platform and libraries of choice there isn't really any existing libraries available which are suitable. There are a couple (one of which I'm already using and hoping to replace) but because they're general-purpose all-singing all-dancing libraries the effort to intergrate and create menus with them makes it far too difficult to just casually drop them in. And the end result ends up being far from satisfactory (ie. it looks like a windows clone, not a game).

I'm mainly after peoples thoughts on how practical (or not) my minimal system will be. I think that 90% of most simple game's GUI can be composed of text + images + push buttons, but I may be missing something obvious...
Heya,


I've coded a simple GUI solution on the last place I worked, and I kinda liked it somewhat, at least the main idea.

This was my first attempt at coding a GUI system from scratch. I wanted to manage passing focus between widgets in a simple manner, something like:

// The base class for the whole systemclass Widget {// Stuffprotected:char Name[64];Widget *Parent, *Son, *Brother;public:// Stuff...// Also include some nice hierarchy managing methods// This is goodWidget* FetchNode(const char *name);virtual ~Widget();virtual Widget* Input(int code) { return(this); }};// And in the main loop, focus manages itself with something likeWidget *Focus;// Stuff...// Getting input and translating it to some code, then haveFocus = Focus->Input(input_code);


So what happens is that when you implement your controls in an interface you're creating, just make sure you return what should be the new widget with focus - note the recursive possibilities.

This idea seemed good at start, and it turned out working quite well for some interfaces I ended up writing using this system. I turned up adding support for sliders, radio buttons (which could also act as simple push buttons), text labels, text input boxes and bitmaps.

So what I'm saying is, for what you want, it's quite simple to come up with something good on your own. But if you like my idea, go ahead and knock yourself out.


Cheers.
Why no right clicking? It's very handy for context help or simple menus.

I'd also impliment scrolling. Maybe not actual scroll UI element, but the ability for arrow keys or pgup/pgdown to scroll lists of text. Pretty much a necessity for any online game and their chat boxes.
I think it's a great idea :), first of all. It is really dull stuff to write, and many games need it.

I would start from a set of interfaces (regular readers of my replies my recognise the pattern here ;D) that define what sort of things widgets can do:
interface IRenderable { // Some form of communication with a graphics engine // Could take a Windoze device context if working with OpenGL, // for example void Render(RenderingDevice renderer);}interface IClickable { // Delegate responsibility for checking if you clicked it to // the control. This lets you have non-rectangular controls easily bool IsInside(int x, int y); void Click(int x, int y, int button, int clickCount);}// must be able to click things to select theminterface ISelectable : IClickable { bool Selected { get; set; }}interface IKeyboardHandler { void KeyUp(int keyCode, int shiftState); void KeyDown(int keyCode, int shiftState); void KeyPress(int keyCode, int shiftState);}


You can now write a skeleton UI manager that deals with these things:
class UIManager { ArrayList objects; ISelectable selected;  void Click(int x, int y, int button, int clickCount){  // called from your input module in some way  foreach(object obj in objects){   if(obj is IClickable){    if((obj as IClickable).IsInside(x, y)){     (obj as Clickable).Click(x, y, button, clickCount);     ISelectable newselection = obj as ISelectable;     if(newselection != selected){      if(selected != null) selected.Selected = false;      if(newselection != null) newselection.Selected = true;            selected = newselection;     }     break;    }   }  } } // and similarly for keyboard and rendering}


You'd probably also want a basic clickable class that covered making a basic rectangular control with a location and size that can draw itself. (I.e., class BaseControl : IClickable, IRenderable {}). Then most of your controls would inherit from that.
The only two controls you really need to internally implement in detail are a textbox and image control. Any other type of control can be derived from these using higher level API functions or a scripting language depending how you design the library.

For example a button is just an image control that changes image when clicked on. If you implement image controls to be reactive to mouse clicks, capable of binding to callback functions and also implement an internal animation system whereby you can instruct an image control to change its image after a specified delay then you have a button.

Also its useful to allow controls to contain controls. This means implementing a control heirarchy. This allows prefab controls to be made and then "copy pasted" easily. For example a scroll bar control is actually a transparent image control which contains a child image control for the bar and another for the slider, and a textbox control for the value. All coordinated by top level scroll bar control. In all honestly it would be a nightmare to design any kind of decent looking menu that didn't allow controls to be imbedded in one another.

Another thing that is necessary is a messaging system that allows messages such as mouse clicks to be passed up or down the control heirarchy until they are trapped by a control willing to process it (kind of like windows messaging). Haven't thought about keyboard messages as I have never done them before.

Also it is absolutely critical to make a good control positioning system. You really need the ability to give widths and heights in percentages rather than pixels or else changing resolution will present problems. There is also a ton of hassle with getting text to align and clip to different sized textboxes. If you don't have a simple and solid positioning api then you will end up wasting tons of time repositioning controls using fiddly function calls wrapped in condition statements.

Finally to really speed up development you can build a GUI editor that drives your api. There are so many GUIs in a typical game that its a huge huge waste of time trying to hardcode the positions of controls manually, even if you have got prefab composite controls. I consider GUI design to be a waste of my time personally and have reached the point where I won't do it unless I have an editor. I don't have an editor.


**note case in point - the good-enough GUI system inbedded in doom3 only used images and textboxes if I recall correctly
www.cegui.org
RunningInt: Thats exactly the kind of complexity and over-engineered approach I'd like to avoid. I don't need a seperate scripting layer, or a complicated composition method, or a messaging system. Although you're right in that hardcoding positions/layout is too much like hard work, and I'll be doing a simple positioning tool to do this.

Bob Janova: That sounds like what I'd go for. Minimal amounts of logic in the controls, with plenty of events to hook into so individual games can do what they need to.

Telastyn: Because I don't need right clicking. [grin] When was the last time you had to right click in (say) Doom3? With no need for context menus the right clicking is largely unused.

Scrolling though I'm still not sure on. It's non-trivial to add in a generic and simple to use way, and I'm not convinced that it's totally required. I'll probably leave it out until I find a specific need for it.
My GUI is designed for DirectX so its heavilly based on Windows. I started defining a base class like this:

#define KGUI_BASEGUICONTROL		0x0class KBASEGUICONTROL;KBASEGUICONTROL* DefaultFunction(KBASEGUICONTROL* me, int kmsg, WPARAM wParam, LPARAM lParam);class KGRAPHICS3D_API KBASEGUICONTROL{protected:	BYTE			iSubType;	LPK3DRENDERDEVICE	lpKRenderDevice;public:	int			ID;	KRECT			bounds;		//outer limits	LPK3DSPRITEANIMATION	background;	float			lAccTicks;	KRECT			ibounds;	//inner limits	LPK3DSPRITEANIMATION	image;	float			lAccTicksimage;	float			margin;	D3DCOLOR		backColor;	D3DCOLOR		imageColor;	BOOL			Enabled;	BOOL			Visible;	BOOL			MouseIn;	BOOL			Selected;	BOOL			Pressed;	BOOL			wasPressed;	BOOL			gotFocus;	int 			renderMode;	int				value;	BOOL			Clipped;	KBASEGUICONTROL* next;	KBASEGUICONTROL* parent;	KBASEGUICONTROL* (*lpControlFunction)(KBASEGUICONTROL* me, int msg, WPARAM wParam, LPARAM lParam);public:	KBASEGUICONTROL(LPK3DRENDERDEVICE lpKRenderer);	KBASEGUICONTROL(LPK3DRENDERDEVICE lpKRenderer, int ID, float PosX, float PosY, float Width, float Height, LPK3DSPRITEANIMATION lpBackground=NULL, int rendermode=KGUI_RENDERMODE_NORMAL, BOOL Visible=TRUE, BOOL Enabled=TRUE);	~KBASEGUICONTROL();	BYTE	getSubType();	HRESULT SetKRenderer(LPK3DRENDERDEVICE ilpKRenderer);	HRESULT setBounds(float left, float top, float right, float bottom);	HRESULT setPosition(float x, float y);	HRESULT setWidth(float w);	HRESULT setHeight(float h);	HRESULT setBackground(LPK3DSPRITEANIMATION f);	HRESULT setIBounds(float left, float top, float right, float bottom);	HRESULT setImage(LPK3DSPRITEANIMATION f);	HRESULT setMargin(float m);	HRESULT setBackColor(D3DCOLOR col) { backColor=col; return S_OK; };	HRESULT setImageColor(D3DCOLOR col) { imageColor=col; return S_OK; };	HRESULT setEnabled(BOOL e) { Enabled=e; return S_OK;};	HRESULT setVisible(BOOL v) { Visible=v; return S_OK;};	HRESULT setSelected(BOOL p) { Selected=p; return S_OK; };	HRESULT setRenderMode(int r) { renderMode=r; return S_OK; };	HRESULT setPressed(BOOL p) { Pressed=p; return S_OK; };	HRESULT setClipped(BOOL c) { Clipped=c; return S_OK; };	virtual HRESULT Render(float z);	virtual KBASEGUICONTROL* inBound(float x, float y);	virtual HRESULT animate(float eTime);	virtual KBASEGUICONTROL* getChildbyID(int id) { return NULL; };	virtual KBASEGUICONTROL* processMessage(int kmsg, WPARAM wParam, LPARAM lParam);	HRESULT setControlFunction(KBASEGUICONTROL* (*func)(KBASEGUICONTROL* me, int kmsg, WPARAM wParam, LPARAM lParam));	void	getRenderBounds(const LPKRECT lpPoint, const LPKRECT lpRect);	void	getRenderIBounds(const LPKRECT lpPoint, const LPKRECT lpRect);	virtual float getWidth();	virtual float getHeight();	HRESULT setValue(int v);	int getValue();	//para evitar el conflicto de pasar punteros de EXE a DLL	void* operator new( size_t tSize );    void  operator delete( void* p );protected:	HRESULT RenderBackground(float z);};typedef KBASEGUICONTROL* LPKBASEGUICONTROL;


The DefaultFunction is the basic response function.
K3DRenderDevice is my rendering device and allows for sprite rendering.
K3DSpriteAnimation is an animated sprite class. Having two images allows my objects to have an animated image or background. Thhere is also color information for each one. Having it separated is helpful for many objects like a progressbar but you can live without the background.
Bounds, ibounds and margin allows to move the image separately from the background.
Other values like enabled, visible, etc allows to control status changes.

Notice the control only have a parent and a brother. No childs. Thats because I got another object for that.

class KGRAPHICS3D_API KCONTROLCONTAINER:public KBASEGUICONTROL{	public:		int eCount;		LPKBASEGUICONTROL list;	public:		KCONTROLCONTAINER(LPK3DRENDERDEVICE lpKRenderer);		~KCONTROLCONTAINER();		int addChild(LPKBASEGUICONTROL c);		int addChildFirst(LPKBASEGUICONTROL c);		LPKBASEGUICONTROL getChild(WORD index);		LPKBASEGUICONTROL getChildbyID(int id);		int deleteChildByID(int id);		int deleteChild(LPKBASEGUICONTROL c);		int deleteChildFirst();		int removeChild(LPKBASEGUICONTROL c);		HRESULT FlushAll();		LPKBASEGUICONTROL inBound(float x, float y);		HRESULT Render(float z);		HRESULT animate(float eTime);		LPKBASEGUICONTROL processMessage(int kmsg, WPARAM wParam, LPARAM lParam);};typedef KCONTROLCONTAINER* LPKCONTROLCONTAINER;


Its a control but also a control list. The container may hold any number of objects, even other containers. I decided to keep it separated because a container can be used to test all the objects inside while the base control only tests one object. Its just architecture but I like it this way.

With those two controls you may customize anything you want. Like:
class KGRAPHICS3D_API KBUTTON:public KBASEGUICONTROL{	public:		LPK3DSPRITEANIMATION clickImage;		LPK3DSPRITEANIMATION moveImage;		float lAccTicksClickImage;		float lAccTicksMoveImage;		D3DCOLOR disableShade;		D3DCOLOR clickShade;		D3DCOLOR moveShade;	public:		KBUTTON(LPK3DRENDERDEVICE lpKRenderer);		KBUTTON(LPK3DRENDERDEVICE lpKRenderer, int ID, float PosX, float PosY, float Width, float Height, LPK3DSPRITEANIMATION lpBackground=NULL, int rendermode=KGUI_RENDERMODE_NORMAL, BOOL Visible=TRUE, BOOL Enabled=TRUE);		~KBUTTON();		HRESULT setClickImage(LPK3DSPRITEANIMATION f);		HRESULT setMoveImage(LPK3DSPRITEANIMATION f);		HRESULT Render(float z);		HRESULT animate(float eTime);		HRESULT setClickShade(D3DCOLOR col) { clickShade=col; return S_OK; };		HRESULT setMoveShade(D3DCOLOR col) { moveShade=col; return S_OK; };		HRESULT setDisableShade(D3DCOLOR col) { disableShade=col; return S_OK; };};typedef KBUTTON* LPKBUTTON;

In this case this button has two additional images for a total of 4: the background, the image, the clicked image and the mousemove image.

One thing missing is a resize feature which I will implement shortly in order to support screen changes.

You may also notice these controls doesn't have a border. And thats right because I have never found the need to use one. But its not that hard to add one.

Hope it helps.

Luck!
Guimo

[Edited by - Run_The_Shadows on March 29, 2006 3:07:16 PM]

This topic is closed to new replies.

Advertisement