Microsoft & .NETVisual C#Cross-Platform Game Development for C++ Developers, Part IV: ClanLib SDK

Cross-Platform Game Development for C++ Developers, Part IV: ClanLib SDK

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.

Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.
Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.

Latest Posts

Related Stories