游戏引擎学习第130天

server/2025/3/4 23:31:46/

仓库:https://gitee.com/mrxiao_com/2d_game_3

回顾

这个节目是我们在直播中编写一个完整的游戏,不使用引擎,也不依赖任何库,完全靠我们自己。现在,我们在某些方面取得了一些进展,做了很多工作,既使我们的渲染器能够在 Sindhi 系统上运行,又使它支持多线程,这两个改动证明是非常有益的,因为现在我们的渲染器能够在 1920x1080 的分辨率下以 60 帧每秒的速度运行,远远超出了我原本设定的目标。

不过现在我们有一堆清理工作要做,因为在推进渲染器并进行探索时,我们没有进行太多的整理,现在我们需要将渲染器巩固成一个能够可靠渲染游戏的系统,按我们希望的方式进行渲染。为此,有几件事情需要做,我们现在想要逐步解决这些问题。所以,我们将开始处理这些任务。

上次进展

昨天我们已经做到了一定的进展,现阶段,我们已经有了这些小的“溅射”图案,用来构建地面瓷砖。然而,我注意到这些图案之间出现了一些缝隙,因此我现在想要调查一下,看看是否能找出这些缝隙的原因,以及是否有一些特别的地方应该引起我们的注意。要解决这个问题,可能需要做一些工作,原因有几个:

首先,我们并不确定这些缝隙是从哪里来的,可能有很多地方是导致这些缝隙的原因。我们可能在源艺术贴图图像上有 bug,导致它的透明度处理不完全平滑;我们也可能在代码中有 bug,导致这些瓷砖被渲染到屏幕上时没有完全对齐,这样如果它们被放置到其他位置,可能就不会出现缝隙;或者我们在处理“溅射”效果时,可能有问题,导致当我们生成地面瓷砖时,无法正确地将其他瓷砖的“溅射”效果合并到一起。其实有很多地方可能是造成这些问题的原因。

正如我所说,我们目前很难确定具体发生了什么。所以,我想做一些实验,看看能不能通过一些实验性的操作来找出问题。就像摇一摇箱子,看看里面是否会有什么东西响,以此判断我们是否应该关注某些特定的地方,或者我们是否需要做一次彻底的检查。
先禁止旋转
在这里插入图片描述

在这里插入图片描述

隔离错误

为了排除源艺术贴图的问题,我打算对当前使用的源艺术贴图进行一些调整。这样我们就能看看是否有任何异常之处。我注意到有一些比较奇怪的图案,想在这里指出给大家,以便大家可以更清楚地理解。我发现的问题是,当我仔细查看时,图案看起来有点不对劲。你可以看到这些图案,当我放大来看时,我觉得它们看起来有些奇怪。这就像是图案的位置可能不太正确,给我的感觉是,如果这些图案稍微下移一些,问题可能就能解决。

为了更清楚地查看这一点,我打算将窗口缩小,放大图像,看看具体的细节。现在我们的渲染器支持通过改变焦距来进行缩放,因此可以轻松实现这一点。具体来说,我们传递了一个焦距参数用于透视变换,我们也可以调整地面上方的距离。如果想要靠得更近,可以把距离调整为较小的值(比如从9调整为5)。由于渲染器现在已经能够做一些很复杂的事情,这些操作变得非常简单。通过这种方式放大图像后,我可以看到更加清楚的细节。

从现在的观察来看,图案的位置似乎有些偏高。如果这个图案稍微下移一点,可能就不会有问题。我继续放大图像,仔细查看,感觉如果这个部分下移一点,线条会对齐,这样就会更准确一些。

我不能完全确定,但就我目前的观察来说,这可能是问题的原因。所以,作为排查的第一步,我打算检查这些图案是否对齐。在渲染这些地面瓷砖时,我打算通过改变它们的位置,看看它们是否会错开。我希望看到的是,如果它们确实错开,我应该能看到它们之间的缝隙。如果它们继续融合到一起,那么就说明这些图案本应分开的地方被画得太靠近了。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

检查区块连接

发现多push 了一个push 操作

在这里插入图片描述

如果图案之间重叠了,那就不是我们想要的效果,因为我们希望它们是紧密接触的,避免出现缝隙。为了确认这个问题,我们查看了渲染地面块的代码。我们在调用 GroundSideInMeters 时,传入的参数看起来是我们预期的值,所以这个部分似乎没有问题。当我们调用 PushBitmap 时,这一部分也感觉正常。

为了验证问题,我决定通过调整缩放值来检查图案的对接情况。我将缩放值乘以一个因子,将它们缩小。接着,我尝试将这些图案调整到刚好接触的位置,然后检查它们是否确实在 1.0 的位置。如果它们的距离是对的,那么它们应该正好接触。如果它们略微超过 1.0,那就说明渲染有问题。

我通过这种方法调整了位置,发现它们正好接触,且没有重叠,位置是正确的。因此,在尺寸上,图案并没有问题,看起来它们正好贴合在一起,没有出现任何不正常的现象。根据这些测试结果,似乎像素填充没有问题,至少目前没有出现可见的瑕疵。

然而,在像素填充的过程中,仍有一个小问题需要解决,以确保一切完美无缺。但就目前来看,像素填充本身并没有导致任何明显的可见瑕疵。即使我们尝试稍微分开图案一点,检查缝隙,也没有看到不正常的现象。

接下来,我希望能找到问题的根源,并进一步检查是否有其他因素导致了这些问题。
在这里插入图片描述

在这里插入图片描述

正式化纹理边界

我们需要解决一个与瓦片渲染相关的问题。记得在渲染器中,我们并没有明确决定如何处理纹理的平铺问题。现在回想起来,这个问题实际上是造成缝隙的原因之一,即使其他地方没有问题,单单这个问题就足以导致缝隙的出现。所以,我们需要深入讨论并解决这个问题。

回想一下,在之前的设计中,我们有一个关于纹理边界的概念,但我们并没有完全实现它。也就是说,如果我们没有准确地按照纹理的边界来采样,那么必然会导致问题,这就需要我们解决。特别是对于双线性滤波器,它暗示了很多细节,因此,我们需要仔细处理。

现在,我会到黑板上做个解释,帮助大家更清楚地理解为什么这个问题是至关重要的。如果我们能正确处理这个纹理采样问题,最终就能避免缝隙的产生,从而实现完美的无缝效果。

黑板:瓷砖中的双线性过滤

现在需要解决一个与双线性过滤(bilinear filtering)相关的问题,这个问题影响到瓦片渲染的效果,尤其是在瓦片之间的缝隙处。首先,我们有一些瓦片,在当前情况下,假设这些瓦片的像素不多。为了简单起见,假设这些瓦片是6x6的,而实际上我们的瓦片要大得多,像是256x256。

每当这些瓦片并排排列时,渲染器需要确保这些瓦片的颜色值正确地对齐。如果我们看着这些瓦片的边缘,理想情况下,瓦片的颜色值应该连续,而不是出现突兀的跳跃。为了确保这一点,我们需要保证每个像素的采样是精确的,尤其是在边缘处。如果像素的颜色变化(比如灰度变化)没有正确衔接,边缘就会出现明显的断层或者“跳跃”,从而导致可见的缝隙。

然而,当前使用的双线性过滤算法并没有完全解决这个问题。在双线性过滤中,渲染器会在四个邻近的像素之间进行颜色混合。当渲染器在一个瓦片的边缘绘制颜色时,它需要在该瓦片的像素与相邻瓦片的像素之间产生平滑过渡。但是问题在于,这两个瓦片的像素分别来自不同的位图。因此,渲染器无法知道如何正确地生成边缘处的颜色,因为它没有足够的信息来从相邻的瓦片采样颜色。

为了解决这个问题,可以考虑对瓦片进行重新纹理映射。具体来说,建议在每个瓦片的四周增加一像素的边框,这个边框存储相邻瓦片的颜色信息。这样,渲染器就能正确地从这些边框像素采样颜色,从而避免在边缘处出现颜色不连续的现象。这种方法不仅能解决瓦片间的缝隙问题,而且对抗锯齿效果也有好处,能够保证在渲染时避免硬边界,使得颜色过渡更加平滑。

因此,为了解决这个问题,首先需要调整UV坐标,使得每个瓦片的采样从第一像素开始。这样,当渲染器采样这些瓦片时,它就能正确地处理每个像素的颜色,确保边缘之间不会出现任何可见的缝隙或断层。

修复纹理采样

在调整纹理采样时,当前的纹理坐标(UV坐标)并不是从左下角开始的,而是应该从纹理的第一个像素开始。为了修正这个问题,需要调整纹理的坐标,使得采样从正确的位置开始。具体来说,在进行纹理采样时,需要确保坐标不会直接从矩形的底角(0, 0)开始,而是应该调整到一个适当的位置。

在纹理采样代码中,当前的获取函数(fetch)使用了一个标准的采样方式,其中通过宽度和高度来获取相应的像素。但在这个过程中,需要进行一些修改。目标是将宽度和高度调整为适当的值,从而确保纹理的采样正确,避免产生任何不连续的颜色过渡或缝隙问题。这些调整主要是通过修改宽度(m2)和高度(m2)来实现。
在这里插入图片描述

黑板:思考采样

在调整纹理坐标时,重点是确保纹理采样准确地对准像素的中心。当前的目标是确保在进行双线性插值时,纹理坐标(UV坐标)能够正确地映射到相邻纹理的像素边缘,避免产生不连续的颜色变化或者接缝。

首先,需要确保纹理的采样从正确的位置开始。考虑到双线性插值的特性,在纹理的边缘部分需要进行适当的偏移,这样才能确保在跨越纹理边界时颜色平滑过渡。例如,如果一个像素在纹理中的中心是0,0,那么在UV坐标上,它应该对应的是(-0.5, -0.5)的位置,这样做可以避免采样错误。

然后,需要考虑纹理的大小。假设纹理为一个小的6x6像素矩阵,并且两个纹理块相邻。在这种情况下,如果不进行适当的采样调整,两个纹理的接缝部分会出现不连续的色差,因为纹理的边界可能没有正确地过渡。

为了解决这个问题,考虑将纹理的坐标设置为一个偏移量。这个偏移量可以通过增加一个1像素的边界来实现,这样就能够确保每个纹理块在采样时,不会直接从零位置开始,而是从纹理块的实际中心开始采样,这样可以保证颜色值在边界处平滑过渡。

最后,调整纹理坐标时还需要考虑到每个纹理块的具体尺寸和采样精度,确保在实际绘制时,纹理的每个像素都能正确对齐,并且避免由于采样不当而产生的图形错误。

偏移采样位置

在计算纹理的UV坐标时,需要进行一定的偏移,以确保正确地对齐纹理的像素并实现平滑的插值。这种偏移是为了补偿纹理采样时的边界问题,尤其是双线性插值可能导致的接缝不连续问题。

具体来说,当计算UV坐标时,首先需要考虑一个负的偏移量(-0.5, -0.5),以将采样点从纹理的边缘调整到中心。这是因为在纹理空间中,像素是以整数为单位的,因此纹理的边界和像素中心的位置不完全一致。为了让纹理正确地映射到像素空间,需要做这个偏移。

然而,这并不是唯一的调整。还需要考虑到一个额外的虚拟像素行,这意味着需要再添加一个偏移量,补偿缺失的部分。因此,最终的偏移量应该是“偏移-0.5 Texel”后再加上“1 Texel”,这样能够确保采样坐标在纹理内部正确对齐。

在实际实现时,计算纹理的偏移坐标时,可以通过修改UV坐标来完成这一过程。首先将UV坐标乘以纹理的宽度,转化为Texel空间,然后再进行相应的偏移调整。这样做可以确保每个纹理块的采样不会出现错位,同时确保在双线性插值时颜色过渡顺畅。

另外,为了确保计算正确,纹理的宽度和高度也需要考虑到偏移的影响。例如,如果纹理的实际宽度是256像素,计算时应考虑去掉一些边缘部分(例如去除2个像素的边界),从而避免在插值过程中出现错误。

总之,通过这些偏移和调整,能够确保在渲染时,纹理的每个像素正确对齐,并且避免因不连续的插值而产生明显的接缝或视觉上的错误。
在这里插入图片描述

检查采样偏移

在调整纹理坐标时,首先需要处理一个“虚拟像素行”的问题,这个虚拟像素行出现在纹理的边界区域,影响纹理的正确映射。为了让UV坐标在纹理中正确对齐,尤其是在双线性插值时避免出现不连续的问题,需要进行一定的偏移。

目标是让原本的UV坐标 (0,0) 映射到纹理中的中心位置,而不是直接映射到纹理的左上角。经过计算后,这个偏移应该将原本的UV坐标调整到 (0.5, 0.5) 的位置,这样可以确保插值时的平滑过渡,并避免边界问题。实际操作中,这意味着要将纹理坐标向负方向偏移半个纹理像素(-0.5, -0.5),并且补偿虚拟的纹理行和列。

通过这种偏移,纹理坐标将正确对齐到纹理的像素中心,确保每次采样时都能从纹理的正确位置获取像素值。而且这还将确保在渲染时双线性插值能够正确地从相邻的纹理像素中计算出平滑的过渡值,避免产生明显的接缝。

虽然当前的代码实现可能没有完全验证其正确性,但通过这种调整,至少我们能够确保纹理的边界被正确处理并且采样时会按照预期的方式进行偏移。接下来,还需要进行调试和验证,确保这些变化在实际渲染中能够正确工作。

从实现的角度来看,使用 width - 2height - 2 来处理纹理的边界是合理的,考虑到纹理的边界需要避免被采样,这样可以确保纹理在绘制时的正确对齐和插值。

地面区块围裙

为了确保纹理在渲染时能够正确显示,我们需要为每个渲染的块添加一圈额外的像素,类似于“围裙”效应。这是因为在进行双线性插值时,我们需要保证每个纹理块的边界能够正确地采样到相邻纹理块的像素值。因此,在渲染时,我们需要调整渲染区域的大小,使得每个块的周围都包含额外的像素信息。

具体来说,当设置正交投影进行渲染时,实际的纹理块大小应该比原始尺寸大两个像素。这是因为渲染过程中需要包含这些额外的像素,来确保纹理之间的边界得到正确处理。换句话说,我们需要将原始的宽度增加两个像素,从而确保在渲染时能够覆盖到每个纹理块周围的邻接区域。

为了实现这一点,需要明确知道每个像素在物理空间中对应的大小,也就是“像素到米”的转换比例。这是为了确保在计算时,能够将这个额外的“围裙”像素正确地融入到渲染区域内。最终的渲染区域将包括原始的纹理块区域,以及周围额外的像素区域,从而确保渲染结果没有接缝,且能够平滑地过渡到相邻的纹理块。

这个操作涉及到一些精确的计算和调整,但它是确保渲染质量的关键一步,特别是在双线性插值和纹理映射的过程中。
在这里插入图片描述

黑板:确认围裙数学

我们正在处理一个尺寸为256x256的纹理,并且希望确保它在世界空间中正确地映射到地面上的某个区域。这个区域应该映射到一个尺寸为255x254的区域,因此需要确保纹理中的254x254的区域准确地映射到相应的地面区域。

具体来说,目标是确保纹理的254x254区域在世界坐标系中的大小与地面上的实际区域大小一致。在进行这种映射时,需要考虑“米到像素”的转换比例,即通过将米转换为像素来保证正确的比例和位置。

通过这种方式,能够确保纹理在世界空间中的显示精度,避免出现错位或不一致的现象。

考虑围裙

在调整了像素边界和纹理坐标后,我们发现原本明显的接缝问题已经解决了。这是因为通过减少两个像素并添加偏移量,成功地解决了接缝问题。通过仔细的数学计算和思考,最终成功地消除了大部分的接缝,看起来非常令人满意。特别是当我们预期问题并解决它时,这种感觉很棒。

不过,尽管明显的接缝已经消失,但我们还是不能完全确定某些地方是否仍然存在轻微的接缝,特别是在一些细节区域。虽然大多数的明显接缝已经消失,但仍然有一些区域可能看起来不像是由纹理采样引起的接缝,而是可能与艺术效果或其他因素相关。

接下来,地面生成的工作还没有完成。为了生成更好的地面,我们不能依赖完全随机的纹理分布,可能还需要进一步调整和优化地面生成的算法。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

地面区块的工作

在地面生成的过程中,我们发现了一些小问题,比如有一些区域没有被完全填充,这提醒了我们为什么完全随机生成可能会带来问题。尽管接缝问题已经基本解决,但地面生成依然需要进一步优化,特别是在纹理的填充和分布方面。虽然接缝问题得到了解决,但是某些区域是否仍然存在轻微的接缝仍然难以确认,尤其是当艺术效果与纹理之间的区别不明显时。

然而,系统性的纹理接缝问题已经解决,地面生成也更加平滑。尽管如此,目前仍然面临着另一个问题,那就是地面块没有正确排序,导致前景和背景的地面块在渲染时出现混乱,造成了一种视觉上的错乱效果。

随着纹理和像素的坐标逐渐正确对齐,渲染的效果也更接近我们预期的标准。这种调整使得图像渲染效果更接近OpenGL的像素和纹理标准,确保了渲染在硬件上看起来也能够一致。

目前,尽管地面生成已经取得了明显进展,但由于地面块没有正确排序,问题依旧存在。接下来,我们可能需要处理这一问题,并对当前的渲染效果做进一步的优化和调整。此外,还需要继续调整和优化生成算法,以解决地面块之间的视觉问题并提升整体的渲染质量。
在这里插入图片描述

在这里插入图片描述

放回调试相机

目前,地面块的生成已经能够按需进行,但在生成地面块时,会出现一个小的卡顿现象,这主要是因为地面块的生成过程存在过多的计算,比如填充地面块时使用了大量的随机分布。理论上,如果使用更合理的生成方式,所需的填充量会大大减少,从而减轻卡顿现象。

一个可能的解决方案是将地面块的填充过程移到另一个线程进行处理。现在已经有了线程系统,利用多线程可以让地面块的填充过程不再占用主线程的计算资源,从而避免卡顿对游戏体验的影响。虽然这一做法可能不一定是最佳方案,但它的确能够有效地减少卡顿现象。

另外,按需生成地面块的做法给了系统很大的灵活性和扩展性。由于地面块是动态生成的,可以根据需要随时调整生成方式,从而为未来的开发带来更多可能性。通过这种方式,可以为游戏世界中的每个区域生成独特的地面纹理,甚至能够利用底层几何信息实现更复杂的效果。

因此,尽管当前的地面生成系统还存在一些问题,但通过引入多线程处理和按需生成的方式,可以有效地优化性能,并为未来的游戏开发提供更大的创作空间。
在这里插入图片描述

在这里插入图片描述

多线程填充地面区块

在考虑使用线程时,需要思考如何实现线程的管理,特别是在处理地面块填充的过程中。如果将填充操作移到另一个线程执行,就能够减轻主线程的负担,避免游戏中的卡顿现象。不过,这也带来了线程管理上的一些挑战,比如如何确保线程安全,如何同步主线程与工作线程之间的数据交换,以及如何合理分配计算资源等问题。

此外,线程的引入可能还需要处理数据的共享与协调,确保地面块生成的结果不会因多线程而导致错误或冲突。虽然使用线程可以提高性能,但也需要谨慎考虑线程间的通信和同步方式,以避免出现性能瓶颈或错误。

总体来说,使用线程处理地面块填充是一个可行的方案,可以有效减少主线程的负担,提高游戏的流畅度,但必须仔细规划线程的使用方式和管理策略,确保它能够在不引入新的问题的前提下优化整体性能。

添加低优先级队列

为了优化地面块填充过程,可以考虑将任务分配到多个线程上。具体来说,可以将任务队列分为高优先级队列和低优先级队列,以此来管理不同类型的任务。高优先级队列中的任务会优先处理,而低优先级队列中的任务则在高优先级任务完成后才会执行。这种设计可以防止低优先级任务阻塞高优先级任务,从而避免性能下降。

在实现过程中,首先需要创建高优先级和低优先级队列,并且为每个队列分配不同的线程数。例如,可以为高优先级队列分配6个线程,为低优先级队列分配2个线程。线程会根据队列中的任务进行处理。为了保证线程池的高效运作,可以将队列初始化为零,并在后续任务执行时避免不必要的资源浪费。

为了更好地管理这些线程,系统会为每个线程分配一个逻辑索引并指向对应的队列。线程会从队列中拉取任务并执行,直到队列为空。任务完成后,线程会根据队列中的任务情况动态调整执行策略,保证高优先级任务优先得到处理。

这种线程管理方式能够使得游戏在进行地面块渲染时,既能保持流畅的画面效果,又能在后台处理较低优先级的任务,确保游戏的整体性能得到提升。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

问题:地面区块需要对齐

在处理地面块生成时,遇到一个问题就是渲染目标的对齐要求。具体来说,在创建地面块时,必须确保内存分配是对齐的,尤其是要对齐到16字节边界。这是由于渲染器加速的要求,所有内存分配都需要满足这个对齐条件。

目前,MakeEmptyBitmap 函数在分配内存时并没有保证对齐,它只是通过 PushSize 方法简单地分配了内存,但没有考虑到对齐问题。这在多线程渲染或者高效渲染的场景中可能会导致性能下降,甚至出现一些意料之外的问题。因此,必须修改内存分配逻辑,确保内存分配时会考虑到对齐要求。

为了修复这个问题,需要让 MakeEmptyBitmap 函数能够接受并处理内存对齐的参数,确保每次内存分配都满足16字节对齐要求。这是一个比较大的任务,可能需要一些时间来完成,因此需要权衡是否立即开始进行这一修改,或者先处理其他的优化和功能。
在这里插入图片描述

将地面区块工作添加到队列

在这个阶段,首先需要记录一个待办事项:地面块分配中的内存对齐问题。具体来说,目前的内存分配方法没有确保内存对齐到16字节边界,这可能在一些情况下没有问题,但不符合最佳实践。为了确保渲染器的性能,需要修改内存分配方式,使其符合16字节对齐的要求。

接下来,计划在地面块渲染的过程中,将 FillGroundChunk 的操作与工作队列结合。这意味着,需要让 FillGroundChunk 函数能够通过平台的工作队列调用。在这之前,函数内部已经完成了许多必要的操作,如内存分配和渲染缓冲区的初始化,问题主要在于如何将这些操作与工作队列的线程系统对接起来。

目前的任务是将 FillGroundChunk 函数集成到工作队列中,使得它可以作为任务被推送到队列中进行渲染处理。这个操作看似简单,但需要确保整个流程可以顺利地通过工作队列并在适当的线程中执行,这样才能保证渲染过程的高效和稳定。

任务中止,时间不够

在这个阶段,主要任务是将渲染工作集成到工作队列中,这要求扩展当前的临时内存管理功能,使其更加灵活和适应新的需求。为了将渲染工作放入队列中,首先需要确保临时内存系统能够支持这种新的操作。这可能涉及到对内存分配方式进行调整,以便在渲染过程中能够灵活地分配和管理内存。

此外,还需要修改渲染操作本身,使得它能够通过队列系统进行调度和执行。此时,工作队列需要能够处理新的渲染任务,并确保这些任务能够在正确的线程上执行,从而避免性能问题。这些更改将增强系统的灵活性,并为后续优化和扩展提供基础。

因对齐问题崩溃

在这个过程中,原本预期不对齐的地面块竟然意外地与16字节对齐,这是因为内存分配恰巧处于16字节边界上。原因在于,低优先级队列(LowPriorityQueue)被添加到了transient state结构中,而该结构位于内存 arena 中,与地面块的内存分配区域重叠。这样,由于结构体的布局,添加了一个8字节指针后,内存边界被推到了16字节对齐的位置。

实际上,因为所有内存都被8字节对齐(在64位模式下编译),所以有50%的机会地面块会自然对齐到16字节。这是一个偶然的巧合,虽然不符合预期,但由于对齐特性,地面块依然能正常渲染。

现在解决方案很简单:可以通过在结构体中再添加一个8字节的填充来确保地面块的对齐。这样,所有的地面块都将按照要求对齐,避免了潜在的内存对齐问题。

最终,这个问题的发生是因为对齐机制的细节和内存分配的微妙影响,虽然当时没有意识到,但偶然的巧合让系统依然运作良好。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
如果还内存边界没对齐
就会导致断言失败
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在 Ubuntu 或其他 Linux 系统上,虽然 GCC 和 Clang(常见的编译器)没有直接等价于 MSVC 的 /d1reportAllClassLayout 的内置选项,但有一些工具和方法可以实现类似的功能,即打印类、结构体的内存布局、成员偏移量和填充信息。以下是适用于 Ubuntu 的解决方案:


1. Pahole

  • 描述pahole 是一个强大的工具,来自 dwarves 软件包,用于分析编译后的二进制文件中的结构体和类布局。它可以显示成员的偏移量、大小和填充字节,非常接近 MSVC 的 /d1reportAllClassLayout 的输出。
  • 安装
    sudo apt update
    sudo apt install dwarves
    
  • 使用方法
    1. 编译你的代码并生成带调试信息的可执行文件(需要 -g 选项):
      g++ -g -o myprogram myprogram.cpp
      
    2. 使用 pahole 分析:
      pahole myprogram
      
      或者指定特定结构体:
      pahole -C StructName myprogram
      
  • 输出示例
    对于以下代码:
    struct Example {char a;int b;double c;
    };
    
    输出可能如下:
    struct Example {char               a;                  /*     0     1 *//* padding: 3 bytes */int                b;                  /*     4     4 */double             c;                  /*     8     8 *//* size: 16, align: 8 */
    };
    
  • 优点:详细显示填充和对齐信息,支持 C 和 C++。
  • 注意:需要调试信息(-g),否则无法分析。

xiaqiu@xz:~/test$ cat test.cpp
#include <iostream>
struct Example {char a;int b;double c;
};
int main(){Example e;
}xiaqiu@xz:~/test$g++ -g -o  test test.cpp
xiaqiu@xz:~/test$ pahole test
struct Example {char                       a;                    /*     0     1 *//* XXX 3 bytes hole, try to pack */int                        b;                    /*     4     4 */double                     c;                    /*     8     8 *//* size: 16, cachelines: 1, members: 3 *//* sum members: 13, holes: 1, sum holes: 3 *//* last cacheline: 16 bytes */
};
xiaqiu@xz:~/test$

我来为你详细解释 pahole 工具输出的注释及其作用。pahole 是一个用于分析 C/C++ 结构体布局的工具,主要用于检查内存对齐、填充(padding)和潜在的优化机会。你的输出是一个 struct Example 的内存布局分析,下面逐步解析每一部分:


输出内容

struct Example {char                       a;                    /*     0     1 *//* XXX 3 bytes hole, try to pack */int                        b;                    /*     4     4 */double                     c;                    /*     8     8 *//* size: 16, cachelines: 1, members: 3 *//* sum members: 13, holes: 1, sum holes: 3 *//* last cacheline: 16 bytes */
};
1. 成员变量及其注释

每一行描述一个结构体成员,包括它的类型、名称和内存布局信息:

  • char a; /* 0 1 */

    • a 是一个 char 类型,占用 1 字节。
    • 0 表示它的起始偏移量(offset),即从结构体开头第 0 字节开始。
    • 1 表示它的大小(size),即 1 字节。
    • 所以 a 占用 [0, 1) 的内存范围。
  • int b; /* 4 4 */

    • b 是一个 int 类型,假设是 4 字节(常见于 32 位或 64 位系统)。
    • 4 表示它的起始偏移量,从第 4 字节开始。
    • 4 表示它的大小,即 4 字节。
    • 所以 b 占用 [4, 8) 的内存范围。
  • double c; /* 8 8 */

    • c 是一个 double 类型,通常占用 8 字节。
    • 8 表示它的起始偏移量,从第 8 字节开始。
    • 8 表示它的大小,即 8 字节。
    • 所以 c 占用 [8, 16) 的内存范围。
2. /* XXX 3 bytes hole, try to pack */
  • 这是 pahole 的一个关键注释,指出结构体中存在内存“洞”(hole)。
  • 含义
    • char a(偏移 0,占用 1 字节)和 int b(偏移 4,占用 4 字节)之间,有一段未使用的空间。
    • 由于内存对齐(alignment)的需要,int 类型通常要求按 4 字节边界对齐,因此编译器在 a 后插入了 3 字节的填充(padding),使 b 从偏移 4 开始。
    • 这个 3 字节的“洞”就是浪费的内存。
  • 建议
    • try to pack 提示开发者可以通过调整成员顺序或使用编译器选项(如 #pragma pack)来减少这种填充,优化内存使用。
3. 总结信息

输出末尾的注释提供了结构体的整体分析:

  • size: 16

    • 整个结构体的大小是 16 字节。
    • 这是因为最后一个成员 double c 结束于偏移 16,且结构体大小必须是对齐到最大成员(这里是 double,8 字节)的倍数。
  • cachelines: 1

    • 结构体占用 1 个缓存行(cache line)。在大多数现代处理器中,一个缓存行是 64 字节,而这里只有 16 字节,所以只需要 1 个缓存行。
  • members: 3

    • 结构体有 3 个成员:abc
  • sum members: 13, holes: 1, sum holes: 3

    • sum members: 13:所有成员实际占用的大小总和是 13 字节(char a 1 字节 + int b 4 字节 + double c 8 字节)。
    • holes: 1:结构体中有一个“洞”。
    • sum holes: 3:这个“洞”的大小是 3 字节(前面提到的填充)。
  • last cacheline: 16 bytes

    • 最后一个缓存行的大小是 16 字节,因为整个结构体只占用 16 字节,仍在第一个缓存行内。

2. GCC 的 -fdump-class-hierarchy

  • 描述:GCC 提供了一个选项 -fdump-class-hierarchy,可以输出类的层次结构和内存布局信息。虽然它主要用于类继承关系的分析,但也能提供一些布局细节。
  • 使用方法
    g++ -fdump-class-hierarchy -o myprogram myprogram.cpp
    
    编译后,会生成一个 .class 文件(例如 myprogram.cpp.class),包含布局信息。
  • 输出示例
    对于带有虚函数的类:
    class A {int x;virtual void foo() {}
    };
    
    输出可能包含虚表指针和成员的偏移量:
    Vtable for A
    A::_ZTV1A: 2u entries
    0u   (int (*)(...))0
    8u   A::fooClass Asize=16 align=8base size=12 base align=8
    A (0x...) definition0 | vptr4 | int x
    
  • 局限性:信息不如 pahole 直观,更多关注虚表和继承,而非填充细节。

3. Clang 的 -Xclang -fdump-record-layouts

  • 描述:Clang 提供了一个选项 -fdump-record-layouts,可以输出结构体和类的内存布局,类似于 MSVC 的功能。
  • 安装
    sudo apt install clang
    
  • 使用方法
    clang++ -Xclang -fdump-record-layouts -o myprogram myprogram.cpp
    
  • 输出示例
    对于:
    struct Example {char a;int b;double c;
    };
    
    输出可能如下:
    *** Dumping AST Record Layout0 | struct Example0 |   char a1 |   (padding 3 bytes)4 |   int b8 |   double c| [sizeof=16, align=8]
    
  • 优点:直接内置于 Clang,输出清晰,接近 MSVC 的风格。
  • 注意:需要使用 Clang 编译器,而不是 GCC。

4. GDB 配合手动分析

  • 描述:使用 GDB(GNU Debugger)可以手动检查结构体和类的内存布局,虽然不如自动化工具方便,但适合动态调试。
  • 安装(通常默认已安装):
    sudo apt install gdb
    
  • 使用方法
    1. 编译带调试信息:
      g++ -g -o myprogram myprogram.cpp
      
    2. 运行 GDB:
      gdb ./myprogram
      
    3. 在断点处检查:
      (gdb) break main
      (gdb) run
      (gdb) ptype struct Example
      (gdb) print &((struct Example*)0)->a
      (gdb) print &((struct Example*)0)->b
      (gdb) print &((struct Example*)0)->c
      
  • 输出示例
    (gdb) ptype struct Example
    type = struct Example {char a;int b;double c;
    }
    (gdb) print &((struct Example*)0)->b
    $1 = (int *) 0x4
    
  • 优点:灵活,可动态分析运行时的内存。
  • 局限性:需要手动操作,不如自动化工具直观。

5. 自定义脚本(使用 sizeofoffsetof

  • 描述:通过编写 C/C++ 代码,使用标准宏 sizeofoffsetof 来手动打印布局。
  • 示例代码
    #include <stdio.h>
    #include <stddef.h>struct Example {char a;int b;double c;
    };int main() {printf("Size of Example: %zu\n", sizeof(struct Example));printf("Offset of a: %zu\n", offsetof(struct Example, a));printf("Offset of b: %zu\n", offsetof(struct Example, b));printf("Offset of c: %zu\n", offsetof(struct Example, c));return 0;
    }
    
  • 输出
    Size of Example: 16
    Offset of a: 0
    Offset of b: 4
    Offset of c: 8
    
  • 优点:简单,跨平台。
  • 局限性:需要手动编写代码,无法直接分析所有类。

推荐选择

  • 最佳工具:在 Ubuntu 上,推荐使用 pahole,因为它功能强大、输出详细,且专门设计用于内存布局分析,与 /d1reportAllClassLayout 的全面性最为接近。
  • 次选:如果使用 Clang,可以用 -Xclang -fdump-record-layouts,输出格式更接近 MSVC。
  • 调试场景:GDB 适合运行时分析。

如果你有具体的代码想分析,可以提供给我,我可以用这些工具帮你模拟输出!

地面区块每次上楼再下来时渲染效果不同。在 Z 排序之前,你是否计划修复这个问题?

当我们上下楼时,环境的渲染不会发生变化,因为它们在世界中的坐标是固定的,所以每次回来时,区域的表现都是相同的。这样设计是为了确保环境看起来是熟悉的。这意味着我们不会随机生成场景,而是基于已设定的坐标点进行渲染,因此当我们返回到某个区域时,环境表现保持一致,这是我们希望达到的效果。我们目前的工作是将这些元素简单地放置在环境中,但最终我们将更加注重如何构建地面纹理,使其对玩家来说更具意义,并且符合所在区域的特点。例如,某些区域的纹理可能需要根据地理环境或文化背景来进行设计,使得每个区域的地面在视觉上具有独特性,进而增强玩家的沉浸感和区域的辨识度。

能否解决一下经常出现的问题:“这个直播不是做游戏的吗?”我已经厌倦了解释这个直播是关于什么的了

这个直播的目的并不是简单地讲解如何做游戏,而是专注于从零开始制作游戏引擎。虽然有很多直播教你如何在Unity中加载一个项目、拖放实体,这些操作相对简单,但这并不是我们关注的核心内容。这个直播的目标是教授如何构建像Unity这样的游戏引擎,了解游戏引擎是如何从零开始开发的,而不仅仅是使用现成的工具。理解这些知识非常重要,因为游戏引擎是游戏开发的基础。如果没有人能够构建这些引擎,我们就没有现有的像虚幻引擎、CryEngine、Source引擎或其他定制的游戏引擎

接下来,虽然我们会在完成引擎开发后做一些更接近于游戏制作的工作,可能会像使用商用引擎一样做一些游戏内容,但从零开始开发一个完整的游戏引擎是非常重要的技能。即使使用Unity或者虚幻引擎这样的工具来开发游戏,它们虽然方便,但也让开发者处于一个劣势,因为这些工具都是公开的,每个人都可以使用。所以,依赖这些工具的游戏往往缺乏技术上的创新和差异化,无法在技术层面脱颖而出。

学习如何构建引擎和开发基础技术对于那些真的想要深入游戏开发的人来说非常重要。这不仅关乎你能做出什么样的游戏,也关乎游戏开发的未来。掌握这些技能的人,未来才能参与到引擎的研发和技术进步中。那些不懂得如何做引擎的人,是无法做出有突破性的技术进展的。

如果你能掌握所有这些技能,那么你就可以成为一个引擎开发者。学习如何从零开始制作引擎,理解引擎开发的每一个细节,是成为一个优秀引擎开发者的基本要求。虽然每个程序员的风格不同,但任何一个有经验的引擎开发者都能轻松理解和实现这些知识点。所以,掌握这些基本技能对任何想成为引擎开发人员的人来说都是必不可少的。

你是如何决定哪些工作被加入队列的,哪些是高优先级,哪些是低优先级的?

在这个设计中,优先级的划分并不是基于高优先级和低优先级的对比,而是根据计算需求的时效性来决定。任何需要在当前帧内完成计算的任务都会被视为高优先级,而那些可以跨多帧进行计算的任务则会被分配为低优先级。这种方法的核心思想是,通过合理安排任务的计算时间,确保关键的计算能够及时完成,而那些不需要立即完成的任务可以延后处理,从而优化整体性能和计算效率。

你打算如何实现与 Yangtian Li 创建的无缝瓷砖?

我们正在使用杨欣·李创作的艺术作品,通过将这些作品拼接成无缝的瓦片来创建环境。她提供了艺术素材,我们正在将这些素材进行随机铺设,形成无缝的效果。然而,目前我们还没有进行深入的工作来将这些素材转化为更有用的内容,所做的只是简单地将它们随机放置在场景中,这样做并不具备实际意义。

在我们最终实现世界构建之前,暂时还没有使用这些素材进行更逻辑化的创作。到目前为止,这些操作还没有涉及到面向对象的设计模式,更多的是在进行初步的测试和实验。因此,虽然目前的工作看起来比较简单,但未来会更加注重如何通过有逻辑的方式来利用这些素材创建更具意义的内容。

我猜测黄色/青色/品红色的方块是用来调试的。它们代表什么?

黄色、青色和紫色的方框用于调试,帮助我们更好地理解当前的游戏视图和模拟区域。在镜头缩小时,我们可以看到这些方框的作用。黄色方框代表玩家在屏幕上能看到的区域;青色方框表示模拟的边界,所有的内容都在这个区域内进行模拟,即使是屏幕外的东西,仍然会被计算和模拟。例如,屏幕外的物体仍然会按预定的规则移动,直到它们进入视野范围。紫色方框则是一个额外的区域,表示该区域内的物体在移动时可能会与屏幕内的内容发生碰撞,但在这个区域内的物体并不会主动参与模拟。

这些方框目前主要用于调试,以确保当物体超出屏幕时,依然能够正确地被模拟,并且与屏幕内的内容发生交互。这对于像AI角色的行走行为或其他动态元素的计算非常重要,确保它们在视野外也能被正确处理。未来,当我们开始进入游戏的具体实现阶段,而不仅仅是引擎的工作时,这些区域将会变得更加重要。

看起来地面区块只有在它们实际上可见时才会生成,而不是提前生成。当你将地面区块放入低优先级队列时,空的地面区块会在生成的过程中短暂可见 1 到 2 帧吗?

目前地面检查只会在它们即将可见时才会生成,而不是提前生成。当地面检查进入低分辨率时,空地会在生成过程中有一到两帧是可见的。为了避免这种情况,一旦地面检查生成完成,我们希望能够提前一点生成地面块。为此,我们需要了解典型的事件视界,即何时生成地面块最为合适,然后稍微提前排队生成它们。这其实并不复杂,我们只需要扩大生成区域的范围,确保在它们真正可见之前,地面块已经准备好。

你认为在最终阶段,像 Jon Blow 这样的人会有兴趣用 Handmade Hero 引擎制作一个游戏,并以类似的方式看看游戏设计阶段如何推动引擎的发展吗?我知道在皮克斯,他们通过艺术家与工程师之间的非计划性合作获得了新鲜且有趣的成果

有一个问题是关于在 Handmade Hero 的游戏引擎上是否会有人像某位知名开发者一样,利用该引擎制作游戏,并以类似的方式展示游戏设计阶段如何影响引擎的演变。对此的回答是,不确定,最好直接向他本人询问。


http://www.ppmy.cn/server/172448.html

相关文章

HIVE中的分组聚合语句

GROUPING SETS使用 grouping sets是一种将多个group by 逻辑写在一个sql语句中的便利写法。 --GROUP BY a, b GROUPING SETS ((a,b)) hive (default)> 1. SELECT order_date,user_id,count(*) FROM ds_hive.ch6_t_order GROUP BY order_date,user_id GROUPING SETS ((orde…

5个GitHub热点开源项目!!

1.自托管 Moonlight 游戏串流服务&#xff1a;Sunshine 主语言&#xff1a;C&#xff0c;Star&#xff1a;14.4k&#xff0c;周增长&#xff1a;500 这是一个自托管的 Moonlight 游戏串流服务器端项目&#xff0c;支持所有 Moonlight 客户端。用户可以在自己电脑上搭建一个游戏…

什么是 Prompt?——一篇详细的介绍

在人工智能&#xff08;AI&#xff09;和自然语言处理&#xff08;NLP&#xff09;的领域&#xff0c;Prompt&#xff08;提示语&#xff09;是与 AI 模型进行交互的核心工具之一。它是一个人类输入的指令&#xff0c;目的是引导 AI 模型执行特定的任务或生成相应的内容。随着生…

Android 获取jks的SHA1值:java.io.IOException: Invalid keystore format

命令生成 keytool -list -v -keystore 全路径.jks -alias 别名 -storepass 密码 -keypass 密码 1、遇到 的问题&#xff1a; 通过快捷键 ‘win r’ 启动的小黑框运行上面的命令会出现下面这个错误keytool 错误: java.io.IOException: Invalid keystore format 2、解决问题 …

java后端开发day24--阶段项目(一)

&#xff08;以下内容全部来自上述课程&#xff09; GUI&#xff1a;Graphical User Interface 图形用户接口&#xff0c;采取图形化的方式显示操作界面 分为两套体系&#xff1a;AWT包&#xff08;有兼容问题&#xff09;和Swing包&#xff08;常用&#xff09; 拼图小游戏…

Hue UI展示中文

个人博客地址&#xff1a;Hue UI展示中文 | 一张假钞的真实世界 如果使用开发分支代码如master分支&#xff09;编译安装&#xff0c;需要自己编译语言文件。例如Hue安装目录为“/opt/hue”&#xff0c;则安装后执行以下命令&#xff1a; $ cd /opt/hue $ make locales 如果…

千峰React:Hooks(上)

什么是Hooks ref引用值 普通变量的改变一般是不好触发函数组件的渲染的&#xff0c;如果想让一般的数据也可以得到状态的保存&#xff0c;可以使用ref import { useState ,useRef} from reactfunction App() {const [count, setCount] useState(0)let num useRef(0)const h…

【硬核拆解】DeepSeek开源周五连击:中国AI底层技术的“破壁之战”

​ 大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 当全球AI竞赛聚焦于大模型军备竞赛时&a…