游戏引擎学习第14天

视频参考:https://www.bilibili.com/video/BV1iNUeYEEj4/

1. 为什么关注内存管理?

  • 内存分配是潜在的失败点

    • 每次进行内存分配(mallocnew等)时,都可能失败(例如内存不足)。
    • 这种失败会引入不稳定性或不可预测性,需要额外的错误处理逻辑。
  • 传统编程的理念

    • 在"老派编程"中,作者更倾向于避免动态内存分配,通过静态或固定大小的缓冲区管理内存,从而使程序更加稳定和可靠。

2. 如何避免动态内存分配?

  • 示例中使用的策略
    • 程序需要两个缓冲区(一个音频缓冲区和一个位图缓冲区),它们通过操作系统一次性分配完成,且在整个程序生命周期内从不释放(never freed)。
    • 这种设计消除了动态内存分配的风险,也避免了与内存回收(如垃圾回收)相关的复杂性。

3. 动态内存分配的挑战

  • 现代编程环境的现状

    • 现代程序大量依赖动态内存分配和垃圾回收(如Java、C#中的GC机制)。
    • 动态内存管理可能导致性能抖动(如虚拟内存分页),难以预测程序的行为。
  • 作者的观点

    • 过多依赖动态内存分配,使得程序开发变得复杂且不可控。
    • 作者更喜欢"静态内存分配"或"固定缓冲区"的方法,使程序从一开始就处于稳定状态。

4. 静态内存管理的优点

  • 高可靠性

    • 程序一旦启动,就不会因为内存问题而失败。
  • 简单性

    • 减少内存泄漏和回收相关的复杂性。
  • 性能一致性

    • 消除了垃圾回收或虚拟内存分页带来的性能抖动。

5. 哲学层面的思考

  • 作者更喜欢探索传统编程中的一些"简单且有趣"的做法,比如尽可能避免动态内存分配。
  • 他认为,这种方法可以让编程回归本质------精心设计和实现代码,而不是依赖现代编程环境中的各种工具和机制。

这段视频主要在讨论内存分配的优化以及游戏开发中不同的设计思路。以下是对这段对话的简要总结:

  1. 内存分配策略

    • 演讲者建议采用预分配内存的方式,而不是在运行时频繁分配。
    • 他提到这种方法类似"旧学校"的平分区策略,即一开始抓取一块大内存,并为每个子系统预先分配固定的空间。
  2. 静态变量的使用

    • 演讲者提到使用 static 定义的"局部持久变量",它本质上是一种作用域限制在函数内的全局变量。
    • 他不喜欢这种设计,因为它可能导致代码混乱和难以维护。
  3. 传统与现代的差异

    • 他对比了旧式街机程序员的开发风格和现代普遍的"内存分配狂欢"方法。
    • 强调大多数现代游戏不会采用他的方式,但他认为提供不同的视角是有意义的。
  4. 未来设计计划

    • 他计划展示如何将游戏状态存储在预先分配的内存中。
    • 演讲者希望避免动态分配,并使用更高效的方式管理内存。
  5. 强调多样化的视角

    • 演讲者明确指出,他并不是在提倡大家完全采用他的方式,而是提供一种不同的思考方式。

这种方法对某些嵌入式系统或资源受限的环境可能很有帮助,但在资源丰富的现代系统中可能并不常见。您对这段内容是否还有特定部分需要进一步解析?

在某些编程或系统设计情况下,处理失败案例的挑战,特别是与内存分配相关的挑战。

  1. 失败案例的策略

    • 必须有一个处理失败案例的计划。例如,每当进行新的资源分配时,需要检查分配是否成功,并妥善处理可能的失败情况。这可能需要复杂的逻辑来处理多种路径的成功或失败。
  2. 现实中的折中策略

    • 一种方法是通过测试运行系统,观察其内存使用情况,并确保启动时分配足够的内存。
    • 使用自定义分配器,预先分配一定的内存以避免运行中内存不足,但这种方法依赖于测试的全面性。
  3. 未知风险的接受

    • 有时,只能假设程序不会超出所需的内存范围,希望玩家或用户不会触发未考虑到的极端场景。这种方法依赖于假设和希望,而不是完全可靠的保障。
  4. 故事的必要性

    • 在设计系统时,需要为失败场景找到一个"故事"或策略,即使有时故事仅仅是"不清楚,但在测试中未发现问题"。
  5. 个人观点

    • 作者对这些策略并不满意,认为这些都不是理想的答案,但在实践中可能不得不接受其中之一。

平台抽象设计中关于游戏与平台之间交互的复杂性,以及如何简化这种交互。以下是总结:

  1. 循环调用的缺点

    • 游戏和平台之间频繁的双向调用(循环调用)会增加复杂性,使得理解两者之间的关联变得更加困难。
    • 这种复杂性在高级功能(如记录和回放整个游戏会话以进行调试)中尤为明显。
  2. 推荐的设计方式

    • 建议设计为简单的数据流模式:
      • 平台作为服务请求游戏提供必要的数据(如视频帧、音频输出等)。
      • 输入(如用户操作)直接传递到游戏,输出直接返回到平台。
    • 避免游戏层再调用平台,这种方式可以减少系统的耦合性,简化回放系统的实现。
  3. 单线程时代的优势

    • 在过去的单线程环境中,这种输入流和输出流定义明确的设计简化了调试工作(例如回放游戏的整个会话)。
    • 单一输入和输出流的模型使得系统在逻辑上更容易追踪和复现问题。
  4. 多核时代的挑战

    • 如今由于多核架构的普及,线程化操作增加了复杂性,使回放系统变得困难。
    • 线程化问题导致这种设计在现代环境中不再像过去那样具有优势。
  5. 结论

    • 过去单线程的设计逻辑有其历史意义和现实效率,但在现代多线程环境中,需要权衡线程管理的难度和系统简单性之间的关系。

游戏开发中的内存管理哲学进行了讨论。以下是要点总结:

  1. 内存管理的责任分配

    • 平台层在游戏启动时分配一块大内存给游戏。
    • 游戏负责对这块内存进行进一步分区和管理。
    • 游戏内的所有系统都必须在一个固定的内存空间中操作,确保游戏启动后能够运行至结束而不会因内存问题失败。
  2. 可靠性目标

    • 游戏开发的目标是做到"一旦启动,就不会失败"。
    • 即使某些资源(如精灵)丢失,游戏仍然能够正常运行。
    • 平台层可能会由于操作系统的问题而失败(例如 Windows 的设计缺陷),但这不会影响游戏的整体可靠性。
  3. 资源管理的哲学

    • 开发中采用"零失败状态"原则,即尽量减少游戏运行中的错误。
    • 提议了"金银铜"标准,以根据不同游戏的复杂程度评估其达到可靠性目标的水平。
  4. 延伸到磁盘 I/O

    • 与内存管理类似,对磁盘 I/O 的管理也遵循相同的哲学,以尽量减少失败。

作者表示,动态分配本质上是一种"自欺欺人"的做法,因为它假设内存是无限的,而实际上硬件的内存是有限的。如果不控制内存使用,程序可能会在运行时遇到不可预见的问题,如内存不足或碎片化。

采用主存池的方案,可以保证程序在固定内存下运行,减少风险。尤其是在面向最终用户的产品中,确保内存不会超出限制并且程序能顺利启动和运行是非常重要的。

1:03:31Why use the void* in game_memory?

在这段字幕中,主要的内容涉及内存管理和如何在游戏中处理不确定类型的内存块。可以从中提取出以下几个关键点:

  1. 不确定的内存块:由于游戏内存的某些部分是不确定的,我们不能为这些部分指定类型。这些部分被视为一个"大内存块",游戏可以根据需要任意处理这些内存数据。

  2. void指针:提到使用空指针(或void指针)来指示这些不确定的内存块。因为我们不知道它们的具体类型,所以使用void指针来管理这些内存是一个常见的做法。

  3. 类型转换:虽然开始时内存块没有明确的类型,但可以在后续转换成具体的游戏状态或其他类型,并开始进行类型化管理。

  4. 游戏中的使用:游戏开发中常常需要处理各种类型的内存,这些内存数据可以根据需求灵活地操作和转换成具体的类型。

  5. "冷启动"和"冷转换":提到的"冷启动"和"冷转换"暗示着这些内存块被快速转换成所需的格式,过程没有太多的复杂操作,直接转换为游戏所需要的状态。

  6. 游戏规模的影响:由于游戏的规模可能很大,这种内存处理方法显得尤为重要,能够动态地应对不同类型的内存需求。

存储管理和游戏开发中的一些技术概念。可以理解为以下几点:

  1. 永久存储和临时存储的区分

    • 永久存储:指的是游戏在运行过程中必须保持的数据,这些数据对游戏的持续运行至关重要,例如玩家的进度、已解锁的内容等。
    • 临时存储:是一些临时存储的数据,可以在不影响游戏运行的情况下清除或重新加载,例如缓存的纹理、声音等资源。
  2. 区分的原因

    • 性能优化:临时存储的数据可以被刷新(如在游戏运行时清空缓存),这有助于优化性能,减少内存占用。比如,在iPhone等设备上,清空缓存有助于减少应用的内存占用。
    • 事件驱动:临时存储的数据可能会受到某些事件的影响(例如用户的操作或系统事件),而这些数据不一定需要长期保存。
  3. 为什么分开处理

    • 这样做是为了能够对数据进行更灵活的管理,例如在需要时清除临时存储的数据,而不会影响到永久存储中的重要数据。
    • 这种区分还有助于一些额外的优化技巧,例如缓存和重新加载过程的优化。
  4. 后期调整

    • 在开发过程中,可能会根据实际需求调整永久和临时存储的具体实现,这种调整是灵活的,因此开发者可能会先写好代码框架,再根据实际情况做修改。

避免在平台层和游戏层之间进行频繁往返调用(即"往返旅行"),这种设计模式可能导致系统中的复杂性和性能问题。以下是一个简要的总结:

  1. 避免内存分配的往返:谈话的重点之一是避免频繁在平台层和游戏之间进行内存分配的往返。这种"往返旅行"指的是平台层调用游戏层,游戏层再调用回平台层,从而引入不必要的复杂性和性能开销。

  2. 虚拟化的例子:提到一个虚拟化的例子,试图通过设计避免频繁的内存分配和平台调用之间的交互,这样可以减少系统中的"蜘蛛网"式的复杂关系。

  3. 堆栈和调用关系:系统中创建了一个堆栈,其中平台层和游戏层交替调用,这种结构可能导致管理上的困难,尤其是当每个组件由不同的开发人员编写时。

  4. 简化设计:最后提到的优化建议是,如果设计得当(比如通过传递合适的参数),可以简化整个调用结构,使得系统运行更加高效且易于维护。

总的来说,这段对话强调了通过避免复杂的相互依赖关系和频繁的内存分配,可以让系统变得更高效、更简洁。

下面是平台独立内存代码的修改

任务管理器查看内存提交的大小

vscode 的一个小插件

json 复制代码
    "workbench.colorCustomizations": {
        // 新增文本的背景颜色
        "diffEditor.insertedTextBackground": "#00501388", // 浅绿色,带透明度
        // 删除文本的背景颜色
        "diffEditor.removedTextBackground": "#62000088", // 浅红色,带透明度
        // 修改区域的边框颜色(可选)
    },
相关推荐
zjw_rp6 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob19 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder27 分钟前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it36 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行38 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
OopspoO2 小时前
qcow2镜像大小压缩
学习·性能优化