欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > 游戏引擎学习第141天

游戏引擎学习第141天

2025/3/9 9:03:52 来源:https://blog.csdn.net/TM1695648164/article/details/146093755  浏览:    关键词:游戏引擎学习第141天

回顾上期内容并介绍今天的议程

我们之前做了一些音频混音的测试,目前的实现方式是每次播放一个新的声音时,都会完整加载整个音频文件。这导致当多个音频同时播放时,可能会出现混乱的声音效果。之前这样做是为了演示是否可以多次播放相同的音频,事实证明是可以的。现在我们希望优化这一部分,使其更合理,不再产生过于嘈杂的效果,比如改回播放一个简单的音效(如 “Asset_Bloop”)。

接下来,我们有几个方向可以研究,涉及音频混音的进一步改进:

  1. 支持流式音频
    目前的音频加载方式是一次性将整个文件载入内存,如果音频文件较大(如 30MB 的音乐文件),会占用较多的内存和加载时间。我们可以考虑将音频拆分成多个小块,按需加载,从而减少内存和 I/O 负担。

  2. 增强混音功能
    目前混音器的音量设置是固定的,想要增加渐变音量(如淡入淡出)或声像移动(如左右声道平移),需要支持音量随时间变化的插值。这将提升音频的动态效果,使其更加自然。

  3. 优化混音器的 SIMD 计算
    计划将混音器的计算优化为 SIMD(如 SSE),以提升性能,确保混音过程的高效性。

从逻辑上来看,首先要解决的是流式音频,因为它会影响整个音频加载和处理的方式,而渐变音量和 SIMD 优化主要是在现有框架下的优化,不会影响音频数据的加载流程。因此,我们的第一步是思考如何实现流式音频加载,并确保它能够与现有的资源管理系统兼容。

接下来,我们开始具体分析和设计流式音频的实现方案。

头脑风暴如何分块加载大型音频文件

我们已经设置了资产系统,其中包含各种资源。当加载一个音频文件时,它会被纳入资产流系统进行管理。这个音频文件实际上来源于一个资产槽(asset_slot),而该资产槽由一个资产信息(asset_sound_info)支持。当前,资产信息仅包含文件名,因为目前还没有专门的资产文件格式,我们只是直接从 Windows 的原始目录中加载 WAV 文件。

未来,资产信息可能会包含更多数据,例如该音频在打包文件中的偏移量、是否经过压缩以及其他必要的元数据。这些信息最终都会存储在资产信息结构中。

由于资产系统已经建立,并且加载的音频文件已经成为资产流系统的一部分,我们可以考虑如何优化音频数据的加载方式。一个可能的方案是,在资产信息中添加一个字段,指示该音频文件由多个数据块(chunks)组成,并允许这些块独立加载。这似乎是一个合理的优化方向。

然而,这种方法与当前资产流系统的设计理念存在冲突。目前,整个资产流系统基于“资产槽”来管理加载状态,资产槽是控制资源是否被加载的核心单位。如果引入分块加载机制,就意味着要在资产槽的层面上扩展管理逻辑,而这可能会引发复杂性增加的问题。

理想情况下,我们希望在不改变现有系统架构的前提下,找到更好的方式来支持音频的分块加载。考虑到资产标签(asset_tag)已经存在,我们可以利用标签来管理音频块的加载。例如,我们可以为音频块分配一个标签 ID,例如 TAG_SOUND_SECTION,然后使用不同的数值(0、1、2、3、4……)来表示音频的不同部分。

如果采用这种方法,在资产信息中只需存储一个字段,指示该音频的最大标签值。这样,我们可以通过标签自然地管理音频块的加载,而无需修改资产槽的基本逻辑。

然而,这种方法也会带来一定的后果。由于它依赖于标签系统,音频块将与其他资产的标签系统混合管理。如果不加区分,可能会影响其他资产的匹配和加载。虽然可以通过权重机制(weighting)来调整优先级,但仍然需要谨慎处理。

另外,资产系统还包含音乐(asset music)管理,通常会通过标签来匹配特定类型的音乐。当请求播放某种音乐时,系统会根据标签返回匹配的音乐 ID。然而,如果音频块采用标签系统进行管理,当前的匹配机制可能无法直接支持音频块的顺序加载。

例如,音频播放系统通常通过音频 ID(sound ID)获取音频数据。如果音频块基于标签进行管理,系统可能难以确定下一个应播放的音频 ID。因此,我们需要考虑如何在现有架构的基础上,提供一种能够顺序访问音频块的机制。

或许可以采用另一种更简单的方式来解决这个问题,例如在音频 ID 体系中直接支持下一个音频块的索引,而不是依赖标签系统。这种方式可能更容易实现,并且不会影响现有的资产管理逻辑。

实现音频的链式播放

如果我们已经加载了一个音频,那么可以引入音频链式播放的概念。例如,我们可以在音频数据中存储一个“下一个要播放音频ID”(NextIDToPlay),这样当当前音频播放完毕时,就会自动播放下一个音频,实现无缝衔接,同时保持流式加载的自然性。

这种方法的优势在于,我们只需要从标签匹配系统中获取一个音频 ID,之后播放系统就可以根据存储的信息自动管理音频的衔接播放,而无需额外的逻辑处理。如果这个“NextIDToPlay”被设置为 0,就意味着不需要继续播放任何音频。此外,如果“NextIDToPlay”指向自身,就可以实现循环播放,使音频无限循环播放下去。虽然尚不确定是否需要利用这个特性,但它确实是一个可行的方案。

如果采用这种方案,在资产音频信息(asset_sound_info)结构中,我们只需要增加一个字段用于存储“NextIDToPlay”。当音频被加载时,就可以直接从该结构中获取这个信息。然而,由于音频ID 和资产槽(asset slot)是一一对应的,我们实际上并不需要显式存储“NextIDToPlay”,而是可以通过已有的资产槽索引体系直接推导出下一个音频的ID。

在具体实现时,我们首先需要确保在播放一个音频时,系统可以立即预加载它的下一个音频。假设我们成功加载了某个音频,并获取到了该音频的资产信息,那么就可以从资产信息中提取“NextIDToPlay”,并将其加入加载队列,确保其在需要播放时已经被加载。

此外,现有系统已经确保了对于无效的加载请求(例如 ID 为 0 的情况)会被安全忽略,因此我们可以无条件请求加载“NextIDToPlay”而不会引发错误。这样,我们能够保证当音频播放到末尾时,下一个音频已经准备就绪,可以无缝衔接播放。

在混音过程中,当我们检测到当前音频的样本已经全部混合完毕时,原本的逻辑可能会直接终止该音频的播放并将其移除。而在新的方案下,我们希望在此时自动切换到“NextIDToPlay”对应的音频,而不是简单地结束播放。为了实现这一点,我们可以先编写一个基础版本,确保音频切换逻辑正确运行,哪怕在最初实现时可能会导致轻微的跳帧或间隙,之后再进行优化以确保平滑播放。

在这里插入图片描述

在这里插入图片描述

编写一个简单但可能会跳帧的版本

当音频播放接近结束时,需要考虑如何在缓冲区(buffer)中处理最后的样本,使得播放能够无缝衔接。如果音频数据的长度小于混音缓冲区的宽度,那么在音频数据结束后的部分,系统可能会填充零值,导致播放出现短暂的静音,而在下一帧中才继续播放新的音频数据。为了避免这种情况,需要对播放逻辑进行优化,使得音频能够无缝衔接,而不是出现静音间隙。

为了解决这个问题,首先可以进行一个基础处理:如果当前音频即将播放完毕,而它有“下一个音频ID”(NextIDToPlay),那么就直接切换到这个音频,并从头开始播放它的数据。这是一个简单的实现方式,虽然暂时没有做更高级的平滑衔接处理,但可以保证音频播放的连续性,避免播放停止或出现未填充的数据段。

具体实现上,首先需要检查“NextIDToPlay”是否有效。如果它的值不等于 0,那么说明存在后续音频可以播放,否则当前音频就真正结束了。为了方便检查,可以引入一个辅助函数 IsValid,用于判断一个音频ID是否有效。这个函数只需要检查音频 ID 是否不为 0 即可。

接下来,在播放系统中,每个正在播放的音频都有一个 PlayingSound 结构,其中包含当前播放的音频 ID 和已经播放的样本数。为了切换到下一个音频,只需要做两件事:

  1. 将当前的音频 ID 替换为下一个音频ID NextIDToPlay
  2. 将已播放的样本数 SamplesPlayed 置零,使得新的音频从头开始播放。

这种方式不需要额外的复杂处理,也不会影响现有的音频管理逻辑,只是简单地在播放结束时切换音频 ID,并继续播放下一个音频。虽然当前的实现还比较基础,后续可以进一步优化,例如在两个音频之间进行平滑过渡,确保播放流畅无缝衔接。
在这里插入图片描述

在这里插入图片描述

讨论加载和预取数据的相关问题

在当前的音频加载系统中,引入了预取(prefetch)机制,以便在音频真正需要播放之前就提前加载,减少播放时的延迟。预取的作用是通知资产系统即将需要某个音频资源,让系统尽早准备,而不是等到真正请求时才去加载。

为了实现这一点,添加了 PrefetchSound 这样的接口,类似于 LoadSound,但不同的是它并不会立即请求加载,而只是进行预取标记。目前,这些方法只是简单地透传调用,没有实际的预取优化逻辑,但未来可能会对预取和即时加载做区分,以优化系统性能。

在实现过程中,调整了 GetSoundInfo 方法,使其能够根据 sound_id 获取对应的音频信息。这里添加了一些检查逻辑,例如确保 sound_id 在合法范围内,即不超过 SoundCount,以避免非法访问。此外,还考虑了 sound_id 可能为零的情况,如果 sound_id 为零,可能需要特殊处理,例如返回 null 或某种无效音频信息。

另外,在 DEBUGAddSoundInfo 函数中,对 NextIDToPlay 进行了初始化,默认设为 0,确保在没有明确后续音频时不会误用无效 ID。这部分逻辑是在 add_sound_info 过程中完成的,保证了每个音频对象的 NextIDToPlay 字段都有一个明确的初始值。

整体来看,这一改动的主要目标是优化音频加载流程,通过预取减少播放时的加载延迟,并增加音频播放的灵活性,同时也引入了更严格的边界检查,以提升系统的稳定性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

扩展 asset_sound_info 以引用文件的特定部分

当前的音频加载系统需要支持部分加载,即能够从音频文件的某个特定位置开始读取特定数量的采样数据,而不是每次都加载整个音频文件。为此,需要在 asset_sound_info 结构体中添加表示数据范围的字段,包括 SampleCount(采样数量)和 FirstSampleIndex(起始采样索引)。这样,每次加载音频时,都可以明确指定要读取的片段,提高灵活性并减少不必要的加载。

在具体实现时,调整了 LoadSoundWork 逻辑,使其在加载音频时可以传入 SampleCountFirstSampleIndex,用于控制读取范围。如果 SampleCount 设为 0,则默认读取整个音频文件,因为不会有实际的音频数据块长度为 0 的情况。

DebugLoadWAV 过程中,增加了参数 FirstSampleIndexSampleCount,并在读取音频数据后,对返回的 loaded_sound 进行调整:

  1. 添加边界检查:确保 FirstSampleIndex + SampleCount 不会超出实际音频数据的范围,避免越界读取。
  2. 修改 SampleCount:将加载的音频数据的 SampleCount 直接设为指定的 SectionSampleCount,截取指定范围的数据。
  3. 调整采样数据:对于每个通道的数据,将采样数据的起始位置向前移动 FirstSampleIndex 个位置,使其对齐到正确的偏移位置,从而确保播放时能够正确渲染该音频片段。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

先验证一下

在处理音频流式加载时,进行了实验性调整,目标是让 AddSoundAsset 支持从音频文件中提取特定时长的片段,并进行播放测试。最初的目标是从音频文件中提取 1 秒钟的音频数据并播放,观察能否正确停止播放。然而,测试结果表明音频仍然在 1 秒后继续播放,说明代码未正确截取音频数据。
在这里插入图片描述

通过调试发现 DebugLoadWAV 并未正确处理音频数据的片段化加载,导致完整音频被读取而非指定片段。问题的根本原因在于没有正确传递 SectionSampleCount,导致音频未被截取。修正后,能够成功提取 1 秒钟的音频数据,并实现预期的播放与停止行为。
在这里插入图片描述

随后,进一步优化 AddSoundAsset 逻辑,使其能够更方便地管理音频数据块。定义了 OneMusicChunk 变量,表示 10 秒钟的音频数据块,并让 AddSoundAsset 自动处理音频片段的索引管理,使得加载多个音频块时可以顺利衔接播放。这样,即使在未来使用打包文件格式(pack file)进行音频管理,也能保持当前的逻辑一致性。
在这里插入图片描述

另外,调整 DebugLoadWAV 逻辑,确保不会超出音频数据范围。如果 SampleCount 超过剩余音频数据量,则进行截断处理,以保证不会读取无效数据。
在这里插入图片描述

完成这些调整后,流式音频播放能够正确运行,并成功读取并播放多个音频块。尽管当前的音频混合(mixer)逻辑仍有待优化,例如修复播放切换时的轻微点击声,但基础的流式加载功能已经可以正常工作。后续的优化方向包括将音频混合逻辑拆分到独立文件,以便更好地管理代码结构,同时优化音频混合算法以改善播放质量。
在这里插入图片描述

实现无缝混音

我们在处理音频混合时,当前的逻辑是如果运行了一个样本,我们会直接停止整个混合过程。但实际上,我们希望能够在当前声音播放完毕后,继续切换到下一个声音并保持混合的进行。为此,我们需要调整当前的逻辑,使其在满足特定条件的情况下保持运行,而不是直接终止。

我们可以考虑使用一个循环,确保混合过程在以下几种情况之一发生时才会终止:

  1. 加载声音失败(意味着没有可播放的声音)。
  2. 当前声音播放完成。
  3. 已经填满了整个缓冲区的样本数。
    在这里插入图片描述

进入循环后,我们首先检查是否需要加载新的声音。如果加载失败,我们直接跳出循环。否则,我们会获取当前声音的信息,预加载可能接下来的声音,并提取必要的数据(如音量和目标缓冲区指针)。

在混合过程中,我们需要计算当前可以混合的样本数,这取决于当前缓冲区的剩余空间和当前声音剩余的样本数。如果当前声音的剩余样本不足以填满缓冲区,我们需要提前停止混合,并记录已经播放的样本数。

我们在每次循环中都会更新:

  • 当前声音已经播放的样本数。
  • 缓冲区中仍需填充的样本数。
    在这里插入图片描述

我们在混合样本后,更新目标缓冲区指针,并检查是否已经播放到当前声音的结尾。如果是,我们需要检查是否有下一个声音可用,并进行切换;否则,混合过程终止。

在循环结束时,我们加入了断言(assert),确保:

  1. 如果声音没有播放完毕,那么应该已经填满了整个缓冲区。
  2. 总体需要混合的样本数不会小于已经混合的样本数。

经过调整后,我们发现之前的某些变量需要在循环之外进行持久化存储,而不是每次循环重新初始化,例如音量数据和目标缓冲区指针。此外,我们发现之前的某些逻辑放置在了错误的位置,例如播放样本数的更新应该发生在混合完成后,而不是之前。修正这些问题后,整体逻辑更加清晰。
在这里插入图片描述

在优化过程中,我们还考虑了如何支持低内存平台。尽管大多数平台有足够的内存一次性加载完整的音频数据,但如果我们将音乐分块加载(例如每 10 秒加载一次),就能更好地适应低内存环境,并支持动态音频切换。因此,我们决定支持这种方式,以便在未来需要时可以灵活适配不同设备。

最终的实现确保了:

  • 音频可以无缝切换,而不会因为当前声音结束就停止混合。
  • 在填满缓冲区之前,我们会继续混合可用的声音。
  • 代码结构更加清晰,避免了之前可能导致问题的逻辑错误。
  • 为未来的低内存支持做好了准备,避免了内存占用过大的问题。
    在这里插入图片描述

将音频相关代码拆分到单独的文件中

我们现在希望将音频混合器拆分到一个独立的文件中,以便更好地管理代码,并为后续的功能扩展做好准备。首先,我们创建了一个新的文件 game_audio.cpp,目前暂时命名为这个,我们还不确定它是否是最合适的命名方式,但先这样处理。

在这个过程中,我们需要将相关的音频处理逻辑从原有文件中抽取出来。GameOutputSound 提取到game_audio.cpp。然后,我们在新的 game_audio.cpp 文件中 #include game_audio.h #include "game.h" 头文件,同时在原文件中也包含了新的音频处理文件。 game_audio.h 包含 #include "game_asset.h"

接下来,我们开始拆分具体的音频处理逻辑:

  1. 提取 playing_sound 结构

    • 我们找到 playing_sound 结构,并将其移动到新的game_audio.h文件中。
  2. 提取音频混合核心逻辑

    • 找到音频混合器的核心函数,比如 OutputPlayingSound ,将其整体迁移到新的音频处理文件game_audio.cpp中。
    • 该函数需要访问 sound buffer(即 game_sound_output_buffer),因此我们确保它作为参数传递。
    • 另外,该函数可能需要访问 游戏资源(game_assets)内存分配区(memory_arena)。初步分析后发现,它主要依赖 *game_assets Assets 以及一个临时内存分配区域(memory_arena TransientArena),因此我们只需要传递这两个参数,而不需要整个 transient_state
    • 由于 game_assets 包含了音频资源,我们确保在调用混合器函数时,将 assets 传递进去。
    • 临时内存分配区域 transient_state 仅用于混合过程中分配临时数据,因此我们也作为参数传递。
  3. 重构 game_state 结构

    • 由于音频混合逻辑依赖 playing_sound,我们在新的文件中定义了 audio_state 结构,该结构包含 FirstPlayingSound 以及 FirstFreePlayingSound 数组。
    • game_state 结构中,我们新增 audio_state,并在 OutputPlayingSound 调用时传递 audio_state,确保所有音频相关数据都被正确管理。
  4. 修改混合器调用逻辑

    • 由于 OutputPlayingSound 现在是一个独立的函数,我们需要在主循环中正确调用它。
    • 在主音频更新逻辑中,我们调用 OutputPlayingSound(audio_state *AudioState, game_sound_output_buffer *SoundBuffer, game_assets *Assets, memory_arena TransientArena) ,确保它能够正常执行音频混合逻辑。
    • 经过测试,音频能够正常播放,说明迁移是成功的。
  5. 提取 PlaySound 函数

    • PlaySound 是一个重要的函数,它用于启动一个新的音频播放,因此我们也希望将其移动到 game_audio.cpp 中,使其归属于 audio_state
    • 目前,我们暂时先将它放入 game_audio.cpp,但后续可能还需要进一步调整它的实现方式,以确保它能够正确处理 audio_state 并与 PlaySound 进行交互。

通过这次重构,我们成功地将音频混合器独立出来,使代码更加模块化,并且便于后续进行优化和功能扩展。这为后续的同步处理、多声道支持等功能奠定了良好的基础。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

抽象化音频内存管理(Audio Arena)

我们希望弄清楚这些音频相关的数据到底是从哪个内存分配区域(arena)分配出来的。目前来看,它们应该不会来自 WorldArena,而是应该与 AudioState 相关。因此,为了让代码保持相对正确,同时避免对内存管理部分进行过多修改,我们可以为音频系统指定一个专用的内存分配区域,用于管理音频数据。

为音频系统指定专用 Arena

  1. 设定一个固定的音频内存区域

    • 我们可以让音频系统使用一个独立的 InitializeAudioState,也就是音频数据的永久存储区域(permanent arena)。
    • 这样,每当音频系统需要分配内存时,它都会使用 AudioState.arena 进行分配,而这个 arena 会在初始化时被指定。
    • 这样做的好处是,在未来如果需要重新调整内存管理方式,我们可以直接修改 AudioState.arena 的分配方式,而不需要改动音频处理的核心逻辑。
  2. 修改音频系统的分配逻辑

    • 任何需要内存分配的地方,都直接使用 AudioState.arena 进行分配,而不是依赖全局的 world arena
    • play_sound 以及 output_playing_sounds 等函数中,我们确保它们使用 AudioState.arena 进行内存管理。
    • 这样可以保证音频相关的数据不会污染其他系统的内存区域,也不会受 world arena 变化的影响。

初始化 Audio State

在系统初始化时,我们需要正确初始化 AudioState

  1. 在游戏初始化阶段,调用 InitializeAudioState,并传入 GameState->WorldArena 作为音频分配的 arena

    InitializeAudioState(&GameState->AudioState, &GameState->WorldArena);
    

    这样,音频系统的所有数据都会从 WorldArena 分配出来,但它们是独立管理的。

  2. game_audio.cpp 中实现 InitializeAudioState

    internal void InitializeAudioState(audio_state *AudioState, memory_arena *Arena) {AudioState->Arena = Arena;AudioState->FirstFreePlayingSound = 0;AudioState->FirstPlayingSound = 0;
    }
    

    这个函数会:

    • 绑定 AudioStateArena
    • FirstFreePlayingSoundFirstPlayingSound 设为 0,确保初始化时音频系统处于清空状态。

调整 play_sound 逻辑

play_sound 函数中,我们需要修改内存分配方式:

  • 原来的实现可能是使用 world_arenatransient_arena 来分配音频数据,现在我们修改PushStruct为 AudioState->Arena,确保它从音频专用 arena 获取内存。

确保音频系统的调用

在游戏主循环中:

  1. 调用 GameGetSoundSamples,确保音频帧数据正确输出。
  2. 这样可以让音频数据保持在自己的管理范围内,而不会影响其他系统。

在这里插入图片描述

在这里插入图片描述

你最喜欢的音乐类型是什么?你认为它会影响最终游戏的氛围吗?

关于音乐风格对游戏氛围的影响,这是一个有趣的问题,但在这个特定的开发过程中,并不会有太大的关联。开发过程中,虽然一般会花时间在艺术性和游戏氛围的定义上,但在这个项目里,由于开发节奏非常快,每晚只有一小时的开发时间,整个过程的性质要求我们更注重自然的推进而非严格的控制。因此,这个游戏的氛围并不完全是通过精心策划的音乐风格来影响的。

游戏的开发非常有机,更多是顺其自然地进行,而不是强行把某种音乐风格或复杂的氛围注入其中。因为这种开发方式限制了我们控制氛围的能力,所以本作更倾向于轻松、随性的感觉,这也是开发过程中采取的方向。如果是其他的游戏项目,可能会更多地关注音效、音乐和氛围的深度构建,但在这种条件下,这些因素只能顺其自然地发展。

因此,这个游戏的最终氛围更多是基于开发方式的自然流动,而不是依赖特定的音乐风格或复杂的情感表达。

为什么在音频混合器中使用临时内存池(temporal arena)而不是普通内存池?

使用临时内存而不是正常的内存池(arena)的原因,是因为这些内存只在函数的一个执行周期内存在。具体来说,这段内存只在当前帧的函数中使用,一旦该函数执行完毕,这些内存就会被释放。这样做的原因可以追溯到C语言和C++的历史。C语言本身设计时并没有考虑到高级内存管理和临时内存分配的需求,因此开发者需要自己管理这些内存。

C语言虽然非常强大,但它的设计和思路已经有些过时,尤其是C++在后续的发展中并没有为这种内存管理的需求提供有效的支持。所以在C和C++中,程序员需要通过其他方式来表示这些临时内存区域,并手动管理它们。临时内存的管理实际上是一项很基础的任务,开发者可以通过使用"内存池"(arena)来管理这些内存,这种方法非常高效且简单。

具体到使用临时内存的例子,比如音频混合操作。在音频处理中,首先会混合浮动点数据到一个临时缓冲区,然后再在最后一步将其转化为16位整数格式的最终缓冲区。这个过程中的临时内存就像程序中的临时变量一样,生命周期仅限于当前操作周期,操作完成后立即释放。而通过使用内存池,程序员可以非常高效地分配和释放这些内存,不需要每次都去管理复杂的内存分配和释放细节。

通过这种方式,内存的分配和释放几乎是“零成本”的,没有复杂的管理逻辑,每次操作只需要简单的增加或减少内存池中的引用值。因此,使用这种临时内存的管理方式可以非常高效地处理需要临时使用的大量内存,并且避免了垃圾回收机制所带来的开销。

总体而言,C和C++语言的内存管理方式使得开发者可以以非常高效的方式来处理多个重叠的临时内存区域。通过使用内存池(arena),开发者可以有效地管理不同生命周期的内存,避免手动管理内存带来的复杂性,并且减少了程序运行时的内存开销。

有些新观众在问为什么不使用 Java,可以再做一次 Java 吐槽来清理聊天区吗?

对于Java的看法主要集中在它作为一门语言的高层抽象性和限制性,尤其是在游戏开发领域。Java的主要问题是它运行在虚拟机(JVM)或即时编译(JIT)环境中,这意味着程序员对底层硬件的控制非常有限。程序员实际上是在与一个庞大而笨重的软件系统互动,而这个系统将程序的指令传达给机器,因此Java类似于一个"电话游戏",程序员的指令在多个中介层之间传递,从而使得与硬件的直接交互变得困难和低效。

此外,Java的垃圾回收机制被视为低效且资源浪费。虽然可以在某些情况下关闭垃圾回收,但关闭这些功能后,Java就失去了其本身的优势,变得更接近C语言。对于许多开发者而言,这种复杂的环境不必要的增加了程序运行时的开销,而C语言则能够直接编译成可执行文件,简化了部署和运行过程。

Java的另一个问题是它的程序包和依赖关系过于庞大。在Java中,必须将虚拟机、JIT编译器和其他依赖项与程序一起打包,这增加了最终产品的大小和复杂性。而在C语言中,程序可以直接编译为独立的可执行文件,运行时不需要额外的依赖。

此外,Java的开发周期和维护也存在问题。当依赖项出现问题时,开发者无法直接干预,只能等待Java社区或相应的维护者发布修复版本。这种对外部环境的依赖被认为是对开发者的制约,尤其是在需要直接控制硬件资源的领域(例如游戏开发)时,Java的抽象层次过高,无法满足这种需求。

对于游戏开发而言,Java并不是一个适合的选择。几乎所有知名的游戏都是用C或C++编写的,Java在游戏开发领域几乎没有竞争力。即使Minecraft这样的例外也不能改变Java在游戏开发中的局限性。Java并不适合游戏开发的高效需求,开发者更倾向于选择能够直接控制硬件、优化性能的语言,如C或C++。

你现在是直接从磁盘流式播放长音频资产了吗?

现在已经开始从磁盘流式加载资产数据,这是今天的工作内容之一。之前可能已经提到过这一点。

我觉得当音频停止时,我们需要一个淡出效果,而不是直接停止,以避免突兀的爆音。

关于音频停止时是否需要渐变淡出(fade out)的问题,通常来说,音频在结束时不需要特别的淡出处理,只要声音素材在边缘处正确衰减,通常不会出现问题。一般情况下,音频在结束时会自动逐渐衰减,因此不需要强制的淡出效果。

然而,当需要提前停止某个音频时,就确实可能出现突兀的点击声。这种情况下,需要对音频进行音量衰减处理,避免突然停顿带来的不适感。此外,如果音频在播放过程中需要从中间开始,或者有其他类似需求,音量衰减处理就显得尤为重要。

即使没有点击声问题,音频的突然停止对听众来说也是一种不自然的体验。因此,音量衰减是非常必要的,不仅能避免不和谐的音效,还能更好地平滑音频的结束,提升用户体验。音量渐变的实现也有助于声音的立体声平移效果,这样可以更好地控制音效的空间感。

因此,音量渐变是一个我们需要实施的重要功能,可能会在未来几天内实现。

以前,声卡的硬件通道数量是个重要问题。现在大多数游戏是不是已经不在乎了,因为软件混音足够快?

在过去,声卡上的硬件通道数量对于大多数游戏来说非常重要,但如今已经不再是重点了。现在,音频处理几乎完全是由软件完成的,硬件的限制已经不再是问题,因为现代的计算机处理能力远远超过了音频处理的需求。

现在,音频数据相较于现代计算机的计算能力非常小。音频处理的工作量远远不及图形处理,因此现代计算机的处理器足够强大,可以轻松处理音频工作,而无需依赖专门的硬件处理器。举个例子,在处理图形时,处理器需要处理大量的像素数据,而音频处理则只需要处理相对少量的样本数据。即使音频有多个声道,每个声道的数据也非常小,通常在十几KB的范围内。而与此相比,图形处理需要处理成千上万的像素,尤其是在高清分辨率下,计算量大得多。

例如,一个音频缓冲区的大小可能只有几千字节,而一个高清视频帧的大小则可能达到几百万字节。即便是多个音频通道的合成,所需要的处理量与图形渲染相比,几乎微不足道。现代的CPU处理器不仅能够轻松完成音频处理任务,还能够应对复杂的图形渲染工作。因此,音频处理对CPU的负担几乎可以忽略不计。

过去,声卡曾试图通过增加更多的声道或提供一些额外的处理功能来区分自己,但现在这些功能已经变得不再重要。如今,音频的混音和处理完全可以通过CPU完成,而不再需要额外的专门硬件。唯一需要注意的是,音频文件的压缩和解压缩可能会对CPU造成一定负担,特别是在大量音频同时播放时。如果使用的是比较重的压缩格式(比如MP3或Vorbis),解压缩的过程可能会增加CPU的负担。

然而,现如今大多数声卡已经不再负责压缩和解压缩音频,而是完全依赖CPU来处理这些任务。因此,声卡的多声道处理能力对于现代音频的需求并不重要,混音和音频处理完全可以通过软件来完成。

这个项目中,你是否需要支持 MP3/OGG,还是说 WAV 格式就足够了?

是否需要在游戏中支持MP3或其他音频压缩格式,主要取决于是否关心游戏的磁盘占用。若关注磁盘占用,特别是下载大小或补丁大小,就需要考虑音频压缩。音乐文件本身体积较大,采用像MP3这样的压缩格式,通常能将文件大小减少到原来的八分之一或更多,具体取决于压缩质量。若游戏中有大量的音乐文件,这样的压缩比例就非常重要,尤其是在减少存储空间和下载时间方面。

另外,压缩也可以影响传输时间。如果游戏的所有资源都已经在进行流式传输,那么压缩音乐文件并能够快速解压缩,可以加速资源加载,减少从磁盘读取的时间,尤其是在低速硬盘(如5200转的笔记本硬盘)上播放时。在这种情况下,压缩格式(如MP3或Vorbis)可能能够在解压缩时消耗的时间少于从硬盘加载数据的时间,从而提高游戏的性能。

不过,音乐压缩涉及的问题比较复杂,涉及的技术内容较多,而且从开发者的角度看,这类问题并不总是最吸引人的。因此,除非在开发过程中出现必要的需求,否则可能不太愿意深入处理压缩和解压缩的问题。如果需要进一步了解如何处理这些压缩问题,可能会邀请专家来进行详细的讲解和指导。

垃圾回收机制(GC)是否能帮助我们管理声音缓冲区和音频块?

在游戏开发中,垃圾回收(Garbage Collector)并不适用于管理声音缓冲区和资源块的情况,因为垃圾回收和虚拟内存是完全不同的概念,它们解决的是不同的问题。

垃圾回收的主要作用是管理内存,它会确保数据只有在不再被程序使用时才被回收和销毁。也就是说,垃圾回收器会跟踪哪些数据不再被任何部分的程序引用,并在不再需要时将其删除,防止内存泄漏。

虚拟内存则是处理内存容量超出工作空间时的一种方式,它允许程序能够访问更多的内存资源,即使这些数据并不完全存在于物理内存中。虚拟内存会根据当前需要加载哪些数据,把当前所需的数据加载到内存,而把其他数据从内存中移除。

在游戏的资源管理中,我们使用的方式类似于虚拟内存,但与垃圾回收无关。我们不会丢弃已经加载的资源,因为每个音频、图片等都是有效的数据,可能会在游戏过程中再次使用。我们不希望一次性加载所有资源,因为这会导致过高的内存占用和加载时间,而是动态地管理资源,只保持当前需要的部分在内存中。当新的资源需要加载时,我们会根据可能的需求,选择性地从内存中移除一些不再立即需要的资源。

总之,垃圾回收机制并不适合处理这种资源管理问题,因为我们始终需要跟踪所有资源的状态,并根据实际需要加载和释放它们。我们并不会把资源当作“垃圾”丢弃,而是根据资源的使用情况来决定是否将其移出内存。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词