歡迎來到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 *imageData; // 圖片數據 (最大 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 imageSize; // 用來圖片大小的存儲
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); // 若有任何錯誤, 關掉文件
return FALSE; // 返回錯誤
}
}
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 得到字節每象素
imageSize = texture->width*texture->height*bytesPerPixel; // 計算 所需內存為 TGA 數據
texture->imageData=(GLubyte *)malloc(imageSize); // 分配 內存 為 TGA 數據
if( texture->imageData==NULL || // 這個內存是否存在?
fread(texture->imageData, 1, imageSize, file)!=imageSize) // 圖片大小與保留內存的大小想等 ?
{
if(texture->imageData!=NULL) // 圖片數據的調入
free(texture->imageData); // 若成功, 釋放圖象數據
fclose(file); // 關掉文件
return FALSE; // 返回錯誤
}
for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel) // 在圖象數據里循環
{ // 交換第1和第3 Bytes (’紅’red 和 ’藍’blue)
temp=texture->imageData[i]; // 臨時存儲 圖象的 ’i’
texture->imageData[i] = texture->imageData[i + 2]; // 設 第1 Byte 得到變量 第3 Byte
texture->imageData[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].imageData);
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].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);
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)
{
miss+=1;
object[loop].hit=TRUE;
}
// 如果到達下邊界,你沒有擊中,則增加丟失的目標數
if ((object[loop].y<-2.0f) && (object[loop].dir==3) && !object[loop].hit)
{
miss+=1;
object[loop].hit=TRUE;
}
//如果到達左邊界,你沒有擊中,則方向變為向下
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);
glEnd();
增加幀數,如果大于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); // 大地材質
glBegin(GL_QUADS);
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);
glEnd();
繪制我們的靶子
DrawTargets(); // 畫我們的靶子
glPopMatrix();
下面的代碼繪制我們的十字光標
// 十字光標 (在光標里)
RECT window; // 用來存窗口位置
GetClientRect (g_window->hWnd,&window); // 取窗口位置
glMatrixMode(GL_PROJECTION);
glPushMatrix();
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) . 玩家失掉的越多, 士氣越低.
if (game) // 游戲是否結束?
glPrint(490,10,"GAME OVER"); // 結束消息
else
glPrint(490,10,"Morale: %i/10",10-miss); // 輸出剩余生命
最后做的事我們選投影矩陣, 恢復(取出) 我們的矩陣返回到前一個情形, 設矩陣模式為 modelview ,刷新緩沖區 ,使所有物體被渲染.
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glFlush();
}
這課程是多次熬夜的成果, 許多的時間用來編碼和寫 HTML. 在這一課結束的時候你應你會學會怎樣picking, sorting, alpha blending and alpha testing 工作. 制做點和軟件. 每一個游戲, 到精選的GUI’們.最好的未來是制做時你不用記錄物體. 你給一個名字和碰撞 . 這很簡單! 用alpha 通道和alpha 測試你能使物體完全顯示, 或漏出一些. 結果是很好, 你不用擔心關于顯示物體的材質, 除非你不顯示他們! 同以往一樣, 我希望你喜歡這個課程,愿看到一些好的游戲或好的項目從這個課程誕生.如果你有什么問題或找到錯誤,讓我知道 ... 我僅是一個普通人 :)
我將花大量的時間加入東西像物理系統, 更多圖, 更多聲音, 等. 雖然只是一個課程! 我不寫不按車燈和車輪. 我寫這個用盡量不混亂的方法教你 OpenGL . 我希望看到一些嚴謹的修改. 若你找一些cool的課程發給我一份. 若是好的修改我將放到下載頁. 若有足夠充分的修改我會專注修改這個課程的版本! 我在這里給你一個起點. 剩下的靠你了 :)
要點: 這是很重要的,稱為glTexImage2D 你設為兩種格式國際 GL_RGBA. 否則 alpha blending 將不工作!