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

  • 声明:转载请链接出处。
  • 作者:来自 BITCA.CN 的 Retro Daddy
  • 邮箱:harrisyu@qq.com

面临的问题

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

  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 源码(依赖 Pygame 模块):

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 + "哦"                               # 加入常用字中没有的字
# 需要记录宽度信息的字符表
varWCharset = codecs.open( "ascii.txt","r","utf-8" ).read()      # 加入常用的 ascii 字符 表
varWCharset = varWCharset + ",。;“”、:?《》"                 # 加入拳脚的汉字标点

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 中检查,每个字都加上了黑色描边

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • pygame图形接口 使用pygame.image模块,可以对图像进行读取和保存。 使用pygame.image....
    sssally92阅读 17,163评论 1 25
  • 概述 关于电子书的字体选择,其实是一个很庞大的命题,讲深了涉及字体设计,这里不扯那么远,主要是从电子书观感的角度来...
    hyx108阅读 9,559评论 10 9
  • 也许由于浪费了太多的时间,才会把最后几天过得格外珍惜。年度总结、年度计划,小心翼翼地、仔细斟酌地,不敢落下。 和那...
    Mint阅读 276评论 0 1
  • 本文由币乎社区(bihu.com)内容支持计划奖励 今天从上海返回北京了,在高铁上也听到朋友说币圈的一些动态了,似...
    七公爱吃鸡阅读 178评论 0 0
  • 文/小妖 下班了,走出写字楼的时候才6点10几分,可天都黑了,才发现原来夏天真的悄悄过去了。路上的灯陆陆续续的亮起...
    小妖的幺阅读 802评论 0 0