游戏引擎学习第170天

发布于:2025-03-20 ⋅ 阅读:(16) ⋅ 点赞:(0)

总结和今天的计划

现在,我们正在处理字体相关的工作。实际上,这个工作已经等待了很久,很多人一直在问,为什么没有新的文件,现在我们正在处理字体。屏幕上已经显示了一些字体,它们看起来还不错,但也有一些部分我们还没有完成,需要进一步完善。

目前最大的一个任务是字距调整(kerning),我们之前稍微讨论过这个问题,今天的主要内容将是进行字距调整,以便让字体的水平间距看起来比现在更好。
在这里插入图片描述

我们仍然需要正确的字距和行距

目前,字体的水平间距只是基于位图,我们没有关于字母应该如何排列的信息。例如,字母“s”和“e”之间的间距看起来太远了,特别是和字母“e”相比。而字体排版中,像字母“t”这样的字符应该离其他字母更近一些,这样更符合排版的标准。

在字形排版中,排版师通常允许字符之间有些许重叠或接近,而不仅仅是严格的对齐。当两个字符的最左边和最右边没有对齐时,通常会稍微将它们拉近一些,这样看起来更自然、阅读起来也更流畅。

此外,还有一些其他的文本度量(text metrics)问题,我们也希望能够解决。例如,行间距的问题。在当前的测试中,字母“p”和“l”之间的行间距看起来不够,因为我们只是随意选择了一些像素数值来设置每行之间的间距。而理想情况下,我们应该从字体的行信息中读取行间距,这样就能确保在处理任何字体时,行间距都能正确应用。

所以,接下来,我们需要解决这些问题,优化字体的水平间距和行间距,让字体排版更精准、更美观。

回顾我们字体渲染系统的当前状态

让我们来看一下字体绘制的过程。之前,我们有一个简单的代码来绘制字体,基本的功能是通过一个循环来处理字符的绘制。在这段代码中,主要的工作就是找到合适的字体和字符图像,然后计算它的大小和位置并进行绘制。这部分代码本身非常直接,基本就是绘制操作的核心部分。

然而,代码中有一些附加功能,例如颜色切换和缩放等,都是为了增加灵活性,但它们并不影响实际绘制字体的核心逻辑。真正关键的部分,就是循环内部的逻辑,它负责决定每个字符的绘制方式。

接下来,想要解决的一个问题是行间距(line spacing)。目前,行间距是硬编码的,使用的是一个随机的值(例如 1.2*8),而这个值显然并不精确。我们希望将它替换为一个真实的行间距值,这样可以确保字体在每一行之间的空间是正确的。这个任务相对简单,只需要用一个已知的行间距值来替换即可。

更复杂的部分是字体的水平间距调整。当前的水平间距是根据每个字符来设置的,而我们希望根据字符对之间的间距来调整,而不是仅仅依赖于单个字符的宽度。这种调整方式叫做“字距调整”(kerning),它根据每一对字符的组合来设置不同的水平间距,而不是为每个字符设置统一的间距。

目前,字体的间距已经是按比例设置的,也就是说,根据字符的宽度大小,间距会有所不同。接下来,我们要做的就是实现字距调整功能,让字体间的距离更加自然和符合排版的标准。
在这里插入图片描述

我们已经支持比例字体,但没有字距调整

实际上,如果我切换使用不同的字体,大家可以更清楚地看到比例间距和字距调整(kerning)之间的区别。比如,当前使用的是Courier New,它是一种固定宽度的字体,我们可以切换成Arial,它是一种比例宽度的字体,这样就可以直观地看到两者的差异。

在字体处理器中,我们将字体从Courier New切换到Arial后,运行程序,可以看到明显的区别。使用Arial字体后,字符之间的间距显著不同,比如字母“T”和字母“Y”之间的间距就大不相同,而字母“T”和字母“E”之间的间距则较小,这说明我们已经支持了比例间距的字体。

然而,当前我们并没有实现字距调整(kerning)。字距调整是指根据字符的形状和组合来调整字符之间的间距。举个例子,当字母“T”和字母“o”出现在一起时,字体设计师通常会调整这两个字母之间的间距,使它们看起来更自然,而不是根据字符的固定宽度来简单计算间距。当前的字体渲染系统遵循的是比例间距的规则,即字符的宽度决定了间距,但是没有根据字符的实际形状来进行微调。

字距调整的关键是,字体设计师并不总是根据字符的宽度来决定间距,而是通过调整不同字符对之间的间距来确保排版的美观。这种调整是针对字符对的,而不仅仅是单个字符的宽度。比如,如果字母“T”与字母“o”组合在一起,它们的间距应该比字母“T”和字母“L”之间的间距更紧凑,因为它们的形状和组合方式不同。

至于如何实现字距调整,实际上并不复杂。我们已经支持了比例间距字体,这一点非常简单,只需要将固定宽度字体的字符间距调整为根据字符宽度动态变化即可。这只是代码中的一个小改动,基本上只需要将字符宽度从固定值改为按比例变化的值。

实现字距调整(kerning)比支持比例间距稍微复杂一些,但依然很简单。我们不需要改变太多的代码,只需要在计算字符间距时,考虑当前字符和前一个字符的组合,而不是单独计算每个字符的宽度。这就涉及到根据字符对来调整间距,而不是单纯依赖字符的宽度。
在这里插入图片描述

在这里插入图片描述

追踪前一个字符

我们希望在处理字体渲染时存储代码点(code point)的信息。目前,我们已经有了当前的代码点,但我们还需要存储前一个代码点,以便后续计算字符间距和调整字距。

初始化时,先将前一个代码点设为 0,表示当前没有前一个字符。然后,在输出每个字符时,记录下它的代码点,以便在下一个字符输出时可以参考。这样,我们就能够同时知道当前的代码点和前一个代码点,从而为后续的字距调整提供基础。

进一步优化,可以更正式地处理代码点,使其结构化,以便未来支持国际化语言,例如不同的字符集和编码方案。因此,代码被调整为一个独立的 CodePoint 变量,另外引入 PrevCodePoint 来存储上一个字符的代码点。

修改后进行编译,确保代码正确。编译后,程序的行为不会有任何变化,渲染逻辑依然相同,但现在已经为后续的改进做好了准备。

接下来,需要修改字符的横向推进量(AdvanceX)。目前,推进量是通过 CharScale 计算的,但我们计划不再使用它,而是引入更灵活的推进计算方式。

新的推进逻辑会先创建 AdvanceX 变量,并赋值为某个待计算的值。这样,我们可以灵活调整推进方式,例如可以根据固定宽度方式计算,也可以根据字符间距(kerning)来计算。在代码中,还可以轻松地切换不同的推进方案,以便观察它们的区别。

最终目标是基于字距调整(kerning)来计算 AdvanceX,这样可以确保字符间距更加合理,提升文本的排版质量。
在这里插入图片描述

GetHorizontalAdvanceForPair 会告诉我们两个字符的字距调整

现在的目标是获取正确的字距调整(kerning)值,即计算两个相邻代码点(code points)之间的水平推进量(horizontal advance)。具体来说,我们希望能够查询当前代码点与前一个代码点之间的水平推进量,以确保字符间距的合理性。

最初的思路是,在每次渲染一个字符时,根据前一个字符的代码点和当前字符的代码点,查询它们之间的推进量,并应用这个推进量。但在实际实现过程中,我们意识到推进的时机需要调整。

原先的方式是在字符绘制之后再计算推进量,但这样在处理字符串时可能不够直观。因此,我们调整思路,决定在处理当前字符之前就计算推进量,并提前应用它。这样一来,在每个字符渲染之前,已经正确计算并设置了合适的推进量。

有一个替代方案是基于“下一个”代码点来计算推进量,但这样会影响一些可能需要逐步解析字符串并动态调整字符渲染逻辑的用法。因此,我们放弃了这种方法,决定始终使用“前一个”代码点和当前代码点来计算推进量。

这种方式在逻辑上更加清晰,每次绘制字符前,都先计算推进量并提前调整位置,从而确保文本排版的一致性。目前代码结构已经调整好,下一步的关键就是确定具体的推进值,也就是如何正确计算不同字符组合之间的字距调整。
在这里插入图片描述

字距调整将使用与字体相同的维度进行存储

我们需要获取水平进距(horizontal advance),但在此之前,需要考虑几个关键因素。

首先,我们需要确保所有字体的缩放比例一致,因为字体的存储空间有一定的分辨率,比如像素单位等。而在渲染时,我们会按照一定的缩放比例调整字体的大小,因此,字距数据应该存储在和其他字体数据相同的坐标空间中,以保持一致性。

字体资产文件的存储格式

在资产文件(asset file)中,所有字距信息都应该采用与其他字体数值相同的存储方式。这是为了确保系统的统一性,避免额外的转换步骤,使得计算更直观且高效。

需要额外的信息

除了字距数据外,我们还需要一个关键的额外信息:字体本身的标识
由于游戏或渲染系统可能包含多个字体,需要确保我们在查询字距信息时,能够准确地定位到当前正在使用的字体。这就涉及到资产系统的进一步优化,使其能够更快地访问所需数据。

为什么要特别处理字体,而不是仅仅把它们当作一组位图

优化字体信息管理与渲染流程

当前的字体系统在获取字形信息时,每次渲染字符都需要遍历匹配系统,这显然不是一个高效的做法。因此,我们希望能够建立一个集中式的字体信息管理系统,以提高查找效率,并存储额外的字体元数据,例如**行高(line height)**等信息,这些信息目前尚未存储。

目标:构建一个高效的字体管理系统

为了更好地管理字体,我们计划实现以下功能:

  1. 直接从资产系统获取字体对象

    • 需要提供一种机制,使得字体可以作为一级资源(first-class citizen)进行管理。
    • 例如,我们可以通过 asset.font(...) 这样的方式直接访问字体,而不需要每次都遍历整个资源系统。
  2. 优化字体加载机制

    • 目前,位图(bitmap)是按需加载的(demand-loaded),但字体本身可以直接作为一个整体数据块加载。
    • 这样,我们可以在资源加载时预先加载所有字体相关信息,避免在渲染过程中频繁查找和匹配。
  3. 使用资产系统进行字体匹配

    • 我们可以基于字体的某些特征(例如字号、字重、风格等)来进行匹配,使得字体管理更加智能化。
    • 这样可以允许我们在渲染时更灵活地选择合适的字体,而不是简单地硬编码特定字体。
  4. 引入字体跟踪机制

    • 由于字体需要参与资源加载流程,我们可能需要为每个加载的字体进行跟踪,确保其正确地存储和管理。
    • 目前尚不确定这种机制是否必要,但如果实现,也可以提高字体管理的灵活性。

实施方案

  • 在资产管理系统中,增加专门的字体存储结构,让字体数据独立存在,而非与其他资源混合。
  • 修改渲染流程,使其能够直接访问预加载的字体数据,避免重复匹配。
  • 评估是否需要一个更智能的字体匹配系统,用于在渲染时自动选择合适的字体。

目前,我们可以先沿着这一方向进行尝试,看是否能够提高渲染效率,并简化字体管理流程。

不分页加载字体的优缺点

字体按需加载的权衡与决策

我们希望字体数据可以尽可能大,但只有在实际使用时才会被加载。这种方式的优势在于:

  • 减少内存占用:未使用的字体不会占据内存,提高整体资源利用率。
  • 提高加载效率:仅在需要时才加载特定字体,避免一次性加载所有字体造成的性能开销。

问题与挑战

然而,这种方式也带来了一些问题:

  1. 依赖字体的代码执行受限

    • 任何需要计算字符布局测量文本的代码,必须等待字体被加载后才能运行。
    • 这意味着某些逻辑(如文本排版)可能会被阻塞,影响渲染的即时性。
  2. 多语言支持的复杂性

    • 如果需要支持不同语言的字体,按需加载可以减少内存消耗。
    • 但同时,必须确保正确的字体在正确的时间被加载,否则可能导致字符渲染错误。
  3. 字体元数据管理

    • 例如字距调整表(kerning tables),如果所有字体都按需加载,那么在进行文本布局时,我们可能无法立即获得所有必要的信息。
    • 如果始终加载所有字体的元数据,则可能造成额外的内存开销。

决策:采用按需加载方案

尽管存在一定的挑战,但考虑到内存优化和多语言支持的灵活性,我们决定采用按需加载的方式。

  • 仅在需要时加载具体的字体数据,而不是一次性加载所有字体。
  • 可能需要实现延迟计算机制,确保在字体加载后可以正确执行布局和渲染逻辑。
  • 未来可以评估是否需要预加载部分核心字体,以优化某些高频操作的性能。

虽然这一方案有一定的复杂性,但整体来看,它是更具扩展性和灵活性的一种选择,因此我们决定沿着这个方向进行实现。

我们来动态加载字体

决策执行:实现按需加载字体系统

由于有些决策无法在一开始完全确定,所以我们决定直接执行按需加载字体的方案。

实现思路

  1. 定义加载字体的数据结构

    • 我们引入了一个 loaded_font 结构,用于存储已加载的字体数据
    • 这个结构将用于按需获取字体,而不是一次性加载所有字体资源。
  2. 获取字体的流程

    • 需要字体时,我们会从 loaded_font 结构中进行匹配。
    • 只有匹配成功的字体才会被加载进内存。
    • 这个匹配过程仅针对已加载的字体,不会影响其他未加载的字体数据。
  3. 优化字体匹配逻辑

    • 之前的匹配逻辑主要用于查找字体类型(如加粗、斜体等),而现在我们只对已加载的字体进行匹配。
    • 具体来说,我们可以基于**字体的制造商(manufacturer)字体粗细(weight)**进行匹配,以找到最佳的可用字体。
  4. 调整字体数据存储方式

    • 之前的 match vector 主要用于查找所有可能的字体,而现在它只用于查找已加载的字体
    • 这样,我们就能高效地获取最适合当前需求的字体,而不会影响整个字体系统的性能。

总结

  • 通过引入 loaded_font 结构,我们实现了按需加载字体的方案,避免了无用的资源占用。
  • 只在需要时进行匹配,减少了不必要的搜索和加载,提高了性能。
  • 未来可以进一步优化字体的匹配规则,以支持更复杂的需求(如动态字体切换、多语言支持等)。
    在这里插入图片描述

找到最匹配的字体

字体匹配和加载过程的设计

  1. 字体ID与最佳匹配字体

    • 在字体加载的过程中,首先定义一个字体ID,该ID用来标识每个字体资源。
    • 使用GetBestMatchFontFrom函数来获取最佳匹配的字体。这个函数通过对比输入的字体请求参数(如字体样式、粗细等)来选择最合适的字体。
  2. 资产系统与字体加载

    • 通过资产系统获取所需的字体资源。具体来说,传递相应的Assets(即字体资产信息)到资产系统中。
    • 在当前阶段,虽然没有实际的字体选择,但可以通过调试字体概念来模拟这一过程。
  3. 加载和匹配字体

    • GetBestMatchFontFrom被调用时,系统会从加载的字体中找到最合适的一个,并将其用于后续的字体渲染。
    • 目前,字体资源的加载与选择并没有复杂的选择逻辑,只是一个简化版的操作。
  4. 简化代码点的使用

    • 由于我们已经获取了最佳匹配的字体,代码中将简化为直接使用code point,即字体的编码点,来确定渲染的字符。
    • 代码中会不再有复杂的字体查找逻辑,而是将字体的选择和获取过程交给了上游的字体加载和匹配机制。
  5. 查询字体信息

    • 为了确保字体渲染时的正确性,我们还需要一个查询机制来获取字体的具体信息,例如字体的大小get_size)等。这些信息会在后续的渲染中被使用。

总结

  • 通过定义字体ID和使用GetBestMatchFontFrom函数,系统能够根据需求动态加载和匹配合适的字体。
  • 通过资产系统,字体资源可以按需加载,避免了不必要的字体资源占用。
  • 字体匹配逻辑被简化为直接获取最佳匹配的字体,并且不再需要复杂的字符查找过程。
  • 系统还将通过查询机制来确保正确的字体信息被传递到渲染模块,从而保证字体渲染的准确性。
    在这里插入图片描述

调用 GetFont

字体加载和水平推进的实现设计

  1. 加载字体和获取字体ID

    • 系统将通过一个已经加载的字体资源(称为“已加载字体”)来获取所需的字体。
    • 在此过程中,传递相应的字体ID来识别和检索字体。此字体ID将与加载的字体相匹配,确保能够返回正确的字体资源。
    • 为了确保加载字体的正确性,还需要提供一个生成ID,这个ID对应于渲染分组的生成ID。通过这个生成ID,能够进一步确认所加载字体的版本或变化。
  2. 字体加载状态检查

    • 在获取字体后,系统会检查字体是否已经加载。如果字体已经加载,系统会继续执行后续操作;如果字体未加载,则无法继续处理。
    • 字体的加载状态决定了后续操作的执行逻辑。如果字体未加载,系统会中止操作,直到字体资源成功加载。
  3. 水平推进的实现

    • 一旦字体成功加载,接下来的目标是计算水平推进(horizontal advance)。水平推进是指在渲染过程中每个字符之间的水平间距,它通常基于字体的具体设计进行调整。
    • 在此实现中,系统将接收代码点(即字符的编码)作为输入,计算该字符的水平推进。通过传递给系统相应的字体和代码点,系统将能够返回每个字符的水平推进值。
  4. 假设的功能实现

    • 目前假设系统能够正确计算并返回所需的水平推进。作为开发者,目标是设计一个能够顺利计算和处理这些推进值的机制。
    • 代码点的处理将依赖于字体的加载状态和字体的具体信息,进而帮助正确渲染字符并确保文字的排版效果。

总结

  • 系统通过加载已匹配的字体资源,并通过字体ID和生成ID来确保正确加载字体。
  • 字体加载后的检查机制确保了只有在字体加载成功的情况下才会进行后续的渲染操作。
  • 水平推进的计算将基于代码点和字体设计进行,确保字符之间有正确的间距。
  • 该设计假设能够有效地计算出水平推进,进而确保文本的正确渲染与排版。
    在这里插入图片描述

GetBitmapForGlyph 会绕过资源匹配系统来获取字符图形

字体和位图ID获取与管理的设计

  1. 获取位图ID的过程

    • 之前使用匹配向量(match vector)来处理位图ID,但现在计划通过直接查询来获取位图ID。
    • 通过对字体进行查询,具体是使用GetBitmapForGlyph方法来根据代码点(code point)查找位图ID。
    • 查询过程会返回相应的位图ID,然后可以使用这个ID继续获取进一步的字体信息。
  2. 位图ID的使用

    • 在获取到位图ID后,系统可以利用这个ID继续获取更多信息,如字符的具体形状、大小等。
    • 通过这个步骤,确保了可以通过代码点找到对应的字符位图,从而能够准确地进行渲染和排版。
  3. 代码结构与代码基的挑战

    • 在当前的开发环境中,存在多个代码基(code bases),开发者在切换不同代码基时,容易遗忘各自的定义和结构。例如,像Sound ID等变量或结构的定义位置可能不容易追踪。
    • 这说明在处理多个项目或模块时,管理和维护代码的挑战,以及如何保持各个代码基的一致性和清晰性,是一个需要注意的问题。
  4. 代码结构的改进

    • 为了更好地管理这些定义,可以使用某些符号(如\n和空格)来组织和搜索代码中的元素,确保能够高效地访问和修改这些值。
    • 这有助于改善代码的可维护性和可读性,减少因切换工作环境或模块时导致的混淆。

总结

  • 通过查询方法(如GetBitmapForGlyph)获取位图ID,确保每个字符都有对应的位图信息,方便后续渲染。
  • 代码结构中,开发者面临跨多个代码基的管理问题,可能会遗忘某些变量的定义位置。
  • 为了提高代码的可维护性,采用一些有效的符号和方法来优化代码结构和搜索,确保开发环境的清晰和高效。
    在这里插入图片描述

字体将有自己的 ID 类型

字体ID与资产管理的设计与实现

  1. 字体ID的设计

    • 目的是设计一个字体ID(font ID),可以用于获取特定字体的信息。通过该ID,可以检索相关的字体数据。这种设计可以确保字体的管理更为集中和有序。
    • 字体ID的使用让系统能够更加方便地查找和操作字体,避免每次都需要手动查找字体的各种属性和信息,提升开发效率。
  2. 游戏资产管理系统(game Asset)

    • 在手动资产管理系统中,计划逐步实现一些基础功能和例程(routines),这些功能将使得字体及其他资产的管理更为简便。
    • 这些例程应当是相对简单且直接的,可以通过简化代码结构来提高资产处理的效率。例如,简单的字体加载、查找等操作,将能够通过这些例程高效完成。
  3. 实现的目标

    • 在实现过程中,目标是将这些操作封装成清晰、易用的接口,使得开发者能够通过统一的接口访问和操作字体等资产。
    • 通过这种方式,字体和其他资产的管理将更加系统化和模块化,有助于长期维护。

总结

  • 字体ID的引入让字体管理更加方便,可以通过它来快速检索字体信息。
  • 手动资产管理系统的设计旨在提供简单的例程来处理字体和其他资产的加载和管理。
  • 这些改进将提升开发效率并帮助管理和维护不同资产的数据结构,减少手动操作的复杂性。
    在这里插入图片描述

实现 GetFont

在实现中,如果调用 GetSound,就不应该直接调用该方法,而应该改为调用 GetFont 方法。调用时,传入一个字体ID生成ID,然后当获取到相应的资产后,只需要关注字体的部分,而忽略声音的部分。这意味着,实际操作中,获取字体信息时,我们会只处理与字体相关的数据,而不需要处理声音部分。这种方式简化了操作,同时保持了对字体数据的集中管理和处理。
在这里插入图片描述

根据我们使用的方式定义字体结构体内容

在实现过程中,可以假设“已加载字体”中包含了所有需要的内容,接下来会仔细确定这些内容是什么。这些内容将基于当前的例程进行处理。同时,由于需要处理更多内容,可以顺便实现一个获取“行间距” (line advance) 的功能。当字体被返回时,可以通过调用 GetLineAdvanceFor 来获取该信息。只需要将其乘以字体缩放比例,即可得到正确的行间距。通过这种方式,不仅能处理字体信息,还能确保在不同的缩放级别下,行间距的调整也能正确处理。
在这里插入图片描述

我们有四个函数需要实现。让我们先占位

在实现过程中,首先需要明确回答四个主要问题,以便确保字体系统能顺利运行。接下来,开始梳理这些问题所需的数据,并准备实现这些功能。在这个过程中,采用的是一种自上而下的方法,先清楚自己要实现的目标,再逐步明确实现细节。

目前,针对字体的各种功能,比如水平间距、行间距等,已经预设了一些初步的值,如水平间距设为10像素,行间距设为80像素,这些数字暂时是随机的,主要用于验证代码框架的正确性。对于字体的匹配,可以通过资产系统进行查询,确保可以获取到正确的字体资源。

接下来,还需要对代码进行调整,以便更好地支持字体加载和查找。具体来说,可以通过传递font_idGenerationID来获取相应的字体。如果字体已加载,可以继续执行后续操作,否则需要进行加载处理。

此外,还需要确保字体的位图信息能够正确获取,这时可能需要资产系统提供更多的支持。对于这些操作的实现,当前的代码已经接近编译完成,并且针对字体加载和查找进行了初步配置。

最后,为了确保系统的稳定性,需要对各种操作进行适当的异常处理和调试,确保加载字体和获取相关信息时不会出现问题。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

测试硬编码的例程

在这里插入图片描述

现在,虽然当前的代码还处于一个较为原始的阶段,进展仍然依赖于硬编码的返回值,每次的间距都是固定的,但是系统的整体结构已经能够正常工作。接下来的步骤是开始实现这些方法,一旦这些方法被实现,代码就能正常运行,并且返回的数据也会是正确的。

接下来需要逐个实现这些方法。首先是实现 GetFont 方法,确保它能够正确地获取字体。之后,再实现其他相关的功能。通过逐步实现这些方法,系统会逐渐完成,并能正确地返回所需的结果,从而确保字体处理和加载过程顺利进行。
在这里插入图片描述

GetBestMatchFontFrom

接下来,需要实现 GetBestMatchFontFrom 方法。实际上,这些方法非常简单,只是负责将正确的类型传递给资产 ID 的处理过程。这些方法本质上非常基础,它们的功能仅仅是确保传递所有必要的信息并通过正确的方式处理。因此,不需要更复杂的逻辑,只需确保将所有参数直接传递给相应的函数就可以了。

这种方法是直接而简单的,通过这种方式,所有的匹配、权重向量等问题都能被处理好,不需要额外的复杂功能。在实现过程中,确保每一步都传递正确的参数,这样代码才能顺利工作。
在这里插入图片描述

在这里插入图片描述

编辑资产文件的字体格式,加入我们需要的信息

现在,需要开始编辑资产文件格式,以便包含我们实际需要的内容。首先,我们可以从一个简单的版本开始,之后再进行更复杂的版本处理。

在这里,假设我们已经拥有所需的内容。首先,我们会假设有一个游戏资源资产,并且在这个资产中加载了字体。我们假设有一个代码点范围,并且能够在这个范围内使用代码点进行操作。这样,基本上我们就能从加载的字体中获取相应的字符信息。

接下来,编辑资产文件时,我们将包括字体信息,并确保我们能够通过代码点进行查询,这样就能处理各种字符和图形。

填充 loaded_font 结构体

一开始,我们可以简单地设定一个代码点计数,因为对于ASCII字符集来说,这是足够的。在之后,可能需要处理更复杂的字符集,比如拼音或其他更复杂的编码方式,但暂时我们不需要考虑这些。

我们可以假设我们有一个代码点计数,然后每个代码点对应一个位图ID,这些位图ID会被保存在代码点表中。除此之外,我们还需要记录每个代码点的水平偏移(horizontal advancement),这可以通过一个二维表来处理。这个二维表的查找相对简单,通过代码点和其他参数进行索引即可。

另外,我们还需要记录每个代码点的行高(line advance),这是一个单一的值。这样,在加载的字体中,基本的结构就包括了代码点、位图ID、水平偏移和行高。

当需要使用这些数据时,操作也非常简单。对于行高,直接返回字体的行高值即可;对于水平偏移,可以通过代码点和代码点计数的组合来进行二维数组索引,返回对应的值。这种方式不像之前那样需要进行复杂的搜索匹配,直接通过数组索引即可获得值,这样的实现更加高效。

最后,我们还需要考虑到数组越界的问题。为了避免使用超出范围的代码点,我们应该引入一个概念,即对数组进行边界检查,确保在访问时不会超出定义的范围。这将使得程序更加健壮,避免潜在的错误。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

防止缺失的字符

在字体处理中,有一个“期望的代码点”和“实际获取的代码点”。通常情况下,期望的代码点和实际代码点是一样的,前提是期望的代码点在有效范围内。如果期望的代码点小于字体的代码点计数,那么就可以接受这个代码点;否则,我们会返回一个零位图ID,表示无法绘制该字符。

为了处理这种情况,可以使用“clamp”(限制)机制。我们希望通过一个函数GetClampedCodePoint来实现这个功能。这个函数将确保返回一个有效的代码点,如果请求的代码点超出有效范围,它会将其限制到有效范围内。

这种“限制”机制的实现非常直接,它会检查所请求的代码点是否超出了有效范围。如果超出范围,则返回一个默认的零位图ID。这样,即使程序请求的代码点不在有效范围内,也不会发生崩溃,而是以一种优雅的方式进行处理。

这种做法的核心思想是,确保查询时总是返回一个有效的结果,即使请求的代码点超出范围。这是非常常见的场景,尤其是在渲染大量字符时,我们不希望每次查询都进行复杂的计算。通过简单的查找表和预先计算好的数据,可以避免不必要的计算,从而提高性能。

所以,最终的目标是让所有的信息都预先整理成表格,以便快速查找。一旦加载了字体,之后的查询就会非常高效,不需要每次都进行复杂的计算。唯一需要较慢处理的是获取加载的字体,这通常是最费时的操作。
在这里插入图片描述

构建字距调整表

现在,虽然编写代码非常简单,因为我们只需要将所有内容组织成表格,但接下来要做的工作就是构建这些表格。这确实需要一些工作量,但我们可以继续进行,虽然这可能是一个挑战。
首先,可能需要做的是在文件格式中进行必要的修改来实现这一点。在这一步,所有的表格和数据结构都需要被定义和初始化,这样在后续的查询中就可以直接使用这些数据了。

引入一个单独的资产类型 ID 用于字体字符

首先,需要引入一个概念,那就是字体和字体字形(font glyph)之间的区别。字体字形在字体中会有不同的位图范围,而这些位图现在已经成为了实际的资源。因此,需要处理两者之间的差异。

我们可能不需要单独定义一个字体字形的资产类型,但是将其包含在内是有意义的,至少这样做可以确保在文件中清楚地区分出字体和字体字形的内容。

接下来,必须存储这些数据。我们已经知道需要存储哪些信息,比如字形的位图信息,以及与字体相关的额外数据。除此之外,可能还会想要存储其他一些数据,但大体上来说,就这些基本信息是必需的。

为此,需要在文件中增加存储结构,将字体作为一种资产存储,并附加上额外的信息,以便它能够在后续使用中更清晰地体现其具体内容。需要确保每个字体都能清楚地标明它包含的是哪些数据,并且这些信息能够被直接利用。

考虑到时间问题,可以继续推进这部分的工作,确保尽可能多地完成任务,为接下来的工作做好准备。
在这里插入图片描述

决定 hha_font 中要包含什么

首先,已经明确需要哪些信息,并且知道这些信息是必需的。但是,从当前的实现来看,存在两个不同的数组和其他一些数据结构。因此,可以考虑采用一种处理方式来加载字体资源。通常情况下,像位图这样的资源会使用延迟加载的方式,在需要时才加载数据。然而,字体可以立即加载,即在打开字体资源时就直接加载,而不需要等到后期。可以选择在数据部分直接存储相关信息,而不是采用延迟加载。

为了实现这一点,将字体的关键数据直接存储在文件中,包括代码点计数和行间距,因为这两个值不会占用太多空间。接下来,将假设字体数据资源指向一个位图 ID 数组,并且另一个数组存储横向推进量(horizontal advancements)。所以,可以在数据部分存储代码点计数、行间距等信息,并将其他资源如位图和推进量数组存储在相应的位置。

这样设计的好处是简化了加载过程,避免了延迟加载的复杂性。
在这里插入图片描述

(插曲) 元生成容器

经常提到的一些编程语言,比如 C++、Java 和 C#,虽然它们有各自的优点,但也带来了很多不必要的复杂性和额外的负担。很多情况下,它们并不容易实现开发者真正想要做的事情,反而迫使开发者承担很多不必要的麻烦。因此,尽管我们常常说 C 语言的一些缺点,但这并不意味着其他语言就更好。

以 C 为例,某些功能,像是自动生成代码,实际上应该是可以自动化处理的。如果使用更高效的语言,很多这些事情本应该自动完成。问题在于,想要在其他语言中实现这些功能,往往需要接受一些不需要的额外负担,像是 Java 和 C++ 就会要求程序员进行更多的配置和管理。开发者更倾向于使用生成器来自动化代码的创建,简化这些繁琐的任务。对于一些小型项目来说,手动写这些代码没问题,但当涉及到更复杂的工程时,生成代码会显得非常重要。

然而,这种技术虽然能简化开发过程,但对于刚刚接触编程的开发者来说,应该先学会如何手动处理这些任务,掌握基础的代码结构和设计。等到熟练之后,开发者可以选择使用自动生成的工具,来简化和优化代码管理,提升开发效率。在一些项目中,使用自动化工具可能会浪费过多时间,特别是当生成代码的成本高于手动编写代码时。

总的来说,编程语言和工具的选择需要权衡不同的利弊。如果目的是快速开发,某些语言和工具可能会显得更有优势,但从长远来看,如果要开发多个项目,自动化代码生成和高效的工具链会带来更大的灵活性和效率。这些都是根据具体项目需求和开发环境来做出的合理选择。

理解这个问题的关键在于,虽然高级语言和服务可以解决一些问题,但它们并不会消除所有的限制,反而可能会带来其他的问题。因此,如果想要突破这些局限性,最好不要依赖过于抽象的工具或自动化方法,因为这通常并不会真正解决根本问题,而只是改变了问题的表现形式。

接下来,关于数据的管理,我们可以通过简单地注释和记录来标明每个数据项的具体含义。例如,数据可以包含一些通道信息,或者可以标明像“零通道”之类的内容。在编程时,虽然理想情况下可以使用更现代的语言来简化这一过程,但目前使用的语言仍然允许我们手动写出清晰的注释,帮助其他人理解数据的结构和含义。

在加载资源时,需要注意的一点是,虽然很多时候会提前知道需要支持哪些资源,但在实际操作中,我们还是要细致地考虑如何正确地加载每种资源。例如,在加载位图和声音时,方法是类似的,实际上可以按照与加载声音相似的方式处理位图的加载。只需要对加载逻辑稍作调整,就能实现相应的功能。

总之,虽然现代编程语言能提供很多便利,但在实际开发中,很多时候我们仍然需要手动处理一些细节,确保代码能在不同情况下按预期工作。

实现 LoadFont

在这个过程中,首先要解决的是加载字体的部分。这部分的实现会比较困难,尤其是在处理指针和映射时。需要注意的是,代码中的一些冗余部分可能会导致潜在的问题,尤其是在处理共享代码时。如果代码在多个地方重复,而不是集中管理,修改时容易遗漏,导致错误。因此,最好将这部分代码集中化,使得修改时只需要改动一个地方,从而减少出错的可能性。

在实现加载字体的过程中,实际上大部分代码与加载其他资源(比如位图)是相似的。唯一的区别是在加载字体时,需要处理指针的映射操作。具体来说,需要设置好加载后的字体的指针,以便后续能正确引用相应的数据。

接下来,要注意的是,虽然在加载字体时,代码中的许多操作是有序且规范的,但实际上可以进一步简化。比如,CodePointCount是加载字体时唯一必需的参数,LineAdvance则并不是必需的。为了简化操作,可以考虑使用字体头部(font header)来管理这些数据,尽量避免冗余的处理。

总的来说,尽管代码执行起来相对简单,但如何组织和管理这些数据会对后续的维护和扩展产生很大的影响。因此,建议将相关的管理部分提炼成函数或方法,使得处理更加简洁、高效。
在这里插入图片描述

将更多字体信息移出并放入资产文件

在这段过程中,首先要解决的问题是如何优化和简化字体加载的方式。字体(font)需要一个额外的头部信息,类似于一个字体头(font header),这个头部将包含关于字体的一些基本数据,例如代码点数量、(line advance)以及其他可能需要的相关数据。字体的核心信息非常简单,实际需要存储的唯一关键数据是代码点数量,其他的信息可以根据需要动态加载。

具体来说,字体头(font header)将包含字体的基本信息,而实际的字体数据(如代码点和水平位移)会紧跟在头部之后。为了加载这些数据,可以将代码点和水平位移的数据按顺序直接存储在内存中。对于每个字体的加载,首先会读取代码点数量,然后根据这个数量加载相应的代码点和水平位移数据。

其中,代码点数量是通过字体头部的 CodePointsCount 来确定的,这样就能明确知道需要加载多少代码点。而每个代码点的大小是固定的,因此可以根据已知的数量来准确地解包这些数据。水平位移的数据则紧跟在代码点数据之后,保证所有数据能够连续存储并按需加载。

总体而言,这一过程非常简洁和直接,基本上是按照一个规范的格式进行数据的组织和存储。通过这种方式,可以避免冗余的操作,也能在加载字体时做到高效而有序。此时,整个加载过程与其他资源的加载过程类似,只不过这里的字体数据结构需要适当的调整。

在实现时,可以通过一个统一的接口来管理加载过程,避免重复的代码,使得整个程序更加简洁易维护。通过合并和共享代码,可以减少不必要的重复和冗余,进一步优化代码结构,从而提高程序的效率和可维护性。

这段描述中的思路非常直接,基本上是通过一个简单的结构来管理字体数据,从而确保加载过程能够稳定高效。
在这里插入图片描述

在这里插入图片描述

计算字体数据占用的大小

首先明确了需要进行科学计算的目标。实际上,要做的事情非常简单,主要是记住数据的大小。数据的大小计算很直白,因此不需要额外复杂的步骤。之前的“大小区段”不再适用,因此被完全去除了。

这里的核心思路是,我们知道数据的大小是什么,因此直接定义一个数据大小的变量。对于这种类型的计算,已经不再需要使用过时的结构和方法。之前的“size section”概念已经被弃用,不再需要。

总结来说,重点是通过简单明了的方式来计算数据的大小,并且避免使用冗余或过时的结构。数据的大小已经是预先确定的,不再依赖于复杂的计算或结构,可以通过直接存储数据的大小来简化流程。

保持代码在可编译状态

主要涉及了如何计算字体数据的总大小,特别是字体的代码点和水平推进(horizontal advance)。首先,需要计算出代码点的大小,然后再加上水平推进的大小,最终得出整个字体数据的总大小。水平推进的大小是通过计算每个代码点所占的内存大小来得出的。代码点的数量和水平推进的数量共同决定了字体数据的总大小。

接下来,处理的过程中还涉及到了一些必要的代码改动。为了能够正确处理字体数据,需要传递字体信息(font info)。这种情况下,代码要同时处理字体本身和字体信息两个部分。虽然这样做可能会让代码看起来比较复杂,但这是目前能够工作的方式。

然后,通过简单的函数调用,可以获取到需要的位图数据和字体信息。在实际开发过程中,通常会进行一些微小的改动,例如传递不同的数据结构,确保正确的函数调用。

最后,代码中还提到了一些文件管理和数据结构管理的细节,比如需要将字体信息和相关数据传递到不同的层级或者函数中。整体上,尽管开发过程中会遇到一些需要调整和优化的地方,但所有的处理逻辑基本上都是非常直观的,步骤清晰,目标明确。

总结来说,核心的工作是计算字体数据的大小、正确传递相关的数据结构,并在不同的层级和函数中处理这些数据。虽然有些步骤涉及到调整和传递多个变量,但这些都是确保功能正常实现的必要步骤。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

尝试运行,预期会爆炸

我们在进行一个测试过程中,尝试更新一个资产并执行一些操作,原本以为在这个过程中会出现崩溃或问题,尤其是在我们更改了资产后。每次进行这些改动时,总是觉得会出错,但是这次竟然没有发生崩溃,反而一切都运行得相当顺利。虽然预期会有问题,但系统并没有出错,反而让我们感到意外的稳定。

你运行过资产构建器吗?

我们决定暂时不运行资产构建器,因为当前没有更新过它,运行它可能会导致更糟糕的情况。由于没有更新过,这样的操作可能会使系统陷入不稳定的状态。因此,我们选择先不启动这个过程,尽管有些意外。

目前,我们还没有存储字体表,因此下一步的任务是编写字体表,完成这一部分后,任务就基本完成了。我们计划在明天开始编写字体表,并且一旦完成这一部分,我们可能会处理一些其他的事情。然而,我们还可能需要花一两天的时间来支持更多的语言,至少要做一些基础的语言支持

能否提供一些上下文,为什么要使用元编程技术?

  1. 资产处理的复杂性

    • 资产处理器在游戏或应用中负责管理各种资源,如纹理、音效、数据等。这个过程非常复杂,需要进行一系列精细的操作,特别是在资源的保存和加载时,很多处理是自动触发的。
    • 在处理过程中,尤其是在保存和加载时,往往需要执行许多重复的、繁琐的操作。例如,代码中的一些逻辑可能涉及到对资源的读取和写入操作,这些操作的顺序和方式必须特别小心,以确保正确性。
  2. 自动化操作的需求

    • 许多资源处理任务其实可以由计算机自动完成,而不需要开发者手动干预。举个例子,如果要从某个数据结构中提取特定的信息,开发者并不需要每次都写冗长的代码来实现这些任务,而是应该利用编程语言本身的能力让计算机去处理这些常见的操作。
    • 例如,在处理音效数据时,开发者并不需要手动管理一个巨大的数组,去读取每个元素并计算样本值等,而是通过合理的内存布局和结构声明,让编译器自动帮助完成这些工作。
  3. 内存布局和编译器优化

    • 通过合适的声明和内存布局,编译器可以自动管理不同类型的数据,而开发者只需要定义好数据的结构。这样可以减少手动管理数据结构的麻烦。
    • 比如,使用C语言的结构体来定义内存布局,编译器可以根据开发者的声明,自动进行数据的对齐和优化,避免开发者自己去操作这些低级细节。
  4. 处理可变大小的数据

    • 在处理动态大小的数据时,虽然近年来编程语言规范有所进展,但仍然存在许多限制。现有的编程语言和工具在这方面的支持并不理想,因此需要自行实现一些解决方案,才能更加灵活地处理不同类型和大小的数据。
  5. 自定义编程语言的需求

    • 当现有编程语言和工具不能满足特定需求时,可以通过扩展或定制语言来解决问题。通过增强C语言,可以为程序员提供更多的控制,避免使用Python等高级语言时带来的效率和灵活性问题。
    • 高级语言(例如Python)虽然易于使用,但也带来了许多不必要的开销和局限性,开发者可以通过自定义工具或语言来弥补这些不足,达到更高效、灵活的开发效果。

总之,这段讨论的核心在于如何优化和自动化资产处理过程,避免冗长的手动操作,利用编译器和程序语言本身的优势来简化开发。通过自定义工具和内存管理,开发者可以更高效地处理数据,减少低级操作的复杂性,从而提高整体的开发效率。

如何让字形表更稀疏,这样就不需要存储那些可能不会被绘制的字距调整(比如“ww”)?

这段讨论的主要内容涉及如何优化字体表的存储,尤其是对于字体表中那些不常用或不需要绘制的字符(如一些特殊的修复字符),如何使存储更加稀疏,避免浪费空间。以下是详细总结:

  1. 稀疏表的存储

    • 为了避免存储不常用或不会被绘制的字符,可以使用稀疏表来减少存储空间的浪费。稀疏表是通过仅存储实际使用的数据来优化存储,而不是存储所有可能的字符和相关信息。
    • 有多种方法可以实现稀疏表的存储,其中一种可能的方法是使用“二叉树”来存储数据。二叉树可以有效地将数据存储为树形结构,在查找时可以通过树的结构进行高效查询。
  2. 性能与查找速度的权衡

    • 使用二叉树存储可能导致查找速度变慢,因为查找操作需要遍历树结构来找到对应的条目。虽然树结构本身能够节省空间,但在实际使用中,这种查找方式可能并不高效,特别是在需要频繁查找的情况下。
    • 因此,选择何种方式存储稀疏数据时需要平衡空间利用和查找速度的关系。如果查找速度非常重要,那么可能需要考虑其他更高效的存储结构。
  3. 最常用的字符和稀疏表的优化

    • 大多数语言中并不需要存储所有的字符,尤其是一些低频使用的字符。例如,像中文这样的复杂语言,可能需要处理大量的字符,但大多数情况下并不需要处理每一个字符的详细信息。
    • 通过识别出哪些字符是最常用的,可以优先存储这些字符的相关数据,而将其他不常用的字符或修复字符排除在外,进一步优化存储空间。
  4. 字体表的存储大小和缓存问题

    • 字体表的存储可能会非常庞大,特别是对于大字符集(如包含大量字形的字体)。例如,一个包含6400个字形的字形表,每个字形占4字节,总共就需要25KB的空间。
    • 这意味着如果在字体表中引入过多不必要的数据,不仅会占用大量内存空间,还可能影响到CPU缓存的使用效率。特别是在L1和L2缓存非常有限的情况下,存储过多的冗余数据可能会导致性能瓶颈。
  5. 数据压缩

    • 为了进一步减少存储空间的占用,可以考虑对字体表进行压缩。例如,可以将浮点数(通常占4字节)压缩为16位的数据,这样每个字形所占的存储空间就可以减少一半。通过这种方式,可以显著减小字体表的存储需求,同时仍然保持足够的精度来进行渲染。

总结来说,优化字体表的存储主要是通过使用稀疏存储结构(如二叉树)来减少空间浪费,同时考虑到查找速度和缓存效率,避免存储过多低频字符数据,并通过数据压缩进一步减少内存占用。这些方法有助于提高系统性能,尤其是在处理大字符集和复杂语言时。

OMGaGiantRock 你使用 CodePointCount < CodePoint,但如果第一个 CodePoint <> 0,这样不会失败吗?或者我们现在需要包括 ASCII 控制字符才能使 CodePointCount 工作吗?

  1. 代码点计数与控制字符

    • 使用代码点计数时,代码点的总数可能少于实际的字符数量。这个问题可能与ASCII控制字符相关,控制字符在计算代码点时是否需要考虑。
    • 如果第一个代码点不是零,系统不会失败,这表明系统在处理这些数据时具有一定的容错能力。
  2. 紧凑的代码点空间

    • 最终的目标是将代码点空间进行压缩,而不是基于Unicode标准(因为Unicode包含成千上万的字符)。目标是根据字体中实际包含的字形数量来建立代码点空间。
    • 这意味着,系统将根据字体的实际使用字形来调整代码点的存储方式,而不依赖于广泛的Unicode字符集。
  3. 使用滑动窗口技术

    • 为了管理不同的代码点范围,将使用滑动窗口的方式。这是一种通过逐步调整窗口来处理数据的方法,能有效管理字体中不同范围的字符。
  4. 进一步的语言处理和压缩

    • 在处理语言相关的内容时,还会进行额外的压缩层级,将字符集压缩到更紧凑的空间。对于像ASCII这样的字符集,由于其本身已经非常紧凑,因此可以很容易地存储少量额外的字形。
    • 这意味着对于大部分语言,系统可以通过合理地压缩和存储字形来节省空间,尤其是在开始时只需要存储一小部分额外字形时,系统会保持高效。
  5. 延迟实施压缩操作

    • 目前,虽然有这些优化和压缩的计划,但这些操作还未开始进行,因此现在不需要过于担心这部分的实现细节。系统将会在后期逐步引入这些压缩技术和优化,最终实现更加高效的代码点存储。

你以后会有关于各种元编程技巧吗?一些对开发者有用的技巧?

  • 会展示一些有用的编程技巧和方法,包括一些针对开发者的实用技巧和窍门。这些技巧将帮助提升开发者的技能,尤其是在某些特定的编程领域,如“元编程”技巧(可能指某种编程方法或技术)。

你打算支持哪种中文方言?

计划支持所有语言,但最初会选择一个现有的方言进行发布,比如阳光(Yangon)常用的方言,作为一个示例,确保游戏支持另一种语言且能够正常运行。由于游戏将会是开源的,意味着在游戏发布后的两年内,所有内容都会开放源代码。这将使得其他人可以自由地贡献语言包。

此外,玩家能够直接对游戏进行本地化和翻译,而不需要与我们进行沟通。游戏将会解析启动时的目录中的资产文件,允许用户将自己的翻译内容放在这些文件中,方便其他语言版本的用户使用。目标是确保任何人都能在官方版本不支持的语言下进行游戏,无需通过任何复杂的审批或协作流程。

我们希望通过这一机制,游戏能够更加多样化,支持更多语言,并且为社区提供足够的自由度,使得任何语言都能得到有效支持。

你觉得这个游戏最终会有多大?

游戏最终可能会非常庞大,甚至达到没有任何电脑能够承载的程度。如果有电脑尝试运行这个游戏,可能会因为其庞大的数据量而导致系统完全崩溃,甚至形成一个“黑洞”般的状态。这个游戏的规模可能如此之大,以至于它超出了目前硬件的承载能力,无法被任何常规计算机处理和运行。

右到左怎么样?

支持从右到左的语言其实非常简单,几乎不需要做什么改变,甚至都不确定是否需要调整代码中的常规操作。唯一需要改变的就是文本对齐的起始位置。具体来说,唯一需要修改的地方就是将“左边缘”改为“右边缘”,其他的代码没有任何假设方向的问题。

所有的字符都有一个锚点,这个锚点可以设定在右下角,而不是左下角。而字符渲染的过程中,卡尼表(kerning table)会告诉字符间距应该是多少,甚至可以使用负值来表示。这样修改后,代码中剩下的部分完全不受影响,基本上不会有任何困难。

总的来说,要支持从右到左的语言,唯一需要做的就是对齐位置的调整,整个过程对于现有代码来说是非常容易实现的。如果需要支持,完全可以做到。