我们可以在资源处理器中使用库
因为我们的资源处理器并不是游戏的一部分,所以它可以使用库。我说过我不介意让它使用库,而我提到这个的原因是,今天我们确实有一个选择——可以使用库。
生成字体位图的两种方式:求助于 Windows 或使用库
基本上,我们在资源处理器中有两种方式来实现这一点。一种是直接从Windows获取字体,另一种是使用某种库。
最初,我的想法是直接展示如何从Windows获取字体,因为这样非常直接。当你在Windows上运行资源处理器时,你可以直接提取字体,而其他平台如Linux也有类似的API可以用来提取字体,操作起来也非常简单。
不过,经过一番思考,尤其是在看到有人提议可以在这里使用库之后,我开始重新考虑这个问题。
决定使用 stb 库
如果要使用库的话,会选择哪些库。我总是推荐使用STB库,因为这些库是由Simon Barrett编写的,专门为游戏开发者设计,且非常方便集成到项目中。
我的想法是,今天可以通过STB库来展示如何获取字体,顺便教大家如何使用这些库,因为这些库是我推荐的,且它们设计得非常简洁易用。由于之前设定的规则是游戏代码中不使用库,因此在实际的游戏代码中我们不会使用这些库。这个规则虽然有些随意,但也是整个系列的一个原则——游戏的代码要完全由我们从头开始编写。
今天的目的是为了回应大家的请求,展示如何使用STB库获取字体,同时也可以在之后介绍如何直接通过Windows来获取字体,如果有人对此有兴趣的话。而且,如果有人想在自己的游戏中使用这个库,现在就可以了解如何操作了。
此外,STB库的一个好处是,它们非常易于使用,哪怕是对于那些注重细节的开发者来说,也不会像其他一些库那样带来痛苦和困扰。因此,使用这些库会非常方便,适合各种开发者使用。
stb 库
如果打算使用STB库,可以按照以下步骤操作:首先,搜索“STB libraries”,就能找到STB的官方网站,网址是stb
。在这个页面上,提供了很多非常实用的库。
其中,STB图像库(stb_image)是一个很棒的选择,能够快速将图像加载功能加入到代码中。这个库只包含一个文件,而且支持多种常见的图像格式,比如JPEG、PNG、BMP和PSD等。它的实现不会按照完整的规范来读取图像,而是只读取游戏开发者通常需要的部分,这样能简化使用的复杂度。
如果有人问我如果要用这些库会选择哪些,我会推荐这些库,因为它们非常简洁易用,而且会减少开发过程中的痛苦和困扰。除此之外,STB库是公共领域的,这意味着没有任何使用限制。你可以自由地分发它们,不需要特别的授权或者标注出处。虽然如果使用这些库发布产品时,可以适当感谢一下开发者,但并不是强制要求的。
在STB官网上,每个库都有简单的描述,说明它们的用途和功能。总体来说,这些库是非常适合开发者使用的,特别是当需要快速实现某些功能时,STB库可以大大减少开发难度。
https://github.com/nothings/stb
stb_truetype 库
如果你特别想处理字体相关的内容,特别是想要将字体文件转换为位图格式,应该关注的库是stb_truetype。这个库正是实现了我们所需要的功能,能够将TrueType字体文件进行栅格化处理,生成适合渲染的位图。它完全符合我们资产处理器的需求,可以准确地将字体转换为我们需要的格式。
stb_truetype库的功能非常简单明了,专门为处理TrueType字体文件设计,能够轻松地将字体转换为位图,这对于开发过程中需要渲染文字的部分非常有帮助。
stb 库的结构
这些库的结构非常简单明了。每个库都是一个单独的头文件(.h
文件)。比如 stb_truetype 库,它的实现方式很特别。这个库的头文件内部实际上包含了一个预处理指令(#define
),用于决定是否包含库的实现部分。
具体来说,如果你只需要头文件中的声明部分,可以直接包含这个 .h
文件。如果你还需要实现部分(即 .c
文件的代码),你只需要在包含头文件之前定义一个预处理指令,比如 #define STB_TRUETYPE_IMPLEMENTATION
,这样编译器就会把实现部分的代码包含进来。如果你没有定义这个预处理指令,库的实现部分就不会被编译进项目中,只有头文件中的声明部分会被编译。
也就是说,这个库的实现方式实际上是将头文件和源文件合并为一个文件。你只需要一个文件就可以使用该库,不需要单独的 .h
和 .c
文件。这种方式非常方便,尤其适合简单集成的场景。
如果要使用这个库,首先需要从 stb_truetype 的官方网站或GitHub页面下载库文件。可以直接获取该文件并将其包含到项目中。虽然可能会遇到一些下载和使用上的小问题,但本质上这是一个非常简单和直接的库,适合快速集成使用。
使用库
使用 stb_truetype 库的方法其实非常简单,下载也很直接。如果你不是 GitHub 专家,可以点击 “raw” 按钮,然后选择 “另存为” 来保存文件。虽然这不是最推荐的下载方式,但对于一些不熟悉 GitHub 操作的人来说,这种方式仍然是可行的。
下载之后,文件就保存到了本地。与其他库不同,stb_truetype 库有一个非常显著的特点,就是它的设计非常简洁合理。它只包含一个文件,这使得它非常易于维护和集成到项目中。所有的代码都集中在一个文件内,不需要多个文件分散管理。这种简单的结构大大减少了整合和管理代码的复杂度。
相比于很多其他库,stb_truetype 库的设计思路更符合简洁、高效的编程原则。它的集成非常清晰,可以很容易地嵌入到项目中,且维护起来非常方便。这也是为什么这种库很适合用于个人项目,或者那些追求高效、简单实现的开发者。
库的集成,只有两行代码
如果要在项目中使用 stb_truetype 库,我们可以直接在代码中引入库文件,并开始渲染字体。首先,在代码中包含 stb_truetype.h
头文件,接着,我们需要在代码中通过 #define
来启用库的实现部分。通过定义一个宏,告诉编译器我们需要包含实现部分(而不仅仅是头文件),这时代码就会编译通过并且生效。
这两行代码的操作非常简单,不需要额外下载大量文件,也不需要配置复杂的构建系统。只需下载一个文件并添加两行代码,便能直接在游戏中实现 TrueType 字体的渲染。相比于其他库需要配置复杂的依赖关系、链接设置以及处理大量编译选项,stb_truetype 库的简洁性是非常突出的。
这一点尤为重要,因为它避免了我们在整合库时遇到的麻烦,如链接多个文件或管理依赖项。只需要将一个文件下载到项目中,并进行简单配置,就能轻松使用 TrueType 字体渲染库。这种简化的流程非常适合快速开发和小型项目,也使得后续的维护变得更加方便,不必担心额外的复杂依赖或不必要的资源消耗。
在接下来的工作中,可以将字体渲染的代码整合进 asset processor(资产处理器)中,并最终生成字体资产文件。这样可以实现将字体数据转化为游戏中所需的格式。
stb 库的文档
在查看 stb_truetype 库时,有一个非常棒的特点是,库的头文件(.h
文件)中已经包含了使用文档和示例程序。这意味着即使不记得如何使用这个库,也没有关系,因为在头文件中已经详细列出了如何使用它。
例如,头文件中提供了关于如何将字体数据转换为位图的示例代码。如果需要将字体渲染为适合游戏的位图,可以通过库中的函数直接进行。这些示例程序展示了如何将字体图形渲染为一个矩形区域,或者如何生成每个字形的单个位图。
特别的是,库内置了一些有用的工具函数,可以帮助开发者简化操作。例如,提供了获取字体字符位图的函数,只需要传入一个字形的代码点和矩形区域,就能将该字形渲染为一个位图。这种封装大大减少了开发者需要编写的代码量,也让使用这个库变得更加简单和方便。
因此,通过查看头文件和示例代码,开发者可以快速了解如何使用这个库,并且利用它提供的功能来实现自己的需求。整体而言,stb_truetype 库的设计非常考虑开发者的使用便利,减少了很多不必要的复杂度。
栅格化一个字母
这个函数的主要作用是生成给定 Unicode 代码点的位图。为了实现这一点,首先需要定义一个函数,这个函数会调用 stb_truetype
库中的某个方法,该方法可以根据指定的字体信息生成字体位图。
函数内部会声明一个用于存储字体信息的结构体,这个结构体是通过传递地址来实现的。接下来,需要准备一个缓冲区,这个缓冲区用于存储从字体文件中读取的信息。字体文件的内容会被读入这个缓冲区中,之后会用来生成字体的位图。
在实现时,程序已经具备了读取文件的功能,例如 game_platform
中的 debug_read_entire_file
方法,它可以帮助读取字体文件。通过这个方法,可以快速读取所需的字体文件并将内容存入内存缓冲区。
更重要的是,stb_truetype
库本身并没有强制要求传入文件句柄或文件名,而是允许传入一个已经准备好的内存缓冲区,这样就避免了不必要的依赖和额外的代码操作。它非常灵活,能根据使用者的需要进行操作,而不是强行要求使用特定的文件读取方式。这种设计方式减少了开发者的负担,使得操作更加直观和简便。
举个例子,可以选择 Windows 系统中常见的字体文件(如 Arial 字体),然后通过 debug_read_entire_file
函数读取该文件。这样就可以将字体文件内容加载到内存中,之后便可以用来生成位图。
将整个内容传递给 stb_truetype
在读取完字体文件之后,接下来会将整个字体文件内容传递给 stb_truetype
库,让它进行处理。具体来说,我们会调用相应的函数来处理字体文件内容,生成所需的位图。
其中一个关键的操作是 stbtt_GetFontOffsetForIndex
函数,这个函数的作用是获取字体文件中的某个字体的偏移位置。如果字体文件中包含多个字体文件,这个函数就能根据指定的索引返回相应字体的偏移位置。该函数的作用是帮助从一个字体文件中获取指定索引的字体。
当处理完这个步骤后,可以继续调用 stb_truetype
中的 stbtt_GetCodepointBitmap
函数来获取字体的位图。这里的“代码点”(codepoint)是一个唯一的数字标识符,用于表示特定的字形,比如字母 t、汉字“肉”等字符。
在获取到字体文件后,程序会初始化相应的字体结构体,然后通过传递这个结构体给 stbtt_GetCodepointBitmap
函数来提取位图信息。这样,我们就能从字体文件中提取特定字符的位图数据,并将其用于图形渲染等后续操作。
使用 GetCodepointBitmap
首先,关于如何获取字体的位图,程序会通过调用函数来获取相应的位图信息。这个过程实际上是基于已知的字体参数,例如 scale x
、scale y
、code point
、宽度和高度等。通过这些参数,程序能够生成一个与指定字符(代码点)相关的位图。
该函数的返回值是位图数据,实际上就是一个单色位图(monochrome bitmap)。这与我们之前讨论的位图是一样的,唯一的区别是这个位图不包含颜色信息,而是以黑白形式呈现。这意味着它只是通过0和1来表示像素,其中0代表空白像素,1代表填充像素。而关于字体的抗锯齿处理,也通过这种方式在位图中表现出来。
总的来说,获取的位图数据仅包含字体的形状轮廓,而不包含任何颜色信息,这是因为大多数客户端处理字体时都只需要黑白图像。
字体缩放;点和像素
在获取字体位图时,首先需要传入一些参数,如代码点(code point)、缩放因子(scale x 和 scale y)。其中,scale x
只是决定了水平的缩放比例,而 scale y
则稍微复杂一些。因为在 TrueType 字体文件中,scale y
的单位通常是以点(points)为单位,而我们常常需要的是像素(pixels)。为了简化这一过程,库提供了一个实用函数 stbtt_ScaleForPixelHeight
,这个函数可以根据给定的像素高度来计算出应该传递的正确缩放比例。这样,我们就能够方便地将字体缩放到所需的像素大小。
例如,假设我们希望获取一个 128 像素高的字形,我们只需使用该函数来确定相应的缩放比例,然后传入 stb_codepoint_bitmap
函数,就可以获得我们想要的字体位图。这时,我们传入一个 Unicode 代码点(比如字符 ‘n’)来获取对应的位图。
一旦获取到位图,返回的数据包括多个关键信息:位图的宽度(width)、高度(height)、X 偏移量(x offset)和 Y 偏移量(y offset)。其中,宽度和高度表示位图的尺寸,X 和 Y 偏移量则表示字符在位图中的位置偏移。
这种方式为字体渲染提供了精确的控制,能够方便地获取和调整所需字符的位图信息。
记录返回位图的大小以及 X 和 Y 偏移量
在获取位图时,返回的位图是一个紧凑的位图,即没有多余的空白区域。对于每个字形,位图会尽量紧密地包含该字符,不会有额外的空白区域围绕着它。因此,位图的尺寸通常会有所不同,取决于字符的形状和大小。
位图的 XOffset
和 YOffset
参数用于调整位图的对齐方式。这些偏移量帮助在渲染多个字符时确保字符之间正确对齐。例如,字符“A”通常比字符“n”高,因此其位图可能需要在Y方向上进行偏移,以确保字符之间对齐。这些偏移量确保了字符能够在字体渲染时正确排列。
此外,位图返回的可能是从上到下的排列方式,因此在处理时可能需要进行垂直翻转(上下反转),以适应不同的渲染需求。这种垂直翻转是因为位图通常是从顶部到底部绘制的,而不是从底部到顶部。
关于内存管理,获取位图后,可能需要释放内存。虽然文档中没有明确指出是否需要释放,但通常情况下,像这样的位图数据可能需要显式地释放内存资源。
释放 stb_truetype 分配的内存
在获取位图后,我们会进行一些额外的处理,确保一切顺利。特别地,位图的内存管理非常重要。处理完位图之后,需要释放相关的内存资源,这时使用 free
函数来释放位图的内存。不过,free
函数的参数并不是直接的位图,而是需要传递一些用户数据,例如传递一个内存管理区域(arena)来处理内存分配。这是因为位图可能涉及到更复杂的内存管理需求。
至于编译,整个过程会经过编译阶段,确保代码能够顺利运行。在此过程中,可能会遇到一些与音频处理相关的代码,需要特别注意。这些步骤看似复杂,但通过合理的内存管理和按需编译,就能顺利完成字体渲染任务。
欣赏完成得很好的工作
整个过程其实非常简单,基本上就是通过这个库来进行接口交互。这个库的设计非常好,使用起来非常直接且高效。只需要少量的代码,甚至无需处理复杂的依赖和配置,就可以在游戏中实现字体渲染功能。相比现代的大型API或者库,这种简洁的设计让人觉得很不一样,尤其是当看到一些现代的库需要成千上万行代码来完成一些基本的任务时,实在是显得不值一提。
这个库的优势在于,它非常符合我们的需求——我们需要做的功能,只需要调用相应的函数,完成后就能直接获得所需的结果,不需要考虑复杂的设置或配置。它将复杂的任务简化为简单的调用,使得开发过程更加高效和流畅。
如果所有库都能像这个库一样设计得如此简洁和易于集成,那么开发过程将会变得更加轻松。相比之下,很多现代的API和库往往包含大量的冗余代码和复杂的依赖,反而让开发者感到头疼。因此,这种库的设计理念和实现方式为开发者提供了极大的便利,也让整个开发体验变得更加愉快。
使用字符位图作为加载的位图
在这个过程中,我们首先创建了一个空的位图,并通过使用字体库加载并处理字形。具体来说,首先初始化了一个空的位图,然后通过给定的字体数据和参数(如宽度和高度),生成了一个字母“n”的位图。为了实现这一点,我们需要先通过字体库读取并获取字形数据,然后将其转换成32位的位图格式,每个像素占四个字节。
这个转换过程实际上并不复杂。我们只是通过将从字体数据中读取到的每个像素值,转换为目标格式,然后填充位图内的每个像素。位图的转换是通过将字形中的每个像素值与其对应的透明度值(alpha值)进行混合来完成的。混合后的结果就是带有透明度的颜色值。
接下来,我们通过一个简单的循环遍历每个像素,进行必要的位图数据转换,最终生成所需的位图。这里需要注意的是,位图的生成是逐行进行的,每一行的数据在内存中是连续存储的。
当完成位图的转换后,我们还做了一些清理工作,确保一切正常进行。接下来,将生成的位图存储在合适的位置,并在游戏初始化时加载这个位图作为测试使用。然后,在游戏启动时,可以通过调用相应的代码来生成并显示该位图。
如果没有错误,最终的结果应该是成功创建并显示字母“n”的位图。尽管在代码的实际运行中可能还需要做一些细节上的调整,例如确保正确显示图像,或者解决潜在的内存管理问题,但总体流程就是如此。
显示位图
在这个过程中,首先决定将位图显示的代码放在头部渲染部分,这样可以避免将大量的“头部”文字直接显示到屏幕上。相反,选择通过显示测试字体来代替。测试字体的目的是验证位图渲染是否正常工作。
在代码中,我们使用了一个名为“加载位图”的功能,这个功能要求传递一个内存地址。接着通过传入测试字体的相关数据,来加载并显示位图。为了进行正确的渲染,需要确保传递给加载位图的内存地址是正确的。
在处理完这些准备工作之后,执行代码并运行测试,检查位图的显示是否如预期一样正确。如果没有出现错误,最终应该能够看到正确渲染出来的字体,而不是直接显示的“头部”文本。
测试它
在这个过程中,出现了一个问题,测试字体并没有正确显示在屏幕上,导致感到很失望。为了找出原因,决定回到代码中,逐步排查问题。通过进入调试模式,可以查看之前调用的函数,进一步分析哪里出了问题。
通过调试,可以看到调用的函数是“MakeNothingsTest”,然后逐步跟踪这段代码,查看每个步骤,确定是哪一部分没有正常执行,进而定位到问题所在。
检查 MakeNothingsTest
在调试过程中,发现了一些问题。首先,检查了加载的字体文件,确认文件内容已经成功加载,并且包含了大量的字形信息。对于每个字形,宽度和高度的值看起来都合适,且没有异常。
接着,检查了生成的位图。虽然某些字形的 alpha 值设置为 1,但在查看过程中发现有些地方存在问题。例如,某些 alpha 值为 70,显然表明在处理位图时出错了。通过调试逐步排查,发现可能是目标位图未正确扩展到 32 位,导致颜色或透明度处理出现问题。
为了进一步确认是否有问题,检查了源代码中的数据,发现某些数据似乎没有按照预期方式工作。尽管设置了正确的 alpha 值,但位图的显示效果并未达到预期。通过进一步的调试,推测可能是位图在存储或渲染时未能正确应用 alpha 值,导致最终结果与预期不符。
总的来说,问题在于某些渲染和位图处理上的细节,虽然数据看起来正常,但由于没有正确处理这些细节,最终效果未能正确显示。
调试绘制代码
在调试过程中,发现了一个问题。查看了游戏的测试状态和内存,确认内存中确实存在数据。然而,尽管应该在屏幕上显示内容,但实际显示的结果并没有按预期出现。这让人感到困惑,因为从内存数据来看,一切似乎正常。
目前,无法完全理解为什么没有看到任何渲染结果,感觉这个问题有些奇怪,可能遗漏了某些明显的细节或步骤。需要进一步分析和排查可能的问题,以确保渲染流程正常。
MakeEmptyBitmap 已过时,缺少一些初始化代码
问题出在没有更新 MakeEmptyBitmap
函数,使其遵循新的位图规则。原来的代码已经不适用了,需要根据新规则进行一些初始化工作,特别是要根据输入调整位图的宽度和高度。可以使用宽度或高度的比例来计算,确保适应新的要求。
此外,不再关心像素的具体位置或中心等信息,只需要根据新的标准进行设置和调整。这些改动会帮助解决渲染问题,使位图能够正确显示。
再次测试
显示的不对
忘记跳到下一行
问题出在位图的方向上。当前的位图是按照从上到下的方式排列的,而所使用的库默认假设位图是从上到下的(top-down),而我们的惯例是从下到上(bottom-up)。这就导致了显示的字符(如字母“n”)可能会显示反转或错位。
为了修复这个问题,需要对加载的位图进行翻转,使其符合我们的惯例,即将位图从底部到顶部加载,这样字母就会正确显示。此时,字母的方向和排列应该正常,符合预期效果。
这个问题可以通过在代码中添加位图翻转的步骤来解决,当前的代码已经处理了从单色到彩色的转换,接下来需要在此基础上增加对位图方向的调整。
将字母位图上下翻转
解决问题的方法非常简单。只需要对位图进行翻转操作,将位图的行反向排列。具体来说,可以通过调整位图的行指针,从最后一行开始逆向写入数据。这样就能使位图符合我们的惯例(即从下到上)。这一操作非常简单,并且能够解决显示问题。
使用这个方法后,字母和字符应该会正确显示,不会出现倒置的情况。这是因为所使用的库默认采用的是自上而下的位图格式,而我们的程序需要底部到顶部的位图格式。因此,通过翻转位图,可以确保一切正常。
总结来说,使用这个方法能够快速解决问题。谈到库的使用,尤其是图像处理库,stb库无疑是一个非常好的选择。它设计得非常简洁,易于使用,而且功能全面。stb库具有很好的架构设计,能够高效地完成大量工作,避免了其他库常见的复杂性和问题。因此,在大多数情况下,推荐使用stb库,而其他库的使用则相对较少。
将此代码移到资源构建器中
为了避免在实际的游戏代码中使用任何外部库,将所有的库功能移至资源构建器中是一个合理的选择。具体来说,处理字体的部分已经从游戏代码中剥离,并且移到了资源构建器中。通过这种方式,游戏本身仍然不依赖任何库,而资源构建器则可以使用外部库(如stb库)进行处理和加载。
在资源构建器中,处理字体的逻辑已经被重新整理,这样就可以在构建资源时加载并处理字体等资源。当资源加载到游戏中时,我们将其转换为游戏能够使用的格式。虽然可以直接将这些资源存储到资产数组中,但接下来的步骤是需要扩展资源格式,加入额外的内容,例如字符映射表等,这些内容是我们需要在文件中存储的,以便在游戏加载时能够使用。
目前,我们的目标是处理和加载这些资源,并将它们转化为游戏能使用的格式,这样游戏的运行过程中就可以通过这些格式化的资源正常渲染和显示了。这一过程中的关键是保持游戏代码的简洁和高效,避免直接在游戏本体中进行过多的资源处理。
将 game_asset_type_id.h 合并到 game_file_formats.hβ
决定将资产标识符(game_asset_type_id)和文件格式(game_file_formats)合并在一起,这样的设计是为了简化代码结构,并消除不必要的冗余。通过这种方式,可以将两个看似不同的功能合并为一个统一的结构,从而提高代码的整洁性和可维护性。
在具体操作上,原本分开的部分被统一处理,删除了一些多余的文件和类型定义,使得整体结构更加简化。对这些调整没有太多情感上的纠结,目标只是让代码更简洁、更高效。因此,尽管删除了一些以前的结构和实现,这种改变并不会带来太多遗憾,反而是为了更好地推进项目。
为每个字母添加一个位图到资源文件
在这里决定做一些非常不寻常的事情,尽管这看起来可能并不是最理想的做法,但既然有这个能力,就决定尝试一下。想要将一个资产(Asset)直接用于处理字体(font),这种方式明显有点过度使用了现有的资产系统。虽然并不建议这么做,但考虑到可以这样操作,就试试吧。
具体实现上,首先定义一个新的资产类型 asset fonts
,然后在资产构建器中进行处理。接下来,为了创建字符集,选择从字母 a 到 Z 生成并将其直接插入到资产系统中。每个字符都用一个 code point
标签标记,表示它的 Unicode 代码点。
虽然这种做法在理论上是非常低效和不负责任的,但目前并不关心执行效率,因为这是在进行资产处理阶段。后期可以对这个过程进行优化,所以现在可以先不考虑速度问题,直接按照这种方式进行处理。这虽然是一种非常不负责任的做法,但考虑到目前的开发阶段,并不觉得这会造成太大的问题。
重新加载整个字体文件并让 Sean 解析它
计划重新加载整个字体文件,并每次解析一次,这虽然是很不高效的做法,但目前不在乎性能问题。决定使用 AddCharacterAsset
方法来实现字体的加载和处理。这个方法会接收一个资产结构、一个字体文件以及代码点。整个过程的思路是:首先使用已有的代码读取整个文件,传入字体文件后,通过某个函数来释放这个文件的内存。
虽然这一做法显得有些混乱,但在目前阶段并不打算关注效率,目标是尽快实现功能。发现之前在处理文件时,我们已经做了一些内存管理的优化,尽管当时没有完全记得细节,但代码中确实有处理文件释放的部分。虽然这种做法非常简单粗暴,但也证明了开发中会逐步意识到内存管理的重要性,最终虽然简单,但也合理有效。
干得好
首先,决定读取整个字体文件并在使用后释放它,因为此时并不需要保持任何不必要的数据。不同于其他情况,这次不需要保留任何内容。接下来,打算检查 stb
库中的相关功能,确保在处理完字体文件后能够释放相应的内存。虽然没有详细检查过,但猜测 stb
库在加载字体时可能不会进行内存分配,或者是它自己管理内存,所以没有特别的释放操作。
最终,发现字体数据处理并不需要额外的内存管理,它只会返回纯数据,不需要额外的操作来释放内存。于是,整个清理工作完成了。
接下来,重点转移到处理与位图加载和释放相关的工作,这虽然看起来有些复杂,但也不难处理。这一过程让整体代码变得更加简化,也方便了后续的开发工作。
将字体栅格化测试代码调整为资源系统
在处理字体时,需要通过 AddCharacterAsset
函数获取合并后的位图。接下来,创建一个实际使用该资产的位图。首先需要确保在处理文件之前,检查字体文件是否加载成功,尽管在这个阶段,文件加载是离线的,不是必须的,但为了稳妥,还是做了这一检查。
随后定义一个位图,并为其分配内存。此时,内存分配的大小应该基于位图的宽度和高度进行计算,因此需要申请足够的内存空间。在这里,使用 malloc
来分配空间,分配的内存大小是位图的宽度乘以高度,乘以每个像素所需的字节数。
此外,还考虑到位图可能需要填充,因此需要处理 pitch
,这代表了每行的字节数。由于可能存在填充的需求,因此在内存分配时需要考虑这一点,并在相关代码中加以处理。
最终,完成了位图内存的分配和处理,为后续的位图加载和使用做好了准备。
将资源系统调整为新的资源类型
如果按照这种方式进行操作,就可以先计算所需的内存大小,具体来说就是 pitch
乘以 width
,以确保分配足够的空间。这样,在后续处理完毕后,就可以直接释放这部分内存,而无需关注其他无关的内容。这部分代码的逻辑和之前的实现完全一致,因此可以直接复用。
在完成内存分配和释放后,还需要处理加载的位图。查看 LoadBMP
发现,它返回的是一个 loaded_bitmap
结构,因此可以创建一个类似的 LoadGlyphBitmap
方法,使其与 LoadBMP
的行为一致。这样,就可以通过 LoadGlyphBitmap
加载特定的字符位图,而 LoadBMP
继续处理普通的位图。
在 LoadGlyphBitmap
方法中,除了文件名之外,还需要传入字符的 CodePoint
。此外,还可能需要处理字体大小相关的逻辑,但暂时先不处理,后续可以在优化时补充更多的字符对齐等细节。等到后面有空时,比如明天或下周,再进一步优化字符对齐等问题。
AddCharacterAsset
的逻辑和 AddBitmapAsset
基本一致,唯一的区别在于加载 bitmap
的方式。因此,可以在 AddCharacterAsset
中直接复用 AddBitmapAsset
的代码,只是更改加载位图的方式。例如,之前 AddBitmapAsset
是在写入时加载位图,因此可以在 AddCharacterAsset
中创建一个类似的代码路径,专门用于字体位图的处理。
接着,可以在 asset_type
中新增一个 font
类型,并让 AddCharacterAsset
走相同的逻辑路径。这样,在写入数据时,可以判断 Source->Type
是否为 asset_type font
,然后调用对应的 LoadGlyphBitmap
方法,否则就继续走普通的 bitmap
处理逻辑。这样,代码路径就可以共享,避免重复实现。
在 asset_type font
处理完毕后,还需要存储 Codepoint
,可以直接使用 FirstSampleIndex
存储 Codepoint
,确保后续能正确匹配字符数据。整体来看,整个逻辑非常基础,基本上是按照既有的系统进行扩展和适配。当前的目标只是按照系统既定规则实现功能,所以就只是按照现有模式继续推进。
AddCharacterAsset
在当前的处理方式中,glyphic maps 仅仅是一种记录我们希望执行某个操作的方式。查看具体实现时,可以看到对于 add bitmap asset
,可以使用完全相同的代码。然而,代码量似乎较多,这是一个值得关注的问题。因此,需要考虑是否可以对其进行某种压缩或合并处理,以优化整体结构。
由于这是资产处理器,而当前的部分更像是一个示例,因此可能不会特别关注这个问题。但仍然让人有些犹豫,是否需要进行调整。代码中涉及 codepoint
相关的逻辑,其余部分基本保持原样,没有太大变化。至于 AlignPercentageX
,目前在这里并不特别重要,因为无论如何,在处理字体对齐时仍然需要执行其他操作,因此可以暂时保持不变。
在代码调试过程中,出现了一些错误。其中涉及 TTFFile
变量的使用,它实际上被命名为 entire file
。此外,还有 font file
、file name
、free
、contents
等变量的引用,需要确保 contents
仍然有效。results
变量是 results
,而 add character assets
需要返回 get map id
,应该是这个返回的内容。当前进度基本接近完成,只需进一步确认这些逻辑是否正确。
继续运行我们的生成器ζ
目前的情况有些类似于孤注一掷的尝试,因为手头只有一堆代码,但对其具体行为并没有清晰的了解。不过,既然已经到了这个阶段,不妨直接运行看看效果如何。毕竟,此时已经接近流程的尾声,理论上不会出现什么严重的问题。
当然,从技术角度来看,仍然有很多可能出错的地方。但既然已经准备好了 test_assets_builder
,就直接运行它来看看实际结果。然而,运行后的情况似乎不太理想,输出的结果显示出某些错误,需要进一步排查和修正。
调试一下
发现断点一直进不来
从新生成hha
尝试将头部改为字体ι
目前已经成功获取了 heads
,接下来尝试将其更改为字体。在这一过程中,当设置 BitmapID
时,将从 GetRandomBitmapFrom
中随机获取一个位图,并观察其实际效果。
筛选从喷泉中喷出的字母
如果想让实现变得更有趣一些,同时保持 “nothing” 这个主题,可以将所使用的字母限定在 N
, O
, T
, H
, I
, N
, G
, S
这几个字符内。为了做到这一点,可以利用资产标签匹配系统,从资产库中筛选出符合条件的标签。
目前的代码并未针对资产标签匹配系统进行优化,因此这样使用可能会导致效率较低。如果要提高性能,最好预先构建匹配表,而不是在运行时强行筛选。不过,仍然可以使用 GetBestMatchBitmapFrom
来匹配最合适的资产。
在调用该方法时,可以传入 TranState->Assets
,并将 Asset_Font
作为 type id
。此外,还需要提供一个 asset vector
,其中包含 MatchVector
和 WeightVector
。这两个向量内唯一的内容就是 Unicode 码点,例如,假设要匹配 N
,并且使用的是大写字母,就可以按照这个逻辑进行筛选。
具体实现上,可以简单地用随机选择的方法,从匹配到的字母集合中挑选一个。这里直接使用 random choice
来从 nothings
字母集中选取一个随机字母,并将其作为最终的选择结果。
最终的输出结果展示了所选的 N
,并且成功按照 “nothing” 主题筛选出了符合要求的字母。整体实现基本完成,逻辑已经跑通,效果也达到了预期。
库成功!κ
使用库的成功秘诀在于选择合适的库。绝大多数情况下,最好的选择是使用由 Shah
设计的库,因为这些库通常遵循特定的风格,并且极具实用性。当然,也有其他开发者按照相同的风格编写了一些优秀的库,这些库也值得考虑。
在选择库时,有几个重要的标准需要关注:
- 单文件实现:如果一个库只有一个文件,那通常是一个很好的信号,意味着它易于集成。
- API 直观易用:例如,如果需要获取代码点位图,而库中正好提供了一个名为
gecko_pipe
的函数,能够直接返回所需的位图,那就是一个理想的选择。 - 集成简单:优秀的库应该不需要复杂的构建选项,理想情况下,只需要一两行代码即可完成集成,而无需额外配置。
- 快速上手:如果一个库可以立即投入使用,并且能够轻松整合到已有的资产处理流程中,那它就是一个优秀的库。
在实际操作中,使用符合这些标准的库确实带来了极大的便利。例如,在本次实现过程中,能够快速集成所需功能,而如果使用其他方案,可能还需要花费大量时间去调试 Apple 的相关接口。
这次实践清楚地展示了一个关键问题:如果要使用外部库,应该优先考虑那些结构清晰、简单易用的库。正因如此,当被问到 “如果使用库,会选择哪些库?” 时,答案始终是这些设计合理的库,而不是那些复杂难用的选项。
‘A’ 是最适合空字符终止符的
在当前实现中,null terminator
的匹配存在一些问题。最优匹配方式需要进行调整,以避免 null terminator
被误算在内。
目前的逻辑存在一个小错误:原本 array counts
应该是 recount - 1
,因为 recount
计算时会包含末尾的 null terminator
,但实际操作时并不希望它被计入。因此,更合理的做法是使用 recount nothings - 1
,这样可以确保不会错误地将 null terminator
作为匹配项。
这个调整可以使得逻辑更加严谨,避免 null terminator
影响最终结果,同时让匹配过程更加精准。
直到今晚,我才明白 CMake 的一个使用场景(就是用户编译程序时,能够找到库,这样你就不需要知道它们的安装位置)。但是现在……看起来 STB 方式是唯一合理的默认方式。我想在可预见的未来我已经不再使用 CMake 了
在最初的设想中,CMake
似乎有一个合理的使用场景,即在用户编译程序时,它能够自动找到所需的库,而不需要手动指定它们的安装路径。然而,现在看来,标准的构建方式才是唯一合理的默认选项,因此不再考虑继续使用 CMake
。
总体来说,CMake
等构建工具之所以被使用,主要是因为许多库和程序的构建方式本身存在问题。如果代码组织合理,即便是一个庞大的代码库,也可以仅依靠单文件构建完成。当然,这并不意味着所有情况下都应该采用这种方式,而是说构建工具本质上是多余的。
使用构建工具通常会导致一系列复杂的问题。例如,整个构建流程可能变得异常繁琐,开发过程中需要不断调试 Makefile
,或者因为错误的 CMake
配置导致各种构建失败的问题。此外,还可能遇到诸如错误的 CMake
版本、错误的编译器选项等问题,导致调试过程极为痛苦。
在实际项目中,CMake
可能会带来很多麻烦。例如,在尝试静态链接 Clang
时,遇到了极大的困难,整个构建过程异常复杂,最终不得不将经验总结成文档,分享给其他开发者,以帮助他们避开类似的陷阱。这种复杂性表明,完全可以通过合理的代码结构和构建策略来避免 CMake
带来的问题。
因此,在项目决策时,应该认真考虑是否真的需要 CMake
或其他类似的构建工具。对于许多项目来说,避免 CMake
可能会使整个开发流程更加清晰、高效,并减少不必要的构建调试工作。
外部库、malloc/free,接下来是 Java 虚拟机吗?
在外部库的使用上,如果涉及到 Java Virtual Machine (JVM)
,那么对于资产处理器来说,完全可以自由选择是否使用 JVM
。
资产处理器本身并不被视为游戏核心的一部分,而只是一个用于演示如何处理某些任务的工具。因此,在资产处理器的实现上,如果需要 JVM
,那完全可以按照需求来决定是否引入。
换句话说,在这种情况下,是否使用 JVM
完全取决于具体需求。如果觉得 JVM
适合资产处理器的工作流程,那就可以放心使用,不必过多纠结其对整体架构的影响。
你现在如何写调试信息?
如何编写调试信息,取决于你需要调试的内容。调试信息的目标是帮助开发者理解程序的运行状态,以便快速定位和修复问题。通常,调试信息应该包括关键的变量状态、函数调用的顺序、错误日志以及可能导致问题的其他上下文信息。
编写调试信息时,可以通过以下几种方式来提高其有效性:
-
明确的标签和时间戳:在日志中添加标签(如
DEBUG
、INFO
、ERROR
)和时间戳,帮助区分不同类型的消息并跟踪事件的发生时间。 -
关键变量的输出:输出关键变量的值,尤其是那些可能影响程序流程的变量,这样可以让调试者看到每一步的状态。
-
错误信息和堆栈追踪:对于异常情况,应该记录错误信息和堆栈追踪,以便定位问题发生的位置。
-
函数调用和返回值:在调试时,记录函数的调用顺序以及返回值,特别是那些可能导致程序状态改变的函数。
-
日志分级:根据不同的严重程度分类日志信息,例如
INFO
用于常规操作信息,DEBUG
用于详细的运行时信息,ERROR
用于异常和错误信息。
总之,调试信息应当简洁且富有意义,帮助开发者理解代码的执行流程和状态,从而更有效地发现和解决问题。
换个说法:你如何输出句子来查看调试字符串,比如 FPS 或错误码?
如何生成调试字符串,例如 FPS 或错误代码,暂时还没有完全解决这个问题。可以先暂时把这个问题放在一边,因为这正是下周要讨论的内容。到时候会深入探讨如何处理和输出这些调试信息。所以,先记住这个问题,等到下周再一起解决。