HGE系列之十 管中窥豹(游戏字体)

HGE系列之十 管中窥豹(游戏字体)

 

对于一款游戏引擎来说,支持显示字体自然是必备的功能,HGE内建的字体功能虽然仅支持一般的位图字体,但是也算是简洁明了,这次的HGE源码之旅就让我们来看一看他的各中实现:)

 

类名hgeFont

  功能 :字体类

  头文件hge/hge181/include/hgeFont.h

  实现文件hge/hge181/src/helpers/hgeFont.cpp

 

  整个hge字体的功能支持皆实现与此,让我们依照惯例来看一看他的头文件声明:

 

class hgeFont

{

public:

    // 构造函数,注意参数

hgeFont(const char *filename, bool bMipmap=false);

    ~hgeFont();

 

    // 渲染函数

    void        Render(float x, float y, int align, const char *string);

    // format string渲染

    void        printf(float x, float y, int align, const char *format, ...);

    // format string 矩形区域渲染

    void        printfb(float x, float y, float w, float h, int align, const char *format, ...);

   

    // 设置字体颜色

    void        SetColor(DWORD col);

    // 设置字体“深度”(Z缓存)

    void        SetZ(float z);

    // 设置混合模式

    void        SetBlendMode(int blend);

    // 设置缩放

    void        SetScale(float scale) {fScale=scale;}

    // 设置比例(宽度缩放值)

    void        SetProportion(float prop) { fProportion=prop; }

    // 设置旋转

    void        SetRotation(float rot) {fRot=rot;}

    // 设置字体间距

    void        SetTracking(float tracking) {fTracking=tracking;}

    // 设置字体行距

    void        SetSpacing(float spacing) {fSpacing=spacing;}

 

    // 获取颜色

    DWORD       GetColor() const {return dwCol;}

    // 获取“深度”

    float       GetZ() const {return fZ;}

    // 获取混合模式

    int         GetBlendMode() const {return nBlend;}

    // 获取缩放值

    float       GetScale() const {return fScale;}

    // 获取宽度比例

    float       GetProportion() const { return fProportion; }

    // 获取旋转

    float       GetRotation() const {return fRot;}

    // 获取字体间距

    float       GetTracking() const {return fTracking;}

    // 获取行距

    float       GetSpacing() const {return fSpacing;}

   

    // 获取字体精灵

    hgeSprite*  GetSprite(char chr) const { return letters[(unsigned char)chr]; }

    // 获取先前宽度

    float       GetPreWidth(char chr) const { return pre[(unsigned char)chr]; }

    // 获取之后宽度

    float       GetPostWidth(char chr) const { return post[(unsigned char)chr]; }

    // 获取高度

    float       GetHeight() const { return fHeight; }

    // 获取字符串宽度

    float       GetStringWidth(const char *string, bool bMultiline=true) const;

 

private:

    // 私有化构造函数和赋值函数,已达到禁用目的

    hgeFont();

    hgeFont(const hgeFont &fnt);

    hgeFont&    operator= (const hgeFont &fnt);

 

// 以下为实现的一些细节

 

    char*       _get_line(char *file, char *line);

 

    static HGE  *hge;

 

    static char buffer[1024];

 

    HTEXTURE    hTexture;

    hgeSprite*  letters[256];

    float       pre[256];

    float       post[256];

    float       fHeight;

    float       fScale;

    float       fProportion;

    float       fRot;

    float       fTracking;

    float       fSpacing;

 

    DWORD       dwCol;

    float       fZ;

    int         nBlend;

};

 

  可以看到,虽然hgeFont本身并未有多少复杂,但是仍然提供了不少方便字体操控的函数功能,接下来就让我们细细的翻看翻看:

 

  首先请让我们跳过hgeFont的构造函数(原因后面再提),先来看看hgeFont的析构函数:

 

hgeFont::~hgeFont()

{

// 依次释放letters精灵数组(最大256个)

// 每一个精灵即代表一个字符

    for(int i=0; i<256; i++)

        if(letters[i]) delete letters[i];

    // 释放字体贴图

    if(hTexture) hge->Texture_Free(hTexture);

    // 释放hge引用

    hge->Release();

}

 

  看来没有什么特别的地方,好的,让我们继续往下看:

 

// 字体渲染

void hgeFont::Render(float x, float y, int align, const char *string)

{

    int i;

    float   fx=x;

   

// 首先将align位与HGETEXT_HORZMASK(水平掩码)

align &= HGETEXT_HORZMASK;

// 如果对齐方式为靠右,修正fx的坐标值

    if(align==HGETEXT_RIGHT) fx-=GetStringWidth(string, false);

    // 如果对齐方式为居中,修正fx的坐标值

    if(align==HGETEXT_CENTER) fx-=int(GetStringWidth(string, false)/2.0f);

 

    // 当前字符不为空 (/0)

    while(*string)

    {

        // 如果当前为换行符

        if(*string=='/n')

        {

            // 更行y坐标,注意计算公式,为 高度*缩放比例*行距比例

            y += int(fHeight*fScale*fSpacing);

            fx = x;

            // 根据对齐方式继续修正fx坐标

            if(align == HGETEXT_RIGHT)  fx -= GetStringWidth(string+1, false);

            if(align == HGETEXT_CENTER) fx -= int(GetStringWidth(string+1, false)/2.0f);

        }

        Else// 其他字符

        {

            // 获取当前字符值

            i=(unsigned char)*string;

            // 如果当前精灵字符数组中找不到,便以 '?' 代替

            if(!letters[i]) i='?';

            if(letters[i])

            {

                // 更新fx坐标,注意计算公式,为 前坐标*缩放比例*字宽比例

                fx += pre[i]*fScale*fProportion;

                // 调用精灵(hgeSprite,可以参看这里)提供的渲染函数进行渲染

                letters[i]->RenderEx(fx, y, fRot, fScale*fProportion, fScale);

                // 渲染之后继续更新fx坐标,以正确渲染下一字符

// 注意计算公式,为 (字宽+后位移+间距)*缩放*宽比

                fx += (letters[i]->GetWidth()+post[i]+fTracking)*fScale*fProportion;

            }

        }

        // 更新至下一个字符的处理

        string++;

    }

}

 

  相关的实现本身并不复杂,但是仍然有几点值的一看:例如HGETEXT_HORZMASK这个掩码的使用,以及各个渲染坐标的更新方式等等,需要我们悉心关注一下。

 

  接着让我们来看看hgeFontprintf函数:

 

void hgeFont::printf(float x, float y, int align, const char *format, ...)

{

    // 获取可变参数的起始位置

    char    *pArg=(char *) &format+sizeof(format);

 

    // 使用_vsnprintf将格式化字符串打印至buffer

    _vsnprintf(buffer, sizeof(buffer)-1, format, pArg);

    // 将字符串最后一位置空

    buffer[sizeof(buffer)-1]=0;

    //vsprintf(buffer, format, pArg);

    // 调用自身的Render函数进行渲染

    Render(x,y,align,buffer);

}

 

  _vsnprintfC语言中用以支持可变参数的库函数之一,不太熟悉的朋友可以参看一下这里 )

 

  让我们接着看看printfb的相关实现:

 

void hgeFont::printfb(float x, float y, float w, float h, int align, const char *format, ...)

{

    char    chr, *pbuf, *prevword, *linestart;

    int     i,lines=0;

    float   tx, ty, hh, ww;

   

    // 取得可变参数起始位置

    char    *pArg=(char *) &format+sizeof(format);

 

    // 使用_vsnprintf将格式化字符串打印至buffer

    _vsnprintf(buffer, sizeof(buffer)-1, format, pArg);

    // 将字符串最后一位置空

    buffer[sizeof(buffer)-1]=0;

    //vsprintf(buffer, format, pArg);

 

    linestart=buffer;

    pbuf=buffer;

    prevword=0;

 

    for(;;)

    {

        i=0;

        // 寻找下一个空格或者换行符号

        while(pbuf[i] && pbuf[i]!=' ' && pbuf[i]!='/n') i++;

 

        // 存储该字符

        chr=pbuf[i];

        // 将原字符位置置空

        pbuf[i]=0;

        // 重新获取字符串长度

        ww=GetStringWidth(linestart);

        // 重新置回原字符

        pbuf[i]=chr;

 

        // 如果当前字符串的长度大于所给宽度参数(w

        if(ww > w)

        {

            // 如果pbuf指向字符串首

            if(pbuf==linestart)

            {

                // 置当前位置为换行符

                pbuf[i]='/n';

                // 更新行首指针

                linestart=&pbuf[i+1];

            }

            else

            {

                // 将前字符置为换行符

                *prevword='/n';

                // 更新行首指针

                linestart=prevword+1;

            }

            // 递增行数

            lines++;

        }

 

        // 如果当前字符串为换行符

        if(pbuf[i]=='/n')

        {

            // 设置前字符为当前位置

            prevword=&pbuf[i];

            // 设置行起始为当前+1位置

            linestart=&pbuf[i+1];

            // pbuf为当前+1位置

            pbuf=&pbuf[i+1];

            // 递增行数

            lines++;

            // 回到循环起始(for(;;)),继续执行

            continue;

        }

 

        // 如果当前字符位置为空,则递增行数,并跳出循环

        if(!pbuf[i]) {lines++;break;}

 

        // 否则更新前字符为当前位置,pbuf为当前+1位置

        prevword=&pbuf[i];

        pbuf=&pbuf[i+1];

    }

   

    tx=x;

    ty=y;

    // 计算高度,注意计算公式,为 字体高度*行距比例*缩放比例*行数

    hh=fHeight*fSpacing*fScale*lines;

 

// 根据对齐方式调整渲染坐标

 

    switch(align & HGETEXT_HORZMASK)

    {

        case HGETEXT_LEFT: break;

        case HGETEXT_RIGHT: tx+=w; break;

        case HGETEXT_CENTER: tx+=int(w/2); break;

    }

 

    switch(align & HGETEXT_VERTMASK)

    {

        case HGETEXT_TOP: break;

        case HGETEXT_BOTTOM: ty+=h-hh; break;

        case HGETEXT_MIDDLE: ty+=int((h-hh)/2); break;

    }

 

// 调用自身Render函数完成最后的渲染

    Render(tx,ty,align,buffer);

}

 

  printfb的思路基本上类同于先前的printf,只是在其基础上根据给定的矩形渲染范围做一些渲染坐标上的调整,最后的渲染也都是转给自己的Render,流程上还是相当清晰的 :)

 

  说完了这些,让我们再来看看hgeFont实现的几个辅助函数,首先便是GetStringWidth

 

// 获取字符串宽度

float hgeFont::GetStringWidth(const char *string, bool bMultiline) const

{

    int i;

    float linew, w = 0;

    // 当前字符不为空

    while(*string)

    {

        linew = 0;

        // 当字符不为空并且不为换行符时

        while(*string && *string != '/n')

        {

            // 获取当前字符

            i=(unsigned char)*string;

            // 如果为未定义字符则以' ?' 代替

            if(!letters[i]) i='?';

            // 如果当前字符存在

            if(letters[i])

                // 递增行宽,注意递增公式,为 字体宽度+前位移+后位移+字体间距

                linew += letters[i]->GetWidth() + pre[i] + post[i] + fTracking;

            // 转至下一个字符继续处理

            string++;

        }

 

        // 如果不处理多行情况,则直接返回第一行行宽,计算公式为 行宽*缩放*宽比

        if(!bMultiline) return linew*fScale*fProportion;

 

        // 否则更新最长行宽

        if(linew > w) w = linew;

        // 当前字符为换行符或者回车符时,则跳过

        while (*string == '/n' || *string == '/r') string++;

    }

   

    // 返回最长行宽

    return w*fScale*fProportion;

}

 

  不难看出,该函数的作用便是取得给定字符串的行宽,并且考虑了多行情况。

 

  接下来的Set* 函数并未有多少内容,基本思想便是设置成员参数,并依次设置hgeFont内部的256个字符精灵,具体实现在此不再赘述,有兴趣的朋友可以参看源码。

 

  最后,让我们来瞅一瞅先前被我故意跳过的hgeFont构造函数,首先来看一看其中用到的一个辅助函数:_get_line

 

char *hgeFont::_get_line(char *file, char *line)

{

    int i=0;

 

    // 如果当前字符为空,则返回0

    if(!file[i]) return 0;

 

    // 当前字符不为空,并且不为换行符,回车符

    while(file[i] && file[i]!='/n' && file[i]!='/r')

    {

        // fine中对应字符拷贝至line

        line[i]=file[i];

        i++;

    }

    // line末尾置空

    line[i]=0;

 

    // 跳过换行符以及回车符

    while(file[i] && (file[i]=='/n' || file[i]=='/r')) i++;

   

    // 返回file的下一字符串位置

    return file + i;

}

 

  通过代码不难看出,该函数的作用其实就是从给定字符串中提取由/n,/r分隔的字串,平心而论,该段代码似乎有些重造车轮,因为CRT中的strtok也可用以完成同样的工作(当然如果考虑到多线程环境的话就不尽然了:)),再者该函数的命令以下划线起头,这点也令我不是特别舒服,因为很容易造成与C/C++库函数的冲突。(btw:上面的printf/printfb的命名其实我也觉得不是很妥,内部使用的‘256’这个魔数也应该至少用个const或者#define包装一下...

 

  废话了不少,其实以上提及的这些问题也多是一些瑕疵,让我真正觉得确实需要改正的倒是hgeFont的构造函数,相关的原因之前我也有所提及,在此就不再讲述了,其实现的源码讲解我也在此略去,仅讲一讲其间实现的功能,有兴趣的朋友可以自行查看相应源码:

 

  hgeFont的构造函数其实是实现了一个特定文本文件格式的完整解析,该文件的名字便是hgeFont构造函数的第一个参数,而该定义文件的格式则基本如下所示:

 

// 首先是字段定义,必须为HGEFONT,大小写一致

[HGEFONT]

 

// 接着定义字体贴图文件路径,为字体定义文件(即本文件)的相对路径

// 注意,该贴图文件必须包含你所要定义的所有字符

Bitmap=font_bitmap.png

 

// 定义各个字符,相关格式如下:

// Char = "字符",x坐标,y坐标,宽度,高度,前位移,后位移

Char=" ",1,1,3,30,-1,4

Char="!",5,1,7,30,1,0

Char=""",13,1,8,30,3,2

// "字符" 也可以使用十六进制数表示,当然,大小上不要超过255FF

Char=FE,445,187,17,30,0,0

Char=FF,463,187,16,30,-2,-1

 

  至于hgeFont构造函数的第二个参数是与渲染效率相关的mipmap,有兴趣的朋友可以从这里开始了解,另外一提的是,由于需要支持mipmap的关系,字体贴图大小必须为2的幂次,譬如64*64,使用时需要注意一下。

 

  当然,手工编写这个定义文件(默认后缀名为fnt)还是相当费劲的,所以HGE提供了一个简单的工具来帮助你完成这件事情,他便是fonted,位置位于:

 

  hge181/tools/fonted/fonted.exe

 

  有兴趣的朋友可以自己捣鼓捣鼓,其相应的代码实现在这里可以在这里找到:

 

  hge/hge181/src/fonted/...

 

  好了,hgeFont的讲解就到此结束吧,有兴趣的朋友可以继续了解,如果弄出了什么好看的字体,或是做出了什么功能上的改进,到时一定要通知一下啊:)

 

  OK,那么下次再见吧!

 

你可能感兴趣的:(游戏)