游戏引擎学习第137天

演示资产系统中的一个 bug

我们留下了个问题,你现在可以看到,移动时它没有选择正确的资产。我们知道问题的原因,就在之前我就预见到这个问题会出现。问题是我们的标签系统没有处理周期性边界的匹配问题。当处理像角度这种周期性的标签时,系统不知道如何匹配最接近的值,这就是问题的根源。因此,今天我们需要花一点时间在黑板上讲解这个问题,看看该如何修复它。

黑板:周期性标签匹配

我们面临的问题是如何处理周期性的标签匹配。昨天,有人提出了一个很好的建议,即为了统一处理所有标签,或许我们可以引入"所有标签都是周期性"的概念,对于那些不想作为周期性处理的标签,我们可以为它们设置一个非常大的周期。这是一个非常不错的思路。接下来,我将详细解释一下我们遇到的问题,并探讨如何实施这个建议,即让所有标签都有一个周期,但将其设置为非常大,以免影响那些不需要周期性处理的标签。

问题的根源

我们的核心问题在于使用了一个叫做 atan2 的函数。这个函数的作用是将一个方向向量转换为一个角度,表示物体朝向的方向。举个例子,当我们使用方向向量(如速度向量)时,atan2 会将其转化为一个角度,这样就能得到一个标量值,用作标签值。我们可以根据这个标签值来从资产库中匹配对应的精灵(比如玩家移动的方向,或者其他物体的移动方向)。

问题出现在 周期性 上。atan2 返回的角度是周期性的,意味着当角度转动一圈(360度)时,它会回到初始位置。比如,如果我们转动了360度,就会回到0度,而不是转到一个新的角度。为了匹配资产,我们不能简单地比较两个角度的标量值,像0度和360度,虽然它们看起来差距很大,但它们其实是相同的,都是代表同一个方向。因此,我们需要一种方式来判断这些看似不同的值其实是接近的,这就是周期性带来的问题。

atan2 的输出范围

atan2 返回的结果是围绕0对称的,它给出的角度范围是 -180 到 +180,而不是从0到360。换句话说,如果角度是0,atan2 会返回 -180 到 +180 之间的值,分别表示逆时针和顺时针的180度。如果用弧度来表示,它的范围是 -π 到 +π。因此,我们的目标是让这个匹配机制适应这样的角度范围。

解决方案

我们可以将角度范围编码成 -π 到 +π,这样的匹配会比较容易。比如,某个标签值可能对应 0、±π/2、π 和 -π/2,这样就能够很自然地进行匹配。而且,负π和π实际上是一样的,应该匹配到同一个标签。

对于那些不需要周期性处理的标签,我们可以简单地为它们设置一个非常大的范围,比如从 -100000 到 100000,这样就避免了它们与周期性标签冲突,因为这个范围远远超过了我们需要处理的角度范围。只要这个范围足够大,不会与周期性标签的范围交错,就可以避免错误匹配。

总结来说,通过将周期性标签的范围设置为 -π 到 +π,对于不需要周期性匹配的标签设置一个很大的范围,这样就能够高效且准确地处理标签匹配问题。

黑板:"邻域运算符"

问题的核心是如何在匹配过程中处理周期性角度。在周期性标签的匹配中,当我们有一个角度 A 和一个角度 B,假设我们想要将这两个角度与一个已设置为 π 的角度进行匹配时,我们需要避免计算出 A 和 B 与 π 的差异非常大,导致不正确的匹配。换句话说,我们希望能够处理这种情况,使得无论角度如何旋转,能够正确地认为它们是接近的。

问题的背景

对于周期性角度,我们希望当角度 A 和角度 B 相差很大时,能够将它们视为接近的,而不是认为它们在圆的两侧,相差接近 360 度。例如,当 A 是 350 度,B 是 10 度时,它们实际上应该是非常接近的,因为在 360 度的周期内,它们实际上只相差 20 度。这个问题的解决方案是引入一个"邻域操作符"(neighborhood operator)。

邻域操作符的作用

邻域操作符的作用是计算两个角度的最近距离,而不是简单地通过角度值来比较它们的大小。邻域操作符将两个值映射到它们之间的最小距离,而这个距离是相对的,考虑了周期性的因素。

假设我们有两个角度 A 和 B,我们希望计算 A 在 B 的邻域中时应该如何调整。我们不关心得到的邻域值本身,而是关心调整后的角度之间的差异。具体地,如果我们有一个角度 A,当前角度值为正值且距离 B 很远,我们希望通过邻域操作符将 A 调整成负值,使其更加接近 B。这样,两个角度之间的差异将变得更小,而不是通过直接计算它们的差值(可能得到一个很大的正数)来产生错误的匹配。

举例说明

假设 A 的值是 350 度,B 的值是 10 度。直接计算它们的差值,会得到 350 度和 10 度之间的差值是 340 度,这会被认为它们距离很远。通过邻域操作符处理后,A 的角度会变为 -10 度(即顺时针旋转 10 度),而 B 的角度依然是 10 度,这样它们之间的差值就变成了 0 度,也就是完全匹配。这个调整是通过邻域操作符来实现的。

实现思路

在实现时,我们可以将邻域操作符视作一个函数,接受两个角度值作为输入,输出它们之间最小的差异。这意味着,如果两个角度值相差超过 180 度(例如 A 是 350 度,B 是 10 度),我们会通过周期性调整将其"拉近"到较小的范围,从而获得更精确的匹配结果。

简单来说,邻域操作符的功能就是根据周期性调整角度的差值,使得它们始终保持在最小的范围内,从而确保匹配过程的正确性。

总结

为了处理周期性角度的匹配问题,我们需要使用邻域操作符来计算角度之间的最小差距,而不是直接计算它们的差异。这使得我们可以在角度的周期性范围内,避免因大范围的旋转而导致错误的匹配,确保角度在匹配时总是能够准确地找到最接近的值。

黑板:"邻域距离"

在这里,我们实际上并不需要一个"邻域算子"(Neighborhood Operator),因为我们并不关心生成这个"a",我们真正关心的是计算"邻域距离"。所以,我们的目标是做一个类似于邻域操作的过程,但我们只关心计算两个值之间的距离,而不需要生成具体的邻域值。

我们不需要实际生成邻域值,只需要计算它们的距离就可以了,这样可以让整个过程变得更简单。虽然在某些情况下,确实需要进行邻域操作(例如插值时),但在这种情况下,我们只关心两个角度之间的最短距离,而不需要真正生成邻域。因此,我们只需要计算距离即可,而不需要实际执行复杂的邻域操作。

接下来,假设我们已经知道所有值都在一个特定的范围内(比如在-π到π之间),我们就可以根据这个范围来判断角度之间的关系。具体来说,正数和负数总是有方向上的差异,但这并不影响它们之间的距离计算,因为无论如何,我们都能够在一个周期范围内进行计算。

在这种情况下,最简单的做法是检查这两个角度的符号(即它们的正负),如果它们的符号不同,那么它们就可能不在同一个邻域内。否则,它们就可能在相同的邻域内。

然而,在进一步思考后,我们发现其实并不需要专门进行符号检查。我们可以简化流程,直接跳过这一步。总的来说,我们只需要关注如何处理这两个角度之间的最短距离,而不需要关心它们是否在同一个邻域内。

黑板:将一个负范围的点的符号改变,计算它到正范围点的距离

我们有两个数,一个是正数,一个是负数,目标是计算它们之间的距离。首先,我们发现它们的符号不同,因此我们想要改变其中一个数的符号,使其变成与另一个数相同。具体来说,我们希望将负数变成正数。

为了做到这一点,我们可以通过以下方式来操作:负数的旋转(负旋转)其实就是从零开始,绕一圈的正向距离加上多出来的部分。因此,我们可以从负π开始,向正方向进行旋转,直到到达目标数B的位置,这样就变成了正旋转。

例如,如果B是负数,我们可以通过从负π开始,顺时针旋转得到B,实际上就是等于B加上整个范围(2π)。所以,B变成正数的操作就是将B加上这个周期范围,即加上2π。

这个过程说明,负数通过加上整个周期(2π)就可以转化为对应的正数。这样我们就可以用这种方式来处理符号不同的数值,使它们符合我们需要的范围和计算要求。

黑板:将一个正数移动到负数

如果我们有一个正数,想要将其转化为负数,过程和之前的情况类似,但这次是进行负旋转。

  1. 负数转正数 :

    如果我们有一个负数,我们可以通过加上整个周期(2π)使其变成正数。这样,通过顺时针旋转的方式,可以将负数转变为正数。

  2. 正数转负数 :

    如果我们想要把一个正数变成负数,步骤就有所不同。具体来说,我们需要从正数的位置(例如π的位置)开始,进行逆时针的旋转。

    以正数A为例,假设A位于0, π范围内,要将其转变为负数,可以按照以下步骤:

    • 从π的地方开始逆时针旋转到A的位置。
    • 这时候,我们需要从π位置开始,逆时针旋转到A,这相当于减去一个π,即我们需要将A减去π(因为π是最大正数)。

    例如,如果A是一个正数,位于(0, π)之间,我们可以将其转换为负数,方法是将这个数减去π,使其变成负数。

  3. 总结:

    • 对于负数转正数,我们通过加上2π来实现。
    • 对于正数转负数,我们通过减去π来实现。

    这个过程就相当于在单位圆上进行旋转,改变角度的符号,通过加或者减掉2π或π来实现符号的转换。

黑板:做这个邻域计算的完整公式

我们讨论的是如何计算两个数之间的"邻域"(neighborhood),这个操作实际上并不是特别复杂,我们可以通过计算它们之间的差距来完成。

1. 正数和负数之间的转化:

  • 如果我们有一个正数和一个负数,并且想要计算它们之间的距离,我们首先需要确定它们的符号。如果这两个数符号不同(一个是负数,一个是正数),我们可以通过调整其中一个数的符号,使得两个数具有相同的符号。
  • 具体来说,如果我们想把一个负数变为正数,方法是加上一个周期 2 π 2\pi 2π。反之,如果我们想把一个正数变为负数,方法是减去 π \pi π。

2. 计算两个数之间的距离:

  • 如果两个数符号相同,我们可以直接计算它们的距离,即它们的差值。
  • 如果两个数符号不同,则我们需要做一些额外的操作。我们可以将负数转化为正数,或者正数转化为负数,来让它们具有相同的符号,然后再计算它们之间的差距。
  • 在计算时,我们可以用一种简化的方式,避免进行符号判断。我们可以分别计算两个数之间的差距,然后选择较小的距离作为最终结果。

3. 具体操作:

  • 假设我们有两个角度值,分别是 θ \theta θ 和 ϕ \phi ϕ。
    • 如果 θ \theta θ 是负数,而 ϕ \phi ϕ 是正数,我们可以通过加上周期来将负数转为正数。
    • 如果 θ \theta θ 是正数,而 ϕ \phi ϕ 是负数,我们可以通过减去 π \pi π 来将正数转为负数。
  • 无论是正负数之间的转换,核心操作是计算它们之间的"差距"。我们可以通过加减周期的方式,将符号统一,然后计算差值。

4. 简化计算:

  • 不需要显式判断符号,我们可以直接计算两者之间的距离:
    • 首先计算 θ − ϕ \theta - \phi θ−ϕ。
    • 然后计算 θ − (range) × sin ⁡ ( θ ) \theta - \text{(range)} \times \sin(\theta) θ−(range)×sin(θ),其中 range 是整个范围的大小(例如 2 π 2\pi 2π)。
    • 然后,取这两个差值的最小值,即可得到两个数之间的实际距离。

5. 结论:

  • 通过简化的操作,我们可以避免使用条件分支来判断符号,而是直接计算两种情况的差值,最终选择较小的距离。这样既避免了分支操作,也提高了计算效率。

game_asset.h:将 HalfTagRange 添加到游戏资产

在进行资产匹配时,我们希望将周期性(periodicity)引入到资产中,以确保匹配的准确性和一致性。为了实现这一点,我们需要对周期的理解和管理。

1. 理解周期性:

  • 我们并不需要为每个标签(tag)都单独设置一个周期。实际上,周期通常是由标签的类型决定的,因此只需要为每种标签类型定义一个周期即可。
  • 这样,周期性实际上是和标签类型紧密相关的,而不是每个标签实例都需要一个单独的周期。

2. 周期的存储和管理:

  • 在资产中,每个标签(tag)通常会有一个周期字段,这个周期字段可以命名为 tag period 或者 half tag range,它表示标签的周期范围。
  • 这个"半标签范围"(HalfTagRange)可以理解为每种标签类型对应的周期的一半。例如,如果一个标签的周期是 2 π 2\pi 2π,那么半周期就是 π \pi π。
  • 这样,我们在管理标签时,只需要在资产的标签中为每个标签类型存储一次周期值,而不需要为每个标签实例都单独存储周期。这样可以减少存储空间并提高效率。

3. 实际操作:

  • 每次在处理资产标签时,只需要知道每种标签的周期范围就可以进行相应的操作。例如,对于每种标签类型,我们可以在标签结构中定义一个字段来存储周期信息(如 tag period),并根据该值来进行计算。
  • 通过这种方式,标签的周期性可以高效地管理和应用,从而简化了资产匹配的过程。

4. 总结:

  • 不需要为每个标签单独定义周期,而是可以通过定义标签类型的周期(或者半周期)来管理周期性。这样,我们只需存储每种标签类型的周期范围,而在实际处理时直接使用这些周期信息。
  • 这种方法不仅简化了周期的管理,也提高了资产匹配过程的效率和准确性。

game_asset.cpp:为每个 TagType 设置 HalfTagRange

大多数情况下,我们并不关心周期性问题,周期性标签的出现频率远低于非周期性标签。因此,我们可以采取一种简化的方式来处理标签的周期性。以下是处理流程的详细步骤:

1. 初始化资产时的周期性处理:

  • 在初始化游戏资产时,我们可以遍历所有的标签类型,并为每种标签类型设置一个周期范围。对于大多数标签类型,我们不需要关心周期性,所以可以将它们的周期设置为一个非常大的数字。
  • 这个大数字的作用是标识这些标签类型没有周期性(即它们的周期范围几乎是无穷大的)。这个值可以是一个极大值,只要它是我们永远不会使用的值就行,因为它只是占位符,并不会实际影响计算。

2. 处理周期性标签:

  • 对于需要周期性处理的标签类型,我们不需要事先知道所有这些标签,只需要关注那些确实具有周期性的标签。
  • 目前已经知道的一个周期性标签是"朝向方向"(facing direction),它的周期是 π \pi π,即无论朝哪个方向,最大旋转角度是 π \pi π(180度)。对于这种情况,我们可以直接将其周期设置为 π \pi π,而其他不需要周期性处理的标签则保持为一个非常大的值。

3. 具体实现:

  • 在初始化过程中,对于所有标签类型,我们先给它们设置一个默认的大周期值(如 1 0 10 10^{10} 1010 或其他极大值)。然后,对于确实需要周期性处理的标签(如朝向方向),我们单独为它们设置实际的周期值(如 π \pi π)。
  • 这样,周期性标签的处理就能被单独标识出来,并在后续的计算中正确处理,而其他标签类型则无需进行周期性计算。

4. 总结:

  • 大部分标签都不涉及周期性,所以我们为这些标签设置一个非常大的周期值,表示它们不需要周期性处理。
  • 只有少数几个标签会涉及周期性处理(如朝向方向),这些标签的周期值设置为实际的周期值(如 π \pi π)。
  • 通过这种方法,可以在初始化时简化标签的周期性处理,并在需要时进行特定的周期性计算。

在 BestMatchAsset 中使用 HalfTagRange

在进行标签匹配时,目的是确保匹配结果是合理的,并且能够正确计算标签之间的差异。以下是实现这一过程的详细步骤:

1. 计算差异:

  • 在进行标签匹配时,我们首先计算两个值之间的差异,假设这两个值分别是 A A A 和 B B B,那么它们之间的差异就是 A − B A - B A−B。
  • 但为了确保差异计算时考虑周期性,我们需要引入"邻域差异"这个概念。也就是说,计算时不仅仅是简单地减去 A A A 和 B B B 的差值,还要考虑它们之间的周期性差异,特别是在涉及周期性标签时。

2. 处理符号问题:

  • 在计算差异时,需要检查两个值的符号(正负)。如果两个值的符号不同,那么我们需要进行符号转换,以确保差异的计算符合周期性要求。
  • 为了实现这个功能,可以使用正弦函数(sine function)来辅助判断。如果符号不同,可以根据需要调整差异,确保计算结果在期望的周期范围内。

3. 符号判断和调整:

  • 如果没有现成的正弦函数实现,我们可以通过简单的条件语句(如 if 语句)来判断 A A A 和 B B B 的符号,并根据符号的不同做出适当的调整。
  • 判断符号的方式可以通过比较 A A A 和 B B B 的大小来实现,如果 A A A 和 B B B 符号不同,则需要调整差异值。

4. 最终实现:

  • 可以在标签匹配时,通过判断标签的符号来决定是否需要调整差异值。
  • 如果符号相同,直接计算差值。如果符号不同,则需要调整差值,确保结果正确地反映周期性影响。

5. 总结:

  • 在进行标签匹配时,我们首先计算两个标签的差异,并根据需要调整周期性差异。
  • 通过符号判断和正弦函数,确保计算结果符合周期性要求。
  • 最终,我们通过条件语句(如 if)来判断符号并调整差异,从而确保匹配过程合理且准确。

game_intrinsics.h:编写一个 real32 版本的 SignOf

在进行操作时,遇到一些问题,可能会觉得有些麻烦。为了简化工作,可以假设将来会实现一个更好的版本来处理这些问题。经过一定的调整,现在已经实现了所需的功能,可以有效地进行操作。

game_asset.cpp:继续将 (Half)TagRange 添加到 BestMatchAsset

为了处理某些标签类型的范围,我们需要首先计算标签的半范围,然后根据标签类型的范围调整。具体操作是,我们将标签的范围(TagRange)乘以正弦值,并进行一些运算,来确定最终的范围。

接着,我们通过计算两个数的差异(比如 a 和 b)来得到两种不同的距离值,然后通过取最小值来找到最佳匹配。这意味着我们会选择这两个差异值中较小的那个,以便获得更准确的结果。

此外,为了优化工作,我们可以使用一个新的变量 TagRange 来替代半范围计算,这样可以简化操作。还可以使用 tau(即 2π)替代 π,以便让数学计算更加一致和简洁。总之,通过这些优化操作,最终能得到更好的匹配结果。

调试器:进入 BestMatchAsset

首先,进入到相关代码部分,查看资产组的内容。查看当前的匹配逻辑。当前的匹配是对两个值进行比较,初步结果是将 0 和 0 进行匹配,显然这两个数的差异为 0,因此结果也会是 0,并且没有特别的变化。

接下来,我们看到程序在进行计算时,计算了两个值之间的差异,同时还会计算另一种可能的差异,即完整的一个周期(tau)。这种计算是正确的,因为对于周期性的匹配问题,考虑整个周期是很重要的。这样处理后,程序能够正确地处理周期性差异,确保匹配的准确性。

玩游戏并查看我们新功能

接下来,我们对计算出的两个差异值取最小值,这样结果依然是 0,然后继续进行下一步操作。之后,通过加权计算,得到最终的差异值(DIF)。这种方法在标签匹配上看起来更为准确。

通过这种方式,我们能够实现更精确的方向匹配,并且成功地加载正确的周期性资产。同时,系统也没有任何问题,能够顺利处理周期性资产的使用。因此,现在的实现已经能够很好地处理这些需求,确保一切正常运行。

我们差不多完成了

今天的基本完成了,但还剩下半小时的时间。接下来,想要处理一些遗留的问题。一个问题是目前系统没有正确地处理位移,导致一些效果(比如阴影)显示不正常。原因是在代码中,位移数据被硬编码了,但是我们没有真正地回溯这些位移数据。

我打算先看看标准位移的值,看是否可以统一使用一个固定值。通过检查,发现对于所有对象,位移值似乎都是一致的,具体数值是 72 和 182,适用于所有的对象。因此,我想先快速验证一下,看看是否可以为所有对象使用相同的位移值。

我计划先在代码中做一个临时的修改,看看效果如何。为了调试,我将检查加载位图的部分,确认一下是否有任何问题。

game_asset.cpp:添加 *FileName\[\] 以便遍历位图并设置 TopDownAlign

为了处理位移问题,我计划通过以下方式进行调试。首先,我将所有的文件名放入一个数组中,然后使用一个循环遍历每个文件名。接下来,我将调用 DebugLoadBMP 函数,加载每个位图。

在这个过程中,我决定通过修改代码来测试加载的效果。首先,创建一个 FileName 数组,然后将所有相关的文件名添加到该数组中。接着,使用一个简单的方式加载位图数据。为了确保一切正常,我还会对加载后的数据进行一些调试,确保按预期进行处理。

通过使用 TopDownAlign 来调整这些位图,我能够对这些位图进行检查,确保它们符合需求,正确加载并且显示。最终,这个操作只是一个调试步骤,用来确保没有其他问题干扰,确保数据处理准确无误。

调试器:查看对齐方式

尝试让调试器以正确的方式显示数据,并逐步获取正确的位图数据。这次,数据看起来正常了,显示为 0.5 和 1.56,并且这些值是一致的,没有发现任何异常。

接下来,我需要做的就是整理这些数据并返回正确的值,确保它们可以用于接下来的处理。至此,调试过程中的问题已经得到了解决,位图数据也加载成功了。

game_asset.cpp:将计算出的对齐方式烘焙到英雄位图中

为了确保数据正确,现在需要创建一个 V2 的变量,这个变量用于表示"英雄对齐"(HeroAlign)。这个英雄对齐的值已经通过之前的调试读取出来,分别是 0.5 和 0.156 等。

接下来,每次执行添加位图资产(AddBitmapAsset)时,都可以指定这个对齐值(alignment value)。这意味着,只要设置了这个值,就能确保位图的正确对齐方式。

至此,这部分的工作就完成了。

运行游戏并检查正确的对齐方式

现在,所有的资产都已经正确对齐,系统也按照需求运行了。对此,我感到满意。虽然目前不确定是否已经完全完成所有工作,但至少目前的进展是令人满意的,资产系统已经达到了预期目标。

开始讨论资产文件格式

目前,所有的资产匹配工作已经完成,系统运行得也很平稳,感觉一切都在预期之内。因此,可能是时候开始考虑资产文件格式了。虽然这可能有些早,但从当前的进展来看,已经有了合理的基础,可以开始讨论资产文件的格式和它的工作方式。我觉得我们不需要在资产文件格式中使用描述符,而是希望所有的描述信息都来自代码那一侧。因此,感觉现在的状态已经接近完成,所有的工作都顺利进行,应该可以进入下一阶段了。

game.h:向资产流式传输 TODO 中添加两项,并优先处理音频

目前,资产流式加载系统已经基本完成,剩下的工作就是对文件格式的设计。为了完成这部分,首先需要确保能够从磁盘加载资产,并进行内存管理,避免数据无序地加载。接下来,需要检查是否还有其他方面需要优化或改进。如果以后需要进行优化,那时再处理即可,因此现在可以将资产流式加载部分标记为完成。

考虑到接下来要进行文件格式的处理,可能需要在这之前先处理音频加载部分。如果没有音频加载功能的支持,资产加载系统就无法完全运作,因此音频部分是必须解决的问题。接下来的20分钟里,可以先完成音频加载的实现,确保音频功能能正常工作,然后再着手文件格式的设计,以确保文件格式能够同时支持音频和视频内容。

game_asset.cpp:引入 LoadSound

当前的任务是为音频资源的加载系统做准备。之前已经为音效准备好了资产槽和标签,但目前还没有实现加载音频的功能。为了完成这个目标,需要实现一个与加载位图类似的音频加载系统。换句话说,需要创建一个加载音频的流程,功能上与加载位图非常相似。

首先,可以借鉴LoadBitmap的概念,创建一个LoadSound的功能。这个功能将负责加载音频资源,具体来说,当音频文件的ID被传入时,加载任务会开始,然后执行实际的音频加载操作。这个过程会涉及到类似于位图的工作,但操作的对象会是音频文件。

为了实现这个目标,需要实现以下步骤:

  1. 音频信息结构 :需要定义一个类似于位图的音频信息结构,其中包括音频的文件名、样本数等数据。
  2. 加载音频文件 :创建一个类似DebugLoadBitmap的函数DebugLoadWAV,用于加载音频文件。这将确保音频文件能够被正确加载到内存中,并提供音频所需的信息。
  3. 音频ID和槽管理:为音频资源分配ID并将其加载到适当的资产槽中,这样就能够方便地访问和管理音频资源。

同时,创建一个LoadedSound结构体,这个结构将存储加载的音频信息,例如样本数和音频文件的内存数据。这个结构与位图的LoadedBitmap结构非常相似,主要的区别在于它是针对音频数据的。

在功能上,音频的加载与位图加载非常相似,因此可以考虑将这些功能进行整合,减少冗余代码。如果这两个过程在实现过程中越来越相似,最终可能会将它们合并为一个通用的加载函数,这样可以减少代码量,使系统更加简洁高效。

目前还没有实现具体的LoadedSound结构体,但可以先假设它的存在,并着手开发音频加载的相关功能,之后再根据需要进一步完善音频结构体的实现。总的来说,音频加载系统的基础功能框架已经初步构建,接下来的工作是实现具体的音频加载和管理逻辑。

引入 DEBUGLoadWAV

当前的任务是实现DEBUGLoadWAV函数,该函数用于加载WAV格式的音频文件,并将其数据解析并存储到适当的结构中,以便后续的音频处理。这个过程与DebugLoadBitmap类似,都是读取文件并解析内容,但WAV文件的解析稍微复杂一些。

实现步骤

  1. 读取整个文件

    • 使用DEBUGReadEntireFile函数将WAV文件的全部内容读取到内存中。
    • 检查读取是否成功,如果失败,则终止加载。
  2. 解析WAV文件头部信息

    • WAV文件的格式比BMP文件稍复杂,但仍然是标准的RIFF格式。
    • 解析WAV文件的RIFF头部,确认文件类型是否正确(应包含RIFF标识和WAVE格式标签)。
    • 解析fmt子块,提取音频格式信息(采样率、通道数、位深度等)。
    • 解析data子块,找到实际的音频数据部分,并记录其大小和位置。
  3. 存储解析结果

    • 提取出音频的关键信息,如:
      • 采样率(Sample Rate)
      • 通道数(Channels)
      • 采样深度(Bit Depth)
      • 总样本数(Sample Count)
      • 音频数据指针(Data Pointer)
    • 创建LoadSound结构体,将这些数据存入其中,以便后续音频播放或处理。
  4. 返回加载结果

    • 确保所有数据正确解析,并返回LoadSound结构的指针,使其可以用于后续的音频处理流程。

额外优化

  • 由于WAV格式支持不同的编码方式(如PCM、ADPCM等),当前可以先实现最常见的PCM(线性脉冲编码调制)格式解析。
  • 可以考虑与位图加载流程进行结构化对比,以便未来优化代码复用,例如抽取通用的"加载资源"方法,减少重复代码。

下一步

  • 继续完善DEBUGLoadWAV函数的实现,并进行基本的测试,确保能够正确解析WAV文件并提取音频数据。
  • 检查是否需要调整LoadSound结构的设计,以适应不同格式的音频资源管理。

网络:展示 WAVE 规格

WAV 文件的格式基于 RIFF(资源交换文件格式),它由多个数据块(Chunk)组成,每个数据块包含不同的信息,如文件头、音频格式和音频数据。

WAV 文件解析流程

  1. 读取 RIFF 头部

    • WAV 文件以 RIFF 作为文件头标识(4 字节)。
    • 之后是文件大小信息(4 字节),用于指示整个文件的大小。
    • 紧接着是 WAVE 标识(4 字节),表明该文件是 WAV 格式。
  2. 解析 fmt 块(音频格式信息)

    • fmt 块(子块 ID 为 "fmt ")包含音频数据的格式信息:
      • 音频编码格式(Audio Format) (2 字节),常见值:
        • 1 代表 PCM(未压缩的脉冲编码调制)
        • 其他值可能代表 ADPCM 或其他压缩格式
      • 通道数(Number of Channels) (2 字节),如:
        • 1 表示单声道
        • 2 表示立体声
      • 采样率(Sample Rate) (4 字节),如 44100 Hz 表示 44.1kHz。
      • 字节率(Byte Rate)(4 字节),用于计算数据流的速率。
      • 块对齐(Block Align)(2 字节),用于存储每个采样块的大小。
      • 位深度(Bits per Sample) (2 字节),常见值:
        • 8(8 位 PCM)
        • 16(16 位 PCM)
  3. 解析 data 块(音频数据)

    • data 块(子块 ID 为 "data")包含音频的原始 PCM 数据:
      • 数据大小(Data Size)(4 字节),表示音频数据的总字节数。
      • 音频数据 (Data),实际的 PCM 采样值,格式取决于 fmt 块中定义的 Bits per Sample。

加载 WAV 文件的实现思路

  1. 读取整个文件并确认 RIFF 头是否正确 :
    • 解析前 12 个字节,检查是否以 "RIFF" 开头,是否包含 "WAVE" 标识。
  2. 查找 fmt 块并解析音频格式信息 :
    • 提取音频格式、通道数、采样率、位深度等信息。
  3. 查找 data 块并读取音频数据 :
    • 提取数据大小,并存储音频 PCM 数据。
  4. 将解析的音频数据存入 loaded sound 结构 :
    • 存储采样率、通道数、样本数及数据指针,以便后续音频播放。

后续优化

黑板:"IFF" 文件格式

块状文件格式(Chunked File Format) 是一种层次化数据编码方式,广泛用于早期计算机文件格式中。例如,IFF(Interchange File Format) 由 Electronic Arts 在 Amiga 计算机 上引入,是最早采用此方法的格式之一。

块状文件格式的基本结构

  1. 数据按块(Chunk)存储:

    • 每个块都有一个 ID(通常是 4 个 ASCII 字符)。
    • 之后是 大小字段,表示该块的数据长度。
    • 紧随其后的是实际的 数据内容。
  2. 块的解析方式:

    • 读取 ID 确定块的类型。
    • 读取 大小字段,确定数据长度。
    • 如果程序支持该块类型,则解析其数据;否则,跳过该块。
    • 部分块可以包含 子块(Sub-chunk),形成层次结构。

WAV 文件的块结构

WAV 文件采用 RIFF(Resource Interchange File Format),其结构与 IFF 类似:

  • RIFF 块(主块)
    • 包含 WAVE 标识
    • 内部包含多个子块:
      • fmt 块(存储音频格式)
      • data 块(存储 PCM 音频数据)
      • 其他可选块(如 LIST、fact 等)

解析 WAV 文件的流程

  1. 读取 RIFF 头部,确认文件类型是否为 WAVE。
  2. 遍历文件中的块:
    • 识别 fmt 块,提取音频格式信息(采样率、通道数、位深度等)。
    • 识别 data 块,读取 PCM 数据。
    • 遇到未知块则跳过,确保兼容扩展格式。

优缺点分析

优点
  • 结构清晰,可扩展(可添加新块类型而不影响旧软件)。
  • 易于解析,支持跳过未知数据块。
  • 适用于存储层次化数据。
缺点
  • 额外的 ID 和 大小字段 增加了文件体积。
  • 解析逻辑较复杂,需处理不同块类型。
  • 现代格式(如 JSON、Protocol Buffers)提供更高效的替代方案。

总结

块状文件格式是一种早期的数据存储方法,在 WAV、AVI、3DS 等格式中广泛应用。尽管 结构灵活 ,但在现代开发中通常使用 更紧凑、高效 的数据格式来代替。

game_asset.cpp:引入 WAVE_header

在解析 WAV 文件时,我们主要关注 文件头(Header) 和 数据块(Chunk) 的基本结构。由于这是调试加载器(Debug Loader),我们不需要支持所有 WAV 格式,只需解析出基本的 PCM 音频数据,以便后续处理资产。

WAV 文件基本结构

WAV 文件使用 RIFF(Resource Interchange File Format) 作为容器格式,其结构包括多个 块(Chunk):

  1. RIFF 头部(文件标识)
  2. fmt 块(音频格式)
  3. data 块(PCM 数据)
  4. 其他可选块(如 LIST, fact 等)

解析 WAV 文件的初始步骤

1. 读取 WAV 头部

  • WAV 文件的前 12 字节 定义为 RIFF 头部 :
    • 4 字节 :"RIFF"(ASCII 编码)
    • 4 字节:整个文件的大小(不包括前 8 字节)
    • 4 字节 :"WAVE"(ASCII 编码)

2. 读取 fmt 块(音频格式信息)

  • 4 字节 :块 ID,通常为 "fmt "(包括空格)
  • 4 字节 :块大小(通常为 16,表示 PCM 格式)
  • 2 字节 :音频编码格式(1 表示 PCM)
  • 2 字节 :通道数(1 = 单声道,2 = 立体声)
  • 4 字节 :采样率(如 44100 Hz)
  • 4 字节:字节率(= 采样率 × 通道数 × 每个样本的字节数)
  • 2 字节:块对齐(= 通道数 × 每个样本的字节数)
  • 2 字节 :每个样本的位数(如 16 表示 16 位 PCM)

3. 读取 data 块(PCM 音频数据)

  • 4 字节 :块 ID,通常为 "data"
  • 4 字节:数据大小
  • 接下来的字节:音频 PCM 数据

代码解析 WAV 头部

我们可以用 C 语言的 结构体(struct) 来定义 WAV 文件头:

c 复制代码
typedef struct {
    uint32_t riff_id;   // "RIFF"
    uint32_t file_size; // 文件总大小 - 8 字节
    uint32_t wave_id;   // "WAVE"
} WaveHeader;

然后是 fmt 块:

c 复制代码
typedef struct {
    uint32_t fmt_id;        // "fmt "
    uint32_t fmt_size;      // 块大小
    uint16_t audio_format;  // 编码格式(1 = PCM)
    uint16_t num_channels;  // 通道数(1 = 单声道,2 = 立体声)
    uint32_t sample_rate;   // 采样率
    uint32_t byte_rate;     // 字节率
    uint16_t block_align;   // 块对齐
    uint16_t bits_per_sample; // 采样位数
} FmtChunk;

最后是 data 块:

c 复制代码
typedef struct {
    uint32_t data_id;   // "data"
    uint32_t data_size; // PCM 数据大小
} DataChunk;

调试加载 WAV 文件

在调试加载器中,我们可以按照以下步骤解析 WAV 文件:

  1. 读取 WaveHeader,确保是 "RIFF" 和 "WAVE" 文件。
  2. 查找 "fmt " 块,解析音频格式信息。
  3. 查找 "data" 块,提取 PCM 数据。
  4. 如果遇到未知块,使用 chunk_size 跳过该块。
  5. 将音频数据存入 LoadedSound 结构体,供后续播放。

总结

  • WAV 文件采用 块状结构 ,解析时按 ID + 大小 + 数据 方式读取。
  • 只需解析 "RIFF" , "WAVE" , "fmt " , "data" 块,即可提取 PCM 数据。
  • 解析时可跳过 未知块,确保兼容不同 WAV 变种格式。
  • 由于本次目标是 调试加载器,不需要支持所有 WAV 格式,只需确保能加载调试所需的 WAV 资源。

引入 WAVE_fmt

在解析 WAV 文件时,fmt 块是关键部分之一,它存储了音频格式信息。我们需要解析其中的多个字段,以确保能够正确读取 PCM 数据。以下是详细解析步骤:


解析 fmt 块(Format Chunk)

fmt 块用于定义音频数据的格式,包括采样率、通道数、比特深度等。其基本结构如下:

偏移量 大小 (字节) 字段名称 说明
0 4 fmt (块 ID) 该块的标识符,固定为 "fmt "
4 4 chunk_size 该块的大小(不包括前 8 字节)
8 2 wFormatTag 音频格式(1 表示 PCM)
10 2 nChannels 声道数(1 = 单声道,2 = 立体声)
12 4 nSamplesPerSec 采样率(Hz)
16 4 nAvgBytesPerSec 平均字节率(= 采样率 × 通道数 × 每样本字节数)
20 2 nBlockAlign 块对齐大小(= 通道数 × 每样本字节数)
22 2 wBitsPerSample 每个样本的比特数(如 16 表示 16 位 PCM)
24 2 cbSize(可选) 扩展数据的大小(仅对非 PCM 格式有效)
26+ 可变 扩展数据 例如 dwChannelMask 和 SubFormat GUID

字段解析

  1. wFormatTag(音频格式)

    • 该字段决定了 WAV 文件的编码方式:
      • 1 = PCM(标准无压缩格式,最常见)
      • 其他值(如 3 = IEEE 浮点格式、6 = A-law、7 = μ-law)不作处理
    • 由于我们只支持 PCM,因此遇到 wFormatTag != 1 的 WAV 文件可以直接忽略。
  2. nChannels(通道数)

    • 1 表示单声道
    • 2 表示立体声
    • 其他通道数(如 5.1、7.1)不在调试加载器的支持范围内。
  3. nSamplesPerSec(采样率)

    • 例如:
      • 44100 Hz(CD 音质)
      • 48000 Hz(专业音频)
      • 96000 Hz(高分辨率音频)
  4. nAvgBytesPerSec(字节率)

    • 计算方式:

      复制代码
      nAvgBytesPerSec = nSamplesPerSec * nChannels * (wBitsPerSample / 8)
    • 该字段可以用来验证数据完整性,但不直接影响 PCM 解析。

  5. nBlockAlign(块对齐)

    • 计算方式:

      复制代码
      nBlockAlign = nChannels * (wBitsPerSample / 8)
    • 该字段决定了每个采样帧的大小,例如:

      • 16-bit 立体声: nBlockAlign = 2 × (16 / 8) = 4
      • 8-bit 单声道: nBlockAlign = 1 × (8 / 8) = 1
    • 解析 PCM 数据时,每次读取 nBlockAlign 个字节,即可得到完整的采样帧。

  6. wBitsPerSample(采样位数)

    • 常见值:
      • 8 = 每个样本 8 位(无符号)
      • 16 = 每个样本 16 位(有符号)
      • 24 或 32 = 高精度音频
    • 仅支持 16 位 PCM,因为它是最广泛使用的格式。
  7. cbSize(扩展数据大小,可选)

    • 仅当 wFormatTag 不是 1 时才存在,如 cbSize = 22 可能用于 IEEE 浮点格式。
    • 由于我们只支持 PCM,因此可以忽略该字段。
  8. dwChannelMask(通道掩码,可选)

    • 该字段用于多通道音频,如 5.1 或 7.1 声道,但调试加载器无需解析。
  9. SubFormat GUID(可选)

    • 该字段用于非 PCM 格式(如 WAVE_FORMAT_EXTENSIBLE),但对于 PCM,我们可以直接跳过。

WAV 解析结构体

我们可以用 C 语言定义 fmt 块的数据结构:

c 复制代码
typedef struct {
    uint32_t fmt_id;         // "fmt "
    uint32_t fmt_size;       // fmt 块大小
    uint16_t wFormatTag;     // 音频格式(1 = PCM)
    uint16_t nChannels;      // 通道数
    uint32_t nSamplesPerSec; // 采样率(Hz)
    uint32_t nAvgBytesPerSec;// 字节率
    uint16_t nBlockAlign;    // 块对齐
    uint16_t wBitsPerSample; // 采样位数
    // 如果需要支持 WAVE_FORMAT_EXTENSIBLE,可在此扩展结构体
} FmtChunk;

解析 WAV 文件的 fmt 块

在调试加载 WAV 文件时,我们按照以下步骤解析 fmt 块:

  1. 读取 fmt 头部(前 8 字节),确保 ID 为 "fmt "。
  2. 读取 fmt_size,确定该块的大小。
  3. 解析 PCM 相关字段,如 nChannels、nSamplesPerSec、wBitsPerSample。
  4. 若 wFormatTag != 1,则跳过该块,不解析。
  5. 跳过可能的扩展字段,直到找到 data 块。

总结

  • fmt 块是 WAV 文件解析的核心部分,决定了音频数据格式。
  • 只支持 PCM(wFormatTag = 1),忽略 cbSize 及扩展字段。
  • 关键字段包括 nChannels(通道数)、nSamplesPerSec(采样率)、wBitsPerSample(采样位数)。
  • 解析时需检查 nBlockAlign 和 nAvgBytesPerSec,确保数据正确性。
  • 如果遇到非 PCM 格式或不支持的位深度,直接跳过该文件。

接下来,我们可以继续解析 data 块,以获取实际的 PCM 音频数据。

引入 WAVE_chunk

在解析 WAV 文件时,我们还需要处理 WAV 文件中的其他数据块(chunk)。WAV 文件采用 分块(chunked)格式 ,其中每个数据块都包含 块 ID 和 块大小。我们需要解析这些数据块,以正确地读取 WAV 文件的结构。


WAV 文件中的通用块格式

WAV 文件的结构基于 RIFF(Resource Interchange File Format),每个块的基本格式如下:

偏移量 大小 (字节) 字段名称 说明
0 4 ChunkID 数据块的 ID(例如 "fmt "、"data" 等)
4 4 ChunkSize 该块的数据大小(不包括 ChunkID 和 ChunkSize)
8 ChunkSize 数据 该块的具体内容

WAV 文件主要包含的块

一个标准的 WAV 文件通常包括以下几个关键块:

1. RIFF 头部

  • ChunkID :"RIFF"(4 字节)
  • ChunkSize:文件的总大小(4 字节)
  • Format :"WAVE"(4 字节)

该部分定义了整个文件的类型,WAV 文件必须以 "RIFF" 开头,并且其格式必须是 "WAVE"。


2. fmt 块(格式块)

  • ChunkID :"fmt "(4 字节)
  • ChunkSize :格式块大小(通常为 16 或 18 字节,PCM 格式时为 16)
  • 数据内容 :
    • wFormatTag(音频格式,1 = PCM)
    • nChannels(通道数)
    • nSamplesPerSec(采样率)
    • nAvgBytesPerSec(平均字节率)
    • nBlockAlign(块对齐大小)
    • wBitsPerSample(每个样本的位数)

3. data 块(音频数据块)

  • ChunkID :"data"(4 字节)
  • ChunkSize:音频数据的大小(4 字节)
  • 数据内容:实际的 PCM 音频数据

这是 WAV 文件中最重要的数据块,它包含了音频的 PCM 采样数据。我们需要找到 data 块,并解析其中的音频数据。


解析 WAV 文件的通用方法

为了正确解析 WAV 文件,我们需要按照以下步骤处理数据块:

  1. 读取文件头部:

    • 确保 ChunkID 是 "RIFF"。
    • 读取 ChunkSize(可忽略)。
    • 确保 Format 是 "WAVE"。
  2. 遍历文件中的数据块:

    • 读取 ChunkID 和 ChunkSize。
    • 根据 ChunkID 进行不同的解析:
      • 如果 ChunkID 是 "fmt ",则解析格式信息。
      • 如果 ChunkID 是 "data",则读取音频数据。
      • 如果 ChunkID 是其他未知值 ,则跳过该块(通过 ChunkSize 跳过相应字节)。
  3. 读取 data 块的数据:

    • 解析 PCM 采样数据(支持 8 位或 16 位 PCM)。

示例代码

我们可以用 C 语言实现 WAV 解析的基本逻辑:

c 复制代码
typedef struct {
    uint32_t ChunkID;   // "RIFF"
    uint32_t ChunkSize; // 文件总大小
    uint32_t Format;    // "WAVE"
} RIFFHeader;

typedef struct {
    uint32_t ChunkID;   // "fmt "
    uint32_t ChunkSize; // 块大小
    uint16_t wFormatTag;
    uint16_t nChannels;
    uint32_t nSamplesPerSec;
    uint32_t nAvgBytesPerSec;
    uint16_t nBlockAlign;
    uint16_t wBitsPerSample;
} FmtChunk;

typedef struct {
    uint32_t ChunkID;   // "data"
    uint32_t ChunkSize; // 音频数据大小
} DataChunk;

然后,我们遍历 WAV 文件的块:

c 复制代码
FILE *file = fopen("example.wav", "rb");
RIFFHeader riff;
fread(&riff, sizeof(RIFFHeader), 1, file);

// 确保是 WAV 文件
if (riff.ChunkID != 0x46464952 || riff.Format != 0x45564157) {
    printf("错误:文件不是 WAV 格式!\n");
    fclose(file);
    return;
}

// 遍历数据块
while (!feof(file)) {
    uint32_t chunkID;
    uint32_t chunkSize;
    
    fread(&chunkID, sizeof(uint32_t), 1, file);
    fread(&chunkSize, sizeof(uint32_t), 1, file);

    if (chunkID == 0x20746D66) {  // "fmt "
        FmtChunk fmt;
        fread(&fmt, sizeof(FmtChunk), 1, file);
        printf("音频格式: %d, 采样率: %d Hz, 通道数: %d\n",
               fmt.wFormatTag, fmt.nSamplesPerSec, fmt.nChannels);
    } 
    else if (chunkID == 0x61746164) {  // "data"
        printf("找到数据块,大小: %d 字节\n", chunkSize);
        break;  // 读取音频数据
    } 
    else {
        fseek(file, chunkSize, SEEK_CUR);  // 跳过未知块
    }
}

fclose(file);

总结

  1. WAV 文件由多个块组成,每个块都有 ChunkID 和 ChunkSize。
  2. 关键块包括 RIFF 头部、fmt 格式块、data 音频数据块。
  3. 解析 WAV 文件时,我们需要遍历所有块,并根据 ChunkID 进行不同的处理 :
    • fmt 块:存储音频格式信息(采样率、位深、通道数)。
    • data 块:存储 PCM 音频数据。
    • 未知块:跳过处理。
  4. 只支持 PCM (wFormatTag == 1),遇到其他格式可以忽略。

这样,我们就可以正确解析 WAV 文件,并提取其中的 PCM 音频数据!

#define RIFF_CODE

在解析 WAV 文件时,我们需要能够识别文件中的不同数据块(chunk),而这些数据块是通过 4 字节的 ID 进行标识的,例如 "fmt "(格式块)、"WAVE"(数据块)和 "RIFF"(文件头)。由于 C 语言不允许直接用字符串字面量作为 uint32_t 类型的值,因此我们需要一种方法来将 4 个字符转换成一个 32 位整数,以便进行高效的比对。


目标:将 4 字节字符转换为 32 位整数

为了便于 WAV 解析,我们希望能够定义这些块 ID,例如:

c 复制代码
enum {
    WAVE_ChunkID_fmt = RIFF_CODE('f', 'm', 't', ' '),
    WAVE_ChunkID_RIFF = RIFF_CODE('R', 'I', 'F', 'F'),
    WAVE_ChunkID_WAVE = RIFF_CODE('W', 'A', 'V', 'E'),
};

然而,C 语言并不支持直接写 'fmt ' 这样的多字符字面量并让它变成 uint32_t 类型,因此我们需要定义一个 宏 来自动转换字符串。


实现思路

由于 WAV 文件使用 小端字节序(Little Endian) 存储数据,我们需要确保字符的存储顺序正确。例如:

  • "RIFF" 在内存中的存储可能是 0x46464952(即 R I F F)。
  • "fmt " 在内存中的存储可能是 0x20746D66(即 f m t )。

为了确保正确的字节顺序,我们可以定义一个 宏 来将 4 个字符转换为 uint32_t 值,确保它们按 小端字节序 排列。


宏实现

我们定义一个宏 RIFF_CODE(),用于将 4 个字符转换为 32 位整数:

c 复制代码
#define RIFF_CODE(a, b, c, d) \
    ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))

这个宏的工作原理如下:

  • a 是最低字节(最右边),不需要位移。
  • b 左移 8 位,占据第二个字节。
  • c 左移 16 位,占据第三个字节。
  • d 左移 24 位,占据最高字节。

使用示例

通过这个宏,我们可以定义 WAV 解析所需的块 ID:

c 复制代码
const uint32_t WAVE_CHUNK_ID_FMT  = RIFF_CODE('f', 'm', 't', ' ');
const uint32_t WAVE_CHUNK_ID_RIFF = RIFF_CODE('R', 'I', 'F', 'F');
const uint32_t WAVE_CHUNK_ID_WAVE = RIFF_CODE('W', 'A', 'V', 'E');

这样,我们就可以用这些值与 WAV 文件中的 ChunkID 进行比对,确保正确解析数据。


如何验证字节顺序

由于 小端存储 可能导致字符顺序错误,我们可以通过读取实际 WAV 文件并打印 ChunkID 来验证:

c 复制代码
uint32_t chunk_id;
fread(&chunk_id, sizeof(uint32_t), 1, file);
printf("Chunk ID: 0x%X\n", chunk_id);

如果打印出的 Chunk ID 和 RIFF_CODE() 计算出的值不匹配,则可能需要调整 字节顺序 (例如更改 RIFF_CODE() 中的移位顺序)。


总结

  1. WAV 文件使用 4 字节的 ChunkID 标识数据块,例如 "fmt "、"data" 和 "RIFF"。
  2. 由于 C 语言不支持直接使用字符串字面量作为 uint32_t 值,我们定义了 RIFF_CODE() 宏,用来将 4 个字符转换成 32 位整数。
  3. RIFF_CODE() 确保字符按小端字节序排列,符合 WAV 文件格式要求。
  4. 通过 RIFF_CODE() 生成的 ChunkID 可以用于比对文件中的数据块,确保 WAV 解析正确。

这使得 WAV 解析代码更加清晰易读,并且方便扩展其他块类型的处理!

解析 WAVE_header 的 ID

在解析 WAV 文件时,我们首先需要解析 WAV 头部(Wave Header)。我们期望 WAV 头部包含 RIFF 块,并且 RIFF 块的 ID 字段应包含 "RIFF",同时 WAVE ID 也应该存在。如果这两个条件都满足,那么文件的基本结构就是符合预期的,我们可以继续解析其中的具体数据块(chunks)。


Interchange(立体交叉道, 互换 vt. 交换, 互换 vi. 交替发生)

解析 WAV 头部

  1. WAV 头部的主要结构

    • RIFF 块(Resource Interchange File Format):
      • 4 字节的 Chunk ID (必须是 "RIFF")。
      • 4 字节的 文件大小。
      • 4 字节的 格式 (必须是 "WAVE")。
    • 只有在 Chunk ID 是 "RIFF" 且 Format 是 "WAVE" 时,我们才会继续解析文件。
  2. 数据结构示例

    c 复制代码
    typedef struct {
        uint32_t ChunkID;   // 必须是 "RIFF"
        uint32_t ChunkSize; // 整个文件大小 - 8
        uint32_t Format;    // 必须是 "WAVE"
    } WaveHeader;

使用断言(assert)验证 WAV 文件

由于这只是用于调试 的 WAV 解析器,而不是最终的资产加载器,因此我们可以使用 assert 来验证 WAV 文件是否符合基本格式:

c 复制代码
   WaveHeader* header = (WaveHeader*)fileData;
   // 断言检查是否是有效的 WAV 头部
   Assert(Header->RIFFID == WAVE_ChunkID_RIFF);
   Assert(Header->WAVEID == WAVE_ChunkID_WAVE);

这里使用 RIFF_CODE() 宏(之前定义过)将 4 字节字符串转换为 uint32_t,确保 ChunkID 和 Format 是 "RIFF" 和 "WAVE",否则程序会在调试阶段崩溃,提醒开发者该文件格式错误。

到这里,稍微展望一下未来

目前正在进行 WAV 文件加载的编写,预计明天能够实现完整的 WAV 文件解析。这不仅有助于完成资产加载的最后阶段,同时也能推进音频部分的开发。这样一来,整体的开发任务会减少,使进度更加顺利。

开发目标

  • 计划在第 200 天左右开始编写正式的游戏逻辑代码,因此希望在此之前完成所有核心引擎工作。
  • 目前正在清理待办事项,希望尽快完成所有和游戏无关的引擎开发工作,使后续能够专注于游戏代码的编写。

当前进度

  • 渲染部分已经基本完成,但仍有两个主要任务需要解决:

    1. 调试工具(Debug Code):这部分相对简单,应该不会花费太多时间。
    2. 字体渲染(Fonts):这需要较多的工作量,仍然是一个待办事项。
  • WAV 文件解析即将完成:

    • 目前已经建立了对 RIFF 结构的解析框架。
    • 计划明天完成具体的数据加载,以便 WAV 文件能够被正确解析和使用。
    • 这一部分完成后,音频子系统的主要功能也将基本完善。

后续规划

  • 继续推进 资产加载和音频系统,确保它们可以稳定运行。
  • 完成调试工具和字体渲染,进一步减少待办事项。
  • 优化代码,减少引擎层面的开发工作,以便在第 200 天左右切换到游戏逻辑的开发。

目前整体进度良好,正在有条不紊地完成各项核心引擎任务,为正式的游戏开发做最后的准备。

对于获取角色精灵,能不能只做以下操作? int sprite_to_load = (angle + (180 / sprites)) / (360 / sprites) % sprites

当前讨论的是角色精灵(sprite)的加载和角度映射问题。有人提出了一种计算方式:

cpp 复制代码
int sprite_to_load = (angle + 180) / (sprites / 360) % sprites;

但这种方式存在多个问题,不能直接适用于当前的资产系统。

主要问题

  1. 缺乏角度分布的信息

    • 当前的资产系统并没有存储精灵在特定角度上的数量信息。
    • 角度的分布可能并不均匀,例如某些角度的精灵较多,而另一些角度的精灵较少。
    • 例如,一个 Boss 角色 可能大部分时间是面向前方的,因此前方视角的精灵较多,而背后视角的精灵较少。
  2. 现有系统无需量化(Quantization)

    • 当前的实现方式已经能够正确处理不同角度的精灵加载,而不需要进行额外的量化计算。
    • 直接量化角度可能会导致问题,例如 某些角度下根本没有可用的精灵,导致错误的映射或加载失败。

结论

  • 直接使用这种计算方式不可行,原因是它假设了 精灵角度均匀分布,但实际情况可能并非如此。
  • 现有系统已经能够正确加载精灵,并适应 不均匀的角度分布,因此不需要进行额外的量化计算。

我有一个关于音频资产结构的联合问题。它需要 SoundLoopable,或者 LoopStart / LoopEnd 吗?(LoopEnd == -1 表示一次性播放的声音)

关于音频资产结构体的问题,是否需要添加如 循环起始点 、循环结束点 以及将 循环结束点设置为 -1 以表示单次播放的声音,目前还不确定。虽然这种功能可能会需要,但由于音频系统还没有完全构建好,因此目前还不清楚具体需求。

大概的做法是,先开发混音器(mixer),然后根据混音器的需求,回过头来调整音频资产系统,加入所需的功能和字段。

实现其他音频格式如 MP3 的计划?

关于实现其他音频格式,如 MP3,无法实现,因为 MP3 格式有专利,受到法律保护,不允许实现。即使在 2016 年之后专利可能会过期,但在当前仍然不允许实施 MP3 解码。美国的专利系统特别复杂和限制,尤其是软件专利,给开发带来很多麻烦。

因此,虽然理论上可以讲解如何实现 MP3 解码,但实际的代码实现是违法的。如果源代码被发布并包含 MP3 播放功能,那将是违法的行为。虽然通过直播或说明可能不违法,但还是不鼓励这样做。对于其他格式,如 FLAC,虽然可以用于压缩,但由于其压缩率较低(通常不到两倍),对空间的节省效果并不显著,使用起来的意义不大。对于真正需要压缩音频的场景,OGG Vorbis 可能是一个更合理的选择,但这类格式实现起来复杂,且消耗的时间和资源较多,因此不太可能在当前实现。

MP3 到底怎么被专利化的?

MP3 格式受到了多个专利的保护,可以在互联网上找到有关 MP3 专利的详细信息,包括专利的持有者和专利过期的时间。根据这些资料,MP3 的专利并非在全球范围内统一到期。在一些地区,比如美国,MP3 专利仍然有效,而在其他地方,可能早在 2012 年就已经过期。因此,在某些地区,可能是合法的来使用 MP3 格式的相关代码,但在美国,使用 MP3 解码或编码相关的技术仍然是违法的。

根据不同的专利信息,有些专利可能在 2015 年或 2017 年才会过期,具体取决于不同的专利和地区的法律规定。因此,尽管在一些地区 MP3 可能不再受专利保护,但在美国,专利依然存在并限制相关技术的使用。

是的,但为什么有人会专利文件扩展名呢?

MP3 文件的专利并不是针对文件扩展名,而是针对创建音频、以及编码和解码音频所使用的技术。因此,仅仅解析 MP3 文件的内容就可能会违反这些专利,因为专利保护的就是这些解析技术。虽然这种做法令人难以理解,但遗憾的是,现行的法律要求遵守这些专利,因此在技术上,我们不能解析 MP3 文件。

FLAC 是 WAV 的 50% 大小,所以它对无损压缩非常有用

FLAC(无损压缩)并不会像很多人认为的那样将文件压缩到原始 WAV 文件的 50% 大小,实际上最好的情况下,压缩率大约是 60%。在许多情况下,FLAC 的压缩效果要比这个更差,甚至接近于无压缩。因此,若只是为了节省一些存储空间,使用 FLAC 并不值得,因为它无法提供显著的节省。

假设一个游戏的总大小为 2GB,其中音频占据 500MB,如果通过 FLAC 压缩能够节省大约 25%的空间,那么最终仅节省 100MB。相对于整个游戏文件大小,这种节省是微不足道的。再加上为了实现这种压缩,游戏代码将变得更加复杂,这样的压缩就没有太大的意义。

如果真需要大幅压缩,应该考虑使用像 OGG Vorbis 这样的格式,它能够提供 8:1 或更高的压缩比,这样的压缩效果才更有实际意义。因此,如果目标是有效地减小音频文件的体积,选择 FLAC 并不会带来太多好处,反而可能增加不必要的复杂性。
https://xiph.org/flac/

https://z-issue.com/wp/flac-compression-level-comparison/

所以这就是 RAD 为什么不专利他们的东西的原因吗?

某些技术并未申请专利,是因为认为这类技术的专利制度非常愚蠢,因此选择不进行专利申请。

相关推荐
回眸&啤酒鸭21 分钟前
【回眸】私人定制旅游路线助手
人工智能
用户3600555790021 分钟前
Day 1·3 确定性测试门:推理引擎的PASS/FAIL自检机制
人工智能
我命由我1234523 分钟前
Photoshop - 将Photoshop图像置入其他应用程序
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
知几蜗牛28 分钟前
HydraFusion的Single、Cascade与Critique如何落到工程门禁
人工智能
mit6.82428 分钟前
plz直接提交 pull equest
人工智能
Solara30 分钟前
29 条回复永远没送到:翻完 108 条投递台账,我才发现「微信限流」是我取错的名字
人工智能·agent·ai编程
lucas_AI31 分钟前
别再无脑堆数据了:腾讯 WeVisDoc 把文档解析卷到 95 分,token 是按预算花的
人工智能
橘和柠31 分钟前
一台笔记本上的三国杀:eNSP、VirtualBox 与 Docker,我让它们共存了
人工智能
个 人 练 习 生32 分钟前
C++-----string基本使用详解
开发语言·c++·学习
achong36 分钟前
AI 写代码比你还啰嗦?用 ponytail「懒人法则」治它
人工智能