回顾并规划今天的任务
正在进行一些复杂的工作。之前正在处理语言相关的任务,不过有些人提到空格处理不当,这是因为字距设置的问题,所以应该优先修复这个问题,这并不是什么复杂的修复,应该可以快速解决。
现在有一些问题需要处理,但由于今天是星期五,不打算做一些大型的修改。可以通过分析现有问题,弄清楚具体需要做什么,并把一些改动做进去,这样下周就能顺利完成字体的处理,大家也会满意。
已经完成了大部分字体的工作,至少对于这个游戏来说,字体已经达到了基本要求,尽管复杂度不高。但是一些人反馈说空格字符没有正确显示,已经知道问题的原因了,所以先解决这个bug,这样在处理语言相关任务时就不会被这个问题困扰。
接下来,会进入调试周期,处理字体和空格的问题。
空格未显示
发现,空格在文本中没有显示出来,这是一个简单的测试案例。空格应该出现在这里,但是它们并没有出现,所以需要弄清楚为什么空格没有显示。
原因其实很简单,空格的处理与其他字符一样,都会进行字距调整(kerning)。不过改变了字距调整的方式,现在字距调整发生在字符操作集(character operation set)中,而这个字符操作集并没有考虑到空格,因为空格并不是一个需要绘制的字形。空格本身是空的,没有任何像素,因此在处理时被跳过了。也没有导出空格字形,因为认为空格不会用到,所以不需要处理。
但是,这样一来,空格的字距调整表(kerning table)就永远无法被设置。为了修复这个问题,需要确保空格能够被正确处理,确保它能够正确地参与到字距调整中。解决这个问题的最简单方法是将空格也包含在处理过程中,以便它能够正确地被处理和导出。
将空格的字距值添加到字距表中
尝试通过直接处理空格来解决问题,认为这样应该能让空格通过标准路径正确显示,理论上应该能工作。然而,在这之前,想考虑一下在字距调整(kerning)代码中是否做了一些改变,可能会导致空格处理不正常。例如,在对齐到左边缘时采用的方式可能与 Windows 的处理方式不同。
尽管不是完全按照 Windows 的方式来做,认为不能百分之百确保它能够正常工作。但至少,这是一个可以尝试的第一步。
结果并没有按预期工作,空格仍然没有显示出来。这个问题可能是预见到的。
现在回想起来,记不清楚到底在字距调整的处理上到底做了什么。之前是否在字距调整表(kerning table)中使用了 32 位对齐?这一点也记不太清楚了。

但等等...那个字距值已经在了
看起来之前的修改应该是可以工作的。现在觉得之前的判断可能是错的,重新考虑了一下。
字距调整表(kerning table)应该在其中添加空格的字距值。虽然可能需要提取原始的字距值,但是重新回顾了一下,发现可能之前并没有完全理解这个问题。以为知道原因,但其实并不清楚。
真正的原因可能是并没有正确处理空格。没有考虑到空格的字距调整,因为空格并不需要渲染出来,这就是问题所在。
因此,需要确保处理字距调整时也考虑到空格,尽管空格本身不需要渲染,但它仍然会影响字形之间的间距。正确的做法是在字距调整时检查每一对字形,确保在计算水平推进(horizontal advance)时处理空格。
解释对于导致错误的先前假设
最初认为问题很简单,但实际上比预期的要复杂。最开始以为空格字符的字距调整没有正确处理,原因可能是空格从未经过字距调整的代码,或者它的字形没有被处理过。猜测这可能是导致空格没有字距调整的原因,因为空格没有实际的字形内容,所以在处理时被忽略了。
认为字距调整的关键部分是负责计算字符之间的间距的代码。它应该能够处理空格的字距调整问题,但以为正是这个环节出了问题,导致空格没有字距调整。
然而,当实际检查时,发现这部分代码是被正确调用的,且在资产打包器中并没有发现空格字符。这让人开始怀疑问题的真正原因------空格字符到底在哪儿?它是否被遗漏或者丢失了?
调试资产打包器
为了弄清楚为什么空格没有正常处理,需要进一步调查实际发生了什么。最初的假设显然不正确,现在需要查看处理空格的具体逻辑。
最简单的方法是切换到资产打包器,使用调试器设置断点,当处理到空格时暂停。这样可以观察到空格在处理过程中究竟发生了什么,确保能够准确找出问题所在。
看起来资产打包器没有处理空格字符
在设置断点时,添加了一个条件,即检查代码点是否等于空格。然而,发现代码点并不是直接以这种方式处理的,可能是大小写的问题。因此,决定深入检查一下,并在调试过程中手动停止,查看代码点是否等于空格。
通过进一步检查,确认了最初的猜测,空格字符确实没有被处理。也就是说,空格没有经过相应的代码流程,这导致空格没有被正确处理。
断点没进来
资产打包器忽略了空格字形,因为它没有可见的像素
问题的原因是空格没有字符,因此它的字形不会被打包。这个问题没有被处理到,导致空格没有经过正确的流程。实际上,之前的猜测是对的,但修复的方式并没有正确实现。
因此,虽然找到了原因,但修复并没有按照预期正确实施。这个错误是在资产构建器中出现的,导致了空格的字形没有被正确处理。


运行 game Hero,尽管...α
问题仍然没有完全解决,空格字形依然没有显示出来。正常情况下,空格应该会出现在调试输出的位置,但现在什么也没有显示。于是开始怀疑是不是存在一个异常的、错误的值,可能是某个需要特别注意的大值导致问题。
需要仔细检查,看看是否有不正常的值或者其他潜在的问题,可能影响了空格字形的显示。此时,开始查看字距调整和字距调整表的变化,分析是否存在错误或者遗漏的地方。

修复在解决空格字形可见性问题时引入的错误
问题的根源是初始化时出现了垃圾数据,因此需要修改代码,确保只有在需要更新时才会进行更新。如果没有实际需要更改的内容,就不更新当前的值。这样可以避免不必要的更新,确保字距调整为零时不会修改其他内容。
通过这种方式,处理空格的逻辑就能正常工作了,确保空格的字距调整正确地被处理。最终的修复看起来已经解决了问题,空格字形显示正常,修复过程也完成了。

检查渲染后的空格宽度
为了确保一切正常,进行了双重检查,确保正确解析了空格字符。通过调试输出,确认空格的字距调整已经处理得差不多,结果看起来是正确的,空格字符已经得到了良好的处理。
经过这些检查,确认现在空格问题已经解决,处理得相当不错。修复后的效果符合预期,空格的显示和字距调整现在应该没有问题了。
选择目标外语字符串来测试非 ASCII 字符的渲染
接下来要处理一个新的任务,决定在项目中加入一个适合的字符,可能是为了增加一些特色。最初打算使用东京的元素,但最终决定选择一个更合适的角色------短耳鸮。
经过考虑,决定将短耳鸮作为新加入的角色。这个选择看起来很合适,因此决定将其加入到项目中,作为新的元素之一。
https://www.edrdg.org/cgi-bin/wwwjdic/wwwjdic?1C

小耳木兎
"小耳木兎" こみみずく 的日语读法是 "こみみのうさぎ" (komimi no usagi)。
- 小耳(こみみ, komimi)意思是"小耳朵"
- 木兎(うさぎ, usagi)意思是"兔子"
所以,整个词组的意思是"有小耳朵的兔子"。
检查 Arial 是否包含所有必要的字符编码点
为了确认字符是否存在于所选字体中,首先尝试检查字体中是否包含特定的字符。由于不确定Windows是否提供一个合适的方法来获取这些字符,因此选择了一个随机的字体进行测试。结果发现,Arial字体确实包含了需要的字符。
通过尝试不同的字体,发现一些字体并没有这些字符,而Arial字体则能正常显示。这表明,如果使用Arial字体,能够成功获取需要的字符。因此,决定继续使用Arial作为字体来处理相关字符。
"小耳木兎" 这个词的字符编码点如下:
-
小 (こ)
- Unicode编码点:U+5C0F
-
耳 (みみ)
- Unicode编码点:U+8033
-
木 (き)
- Unicode编码点:U+6728
-
兎 (うさぎ)
- Unicode编码点:U+514E
因此,"小耳木兎" 这一串字符的编码点分别是:
U+5C0F, U+8033, U+6728, U+514E
"Kanji owl" 的意思是"汉字猫头鹰"。其中:
- Kanji(漢字)是指日语中的汉字,来自中文的字符系统,用来表示词汇、概念和语法。
- Owl 是"猫头鹰"或"鸮"的意思,一种夜行的猛禽。
所以,"Kanji owl" 可以理解为"汉字猫头鹰",可能是某种带有汉字或与汉字相关的猫头鹰形象或概念。
查找与测试字符串相关的字符编码点
首先,需要找到这些字符的编码点。通过使用一个字符编码查询工具,逐一查找这些编码点。在这个过程中,获取了几个特定的编码点,如5C0F、8033、6728、514E等。然后手动将这些编码点记录下来,方便后续使用,并保存文件以备参考。
接下来,创建了一个文件用于保存"Kanji Owl"(汉字猫头鹰)的相关数据,这些字符现在已经被标注和整理好。但在进行下一步之前,发现存在一些问题,可能会影响接下来的工作,需要进一步分析和解决这些问题。这些问题具体是什么尚不明确,但显然是与当前的编码和数据处理流程有关,需要对其进行修复。
一个涵盖所有必要字符编码点的表将会非常庞大
从目前的情况来看,这些字符的跨度非常大。如果要使用直接的查找表,那么表格会非常庞大。字符的编码范围从5到8之间,这意味着至少要在上面使用12个比特来偏移,所以存储一个如此庞大的查找表显然不现实。此外,即使是1D表格,也会非常庞大,因为它必须覆盖到8033这个值,意味着表格的大小会非常高。
将表格放入到中间的应用中后,发现需要一个包含三万个条目的查找表,这对一个只需要存储大约500个字符的游戏来说是极其不合理的。这样几乎每个条目都会是零,浪费空间和资源。因此,存储这样一个32,000条目的表格只为存储500个字符显然是不切实际的。
为了更清楚地解释这个问题,假设有一个查找表,其中每个条目代表一个字符的映射。每个条目存储的内容可能是该字符的渲染信息(比如曲线、字形等)。如果字符的编码范围很大,比如从某个较小的数字(例如 0x0000)到一个较大的数字(例如 0x7FFF),那么这个查找表的条目数就会非常庞大。
计算方式:
假设字符的编码范围从 0x0000 到 0x7FFF,即总共有 32768 个可能的字符编码(因为 0x7FFF - 0x0000 + 1 = 32768)。如果使用查找表来存储所有字符,那么每个编码位置都会占用一个条目。
但是实际需求:
假设实际的需求只需要 500 个特定字符(例如,可能是一些常用的汉字、日文字符等),这意味着实际需要的条目数量是 500。
问题所在:
如果使用 32768 个条目来表示所有可能的字符编码,但实际上只需要 500 个字符的信息,那么大部分的表格条目就会是空的或为零(因为这些字符并不需要被渲染或者处理)。这就造成了巨大的浪费。
在这种情况下,使用如此庞大的查找表显然是不高效的,因为大部分条目是没有被实际使用的,且存储和访问这么大的表格会占用大量的内存和计算资源。
总结:
- 总条目数:32768
- 实际需要的条目数:500
- 浪费条目数:32768 - 500 = 32268(大部分条目为空或为零)
因此,使用一个包含32768条目的查找表来存储500个字符的数据是非常不合理的,因为它会导致巨大的空间浪费和性能开销。
我们需要一种方法来压缩可用字符的范围
为了解决查找表条目过多的问题,需要将符号集压缩至实际需要的范围。如果游戏中实际使用的符号只有大约500个,而查找表中存储了高达32,000个条目,那么就会存在极大的空间浪费和性能开销。为了避免这种情况,需要一种方法将这些符号集中在实际需要的500个符号上,而不是保持32,000个条目的庞大查找表。
解决方案的关键是压缩符号集,即只存储实际使用的符号,避免存储所有可能的符号。通过某种方式,将符号映射压缩到一个较小的范围,这样查找表的大小就可以减少到实际需要的数量级,大大提高效率并节省存储空间。
具体的实现方法可以包括以下几种思路:
-
过滤不需要的符号:首先,确定游戏中需要的所有符号(例如大约500个符号),然后在查找表中只保留这些符号,而去除其他不需要的符号。
-
映射优化:为每个符号创建一个唯一的索引,通过该索引来访问符号的渲染信息。这样,只需要为500个符号分配索引,而不必为32,000个符号保留条目。
-
使用稀疏查找表:对于这些符号,可以使用稀疏查找表来存储信息,即只为使用的符号保留条目,而不是为每一个可能的符号都分配一个条目。这样可以大大减少内存的消耗。
通过这些方法,可以有效地将查找表的大小缩小到实际需要的范围,从而提高游戏的性能和效率。
(黑板)一维符号查找表的布局
从较简单的问题开始解决,即查找问题。首先,如果要以最直接的方式存储这些符号,最简单的方法是创建一个扁平的数组,并假设符号的编号是连续的。例如,可以有类似a、b、c、d、e这样的符号,然后假设它们的索引分别是0、1、2、3、4,这样可以确保查找过程简单而直接。对于一个连续的符号集,这种方式完全可行。
然而,如果要进一步优化,并且知道不需要从0开始,可以从其他值开始索引。例如,如果第一个符号的代码点从33开始,那么就可以将所有符号的索引调整为从33开始,这样可以避免不必要的存储,进一步压缩查找表。这个做法在符号集非常密集且连续时会很有效,能将空间利用得更紧凑。
问题出现在符号之间存在巨大间隔时。比如,某些符号的代码点可能是12000,而下一个符号的代码点可能是15472,这时符号集之间的间隔就很大,导致不再是一个简单的连续序列。这样直接使用从0开始的线性数组就不再适用了,因为这种间隔会造成存储的低效,浪费大量空间。
因此,当符号集出现间隔很大的情况时,就必须考虑一种新的存储方式,而不是简单地使用线性数组,这样可以避免内存浪费并提高效率。
最简单但较慢的解决方案:为表中的每一行存储字符编码点本身
如果不关心查找的时间,最简单的做法就是将查找表与符号的实际代码点一起存储。也就是说,不再隐含地假设索引,而是直接存储符号对应的实际代码点。每个符号都会附加上其代码点,这样就可以快速完成查找。
例如,当我们有一个字符串 "foo bar" 时,可以直接查找每个字符的代码点。比如,遇到字符 "f",就可以查找其代码点(例如是37或39等),然后在表中查找相应的字符。我们可以通过线性搜索来找到这些代码点,也可以通过更高效的搜索算法,如二分查找,来加速查找过程。虽然二分查找可以提高查找效率,但也要注意,二分查找的缓存性能可能不如线性搜索,因此虽然能加速,但仍需要进行一定的搜索操作。
总结来说,虽然可以通过存储代码点来实现查找,但即便采用更高效的搜索算法,依然需要进行某种形式的查找操作,这可能会影响效率,尤其在面对较大的查找表时。
那么,是否可以预处理每个字符串,存储到字形表的索引,而不是字符?
如果是一个允许输入文本的游戏,那么在游戏中输入的任何文本都需要被转换成可识别的内容。在这种情况下,需要有办法将玩家输入的字符映射到已知的字符集,进行处理和识别。
然而,如果游戏中的所有字符串都是预先创建好的,情况就不同了。在这种情况下,可以选择在游戏启动时对字符串进行预处理,将其映射到一个已压缩的字符集。这种方法通过在预处理阶段将字符串中的字符映射到较小的表中,然后在运行时直接查找这些压缩后的值,这样就不需要在运行时进行昂贵的计算或查找操作。这样一来,查找速度会非常快,且表格的大小也会保持最小。
这就意味着,不再需要在运行时生成新的字符集,而是通过预处理将字符映射到一个更小、更紧凑的表格中,从而提高性能。在这种情况下,游戏的处理过程会非常高效,因为所有的映射都已经在游戏启动时完成,减少了运行时的计算成本。
不过,这也有一个限制,就是在运行时无法动态生成新的字符串。这意味着只能处理预定义的字符集,而不能根据玩家的输入动态添加新的字符。然而,这种方法依然是一个非常合理的选择,尤其是在字符集不需要频繁变化的情况下。
最终,这种方法的好处在于,可以最大程度地减少运行时的计算量,使得游戏运行更加流畅和高效。
或者我们可以简单地存储一个大表,将每个字符映射到它的字形索引
与其使用预处理和映射表来压缩字符集,另一种更简单且高效的方案是使用一个更大的查找表(例如,128K的表),直接映射到当前选择的字体中。这个查找表的大小足够覆盖所有可能的字符编码点,表中存储的是每个字符在当前字体中的实际映射位置。
这种方法的优点在于,处理过程非常简单,不需要进行复杂的预处理或动态生成字符集。只需通过查找表直接获取字符的实际映射即可,无论用户输入什么字符,查找表都会提供正确的映射位置。这种方法使得游戏的文本处理更加灵活,同时避免了在运行时进行复杂的计算和查找,从而提高了性能。
这种方式看起来比其他方法更为直观和高效,因此被认为是更理想的选择。
存储字符编码点和位图 ID 的对,而不仅仅是位图 ID
在文件格式中,只需要做一些简单的修改。在现有的位图表格中,需要存储的信息比单纯的位图ID稍微复杂一些,具体是需要存储字符的编码点 (code point)和对应的位图ID。
这样做的方式是,在表格中每一项都记录两个信息:一个是Unicode编码点 ,一个是位图ID。这意味着每个表项不仅仅是存储字符的图像ID,还包括了该字符在Unicode中的编码值。
因此,加载字体时,只需要根据字符的编码点找到相应的位图ID,这样就能轻松地渲染出相应的字符。这种方法简化了查找过程,也使得字符和图像的映射变得更加直观和容易处理。

新的字符编码点表将在运行时解压并用作直接查找表
可以将表格解压缩成一个直接查找的表格。这意味着将编码点和位图ID的映射关系直接加载到一个查找表中,从而实现快速查找和访问。这样一来,整个过程就变得非常简单,不需要做其他复杂的操作。
由于内存变得更充足,如果使用一个128K大小的表格,就能轻松存储和查找数据,因此解决方案变得非常简便。这个过程可能比预期的更简单,也许这是一个关键的经验教训,表明随着内存的增加,处理这些任务变得更加容易。
让我们实现这个
首先,进入测试资产构建器后,需要对位图ID进行修改,替换为字符的编码点。接下来,需要处理位图ID的大小,它不再是简单的位图ID,而是需要与编码点相关联。
在实现时,首先需要定义编码点或局部索引。然后,通过每个字符和它对应的编码点进行处理,这样能够确保每个字符的编码点被正确引用。
此外,需要注意字符集的范围,包括空格字符等,虽然可能有些麻烦,但可以通过复制和粘贴的方式解决问题。这里的关键是为每个字符处理编码点并将其与位图ID关联,以便能够正确生成字体资源。
在处理过程中,可以增加一些调试信息,如字符图形的计数,确保字符数量不超过最大值。同时,可以将位图ID直接设置在对应的地方,这样就不再需要关注其它细节,简化了整个过程。
最终,通过这种方式生成的字体资源将包含字符的编码点和对应的位图ID,确保在游戏中正确显示每个字符。这种方法不仅简化了流程,而且减少了运行时的复杂性。

手动将汉字"猫头鹰"字符添加到资产文件中
通过这种方式,代码变得更加简洁。接下来,可以轻松地添加所需的内容。具体来说,可以将字体资源的类型定义为 font list
,然后通过 add character asset
添加每个字符资源。
这个过程包括为每个字符资源指定其对应的编码点和位图ID。可以直接将需要的字符资源添加到字体列表中,而不需要额外的复杂操作。这使得整个操作更加直接和高效,便于管理和使用。
通过这种方式,能够更方便地处理字体和字符的映射,并确保每个字符都能正确地呈现在游戏或应用中。
在添加其他字体资产后立即执行 AddCharacterAsset 来添加"猫头鹰"汉字
在这个过程中,可以直接在代码中实现这些操作,不需要额外的步骤。通过这种方式,能够快速地在字体资源中添加新的字符,并确保每个字符都能正确地映射到其对应的编码点和位图ID。这样一来,字符的管理和使用变得更加高效、简便。
此时,已经能够随心所欲地添加字符编码,只需进行必要的配置,就能将字符顺利整合到字体资源中,而不再需要复杂的处理流程。同时,还能确保添加的字符能够顺利显示和使用,这为后续的开发工作提供了更大的灵活性和便利。
接下来,需要退出并重新处理字符的使用,确保所有字符都能得到正确应用。
修改使用普通字符的代码,将其改为使用字形索引
接下来,需要做的工作主要是将字符的索引应用到各个地方,代替之前使用的字符地址。这项工作并不复杂,更多的是一种机械性的操作,需要确保在代码中每个地方都能正确地引用字符的索引。
通过这种方式,可以简化对字符的管理,减少对具体字符位置的依赖,而是使用索引来快速定位。这种方法能够提高效率,并且减少错误发生的可能性。
尽管要进行一些调整,但这些都是相对直接的操作,不需要复杂的逻辑处理。因此,接下来的工作只是对现有流程的细节进行优化和调整,以确保字符索引能够准确、高效地被应用到整个系统中。
设置最大字形数量
在进行字符编码管理时,设定最大字符数是非常重要的。虽然设定最大字符数时可以考虑大于五千个字符,但一般来说,五千个字符足够应付大多数情况。如果想进一步优化,可以将最大字符数限制设定为六十四千,这样接近640K,实际上对于大多数应用来说,这个数字已经非常充足。
在计算表的大小时,如果想避免过大,可以选择在内存上使用一个合理的大小,譬如五千乘五千的大小,这样也足够支持大部分情况,约25MB的内存大小也完全能够承受。通过这种设定,可以避免不必要的内存开销,同时保证表的容量足够处理实际需要的数据。
接下来,配置过程中需要设定字体的最大字形数量及相关的最大编码点范围,确保字体加载时能够处理合适的字符数量。设置一个高的初始编码点值以及0作为最大编码点值,可以更好地适应不同大小的字符集。通过这种方法,字体系统能够灵活管理实际字符的映射,而不受固定编码范围的限制。
当使用字形而非直接的Unicode编码时,编码管理变得更加灵活,因为字形是根据自己特定的编号来存储的,而这些编号在Unicode编码中并不连续。因此,新的管理方法就需要将这两个不同的编号方式区分开来,避免混淆。
最后,虽然不再按每次固定的字形数量处理,导致输出表格的方式会有所不同,但这些更改并不影响整体的编码逻辑,只是在输出表格时需要稍微调整策略。通过这些调整,字符编码管理系统能够更高效、更灵活地处理大量字符数据。

字体完成δ
接下来,重要的任务是开始处理字符映射的问题。由于有了自己特定的编号体系,需要能够将实际的Unicode编码点映射到可以使用的格式。也就是说,现在需要为每个字符设置一个合适的映射表,将Unicode的字符编码与实际使用的编号或标识符相对应,从而使得字符能够在应用中正常呈现。这一映射过程是确保字符集能够被系统正确识别和渲染的重要步骤。
在资产打包时使用一个庞大的表,将每个 Unicode 字符编码点映射到我们的字形
在资产打包过程中,考虑到Unicode字符的映射,我们可能需要按Unicode编码点索引字符,并为它们分配一个庞大的表格,直到进行资产打包时。具体操作是,在打包时,创建一个足够大的表格,用来存储每个Unicode编码点对应的字符在字体中的位置。这个表格的大小可以根据最大编码点来决定,假设最大编码点是64K。
这个表格的作用是提供一个映射,使得在字体中,每个Unicode编码点都能映射到一个特定的字符索引位置。这样,通过Unicode编码点就能找到该字符在字体中的位置。为了实现这一点,首先需要为每个Unicode编码点分配一个相应的槽位。在此过程中,我们不需要担心表格的内存分配问题,因为内存分配可以非常灵活,根本不需要过于担心大小限制。
我们将最大Unicode编码点设为 0x10FFFF
,这是Unicode标准中的最大编码点值。为了能够支持所有字符,我们将为该表格分配足够的内存空间,这样每个字符都能被正确映射和存储。接下来,在初始化时,我们会将所有索引初始化为零,表示默认情况下这些字符不存在。只有当实际加载时,相关的Unicode编码点才会对应到一个有效的索引位置。
通过这种方式,字体加载器就能够处理Unicode字符的映射,将每个字符正确地存储并映射到字体表格中,确保字符能被正确渲染和显示。
释放与 loaded_font 变量相关的内存
假设我们创建了一个表格,那么也应该实现一个机制来释放这个表格的内存。这意味着需要有一个清理函数,用来清除所有的表格和字形(glyphs),同时释放与其相关的所有内存。具体来说,这个函数应该能根据索引清空每个字符的字形数据。
虽然没有人会频繁地去释放这些事件,但为了确保代码的健壮性,最好还是做得完整和正确。如果最终真的需要释放内存,那么就应该为这一操作做好充分的准备和实现。


将最后的更改传播到代码中
在这个过程中,首先会分配内存来存储字体的数据,并为表格分配足够的空间。接下来,会将表格中的内容清零,然后为字形表分配相应数量的字形 ID(bit map ID)。这些字形表的大小会根据最大能容纳的字形数量来决定。
同样的操作也会进行在水平间距(horizontal advance)上,分配内存并清空内容。这些操作的构建过程是一样的,唯一的不同之处在于,目前无法进行这些操作,因为还没有准备好字形映射(mapping)。
在映射两个字形之前,我们不能引用一对字形的字距
在这个过程中,需要处理字符对的(kerning)问题,但目前没有方法直接将字符对映射到字形。为了做到这一点,需要在所有字符映射完成之后,才可以进行字符对的"kerning"。这是因为当前的字形索引实际上是Unicode索引,需要通过映射表来转换,但映射表的构建需要在提取所有字符之后才能完成。
在完成所有映射之后,可以通过为每个字形存储"KerningChange"的方式来解决这一问题。具体来说,可以将这个"KerningChange"直接存储在字体记录中的字形(glyph)数据中,之后在需要时就可以直接应用。
处理"kerning"的方式是将每个字形的"KerningChange"应用到相应的字符上,更新字符的"水平间距"值。这样,所有的"KerningChange"都可以在最后阶段一次性完成,不需要单独处理。
此外,还可以为"水平间距"提供一个实用函数,以便在多个地方调用,以避免重复的代码。这个函数的作用是简化对"水平间距"的查找操作,使得多个操作都可以使用统一的处理方式。
在处理过程中,字形索引(glyph index)会在特定的处理阶段被分配。具体来说,通过传递字符代码点和字形索引,可以加载字形索引并进行渲染。这时,字形索引实际上可以从事先构建的代码点表中获取,完成这一过程后,整个映射过程会变得顺利且高效。

在将字体写入资产文件之前,最后一步修改水平推进表
【kerning】 n. 字距调整 [计] 字距调整 原型: kern; 原型变换形式: 现在分词;
在处理字体的映射和字符的"kerning"时,计划将这些步骤移动到处理的最后阶段,确保所有字符都已经提取完毕。具体来说,在写出字体数据之前,需要进行"kerning"的处理,确保字符对的"kerning"已经完成。
为了实现这一点,将引入一个新的函数,名为"FinalizeFontKerning"(完成字体kerning),并在该函数中处理kerning相关的操作。通过该函数,字体的kerning工作将在所有字符都提取出来之后进行,确保每个字符的字形索引(glyph index)都已经被正确转换,并能够通过映射表进行查找。
在具体操作中,首先需要选择正确的字体,提取其中的字符对数据,然后将每个字符的代码点转换为字形索引。这样一来,字形索引就可以直接用于查找表格中的数据。
在实现时,还需要确保内存释放操作的正确性,虽然此时内存释放操作可能没有被完全实现,但这并不会影响最终的处理。
最终,在"FinalizeFontKerning"函数中,将对字形的计数、宽度等信息进行处理,并将所有数据写出。这些操作完成后,所有的kerning和映射步骤都将结束,字体数据也会被正确写出。

(黑板)只写我们将使用的水平推进表部分
目前的实现方式存在一个问题,即在写出最终的字体数据时,表的大小比实际需要写出的数据要大。这意味着不能直接一次性写出整个表,而是需要逐步写出部分数据,以确保只输出实际需要的部分。
具体来说,整个表格的结构是一个完整的大表,而最终需要写出的只是其中的一个子集。输出数据时,需要按照行的方式逐步写出每一部分,并在写出每一行后跳过那些不需要写出的部分。
造成这一问题的关键原因在于 最大字形数(max glyph count) 和 实际字形数(actual glyph count) 之间存在差异,导致表格中有大量未使用的空间。因此,在写出数据时,需要确保只写出有效的数据部分,而跳过那些未使用的空白区域。
在具体实现时,写出操作需要按照行进行,写完一行后跳过不需要的部分,然后继续写出下一行,直到所有有效数据都被正确写出。这种处理方式能够确保最终的输出数据仅包含必要的信息,并避免写出多余的无效数据。
开始实现部分存储水平推进表
在当前的实现中,我们需要确保正确地写出 水平进位(horizontal advance) 数据,并且按照正确的方式进行逐行处理,确保最终的表格大小符合实际需求,而不会包含额外的无效数据。
具体实现步骤:
-
遍历字形索引(GlyphIndex):
- 使用
GlyphIndex
进行循环,范围是[0, GlyphCount]
,确保遍历所有有效的字形数据。
- 使用
-
确定切片大小(HorizontalAdvanceSize):
- 由于这里处理的是 二维数组的切片 ,所以
HorizontalAdvanceSize
仅仅对应单行的数据,而不是整个表格的大小。
- 由于这里处理的是 二维数组的切片 ,所以
-
按步长(stride)写出数据:
- 通过指针操作,逐行写出
HorizontalAdvance
数据。 - 每次写完一行后,指针增加 完整行的大小,以正确地跳过无效部分并写出下一行。
- 通过指针操作,逐行写出
-
与位图复制(bitmap copy)类似的处理方式:
- 这一逻辑类似于之前在位图处理时的复制操作,不同的是这里处理的是 2D 数组的切片数据,而不是位图。
改进点:
- 避免 一行少一个元素 的情况,确保分配的表格大小足够,以防止 越界错误(off-by-one error)。
- 逻辑上确保 最大字形索引不会超出表格范围 ,保证
MaxGlyphCount
计算正确。 - 代码结构更加清晰,变量命名和使用更加合理,提高可读性和维护性。
最终效果:
该实现能够高效地写出 正确大小的水平进位数据 ,避免了写出无效数据,并确保所有的字形数据都能正确存储和访问。
释放相关内存ε
在当前的实现中,释放内存是一个重要的步骤,以确保资源不会被浪费,并避免 内存泄漏(memory leak)。
具体实现细节:
-
释放字体相关的资源
- 在完成所有必要的处理后,确保正确释放 字体数据表(如映射表、字形索引表等)。
- 这些数据表在分配时可能占用较大的内存,因此释放它们可以优化 内存使用。
-
释放字形映射表(glyph mapping table)
- 该表用于存储 Unicode 码点到 字形索引 的映射,确保在不再使用时进行释放。
- 由于该表较大(覆盖整个 Unicode 码点范围),适时释放能够节省大量内存。
-
释放水平进位数据(horizontal advance data)
- 该数据表存储了字形的 水平进位信息,用于计算字符间距。
- 在字体资源不再使用时,同样需要正确释放。
-
统一管理释放操作
- 可能会创建一个 统一的释放函数 ,确保 所有相关资源 都能在适当的时机被释放,而不是分散在各处手动释放。
- 这样可以避免 遗忘某些资源的释放 ,提高代码的 可维护性 和 可靠性。
-
避免二次释放(double free)
- 在释放指针前,通常会检查指针是否为
NULL
,以防止重复释放导致程序崩溃。 - 释放后,通常会将指针设置为
NULL
,确保不会错误地访问已释放的内存。
- 在释放指针前,通常会检查指针是否为
改进点:
- 确保所有动态分配的资源都被正确释放 ,避免 内存泄漏。
- 提供统一的资源管理方式 ,减少手动管理的复杂度,提高代码的 稳定性。
- 优化释放顺序 ,确保 先释放子资源,再释放主资源,防止错误访问已释放的内存。
最终效果:
正确的释放策略可以确保 内存使用高效、系统稳定 ,并避免 意外的程序崩溃或性能问题 。
在结束前将代码保持在可编译状态
当前的实现接近完成,整体流程已基本清晰,但仍有部分细节需要进一步完善。
具体实现细节:
-
字形索引的存储和映射
- 主要操作是将 字形索引(glyph index) 存入 映射表 ,以便正确映射 Unicode 码点 到 具体的字形。
- 代码逻辑确保在 正确的位置 存储 字形索引 ,并能通过 码点查询 获取相应的 字形索引。
-
调整索引存储逻辑
- 现有逻辑会提取 字形索引 并存入表格,同时调整相应的 码点映射,保证映射关系的正确性。
- 修正了变量命名问题 ,例如
CodePointCount
并非实际存储的正确字段,应调整为 GlyphCount,确保代码逻辑清晰。
-
优化
horizontal advance
存储- 水平进位(horizontal advance) 需要正确映射到 字形索引,确保字体渲染时的间距计算正确。
- 改进了存储方式 ,保证
horizontal advance
逻辑与glyph index
对应关系正确。
-
修正
CodePointCount
变量命名- 变量
CodePointCount
已调整为GlyphCount
,统一名称以提高代码可读性。 - 保证所有涉及字形数量的计算 都基于
GlyphCount
,减少混淆和潜在的错误。
- 变量
-
编译状态
- 目前代码 可以正确编译 ,但仍需进一步 整理和优化,以确保所有逻辑正确执行。
- 现阶段主要目标是保证功能 完整 ,并让最终代码 可读、可维护。
改进点:
- 确保所有索引操作的正确性 ,避免 错误映射。
- 优化变量命名,提高代码可读性 ,避免
CodePointCount
和GlyphCount
之间的混淆。 - 保证
horizontal advance
数据的正确性 ,确保其与 字形索引一致,避免数据错位。 - 整理代码结构,提高可维护性,确保后续修改更容易进行。
最终效果:
当前的实现即将完成,剩余的部分将在 下一个开发阶段 继续优化,并最终完成所有功能的实现。



我注意到你很少写代码注释。你的注释主要是 TODO 类型的注释。考虑到很多代码比较复杂,你如何看待写注释,除非你认为它并不复杂?或者你认为自己写得好,所以代码不需要注释来增加可读性?
在代码编写过程中,注释的作用 以及 是否应该添加注释 是一个需要权衡的问题。
关于代码注释的观点:
-
注释容易过时,反而会导致误导
- 代码会不断变化,而注释不会经过编译器的校验,无法保证始终正确。
- 一旦代码发生变化,但注释没有同步更新,阅读者可能会被错误的注释误导,导致理解偏差,甚至引发 bug。
- 代码的演化速度很快,维护过时的注释比直接阅读代码 还要浪费时间。
-
代码应当具备自解释性
- 良好的代码结构、清晰的变量命名、合理的函数拆分 比写大量注释更重要。
- 如果代码本身写得清楚,逻辑自洽,那么不需要依赖注释也能快速理解其功能。
- 过多的注释可能反而会掩盖代码的核心逻辑,降低可读性。
-
在特定情况下,注释仍然有价值
- 当代码稳定且不会频繁修改时,在关键部分添加详细注释可以帮助未来的维护者快速理解。
- 对于复杂的算法、涉及外部依赖或约定的部分,适当的注释能减少理解成本。
- 在代码模块长期不维护时,添加注释记录关键逻辑,防止时间久了之后遗忘设计思路。
- 适合在 头文件(Header)或者文档 中提供大段说明,而非在代码中逐行添加冗长的注释。
最佳实践总结:
- 避免在频繁修改的代码中添加注释,因为它们很快会变得过时并产生误导。
- 通过清晰的代码风格替代注释,让代码本身具备可读性。
- 仅在必要时添加注释,尤其是涉及复杂逻辑、特殊约定、长期维护的代码部分。
- 在代码稳定后再添加注释,避免随着代码变动而反复修改注释。
- 对于长期不维护的代码,可以在文件顶部编写详细的注释说明,以备未来使用。
结论:
在代码仍然处于开发和调整阶段时,写注释的价值远不如编写清晰的代码本身 。频繁变化的代码很难保持注释的同步更新,最终会造成比没有注释更大的问题。合理的代码结构与良好的命名 本身就是最好的注释,而注释应该主要用于记录那些不会轻易改变的重要信息。
为什么你最初计算的字形表索引大小太大(64k),后来缩小到2k,但最终又回到 64k?是什么改变了大小?
在计算 Gliff 表索引大小 时,曾经历过几次调整,原因如下:
-
最初计算出的表索引大小过大
- 最开始计算出的表大小高达 5000×5000 ,大约占用 25MB,这个大小在可接受范围内。
- 但如果按照固定的 65536×65536 计算,整个表将达到 4GB ,这显然过于庞大,占用过多内存,因此需要调整。
-
缩减表大小
- 由于固定的 65536×65536 方案占用空间过大,因此进行了一次缩减,减少表的大小,以 降低内存占用。
- 通过限制存储范围,使得表的大小不会过度增长。
-
后续调整回 4K 及更大规模
- 在后续优化时,又回到了 4K 的规模,这可能是为了在保证功能的同时减少资源浪费。
- 具体回调 4K 的原因可能涉及 性能优化 或 存储格式的调整,以确保表的效率和可用性。
核心原因总结:
- 计算表索引大小时,需要在存储空间 与性能之间权衡。
- 初始计算的 5000×5000 占用 25MB,在可接受范围内。
- 固定 65536×65536 占用 4GB,过大,导致调整方案。
- 最终折中回 4K 大小,以兼顾存储效率与性能优化。
写完所有代码后再加注释给别人看,这样做是个好习惯吗?
在代码完成后再添加注释是一个好的实践,但前提是代码已经基本稳定,不会再进行大量修改(除了小的 bug 修复)。如果代码仍然处于开发过程中,不建议添加注释,因为这样做往往弊大于利。
关于何时添加注释
- 代码完全开发完毕,不再进行结构性修改时,才值得添加注释。
- 如果代码仍在不断变化,注释很容易过时,反而会误导阅读代码的人。
- 代码完成后,适当的注释可以帮助未来的维护人员快速理解代码的整体架构和设计思路。
如何编写有效的注释
-
不要写无用的注释
-
例如:
cint count = 0; // 变量 count 设为 0
-
这种注释完全没有价值,因为代码本身已经非常直观。
-
-
注释应该补充代码无法表达的信息
-
例如,如果某个变量用于多线程环境 ,可以在注释中说明其使用方式:
cint sharedCounter; // 该变量在多线程环境下使用,必须加锁访问
-
这类注释能提供代码无法直接体现的信息,帮助开发者避免潜在问题。
-
-
关注系统级别的说明,而不是单个函数
-
代码中的函数通常可以通过函数名和实现理解其作用,没必要每个函数都写详细注释。
-
更重要的是在模块级别提供整体介绍 ,说明整个系统的设计目的、数据流、关键概念等,例如:
c/* * 资源管理系统(Asset System) * 该系统用于从磁盘加载打包的资源文件,并根据资源类型组织数据。 * 资源文件遵循 .asa 格式,每个文件包含多个资源条目,索引存储在扁平化的哈希表中。 * 主要组件: * - 资源加载器(Loader):负责解析 .asa 格式并创建资源对象 * - 资源缓存(Cache):用于管理已加载资源,避免重复加载 * - 资源释放管理(Eviction):使用引用计数 + 代数系统(Generations)来确保资源不会被错误释放 */
-
这比在每个函数中加"这个函数是加载资源的 "的注释有价值得多,因为它能帮助开发者理解整个系统的架构。
-
-
解释关键概念,而不是代码本身
-
例如,不需要 这样注释:
cgeneration++; // 变量 generation 自增
-
但如果这个
generation
变量的用途是防止资源被错误回收,那就值得加注释:c// 代数系统(Generation System):用于确保资源不会在仍被引用时被回收 generation++;
-
总结
- 代码未完成时,不要急着写注释,否则很可能会过时,造成误导。
- 注释应该提供代码无法直接表达的信息,比如多线程、性能优化、资源管理策略等。
- 注释重点应该放在系统级别的整体说明,而不是单个函数的实现细节。
- 要注释"为什么"和"概念",而不是注释"怎么做",这样才能真正提升代码的可维护性。
由于今天问题较少,你认为什么时候会写一个合适的 RNG?
当前问题较少,讨论到何时会编写一个真正的 RNG 。目前来看,唯一真正需要关注这个问题的时机 可能是在进行世界生成的时候。在那时,才有必要认真考虑和实现它。
或者一个真实的哈希函数?
在编写代码时,并不会真正去实现一个真正的哈希函数。
每个发布的游戏代码中,几乎都会有一条类似的注释:"更好的哈希函数"(Better Hash Function),这已经成为了一种惯例。如果有一天检查代码时,发现没有这条注释,那么从某种角度来说,这个游戏都不能算是真正的游戏。
当然,现有的哈希函数可能并不完美,因此可以挑选其中的一两个进行改进,使其更加合理和高效。但是,只要至少 有一个地方保留了"更好的哈希函数"这个注释,那么整体上就仍然符合预期,并不会影响游戏的"正统性"。
我的注释写作规则是:"代码应该清楚地告诉你在做什么;如果需要注释,它们应该只告诉你为什么"
对于编写代码的原则,通常的做法是代码本身应该能够清楚地表达正在做什么。如果需要注释,那么注释应该仅仅告诉你为什么这么做。
但其实,并不完全赞同这种做法,重点不在于"做什么"或"为什么",而是注释的细节层次 。更重要的是需要提供一个大局观的注释。大局观注释可能会涵盖"做什么"的部分,但更侧重于提供整体的理解。
至于细节部分,其实并不需要注释,因为这些细节正是代码本身应该完成的内容。如果代码本身难以理解,可能需要考虑重新编写代码,使其更加易读。比如,可以通过更合适的命名来让代码更清晰易懂。