EtherTK 解构《武士零》 - 动态文本

动态文本在很多拥有文字故事讲述的游戏中都各有体现,从拥有相当文本体量的 Galgame 类型游戏的经典打字机特效,到《武士零》《传说之下》这类融合了更多文字动态效果的游戏,动态文本在提升交互体验、增加画面质感、表达作者情绪 等方面均发挥了不小的作用。

考虑到《武士零》这款游戏对于团队在下一阶段的作品很有参考和学习的价值,于是便开始尝试从程序功能角度去解构这一部优秀的作品,动态文本作为相对简单的功能,便被优先提上了日程;在 EtherTk 的基础上,实现了相对丰富的动画文本效果,并提供了类似 Markdown 的轻量标记语法,使得策划同学可以在文案阶段对游戏内的文本动态效果进行设计,从而减轻引擎内工作量。

EtherTK 是我个人在旧有开源项目 EtherEngine 基础之上重构实现的一套工具集,核心理念便是 All In One Toolkit,用来方便使用 Lua 脚本快速开发和算法功能等验证;虽然 EtherTk 源代码现阶段仍未公开,但在本功能的实现过程中主要使用的 Graphic 模块主要是对 SDL Renderer 渲染管线相关功能的 1:1 封装,只要对 SDL2 全家桶的使用较为熟悉,便完全可以理解动态文本实现的相关思路。

下面对该功能的实现过程进行简要描述(先放一张游戏内的效果)。

《武士零》游戏内文本效果

文字排版实现

无论是 SDL2,亦或是 OpenGL,底层图形接口显然不会提供文本排版这种顶层逻辑,所以实现简单的排版功能便成为了我们想要渲染对话类文本的首要任务;对于游戏内基于引擎架构的排版功能太过复杂,相关内容仍在研究学习,此处只实现简单的自动换行功能——毕竟我们总不能在文本导入阶段小心翼翼地去拆分文本行,又或者是容忍在游戏内看到超出对话框区域的文字。

考虑到每个文字相对独立的动画效果,所以我们只需要为对话文本中的每个文字都生成一张独立的纹理即可——这只是为了简明起见相对偷懒的思路,如果考虑到程序性能,可以考虑用 map 对多次出现的同一字符进行优化,或者将全部文本生成在同一张纹理中,在显示时再计算裁剪矩形。

那么在这种思路之下,我们只需要逐个字符累加水平坐标,如果下一个需要显示的字符超出了对话框范围,那么我们便将下一个字符显示的位置在竖直方向上推进一行的距离,这样便实现了简单的换行排版。

标记语法实现

正如文章开始所说,想要在策划阶段便对海量的对话文本进行动画表现的设计,而不是在引擎内提供诸如 Word 之流的富文本编辑器工具,引入类似 Markdown 的轻量标记语法在我看来是极好的思路。

在对语法进行设计时,首先要考虑的便是拥有特殊含义的标识符不能是常见的字符,否则,尽管可以通过转义字符解决歧义问题,但是大量的转义不仅会对编写造成麻烦,在后续修改时阅读体验也是极差的,所以在语法上,规定了使用 %*# 三种字符,来分别对应 抖动、强调和着色 三种功能的声明。

但是,在本程序的实现过程中,并没有实现对这些特殊字符的转义功能。

而在对文本进行解析思路,便是逐个扫描文本中的字符,并记录当前的动画效果状态,如 %这是一句抖动的话% 这句带有标记的文本在进行解析时,我们只需要在遇到第一个 % 时开始设置动画状态为抖动,那么一直到遇到下一个 %,才会结束动画抖动状态,区间内的字符就被赋予了抖动的效果;这种基于状态的解析弊端便是不能进行嵌套,如果我们在动画的结束部分遗漏了 %,那么在下一段相同动画开始时便会出现奇怪的效果——当然,这也是使用相同的符号作为标记的弊端,如果使用诸如 ()[]{} 等符号进行标注的话,或许会更方便在出现问题后进行调试。

虽然相同的动画不能嵌套(其实说来相同的动画效果进行标记嵌套在设计上同样是冗余的),但是不同的动画效果之间是可以进行嵌套的,一行文本可以同时拥有抖动效果,同时被强调和赋予特殊的颜色,这是允许的。

着色效果的设计是一个小细节,在 EtherTK 或者说 SDL2 的设计中,颜色一般使用 32 位的结构体表示,但是如果使用类 table 的声明方式,如 (255, 255, 255, 255) 来表示白色,那么在文本标记中会占据较大的体量,考虑到标记的其中一点便是精炼,所以将其设计为 16 进制的颜色表示法,如:#FFFFFFFF这是白色文本#

以及一个相对粗糙的十六进制颜色字符串转颜色 table 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
-- 将用 16 进制表示的颜色文本转换为 32 位颜色
local ConvertColor16ToColor8 = function(value_text)
-- 将两位颜色分量文本转换为数字值
local _ConvertTextToValue = function(text)
-- 将 16 进制字符转换为 10 进制数字
local _ConvertHexTextToDecNum = function(hex_text)
local dec_num = tonumber(hex_text)
if not dec_num then
if hex_text == "A" then
dec_num = 10
elseif hex_text == "B" then
dec_num = 11
elseif hex_text == "C" then
dec_num = 12
elseif hex_text == "D" then
dec_num = 13
elseif hex_text == "E" then
dec_num = 14
elseif hex_text == "F" then
dec_num = 15
-- 对于不合法的十六进制字符,赋值为 0
else
dec_num = 0
end
end
return dec_num
end

local low = _ConvertHexTextToDecNum(string.sub(text, 2, 2))
local high = _ConvertHexTextToDecNum(string.sub(text, 1, 1))
return high * 16 + low
end

return
{
r = _ConvertTextToValue(string.sub(value_text, 1, 2)),
g = _ConvertTextToValue(string.sub(value_text, 3, 4)),
b = _ConvertTextToValue(string.sub(value_text, 5, 6)),
a = _ConvertTextToValue(string.sub(value_text, 7, 8))
}
end

动画效果实现

动画效果主要分为抖动、强调和着色三种,但更基础的动画效果便是打字机特效,即文本是逐个出现到场景中的;在处理打字机特效时,进行了一点小小的优化,即每个字符是从下方以极小的偏移飞入到场景中的,同时进行了淡入效果,这样来看,画面的质感会更加爆满同时也更丝滑。

下面是三大主要动画效果的实现细节:

  • 抖动:抖动的效果首先要确定好抖动的最大偏移,这个偏移是不可以超过水平或竖直文本间距的,否则就有可能出现文字重叠的情况——除非这正是你所追求的混沌感;这样,只需要不断地在最大偏移之内不断随机文字移动的目标位置,同时在 Tick 时使文字向着当前的目标点进行移动,到达目标点后再继续重置随机的新位置,控制好文字移动的速度,便可以实现抖动效果。
  • 强调:强调文本通常会使用相对醒目的颜色,如红色进行着色,或是带有一点小小的粗体(在 SDL_ttf 的支持下粗体会很奇怪,所以在本程序中并未实现),同时减少其淡入或飞入的时间,让这些被强调文本的出现更加干净利落,再配以较强的音效,使这些文字如同将泥巴丢到黑板上一样砸出来,将会带来比较明显的强调感。
  • 着色:即简单的对文本赋予不同的颜色,在标记语法实现时已有相当的实现细节,此处不再赘述;除此以外,在《武士零》游戏中,文本并不一定是纯色进行着色,还有可能使用自发光的霓虹动画效果,这需要 shader 进行支持,考虑到本次探索使用的渲染后端为 SDL Renderer,遂放弃此功能。

效果演示

最后,用一段《尼尔:机械纪元》中的台词进行演示:

演示效果

对应的标记文本为:

1
2
3
你认为这一切都是没有意义的吗?%你认为这仅仅是游戏便毫无价值吗?%
你所做的一切可能#FFDB4FFF得不到任何感谢#,你所做的一切*在他人眼里可能只是伪善!*
%即使如此你也愿意帮助他吗?%

源码链接

感兴趣的朋友可以在这里查看本项目源码:https://github.com/VoidmatrixHeathcliff/DeconstructKatanaZERO/tree/main/SuperWord

作者

Voidmatrix

发布于

2023-06-04

更新于

2023-06-04

许可协议

评论