关于低分辨率像素游戏下显示非防锯齿中文 / 汉字的研究

作者:bitca.cn
2018-07-05
64 55 20

像素游戏是独立游戏的一种常用表现方式,在制作中文游戏时我们要面临显示点阵汉字的问题。当前各大游戏引擎中都会有显示中文的功能,但显示出来的中文字体效果一般都差强人意任意,在低分辨率的像素游戏画面下会产生一些问题...


面临的问题

像素游戏是独立游戏的一种常用表现方式,在制作中文游戏时我们要面临显示点阵汉字的问题。当前各大游戏引擎中都会有显示中文的功能,但显示出来的中文字体效果一般都差强人意任意,在低分辨率的像素游戏画面下会产生一些问题:

  1. 默认的防锯齿使得字体跟游戏画面的整体风格不搭;
  2. 关掉防锯齿后,矢量的字库渲染到低分辨率画面上字型比较难看;
  3. 为了实现在各设备上的统一效果,可以将字体嵌入到游戏中,但是一个中文字库动辄十几 M 的容量会消耗大量的资源空间,甚至超过游戏本体的容量大小,对于 HTML5 这样的平台更会增加下载时间。

令我困扰的是像 Construct 这样的 HTML5 引擎在使用原生字体渲染的时候,无法把字号调整到最小(也许跟设备有关),这是我把字号设置为0.1pt 的情况: 

很多 GBA 汉化游戏都做得很好,这就是我要追求的效果: 

解决思路

  1. 使用贴图的形式来显示汉字,把用到的汉字当作图片存储和显示;
  2. 汉字库只存放常用汉字,或者说只存使用到的汉字库;
  3. 使用点阵汉字库,而不是矢量汉字库;

具体的实现

首先使用贴图的形式会比直接渲染矢量的会更节省性能的消耗,大多数游戏引擎都提供了 Bitmap Font 或者 Sprite Font 这样的使用贴图来显示字体的功能。实际上就是把所有可能用到的字符事先画到一张贴图上,需要时再逐个渲染出来。当然英文及类似的拼音文字系统所使用的字符数目比较少,所以在这方面比较省事,一张贴图就能搞定。可是中文可以在一张贴图内搞定么?让我们来算一下,比如我们理想中的点阵汉字大小为 16x16 像素,那么在一张 1024x1024 的贴图中,一共可以存放 4096 个汉字,太好了,因为我们所常用的汉字也就是 3500 个,你可以上网搜索到这3500个汉字的表。有了这个常用汉字表,我们就可以用这张表来生成贴图。有些游戏引擎,比如 game maker studio 是内置了这样的点阵字库生成功能的。另外一些就需要借助一些工具。有不少可以生成字库点阵贴图,比较有名的是 BMFont(http://www.angelcode.com/products/bmfont/)。这些软件可以让你输入要生成的字符表,选择你想要的字体,设定生成的字体大小,还可以设定颜色和描边的效果以及是不是防锯齿等。这些软件生成贴图的同时会生成一个数据文件,这个数据文件会保存有常用汉字对应贴图中的位置等信息,游戏在需要渲染点阵字体时可以使用这些数据来得到每个汉字对应的贴图区域。这样做还有一个额外的好处:可以预先叠加效果到字体上,比如描边和渐变等,这样也会省去处理这些效果时产生的性能消耗。

Construct 中默认的像素字体,西文的好处就是字符量很少,你甚至可以自己手写设计,工作量不大:

比较常见的位图字体生成工具 BMFont: 

BMFont 使用微软雅黑输出16像素非防锯齿的汉字,很不好看:

字体的选择

现在有了工具,我们接下来要选择使用什么样的字体了。这个问题需要注意,因为大多数字体都不是免费的,特别是你要用在商业用途上,所以在选择字体时一定要注意看准字体的版权声明。中文可免费商用的字体其实并不多,其中最有名的是 Google 和 Adobe 开发的思源系列字体。不过我测试了一些的这样免费商用的矢量字体,都普遍存在一个问题:这些字体并不是为了点阵显示而制作的,在选择比较小的字号同时关掉防锯齿时,出来的效果是机器不美观的。因为我当前追求的是低像素分辨率的画面,所以这些字体并不能符合我的要求。

我要寻找在低字号大小无防锯齿情况下都能表现良好的字体。回想一下,在 DOS 时代,我们的汉字字体都是点阵的,如果你现在搜索 HZK16 时可以搜索到不少信息的,但是关于以前 DOS 时代的这些汉字字体的版权,能够查到的信息并不多。我们暂且把这个作为一个备选方案。另外,其实我们很多主机游戏的汉化都会涉及点阵汉字字体的问题,我的印象中不少 GBA/3DS 汉化游戏的字体都是处理得不错的,当然因为是非商用,字体选择可以很多。同时,虽然现在我们的大多数设备都可以渲染矢量字体,但还是有很多设备是需要显示点阵的,比如各种 LCD 显示屏。所以我觉得还是有针对点阵显示设备设计的字体。我搜索到了“最像素”(https://github.com/SolidZORO/zpix-pixel-font)这个字体,这个字体似乎是一个人开发的,而且是专门为极小分辨率点阵显示准备的。不过唯一的问题是,商业使用还是需要付费授权的。

DOS 时代,320x240 256 色是比较常见的分辨率,当时的中文处理是这样的:

UCDOS:

WPS:

最像素字体:

当我在尝试各种可以免费商用的字体时,我发现了“文泉驿”(http://wenq.org)这个开源的字体系列。里面竟然有一款专为点阵设计的宋体,字号从9像素到12像素,显示效果非常的不错,那么决定就是它了!

补充一下:文泉驿为 GPL 协议,商用需要作者授权,提醒大家注意~

输出的问题

在一般情况下,使用前面提到的 bmfont 这样的软件工具,以及文泉驿点阵宋体,已经可以解决大多数需求,只要你使用的游戏引擎支持使用的 bmfont 生成的数据文件就可以了。不过因为我用的是 Consturct ,一个 HTML5 游戏制作软件,它支持使用的点阵贴图要求每个字符是同等大小的,但是那些字体贴图软件大多数都不支持生成等宽的字体贴图,或者是支持生成等宽字体贴图的软件有各种缺陷,比如贴图大小不可控,无法关掉防锯齿,不支持太多字符集等。

BMFont 输出的字体都是不等宽的,也就是说输出时要经过计算矫正:

有人专门做了给 Construct 用的工具,原先的问题是不支持太大的字符集,现在已经修正,现在唯一的问题就是没有去掉防锯齿的功能:

编写工具

最后还是得自己动手做工具,既然在前面我们已经研究了这么多,生成一张这样的贴图对于做游戏的我们来说就不是什么难事了。我现在面临的选择就是用什么来做。本来我是很熟悉 Javascript 这一块的,但是我所知道的 HTML5 相关的引擎都很难渲染出小字号的不带防锯齿的字体。那么用 Lua 呢?我以前用过一段时间 Love2D 感觉处理这样的 2D 像素是比较好的,以前我还用它来制作过处理像素画的软件。但是问题是我没发现它能够渲染没有防锯齿的字体,可惜,而且 Love2D 还有一个缺点就是处于安全性的考虑,它只能写入文件到一个固定的 sandbox 文件夹中,这样做出来的工具使用上比较麻烦些。

最后,我开始考虑到我可以使用的另外一个脚本式语言:Python。如果这个还不行,我就只能考虑 Haxe 和 C 之类的了。说到 Python,我会熟悉 Python 主要是因为我使用 Blender ,使用 Python 可以让我做一些插件扩展。所以以前我是考核过它的游戏制作能力的。它的最出名的游戏库就是 Pygame(https://pygame.org),不过这个 Pygame 的确很 Old School,它是个 2D 引擎,有很多跟像素相关的功能,而且很多概念还停留在 Blit 位图的层面上。不过我仔细看了一下它的最新版的文档,发现它的字体处理应该可以满足我的要求,因为我明确的看到了它可以关掉字体的防锯齿渲染。所以决定就是它了!拿出 Python 书,临时温习一下,同时看下 Pygame 的文档,很快我就做出了自己想要的工具,输出了合适的位图。

推荐使用 PyCharm ,用来写 Python 体验还是很好的:

收尾

在收尾工作中,我需要处理一些问题:

- 因为对话中也不免出现英文。这个时候会遇到一点小麻烦,因为中文基本都是等宽的,而英文每个字符有可能是不等宽的,如果我们按照汉字的方式来显示每一个英文字母的话,会出现英文字符之间间隔过宽的问题,看起来就是不好看。不过 construct 是考虑到这个情况的,你只要输出对应需要调整宽度的字符列表及其宽度就可以了。

没有宽度矫正和有宽度矫正的西文字符的区别:

还有一个比较麻烦的问题就是英字其实是有基线的,在我们单独输出某一个小写的英文字母时会失去基线的对齐,幸好 Pygame 里面是可以取得基线的信息的,输出字母时调整这个高度即可。

所谓的基线就是红线标的位置:

没有考虑基线时输出的西文小写字符:

按照基线调整输出的西文小写字符:

如果不考虑基线输出的话,结果会是这样:

最后,在使用过程中还是出现缺字的情况。这主要是因为我们使用的某个汉字不在常用汉字列表里面,这个时候我们只需要在常用汉字表中加入这个字就可以了。其实我碰到的这个字是“哦”字,显然人们的汉语表达用语也在不断的变化中,现在的一些常用口语有可能并不在这个常用汉字表中。也许以后根据游戏的结构,我会考虑做一个只按照使用过的汉字生成贴图的功能。

Python 源码

import pygame                   # Pygame 游戏模块
from pygame import freetype     # 处理矢量字库的 Pygame 模块
import codecs                   # 处理 unicode 所需模块
import json                     # 输出 json 格式 所需模块

# 等宽部分的字符表
fixWCharset = codecs.open( "hz3500.txt","r","utf-8" ).read()     # 读取3500个常用汉字的表
fixWCharset = fixWCharset + u"哦"                               # 加入常用字中没有的字
# 需要记录宽度信息的字符表
varWCharset = codecs.open( "ascii.txt","r","utf-8" ).read()      # 加入常用的 ascii 字符 表
varWCharset = varWCharset + u",。;“”、:?《》"                 # 加入拳脚的汉字标点

gridW     = 14 # 每个字符输出区域的宽度
gridH     = 14 # 每个字符输出区域的高度
outColNum = 90 # 每行输出的字符数
outRowNum = 42 # 一共输出的行数
textureW  = gridW * outColNum # 最终输出的贴图宽度
textureH  = gridH * outRowNum # 最终输出的贴图高度

pygame.init()                                                # 初始化游戏引擎
pygame.display.set_caption("像素点阵汉字生成")                 # 窗口的标题
screen = pygame.display.set_mode( (textureW, textureH) )       # 打开的窗口大小
buffer = pygame.Surface( (textureW,textureH),pygame.SRCALPHA ) # 建立一个透明贴图大小的缓冲区,贴图先

# 因为非等宽字体还要需要处理基线的问题,所以同一个字体载入到两个变量之中,可以进行不同的设置
fixWFont = pygame.freetype.Font( 'wenquanyi_9pt.pcf' ) # 等宽字符所用字体
varWFont = pygame.freetype.Font( 'wenquanyi_9pt.pcf' ) # 非等宽字体所用字体

# 关掉防锯齿
fixWFont.antialiased = False
varWFont.antialiased = False

varWFont.origin = True # 使用基线方式渲染字体
varWFontSize    = 12   # 非等宽字体的固定输出为 12 像素
baseLine        = 10   # 设定从顶部往下 10 个像素为基线

x = 0 # 字符输出的行坐标
y = 0 # 字符输出的列坐标
fontColor    = ( 255,255,255 )    # 字体颜色
outlineColor = ( 0,0,0 )          # 描边颜色

for i in range( 0, len(fixWCharset) ): # 遍历常用汉字表
    fx   = x * gridW # 字符输出的像素坐标 x
    fy   = y * gridH # 字符输出的像素坐标 y
    char = fixWCharset[i]
    # 渲染字符描边
    fixWFont.render_to( buffer, (fx+1, fy+0), char, outlineColor )
    fixWFont.render_to( buffer, (fx+1, fy+2), char, outlineColor )
    fixWFont.render_to( buffer, (fx+0, fy+1), char, outlineColor )
    fixWFont.render_to( buffer, (fx+2, fy+1), char, outlineColor )
    # 渲染字符
    fixWFont.render_to( buffer, (fx+1, fy+1), char, fontColor )
    # 行列递增
    x = x + 1
    if (x>=outColNum):
        x = 0
        y = y + 1

widthDict = {} # 记录宽度的字典
for enIndex in range(0, len(varWCharset)):
    fx = x * gridW # 字符输出的像素坐标 x
    fy = y * gridH # 字符输出的像素坐标 y
    char = varWCharset[enIndex]
    # 渲染字符描边
    varWFont.render_to( buffer, (fx+1, baseLine+fy+0), char, outlineColor, size=varWFontSize )
    varWFont.render_to( buffer, (fx+0, baseLine+fy+1), char, outlineColor, size=varWFontSize )
    varWFont.render_to( buffer, (fx+2, baseLine+fy+1), char, outlineColor, size=varWFontSize )
    varWFont.render_to( buffer, (fx+1, baseLine+fy+2), char, outlineColor, size=varWFontSize )
    # 渲染字符
    varWFont.render_to( buffer, (fx+1, baseLine + fy+1), char, fontColor, size=varWFontSize )
    # 记录字符宽度
    m     =  varWFont.get_metrics( char, size=varWFontSize )
    lineX = fx + m[0][1]
    charW = m[0][1] + 3
    if not charW in widthDict : widthDict[charW] = []
    widthDict[charW].append( char )
    # 行列递增
    x = x + 1
    if ( x >= outColNum ):
        x = 0
        y = y + 1

# 输出 construct 3 所需的宽度 json 文件
outputList = []
for wKey in widthDict:
    charStr = ""
    for char in widthDict[wKey] : charStr = charStr + char
    outputList.append( [wKey,charStr] )
outJson = json.dumps( outputList )
print( "Json String For Construct : " )
print( outJson )
filename = "construct-spriteFont-spaceData.json"
file     = open( filename, "w" )
file.write( outJson )
file.close()
print( "saved to : " + filename )

# 输出整体字符集文件
charset  = fixWCharset + varWCharset
filename = "charset.txt"
file     = codecs.open( filename, "w", "utf-8" )
file.write( charset )
file.close()
print( "charset saved to : " + filename )

# 保存贴图文件
filename = "pixel-hz.png"
pygame.image.save( buffer, filename )
print( "texture saved to : " + filename )

# 主循环
running = True
while running:
    # 在窗口中显示贴图
    screen.blit( buffer, (0, 0) )
    pygame.display.update()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

完成结果

最终生成的贴图,我还加入了常用的全角中文标点符号:

在 Aseprite 中检查,每个字都加上了黑色描边:

到此,对于在低分辨率像素游戏中使用点阵汉字的心得分享就这么多,希望对你有所帮助。

补充一下:文泉驿为 GPL 协议,商用需要作者授权,提醒大家注意~

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. 千罹 2018-07-05

    先给一个赞再看

  2. IGNI 2018-07-05

    超级详尽,棒

  3. Rusty 2018-07-05

    需要注意一点的是,我记得文泉驿的字体大多是基于GPL协议,所以如果你的游戏要使用他们的字体,应该也要遵守GPL协议,即开源、免费。创作商业游戏、不提供源代码的免费游戏,都不适合使用文泉驿的字体。

    • 千罹 2018-07-05

      @Rusty:文泉驿的 GPL 不是仅指字体吗?我用了他的字体A,改成了自己的艺术字体B,用在我的商业游戏G中。我仅仅需要开源掉我的字体B吧?难道我用了他的字体,就要开源我的游戏G吗?

    • Rusty 2018-07-05

      @千罹:GPL是有传染性的,你将遵循GPL的字体A改为字体B,那么字体B也应遵循GPL,使用遵循GPL的字体B的游戏G自然也应遵循GPL。GPL是有传染性的。
      文泉驿字体页面都有一行声明,供参考:本字体版权为“文泉驿信任委员会(Board of Trustees)”, FangQ和Firefly所有。字体授权形式为GNU General Public License v2(附加允许文档嵌入条款)。企业用户如果希望在产品中包含文泉驿字体但不希望自己的软件开放源代码,请与FangQ联系,协商使用授权事宜。

    • Rusty 2018-07-05

      @千罹:不知道为什么回复你就无法发出评论,请看下面的说明。

    • bitca.cn 2018-07-05

      @eastecho:
      麻烦补充一下文泉驿为 GPL 协议的说明:商用需要作者授权,提醒大家注意~

    • ttso 2018-07-08

      @千罹 @Rusty : GPL 開源字體是不會將 GPL 許可證感染到程式上的,FSF 在 2005 年就釋出了 GPL font exception(GPL 字型例外),就是爲了解決這個問題。

  4. Rusty 2018-07-05

    @千罹 不知道为什么直接回复你无法发出评论。这个要看文字具体版权归属了,属于你自己自然是没有问题的。但是开发字体本身就是一件十分繁重的工作,哪怕是基于一套字体进行细节上的修改。

    最近由 Rusty 修改于:2018-07-05 14:01:28
  5. 千罹 2018-07-05

    fixWCharset = fixWCharset + "哦"
    varWCharset = varWCharset + ",。;“”、:?《》"

    这两句最好加一个 u 字,要不会报字符串拼接的错误

    fixWCharset = fixWCharset + u"哦"
    varWCharset = varWCharset + u",。;“”、:?《》"

    • eastecho 2018-07-05

      @千罹:文章中代码已改

  6. bitca.cn 2018-07-05

    感谢 @Rusty 的提醒~
    麻烦 @eastecho 补充一下文泉驿为 GPL 协议的说明:商用需要作者授权,提醒大家注意~

    现在我再看看原来 UCDOS 中的点阵字库是否还保留版权,如果不能找到一款可以嵌入的字体,这个问题还是没能解决。也请研究过相应问题的朋友指点。

    而且的确如 Rusty 所说不能点击回复按钮来直接回复评论。

    最近由 bitca.cn 修改于:2018-07-05 21:16:35
    • eastecho 2018-07-05

      @bitca.cn:已修正

      最近由 eastecho 修改于:2018-07-05 23:26:16
    • Neon Studios 2018-07-05

      @eastecho:刚才是有问题

    • eastecho 2018-07-05

      @bitca.cn:谢谢提醒,已修正!

  7. DosxC 2018-07-08

    这种硬核研究文章必须先赞再看已是习惯

  8. 千罹 2018-07-10

    @bitca.cn: 我根据你的代码,稍微支持了一下导出 fnt 文件,贴在这里给大家参考 https://indienova.com/u/llq/blogread/8179

    最近由 千罹 修改于:2018-07-10 14:23:34

您需要登录或者注册后才能发表评论

登录/注册