Sign in to follow this  
popcorn

Making Menus with SDL

Recommended Posts

I was thinking about creating a rpg type battle system based on some code I had wrote before in cpp workshop on these forums. I would like to make it a graphical application though rather than a console text based one like before. I understand that there are different options I could use like win32 etc but I'm going to choose to do it in SDL since I've been messing about with it quite a lot lately. Unfortunately this seems to be more difficult than I realised since SDL doesn't seem to have any GUI classes that I can just use. The main thing I would need would be some sort of menu. How can I do this?

Share this post


Link to post
Share on other sites
Quote:
Original post by popcorn
I was thinking about creating a rpg type battle system based on some code I had wrote before in cpp workshop on these forums. I would like to make it a graphical application though rather than a console text based one like before. I understand that there are different options I could use like win32 etc but I'm going to choose to do it in SDL since I've been messing about with it quite a lot lately.

Unfortunately this seems to be more difficult than I realised since SDL doesn't seem to have any GUI classes that I can just use. The main thing I would need would be some sort of menu. How can I do this?
You have a couple of options:

1. Use an existing GUI library, such as CEGUI or Guichan.

2. Write your own GUI code.

Writing a simple GUI system is a fairly straightforward process (although it can get somewhat involved once you start adding features like grouping, sliders, drop-down menus, and so forth). For the basics though, a typical approach is as follows:

1. Create a 'menu item' type (e.g. class or struct) with a rectangle that defines its 'active area'.

2. Associate function objects of some type (e.g. plain function pointers, function objects bound using boost::function, callbacks via an interface, etc.) with various events. The events of greatest interest will most likely be the mouse entering the active rect (gain focus), the mouse leaving the active rect (lose focus), and clicking while the mouse is in the active rect.

3. Every update, give each menu item an opportunity to process any pending mouse or mouse button events. The menu items should then invoke the aforementioned callbacks as appropriate.

Those are just the basics, of course - in addition to the features mentioned earlier, you might also want to support keyboard navigation, text entry (e.g. for entering the player name), and so on.

I haven't used any third-party GUI libraries, so I can't comment on them directly. As for whether to use a library or write your own code, that probably just comes down to what you want to spend your time on, and what features you need.

Share this post


Link to post
Share on other sites
Thanks jyk for pointing out the libraries that I can use, I didn't know these existed. Having had a quick look though, they might be a bit too much, I'm only really looking for a menu, nothing complicated.

I've had a quick go at implementing this but after seeing your post I feel I could make some improvements. The way I see it a menu consists of a rectangle on the screen with some text on it. At the moment my code only supports the cursor keys but I might try and implement mouse input too.

Heres my code:

Menu.h

#pragma once
#include "SDL.h"
#include "SDL_ttf.h"
#include "SDLUtils.h"
#include <vector>
#include <string>

using std::vector;
using std::string;

class Menu
{
public:
Menu(int width, int height, Point location, const vector<string> &options);
~Menu();
void moveUp();
void moveDown();
void draw();
int getCurrentItem();
private:
int menuWidth;
int menuHeight;
Point menuLocation;
vector<string> items;
int currentItem;
TTF_Font *font;
SDL_Color textColor;
SDL_Surface *textSurface;
SDL_Rect cursor;
};





menu.cpp

#include "Menu.h"

Menu::Menu(int width, int height, Point location, const vector<string> &options)
{
menuWidth = width;
menuHeight = height;
menuLocation = location;
items = options;
currentItem = 0;

TTF_Init();
font = TTF_OpenFont("DOTMATRI.ttf", 20);
textColor.r = 255;
textColor.g = 0;
textColor.b = 0;

cursor.x = menuLocation.x + 5;
cursor.y = menuLocation.y + 10;
cursor.h = 20;
cursor.w = 10;
}

Menu::~Menu()
{
SDL_FreeSurface(textSurface);
TTF_CloseFont(font);
TTF_Quit();
}

void Menu::moveUp()
{
if(currentItem == 0)
{
currentItem = items.size() - 1;
cursor.y += ((items.size() - 1) * 20);
}
else
{
currentItem--;
cursor.y -= 20;
}
}

void Menu::moveDown()
{
if(currentItem == items.size() - 1)
{
currentItem = 0;
cursor.y -= ((items.size() - 1) * 20);
}
else
{
currentItem++;
cursor.y += 20;
}
}

void Menu::draw()
{
SDL_Rect rect;
rect.x = menuLocation.x;
rect.y = menuLocation.y;
rect.h = menuHeight;
rect.w = menuWidth;

SDL_Rect rect2;
rect2.x = menuLocation.x - 2;
rect2.y = menuLocation.y - 2;
rect2.h = menuHeight - 2;
rect2.w = menuWidth - 2;

SDL_FillRect(SDL_GetVideoSurface(), &rect, SDL_MapRGB(SDL_GetVideoSurface()->format, 100, 0, 0));
SDL_FillRect(SDL_GetVideoSurface(), &rect2, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 50, 100));

Point textPos;
textPos.x = rect.x + 20;
textPos.y = rect.y + 10;

SDL_FillRect(SDL_GetVideoSurface(), &cursor, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 100, 0));

for(unsigned int i = 0; i < items.size(); i++)
{
textSurface = TTF_RenderText_Solid(font, items[i].c_str(), textColor);
applySurface(textPos.x, textPos.y, textSurface, SDL_GetVideoSurface());
textPos.y += 20;
}
}

int Menu::getCurrentItem()
{
return currentItem;
}






and heres the executable.

I would be interested in your or other peoples opinion on it.

For point 1 would menuItem be inside a vector or some other container?
Also could you possibly expand on points 2 and 3 in your post. If possible do you have any code examples of this that I could look at?

Share this post


Link to post
Share on other sites
I wasn't able to run the executable (I got the 'The application has failed to start because the application configuration is incorrect' error message).
Quote:
For point 1 would menuItem be inside a vector or some other container?
Sure, that would be fine (and would probably be the most logical choice).

First a quick stylistic note: be sure not to use 'using' directives or declarations at file scope in header files, since they'll pollute the global namespace for any file that includes that file. If it's just a small project of your own it may not matter that much, but it's still best avoided, IMO. (Also, you might Google 'C++ initializer list'.)

As for your implementation, it looks fine for what it is. Flexible, extensible GUI systems are great and all, but sometimes simple is good too. If all you need is a simple menu with a few options, something like what you posted might be perfectly sufficient.

I don't have any code examples handy, but the 'event callback' system I mentioned would actually be pretty easy to integrate (in a simple form) into what you already have. Your 'items' variable is exactly the 'container of items' I was speaking of - it's just that right now your items are very simple and only consist of some text.

In order to expand your system a bit, you'd probably want to create an 'item' struct or class that contains the item text (as you have now), along with some other info. For starters, you could add to this class or struct a function pointer, pointing to a function with the signature void(). Then, if an 'action' key is pressed (e.g. 'enter'), you would invoke the function associated with the currently active item. The callback functions themselves can do whatever you want them to do - change the game state, turn the sound on and off, toggle fullscreen mode, etc.

In a more extensible system you would probably want to use boost::function and boost::bind (or the standard library equivalents, if you have them available), which would allow you to install arbitrary function objects as callbacks. For a basic system though, simple function pointers would probably suffice.

If I were you, I'd give the function pointer thing a try, and then post back if you run into any problems.

Share this post


Link to post
Share on other sites
jyk, I don't know why the executable doesn't work. It seems to work for me when I download it.

Anyway I've given your suggestion to use function pointers a go. There was a part of me that thought this was the way to go but I just couldn't picture how to make it work. This is why the first attempt I posted up I had a getCurrentItem() method which returned the currently selected index. This would then be used with a switch statement to select the correct function. It's definitely cleaner to use function pointers though.

Well here's my new code:

menu.h

#pragma once
#include "SDL.h"
#include "SDL_ttf.h"
#include "SDLUtils.h"
#include <vector>
#include <string>

struct menuItem
{
std::string text;
void (*ptToFunc)();
};

class Menu2
{
public:
Menu2(int width, int height, Point location, const std::vector<menuItem> &options);
~Menu2();
void moveUp();
void moveDown();
void draw();
void selectCurrentItem();
private:
int menuWidth;
int menuHeight;
Point menuLocation;
std::vector<menuItem> items;
int currentItem;
TTF_Font *font;
SDL_Color textColor;
SDL_Surface *textSurface;
SDL_Rect cursor;
};



menu.cpp


#include "Menu2.h"

using std::vector;
using std::string;

Menu2::Menu2(int width, int height, Point location, const vector<menuItem> &options)
{
menuWidth = width;
menuHeight = height;
menuLocation = location;
items = options;
currentItem = 0;

TTF_Init();
font = TTF_OpenFont("DOTMATRI.ttf", 20);
textColor.r = 255;
textColor.g = 0;
textColor.b = 0;

cursor.x = menuLocation.x + 5;
cursor.y = menuLocation.y + 10;
cursor.h = 20;
cursor.w = 10;
}

Menu2::~Menu2()
{
SDL_FreeSurface(textSurface);
TTF_CloseFont(font);
TTF_Quit();
}

void Menu2::moveUp()
{
if(currentItem == 0)
{
currentItem = items.size() - 1;
cursor.y += ((items.size() - 1) * 20);
}
else
{
currentItem--;
cursor.y -= 20;
}
}

void Menu2::moveDown()
{
if(currentItem == items.size() - 1)
{
currentItem = 0;
cursor.y -= ((items.size() - 1) * 20);
}
else
{
currentItem++;
cursor.y += 20;
}
}

void Menu2::draw()
{
SDL_Rect rect;
rect.x = menuLocation.x;
rect.y = menuLocation.y;
rect.h = menuHeight;
rect.w = menuWidth;

SDL_Rect rect2;
rect2.x = menuLocation.x - 2;
rect2.y = menuLocation.y - 2;
rect2.h = menuHeight - 2;
rect2.w = menuWidth - 2;

SDL_FillRect(SDL_GetVideoSurface(), &rect, SDL_MapRGB(SDL_GetVideoSurface()->format, 100, 0, 0));
SDL_FillRect(SDL_GetVideoSurface(), &rect2, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 50, 100));

Point textPos;
textPos.x = rect.x + 20;
textPos.y = rect.y + 10;

SDL_FillRect(SDL_GetVideoSurface(), &cursor, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 100, 0));

for(unsigned int i = 0; i < items.size(); i++)
{
textSurface = TTF_RenderText_Solid(font, items[i].text.c_str(), textColor);
applySurface(textPos.x, textPos.y, textSurface, SDL_GetVideoSurface());
textPos.y += 20;
}
}

void Menu2::selectCurrentItem()
{
items[currentItem].ptToFunc();}



main.cpp


#include "SDL.h"
#include "SDL_ttf.h"
#include <vector>
#include <string>
#include "Menu.h"
#include "Menu2.h"

using std::vector;
using std::string;

const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

void drawRedRect()
{
SDL_Rect rect = {50, 150, 50, 50};
SDL_FillRect(SDL_GetVideoSurface(), &rect, SDL_MapRGB(SDL_GetVideoSurface()->format, 100, 0, 0));
}

void drawBlueRect()
{
SDL_Rect rect = {100, 150, 50, 50};
SDL_FillRect(SDL_GetVideoSurface(), &rect, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 0, 100));
}

void drawGreenRect()
{
SDL_Rect rect = {150, 150, 50, 50};
SDL_FillRect(SDL_GetVideoSurface(), &rect, SDL_MapRGB(SDL_GetVideoSurface()->format, 0, 100, 0));
}

int main( int argc, char* args[] )
{
//Start SDL
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Surface* screen = NULL;
screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE);
SDL_WM_SetCaption("SDL Menu Test", NULL);

SDL_Event event;
bool quit = false;

Point pt;
pt.x = 10;
pt.y = 10;

/*vector<string> options;
options.push_back("option1");
options.push_back("option2");
options.push_back("option3");
options.push_back("option4");*/


//Menu menu(90, 100, pt, options);
menuItem m1 = {"Draw red rectangle", &drawRedRect};
menuItem m2 = {"Draw blue rectangle", &drawBlueRect};
menuItem m3 = {"Draw green rectangle", &drawGreenRect};

vector<menuItem> options;
options.push_back(m1);
options.push_back(m2);
options.push_back(m3);

Menu2 menu2(250, 80, pt, options);

while(!quit)
{
//menu.draw();
menu2.draw();
SDL_Flip(screen);

while(SDL_PollEvent(&event))
{
if(event.type == SDL_KEYDOWN)
{
switch(event.key.keysym.sym)
{
case SDLK_UP:
//menu.moveUp();
menu2.moveUp();
break;
case SDLK_DOWN:
//menu.moveDown();
menu2.moveDown();
break;
case SDLK_RETURN:
menu2.selectCurrentItem();
break;
}
}

if(event.type == SDL_QUIT)
{
quit = true;
}
}
}

//Quit SDL
SDL_Quit();

return 0;
}




and heres the executable again. Not sure it will work.

This is how it looks:



Ok some questions now:

1. Why specifically a function pointer, pointing to a function with the signature void()?
2. Can it be a function pointer, pointing to a function with a more complicated signature?
3. How would I implement mouse support?

Share this post


Link to post
Share on other sites
Quote:
This is how it looks:
Looks good :)
Quote:
1. Why specifically a function pointer, pointing to a function with the signature void()?
2. Can it be a function pointer, pointing to a function with a more complicated signature?
The signature void() was just an example; you can use whatever signature you want.
Quote:
3. How would I implement mouse support?
Presumably, somewhere in your application you have a loop where you process all pending SDL events. When you receive a mouse movement or mouse click event, pass the event to the menu class so that it has a chance to respond. For mouse movement events, the menu class should iterate over the menu items to determine which item should be given focus. For mouse click events, you would check to see if the cursor is over any of the items, and if so, invoke the callback for that item.

That's just an overview, of course, and there's more than one way it can be done, but the above method should work fine.

Share this post


Link to post
Share on other sites
Quote:
Original post by popcorn
Do you mean I should have a method in my Menu class like:

*** Source Snippet Removed ***
Yup, pretty much :)

Stylistically, you'd probably want to use a switch statement instead of a series of 'if' statements, and would probably also want to pass in the event by constant reference. Those are just details though - the basic idea is correct.

Share this post


Link to post
Share on other sites

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

Sign in to follow this