游戏引擎学习第82天
回顾一下目前的进展
今天的目标是轻松一些的任务,主要是对地面渲染进行一些调整和实验。计划首先完成一些背景渲染的工作,因为前一天我们遇到了一些问题。今天的重点是解决这些问题,并为明天进行一些清理工作。下周将继续集中精力在碰撞检测等逻辑清理上,特别是地面检测的部分。
从目前的进度来看,我们已经实现了一个简单的功能,可以将地面渲染到屏幕上。然而,由于没有进行优化,背景的绘制会变得非常缓慢。
关闭调试边界
为了简化当前的工作,决定暂时关闭调试边界的显示,因为此时并不需要查看这些信息。原本用来绘制边界的代码调用了 PushRectOutline
,这些矩形是用来显示调试用的蓝色线条,但现在不再需要这些线条,因此决定将其移除。这样,画面上就只会显示真正需要渲染的内容。
需要关注的两点:1) 加速纹理溅射,2) 多层处理
在讨论如何提升性能时,提出了一种方法:通过使用背景瓦片,只绘制一次背景并进行翻转,从而避免每帧都重新生成大量图案的性能问题。这种方法能够有效减少每帧的绘制负担,只需在小范围内进行合成操作。
另外,还需要考虑如何实现多层的效果,比如有上下两层的设计。这样可以实现类似楼层的结构,虽然目前还没有图形化的表现形式,但这种概念的核心仍然存在。如果想要实现多个楼层的效果,就需要一种方式来处理这些层次,并考虑如何管理这些元素的显示位置。
如何让随机数生成更加系统化
首先,决定清理一下随机数生成的部分,目的是让它更加系统化,并为将来可能引入的更复杂的随机数生成方式做准备。当前的随机数生成仅仅依赖一个表格,直接从中抽取数值,因此需要将这些代码整理成一个可以重复使用的模块。
接着,讨论了关于如何使用随机数的问题。通常,有两种需求:一种是完全随机的数值,另一种是可预测的、但具有随机性质的数值。后者常用于生成世界等内容时,玩家希望能够通过一个小的信息片段(即种子)重新生成相同的随机世界,而无需传输庞大的数据量。种子值可以理解为随机数生成的起始点,它决定了从哪里开始生成随机数序列。
种子的概念非常重要,它能够保证同一串随机数在不同的计算机上重现。虽然目前没有实际使用这种种子机制,但这是未来可能用到的设计概念。因此,为了确保随机数生成更加有序,建议不要让程序直接随机生成数值,而是通过某种方式获取随机数流,这样能够管理多个随机数流,一些流需要表现得像真正的随机数,另一些则需要控制其可重复性,以便能够在不同机器之间共享种子,从而保证一致的随机结果。
这种设计方法类似于一些游戏中的“种子”功能,玩家可以通过输入一个种子值来重新生成特定的游戏内容,比如《Binding of Isaac: Rebirth》等游戏就采用了类似的方式。
引入 random_series,它接受一个种子
目标是将随机数的处理改为使用一个“句柄”(handle)来管理随机数序列,代替当前的随机数索引。通过这种方式,可以让随机数序列更加灵活和可控,并且能保证每次生成的随机序列是一致的,只要输入相同的“种子”(seed)。
首先,需要为随机数序列初始化一个种子,创建一个随机序列。种子是一个初始值,用于生成可复现的随机数序列。例如,通过给定一个种子值,系统会根据这个种子生成一个随机数序列。这个过程保证了每次使用相同的种子时,生成的序列都相同。
接下来,系统中的代码需要被重构,将随机数生成的逻辑抽象成多个函数,例如“随机选择”和“随机数生成”函数。对于“随机选择”功能,系统会从多个选项中随机选择一个,例如在0和1之间选择。对于“随机数生成”功能,系统会生成一个在0到1之间的浮动值。
通过这种方法,随机数的生成变得更加模块化,能够根据需要进行不同类型的随机操作,而不再直接操作原始的随机数表。这种重构不仅能提高代码的可维护性,还能在后续扩展时更方便地添加不同的随机数生成方式。
RandomUnilateral (0 - 1) 和 RandomBilateral (-1 - 1)
在随机数生成中,可以将随机数分为“单边”和“双边”两类。单边随机数(unilateral)表示范围从 0 到 1,而双边随机数(bilateral)表示范围从 -1 到 1。术语“单边”和“双边”源于对称性,其中单边表示只有一个正方向,而双边表示有正负两个方向。另一个可能的命名方式是“正态随机数”和“二元正态随机数”,即将单边视为 [0, 1] 范围的标准化数字,而双边视为 [-1, 1] 范围的标准化数字。
虽然存在其他命名方式,但选择术语时应根据个人偏好和理解来决定,确保命名符合代码的实际含义,并且便于理解和使用。
黑板:简单回顾昨天的数学课程
在这个部分,讨论的重点是如何简化生成负一到一之间的随机数的过程,而不需要每次都手动进行复杂的数学运算。为了简化这一过程,目标是能够直接得到所需的随机数,而不必每次都计算和处理相关的数学公式。这种做法的优势在于可以减少繁琐的数学推导步骤,使得生成随机数的过程变得更加简洁高效,尤其在需要频繁生成这类数值的情况下。
这种方法在未来的实现中将帮助减少计算负担,避免每次使用时都重新做复杂的数学运算,从而提高代码的可读性和可维护性。
引入 RandomBetween
讨论的核心是如何生成一个灵活的随机数范围,避免每次都手动计算数值。提出了一种更简便的方法来生成一个随机数,允许指定最小值和最大值。例如,可以定义一个函数RandomBetween
,输入最小值和最大值即可得到该范围内的随机数。
在此例中,目标范围是负一到一,因此可以直接使用random bilateral
来生成所需的随机数,这正好符合目标范围。但这个方法可以更灵活,支持不同的数值范围。为了实现这一点,可以将“随机双向数”(random bilateral
)的范围调整为任何用户所需的范围,而不仅仅是固定的负一到一。
进一步讨论了扩展的可能性,提出了生成二维或更多维度的随机数对的想法,尽管目前尚未深入展开,因为这个功能的应用场景可能会在之后的开发中才出现。
将 RandomChoice 传递给 Stamp 选择器
讨论的核心是改进“随机选择”的实现。目的是简化代码,使其更加清晰和易于理解。提到通过去除不必要的括号来简化RandomChoice
的调用,从而使代码看起来更简洁。具体地,对于选择比特图的情况,只需指定一个计数值,然后从中选择一个,而不需要额外的复杂操作。
最终目标是使代码更简洁、更易于编译和维护。完成这些修改后,计划进行编译测试,确保代码能够正常工作并没有引入新的问题。
创建 Seed 函数
讨论的重点是创建一个C语言的seed
函数,其主要功能是初始化随机数生成器的索引。具体来说,seed
函数接收一个无符号整数作为输入,并将其赋值给随机数索引。在实现时,必须确保该索引不会超出随机数表的范围,因此需要对其进行“限制”(clamp),确保它始终保持在表的有效范围内。这样可以避免数组越界访问,从而保证程序的稳定性。
实现其余的随机数生成部分
在实现随机数生成功能时,整体过程比较简单,基本上就是在前面已完成的基础上进行扩展。关键的操作就是利用随机数表来产生随机数,并且根据需要递增随机数的索引。
首先,可以假设存在一个类似 NextRandomUInt32
的函数,传入 random_series
对象后,该函数负责返回下一个随机数。此时,NextRandomUInt32
函数通过返回包含随机数索引的随机数表来生成随机数,可以直接使用该索引而不需要每次都操作 series
内部的索引。生成的随机数会返回,而一旦该索引超过随机数表的大小,就会将其重置为 0,以实现循环。
接下来,使用这个随机数进行一系列计算。具体来说,如果需要获得某个范围内的随机数,可以通过生成一个随机数并将其与期望的范围值相乘来得到。例如,对于一个随机数生成器,可以根据实际需求调整生成的结果,可能会乘以某个系数来简化操作,因为通常情况下,CPU 在乘法运算上效率较高。
另外,还可以对已有的随机数生成方式进行一些优化。举个例子,如果需要进行随机的双边数生成,可以通过以前的方式生成一个单边的随机数,然后乘以 2 再减去 1 来得到双边的随机数。对于随机数范围生成的方法,首先计算出最大值和最小值的差值,得到整个范围,再通过生成的随机数来缩放这个范围,最终得到需要的数值。
这样,整个实现过程清晰且直接,能够生成所需范围内的随机数。
(重新)引入 Lerp
在实现过程中,使用了一种常见的插值方法,即线性插值(linear interpolation)。这种方法是从一个起始点(比如最小值)到另一个终点(比如最大值)之间,按某个比例进行过渡。可以通过指定一个比例值 t
(通常在 0 到 1 之间),来决定在起始值和结束值之间的位置。公式为:
结果 = ( 1 − t ) × a + t × b \text{结果} = (1 - t) \times a + t \times b 结果=(1−t)×a+t×b
其中,a
是起始值,b
是结束值,t
是从 0 到 1 的比例值。
这种线性插值方法在很多地方都能看到应用,尤其是在需要平滑过渡的场合。例如,当需要将一个数值从最小值逐步过渡到最大值时,就会使用这种形式。
对于随机数的生成,也可以利用这种线性插值的方法,将随机值映射到指定范围内,从而得到所需的随机数。
尽管在实现过程中,可能没有事先定义一个专门的线性插值函数,但这种插值的思想在不同的地方都有应用,尤其是在处理类似的区间过渡时非常有用。
黑板:Lerp 或线性插值
在讨论线性插值时,关键的公式是:
结果 = ( 1 − t ) × a + t × b \text{结果} = (1 - t) \times a + t \times b 结果=(1−t)×a+t×b
其中,a
和 b
是两个值,t
是一个从 0 到 1 的比例值。这个公式的基本原理是通过调节 t
来平滑地从 a
过渡到 b
,t = 0
时结果是 a
,t = 1
时结果是 b
,在 0
到 1
之间的任何 t
值都会得到一个介于 a
和 b
之间的数。
通过展开公式,可以得到:
( 1 − t ) × a + t × b = a − t × a + t × b (1 - t) \times a + t \times b = a - t \times a + t \times b (1−t)×a+t×b=a−t×a+t×b
利用分配律,公式可以重写为:
a + t × ( b − a ) a + t \times (b - a) a+t×(b−a)
这个形式清晰地显示出 b - a
是 a
和 b
之间的差值,也就是区间的范围。a
则是开始的值,b
是结束的值,t
决定了过渡的比例。
这种形式有很多用途,特别是在计算两个值之间的过渡时。无论 a
和 b
是最小值和最大值,还是其他任何值,只要 t
在 0 到 1 之间,都可以使用这个公式进行插值。
这种线性插值公式在游戏编程中非常有用,几乎是最基础和最常用的数学公式之一,掌握它对于任何游戏编程来说都是至关重要的。
将它放入 game_math.h 并使用?
在实现过程中,提到了对线性插值的公式进行了确认,并在代码中加入了这个公式。这是一个常见的操作,用于在两个数值之间按比例进行过渡。虽然在搜索过程中出现了一些小问题,比如由于输入错误没能找到相关代码,但最终成功定位并将其正确地使用在实现中。
该公式的核心是:通过调整比例 t
(从 0 到 1)来实现从 a
到 b
的平滑过渡。这个操作本质上是标量操作(scalar operation),即对单一值进行的操作,计算过程简单且高效。
此外,反复强调了这个公式在游戏编程中的重要性,并表示这是必须掌握的基础函数。这个线性插值公式几乎在所有涉及数值过渡的场景中都会用到,因此它是游戏开发中的基础技能之一。
完成 RandomChoice 的传播
在这一段中,首先需要完成代码中的修复和替换工作,具体来说,需要将某些地方修改为随机选择(RandomChoice)。接着,通过使用随机双边数(random bilateral),可以对该操作进行进一步的处理。使用 series
地址来处理这些数值,最终检查其是否按预期工作。
为了进一步引导和展示变化,可以选择使种子(seed)周期性地自我调整,虽然这并非必须的操作,但可以帮助观察背景的变化。接下来,将继续清理和优化代码,确保所有地方都能够使用随机数表,利用新的随机系列来保持一致性。
在修改过程中,发现了一些变量命名上的问题,比如将 RandomChoice
实际上应该是“门的方向”(DoorDirection),因此会进行适当的更名和调整。具体操作是将 RandomChoice series
改为 RandomChoice series of two
和 RandomChoice series of three
,根据不同的条件选择不同的数值。
最后,检查到没有太多其他需要修改的地方,剩下的只是关于“Familiar生成器”的随机数部分,其中涉及到 x
和 y
的值范围,最终结果是生成一个介于负七和三之间的数值,具体来说是从零到九之间的数值减去七,确保生成的值在这个范围内。
创建用于整数空间的 RandomBetween
这段讨论涉及到生成随机整数的方式,以改进当前的代码实现。首先,讨论者提到目前生成随机数的方式较为难以阅读,并且不太清楚其具体作用。因此,提出了一种可能的改进方案:使用支持整数的随机数生成方法,比如通过设定最大值和最小值来生成一个整数范围内的随机数。
通过使用一种类似于RandomBetween
的函数,可以将最小值和最大值作为输入,生成一个指定范围内的随机整数。这种方法能够清晰地表达生成随机数的意图,使代码更加易读。讨论者特别强调,使用浮点数的lerp
方法不适合此场景,因为需要生成的是整数。
进一步的讨论中,提出了一种方法:将最小值加上最大值与最小值之差,作为随机数的范围,利用RandomBetween
来生成该范围内的随机数。然后,结合数学公式进行计算,得到最终的偏移值。这样,能够确保生成的随机数在指定的整数范围内。
尽管初步的测试代码出现了一些问题,比如生成的随机数类型不符合预期(输出为u32
类型),讨论者表示希望对代码进行调整,确保生成的随机数满足所需的正负范围,并确保它们的类型正确。
最后,讨论者计划在代码中设置断点,检查生成随机数的过程,排查出现问题的原因,并进一步验证代码的正确性。
找到 Familiar
一个与随机数生成相关的过程。首先,提到“RandomBetween”函数的工作原理,即通过生成一个介于最小值(min)和最大值(max)之间的随机数。实际计算中,max - min
结果是9,符合预期。接着,结果为负数 -7,这引起了疑问,但后来发现这是一个合理的结果,因为生成的随机数有可能在给定范围内。随后,另一个结果为5,也是一个有效的结果。
接下来,讨论了可能的误解。认为生成的负数可能是错误的,最终发现这只是误读了随机数生成的逻辑。实际情况是,生成的随机数是介于0到9之间的,符合预期的行为。还探讨了随机数生成器的行为,认为可能只是碰巧生成了一个看起来不合常规的数值,而没有重新运行随机数生成器,因此未察觉到正常的随机行为。
最终,验证了结果,确认生成的随机数确实符合预期,并且没有问题。总结认为,之前的困惑可能仅仅是由于一次偶然的情况,导致了错误的解读,而实际上随机数生成是按预期工作的。
修正 FamiliarOffset 范围
在这段讨论中,首先考虑了一个与“Familiar的随机性”相关的问题。通过调整偏移量,选择了将“Familiar”偏移量设定为负值,并且可能会在负七到七的范围内变化,以确保它的位置合理。这一调整被认为能够产生更自然的随机结果,并且结果看起来更符合预期。为了进一步验证这一点,生成了一些随机数,并观察了结果,认为增加更多的范围可能会使位置更分散,看起来更合理。
接着,讨论了关于“最大值”和“最小值”范围的问题。发现当前的处理方式在某些情况下可能会导致范围问题。例如,当前的做法没有包括最大值,这导致了结果偏向最小值。讨论中提出,应该将最大值加一,以确保范围的正确性。这种修改可以确保当范围为1时,随机数生成器能够正确地返回最小值或最大值,而不是仅仅返回最小值。根据这个修改,问题得到了解决,原本应该显示三行的“熟悉”现在显示了三行,效果更好。
我们已经处理了所有关于随机数表的事情
在这段讨论中,提到对随机数生成的进一步优化。首先,确保代码中没有直接依赖随机数表,这样当不再使用表时,就能够顺利移除它。代码中只需保留与生成随机数相关的部分,特别是 NextRandomUInt32
这一例程,这就是唯一需要重新实现的部分。除了这一例程,还提到可能需要重新设置种子(seed),当改用真正的随机数生成器时,这一改动将更加重要。
接下来:尝试进行预合成
在这段讨论中,提出了接下来的工作计划。首先,考虑到下一步的任务不太明确,决定尝试做一个“小而简单”的任务,即进行预合成(pre-composite)。具体来说,目的是将当前的绘制和地面处理(如 DrawTestGround
)优化,使其能够合成地面图像,而不是每一帧都重新绘制。为此,计划实现一个方法,将地面图像合成到一个缓冲区中,避免重复绘制。
另外,提出可能将这一过程命名为 DrawTestGround
,虽然名称上可以保持一致,但核心目标是利用一个离屏缓冲区来存储生成的地面图像,以便后续使用,而不是每次都重新生成。这将使得地面合成过程更加高效,并减少不必要的计算。
查看 game_offscreen_buffer 过去是如何工作的
提出了将两个功能合并的想法。具体来说,提到当前系统中有两个类似的对象:game_offscreen_buffer
和 loaded_bitmap
,它们非常相似,因此考虑是否可以将这两个功能合并为一个。这意味着,原本分别用于存储游戏离屏缓冲区和加载位图的功能,可以通过简化为一个统一的对象来减少重复和冗余,从而使得代码更加简洁易懂。
将 game_offscreen_buffer 和 loaded_bitmap 简化为一个对象
在这段讨论中,提出了对位图加载过程的优化方案。首先,考虑是否可以将当前的 loaded_bitmap
重新命名为更简洁的 bitmap
,并引入 pitch
参数来优化图像处理。通过查看现有代码,发现只有一个功能依赖于 pitch
,因此在处理图像时,理论上可以采用相同的方式来更新源缓冲区,并利用 pitch
来调整图像的偏移量。
具体做法是,将图像处理过程中的行偏移量改为使用 pitch
,而不是仅仅依赖图像宽度,这样可以处理不同的图像加载方式,避免上下翻转的问题。进一步来说,修改代码以支持 pitch
,只需在加载位图时,按照图像的 pitch
进行行偏移计算,确保图像的正确读取。
另外,讨论中提到,由于位图的每个像素总是占用 4 字节,因此可以直接假设每个像素占 4 字节,而不需要考虑不同位图可能具有不同的字节数。最终,认为这些修改能够简化代码,并使得图像加载更加高效。
总结来说,优化了图像加载的过程,通过引入 pitch
使得图像的处理更加灵活,减少了依赖宽度的局限性,使得系统能够适应不同图像的加载需求。
从 Bitmap 设置 Pitch 和 Pixel 指针
在这段讨论中,重点是如何正确设置图像的 pitch
和 pixels
指针。首先,pitch
被定义为图像宽度乘以每个像素的字节数,表示每一行的字节数。为了避免重复指定每个像素字节数,建议定义一个全局常量 BytesPerPixel
,以便在需要时统一使用。
接下来,设置 pixels
指针时,目标是将其定位到图像的第一行。这是通过将原本的 pixels
指针向上偏移 pitch
的大小来实现的。由于图像的存储是从底部开始的(上下翻转),因此 pitch
的值被设置为负值,这样就能够正确指向顶部行。
最后,讨论中提到,虽然调整了指针,但并未改变每个像素的字节数,因此代码仍然按照 32 位像素进行处理,不存在其他问题。
总结来说,关键是调整 pitch
和 pixels
指针的设置,确保图像数据正确对齐,同时避免引入不必要的参数化。通过这些调整,图像处理得到了优化。
会段错误
小修正,并查看结果
在这段讨论中,问题出在 pitch
值的方向上。最初,pitch
被设置为负值,目的是使指针向上移动一行。然而,这样的做法实际上导致了指针向回移动,而不是向前移动,因为图像的存储方向是从底部到顶部。为了解决这个问题,需要调整 pitch
的方向,使其正确地向前移动一行。这个调整之后,问题得以解决。
上面的段错误是 SourceRow += Bitmap->Pitch;
才对
修改 DrawBitmap 函数,传递 loaded_bitmap *Buffer,并将 *Pixels 改为 *Memory 在 loaded_bitmap 中
在这段讨论中,修改的核心是使得 DrawBitmap
函数更加灵活,不再依赖于特定的缓冲区。现在,可以将一个离屏缓冲区或其他位图作为参数传递给该函数。接着,将内存处理的部分改为统一的内存接口,以确保所有相关操作能够正常对接。最后,解决了关于字节每像素(BytesPerPixel)的问题,确保每个参数传递的一致性和正确性。
从 game_offscreen_buffer 中移除 BytesPerPixel
在这段修改中,去掉了 BytesPerPixel
的参数,因为已经明确了每个像素都是 32 位宽,因此不再需要让其可变。此外,将像素处理的代码进行了整理,确保内存处理部分统一。修改了 DrawBitMap
函数中的一些成员变量,以确保其能正常工作,并且最终解决了与像素相关的错误。
让所有内容都从 loaded_bitmap 中获取数据
在这段修改中,将 DrawBitMap
函数进行了调整,使其能够处理不同类型的缓冲区(如 DrawBuffer
)。通过将缓冲区名称更改为 draw buffer
,简化了代码中的命名。同时,去掉了不必要的 BytesPerPixel
参数,因为像素宽度已经固定为 32 位。在初始化过程中,将缓冲区指针设为传递给函数的内存地址,确保了代码的一致性和简洁性。此外,也考虑了将 loaded_bitmap
改名为更合适的名称,以提升代码的可理解性。
实现一个 GroundBuffer 缓存
在这段修改中,游戏的渲染性能得到关注,通过观察帧率,可以发现性能较慢。为了优化性能,可以考虑将渲染结果缓存在一个缓冲区中。具体来说,在绘制测试地面时,可以将渲染结果保存到一个缓冲区中,例如 game_state
中的 GroundBuffer
。为此,可以创建一个空的位图缓冲区,并将其大小设为合适的值(例如 1024x1024)。
此外,还讨论了如何在处理世界数据时,利用一个 arena(内存区域)来分配和管理缓冲区。通过这个 arena,可以为渲染生成的缓冲区分配内存,并将其用于后续的绘制操作。最终,修改了调用,确保渲染缓冲区正确绘制。
实现 MakeEmptyBitmap
在这段修改中,游戏的图像缓冲区的大小被设置为512x512,并计划对其进行测试。在实现过程中,遇到了内存分配问题,可能是因为计算了过大的内存空间导致。通过分析,发现计算时出现了错误,导致分配了过大的内存空间。需要修正计算方式,确保所需内存大小合理。
检查时发现,绘制的是基于屏幕中心的位图,而不是缓冲区的中心,这可能导致问题。最终,需要确保图像在缓冲区中正确对齐,以便准确显示。
设置合成位图的 alpha 通道
在这个过程中,首先发现了一个问题,原本应该设置的 alpha 通道没有设置,导致了合成位图的问题。为了修复这个问题,必须确保正确设置 alpha 通道,这样才能正确地合成图像,否则合成的效果会出现不正确的显示。
解决方案是,将 alpha 通道添加到处理流程中,确保正确的 alpha 值被写入到缓冲区。在进行 alpha 计算时,需要考虑如何计算正确的 alpha 值,这个话题可能会留到后面继续讨论。在当前的实现中,可以暂时将 alpha 设置为图像本身的 alpha 值,但这并不完全正确,未来可以进行进一步的调整。
接下来,通过对 alpha 值的正确处理,可以实现更准确的图像合成,但还存在一些问题,比如未被触及的区域仍然保持 alpha 为零,因此需要进一步处理这些区域。
通过这些修改,帧率已经恢复到正常水平,这表明在进行了一些额外操作后,程序仍能高效运行。这些修改只在初始阶段进行一次,之后就可以重复利用这些数据,从而提高效率。
回顾
在这个过程中,讨论了如何处理目标 alpha 值的计算,虽然这个话题暂时搁置,但说明了将来需要进一步探讨如何准确处理 alpha 通道。
接着,重点讨论了如何简化和对称化图像绘制过程。关键是,必须使得读取和写入的目标能够对称处理。这样做的重要性在于,它允许在绘制到屏幕的过程中,将相同的代码逻辑应用于绘制到其他目标,比如将图像保存为合成纹理。
这实际上就是图形硬件中常见的 “渲染到纹理” 技术,即将绘制的内容直接输出到纹理中,并可以在之后作为纹理使用。这种方式非常简单,只需要确保能够使用结构体来指向目标,并且清楚地知道其大小、跨度和内存位置。
通过这种方式,代码变得更加灵活,可以在不同的绘制目标之间切换,而不需要做复杂的修改,简化了图形渲染的处理流程。这也是在未来构建渲染系统时一个非常重要的设计原则。
问:为什么 RandomUnilateral 和 RandomBilateral 没有直接使用 RandomBetween 的实现?看起来这违反了 DRY 原则,尽管我知道你当前并没有计划让代码非常干净。
解释了一个重要观点,即在代码结构上,这些函数相对简单,不需要为了优化代码的重用性而强行合并。实际情况是,这两个函数并没有完全相同的行为,因此保持它们的独立性有助于未来的优化。特别是在代码较简单时,过度重构可能会导致性能损失,并使编译器更加困惑。
另外,提到当涉及到性能优化时,往往会考虑具体操作的特性,这些函数在处理时可能会有不同的需求,因此不会立即进行合并。在优化阶段,可能会对它们进行调整,或许会采用不同的方式来提高效率。
总结来说,尽管从某种角度看,DRY原则可能要求函数复用,但在这种简单的情况下,保持函数的独立性更有助于未来的代码优化和清晰性。
有人想要一个代码行数统计
问:为什么 Seed 函数没有加上 Random 前缀或后缀?
在讨论中提到,seed
函数通常没有带有前缀或后缀的 random
,原因是它的功能本身非常直接,且没有其他显而易见的用途,因此不需要额外的修饰。如果有需要,可以在命名中添加前缀或后缀来使其更具区分性,尤其是如果这种命名方式让代码结构更加清晰,便于理解和使用。这种方式并不会带来实际的功能性改善,但有时为了帮助代码更好地分模块或避免混淆,添加 random
前缀也是一个不错的选择。
对于重载函数的情况,如果不同的函数名和功能相似,random
和 seed
之间的区分仍然是有效的。即使其他库或代码中也使用了 random
相关名称,函数重载也能确保它们不会发生冲突。因此,尽管 seed
函数本身并不需要特殊标记,但如果这样做能带来更多的规范性或帮助理解,完全可以接受。
问:RandomChoice 的随机选择值不均匀,这样可以吗?[…]
RandomChoice
函数并不会均匀地选择随机值,具体取决于传入的选择数量(ChoiceCount
)。例如,如果 random
的最大值是十,那么返回的值可能更倾向于较小的数字,这不是理想的行为。虽然目前这种情况还可以接受,但从长远来看,这种偏好并不合适,因为这意味着随机数的分布并不均匀。
使用的随机数生成器应当能够生成完全均匀的随机分布,避免出现这种偏向性。这问题需要在实际实现随机数生成器时解决,此时 RandomChoice
应该返回一个均匀分布的结果,除非有其他特殊的需求或原因。
目前这种偏差虽然存在,但由于暂时并没有使用真正的随机数生成表,因此还可以接受。但一旦切换到实际的随机数生成器时,确保随机选择均匀分布就显得尤为重要。
问:如果选择的选项数量不能均匀除尽最大随机数,RandomChoice 会有轻微的不均匀分布吗?
讨论中提到,RandomChoice
可能会在选择数量无法均匀划分最大随机数时导致分布略微不均。具体来说,当随机数生成器使用的是32位的随机数,而不是一个固定的随机数表时,这种不均匀性可能会变得更明显。由于使用了模运算,确实存在一个非常微小的偏向,尤其是对于较小的数字,这种偏向几乎可以忽略不计。
然而,尽管这种偏差存在,它的影响通常非常微小,几乎不会在实际应用中产生问题。如果极端要求没有任何偏差,可能需要采取其他方法来解决这个问题,但就目前来看,这种小范围的偏差不太可能引发实际问题。理论上,如果要完全消除这种偏差,可能需要对算法进行调整或采取不同的实现方式。
问:什么时候可以通过值传递而不是引用传递来传递更大的对象,比如r3?
讨论中提到,是否应该通过值传递而不是通过引用传递较大的对象,取决于具体的情况。对于内联函数,通常可以传递任何类型的对象,因为编译器会自动优化并决定最合适的处理方式。大多数情况下,编译器足够聪明,可以扩展内联函数并根据需要进行优化,虽然在某些情况下仍然需要验证是否符合预期。
在64位系统中,指针通常占用8字节,因此如果传递的结构体大小接近8字节,甚至是16或32字节,传递指针与传递结构体的差异几乎不大。尤其是当函数调用时,编译器会根据应用二进制接口(ABI)将参数放入寄存器或栈中。如果寄存器无法容纳所有参数,编译器会将它们放到栈上。一般来说,64位寄存器的大小为8字节,较大的寄存器如XMM寄存器和AVX寄存器可达到128位甚至更大,因此大多数参数会被传入寄存器,只有当参数较大时,才会被推送到栈中。
如果传递的参数不超过64字节或80字节,通常这些参数会被直接存入寄存器,这样传递的效率不会受到太大影响。对于需要优化的场景,通常通过查看编译器的实现来确保代码是正确的。如果对性能有严格要求,可以通过实际测试来检查寄存器与栈之间的性能差异,尽管在很多实际应用中,这些差异往往不会导致显著的问题。
最后,如果传递的对象不特别大,通常不需要太过担心其性能影响。对于较大的对象,特别是涉及到栈传递的对象,最好避免传递过大的结构体,并确保在需要时进行适当的性能测试和验证。
问:背景瓦片目前是静态的,那么…
背景图块目前是静态的,这是因为还没有决定如何让地面围绕玩家动态调整。这一部分属于更具体的功能设计,目前的工作重点是让图像合成正常运行,因此背景图块暂时保持静态状态。后续将会对地面动态调整的实现方式进行规划和处理,这可能会作为接下来的工作安排来完成。
问:你是否考虑过将 RandomNumber 扩展为支持除均匀分布之外的其他分布?
随机数生成可能会扩展为非均匀分布,但不一定直接在当前随机数模块中实现。进行程序化寻路生成时,可能需要使用非均匀分布,因此预计会有某种方式来生成非均匀分布的结果。然而,随机数生成的基础通常仍会以均匀分布为起点,通过对分布进行变换来实现非均匀效果,而不是直接在核心部分设计为非均匀随机数生成器。这是一种假设,但总体思路是保留均匀分布的核心,然后根据需求进行分布的调整或变形。
问:这完全在内存中处理吗?你是否有计划使用 GPU 资源进行渲染?
渲染将首先完全通过软件方式进行,目标是开发一个完全基于软件的渲染器。这是为了达到学习和教学的目的。在完成软件渲染之后,将会展示如何实现硬件加速的渲染作为一条独立的路径。整个游戏始终可以通过软件渲染模式运行,但某些功能可能会因性能限制在软件渲染中被关闭。整体目标是确保软件路径始终可用,同时在适当时引入硬件加速以优化性能。
问:我注意到在函数中你总是使用 Result 变量,即使计算仅是单行代码,而不是直接在 return 语句中使用表达式。这样会产生额外的复制操作吗?有没有性能上的影响?
在函数中使用一个结果变量,即使计算仅是一行代码,而不是直接在 return
语句中使用表达式,不会在优化模式下导致额外的拷贝或性能影响。任何正常优化的编译器都会在这种情况下消除多余的拷贝。然而,在调试模式下,可能会存在额外的拷贝,因为编译器在调试模式下通常不会进行优化,但调试模式本身会引入许多类似的额外拷贝,因此影响可以忽略。
之所以使用结果变量,是为了在调试时更加方便地检查返回值。通过设置断点,可以在 return
语句之前查看结果,而无需依赖调试器是否能够轻松显示返回值。这种做法提高了调试的便利性,尤其在检查复杂表达式时更加直观和可靠。
问:你计划将所有其他 loaded_bitmap 的内存也移动到 memory_arena 中吗?
当前加载的位图内存暂时未迁移到内存区域管理中,但未来会进行迁移。当前使用的是调试文件加载方法,而在完成资源加载路径的实现后,所有加载的位图内存将基于内存区域进行管理。这是后续开发的一部分计划。
问:我看到你在某个地方使用了聚合初始化器来为 random_series 设定种子,也就是说你只调用了一次 RandomSeed,但至少有…
在代码中发现了一个关于随机数系列初始化的问题,目前仅调用了一次随机种子初始化函数(RandomSeed
),但存在至少一个额外调用的需求。对这种写法表示这是由于习惯造成的,尽管在实际代码中并不这样处理。在代码书写时尝试使用一种更直观的方法,但这与平时的风格不一致,指出更好的方式是直接按照结构需求进行书写。对发现该问题表示感谢,并承认在常规代码中通常会避免这种情况,同时考虑改进写法使其更加规范。
问:最初你把 BytesPerPixel 放入位图结构中,因为它是有用的。然后我让你移除它,因为它永远不会改变,你也做了。但十集之后,你又加回了它,因为它会很有用。而现在你又把它移除掉了,这真有趣。
在编程过程中,关于位图结构中每像素字节数(BytesPerPixel
)的存储位置经历了多次调整。最初将其包含在位图结构中,因为认为其可能有用,但后来被移除,因为其值始终不变。然而,在进一步开发中再次将其添加,因为发现某些情况下需要引用此信息。现在又重新讨论是否将其移除,这种反复调整的现象在编程中并不罕见。某些决策的灵活性会随着需求变化而调整。最终可能会决定将其设为常量,因为其值实际上是固定的,这样更符合设计初衷。过去的移除决策在当时是正确的,现在则可能会根据实际需求再次修正,并保持一致。
问:你通过生成一个完整的 32 位随机数,对其进行 mod 操作以获得下一个 2 的幂次方的选项数量,然后检查结果是否小于选项数量。如果不是,就重复整个过程,这样能确保 Random 是均匀的。
在随机数生成过程中,为了确保结果在特定范围内均匀分布,可以通过以下方式实现:生成一个完整的32位随机数,取模下一个比目标范围更大的幂次值,然后检查结果是否小于目标范围。如果不满足条件,则重复整个过程。这种方法虽然能够保证严格的均匀分布,但实现过程较为复杂且涉及循环,因此效率较低。最终选择接受生成过程中的轻微偏差,以简化实现并提高效率。