Figure 1. Screenshot of Asteroid Arena
ClanLib works on Windows, Linux, and MacOS and is supplied in source-only zip or tar files. Windows developers can use Microsoft Visual Studio, Borland C++, or MinGW (Minimalist GNU for Windows) compilers and environments. Third-party support for Ruby and Perl language bindings is also available. Optional extras include a Lua plug-in (the popular lightweight scripting language) and FreeType (a free TrueType font library).
Inside the ClanLib Feature Set
Before digging into the API, take a quick glance at its major features:
- Basic cross-platform runtime (GUI, threading, file I/O, and so forth)
- Template-based C++ signal/slots library (type-safe callbacks/delegates)
- Integrated resource management
- Sound mixer supporting .WAV files, Ogg Vorbis, and anything supported by the MikMod library (MOD, S3M, XM, and so on)
- Document Object Model (DOM) XML parser support
- High-level 2D graphics API supporting OpenGL, DirectX, and SDL as render targets
- Batch rendering engine for best performance when rendering 2D with OpenGL
- 2D collision detection
- 2D sprite animation support
- Highly customizable GUI framework
- Network library with low- to high-level interfaces
The Basic Gaming Model of ClanLib
Now, examine the ClanLib API model from the inside out. I’ve always found that the best tutorial is a fully explained example program. Specifically, you’ll look inside Luke Worth’s boxes, an electronic version of the two-player, paper-and-pencil game (see Figure 2). The boxes game consists of a grid of points between which players alternately draw lines. Whoever encloses a square with the final line, scores one point and gets to play another turn. (If you’re eager to see the API reference first, by all means do so.)
Figure 2. A Boxes Game in Progress with a Score of Blue 8 and Red 3
I purposely kept the main program as short as possible to better highlight the “game loop:”
1 #include <iostream>
2 #include <ClanLib/application.h>
3 #include <ClanLib/core.h>
4 #include <ClanLib/display.h>
5 #include <ClanLib/gl.h>
6 #include <ClanLib/sound.h>
7 #include <ClanLib/vorbis.h>
8
9 const int boardsize = 6, spacing = 50, border = 20;
10 const int numsquares = int(pow(float(boardsize – 1), 2));
11
12 enum coloursquare { off, blue, red };
13 struct cursor {
14 int x, y;
15 bool vert;
16 };
17
18 class Boxes: public CL_ClanApplication {
19 bool ver[boardsize][boardsize – 1];
20 bool hor[boardsize – 1][boardsize];
21 coloursquare squares[boardsize – 1][boardsize – 1];
22 bool redturn;
23 bool fullup;
24 cursor curs;
25
26 void inputHandler(const CL_InputEvent &i);
27 bool findsquares(void);
28 inline int numaroundsquare(int x, int y);
29 void init();
30 void drawBoard();
31 void endOfGame();
32
33 public:
34 virtual int Boxes::main(int, char **);
35 } app;
36
37 using namespace std;
40
41 int Boxes::main(int, char **)
42 {
43 int winsize = spacing * (boardsize – 1) + border * 2;
44 try {
45 Boxes::init();
46 while (!CL_Keyboard::get_keycode(CL_KEY_ESCAPE)) {
47 Boxes::drawBoard();
48 if (fullup) break;
49 CL_System::keep_alive(20);
50 }
51 Boxes::endOfGame();
52
53 CL_SetupVorbis::deinit();
54 CL_SetupSound::deinit();
55 CL_SetupGL::deinit();
56 CL_SetupDisplay::deinit();
57 CL_SetupCore::deinit();
58 }
59 catch (CL_Error err) {
60 std::cout << “Exception caught: ”
<< err.message.c_str()
<< std::endl;
61 }
62
63 return 0;
64 }
The first thing to notice about this application is that the main() function (see line #41) is not a top-level function but rather is embedded in an object derived from CL_ClanApplication. This serves to encapsulate a lot of the inevitable platform dependencies that might surround a traditional ::main() implementation (such as having to use WinMain() in Win32 apps).
Also, note that virtually all the executable code (lines 43-58) are enclosed in a try{}/catch{} exception handler. ClanLib will throw exceptions if needed and you can restart a game, and the like. Basically, all program logic resides in init(), drawBoard(), endOfGame(), and inputHandler(). If the board has no more moves (fullup==true), you exit the game loop (in line 48). CL_System::keep_alive() updates all input and system events (like closing the window or moving it), and if given an argument, it will wait that many milliseconds before proceeding. This frees up CPU cycles in the vein of the old::Yield() from Win16 API or sleep() in Linux.
66 void Boxes::init() 67 { 68 CL_SetupCore::init(); 69 CL_SetupDisplay::init(); 70 CL_SetupGL::init(); 71 CL_SetupSound::init(); 72 CL_SetupVorbis::init(); 73 74 CL_DisplayWindow window("Boxes", winsize, winsize); 75 CL_SoundOutput output(44100); //44Khz sampling selected 76 77 CL_Surface *cursimg = new CL_Surface("cursor.tga"); 78 cursimg->set_alignment(origin_center); 79 CL_Surface *redpict = new CL_Surface("handtransp.tga"); 80 redpict->set_alignment(origin_center); 81 redpict->set_scale(float(spacing)/float(redpict->get_width()), 82 float(spacing)/float(redpict->get_height())); 83 CL_Surface *bluepict = new CL_Surface("circlehandtransp.tga"); 84 bluepict->set_alignment(origin_center); 85 bluepict->set_scale(float(spacing) / float(bluepict->get_width()), 86 float(spacing) / float(bluepict->get_height())); 87
The init() method does mostly what you would expect from game setup: initializes needed subsystems of ClanLib for graphics and sound (lines 68-72) and then constructs a window to be used for all display graphics (line 75).
CL_Surface (lines 77-87) is a 2D bitmap class used for cursors, the blue-filled square, and the red-filled square (in other words, indicating that red won this square).
TGA files are bitmap file formats from the now obscure AT&T Targa Graphics Adapter (TGA). ClanLib has an integrated copy of PNG library, so it can read and write most popular bitmap file formats.
Next, you must initialize the board to an empty state (lines 87-103) and perform similar housekeeping for other game counters.
89 90 redturn = true; 91 curs.vert = false; 92 fullup = false; 93 curs.x = curs.y = 1; 94 95 srand(CL_System::get_time()); // Seed random number // generator 96 97 for (int x = 0; x < boardsize - 1; x++) { 98 for (int y = 0; y < boardsize; y++) 99 hor[x][y] = ver[y][x] = false; 100 101 for (int y = 0; y < boardsize - 1; y++) 102 squares[x][y] = off; 103 104
An interesting aspect of ClanLib is that it eschews the traditional callback model used in many frameworks for the “signals and slots” model. This has been popularized by the Boost C++ library and implemented in QT (among other frameworks). Signals represent callbacks with multiple targets, and are also called publishers or events in similar systems. Signals are connected to some set of slots, which are callback receivers (also called event targets or subscribers), which are called when the signal is “emitted.” Signals have the great advantage of being typesafe and they avoid the inevitable cast-a-thon seen in traditional frameworks.
Signals and slots are managed, in that signals and slots (or, more properly, objects that occur as part of the slots) track all connections and are capable of automatically disconnecting signal/slot connections when either is destroyed. This enables the user to make signal/slot connections without expending great effort to manage the lifetimes of those connections with regard to the lifetimes of all objects involved. In line 105, you just want to trap all the keypress (“down”) events and insure your own inputHandler() is used (see lines 168-216).
105 CL_Slot keypress = CL_Keyboard::sig_key_down().connect(this, &Boxes::inputHandler);
Now, you are ready to initialize the musical portion of the program. First, you load up a CL_SoundBuffer with a waveform (“binary”) music file and then prepare a session handle so it’s ready to play. Next, you apply a fade filter to asynchronously bring the music from zero to 60 percent volume over the course of five seconds (lines 108-112).
106 CL_SoundBuffer *music = new CL_SoundBuffer("linemusic.ogg"); 107 CL_SoundBuffer_Session session = music->prepare(); 108 CL_FadeFilter *fade = new CL_FadeFilter(0.0f); 109 session.add_filter(fade); 110 session.set_looping(true); 111 session.play(); 112 fade->fade_to_volume(0.6f, 5000); 113 }
The drawBoard() method draws the dotted grid pattern where line segments lie, the tomato red and cornflower blue squares that each player has won, and last but not least the simulated cursor. The most important line of code here is line 165. CL_Display::flip() swaps the front buffer and back buffers. The back buffer is where you draw all your graphics during the frame, and the front buffer is what displays on the screen.
115 void Boxes::drawBoard() 116 { 117 CL_Display::clear(redturn ? CL_Color::red : CL_Color::blue); 118 CL_Display::fill_rect(CL_Rect(border/2, border/2, 119 winsize - border/2, winsize - border/2), CL_Color::black); 120 121 // Draw squares 122 for (int x = 0; x < boardsize - 1; x++) 123 for (int y = 0; y < boardsize - 1; y++) { 124 if (squares[x][y] == red) { 125 CL_Display::fill_rect(CL_Rect(x * spacing + border, y * spacing + border, x * spacing + border + spacing, 127 y * spacing + border + spacing), CL_Gradient(CL_Color::red, 128 CL_Color::red, CL_Color::tomato, CL_Color::tomato)); 129 redpict->draw(x * spacing + border + spacing / 2, 130 y * spacing + border + spacing / 2); 131 } 132 else if (squares[x][y] == blue) { 133 CL_Display::fill_rect(CL_Rect(x * spacing + border, 134 y * spacing + border, x * spacing + border + spacing, 135 y * spacing + border + spacing), CL_Gradient(CL_Color::blue, 136 CL_Color::blue, CL_Color::cornflowerblue, CL_Color::cornflowerblue)); 137 bluepict->draw(x * spacing + border + spacing / 2, y * spacing + border + spacing / 2); 139 } 140 } 141 142 // Draw lines 143 for (int x = 0; x < boardsize; x++) { 144 for (int y = 0; y < boardsize - 1; y++) { 145 if (ver[x][y]) CL_Display::draw_line(x * spacing + border, 146 y * spacing + border, x * spacing + border, 147 y * spacing + border + spacing, CL_Color::yellow); 148 if (hor[y][x]) CL_Display::draw_line(y * spacing + border, 149 x * spacing + border, y * spacing + border + spacing, x * spacing + border, CL_Color::yellow); 151 } 152 } 153 154 // Draw grid 155 for (int x = 0; x < boardsize; x++) 156 for (int y = 0; y < boardsize; y++) 157 CL_Display::draw_rect(CL_Rect(x * spacing + border, 158 y * spacing + border, x * spacing + border + 2, 159 y * spacing + border + 2), CL_Color::white); 160 161 //Draw cursor 162 if (curs.vert) cursimg->draw((curs.x - 1) * spacing + border, int((curs.y - 0.5) * spacing + border)); 163 else cursimg->draw(int((curs.x - 0.5) * spacing + border), (curs.y - 1) * spacing + border); 164 165 CL_Display::flip(); 166 }
You installed the inputHandler() to watch the key down signal back on line 105. This function handles the cumbersome details of turning keystrokes into game moves, the most significant being SPACE or ENTER to signify a selection by the current player (lines 200-210). Then, you check to see whether you completed a “square” and return control to the same player if you’ve done so.
168 void Boxes::inputHandler(const CL_InputEvent &i) 169 { 170 if (redturn) { 171 switch(i.id) { 172 case CL_KEY_LEFT: 173 case CL_KEY_G: 174 if (curs.x > 1) curs.x--; 175 break; 176 case CL_KEY_RIGHT: 177 case CL_KEY_J: 178 if (curs.x < boardsize) curs.x++; 179 break; 180 case CL_KEY_UP: 181 case CL_KEY_Y: 182 if (!curs.vert && curs.y > 1) { 183 curs.y--; 184 curs.vert = !curs.vert; 185 } 186 else if (curs.vert) curs.vert = false; 187 break; 188 case CL_KEY_DOWN: 189 case CL_KEY_H: 190 if (curs.vert && curs.y < boardsize) { 191 curs.y++; 192 curs.vert = !curs.vert; 193 } 194 else if (!curs.vert) curs.vert = true; 195 break; 196 } 197 if (curs.x == boardsize && !curs.vert) curs.x--; 198 if (curs.y == boardsize && curs.vert) curs.vert = false; 199 200 if (i.id == CL_KEY_SPACE || i.id == CL_KEY_ENTER) { 201 if (curs.vert) { 202 if (!ver[curs.x-1][curs.y-1]) { 203 ver[curs.x-1][curs.y-1] = true; 204 if (!findsquares()) redturn = !redturn; 205 } 206 } 207 else { 208 if (!hor[curs.x-1][curs.y-1]) { 209 hor[curs.x-1][curs.y-1] = true; 210 if (!findsquares()) redturn = !redturn; 211 } 212 } 213 } 214 } 215 }
Lastly, the endOfGame() method computes the final score. Remember that the game is not over until the board is full (see line 48) or someone resigns by hitting the ESC key (see line 46). Last of all, you fade out the volume to 0 percent over the course of one second.
217 void Boxes::endOfGame() 218 { 219 // Count scores 220 int redscore, bluescore; 221 redscore = bluescore = 0; 222 for (int x = 0; x < boardsize - 1; x++) 223 for (int y = 0; y < boardsize - 1; y++) { 224 if (squares[x][y] == red) redscore++; 225 else if (squares[x][y] == blue) bluescore++; 226 } 227 228 cout << "Red: " << redscore << "nBlue: " << bluescore << endl; 229 if (bluescore != redscore) 230 cout << (bluescore > redscore ? "Blue" : "Red") << " player winsn"; 231 else cout << "It was a tien"; 232 233 if (fullup) { 234 fade->fade_to_volume(0.0f, 1000); 235 CL_System::sleep(1000); 236 } 237 }
Stay Tuned!
Be sure to come back next week for the fifth and final installment of Cross-Platform Game Development for C++ Developers.
About the Author
Victor Volkman has been writing for C/C++ Users Journal and other programming journals since the late 1980s. He is a graduate of Michigan Tech and a faculty advisor board member for Washtenaw Community College CIS department. Volkman is the editor of numerous books, including C/C++ Treasure Chest and is the owner of Loving Healing Press. He can help you in your quest for open source tools and libraries; just drop an e-mail to sysop@HAL9K.com.