天天看點

NeHe OpenGL第三十二課:拾取遊戲

NeHe OpenGL第三十二課:拾取遊戲

NeHe OpenGL第三十二課:拾取遊戲
NeHe OpenGL第三十二課:拾取遊戲

拾取, Alpha混合, Alpha測試, 排序:

這又是一個小遊戲,交給的東西會很多,慢慢體會吧

歡迎來到32課. 這課大概是在我所寫作已來最大的一課. 超過1000 行代碼和約1540行的HTML. 這也是第一課用到我新的NeHeGL 基本代碼. 這課寫了很長時間, 但我想他是值得期待的. 一些知識點用到是: Alpha 混合, Alpha 測試, 讀取滑鼠, 同時用到Ortho 和透視, 顯示客戶滑鼠, 按深度排列物體, 動畫幀從單張材質圖 和更多要點, 你将學到更多精選的内容!

最初的版本是在螢幕上顯示三個物體,當你單擊他們将改變顔色. 很有趣!?! 不怎樣! 象往常一樣, 我想給你們這些家夥留下一個超極好的課程. 我想使課程有趣, 豐富,當然..美觀. 是以, 經過幾個星期的編碼之後, 這課程完成了! 即使你不編碼,你仍會喜歡這課. 這是個完整的遊戲. 遊戲的目标是射擊更多的靶子, 在你失去一定數的靶子後,你将不能再用滑鼠單擊物體.

我确信會有批評,但我非常樂觀對這課! 我已在從深度裡選擇和排序物體這個主題裡找到快樂!

一些需要注意的代碼. 我僅僅會在lesson32.cpp裡讨論. 有一些不成熟的改動在 NeHeGL 代碼裡. 最重要的改動是我加入滑鼠支援在 WindowProc(). 我也加入 int mouse_x, mouse_y 在存滑鼠運動. 在 NeHeGL.h 以下兩條代碼被加入: extern int mouse_x; & extern int mouse_y;

課程用到的材質是用 Adobe Photoshop 做的. 每個 .TGA 檔案是32位圖檔有一個alpha 通道. 若你不确信自已能在一個圖檔加入alpha通道, 找一本好書,上網,或讀 Adobe Photoshop幫助. 全部的過程非常相似,我做了透明圖在透明圖課程. 調入你物體在 Adobe Photoshop (或一些其它圖形處理程式,且支援alpha 通道). 用選擇顔色工具選你圖檔的背景. 複制選區. 建立一個圖. 粘貼生成新檔案. 取消圖檔選擇,你圖的背景應是黑色. 使周圍是白色. 選全部圖複制. 回到最初的圖且建一個alpha 通道. 粘貼黑和白透明圖你就完成建立alpha通道.存圖檔為32位t .TGA檔案. 使确定儲存透明背景是選中的,儲存!

如以往我希望你喜歡這課程. 我感興趣你對他的想法. 若你有些問題或你發現一些問題,告訴我. 我匆忙的完成這課程 是以若你發現哪部分很難懂,給我發些郵件,然後我會用不同的方式或更詳細的解釋!

#include <windows.h>        

#include <stdio.h>        

#include <stdarg.h>        

#include <time.h>        

#include "NeHeGL.h"       

在第1課裡, 我提倡關于适當的方法連接配接到 OpenGL 庫. 在 Visual C++ 裡點選’項目’,設定,連接配接項. 移下到 對象/庫 子產品 加入 OpenGL32.lib, GLu32.lib 和 GLaux.lib. 預編譯一個需要的庫的失敗将使編譯器找出所出的錯誤. 有時你不想發生! 使事情更壞, 若你僅僅預編譯庫在debug 模式, 和有人試在release 模式建立你程式... 更多的錯誤. 有許多人看代碼. 他們大多數是新程式員. 他們取到你的代碼, 試着編譯. 他們得到錯誤, 删掉代碼,移走.

下而的代碼告訴編譯者去連接配接需要的庫. 一點多些的字, 但少些以後的頭痛. 在這個課程, 我們将連接配接 OpenGL32 庫,GLu32庫 和 WinMM庫 (用來放音樂). 在這課程我們會調入 .TGA 檔案,是以我們不用 GLaux庫.

#pragma comment( lib, "opengl32.lib" )      // 在連結時連接配接Opengl32.lib庫

#pragma comment( lib, "glu32.lib" )       // 連結glu32.lib庫

#pragma comment( lib, "winmm.lib" )       // 連結winmm.lib庫

下而的3 行檢查若 CDS_FULLSCREEN 已被你的編譯器定義. 若還沒被定義, 我們給 CDS_FULLSCREEN 為 4. 馬上你完全部丢掉... 一些編譯器不給 CDS_FULLSCREEN 變量,将傳回一個錯誤,但是 CDS_FULLSCREEN 是有用的! 防止出錯消息, 我們檢查若 CDS_FULLSCREEN 是否定義,若出錯, 我們定義他. 使每人生活更簡單.

我們再定義 DrawTargets函數, 為視窗和按鍵設變量. 你若不懂定義,讀一遍MSDN術語表.保持清醒, 我不是教 C/C++, 買一本好書若你對非gl代碼要幫助!

#ifndef  CDS_FULLSCREEN       

#define  CDS_FULLSCREEN 4      

#endif         

void DrawTargets();        

GL_Window* g_window;

Keys*  g_keys;

下面的代碼是使用者設定變量. base 是将用到的字型顯示清單的開始清單值. roll 是将用到的移動的大地和建立旋轉的雲. level 應是級别 (我們開始是 1級). miss 保留失去了多少物體. 他還用來顯示使用者的士氣(不丢失意味着高士氣). kills 保留每級打到多少靶子. score 會儲存運作時打中的總數, 同時用到結束比賽!

最後一行是讓我們通過結構比較的函數. 是等待qsort 最後參數到 type (const *void, const *void).

// 使用者定義的變量

GLuint  base;        // 字型顯示清單

GLfloat  roll;        // 旋轉的雲

GLint  level=1;        // 現在的等級

GLint  miss;        // 丢失的數

GLint  kills;        // 打到的數

GLint  score;        // 目前的分數

bool  game;        // 遊戲是否結束?

typedef int (*compfn)(const void*, const void*);     // 定義用來比較的函數

現在為我們物體的結構. 這個結構存了所有一個物體的資訊. 旋轉的方向, 若被打中, 在螢幕的位置, 等等.

一個快速運動的變量... rot 我想讓物體旋轉特别的方向. hit 若物體沒被打中将是 FALSE . 若物體給打中或飛出, 變量将是 TRUE.

變量frame 是用來存我們爆炸動畫的周期. 每一幀改變增加一個爆炸材質. 在這課有更多在不久.

儲存單個物體的移動方向, 我們用變量 dir. 一個dir 能有4 個值: 0 - 物體左移, 1 - 物體右移, 2 - 物體上移 和最後 3 - 物體下移

texid 能是從0到4的數. 0 表示是藍面材質, 1 是水桶材質, 2 是靶子的材質 , 3 是 可樂的材質 和 4 是 花瓶 材質. 最近在調入材質的代碼, 你将看到先前5種材質來自目标圖檔.

x 和 y 兩者都用來記屏模上物體的位置. x 表示物體在 x-軸, y 表示物體在 y-軸.

物體在z-軸上的旋轉是記在變量spin. 在以後的代碼, 我們将加或減spin基數在旅行的方向上.

最後, distance 儲存我們物體到螢幕的距離. 距離是極端重要的變量, 我們将用他來計算螢幕的左右兩邊, 而且在對象關閉之前排序物體,畫出物體的距離.

struct objects {

 GLuint rot;        // 旋轉 (0-不轉, 1-順時針轉, 2-逆時針轉)

 bool hit;        // 物體碰撞?

 GLuint frame;        // 目前爆炸效果的動畫幀

 GLuint dir;        // 物體的方向 (0-左, 1-右, 2-上, 3-下)

 GLuint texid;        // 物體材質 ID

 GLfloat x;        // 物體 X 位置

 GLfloat y;        // 物體 Y 位置

 GLfloat spin;        // 物體旋轉

 GLfloat distance;        // 物體距離

};

解釋下面的代碼沒有真正的結果. 我們在這課調入TGA圖代替bitmaps圖檔. 下面的用來表示TGA圖檔資料的結構是盡可能好的 . 若你需要詳細的解釋下面的代碼,請讀關于調入TGA 檔案的課程.  

typedef struct         // 建立一個結構

{

 GLubyte *p_w_picpathData;       // 圖檔資料 (最大 32 位)

 GLuint bpp;        // 圖檔顔色深度 每象素

 GLuint width;        // 圖檔寬度

 GLuint height;        // 圖檔高度

 GLuint texID;        // 貼圖材質 ID 用來選擇一個材質

} TextureImage;         // 結構 名稱

緊接下面的代碼為們10個材質和個30物體留出空間. 若你打算在遊戲裡加更多物體,得增加這個變量到你想到的數 

TextureImage textures[10];       // 定義10個材質

objects object[30];       // 定義 30 個物體

我不想限制每個物體的大小. 我想瓶子(vase)比can高, 我想水桶bucket比瓶子寬. 去改變一切是簡單的, 我建了一個結構存物體的寬和高.

我然後在最後一行代碼中設每個物體的寬高. 得到這個coke cans的寬, 我将檢查size[3].w. 藍面是 0, 水桶是 1, 和靶子是 2, 等. 寬度表現在 w. 使有意義?

struct dimensions {        // 物體維數

 GLfloat w;        // 物體寬

 GLfloat h;        // 物體高

// 每個物體的大小: 藍面, 水桶, 靶子, 可樂, 瓶子

dimensions size[5] = { {1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f} };

下面是大段代碼是調入我們 TGA 圖檔和轉換他為材質. 這是同我在第25課所用的一樣的代碼,你可回去看一看.

我用TGA 圖檔的原因是他們是有alpha 通道的. 這個alpha 通道告訴 OpenGL 哪一部分圖是透明的,哪一部分是白底. alpha 通道是被建立在圖檔處理程式, 并儲存在.TGA圖檔裡面. OpenGL 調入圖檔, 能用alpha 通道設定圖檔中每個象素透明的數量.

bool LoadTGA(TextureImage *texture, char *filename)     // 調入一個TGA 檔案到記憶體

 GLubyte  TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0};   // (未)壓縮的 TGA 頭

 GLubyte  TGAcompare[12];      // 用來比較 TGA 頭

 GLubyte  header[6];      // 首先 6 個有用的位元組

 GLuint  bytesPerPixel;      // 每象素位元組數在 TGA 檔案使用

 GLuint  p_w_picpathSize;      // 用來圖檔大小的存儲

 GLuint  temp;       // 臨時變量

 GLuint  type=GL_RGBA;      // 設定預設的 GL 模式 為 RBGA

 FILE *file = fopen(filename, "rb");      // 打開 TGA 檔案

 if( file==NULL ||       // 檔案是否已存在 ?

  fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) || // 是否讀出12個位元組?

  memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 ||   // 檔案頭是不是我們想要的 ?

  fread(header,1,sizeof(header),file)!=sizeof(header))   // 若正确則讀下 6 個 Bytes

 {

  if (file == NULL)       // 檔案是否已存在 ?

   return FALSE;      // 傳回錯誤

  else        // 否則

  {

   fclose(file);      // 若有任何錯誤, 關掉檔案

  }

 }

 texture->width  = header[1] * 256 + header[0];    // 定義 TGA 寬

 texture->height = header[3] * 256 + header[2];    // 定義 TGA 高

  if( texture->width <=0 ||      // 若 寬<=0

  texture->height <=0 ||      // 若 高<=0

  (header[4]!=24 && header[4]!=32))     // 若 TGA 是 24 or 32 位?

  fclose(file);       // 若有任何錯誤, 關掉檔案

  return FALSE;       // 傳回錯誤

 texture->bpp = header[4];      // 取 TGA 的位每象素 (24 或 32)

 bytesPerPixel = texture->bpp/8;      // 除以 8 得到位元組每象素

 p_w_picpathSize  = texture->width*texture->height*bytesPerPixel;  // 計算 所需記憶體為 TGA 資料

 texture->p_w_picpathData=(GLubyte *)malloc(p_w_picpathSize);    // 配置設定 記憶體 為 TGA 資料

 if( texture->p_w_picpathData==NULL ||      // 這個記憶體是否存在?

  fread(texture->p_w_picpathData, 1, p_w_picpathSize, file)!=p_w_picpathSize)  // 圖檔大小與保留記憶體的大小想等 ?

  if(texture->p_w_picpathData!=NULL)     // 圖檔資料的調入

   free(texture->p_w_picpathData);     // 若成功, 釋放圖象資料

  fclose(file);       // 關掉檔案

 for(GLuint i=0; i<int(p_w_picpathSize); i+=bytesPerPixel)    // 在圖象資料裡循環

 {         // 交換第1和第3 Bytes (’紅’red 和 ’藍’blue)

  temp=texture->p_w_picpathData[i];      // 臨時存儲 圖象的 ’i’

  texture->p_w_picpathData[i] = texture->p_w_picpathData[i + 2];   // 設 第1 Byte 得到變量 第3 Byte

  texture->p_w_picpathData[i + 2] = temp;     // 設第3 Byte 得到變量 ’temp’ (第1 Byte 變量)

 fclose (file);        // 關掉檔案

 // 建立一個貼圖材質從以上資料

 glGenTextures(1, &texture[0].texID);      // 生成 OpenGL 材質 ID

 glBindTexture(GL_TEXTURE_2D, texture[0].texID);    // 綁定我們的材質

 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  // 線過濾器

 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  // 線過濾器

 if (texture[0].bpp==24)       // 若 TGA 是24 位的

  type=GL_RGB;       // 設 ’type’ 為 GL_RGB

 glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height, 0, type, GL_UNSIGNED_BYTE, texture[0].p_w_picpathData);

 return true;        // 材質建立成功, 傳回正确

}

2D 字型材質代碼同我已在前一課用的是一樣的. 然而, 有一些小不同. 第一是你将看到僅僅唯一生成95 顯示清單. 若你看字型材質, 你會看到隻有 95 字母計算空間在圖檔頂,左. 第二個事是你将通知分在16.0f 為 cx 和 我們隻分在8.0f 為cy. 我這樣做的結果是因為字型材質是256 象素寬, 但僅僅一伴就高(128 象素). 是以計算cx 我們分為16.0f 和計算分cy 為一半(8.0f).

若你不懂下面的代碼, 回去讀17課. 建立字型的代碼的詳細解釋在第17課裡!

GLvoid BuildFont(GLvoid)       // 建立我們字型顯示清單

 base=glGenLists(95);      // 建立 95 顯示清單

 glBindTexture(GL_TEXTURE_2D, textures[9].texID);   // 綁我們字型材質

 for (int loop=0; loop<95; loop++)     // 循環在 95 清單

  float cx=float(loop%16)/16.0f;    // X 位置 在目前字母

  float cy=float(loop/16)/8.0f;    // Y 位置 在目前字母

  glNewList(base+loop,GL_COMPILE);    // 開始建立一個清單

   glBegin(GL_QUADS);     // 用四邊形組成每個字母

    glTexCoord2f(cx,         1.0f-cy-0.120f); glVertex2i(0,0); // 質地 / 點 座标 (底 左)

    glTexCoord2f(cx+0.0625f, 1.0f-cy-0.120f); glVertex2i(16,0); // 質地 / 點 座标 (底 右)

    glTexCoord2f(cx+0.0625f, 1.0f-cy);   glVertex2i(16,16); // 質地 / 點 座标 (頂 右)

    glTexCoord2f(cx,         1.0f-cy);   glVertex2i(0,16); // 質地 / 點 座标 (頂 左)

   glEnd();      // 完成建立我們的 四邊形 (字母)

   glTranslated(10,0,0);    // 移到字型的右邊

  glEndList();      // 完成建軍立這個顯示清單

 }        // 循環直到所有 256 完成建立

輸出的代碼也在第17課, 但已修改為在螢幕輸出我們的分數, 等級和士氣(不斷改變的值).  

GLvoid glPrint(GLint x, GLint y, const char *string, ...)    // 輸出在屏慕的位置

 char  text[256];      // 儲存在我們的字元串

 va_list  ap;       // 到清單的指針

 if (string == NULL)       // 若文字為空

  return;        // 傳回

 va_start(ap, string);       // 解析字元串

     vsprintf(text, string, ap);      // 轉換字元串

 va_end(ap);        // 結果的字元串

 glBindTexture(GL_TEXTURE_2D, textures[9].texID);    // 選擇我們字型材質

 glPushMatrix();        // 存觀看模式矩陣

 glLoadIdentity();        // 設觀看模式矩陣

 glTranslated(x,y,0);       // 文字輸出位置 (0,0 - 底 左-Bottom Left)

 glListBase(base-32);       // 選擇字型設定

 glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);    // 輸出顯示清單中的文字

 glPopMatrix();        // 取出以前的模式矩陣

這些代碼調用排序程式. 它比較距離在兩個結構并傳回-1 若第一個結構的距離小于第二個 , 1 i若 第一個結構的距離大于第二個 0 否則 (若 距離相等)  

int Compare(struct objects *elem1, struct objects *elem2)   // 比較 函數

   if ( elem1->distance < elem2->distance)     // 若 第一個結構的距離小于第二個

      return -1;        // 傳回 -1

   else if (elem1->distance > elem2->distance)    // 若 第一個結構的距離大于第二個

      return 1;        // 傳回1

   else         // 否則 (若 距離相等)

      return 0;        // 傳回 0

InitObject() 代碼是來建立每個物體. 我們開始設 rot 為 1. 這使物體順時針旋轉. 然後設爆炸效果動畫幀為0(我們不想爆炸效果從中間開始).我們下面設 hit 為 FALSE, 意思是物體還沒被擊中或正開如. 選一個物體材質, texid 用來給一個随機的變量從 0 到 4. 0是blueface 材質 和4 是 vase 材質. 這給我們5種随機物體.

距離變量是在-0.0f to -40.0f (4000/100 is 40)的随機數 . 當我們真實的畫物體,我們透過在螢幕上的另10 個機關. 是以當物體在畫時, 他們将畫從-10.0f to -50.0f 機關 在螢幕(不挨着, 也不離得太遠). 我分随機數為 100.0f 得到更精确的浮點數值.

在給完随機的距離之後, 我們給物體一個随機的 y . 我們不想物體低于 -1.5f, 否則他将低于大地, 且我們不想物體高于3.0f. 是以留在我們的區間的随機數不能高于4.5f (-1.5f+4.5f=3.0f).

去計算 x 位置, 用一些狡猾的數學. 用我們的距離減去15.0f . 除以2 減5*level. 再 減随機數(0.0f 到5) 乘level. 減 5*level rndom(0.0f to 5*level) 這是最進階.

選一個方向.

使事情簡單明白x, 寫一個快的例子. 距離是 -30.0f ,目前級是 1:

object[num].x=((-30.0f-15.0f)/2.0f)-(5*1)-float(rand()%(5*1));

object[num].x=(-45.0f/2.0f)-5-float(rand()%5);

object[num].x=(-22.5f)-5-{lets say 3.0f};

object[num].x=(-22.5f)-5-{3.0f};

object[num].x=-27.5f-{3.0f};

object[num].x=-30.5f;

開始在屏模上移 10 個機關 , 距離是 -30.0f. 其實是 -40.0f.用透視的代碼在 NeHeGL.cpp 檔案.

GLvoid InitObject(int num)       // 初始化一個物體

 object[num].rot=1;       // 順時針旋轉

 object[num].frame=0;      // 設爆炸效果動畫幀為0

 object[num].hit=FALSE;      // 設點選檢測為0

 object[num].texid=rand()%5;      // 設一個材質

 object[num].distance=-(float(rand()%4001)/100.0f);   // 随機距離

 object[num].y=-1.5f+(float(rand()%451)/100.0f);   // 随機 Y 位置

 // 随機開始 X 位置 基于物體的距離 和随機的延時量 (确定變量)

 object[num].x=((object[num].distance-15.0f)/2.0f)-(5*level)-float(rand()%(5*level));

 object[num].dir=(rand()%2);      // 選一個随機的方向

 檢查方向

 if (object[num].dir==0)       // 若随機的方向正确

  object[num].rot=2;       // 逆時針旋轉

  object[num].x=-object[num].x;     // 開始在左邊 (否定 變量)

 現在我們檢查texid 來找出所選的的物體. 若 texid 為0, 所選的物體是 Blueface . blueface 總是在大地上面旋轉. 确定開始時在地上的層, 我們設 y 是 -2.0f.

 if (object[num].texid==0)       // 藍色天空表面

  object[num].y=-2.0f;      // 總是在大地上面旋轉

下面檢查若texid 是 1. 這樣, 電腦所選物體的是 Bucket. bucket不從左到右運動, 它從天上掉下來. 首先我們不得不設 dir 是 3. 這告訴電腦我們的水桶bucket 是掉下來或向下運動.

我們最初的代碼假定物體從左到右運動. 因為bucket 是向下落的, 我們得不給它一個新的随機的變量 x . 若不是這樣, bucket 會被看不到. 它将不在左邊落下就在螢幕外面. 我們給它一個新的随機距離變量在螢幕上. 代替減去15, 我們僅僅減去 10. 這給我們一些幅度, 保持物體在螢幕??. 設我們的distance 是-30.0f, 從0.0f -40.0f的随機變量. 為什麼從 0.0f 到 40.0f? 不是從0.0f to -40.0f? 答案很簡單. rand() 函數總傳回正數. 是以總是正數. 另外,回到我們的故事. 我們有個正數 從0.0f 到 40.0f.我們加距離 最小 10.0f 除以 2. 舉個例子,設x變量為 15 ,距離是 -30.0f:

object[num].x=float(rand()%int(-30.0f-10.0f))+((-30.0f-10.0f)/2.0f);

object[num].x=float(rand()%int(-40.0f)+(-40.0f)/2.0f);

object[num].x=float(15 {assuming 15 was returned))+(-20.0f);

object[num].x=15.0f-20.0f;

object[num].x=-5.0f;

下面設y. 我們想水桶從天上下來. 我人不想穿過雲. 是以我們設 y 為 4.5f. 剛在去的下面一點.

 if (object[num].texid==1)      // 水桶(Bucket)

  object[num].dir=3;      // 下落

  object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);

  object[num].y=4.5f;     // 随機 X, 開始在屏模上方

我們想靶子從地面突出到天上. 我們檢查物體為 (texid 是 2). 若是, 設方向(dir) 是 2 (上). 用精确的數 x 位置.

我們不想target 開始在地上. 設 y 初值為-3.0f (在地下). 然後減一個值從0.0f 到 5 乘目前 level. 靶子不是立即出現. 在進階别是有延時, 通過delay, 靶子将出現在一個在另一個以後, 給你很少時間打到他們.

 if (object[num].texid==2)      // 靶子

  object[num].dir=2;      // 開始向上飛

  object[num].y=-3.0f-float(rand()%(5*level));   // 随機 X, 開始在下面的大地 + 随機變量

所有其它的物體從右到左旅行, 因而不必給任何變量付值來改變物體. 它們應該剛好工作在所給的随機變量.

現在來點有趣的材料! "為了alpha 混合技術正常的工作, 透明的原物必須不斷地排定在從後向前畫". 當畫alpha 混合物體是, 在遠處的物體是先畫的,這是非常重要的, 下面畫緊臨的上面的物體.

理由是簡單的... Z 緩沖區防止 OpenGL 從已畫好的混合東西再畫象素. 這就是為什麼會發生物體畫在透明混合之後而不再顯示出來. 為什麼你最後看到的是一個四邊形與物體重疊... 很不好看!

我們已知道每個物體的深度. 因而在初始化一個物體之後, 我們能通過把物體排序,而用qsort 函數(快速排序sort),來解決這個問題 . 通過物體排序, 我們能确信第一個畫的是最遠的物體. 這意味着當我們畫物體時, 起始于第一個物體, 物體通過用距離将被先畫. 緊挨着那個物體(晚一會兒畫) 将看到先前的物體在他們的後面, 再将适度的混合!

這文中的這行線注釋是我在 MSDN 裡發現這些代碼,在網上花時間查找之後找到的解答 . 他們工作的很好,允許各種的排序結構. qsort 傳送 4 個參數. 第一個參數指向物體數組 (被排序的數組d). 第二個參數是我們想排序數組的個數... 當然,我們想所有的排序的物體普遍的被顯示(各個level). 第三個參數規定物體結構的大不, 第四個參數指向我們的 Compare() 函數.

大概有更好的排序結構的方法, 但是 qsort() 工作起來... 快速友善,簡單易用!

這個是重要的知識點, 若你們想用 glAlphaFunc() 和 glEnable(GL_ALPHA_TEST), 排序是沒必要的. 然而, 用Alpha 功能你被限制在完全透明或完全白底混合, 沒有中間值. 用 Blendfunc()排序用一些更多的工作,但他顧及半透明物體.

// 排序物體從距離:我們物體數組的開始位址 *** MSDN 代碼修改為這個 TUT ***// 各種的數按// 各自的要素的// 指針比較的函數

 qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare );

初始化的代碼總是一樣的. 首先的現兩行取得我們window 的消息和我們建盤消息. 然後我們用 srand() 建一個基于時間的多樣化的遊戲. 之後我們調入 TGA 圖檔并用LoadTGA()轉換到材質 . 先前的 5個圖檔是将穿過螢幕的物體. Explode 是我們爆炸動畫, 大地和天空 彌補現場背景, crosshair是你在螢幕上看到表現滑鼠目前位置的十字光标, 最後, 用來顯示分數,标題和士氣值的字型的圖檔. 若任何調入圖檔的失誤,則到傳回 FALSE 值, 并程式結束. 值得注意的是這些基本代碼不是傳回整數型(INIT)的 FAILED 錯誤消息.

BOOL Initialize (GL_Window* window, Keys* keys)    // 任何 OpenGL 從這初始化

 g_window = window;

 g_keys  = keys;

 srand( (unsigned)time( NULL ) );     // 使随機化事件

 if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) ||   // 調入藍面材質

  (!LoadTGA(&textures[1],"Data/Bucket.tga")) ||   // 調入水桶材質

  (!LoadTGA(&textures[2],"Data/Target.tga")) ||   // 調入靶子材質

  (!LoadTGA(&textures[3],"Data/Coke.tga")) ||   // 調入 可樂材質

  (!LoadTGA(&textures[4],"Data/Vase.tga")) ||   // 調入 花瓶 材質

  (!LoadTGA(&textures[5],"Data/Explode.tga")) ||   // 調入 爆炸材質

  (!LoadTGA(&textures[6],"Data/Ground.tga")) ||   // 調入 地面 材質

  (!LoadTGA(&textures[7],"Data/Sky.tga")) ||   // 調入 天空 材質

  (!LoadTGA(&textures[8],"Data/Crosshair.tga")) ||  // 調入 十字光标 材質

  (!LoadTGA(&textures[9],"Data/Font.tga")))   // 調入 字元 材質

  return FALSE;      // 若調入失敗, 傳回錯誤

若所有圖檔調入成功則輪到材質, 我們能繼續初始化. 字型材質被調入, 因而保險能建立我們的字型. 我們跳入BuildFont()來做這些.

然後我們設定OpenGL. 背景色為黑, alpha 也設為0.0f. 深度緩沖區設為激活小于或等于測試.

glBlendFunc() 是很重要的一行代碼. 我們設混合函數(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). 這些加上alpha變量的螢幕上的混合物體存在物體的材質. 在設定混合模式之後, 我們激活blending(混合). 然後我們打開 2D 材質貼圖, 最後,打開 GL_CULL_FACE. 這是去除每個物體的後面( 沒有一點浪費在一些我們看不到的循環 ). 畫一些四邊形逆時針卷動 ,因而精緻而适當的面片.

早先的教程我談論使用glAlphaFunc()代替alpha 混合. 若你想用Alpha 函數, 注釋出的兩行混合代碼和不注釋的兩行在glEnable(GL_BLEND)之下. 你也能注釋出qsort()函數在 InitObject() 部分裡的代碼.

程式應該運作ok,但sky 材質将不在這. 因為sky的材質已是一個alpha 變量0.5f. 當早在我說關于Alpha函數, 我提及它隻工作在alpha 變量0 或 1. 若你想它出現,你将不得不修改sky的材質alpha 通道! 再則, 若你決定用Alpha 函數代替, 你不得排序物體.兩個方法都有好處! 再下而是從SGI 網站的快速引用:

"alpha 函數丢棄細節,代替畫他們在結構緩沖器裡. 是以排序原來的物體不是必須的 (除了一些其它像混合alpha模式是打開的). 不占優勢的是象素必須完全白底或完全透明".

 BuildFont();        // 建立我們的字型顯示清單

 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);     // 黑色背景

 glClearDepth(1.0f);       // 安裝深度緩沖器

 glDepthFunc(GL_LEQUAL);       // 深度的類型測試

 glEnable(GL_DEPTH_TEST);       // 打開深度測試

 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);    // 打開 Alpha 混合

 glEnable(GL_BLEND);       // 打開混合

 glAlphaFunc(GL_GREATER,0.1f);      // 設 Alpha 測試

 glEnable(GL_ALPHA_TEST);       // 打開 Alpha 測試

 glEnable(GL_TEXTURE_2D);       // 打開材質貼圖

 glEnable(GL_CULL_FACE);       // 去掉畫物體的背面

在程式的這段, 還沒有物體被定義,是以循環30個物體,每個物體都調InitObject(). 

 for (int loop=0; loop<30; loop++)     // 循環在 30 個物體Objects

  InitObject(loop);      // 初始化每個物體

 return TRUE;       // 傳回正确 (設初值成功)

在初始化代碼裡, 調入BuildFont() 建立95 的顯示清單. 是以這裡在程式結束前删掉95顯示清單 

void Deinitialize (void)       // 任何user 結束初始化從這

 glDeleteLists(base,95);       // 删掉所有95 字型顯示清單

現在為急速原始物體... 是實際被選?形锾宓拇??. 第一行為我們選擇物體的資訊配置設定記憶體. hits 是當選擇時碰撞迅檢測的次數.

void Selection(void)        // 這是選擇正确

 GLuint buffer[512];       //  設選擇緩沖

 GLint hits;        // 選擇物體的數

下面的代碼, 若遊戲結束(FALSE).沒有選任何, 傳回(exit). 若遊戲還在運作 (TRUE),用Playsound() 指令放射擊的聲間. 僅僅調Selection()的時間是在當已滑鼠按下時和每次按下按鍵調用時, 想放射擊的聲音. 聲音在放在 async 模式 ,是以放音樂是程式不會停.  

 if (game)        // 遊戲是否結束?

  return;       // 是,傳回, 不在檢測 Hits

 PlaySound("data/shot.wav",NULL,SND_ASYNC);    // 放音樂 Gun Shot

設視點. viewport[] 包括目前 x, y, 目前的視點(OpenGL Window)長度,寬度.

glGetIntegerv(GL_VIEWPORT, viewport) 取目前視點存在viewport[]. 最初,等于 OpenGL 視窗維數. glSelectBuffer(512, buffer) 說 OpenGL 用這個記憶體.

 // 視點的大小. [0] 是 <x>, [1] 是 <y>, [2] 是 <length>, [3] 是 <width>

 GLint viewport[4];

 // 這是設視點的數組在螢幕視窗的位置

 glGetIntegerv(GL_VIEWPORT, viewport);

 glSelectBuffer(512, buffer);      // 告訴 OpenGL 使我們的數組來選擇

存opengl的模式. 在這個模式什麼也不畫. 代替, 在選擇模式物體渲染資訊存在緩存.

下面初實化name 堆棧,通過調入glInitNames() 和glPushName(0). I它重要的是标記若程式不在選擇模式, 一個到glPushName()調用将忽略. 當然在選擇的模試, 但這一些是是緊記的.

 // 設 OpenGL 選擇模式. 将不畫東西. 物體 ID’的廣度放在記憶體

 (void) glRenderMode(GL_SELECT);

 glInitNames();        // 設名字堆棧

 glPushName(0);        // Push 0 (最少一個) 在棧上

之後, 不得不限制在光标的下面畫圖. 為了做這些得用到投影矩陣. 然後把它推到堆棧中.重設矩陣則用到 glLoadIdentity().

用gluPickMatrix()限制的畫. 第1個參數是目前滑鼠的 x-座标, 第2個參數是目前滑鼠的 y-座标, 然後寬和高的選區. 最後目前的 viewport[]. viewport[] 是指出視點的邊界. x 和_y 将在選區的中心

 glMatrixMode(GL_PROJECTION);      // 選投影矩陣

 glPushMatrix();        // 壓入投影矩陣

 glLoadIdentity();        // 重設矩陣

 //  這是建一個矩陣使滑鼠在螢幕縮放

 gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3]-mouse_y), 1.0f, 1.0f, viewport);

調入 gluPerspective() 應用透視矩陣 ,被gluPickMatrix()選擇矩陣限制所畫區域 .

打開modelview 矩陣,調用DrawTargets()畫我們的靶子. 畫靶子在DrawTargets() 而不在 Draw() 是因為僅僅想選擇物體的碰撞檢測且, 不是天空,大地,光标.

之後, 打開回到發射矩陣, 從堆棧中彈出矩陣. 之扣打開回到modelview 矩陣.

最後一行,回到渲染模式 因而物體畫的很真實的在螢幕上. hits 将采集gluPickMatrix()所需要取渲染的物體數 .

 // 應用透視矩陣

 gluPerspective(45.0f, (GLfloat) (viewport[2]-viewport[0])/(GLfloat) (viewport[3]-viewport[1]), 0.1f, 100.0f);

 glMatrixMode(GL_MODELVIEW);       // 選擇模型變換矩陣

 DrawTargets();        // 畫目标

 glMatrixMode(GL_PROJECTION);      // 選擇投影變換矩陣

 glPopMatrix();        // 取出投影矩陣

 glMatrixMode(GL_MODELVIEW);       // 選模式顯示矩陣

 hits=glRenderMode(GL_RENDER);      // 切換模式, 找出有多少

檢查若多于0 個hits 記錄. 若這樣, 設choose 為 第一個物體的名子. depth 取得它有多遠.

每個hit 分有4 個項目在記憶體. 第一,在名子堆棧上打擊發生時的數字 .第二, 所選物體的最小z值. 第三,所選物體的最大 z 值, 最後,在同一時間裡所選物體名子堆棧的内容 (物體的名子). 在這一課,我們僅對最小z值和物體名子感興趣.

 if (hits > 0)        // 若有大于0個 Hits

  int choose = buffer[3];     // 選擇第一物體

  int depth = buffer[1];      // 存它有多遠

做循環所有hits 使沒有物體在第一個物體旁邊. 否則, 兩個物體會重疊, 一個物體碰到另一個.當你射擊時, 重疊的物體會被誤選. 

  for (int loop = 1; loop < hits; loop++)    // 循環所有檢測到的物體

   // 對于其它的物體

   if (buffer[loop*4+1] < GLuint(depth))

   {

    choose = buffer[loop*4+3];    // 選擇更近的物體

    depth = buffer[loop*4+1];    // 儲存它有多遠

   }

若物體被選.

  if (!object[choose].hit)     // 如果物體還沒有被擊中

   object[choose].hit=TRUE;    // 标記物體象被擊中

   score+=1;      // 增加分數

   kills+=1;      // 加被殺數

如下

   if (kills>level*5)     // 已有新的級?

    miss=0;     // 失掉數回0

    kills=0;     // 設 Kills數為0

    level+=1;     // 加 Level

    if (level>30)    // 高過 30?

     level=30;    // 設 Level 為 30 (你是 God 嗎?)

void Update(DWORD milliseconds)       // 這裡用來更新

 if (g_keys->keyDown[VK_ESCAPE])      // 按下 ESC?

  TerminateApplication (g_window);     // 推出程式

如下  

 if (g_keys->keyDown[' '] && game)     // 按下空格鍵?

  for (int loop=0; loop<30; loop++)    // 循環所有的物體

   InitObject(loop);     // 初始化

  game=FALSE;      //設game為false

  score=0;       // 分數為0

  level=1;       // 級别為1

  kills=0;       // 殺敵數為0

  miss=0;       // 漏過數為0

 if (g_keys->keyDown[VK_F1])      // 按下f1?

  ToggleFullscreen (g_window);    // 換到全屏模式

 roll-=milliseconds*0.00005f;     // 雲的旋轉

 for (int loop=0; loop<level; loop++)     // 循環所有的物體

下面的代碼按物體的運動方向更新所有的運動 

  if (object[loop].rot==1)     

   object[loop].spin-=0.2f*(float(loop+milliseconds)); // 若順時針,則順時針旋轉

  if (object[loop].rot==2)

   object[loop].spin+=0.2f*(float(loop+milliseconds)); // 若逆時針,則逆時針旋轉

  if (object[loop].dir==1)     

   object[loop].x+=0.012f*float(milliseconds);  // 向右移動

  if (object[loop].dir==0)     

   object[loop].x-=0.012f*float(milliseconds);  // 向左移動

  if (object[loop].dir==2)     

   object[loop].y+=0.012f*float(milliseconds);  // 向上移動

  if (object[loop].dir==3)     

   object[loop].y-=0.0025f*float(milliseconds);  // 向下移動

下面的代碼處理當物體移動到邊緣處,如果你沒有擊中它的結果 

  // 如果到達左邊界,你沒有擊中,則增加丢失的目标數

  if ((object[loop].x<(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==0) && !object[loop].hit)

   miss+=1;      

   object[loop].hit=TRUE;     

  // 如果到達右邊界,你沒有擊中,則增加丢失的目标數

  if ((object[loop].x>-(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==1) && !object[loop].hit)

   object[loop].hit=TRUE;    

  // 如果到達下邊界,你沒有擊中,則增加丢失的目标數

  if ((object[loop].y<-2.0f) && (object[loop].dir==3) && !object[loop].hit)

  //如果到達左邊界,你沒有擊中,則方向變為向下

  if ((object[loop].y>4.5f) && (object[loop].dir==2))  

   object[loop].dir=3;    

下面的代碼在螢幕上繪制一個圖像 

void Object(float width,float height,GLuint texid)    // 畫物體用需要的寬,高,材質

 glBindTexture(GL_TEXTURE_2D, textures[texid].texID);   // 選合适的材質

 glBegin(GL_QUADS);       // 開始畫四邊形

  glTexCoord2f(0.0f,0.0f); glVertex3f(-width,-height,0.0f); 

  glTexCoord2f(1.0f,0.0f); glVertex3f( width,-height,0.0f); 

  glTexCoord2f(1.0f,1.0f); glVertex3f( width, height,0.0f); 

  glTexCoord2f(0.0f,1.0f); glVertex3f(-width, height,0.0f); 

 glEnd();        

下面的代碼繪制爆炸的效果 

 void Explosion(int num)      // 畫爆炸動畫的1幀

 float ex = (float)((object[num].frame/4)%4)/4.0f;   // 計算爆炸時生成的x的紋理坐标

 float ey = (float)((object[num].frame/4)/4)/4.0f;   // 計算爆炸時生成的y的紋理坐标

 glBindTexture(GL_TEXTURE_2D, textures[5].texID);   // 選擇爆炸的紋理

 glBegin(GL_QUADS);       

  glTexCoord2f(ex      ,1.0f-(ey      )); glVertex3f(-1.0f,-1.0f,0.0f); 

  glTexCoord2f(ex+0.25f,1.0f-(ey      )); glVertex3f( 1.0f,-1.0f,0.0f); 

  glTexCoord2f(ex+0.25f,1.0f-(ey+0.25f)); glVertex3f( 1.0f, 1.0f,0.0f);

  glTexCoord2f(ex      ,1.0f-(ey+0.25f)); glVertex3f(-1.0f, 1.0f,0.0f); 

增加幀數,如果大于63,則重置動畫 

 object[num].frame+=1;      // 加目前的爆炸動畫幀

 if (object[num].frame>63)      // 是否已完成所有的16幀?

  InitObject(num);      // 定義物體 (給新的變量)

畫靶子 

void DrawTargets(void)        // 畫靶子

 glLoadIdentity();       

 glTranslatef(0.0f,0.0f,-10.0f);      // 移入螢幕 20 個機關

 for (int loop=0; loop<level; loop++)      // 循環在 9 個物體

  glLoadName(loop);       // 給物體新名字

  glPushMatrix();       // 存矩陣

  glTranslatef(object[loop].x,object[loop].y,object[loop].distance); // 物體的位置 (x,y)

  if (object[loop].hit)      // 若物體已被點選

   Explosion(loop);      // 畫爆炸動畫

  else        

   glRotatef(object[loop].spin,0.0f,0.0f,1.0f);   // 旋轉物體

   Object(size[object[loop].texid].w,size[object[loop].texid].h,object[loop].texid); // 畫物體

  glPopMatrix();       // 彈出矩陣

下面的代碼繪制整個場景 

void Draw(void)        // 畫我們的現場

 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清除螢幕和深度緩沖

 glLoadIdentity();       // 重設矩陣

下面的代碼繪制飄動的天空,它由四塊紋理組成,每一塊的移動速度都不一樣,并把它們混合起來 

 glPushMatrix();        

 glBindTexture(GL_TEXTURE_2D, textures[7].texID);   // 選天空的材質

 glBegin(GL_QUADS);      

  glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f); 

  glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f); 

  glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f); 

  glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f); 

  glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  

  glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  

  glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);  

  glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);  

  glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,0.0f); 

  glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,0.0f); 

  glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f); 

  glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f); 

  glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);  

  glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);  

  glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  

  glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  

 glEnd();       

下面的代碼繪制地面 

 glBindTexture(GL_TEXTURE_2D, textures[6].texID);   // 大地材質

  glTexCoord2f(7.0f,4.0f-roll); glVertex3f( 27.0f,-3.0f,-50.0f); 

  glTexCoord2f(0.0f,4.0f-roll); glVertex3f(-27.0f,-3.0f,-50.0f); 

  glTexCoord2f(0.0f,0.0f-roll); glVertex3f(-27.0f,-3.0f,0.0f); 

  glTexCoord2f(7.0f,0.0f-roll); glVertex3f( 27.0f,-3.0f,0.0f); 

繪制我們的靶子 

 DrawTargets();        // 畫我們的靶子

 glPopMatrix();        

下面的代碼繪制我們的十字光标

 // 十字光标 (在光标裡)

 RECT window;        // 用來存視窗位置

 GetClientRect (g_window->hWnd,&window);     // 取視窗位置

 glMatrixMode(GL_PROJECTION);      

 glLoadIdentity();      

 glOrtho(0,window.right,0,window.bottom,-1,1);    // 設定為正投影

 glMatrixMode(GL_MODELVIEW);      

 glTranslated(mouse_x,window.bottom-mouse_y,0.0f);   // 移動到目前滑鼠位置

 Object(16,16,8);       // 畫十字光标

下面的代碼用來顯示幫助文字 

 // 遊戲狀态 / 标題名稱

 glPrint(240,450,"NeHe Productions");     // 輸出 标題名稱

 glPrint(10,10,"Level: %i",level);     // 輸出 等級

 glPrint(250,10,"Score: %i",score);     // 輸出 分數

如果丢失10個物體,遊戲結束 

 if (miss>9)        // 我們已丢失 10 個物體?

  miss=9;        // 限制丢失是10個

  game=TRUE;       // 遊戲結束

在下面的代碼裡, 我們檢視若game 是TRUE. 若 game 是TRUE, 我們輸出 ’GAME OVER’遊戲結束的消息. 若game 是false, 我們輸出 玩家的士氣morale (到10溢出). 士氣morale是被設計用來從10減去玩家失誤的次數(miss) . 玩家失掉的越多, 士氣越低.

  glPrint(490,10,"GAME OVER");    // 結束消息

 else

  glPrint(490,10,"Morale: %i/10",10-miss);   // 輸出剩餘生命

最後做的事我們選投影矩陣, 恢複(取出) 我們的矩陣傳回到前一個情形, 設矩陣模式為 modelview ,重新整理緩沖區 ,使所有物體被渲染.  

 glFlush();        

原文及其個版本源代碼下載下傳:

http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=32

繼續閱讀