金橋科普  
設為首頁 關于我們 郵箱登錄
科普聚焦
您現在的位置: 首頁 >> 科普頻道 >> 電腦遊戲 >> 文章詳情
遊戲基礎──深入WINDOW字型

akinggw gameres 2006-08-15
 
 
 

深入WINDOW字型


文檔簡介:
  在探討更深入的劇情處理以前,我們必須擁有輸出文字的能力。這次我們的目標是擷取
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%,劇情的表現上,大致上可以看成「說故事」一樣,不斷的把文字輸入到腦海裡面,就形成了劇情,當然也需要各種事件的參與。更有趣的是,架構單線劇情與多線劇情都是相當富挑戰性的一件工作。我們是不是該開始規畫了呢?規畫前記得先洗把臉(可以不使用洗面皂),讓思緒更加暢通。

 

 
 

  發表評論 寫信給編輯 關閉窗口
同欄目內容
· 瑞典在網遊中開設虛擬大使館 2007-06-06
· 魔獸爭霸3秘籍(魔獸無敵秘籍) 2007-03-28
· SDL系列教程:SDL和IME 2006-08-14
· DirectDraw之旅(1) 2006-08-11
· 2D遊戲中人物被遮擋的透明效果 2006-08-11
· 如何實現遊戲截屏功能 2006-08-11
相關內容
近期主推
西藏發現青藏高原最大冰川群
·西藏發現青藏高原最大冰川群
·中日: "月亮女神"攜手"嫦娥"
·行星大碰撞砸死恐龍催生人類
·美研制會飛汽車定價9萬美元
·美國教授設計出自適應機器人
·銀河系可能有20多個黑洞
近期熱門
英海域驚現大龍蝦為普通龍蝦5倍
·英海域驚現大龍蝦為普通龍蝦5倍
·新物種金蛙體表有劇毒
·我國嫦娥工程計劃測量月球
·精神病藥物可治多種癌症
·視覺震撼:來自海底的精靈
·能刪除不良記憶的藥物誕生
近期焦點
廣西發現罕見溶洞鐘乳石有30萬年
·廣西發現罕見溶洞鐘乳石有30萬年
·澳大利亞驚現“卡布基諾”海灘
·美國"彩色"瀑布夜幕下震撼遊人
·地球上究竟有多少個物種
·英國探險家北極遊泳破紀錄
·世界之最 世上最小的動物
金橋科普是一個公益性欄目,內容由作者提供或摘自互聯網,其目的是向廣大網民普及科學技術知識。如果您發現本網站轉載或摘編了您擁有著作權的作品,請通知我們(電話:023-63659911),我們立即刪除。
關于我們 | 網站地圖 | 用戶注冊 | 廣告客戶 | 招聘信息 | 業務信息
Copyright@2006-2009 JQInfo.com,WONDER. All Rights Reserved
重慶科技咨詢中心.重慶網得信息技術有限公司.版權所有
Email:webmaster@jqinfo.com
重慶數據通信局提供網絡帶寬. 渝ICP備05002327號