游戏引擎学习第175天

发布于:2025-03-23 ⋅ 阅读:(24) ⋅ 点赞:(0)

回顾和今天的计划

今天的主要任务是完成稀疏 Unicode 支持。之前我们已经完成了所有的思考和设计工作,但代码部分尚未完成,因为有许多内容需要调整和重构。因此,今天的目标就是把这些内容全部整理好并最终实现。

回顾当前测试资源构建器的状态

上次的修改还没有完全完成,尤其是**测试资产构建(test asset builder)**部分的调整仍然存在问题。因此,今天的第一步是检查相关代码,确保所有修改都真正完成。很可能仍有未完成的部分,需要进一步完善后再进行加载和测试。

目前的代码结构中,我们将所有内容移动到了外部,并在 .h 文件中定义了 loaded_font 结构。在 loaded_font 里,我们存储了一些非稀疏的数据。例如,字形(glyphs)存储在稀疏表中,而水平方向的前进量(horizontal advance)则并非完全稀疏,而是只在某个维度上稀疏。这是因为用户可能会添加不同数量的字形,所以结构需要灵活处理。
在这里插入图片描述

此外,还实现了 GlyphIndexFromCodePoint 这一转换表,用于从特定的 Unicode 码点(code point)映射到字形索引。这一部分是稀疏存储的,而 HorizontalAdvance 在某个轴向上稀疏但在另一个轴向上是稠密的。至于 glyphs 数据本身是稠密存储的,也就是说,所有实际使用的字形是紧密排列存储的。
在这里插入图片描述

在数据存储时,每个字形都会关联其对应的 Unicode 码点,并存入相应的结构中。目前的实现方式基本合理,接下来需要进行整体检查,以确保代码结构完整、逻辑清晰,至少能够通过基本的合理性检查

具体到 LoadFont 这部分,它的主要作用是加载字体,并返回 loaded_font 结构。所有内容都被封装到结构体中,以便支持多个字体加载。例如,通过 SelectObject 选择对象,并确定 MaxGlyphCount(最大字形数量)。
在这里插入图片描述

最大字形数量的设定是一个经验值,我们假设不会有超出此数量的字形,并以此作为上限。因为这部分仅适用于资产处理阶段(asset processor),而非运行时(runtime),所以可以在后续根据需要进行调整。

在进一步检查代码时,发现 GlyphIndexFromCodePointSize 相关逻辑可能有问题。看起来 loaded_font 不应该按照当前的方式进行存储,因为这样显然是不合理的。这部分代码可能需要调整,使 GlyphIndexFromCodePointSize 仅分配一次,而不是错误地为每个 loaded_font 进行分配。
在这里插入图片描述

关于类型安全内存分配

在代码编写过程中,使用特定的方式可以减少类型相关的错误。例如,在实际代码中,PushArray 这种方法会进行类型转换,并传递计数值,同时使用相同类型的 sizeof,这样可以在一定程度上增加类型安全性。这种做法类似于更强类型的语言中 new 关键字的作用,有助于避免某些错误。因此,在正式代码中,会更加谨慎地使用这些方法,以减少潜在的 bug,而在一些临时测试代码中,这方面可能不会特别注意。

这次遇到的 bug 也是一个典型例子,正好可以借此机会强调这一点。这种方法并不是说直接“捕获”错误,而是通过正确的大小计算和类型匹配,使得错误不会发生。

在代码实现上,首先需要存储 glyphs,所以进行内存分配,并清除已有数据。这一步非常重要,因为需要在一开始就将所有内容清空,以确保不会受到之前数据的影响。

在检查代码时,发现目前尚未有 null glyph,但最好增加一个,这样在处理字符映射时会更灵活。

然后,开始对 Font->Glyphs 进行分配,malloc 申请存储空间,数量取决于最大可能的字形数(MaxGlyphCount)。

同时,对水平方向的前进量(horizontal advance)进行类似的存储分配。这里的 horizontal advance 其实在两个轴向上都是稠密存储的,但因为它是按照最大数量来分配的,而不是根据实际使用的 glyph count,所以会有额外的空间,形成一个嵌套矩形的结构。

因此,在存储 horizontal advance 时,会先申请一块较大的矩形区域,并进行初始化,以确保后续计算不会受到未定义数据的影响。
在这里插入图片描述

为空字形预留空间

当前的实现基本上是合理的,但 glyph_count 仍然存在一些问题。需要预留一个 null glyph(空字形),因此 glyph_count 应该从 1 开始,而不是 0。这样,第 0 个字形永远不会被实际使用,以确保在处理字形索引时不会意外访问无效数据。

对于 null glyph,需要明确设置其 unicode code pointbitmap_id,以确保它不会被错误地解析为有效字符。unicode code point 应该设置为一个不会被使用的值,而 bitmap_id 需要明确标记为无效,以避免后续渲染或解析逻辑误用这一字形。

finalize_font 过程中,需要遍历所有字形配对,检查每个配对的有效性。目前的逻辑在处理配对时存在漏洞,因为在判断是否为有效代码点后,会直接执行查找操作,而不会检查是否为 0(即 null glyph)。应当避免 null glyph 参与 kerning(字距调整),因为 null glyph 只是一个占位符,不应该与任何其他字符产生影响。因此,应该在处理配对关系时,确保不会向表中写入 null glyph 相关的数据。

InitializeFontDC 过程中,主要确保 font dc 资源正确初始化,避免后续重复使用时出现异常。
在这里插入图片描述

LoadGlyphBitmap 中,确保对 code point 的赋值逻辑正确,当前代码已经在加载字形时设置了 unicode code pointbitmap_id,并正确维护了稀疏查找表,确保 glyph_index 的映射关系正常工作。
在这里插入图片描述

最后,在加载字体时,仍然需要遵循 Windows API 的调用规则,正确传递 unicode 代码点,以确保 Windows 能够正确解析并返回相应字形数据。
在这里插入图片描述

当前的字体处理流程基本完善,首先进行字体加载,然后执行一系列计算和存储操作,以确保所有字形数据正确写入。在完成主要操作后,会计算字符宽度,并遍历所有字形,更新相关信息。

在遍历字形索引的过程中,避免对 OtherGlyphIndex0 的情况进行多余的操作,尽量减少无意义的计算。不过,部分逻辑可能是之前用于修正 kerning(字距调整)的,涉及与 Windows 的对齐,因此暂时不修改,避免影响已有的调整逻辑。
在这里插入图片描述

在字体数据存储时,严格使用字形索引,而不是通过转换表进行二次查询,以确保效率。所有的处理逻辑在当前设计下是合理的。

在最终写入字体数据时,按照以下步骤进行:

  1. 遍历所有字形,计算所需的存储大小。
  2. 将所有字形数据依次写入文件,包括每个字形的具体大小。
  3. 处理水平前进量(horizontal advance),按照行的方式写入,只写入有效数据部分,忽略多余的填充空间。
  4. 通过最大字形数量(MaxGlyphCount)进行偏移,以保证数据排列符合存储格式,类似于位图操作中的子区域拷贝(blit)。

在检查字体处理代码时,确认了 initialize_font_dc 被正确调用,确保 font_dc 资源的正确初始化。此外,整体逻辑看起来是合理的,没有发现明显错误。

最后,在 bitmap_id 相关代码中出现了一个错误提示,但问题不大,主要是涉及位图 ID 处理,需要再检查相关代码逻辑,确保所有 glyph 对应的 bitmap 被正确关联。总体来看,字体处理逻辑已经完成。

运行资源构建器

在这里插入图片描述

在这里插入图片描述

接下来运行 asset builder,观察其实际执行情况,以确认字体数据的写入是否正确。当前并没有明确的错误或异常,但由于刚刚完成数据写入,还无法确定一切是否如预期般正常工作。

执行 asset builder 后,会将数据写入 game data 目录。通过检查该目录中的数据文件,可以进行初步验证。尽管目前没有发现明显的灾难性问题,但由于对数据格式尚不完全确定,仍然需要进一步检查,确保数据的正确性。

当前的调试状态,缺乏明确的反馈信息,仍需更详细的检查手段,例如:

  • 直接打印或查看存储的字形数据,以确保其格式正确。
  • 进一步调试,查看字体数据在加载时是否匹配预期的字形信息。
  • 运行游戏实际渲染字体,观察其显示是否符合预期,以确认整个流程正常运行。

目前暂时没有发现致命问题,但仍需进一步检查,以确保字体系统能够正确加载和使用所生成的字形数据。
在这里插入图片描述

回顾资源加载代码

当前在加载 assets 时,相较于之前的流程发生了一些变化,尤其是在处理字体数据时。

位图索引部分

位图索引的加载方式仍然保持不变,意味着 bitmap 相关的加载逻辑没有调整,依旧按照原有方式读取和解析数据。

字体数据的加载

字体的加载过程发生了一些变化,需要适配新的数据格式。新格式的存储结构如下:

  1. GlyphCount(字形总数)
  2. AscenderHeight(上升高度)
  3. DescenderHeight(下降高度)
  4. ExternalLeading(外部行距)
  5. 字形数据,其中包括:
    • UnicodeCodePoint(Unicode 码点):每个字形对应的 Unicode 编码
    • 其他字体相关信息
      在这里插入图片描述

新的数据格式增加了一些额外的参数,如 AscenderHeightDescenderHeightExternalLeading,这些信息可能用于文本排版,确保字体的对齐和间距正确无误。

在加载字体数据时,需要按照新的格式逐步解析每个字段,并正确映射字形索引与 Unicode 码点的关系。接下来需要检查加载逻辑,确保它能够正确解析新格式的数据,并在内存中正确构建字体结构。

我们现在不直接处理Unicode码点

在新的加载流程中,之前可以直接访问的内容现在不能再直接访问了。因此,加载字体时,需要更新处理方式。

更改内容

  1. 水平推进(Horizontal Advance):这部分内容需要重新加载,不再使用之前的方式。
  2. Unicode 码点表(Code Point Table):这部分也发生了变化,不再直接使用 CodePointsSize 来表示字形数据的大小。
  3. 字形大小(Glyph Size):之前的 CodePointsSize 不再适用,取而代之的是新的字形大小。

更新后的加载方式

现在需要读取的是字形的大小,而不再是每个码点的大小。这个变化意味着需要更新代码,确保能够正确地加载新的字形大小,而不是依赖于原来的 CodePointsSize。这意味着在加载时,必须根据新的格式处理这些数据。

虽然更新后的流程看起来没有什么问题,但需要仔细调整代码,确保正确读取并处理新格式的数据。
在这里插入图片描述

解包密集字形表

当前,我们有一个密集的字形表(dense glyph table),这对我们来说是一个问题。因为,虽然我们知道每个字形的位图 ID以及它们对应的Unicode 码点,但我们缺乏一个能够将用户输入的Unicode 码点映射到具体字形的表格。用户输入通常会是 Unicode 码点,而我们目前的表格并没有提供这样的映射机制。

解决方案:

为了处理这个问题,需要构建一个从 Unicode 码点到字形的映射表。这个表格基本上是将原来的字形表(step table)进行反向处理,使其变得稀疏(sparse)。这意味着我们需要将现有的密集字形表转换成一个可以根据 Unicode 码点来快速查找对应字形的稀疏表格。

这种反向处理的目的是为了能够更方便地根据用户输入的 Unicode 码点查找相应的字形数据,从而支持正确的字体渲染。
在这里插入图片描述

在这里插入图片描述

暂时使用过大的Unicode映射表

在加载字体时,我们还需要创建一个Unicode 映射表。问题是,这个映射表可能会非常庞大。我们知道,如果要为每个 Unicode 码点建立一个完整的查找表,这个表会占用大量的内存。具体来说,如果包含所有 Unicode 码点的查找表是一个16位的索引表,那么它的大小将会达到 2MB。这个大小对于某些情况下可能会比较大。

解决方案:

  1. 初步方案:可以从一个 2MB 的查找表开始,这样的查找表会包含所有 Unicode 码点的映射。这个表是基于每个字形的 Unicode 码点,可以直接映射到字形。

  2. 后续优化:不过,这个表可能会浪费大量内存。未来可以考虑采用 范围查找(range lookup)二分查找(binary search) 等更高效的方法,因为字形的 Unicode 码点是可以排序的。如果采用排序后的查找,可能会减小内存使用和提高查询效率。

  3. 表格存储:这个映射表将会储存在一个结构中,并且大小为16位的 Unicode 映射表,这确保了每个字体中最大不会超过 64,000 个字形(Unicode 码点)。这将意味着映射表的大小不会超过约 128 KB,远小于2MB的查找表大小。

  4. 最终决定:虽然 2MB 查找表可能是一个初步解决方案,但未来可以根据实际需要逐步优化它。初期阶段可以保持使用查找表,等系统完全确认没有问题时,再考虑做性能优化(如使用二分查找)。

最终,这个 Unicode 映射表和其他字形信息将一起存储在文件格式中,确保每个字形都有正确的 Unicode 映射关系,帮助后续的字体渲染过程。
在这里插入图片描述

在这里插入图片描述

在资源文件中存储使用过的最高码点

为了优化字形加载过程,并减少映射表的大小,可以考虑一种方法,这样做可能会解决一些潜在的问题,尤其是在处理一些特殊字符时。具体来说,可以使用一个策略来避免映射表过大。

方案:

  1. 引入最大 Unicode 码点加一:我们可以通过将最大 Unicode 码点加一的方式来简化映射表。这意味着我们不再需要为每一个 Unicode 码点都创建映射,而只需要使用最大码点加一的值来设定一个“最大值”的参考点。

  2. 避免过大的映射表:通过这种方式,我们可以确保映射表的大小不会超过某个指定的范围。比如,最大码点加一可以作为新的上限,这样就可以避免映射表过大,从而减少内存消耗。

  3. 具体操作:在加载字体时,可以先将该最大码点加一的值初始化为零。然后,每次使用时,检查当前使用的 Unicode 码点。如果该码点大于或等于当前设定的最大码点加一的值,就可以将其重置为新的“有效”值。

  4. 确保有效性:如果在使用过程中遇到码点大于或等于最大 Unicode 码点加一的情况,可以将该码点值调整为 最大码点 + 1,确保它不会超过最大范围。

总结:

这种方法能够有效地减少映射表的空间需求,避免使用过大的查找表,同时仍然能够保证每个 Unicode 码点都有相应的处理。虽然这种方法不会立即解决所有问题,但它为将来的优化提供了一个灵活的方案,可以根据实际需求进一步调整。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

UnicodeMap

在这里插入图片描述

在这里插入图片描述

在编程的过程中,需要注意的一些细节被总结如下:

  1. 加载字体时的 Unicode 映射表:在加载字体时,需要确保 Unicode 映射表正确地被加载。由于在字体加载时并没有直接包含字形(glyph),因此需要确保映射表包含正确的数据。为了支持这一点,必须先计算出 Unicode 映射表的大小,并在内存中分配相应的空间。这个过程涉及到计算 Unicode 映射表的大小,并通过 AcquireAssetMemory 函数申请内存。

  2. 字形数据和 Unicode 映射表的顺序:在内存分配和加载过程中,首先加载字形数据,然后紧接着加载 Unicode 映射表。Unicode 映射表应该位于字形数据之后,按照一定的顺序进行内存安排。

  3. 内存分配:在进行内存分配时,特别是涉及到 Unicode 映射表时,要确保分配的内存大小足够,并且数据的排列顺序合理。需要在内存中为字形数据和 Unicode 映射表分别分配空间。

  4. 内存数据结构:例如,字形数据是以 16 位大小存储的,而 Unicode 映射表需要与字形数据紧密相连,因此要确保这两者的内存布局是连贯的,并且能有效管理。

  5. 附加信息:在处理内存和数据时,还需要注意额外的调整和注意事项,比如数据的顺序、内存对齐以及如何管理额外的空间。

总结来说,程序中涉及到内存管理和数据顺序时,确保对 Unicode 映射表和字形数据的内存进行合理分配是非常关键的。这需要在内存中正确排序,确保每个部分都能被有效读取和使用。

通常我们不会手动进行复杂的子分配

在处理内存对齐和数据分配时,采取了一些特定的策略:

  1. 内存对齐和子分配:在进行内存分配时,考虑到内存对齐的需求,采用了子分配的方法。通过为每个资产类型分配少量内存,确保字形和相关数据的对齐。虽然这种操作看起来简单,但它确保了内存的有效使用,避免了不必要的浪费。

  2. 简化内存分配:由于这种内存分配的操作不会频繁发生,因此没有专门为此创建一个系统。通常,面对需要频繁进行内存分配的情况时,会为这些操作创建一个统一的系统来减少错误的发生,并提高开发效率。但是在当前情况下,认为这种操作的频率较低,不值得额外为此构建一个复杂的内存管理系统。

  3. 代码的简化:通过这种方法,可以减少重复的代码,也使得内存分配更加直观和容易管理,特别是在只进行少量内存分配时。

  4. 字体数据的更新:加载的字体数据现在包含了 Unicode 映射表,并且字形数据也得到了更新。每个字形(glyph)现在会通过新的数据结构进行管理,确保能够正确处理字体中的每个字符。Unicode 映射表和字形数据的内存布局已经经过了调整,以适应新的要求。

  5. 修改代码结构:加载的字体数据结构中加入了新的成员,包括 Unicode 映射表和字形数据。这些变化需要在代码中进行适当的调整,以确保能够正确读取和使用这些新的字段。

总结来说,通过调整内存管理和数据结构,优化了字体数据的加载过程,确保了内存的高效使用,并为未来可能的需求做出了灵活的准备。

在字体加载后添加额外的终结代码

在处理字体加载时,发现了一个问题:目前还没有构建 Unicode 映射表。为了解决这个问题,需要在加载字体数据时,处理和准备这些数据,确保 Unicode 映射表能够正确生成。

  1. 初始化和准备:在加载字体数据时,必须在读取文件数据并返回之后,进一步处理这些数据。如果是字体文件,需要进行额外的处理步骤。例如,可以通过在加载资产时增加一个步骤来完成这一操作。

  2. 额外工作项:为了解决这个问题,可以为资产加载过程增加一个“额外工作”的选项。例如,在资产加载完毕后,通过一个开关语句检查是否需要做其他处理。如果需要处理,则可以调用相应的操作(比如生成 Unicode 映射表)。

  3. 状态更新与处理:为了避免状态不一致,在加载过程中,需要确保在加载完数据后,检查是否需要生成或更新 Unicode 映射表。可以通过一个简单的开关语句来判断当前需要进行的操作,例如如果是字体类型,就生成 Unicode 映射表。

  4. 终结操作:对于完成加载的操作,可以通过“卸载资产”阶段执行必要的终结步骤,确保所有必要的操作都已经完成。比如在卸载时,可以进行一些最终清理工作。

总结来说,为了确保 Unicode 映射表能够正确生成,需要在加载资产后添加额外的处理步骤,通过状态控制和终结操作来确保整个过程的顺利进行。
在这里插入图片描述

构建Unicode映射表

为了构建 Unicode 映射表,步骤其实相当简单:

  1. 确定字形数量:首先,我们知道有一些字形需要处理。第一个字形是特殊的,不需要关心它。因此,我们可以遍历其他字形,逐一处理。

  2. 检查字体是否加载:在构建映射表之前,必须确保字体已经加载。如果字体没有加载,构建映射表是没有意义的。因此,需要检查字体是否已经正确加载。

  3. 获取字体信息:通过获取字体的资产信息,可以获取加载的字体列表。加载的字体包含了所需的字形数据,包括每个字形的 Unicode 代码点。

  4. 处理字形并填充映射表:对于每个字形,我们获取它的 Unicode 代码点,并将该字形的索引存储在映射表中。此时,映射表中的位置对应于 Unicode 代码点,并且存储的是字形的索引。

  5. 确保映射表大小合适:映射表的初始状态是空的,为了确保后续填充映射表时不会出错,需要先将映射表清空。这样可以确保每个位置从一开始就是零,避免出现未初始化的值。

  6. 验证映射表的有效性:在填充映射表时,还可以通过断言来验证每个 Unicode 代码点是否小于最大支持的 Unicode 代码点,以确保填充过程中不会超出限制。

  7. 最终清理和填充:当所有字形的 Unicode 代码点都填充到映射表中后,映射表就完成了。此时,映射表将变得更加稀疏,因为只有在 Unicode 代码点存在字形时,才会有值。

总结来说,构建 Unicode 映射表的过程包括了:检查字体是否加载、获取字形信息、构建映射表并填充字形索引,确保映射表大小合适,并通过断言和清理确保最终结果正确。
在这里插入图片描述

实现ZeroArray辅助函数

在处理字体加载和映射表构建时,决定采取的方案是:

  1. 清空映射表:在开始填充映射表之前,首先要确保将映射表清空。这样可以确保每个位置都从零开始,避免未初始化的值。这是一个好主意,能确保后续填充过程的正确性。

  2. Unicode 代码点的最大值:虽然一开始考虑要对 Unicode 映射表进行大小检查,但现在回想起来,已经不需要因为布局系统已经能够处理这些信息。尽管如此,仍然觉得清空映射表的想法是有益的,能够确保数据的有效性。

  3. 优化操作:考虑到清空映射表是一个简单但有效的操作,决定继续推进这个方案,并且已经准备好设置最终的操作步骤。

  4. 处理指针问题:在做这个操作时,涉及到一些指针转换问题。当前需要确保资产转换过程能够正确进行,以避免出现无法转换的情况。

总结来说,决定继续清空映射表并进行后续处理,虽然有些细节最初看似多余,但最终还是认为这样做可以提高代码的健壮性和可维护性。
在这里插入图片描述

在这里插入图片描述

通过双重转换防止溢出

在构建 Unicode 映射表时,接下来需要做的几个步骤包括:

  1. 断言检查:为了确保代码的正确性,需要对 Glyph 索引做断言检查。具体来说,检查使用的 16 位 Glyph 索引是否等于实际的 Glyph 索引,这样可以确保在映射过程中没有发生溢出。

  2. 加载后处理:在加载字体数据后,还需要添加一个最终的操作(finalize)来完成一些必要的清理或检查。这是为了确保所有资产都在加载完成后正确处理。尤其是对于字体数据,必须在其他类型的资产加载后,单独进行字体的最终化处理。

  3. 代码整理和优化:虽然目前的代码能顺利编译,但需要减少重复的模板代码,因为每次都需要重复这种格式化操作,显得非常繁琐。这是一个优化目标,计划在未来简化这一部分内容。

  4. 代码的稳定性:目前代码虽然能够编译,但并不一定完美,需要在后续进一步测试和调整,确保它能够稳定运行。

总结起来,当前的关键任务包括确保索引映射的正确性,添加加载后的处理步骤,以及优化代码结构以提高代码的可维护性和效率。
在这里插入图片描述

用字形替代码点

接下来的步骤是修复代码中涉及到代码点(code point)的地方,因为我们希望这些部分处理的是 Glyph(字形)而不是代码点。具体的处理方式包括:

  1. 修改代码点为字形:所有涉及到获取横向推进(HorizontalAdvance)或(GetGlyphFromCodePoint)的地方,都需要改为处理字形。也就是说,当代码中使用了“GetHorizontalAdvanceForPair”或“GetGlyphFromCodePoint”这些方法时,需要修改为处理字形的版本。

  2. 修改函数和方法:具体而言,将 GetClampedCodePoint 方法替换为 GetGlyphFromCodePoint`,这样我们在传递代码点时,实际处理的将是对应的字形。这一步是为了确保代码点被正确转换成字形,而不是仅仅作为一个代码点存在。

  3. 修改代码中涉及字体的部分:当涉及到字体的代码点时,要确保这些代码点被映射到字形,并且这些字形是通过相应的方法获取的。此时,“GetGlyphFromCodePoint” 方法将被用来将代码点转换为字形。

  4. 调整偏移量:确保所有的值都按照预期的方式进行偏移,这样可以避免出现偏差或错误,确保字形处理的准确性。

总结来说,主要的调整是在所有涉及到代码点的地方进行修改,使它们改为处理字形,同时确保这些修改后的代码能够正确映射字形,并且字形的获取和处理方式符合预期。这样,代码的逻辑将更加清晰和一致。
在这里插入图片描述

实现字形查找例程

现在我们需要实现 GetGlyphFromCodePoint 方法。具体的步骤如下:

  1. 检查代码点是否在范围内:首先需要确认传入的代码点是否在有效的范围内,即是否小于等于最大的有效代码点。如果代码点在范围内,接着继续执行后续步骤。

  2. 获取字形索引:如果代码点有效,字形索引将根据字体的 Unicode 映射表获取。我们从映射表中查找该代码点对应的字形索引。

  3. 确保索引有效:在获取字形索引后,我们需要通过断言(assert)确保该索引小于字体的字形总数。因为字形索引不能超过字体中实际存在的字形数量,这样能防止超出范围的错误。

  4. 返回字形索引:一旦确认字形索引有效,返回该索引以供后续使用。

总结来说,GetGlyphFromCodePoint 方法的目的是验证代码点的有效性,获取对应的字形索引,并确保该索引在有效范围内,最后返回该字形索引。这些操作保证了我们在处理字形时不会出现越界或无效的错误。

调试今天和昨天的更改

首先运行资产打包工具和旧的自定义资产构建器。接下来,需要进行基准测试,检查当前的状态,看看到底发生了什么。需要检查程序是否会崩溃,或者是否能够正常运行。根据结果,调整程序,确保不会发生崩溃。

没有字体显示。让我们检查字体加载代码

首先,发现屏幕上没有任何内容显示,所以需要检查加载过程。为此,我们将从进入 load font 函数开始,查看加载过程中的信息,检查字体相关的数据和尺寸。这样可以了解加载过程中是否存在问题,找出任何可能的错误。在此过程中,需要注意一些值和信息的大小,以便找出加载失败的原因。

OnePastHighestCodepoint已设置但未保存到资源文件

在调试过程中,发现没有显示内容的原因是之前跟踪了一些值,但却没有实际写入它们,导致这些值没有被正确设置。经过分析,发现字体数据已经写出,但是加载过程并没有正确记录字体的相关信息,这就是导致问题的根源。

进一步调查后,发现虽然设置了一些字体数据,但并没有正确地记录在加载的字体对象中。由于加载过程中的顺序问题,字体数据没有被正确传递和使用,这使得最终的加载结果并不符合预期。

接下来,通过设置断点来调试,验证字体的代码点是否被正确处理。确定了最高的代码点值,并确认它符合预期的范围,这使得调试进展更加顺利。尽管分配的内存较大,但这是正常情况。

在此基础上,将继续进行内存分配和后续的调试步骤。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

字体加载代码中的另一个小错误

在调试过程中,发现了一个错误,因为之前没有正确检查一些条件,导致程序假设没有任何失败发生。现在已经修正了这个问题,确保所有的检查都被正确执行。

接下来,开始逐个检查字形(glyphs)是否正确,确保它们符合预期的标准。通过逐个检查字形,确认它们的表现是准确的,而不是完全错误的。最终,所有的字形数据看起来都符合预期。
断点没进来
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

显示game Owl Unicode码点

在这个阶段,开始考虑输出一些其他的调试信息,比如重新添加一个小的“Owl”图标作为测试元素。既然已经完成了获取这些数据的工作,接下来可以通过调试测试一下我们的Unicode是否正常运行。

通过这种方式,进行调试文本输出,并检查是否一切按预期工作。之前的调试代码已经做了一些设置,现在可以进一步验证这些内容的正确性。

在这个过程中,决定进行一些调试操作,尤其是测试如何输出Unicode码点。为了这个目的,使用了一个相对不常见的方式来展示Unicode字符。首先,尝试通过打印出对应的Unicode码点来调试,方法是将字符转换为十六进制表示,并通过拼接字符来实现这一功能。调试时,还进行了字符的范围检查,确保字符在合法的范围内。

其中,为了处理十六进制字符,设计了一个辅助函数GetHex,它根据字符的值来判断并返回对应的十六进制数值。如果字符是数字字符,就会直接计算;如果字符是字母字符,则进行相应的转换。这个过程显得有些繁琐,但它对于调试来说是必须的。最终通过这些步骤,成功地输出了字符的Unicode码点。

在测试时,使用了一个“小耳木兎”符号作为调试对象,通过不断调整代码,最终实现了将字符和其Unicode码点输出到控制台。调试过程中,遇到了一些错误,比如没有正确处理大小写字母的问题,但通过调整代码,逐步修复了这些问题。

测试的目的是确认字体是否能正确加载,是否能够正确处理Unicode字符的映射,以及字体是否能够正确显示这些字符。通过对字符的逐个检查,发现大部分字符都能正确显示,调试代码也成功运行,但仍需进一步验证字体是否完全加载和正常显示。

总结来说,这个过程涉及了多个调试步骤,从输出Unicode码点到处理字符映射,最终确保字体能正确显示预期字符。

在这里插入图片描述

在这里插入图片描述

Kanji码点存在,但没有正确的字距调整

目前已经完成了大部分工作,但字体的渲染并没有完全符合预期,存在一些问题。尽管大部分内容看起来是对的,字符的排列还是没有按照预期的方式正确显示。可能是字符对齐的处理没有与当前实现方式完全兼容,或者字体表格中存在某些错误。尽管如此,字体的渲染和显示问题并没有完全出错,因此认为整体工作还是相当成功的。

接下来可能需要进一步调整,检查字符的对齐方式,或者探讨从Windows中提取数据的方式,确保能够精确对齐,同时保留其他功能的正确性。虽然目前的结果还不完美,但已经接近目标,可以继续深入调试和优化。

那么,我们现在完成字体了吗?是否可以转向调试的其他部分?

目前字体的处理大部分已经完成,但仍然存在一些问题。具体来说,字符对齐的情况不完全正确,因此需要进一步调试和解决这些问题。可能的问题包括字符的显示顺序不对,或者是字符在处理时被错误解释了。虽然问题并不复杂,但仍然需要找出具体的原因,以确保一切能够正常运行。因此,在移动到其他部分之前,需要先解决这些字体相关的调试问题。

后面我们将转向哪个部分?

接下来,我们将开始处理调试代码部分。这意味着我们需要进行一些调试构建,确保我们能够查看内存布局、线程活动等相关信息。这是非常重要的,因为这样可以帮助我们深入了解程序的执行情况。接下来的几周,我们将花时间做这部分工作,确保一切正常运行。同时,还需要确保能处理好调试过程中可能出现的各种问题,包括如何查看和解析字符映射等相关问题。

这可能就是字距调整的方式

在检查当前字体时,发现它可能没有正确显示,尽管Windows报告了相关信息。通过查看其实际输出,发现字体的显示效果看起来像是空格,而不是期望的样式。感觉这并不是我们所期望的正确显示,可能在某些地方忽略了重要的细节或有其他问题。为了进一步确认,我们决定进行调试,特别是在数学计算和字体对齐方面,查找可能存在的问题。
在这里插入图片描述

但Red中的字母在边缘看起来有点奇怪

在查看字体渲染时,注意到红色字母的边缘有些问题,显示效果看起来不太对。当前的渲染过程中,我们没有使用任何提示(hinting),所以字体的边缘可能会稍微模糊,且可能在边缘存在一些额外的透明像素。这可能是由于在渲染时,使用了Alpha混合导致的。

此外,也没有确保字体的显示是完全精确的,可能在渲染时引入了一些额外的模糊,尤其是在双线性插值的影响下。为了进一步验证,可以在渲染过程中进行测试,查看是否这种模糊效果是由于渲染设置引起的。

因此,虽然这看起来是个问题,但并不完全确定是否是设计问题,还是渲染时的模糊效果导致的。这个问题值得进一步调试,尤其是确认是否在渲染过程中引入了额外的模糊或其他不必要的效果。

可能是个基础问题,为什么我们需要自己的字体渲染?Windows不是有我们可以调用的字体渲染函数吗?

虽然Windows确实有自己的字体渲染函数,但我们不能直接依赖它们,因为这样做会带来跨平台的问题。如果不自己渲染字体,当程序在其他平台上运行时,比如Mac、Linux或Raspberry Pi等,可能会遇到没有字体系统的情况。在某些情况下,直接启动的系统可能根本没有可用的字体库,这时就没有任何可调用的字体渲染系统。

即使在某些平台上有字体渲染系统,我们也无法保证它们的行为完全相同。这样会导致我们的游戏在不同平台上看起来有所不同,而这并不是我们想要的。游戏本质上是一个艺术媒介,所有的艺术和视觉效果都是经过精心设计和调整的,我们希望游戏在所有平台上看起来都一致。如果依赖平台的字体渲染系统,可能会导致不同操作系统下游戏外观的差异,或者在操作系统更新后,字体渲染出现不兼容等问题,从而影响游戏的可靠性和稳定性。因此,我们选择自己实现字体渲染,确保跨平台的一致性。

人们不理解为什么字体渲染系统可能会很复杂,因为他们只从像素的角度思考。对此你有什么评论吗?(如果没有其他问题的话)

字体渲染的复杂性常常被误解,因为很多人只是从像素的角度来理解问题。他们可能认为字体只是由像素组成的,每个字形就是一个固定的像素点集合,但实际上字体渲染要比这复杂得多。

首先,字体不是简单地由固定的像素组成,而是由矢量图形(例如贝塞尔曲线)描述的。这些矢量图形在渲染时会根据显示设备的分辨率进行处理和转换,通常会涉及抗锯齿、字体平滑、缩放和不同的字体样式等技术。这些因素共同影响了最终显示的效果。

其次,字体的渲染不仅仅是把图形显示出来,还要考虑字符间距、行距、字形的可变性、不同语言的支持等问题。此外,渲染时还需要考虑显示设备的不同特性,比如分辨率、显示模式等,甚至同一个字体在不同平台上可能会有不同的渲染效果。因此,字体渲染的复杂性不仅仅是处理像素,更是要处理这些背后的数学和技术细节,确保字体在各种情况下都能清晰、正确地显示出来。

你能检查一下你的数字是否是等宽字体吗?例如,111111应该和999999宽度相同

我们可以很容易地追踪这个问题,查看是否像“11111”和“99999”这样的数字占用相同的空间。经过检查,发现它们实际上并没有占用相同的空间。
在这里插入图片描述

在这里插入图片描述

ZoomIt

在这里插入图片描述

在这里插入图片描述

在游戏中,1和9的对齐方式和在记事本中不一样

在讨论字体对齐的问题时,指出游戏中的字体和记事本中的字体并没有完全对齐。虽然已经做了调整,确保尽可能接近微软的规范,但仍然无法做到完全一致。一个可能的问题是字体的尺寸,当前使用的单位是“点”,这可能导致一些微小的偏差。

尽管如此,字体的对齐还是非常接近预期,几乎已经非常精确了。然而,仍然存在一些小差异,特别是在尝试让文本与左边缘对齐时。为了实现这一点,可能需要做一些额外的调整,因为有些部分没有完全达到预期的效果。

你喜欢用循环还是递归来遍历树?为什么?

在遍历树形结构时,如果不太在意效率或者只是为了快速实现,通常会选择递归,因为它写起来比较简洁。然而,如果这个操作很重要或者对性能有较高要求,那么更倾向于使用循环,因为循环在速度和可靠性方面比递归要好得多。

如果你想开始自己的游戏项目,你是从零开始,使用框架/API/SDK(SFML, SDL等),还是使用引擎(自己做的或已经存在的)?

在开始自己的游戏项目时,选择使用框架、API、现成的引擎或自己定制的工具,应该根据项目的需求来决定。就像其他任何选择一样,关键在于你想要做的是什么。如果是想做一个类似已有的游戏,比如一个第一人称射击游戏,如果目标是模仿《虚幻引擎》那样的游戏,那么使用虚幻引擎是一个非常合适的选择。如果项目需求不同,或者有特定的问题需要解决,那么使用不同的工具或定制化的解决方案可能更合适。

它们之间的对齐不一样

之所以不让它们对齐,是因为我们做了左对齐的处理,而其他的系统没有这样做。我们特别为了实现左对齐的功能,所以在做测试时,刻意没有让它们对齐。这是一个有意的设计,以确保文本能够左对齐。

有没有计划添加脚本或modding接口?

没有计划添加脚本或现代化的钩子,因为这个项目是一个教育项目,目的是让用户去编辑随项目提供的源代码。这是项目的核心理念之一,鼓励用户通过修改源代码来进行学习和实践。

是的。有些人(合理地)把屏幕看作一个像素网格,认为字体渲染只是直接把字体贴到屏幕上。所以没关系,他们只是还没理解超出位图字体的内容

大多数人将屏幕视为像素的网格,并且认为字体渲染只是将这些像素直接贴到屏幕上,这种理解大多基于位图字体。在这种情况下,确实如他们所说,位图字体就是简单地将图像(字符)显示在屏幕上。但是,如果不使用基于字形(manas)的字体,就需要对字形的排版有更深的理解和意识,包括字符的大小、字符之间的间距等。如果只是使用基于字形的字体,那确实只是简单地将字符图像贴到屏幕上,但如果想要使显示效果更好、看起来不那么糟糕,就必须考虑到排版细节,确保字符的适当排列和对齐,从而增加了复杂性。

为什么你使用GlyphIndexFromCodePoint而不是一个(大多数为空)指针数组来实现稀疏性?

使用字符点(code point)对应的字形索引(glyph index)而不是一个包含大多数为空指针的数组来实现稀疏性,是因为后者会导致大量不必要的内存浪费。每个指针占用8个字节,而16位的索引只占2个字节。当表格有三万多条记录时,如果使用指针数组,整个表格的内存大小将是使用字形索引的四倍多,这样会浪费大量的内存带宽。因此,选择字形索引来节省内存是更高效的做法。

喜欢将函数的作用域限制在仅在使用的地方定义。你是如何绕过C语言不支持局部定义函数的限制的?你使用函数对象吗?

对于C语言没有局部定义函数的问题,实际上并不觉得这是个特别大的问题。尽管C语言没有内建的功能让函数仅在使用的地方定义,但在实际使用中,并没有发现这会带来太多麻烦。唯一可能遇到的问题是名称冲突,但这种情况其实也很少。自己并不觉得这种作用域限制是个问题,虽然如果能够像lambda函数那样在函数内部定义局部函数,且可以访问堆栈或外部函数的活动范围,那会更加方便。虽然C语言最近添加了类似的功能,但实现起来比较混乱。如果C语言最初就具备这种特性,可能会更加简洁和实用。

你是如何处理自省/元数据来构建你的编辑器检查工具的?

在构建编辑器检查工具时,通常会使用一种类似的系统进行自我检测和反思。在正常的引擎中,有一个叫做“元数据系统”的机制,这个系统能够帮助进行所需的自省工作。这个系统负责对代码进行检查和管理,确保所有信息都能被有效地获取和显示,用来支持编辑器工具的构建和调试。这种机制可以让我们更轻松地进行自我检查和反向操作,提高效率。