C++跨平台遊戲開發之ClanLibSDK

  一、 簡介ClanLib是一個主要針對遊戲開發者的跨平台C++框架。盡管API主要爲遊戲開發設計,你照樣可以輕易地使用ClanLib來開發一個科學的3D可視化工具或多媒體應用程序(例如Gecko多媒體系統)。ClanLib擁有各種API-2D和3D圖形,聲音,網絡,I/O,輸入,GUI以及資源治理。它還提供透明的OpenGL支持,因此你可以使用本機OpenGL命令而讓ClanLib處理依靠于操作系統的窗口治理和其它一切事情。ClanLib通過DirectX或簡單的Direct Media Layer(一平台獨立的多媒體庫)生成2D圖形。ClanLib遊戲主頁上列舉了約50多個開發非常成功的遊戲,包括以2D和3D形式完成的難題、策略以及射手類遊戲。例如,Asteroid Arena(見圖1)使用了ClanLib和OpenGL技術,實現了勝人一籌的經典街機遊戲。
  
C++跨平台遊戲開發之ClanLibSDK

  圖1.Asteroid Arena屏幕快照
  ClanLib可以工作在Windows,linux和MacOS操作系統之上,並且提供源碼級的zip或tar文件支持。Windows開發者可以使用微軟Visual Studio,Borland C++或者MinGW(小型GNU for Windows)編譯器和環境。第三方的對于Ruby和Perl語言的綁定支持也是可用的。可選的特效程序包括一個Lua插件(流行的小腳本編程語言)和FreeType(一個免費的TrueType字體庫)。
  二、 ClanLib特征集
  在具體使用API之前,讓我們看一下ClanLib的主要特征:
  ·基本跨平台運行時刻庫(GUI,多線程,文件I/O,等等)
  ·基于模板的C++信號/槽庫(類型安全的回調/代理)
  ·綜合的資源治理
  ·聲音混合器支持。WAV文件,Ogg Vorbis,以及由MikMod庫(MOD,S3M,XM,等等)支持的任何類型文件
  ·文檔對象模型(DOM)xml分析器支持
  ·高級2D圖形API,支持OpenGL,DirectX和SDL作爲著色目標
  ·高性能的批量著色引擎,當用OpenGL著色2D時
  ·2D碰撞檢測
  ·2D精靈動畫支持
  ·高度可定制的GUI框架
  ·從低級到高級的網絡庫接口
  三、 ClanLib基本的遊戲模型
  現在,讓我們仔細分析一下ClanLib API模型。我發現最好的教程是一個完全自解釋的示例程序。具體地,讓我們分析一下Luke Worth的盒子遊戲,這是一個有兩個玩家的紙和鉛筆遊戲(見圖2)。這個盒子遊戲包含一些格子點,在任意兩點間玩家都可以畫線。誰用最後一條線畫成一個封裝的矩形,誰就得一分,並進入到下一輪中。
  
C++跨平台遊戲開發之ClanLibSDK

  圖2.一個進行中的盒子遊戲,得分情況是藍8/紅3
  我特意使程序的main函數盡可能簡短,這樣我們可能集中注重力于高亮處的"遊戲循環":
  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 }
  關于這個應用程序,應注重的第一事情是main()函數(見行41)並不是一個最頂層的函數,而是嵌入到一個從CL_ClanApplication派生的對象中。該對象封裝了不少難以避免的平台依靠性-這可能包含一個傳統的::main()實現(例如在Win32應用程序中必須使用WinMain())。
  而且還應注重,事實上所有的可執行的代碼(行43-58)被封裝在一個try{}/catch{}異常處理器塊中。假如需要的話,ClanLib將引發異常,你可以重啓一遊戲,等等。基本上,所有的遊戲邏輯包含在init(),drawBoard(),endOfGame()和inputHandler()這幾個方法中。假如board不再移動(fullup==true),則退出遊戲循環(行48)。CL_System::keep_alive()更新所有的輸入和系統事件(象關閉窗口或者移動它)。這在老式的Win16 API ::Yield()或者Linux上的sleep()中將會釋放CPU周期。
  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采樣
  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
  這裏的init()方法完成大部分的遊戲初始化工作。當然,在此需要ClanLib子系統以用于處理圖形和聲音(行68-72),然後構建一個窗口用于顯示所有的圖形(行75)。
  
  
  CL_Surface(行77-87)是一個2D位圖類,用于繪制光標,用藍色填充的方格和用紅色填充的方格。
  TGA文件是一種位圖文件格式。ClanLib有一個集成的PNG庫,因此它可以讀寫最流行的位圖文件格式化。
  下一步,你必須把板子初始化成一個空狀態(行87-103)並執行類似的其它的清理工作以實現新的遊戲計數器。
  89
  90 redturn = true;
  91 curs.vert = false;
  92 fullup = false;
  93 curs.x = curs.y = 1;
  94
  95 srand(CL_System::get_time()); //啓動隨機數字生成器
  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
  ClanLib的一個非凡突出的方面是它避開傳統型應用于許多框架中的回調模型,而引入了"信號和槽"模型。這種模型廣泛應用于Boost C++庫中,並在QT中得到實現。信號代表具有多個目標的回調函數,又在一些類似的系統中稱作"出版者"或者"事件"。信號被連接到一些槽上,它們是回調函數接收器(也稱作事件目標或者訂戶),當信號被"發出"時即被調用。信號具有類型安全的優點,它們避開了在傳統型的框架中的不可避免的cast操作。
  信號和槽被統一治理。在信號和槽中(或者更准確些說是,作爲槽的一部分出現的對象)跟蹤所有的連接,並當任何其一被破壞時能夠自動地斷開信號/槽連接。這能夠使用戶建立信號/槽連接而不需要花費多大的代價來治理那些連接以及所有包含于其中的對象的生命周期。在行105中,你只要捕捉所有的鍵擊("down")事件並確保使用了你自己的inputHandler()(見行168-216)。
  105 CL_Slot keyPRess =
  CL_Keyboard::sig_key_down().connect(this,
  &Boxes::inputHandler);
  現在,你將開始初始化程序的音樂部分。首先,你用一個.wav格式的("binary")音樂文件裝載一個CL_SoundBuffer,然後預備一個會話句柄以爲玩遊戲之用。下一步,你應用一個淡入淡出過濾器來異步地調整音量-在五秒(行 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 }
  drawBoard()方法繪制線段所在的點畫格子圖案,如,每個玩家贏得的紅色的西紅柿和藍色的矢車菊框出的方格,還有模擬的光標。而最重要的代碼行是第165行。CL_Display::flip()交換前後台緩沖區。後台緩沖區是在該幀中你繪制所有圖形的地方,而前台緩沖區是顯示在屏幕上的內容。
  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 //畫方框
  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 //畫線
  
   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 //畫格子
  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 //畫光標
  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 }
  你安裝的inputHandler()函數用于觀察在行105的按鍵信號。這個函數負責處理細節問題-把鍵擊變成遊戲運動,還有最重要的空格或者回車鍵-用于指示當前玩家的一個選擇(行200-210)。然後,你要檢查一下是否已完成了一個"方形"並把控制返回到原來的玩家。
  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 }
  最後,由endOfGame()方法計算最後的得分。記住遊戲還沒有結束,直到板子滿了爲止(見行48)或者某人通過按下ESC鍵(見行46)退出。最後,你用大約1秒的時間把音量淡出到0。
  217 void Boxes::endOfGame()
  218 {
  219 // 計數得分
  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 wins\n";
  231 else cout << "It was a tie\n";
  232
  233 if (fullup) {
  234 fade->fade_to_volume(0.0f, 1000);
  235 CL_System::sleep(1000);
  236 }
  237 }