引言

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

作者:来自 BITCA.CN 的 Retro Daddy
邮箱:harrisyu@qq.com
简书:https://www.jianshu.com/p/c44e17f3b8bb
转自INDIENOVA

 

 



面临的问题

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

 

 

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

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

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

104335dpmg66ukpsc67xcq.png
解决思路

 

 

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

具体的实现

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

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

104336qk2zjrfktzfkx7cx.png
比较常见的位图字体生成工具 BMFont:

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

104338lf9hfc7srjsts6t9.png
字体的选择

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

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

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

104340ej4ecf8njnala8xn.png
UCDOS:

104340ugntq16w4hhhz1hq.png
WPS:

104340y4yktm0gyaytif6k.png
最像素字体:

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


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

输出的问题

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

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

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

104342ymzn77xs35pparmx.png
编写工具

最后还是得自己动手做工具,既然在前面我们已经研究了这么多,生成一张这样的贴图对于做游戏的我们来说就不是什么难事了。我现在面临的选择就是用什么来做。本来我是很熟悉 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 体验还是很好的:

104344xesrzrs9mr9r3yje.png
收尾

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

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

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

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

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

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

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

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

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

Python 源码

 

  1. import pygame # Pygame 游戏模块
  2. from pygame import freetype # 处理矢量字库的 Pygame 模块
  3. import codecs # 处理 unicode 所需模块
  4. import json # 输出 json 格式 所需模块
  5.  
  6. # 等宽部分的字符表
  7. fixWCharset = codecs.open( hz3500.txt,r,utf-8 ).read() # 读取3500个常用汉字的表
  8. fixWCharset = fixWCharset + u哦 # 加入常用字中没有的字
  9. # 需要记录宽度信息的字符表
  10. varWCharset = codecs.open( ascii.txt,r,utf-8 ).read() # 加入常用的 ascii 字符 表
  11. varWCharset = varWCharset + u,。;“”、:?《》 # 加入拳脚的汉字标点
  12.  
  13. gridW = 14 # 每个字符输出区域的宽度
  14. gridH = 14 # 每个字符输出区域的高度
  15. outColNum = 90 # 每行输出的字符数
  16. outRowNum = 42 # 一共输出的行数
  17. textureW= gridW * outColNum # 最终输出的贴图宽度
  18. textureH= gridH * outRowNum # 最终输出的贴图高度
  19.  
  20. pygame.init() # 初始化游戏引擎
  21. pygame.display.set_caption(像素点阵汉字生成) # 窗口的标题
  22. screen = pygame.display.set_mode( (textureW, textureH) ) # 打开的窗口大小
  23. buffer = pygame.Surface( (textureW,textureH),pygame.SRCALPHA ) # 建立一个透明贴图大小的缓冲区,贴图先
  24.  
  25. # 因为非等宽字体还要需要处理基线的问题,所以同一个字体载入到两个变量之中,可以进行不同的设置
  26. fixWFont = pygame.freetype.Font( wenquanyi_9pt.pcf ) # 等宽字符所用字体
  27. varWFont = pygame.freetype.Font( wenquanyi_9pt.pcf ) # 非等宽字体所用字体
  28.  
  29. # 关掉防锯齿
  30. fixWFont.antialiased = False
  31. varWFont.antialiased = False
  32.  
  33. varWFont.origin = True # 使用基线方式渲染字体
  34. varWFontSize = 12 # 非等宽字体的固定输出为 12 像素
  35. baseLine = 10 # 设定从顶部往下 10 个像素为基线
  36.  
  37. x = 0 # 字符输出的行坐标
  38. y = 0 # 字符输出的列坐标
  39. fontColor = ( 255,255,255 ) # 字体颜色
  40. outlineColor = ( 0,0,0 ) # 描边颜色
  41.  
  42. for i in range( 0, len(fixWCharset) ): # 遍历常用汉字表
  43. fx = x * gridW # 字符输出的像素坐标 x
  44. fy = y * gridH # 字符输出的像素坐标 y
  45. char = fixWCharset[i]
  46. # 渲染字符描边
  47. fixWFont.render_to( buffer, (fx+1, fy+0), char, outlineColor )
  48. fixWFont.render_to( buffer, (fx+1, fy+2), char, outlineColor )
  49. fixWFont.render_to( buffer, (fx+0, fy+1), char, outlineColor )
  50. fixWFont.render_to( buffer, (fx+2, fy+1), char, outlineColor )
  51. # 渲染字符
  52. fixWFont.render_to( buffer, (fx+1, fy+1), char, fontColor )
  53. # 行列递增
  54. x = x + 1
  55. if (x>=outColNum):
  56. x = 0
  57. y = y + 1
  58.  
  59. widthDict = {} # 记录宽度的字典
  60. for enIndex in range(0, len(varWCharset)):
  61. fx = x * gridW # 字符输出的像素坐标 x
  62. fy = y * gridH # 字符输出的像素坐标 y
  63. char = varWCharset[enIndex]
  64. # 渲染字符描边
  65. varWFont.render_to( buffer, (fx+1, baseLine+fy+0), char, outlineColor, size=varWFontSize )
  66. varWFont.render_to( buffer, (fx+0, baseLine+fy+1), char, outlineColor, size=varWFontSize )
  67. varWFont.render_to( buffer, (fx+2, baseLine+fy+1), char, outlineColor, size=varWFontSize )
  68. varWFont.render_to( buffer, (fx+1, baseLine+fy+2), char, outlineColor, size=varWFontSize )
  69. # 渲染字符
  70. varWFont.render_to( buffer, (fx+1, baseLine + fy+1), char, fontColor, size=varWFontSize )
  71. # 记录字符宽度
  72. m =varWFont.get_metrics( char, size=varWFontSize )
  73. lineX = fx + m[0][1]
  74. charW = m[0][1] + 3
  75. if not charW in widthDict : widthDict[charW] = []
  76. widthDict[charW].append( char )
  77. # 行列递增
  78. x = x + 1
  79. if ( x >= outColNum ):
  80. x = 0
  81. y = y + 1
  82.  
  83. # 输出 construct 3 所需的宽度 json 文件
  84. outputList = []
  85. for wKey in widthDict:
  86. charStr =
  87. for char in widthDict[wKey] : charStr = charStr + char
  88. outputList.append( [wKey,charStr] )
  89. outJson = json.dumps( outputList )
  90. print( Json String For Construct : )
  91. print( outJson )
  92. filename = construct-spriteFont-spaceData.json
  93. file = open( filename, w )
  94. file.write( outJson )
  95. file.close()
  96. print( saved to : + filename )
  97.  
  98. # 输出整体字符集文件
  99. charset= fixWCharset + varWCharset
  100. filename = charset.txt
  101. file = codecs.open( filename, w, utf-8 )
  102. file.write( charset )
  103. file.close()
  104. print( charset saved to : + filename )
  105.  
  106. # 保存贴图文件
  107. filename = pixel-hz.png
  108. pygame.image.save( buffer, filename )
  109. print( texture saved to : + filename )
  110.  
  111. # 主循环
  112. running = True
  113. while running:
  114. # 在窗口中显示贴图
  115. screen.blit( buffer, (0, 0) )
  116. pygame.display.update()
  117. for event in pygame.event.get():
  118. if event.type == pygame.QUIT:
  119. running = False

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

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

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


锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛