|
文檔簡介: 在探討更深入的劇情處理以前,我們必須擁有輸出文字的能力。這次我們的目標是擷取WINDOW系統資源,並且與DirectX全螢幕遊戲相結合,你將會學習到如何有效快速地將字型套用到你的遊戲上面。
目錄: 掌握正確方向 WINDOW的字型 取得字型資訊 將字型設定給DC 規畫秀字的方式 整合一下 末語
文檔內容:
□ 掌握正確方向
記得我曾經說過,在DOS下開發遊戲,與WINDOW下的差別是很大的,曾經令人苦惱的問題,在WINDOW下都有更方便的解決方式,最明顯的例子就是音樂播放,以及我們這次討論的字型使用。在很多較老舊的遊戲書籍裡面,都會教你如何秀中文,比較常見的方法就是利用倚天中文的字型檔。並且還需要利用文字索引的技巧,以節省記憶體空間。這一切都過去了,等等你會看到我們在WINDOW下面的實作方式。這邊所謂「正確的方向」是指有效率的解決方式,當然早期的作法仍然可以適用于WINDOW環境。只是要額外付出許多代價就是了。
□ WINDOW的字型
在中文的WINDOW裡面,一安裝完畢就會有許多字型可以套用,其中的中文字型至少有明體與標楷體,這些字型屬于系統資源的一部份,任何應用程式皆可大方地使用。該如何使用呢?寫過WIN32 APP的人大抵上都知道WINDOW API中,有一個字型對話方塊,可以很方便的取的某個字型的資料,不過我不打算使用這個對話方塊函示,實際上自己動手更有彈性,也不會浪費多少時間,以下開始實作。
□ 取得字型資訊
一個應用程式如果沒有指定字型,則使用系統內定的字型。如果要使用標楷體字型的話,必須先取得此種字型的資訊,當然還可以指定字型的大小等等。打開字型對話方塊,你會發覺字型的種類很多,但是我們需要的只有中文字型而已,所以我們忽略其他資訊,專注于我們需要的部份。這裡我要強調的是,如果只強調秀出中文,你大可不必管他字型怎麼來的(畢竟,我們的WINDOW環境本身就是中文的),使用內定字型即可,如果你還要強調系統字型的美觀,就需要選擇一種比較容易搭配的字型,更進一步如果要讓使用者在遊戲內自由選擇系統任一種字型,則需要列舉出所有可用的字型。並且將這些資訊收集起來,提供程式使用。
好的,我們先介紹如何列舉出所有的系統字型,首先介紹這個函示:
int EnumFontFamilies( HDC hdc, // handle to device control LPCTSTR lpszFamily, // pointer to family-name string FONTENUMPROC lpEnumFontFamProc, // pointer to callback function LPARAM lParam // address of application-supplied data ); //取自VC線上說明
這個函示需要的參數共有四個,第一個參數是繪圖使用的設備代碼,在一般應用程式,我們會使用GetDC()來取得他的設備代碼,而在DirectX裡面,我們必須使用IDirectDrawSurface3::GetDC(),由這個函示取得的DC才能保證與GDI的函示相容。第二個參數設定為NULL則會取得所有的字型,包括固定寬度字型與向量字型。第三個參數是一個CALLBACK函示指標,他的原型固定,系統會自動呼叫這個函示,而實作此函示的我們,正好可以將系統字型擷取下來,第四個參數用不到,設為NULL即可。
至于FONTENUMPROC的原型是這樣子的:
int CALLBACK EnumFontFamProc( ENUMLOGFONT FAR* lpelf, // pointer to logical-font data NEWTEXTMETRIC FAR* lpntm, // pointer to physical-font data int FontType, // type of font LPARAM lParam // address of application-defined data ); // 取字VC 線上說明
這個函示的四個參數由系統傳給我們,裡面包含我們所需要的一切資訊,現在我們就看看實際上該如何使用,底下擷取自CFONT類別的實作內容:
//此成員函示呼叫以後,會開始取得系統字型
void CFont::QueryFont() { lpFrontBuffer->GetDC(&hdc); //取得前景DC EnumFontFamilies(hdc, (LPCTSTR)NULL,(FONTENUMPROC) EnumFamCallBack,(LPARAM)NULL); lpFrontBuffer->ReleaseDC(hdc); //釋放DC }
CFont::QueryFont()僅設定好初始的資料,真正接收字型資料的部份在CALLBACK函示,而CALLBACK函示我們應該怎麼實作呢?底下便是:
首先我們配置三個結構以存放「細明體」「新細明體」與「標楷體」的字型資料, LOGFONT logfont[3];
這個結構可以存放字型的細部資料,其內容相當繁雜,且不是所有的資料都派上用場,更詳細的資料可以在VC線上說明取得。需要的話,你可以配置更多的空間以存放各式各樣的字型資料,這邊我只示範三種常用字型。接著我們看一下該怎麼在CALLBACK函示裡面接收這些資料:
BOOL CALLBACK CFont::EnumFamCallBack(LPLOGFONT lplf,LPNEWTEXTMETRIC lpntm ,DWORD FontType,LPARAM aFontCount) { if(strcmp(lplf->lfFaceName,"細明體")==0) //僅找明體與標楷體 memcpy(&logfont[0],lplf,sizeof(LOGFONT)); //資料存放到logfont[]陣列 if(strcmp(lplf->lfFaceName,"新細明體")==0) memcpy(&logfont[1],lplf,sizeof(LOGFONT)); if(strcmp(lplf->lfFaceName,"標楷體")==0) memcpy(&logfont[2],lplf,sizeof(LOGFONT));
return TRUE; }
剛剛有說到這個CALLBACK函示的四個參數是系統傳給我們的,其中的LPLOGFONT包含了一種字型的資料,我們藉由判斷其名稱來決定這個字型是不是我們需要的,如果是的話,將他拷貝到我們預先配置好的LOGFONT結構裡面。這個函示事實上會持續呼叫,直到找完所有的字型為止,所以在這個過程中,我們只接收三種字型的資料,其他的都忽略不處理。當這個函示完成以後,我們配置的LOGFONT[3]這個陣列裡面,已經包含我們所需要的資料了。大事已經完成一半了,接著我們應該做什麼事情呢?
□ 將字型設定給DC
取得字型資料以後,實際上什麼事情也沒發生,我們必須根據字型的資料,來產生一個字型代碼給API函示使用,這個處理過程我將他包在成員函示CFont::SetFont(HDC hdc,int FontType,int width,int height)裡面,實作內容如下:
void CFont::SetFont(HDC hdc,int FontType,int width,int height) { switch(FontType) { case MINGLIU: logfont[0].lfHeight=height; logfont[0].lfWidth=width; hFont = CreateFontIndirect (&logfont[0]); break;
case NEWMINGLIU: logfont[1].lfHeight=height; logfont[1].lfWidth=width; hFont = CreateFontIndirect (&logfont[1]); break;
case KAIU: logfont[2].lfHeight=height; logfont[2].lfWidth=width; hFont = CreateFontIndirect (&logfont[2]); break; }
SelectObject(hdc,hFont); }
這個成員函示接收四個參數,第一個參數是欲設定字型的目的DC,第二個參數決定要設定何種字型,第三第四個參數決定字型的大小。多方便阿,連字型大小都可以自由設定,不過還是要適中才會好看。所以實際呼叫的時候,我們是這樣做的:
SetFont(hdc,NEWMINGLIU,10,15); //將前景DC與字型相結合
為了美觀起見,我把三種字型另外定義其名稱:
#define KAIU 5 //標楷體 #define MINGLIU 6 //細明體 #define NEWMINGLIU 7 //新細明體
所以上面的SetFont()我們是選擇了新細明體,並且決定其字型寬度10,高度15,當然,寬度高度是可以任意變化的,決定好寬度高度以後,接著使用CreateFontIndirect ();其傳回值為字型代碼,最後利用SelectObject(hdc,hFont);把他真正設定給DC就可以了。
感覺上這個過程繞來繞去的,都沒有一槍斃命的感覺,唔~~~我也這樣認為,所以我還是把到目前為止的過程整理一下吧:
1. 使用EnumFontFamilies()列舉字型 2. 在CALLBACK函示裡面,接收字型資訊 3. 使用字型的時候,將字型與DC結合 4. 目前為止,大事完成2/3。
□ 規畫秀字的方式
好的,我用最簡單的方式來秀字看看,要用什麼函示呢?TextOut()是也,簡單又大方,親切又可愛,而且在任何地方,秀字總免不了使用這個函示,我們來看看他的樣子:
BOOL TextOut( HDC hdc, // handle of device context int nXStart, // x-coordinate of starting position int nYStart, // y-coordinate of starting position LPCTSTR lpString, // address of string int cbString // number of characters in string ); // 取自VC++4.0線上說明
可以指定座標與字串,果然是為我們精心設計的API,不用怎麼對得起別人呢?示範一下我要在座標(20,25)的地方秀出一段文字,我這麼做:
TextOut(hdc,20,25,"相當穩用",8);
果然沒問題,不過呢,我們需要再包裝一層,讓這個函示更人性化一點,所以我又實作了一個成員函示void CFont::ShowFont(char* string),這個成員函示接收一個指向任意長度的字串指標,並且按照我們預先設計好的格式秀出來,這個格式需要討論一下,我自己決定的方式是這樣子的:
1. 在螢幕座標 (46,120)的地方開始秀字 2. 一列以十四個中文字為最長的長度,超過換列。 3. 螢幕最多同時容納四列,超過的話清除字串,從第一列輸出。 4. .........(依照喜好,自己定格式)
一旦決定好以後,根據這些規則我們實作出來的內容是這樣子的:
void CFont::ShowFont(char* string) { int Line,Cycle; int i,j,k,m=0; int count=0,ShowedFont=0;//累計秀出的字
while(string[count]!=0)
count++;//先取得這個字串的長度
if(count%2) //不足2 bytes則補足 count++;
count/=2; //COUNT除以二變成中文字個數
Line=count/14; // 每列14個中文字,所以我們計算共需要幾列
Cycle=Line/4; //每個畫面最多4列,所以我們計算需要幾個畫面
Cycle++; //至少一個畫面
//可見頁拷貝到隱藏頁,文字秀出以後,恢復螢幕用
lpBackBuffer->Blt(NULL,lpFrontBuffer,NULL,DDBLT_WAIT,NULL);
SetFont(hdc,NEWMINGLIU,10,15);//將前景DC與字型相結合
for(k=0;k四行字為一個回圈 { lpFrontBuffer->GetDC(&hdc);//取得前景DC SetBkMode(hdc,TRANSPARENT);//設定秀字背景為透明色 for(i=0;i<4;i++)//一行字為一個回圈,共四行 {
for(j=0;j<14;j++)//一行字裡面有14個中文字 { //第一次秀黑色 SetTextColor(hdc,RGB(0,0,0)); TextOut(hdc,46+j*17,120+i*15,&string[m*28+j*2],2);
//第二次左移一個像點秀出另一色,制造框線效果 SetTextColor(hdc,RGB(255,0,0)); TextOut(hdc,45+j*17,120+i*15,&string[m*28+j*2],2); Sleep(20);//稍微延遲,控制速度 ShowedFont++;
if(ShowedFont==count)goto Finish;//秀完了嗎?離開回圈 }//for j end
m++;//指向下一列給TextOut()使用 }// for i end
Finish:
lpFrontBuffer->ReleaseDC(hdc);
ReleaseFont();
Sleep(200); //延遲
while(GetAsyncKeyState(VK_SPACE)>=0);//按空白鍵繼續
lpFrontBuffer->Blt(NULL,lpBackBuffer,NULL,DDBLT_WAIT,NULL);
}//for k end
//還原螢幕畫面 lpFrontBuffer->Blt(NULL,lpBackBuffer,NULL,DDBLT_WAIT,NULL); Sleep(300); return;
} // function end
雖然加上注解了,我還是稍微說明一下,首先函示會收到一個不定長度的字串,第一個步驟我們先計算他的長度,此處的長度單位是byte,除以二以後就變成中文字的個數了,接著利用國小數學,我們計算出總共要幾列,幾個畫面才得以把這個字串秀完。所以回圈裡面就是在做這一件事情,最後跳出回圈的方式,我們判斷已經秀出的字是否跟傳進來的長度一樣,如果一樣,代表我們已經秀字完畢了,goto跳出來就好了,輕鬆。
另外保存螢幕畫面是有必要的,不過這邊我的作法相當直接。在秀字的時候,我們是直接秀到前景的繪圖頁(surface),所以TextOut()一呼叫完畢,螢幕上馬上會出現這些字串。所以你知道了,在我們秀字的當時,背景的繪圖頁是閒置的,于是我們在秀字串以前,把螢幕畫面Blt()拷貝到背景繪圖頁,等到秀字完畢以後,要恢復畫面,則反向操作,從背景繪圖頁拷貝到前景繪圖頁即可。我的初步結論是,一切較靜態的畫面,你直接畫在前景surface即可,不必擔心會有閃爍的現象。
在秀字的過程中,每一個字我們都秀兩次,這是為什麼呢?達成文字邊框的效果是也。同一套字型,我們利用不同顏色畫上去,效果不錯,其原理是這樣子的,我隨便舉例說明:
第一次我在座標(46,120)的地方用黑色秀出一個「中」字,第二次我在座標(45,120)的地方用紅色秀出一個「中」字,你可以看到這兩個字的偏移只有一個x座標單位,所以紅色的「中」字會覆蓋掉黑色的「中」字,但是最右邊的黑色部份則不會覆蓋到(因為我們偏移一個x單位嘛),這簡單的過程,我們好像獲得了一套新的,更漂亮的字型一樣,這技巧夠酷。
各位也可以看到函示裡面Sleep()的部份,主要是控制秀出字串的速度,你可以在字與字之間控制間隔的速度,如果沒有稍微延遲的話,則一整面的字瞬間秀完。當然最好的方法是把Sleep()函示的參數(延遲時間)當成變數,讓使用者決定他想要的速度。
最後我要說的是,在你使用IDirectDrawSurface3::GetDC()獲得DC以後,記得要釋放他,不這麼做的話,其他繪圖的動作都會失敗,這跟我們平常使用GDI的GetDC()是有差別的。
□ 整合一下
到目前為止,初步完成99%的任務。我們學到了如何利用系統的字型資源,並應用在我們的程式上面,整個字型類別是這樣子的:
class CFont { public: void QueryFont(); //找中文字型存入 LOGFONT[] void SetFont(HDC,int,int,int);//設定字型以及大小 void ReleaseFont();//釋放字型資源 void ShowFont(char *);//秀出格式化字串
private: static BOOL CALLBACK EnumFamCallBack(LPLOGFONT, LPNEWTEXTMETRIC,DWORD,LPARAM); HFONT hFont;//字型代碼 };
我的目的是解釋其過程,並非要寫一個現成的東西給你用,所以還有很多事情需要靠你自己去完成。比方說,一般人系統裡面的中文字型,並不僅止于明體與標準楷體,或許還有華康等其他的字型,我們或許應該列舉出所有的中文字型,讓程式更有彈性。在字型大小方面,是可以調整的,所以你也不必要擔心在320x200的畫面字型是否會過大,在800x600的畫面下,字型是否太小。一切的操控權都在你手上。在秀字的同時,加上一個美麗的背景圖框也是不錯的選擇。
□ 末語
看到這裡,你大概已經知道WINDOW下的處理方式跟DOS下有相當相當的差別吧。如果你掌握到正確的處理方式,並節省下額外的時間,那麼,看這篇文章就值回票價了。
以處理字型為目標,我們已經達成階段性任務,以處理劇情為目標,我們只完成了開始的5%,劇情的表現上,大致上可以看成「說故事」一樣,不斷的把文字輸入到腦海裡面,就形成了劇情,當然也需要各種事件的參與。更有趣的是,架構單線劇情與多線劇情都是相當富挑戰性的一件工作。我們是不是該開始規畫了呢?規畫前記得先洗把臉(可以不使用洗面皂),讓思緒更加暢通。
|