仓库 :https://gitee.com/mrxiao_com/2d_game
回顾之前的内容
在这个程序中,目标是通过手动编写代码来从头开始制作一个完整的游戏。整个过程不使用任何库或现成的游戏引擎,这样做的目的是为了能够全面了解游戏执行的每一个细节。开发过程中,关注的不仅是游戏的最终效果,还包括理解计算机在运行时所做的每一个操作。
目前,项目已经进入了较为深入的阶段,已经完成了一个构建,并设计了一个瓦片地图存储系统。这个系统已经开始得到一些研究,开发者正在努力理解如何设计引擎的架构,以便能够开始实现游戏的具体功能。尽管目前系统还没有完全成型,但开发者已经在此过程中积累了许多有价值的经验。
在开发过程中,有一项重要的编程实践被强调,那就是在进行设计之前应该先进行实验和探索。这是一种非常有效的方式,通过探索来产生设计,而不是先做出设计再试图强迫其实现。当事先没有足够的了解时,强行设计可能会导致不理想的结果。
在目前的阶段,开发者对现有的平铺地图系统已经非常满意,接下来会将其打包起来,准备进入开发的下一个阶段。除了城镇相关的部分,游戏的其他内容也在考虑之中,这些内容也是完成游戏所不可或缺的部分。
加载 BMP 文件的介绍
接下来将开始使用之前下载的测试资源,目标是将它们加载到屏幕上并显示出来。首先会写一个非常简单的小系统,用来加载位图文件并将其显示在屏幕上。这个过程将帮助开始处理游戏的显示部分,但它还不是真正的渲染,因为渲染是一个庞大而复杂的过程,涉及到大量的数学运算和技术细节,需要逐步讲解。在当前阶段,重点是通过简单的矩形显示内容,为后续的更复杂渲染工作打下基础。
随着项目进展,工程架构将会逐渐完善,而此时的矩形显示将为之后的工作提供基础,帮助更好地理解和决定如何组织和管理其他图形和资产。为了在家中跟进这一部分工作,确保已经下载了测试资产包。未来,如果有新的资产发布,会及时通知,通常这些更新不频繁,但会在重要的阶段进行资产批量更新。
在 BMP 加载器之前处理玩家上下"楼梯"的动作
接下来的目标是处理玩家如何在不同层次之间移动,特别是在有上下楼梯或门的场景中。为了实现这一点,设计了两个时间平面,分别表示上层和平层,以便能够让玩家在这些平面之间上下移动。这将通过引入一个z分量来实现,允许多个平面堆叠在一起,并且处理这些平面时需要一个通用的系统。
要实现这一功能,首先需要对玩家代码进行修改。目前的玩家代码只负责检查玩家是否能够移动到指定的位置,并将其移动到正确的位置,但没有处理玩家在不同平面之间切换的逻辑。为了实现这一点,计划引入一个新的函数来检测玩家是否移动到了新的平面,即新的城镇或不同的地砖。当玩家位置发生变化时,通过一个谓词函数来检查玩家是否仍然在原来的地砖上。如果玩家移动到了新的地砖,就会触发相应的操作。
目前,正在使用一种基本的检查方法,比较玩家的当前位置与目标位置的不同,以便确定是否需要做出响应。未来,随着玩家移动逻辑的进一步开发,将会采用更正式的方式处理这一问题,包括处理玩家进入不同地砖后的具体行为。
上下文切换成本
在开发过程中,有时会编写一些临时的"胶水代码",其主要目的是将不同系统或模块连接起来,以便于进行测试和实验。这些代码通常不是最终代码,也不会出现在最终的项目中。其存在的目的是让开发者能够快速推进实验,而不必过多关注如何精确地设计或实现代码。
为了避免浪费时间,开发者倾向于在处理问题时,将注意力集中在当前的任务上,而不是在不合时宜的时间里纠结于代码的细节或质量问题。尽管某些代码可能是低质量的或者未完全实现,但只要它能推动当前任务的进展,且开发者清楚其临时性和不必要性,就可以继续使用。这种方式有助于集中精力,避免频繁切换任务,因为频繁的上下文切换会浪费大脑的处理能力,就像虚拟内存系统那样,会导致系统的效率降低。
因此,开发者会优先关注当前任务,尽可能提高集中度,而不花太多时间去考虑细节或完美代码的实现,特别是在暂时不必要的情况下。最终,这些临时代码会被替换掉,只要能达到当前的工作目标,其他问题就会暂时搁置。
实现上下楼梯的动作
在开发过程中,需要处理玩家的移动和位置更新。为了确保玩家可以顺利地移动到新的位置,代码会检查玩家的当前位置,并根据该位置的不同类型值来决定玩家的行为。如果玩家的当前位置是门(例如,值为3或4),他们会被引导到上方或下方的不同平面。
为了实现这一点,代码会首先获取玩家的当前位置,并通过一个函数获取该位置的瓦片值。然后,根据瓦片值的不同,采取不同的行动。如果玩家移动到一个门上,程序会根据设定的规则引导玩家向上或向下移动。对于门和其他空白区域,代码会检查是否有合法的移动路径,并执行相应的操作。
此外,代码中的一些功能,例如获取瓦片值的函数,被设计成"代理"函数,用来简化重复调用的过程。这样开发者不必每次都手动处理复杂的细节,而是通过更简化的调用来实现相同的功能。
开发过程中,临时代码(例如"懒代码")是不可避免的,这些代码并不追求完美,而是帮助开发者快速推进任务。虽然这种方式有效,但开发者也意识到,频繁的上下文切换和处理多个任务时的困难,会使得集中精力变得更为挑战。最终,所有临时性或"懒"代码会在项目成熟时被替换或优化。
瓦片地图系统与瓦片地图定位系统的区别
在开发过程中,有一些代码涉及到定位和几何问题,但它们与瓦片地图本身无关。开发者意识到这些部分应该从当前的城市地图系统中剥离出来,可能应该被移到一个更适合的几何文件中。定位和几何细节本质上并不直接影响城市的瓦片地图,而更关乎瓦片的实际大小等信息。
因此,开发者计划将这些与瓦片地图无关的部分标记出来,并移到适当的位置。这些定位相关的部分,虽然在某些情况下可能看似和瓦片地图有关,但其实它们与地图的存储、瓦片布局并无直接关联,反而可能是更广泛的几何或定位问题。
接下来,开发者回到项目的核心,继续处理瓦片地图的位置,并计划确定如何处理这两个位置:位置A和位置B。这些位置将有助于进一步推动开发进程,特别是在处理城市地图系统的细节时。
cpp
// 检查玩家当前位置是否与新的目标位置在同一块瓦片上
if (!AreOnSameTile(&GameState->PlayerP, &NewPlayerP)) {
// 获取新目标位置的瓦片值
uint32 NewTileValue = GetTileValue(TileMap, NewPlayerP);
// 如果瓦片值为 3,则表示当前瓦片有向上的楼梯
// 玩家需要移动到上一层(AbsTileZ 增加)
if (NewTileValue == 3) {
++NewPlayerP.AbsTileZ;
}
// 如果瓦片值为 4,则表示当前瓦片有向下的楼梯
// 玩家需要移动到下一层(AbsTileZ 减少)
else if (NewTileValue == 4) {
--NewPlayerP.AbsTileZ;
}
}
注释说明:
-
AreOnSameTile
的作用:- 用于检查玩家当前位置和目标位置是否在同一个瓦片上。
- 如果玩家没有离开当前瓦片,则不需要进一步处理移动逻辑。
-
GetTileValue
的作用:- 返回目标位置瓦片的值。
- 不同的瓦片值表示不同的功能或场景,例如楼梯、障碍等。
-
瓦片值的含义:
3
表示该瓦片是向上的楼梯。4
表示该瓦片是向下的楼梯。
-
更新玩家的层级(
AbsTileZ
):- 如果瓦片值为
3
,玩家沿楼梯向上,层级增加。 - 如果瓦片值为
4
,玩家沿楼梯向下,层级减少。
- 如果瓦片值为
整体逻辑:
此代码实现了一个楼层切换机制,玩家移动到楼梯瓦片时,可以根据楼梯的方向(向上或向下)更新其所在的楼层,确保玩家能够跨层移动。
修复生成代码
1. 瓦片是否相同
- 当物体位于相同瓦片上时,讨论如何判断瓦片数据是否不同。这可能是为了避免在逻辑中忽略某些关键信息(例如瓦片属性差异)。
- 在同一瓦片上,所有瓦片的部分数据通常是相同的,除非明确标记了不同属性。因此,判断这些差异是为了逻辑严谨。
2. 房间布局与门的生成逻辑
- 房间的门在生成时,需要考虑合理性:
- 一个房间最多应该有两个出口(一个是进入的门,另一个是离开的门)。
- 当前程序逻辑中,门的生成过程可能出错,导致过多的门被添加到房间中。
3. 逻辑的主要问题与修复方法
问题描述:
- 门的重复切换 :
- 如果存在上下的门(
door up
或door down
),程序在处理门的状态时,会多次切换其状态,导致门无限制地添加。
- 如果存在上下的门(
- 生成门的条件控制 :
- 门的生成过程未明确限制条件,比如没有明确判断当前是否已经存在门。
修复步骤:
-
避免重复添加门:
- 在生成门时,检查当前是否已存在
door up
或door down
状态。 - 如果已存在,则不再生成新门。
- 在生成门时,检查当前是否已存在
-
记住门的生成状态:
- 使用标志位(如
createdVerticalDoor
)记录当前是否生成了门。 - 如果生成了门,再进入下一房间时对其状态进行切换;否则保持原样。
- 使用标志位(如
-
修正门与房间的逻辑关系:
- 如果当前房间有
door up
或door down
,需要保证下一房间的门与之对应。 - 例如,当前房间的
door up
对应下一房间的door down
。
- 如果当前房间有
4. 门切换逻辑优化
- 在门的切换逻辑中,明确区分
z门
(上下门)和其他类型的门:- 如果当前生成了
z门
,下一步需要生成其反向门。 - 如果当前未生成
z门
,则无需考虑反向切换,只需保持当前门状态。
- 如果当前生成了
5. 总结改进后的核心逻辑
- 检查现有状态:在生成门前,检查当前状态是否允许生成新门。
- 设置状态标志 :使用标志位(如
createdVerticalDoor
)记录门的生成情况。 - 优化切换过程:只在需要切换状态时执行逻辑,避免不必要的重复操作。
- 保持合理性:确保每个房间有合理数量的门,并且与房间布局一致。
实现效果
修复后,程序能够:
- 正确判断是否生成新门。
- 避免门的重复生成问题。
- 维持房间布局的逻辑合理性,实现动态楼层切换与房间连通性。
实现完成,工作中的瓦片地图系统
-
瓷砖地图生成与楼梯设计
地图的设计和生成由一系列随机决定的因素控制,例如是否生成楼梯(Z门)或其他门的方向。随机数表用于生成门的选择,每次循环会检查当前楼层是否有楼梯,并且根据条件决定是否生成向上的或向下的楼梯。地图的绘制包括墙壁和门的判断,通过这些判断确定不同的瓷砖类型(例如墙壁、可行走区域)。
-
楼梯与门的交替生成
在每轮迭代中,生成不同类型的门(右侧、顶部或楼梯)。如果生成了楼梯,那么下一次迭代会调整楼梯的方向。如果没有生成楼梯,则确保两个楼梯的状态都关闭。特别是当随机选择为生成楼梯时,会根据当前楼层的状态切换楼梯的方向,可能是从上到下,或者反之。
-
地牢的探索与反思
游戏环境被描述为地牢,玩家可能会在其中探索,虽然此时地牢内没有具体的战利品或路径。尽管如此,地图生成的过程本身就是游戏设计的一部分,且逐步推进。随着设计的推进,开发者认为已经达到了一个相当不错的地牢地图。
-
未来的发展方向
一旦地图的生成完成,接下来的工作是使得游戏具备更多功能,例如玩家的运动与碰撞检测。这是为了使游戏变得可玩并为未来的开发奠定基础。此外,还考虑了图形显示的改进,尽管当前的地图显示是基本的,但未来会通过加载位图来提升视觉效果。
-
关于开发过程的探索
在开发过程中,重点在于逐步改进和修改已有的设计。开发者表达了对当前工作进展的满意,并且承认随着时间的推移,可能会需要做进一步的修改和调整。这些修改的目标是使得游戏更具可玩性和吸引力。
-
逐步推进开发目标
在实现基础功能后,开发者计划继续探索更复杂的功能,比如处理玩家的运动、碰撞检测以及游戏中的动画效果。更长远的目标是提高游戏的图形表现,使其在视觉上更具吸引力。
-
实现和改进
随着游戏逐步开发,开发者认识到,接下来的任务是解决如何在屏幕上呈现合理的图形,确保地图和游戏元素的表现是合理的。为了做到这一点,开发者考虑了加载位图和改进图形代码的需求。
-
未来方向与图形优化
接下来,开发者计划通过加载和优化位图来提升游戏的视觉效果,使得游戏在实际发布时能够提供更高质量的图形表现。这包括替换玩家运动代码以使其更加真实,以及在开发过程中逐步优化和改进游戏的各个方面。
总结:整个开发过程涉及多个步骤,从地牢地图的生成到玩家运动、碰撞检测的处理,再到图形优化。开发者通过逐步实验和调整,尝试找到合理的方案来实现一个既有趣又可玩的游戏。通过这一过程,开发者不断反思和改进设计,目标是最终实现一个具备完整功能的游戏。
加载 BMP 文件
今天的目标是把一张地图放到屏幕上。目标很简单,就是从一个文件中加载位图图像并显示在屏幕上,这为之后的原型设计提供基础。
需要加载一个位图文件作为示例。例如,可以选择一个测试背景,它包含一些树、蘑菇,以及草地背景。这是一个很合适的例子,能够帮助演示位图加载的基础工作流程。
该背景图像的分辨率为 1024x576。这需要与窗口大小对齐。在代码部分,可以根据图像尺寸和显示窗口的具体设置,完成加载和显示过程。
总结来说,重点是实现位图从文件加载并渲染到屏幕上的功能,为后续开发提供基础支持。
讨论游戏屏幕分辨率
-
屏幕分辨率目标:
- 目标分辨率是 1920x1080(典型的高清分辨率)。
- 为了便于处理并减少计算负担,考虑以一半的分辨率运行(960x540)。
-
调整为 2 的幂:
- 许多图形处理单元(GPU)偏好纹理大小为 2 的幂(如 1024x1024 或 2048x2048),因此在设计纹理时通常会考虑这些尺寸。
- 对于 1920 宽的屏幕,接近的 2 的幂是 2048,但这会浪费 128 像素。
- 对于 1080 高的屏幕,接近的 2 的幂是 1024 或 2048,但 2048 高浪费太多空间,因此选择 1024 并为顶部和底部添加 128 的缓冲带。
-
缓冲和屏幕抖动:
- 为支持屏幕抖动和溢出效果,增加了缓冲区域,比如在宽度上左右各 64 像素,在高度上上下各 36 像素。
-
内存利用效率:
- 选择的纹理尺寸(如 1152 (1024+128) 高或 2048 宽)既能容纳整个屏幕,也能支持额外的缓冲区域,尽可能减少浪费。
-
设计逻辑:
- 这一选择背后的逻辑是将图像资源设计为稍大于目标屏幕尺寸,以便于 GPU 的处理和动画效果的实现。
- 尽管这种方法可能并非最优,但它在实现时相对简单且足够灵活。
实现加载器
我们需要加载一些图像,并将它们显示在屏幕上,以确保一切正常运行。目标是通过绘制矩形的方式,将图像内容渲染到屏幕上。然而,这里不是直接填充纯色,而是通过矩形将完整的图像数据呈现到屏幕上。
绘制矩形的核心思想是用实际的图像内容替代简单的颜色填充。最终目标是实现图像渲染的基本功能,使图像可以正确显示在指定位置的矩形区域内。
为了实现这一功能,需要编写代码来加载图像文件。这里的初步实现并不是最终工业级的,而是为了快速完成一个可以运行的版本。稍后会对这些功能进行更详细的优化和改进。
加载图像需要读取位图文件的数据。可以利用现有的文件读取功能,将整个文件内容读取到内存中。通过一个函数调用,这些数据将被存储在内存缓冲区中,供后续操作使用。
这个读取功能依赖于一个已经实现的读取整个文件的工具。它接受一个线程上下文和文件名作为参数,完成文件内容的加载。位图文件的数据会被完整地传入内存中,以便进一步处理。
最后一步是解析文件中的位图数据,确保这些数据能够以正确的方式用于屏幕上的渲染。虽然这里的代码仅展示了初步实现,但它为后续更复杂的图像处理和优化奠定了基础。
我们需要加载一些图像,并将它们显示在屏幕上,以确保一切正常运行。目标是通过绘制矩形的方式,将图像内容渲染到屏幕上。然而,这里不是直接填充纯色,而是通过矩形将完整的图像数据呈现到屏幕上。
绘制矩形的核心思想是用实际的图像内容替代简单的颜色填充。最终目标是实现图像渲染的基本功能,使图像可以正确显示在指定位置的矩形区域内。
为了实现这一功能,需要编写代码来加载图像文件。这里的初步实现并不是最终工业级的,而是为了快速完成一个可以运行的版本。稍后会对这些功能进行更详细的优化和改进。
加载图像需要读取位图文件的数据。可以利用现有的文件读取功能,将整个文件内容读取到内存中。通过一个函数调用,这些数据将被存储在内存缓冲区中,供后续操作使用。
这个读取功能依赖于一个已经实现的读取整个文件的工具。它接受一个线程上下文和文件名作为参数,完成文件内容的加载。位图文件的数据会被完整地传入内存中,以便进一步处理。
最后一步是解析文件中的位图数据,确保这些数据能够以正确的方式用于屏幕上的渲染。虽然这里的代码仅展示了初步实现,但它为后续更复杂的图像处理和优化奠定了基础。
快速学习如何读取文件格式
以下是关于读取文件格式的快速入门概述。
要读取一种文件格式,首先需要对该格式有一定了解。可以通过在网络上搜索文件格式的名称和相关文档来获取信息。通常,搜索"bmp file format"可以找到相关的技术细节和结构说明。
一旦找到合适的资源,可以根据文件组织结构来理解数据布局。例如,对于位图文件(BMP),一般会有如下结构:
- 文件头:包含文件的基本信息,例如文件大小和偏移量。
- 位图头:包含位图数据的具体信息,例如宽度、高度和色彩深度。
- 调色板(可选):存储颜色信息,通常仅在文件使用索引颜色模式时存在。
- 位图数据:图像的实际像素数据。
如果确定文件存储的是RGB数据而非索引颜色,就可以跳过调色板部分,仅处理文件头、位图头和位图数据。
BMP(Bitmap)文件格式是一种无压缩的图像文件格式,用于存储位图数字图像,尤其是在 Windows 操作系统中被广泛使用。以下是 BMP 文件格式的详细介绍:
BMP 文件结构
BMP 文件由多个部分组成,每个部分有特定的用途。以下是 BMP 文件的主要结构:
1. 文件头 (File Header)
- 大小:14 字节
- 描述:文件的基本信息,包括文件大小、文件类型等。
- 字段 :
- 文件标志 (
bfType
) :2 字节,固定为BM
(0x4D42),表示这是一个 BMP 文件。 - 文件大小 (
bfSize
):4 字节,文件总大小(以字节为单位)。 - 保留字段 (
bfReserved1
和bfReserved2
):各 2 字节,通常为 0。 - 位图数据偏移量 (
bfOffBits
):4 字节,从文件开头到位图数据开始的偏移量。
- 文件标志 (
2. 位图信息头 (Bitmap Info Header)
- 大小:40 字节(常见的版本);不同版本的 BMP 文件可能扩展。
- 描述:图像的详细信息,如宽度、高度、颜色深度等。
- 字段 :
- 结构大小 (
biSize
):4 字节,信息头的大小,通常为 40 字节。 - 图像宽度 (
biWidth
):4 字节,图像的宽度(以像素为单位)。 - 图像高度 (
biHeight
):4 字节,图像的高度(以像素为单位)。正值表示自下而上存储,负值表示自上而下存储。 - 颜色平面数 (
biPlanes
):2 字节,始终为 1。 - 位深度 (
biBitCount
):2 字节,每个像素的颜色位数(1、4、8、16、24、32)。 - 压缩方式 (
biCompression
) :4 字节,图像数据的压缩方法:- 0:BI_RGB(无压缩)。
- 1:BI_RLE8(8 位 RLE 压缩)。
- 2:BI_RLE4(4 位 RLE 压缩)。
- 3:BI_BITFIELDS(每像素指定颜色掩码)。
- 图像大小 (
biSizeImage
):4 字节,位图数据的大小(可能为 0,如果未压缩)。 - 水平分辨率 (
biXPelsPerMeter
):4 字节,水平分辨率(像素/米)。 - 垂直分辨率 (
biYPelsPerMeter
):4 字节,垂直分辨率(像素/米)。 - 颜色索引数量 (
biClrUsed
):4 字节,调色板中使用的颜色数(0 表示使用所有颜色)。 - 重要颜色索引数量 (
biClrImportant
):4 字节,重要颜色的数量(0 表示所有颜色都重要)。
- 结构大小 (
3. 调色板 (Color Table) (仅限 1、4、8 位色深的 BMP 文件)
- 大小:可变
- 描述:存储颜色索引,定义图像的调色板。
- 字段 :
- 每种颜色通常占用 4 字节:红色、绿色、蓝色和保留字段。
4. 像素数据 (Pixel Data)
- 大小:可变
- 描述:图像的实际像素数据,从左下角开始,逐行存储。每行的大小需要对齐到 4 字节的倍数(即填充字节)。
- 格式 :
- 如果是 24 位色,每个像素占用 3 字节(B、G、R 顺序)。
- 如果是 32 位色,每个像素占用 4 字节(B、G、R、A 顺序)。
BMP 文件示例
示例文件头和信息头 (24 位色无压缩)
字节偏移量 | 长度(字节) | 字段名称 | 示例值 | 描述 |
---|---|---|---|---|
0 | 2 | bfType |
BM (0x4D42) |
BMP 文件标志 |
2 | 4 | bfSize |
0x00036E |
文件大小 |
6 | 2 | bfReserved1 |
0x0000 |
保留字段 |
8 | 2 | bfReserved2 |
0x0000 |
保留字段 |
10 | 4 | bfOffBits |
0x36 |
位图数据偏移量 |
14 | 4 | biSize |
0x28 |
信息头大小 |
18 | 4 | biWidth |
0x00000200 |
图像宽度 |
22 | 4 | biHeight |
0x00000200 |
图像高度 |
26 | 2 | biPlanes |
0x01 |
颜色平面数 |
28 | 2 | biBitCount |
0x18 (24 位色) |
每像素位数 |
30 | 4 | biCompression |
0x00 (BI_RGB) |
压缩方式 |
34 | 4 | biSizeImage |
0x00000000 |
图像数据大小(未压缩可为 0) |
BMP 文件的特点
- 优点 :
- 结构简单,易于解析。
- 无压缩格式保留了图像的完整质量。
- 缺点 :
- 文件体积大(特别是高分辨率图像)。
- 不支持现代图像功能(如透明度或高效压缩)。
解析 BMP 文件时的注意事项
- 字节顺序:BMP 文件采用小端序存储数据。
- 像素对齐:每行像素的字节数需要是 4 的倍数,不足的部分需要填充字节。
- 支持的色深:1 位、4 位、8 位、16 位、24 位和 32 位。
实现加载功能
编写代码时,可以利用现有的文件读取工具来读取整个文件。例如,使用一个函数将文件完全加载到内存中。加载后,文件数据将以缓冲区的形式存储,可以进行解析。
加载步骤:
- 读取文件内容:通过调试工具或自定义的文件读取函数,将文件加载为内存缓冲区。
- 解析文件头和位图头:根据文件格式文档,读取文件的元数据,例如图像尺寸、偏移量等。
- 读取位图数据:跳转到文件的位图数据部分,将其解析为实际像素信息。
调试与验证
加载文件后,可以通过调试工具查看加载的数据,以验证文件格式是否正确解析。通过观察文件内容的前几字节,可以判断使用的具体文件格式版本。
测试案例:
- 使用预先保存的BMP文件测试加载功能。
- 确保程序能够正确读取文件头、位图头以及图像数据。
- 验证加载的数据是否符合预期的像素信息。
代码示例
在初始化阶段,调用加载函数并传入文件路径。以下是示例逻辑:
- 调用文件读取函数,将文件内容加载到内存。
- 检查文件头和位图头的格式,以确保解析的准确性。
- 使用调试工具输出加载的数据,用于验证文件内容。
通过这些步骤,可以快速实现一个基础的文件加载功能,为后续的图像处理或渲染奠定基础。
在内存中调试 BMP 文件
清理屏幕时涉及的对话内容包括对文件的读取过程、文件格式的解释以及调试步骤的详细说明。涉及的关键点包括:
-
文件读取和验证:
- 确认文件是否成功读取,检查文件大小和内容是否一致。
- 在调试过程中查看文件内容,看它的格式是否如预期的那样,包括 BMP 文件的标头、文件大小和偏移量。
-
文件格式解析:
- 解释 BMP 文件的结构,如文件头的前 2 个字节标识文件类型,文件大小的 4 个字节、偏移量等。
- 解释这些数据的意义,例如文件头的大小和具体的位图数据位置。
-
调试步骤:
- 在调试中检查文件内容,确保每一步都符合预期。
- 将文件内容从未压缩格式处理和读取,以便进行进一步分析和处理。
这些对话展示了如何使用调试工具来解析和理解文件内容,如何检查文件头和数据的各个部分,并确保在处理过程中每个步骤都是正确的。
处理 BMP 文件头
打包结构体以避免填充
我们遇到一个问题,这个问题之前讲过很多次。为了巩固这件事,可能有些人对这类事情不熟悉。当我们试图读取一个文件,查看其中的内容时,你会发现它看起来不像我们预期的那样。我们期望看到文件的大小和内容像预期的一样,但事实是,它们可能会看起来不正确。这是因为在布局数据时,C++ 并不一定会紧紧地打包这些结构体。换句话说,C++ 在安排内存时,不会按照数据的实际大小紧密排列,而是可能会跳过一些字节,把数据安排到一个边界上。即使我们在声明了结构后告诉编译器要紧紧包裹这些结构,编译器可能依然会使用默认的包装方式。这导致了我们在读取文件时可能会遇到数据错位的问题。
为了解决这个问题,我们可以使用 #pragma pack
指令来告诉编译器把结构体紧紧包裹起来。这个指令可以指定打包的水平,使得结构体中的字段以最紧凑的方式排列。然而,这样做的一个问题是我们无法确定是否在其他地方的代码中已经使用了这种打包方式。因此,为了确保我们不会影响到其他地方,编译器提供了一个push
和pop
的机制。使用 #pragma pack(push, 1)
可以把当前的打包设置推到堆栈上,并设置新的紧凑打包水平,然后在结束时使用 #pragma pack(pop)
恢复之前的打包设置。这样就可以在读取文件的过程中保持结构体数据的正确排列,从而避免数据错位问题。
通过这样做,我们可以确保读取的位图文件的实际数据大小、宽度和高度以及位深度都符合预期,从而正确处理文件中的位图数据。
处理 BMP 像素
在进行二进制文件解析时,我们了解到了一些关键的概念。这包括我们如何处理宽度和高度,数据的偏移量,以及如何确保内存中像素的正确对齐。我们知道了像素数据将会从位图偏移处读取,这个偏移量是相对于整个文件的起始位置的。这样,我们可以确保计算得到的像素指针能够正确地指向每个像素。
然而,存在一些问题,像是图像可能会出现剪切现象,颜色顺序可能不正确等。这些问题需要在调试和修复过程中逐一解决。下周我们将继续探讨如何将这些数据正确地展示到屏幕上,并解决这些问题。
总体来说,虽然我们现在已经基本理解了如何处理位图文件格式和内存布局,但仍有一些细节需要精确调整,特别是涉及到像素对齐和颜色顺序的问题。这是未来需要解决的重要挑战。
BitmapOffset
在位图文件格式中表示图像数据的偏移量,即从文件的开始位置到实际图像数据开始的字节数。它是文件头结构的一部分,用于指示图像数据在文件中的起始位置。
在 BitmapFileHeader
或 BitmapInfoHeader
结构中,BitmapOffset
是一个 uint32
类型的整数,它告诉解码程序从文件的开头开始读取多少个字节后才能开始解析位图图像数据。
具体作用
BitmapOffset
提供了从文件的开始位置到图像数据的起始位置的字节数。这对于读取二进制文件中的图像数据至关重要。- 它保证了解码程序从文件的正确位置开始解析图像数据,而不是从文件的开头开始读。
- 这个值通常会是文件头部分的大小加上图像信息头部分的大小。
举例
假设 BitmapOffset
值为 54
(字节数),则图像数据开始的字节是从文件的第 54 个字节开始。解析程序需要从文件的第 54 个字节开始读取图像数据并解码出来。
这个偏移量保证了解析程序能够正确地跳转到图像数据的位置,而不必从文件的开头逐个字节地进行读取,这大大提高了读取图像数据的效率。
我错过了你处理文件字节序的部分,还是位图格式始终相同?
就位图而言,我想说的是,我不确定它们是否可以在不同的系统上被保存。例如,如果他们在Mac上保存的位图,它们总是会是小端字节序的,因为现代计算机通常都使用小端字节序。因此,不论是在PC还是Mac上保存,位图文件都将保持一致。
对于其他格式,比如PSD文件,它们可能会包含不同的字节序(大端或者小端)。但对于位图而言,无论是在PowerPC、Mac 还是现代PC上,它们总是小端格式保存。这就意味着,不论在哪个系统上保存或加载位图,位图数据格式都不会改变。
你更喜欢 #pragma pack 还是 gcc 风格的注解?
可能是gcc喜欢注释,并且这种注释方式虽然看起来有点丑,但我并不介意。它更像是简单直接的表达方式,就像是包装一样,这种注释方式更直观。例如,gcc和某些工具也有类似的属性。尽管如此,这通常不是一个大问题,因为这并不是经常需要的功能,也就是说,它通常不是那么重要。
在将多个参数放入一个结构体之前,你对函数参数数量的阈值是什么?
我没有一个具体的阈值来决定是否将所有函数参数放入一个单一的结构中。这主要取决于是否有很多人会频繁地传递同样的一组数据。 如果一组内容被多个函数频繁使用,我会将它们整合到一个结构中,以便更简洁地管理代码。然而,如果一个函数只包含一组独特的数据,即使是多个参数,我不介意把它们保持为独立的参数。关键在于这些数据是否能够被整合在一起,允许更高效的代码结构和管理。
为什么不使用函数模板,将原始字节读取到任何结构体中,一次读取一个基本类型,而不是使用编译器注解?
该解释围绕一种稀疏地图存储的概念,特别是在游戏引擎中的基于网格的世界中的应用。以下是关键点的中文翻译:
-
两级数组方案:地图组织成一个三维数组,其中:
- Z轴表示世界中的层级或深度。
- Y轴表示网格中的垂直层。
- X轴表示横向网格中的层。
-
稀疏存储:而不是存储整个地图的每个网格内的每个层(这将非常低效),引擎使用一个稀疏方案:
- 只存储实际使用的网格。
- 未使用的区域标记为零,表示不存在任何网格。
- 这显著减少了内存占用,因为只存储了地图中的活跃部分,而不是整个世界。
-
网格:网格的概念允许将一组网格以紧凑的、可管理的格式存储:
- 每个网格是一个平面的密集的二维数组(16x16网格),存储实际的数据。
- 这个平面数组只在稀疏的三维地图中存储必要的部分。
- 网格通过其坐标(x, y, z)进行索引,这些坐标表示它们在地图中的位置,使得引擎能够仅在需要时获取必要的数据。
-
效率:稀疏地图允许通过仅存储实际使用的世界局部,来高效地使用内存。这样可以避免存储大量空白或未使用的数据,否则在一个完全密集的地图中必须存在。
-
代码实现:
- 代码涉及从文件中读取所需的网格,并在需要时动态重建它们到活跃的地图中。
- 使用基本的数据类型来读取特定类型的数据(如头部或指针),允许引擎动态拼接地图,而不是在游戏开始时存储所有可能的数据。
- 这种方法减少了游戏过程中的计算开销,因为它避免了处理不必要的数据,优化了内存使用和处理效率。
这种方法提供了一种灵活且高效的处理大型游戏世界的方式,确保了在任何给定时间只使用和存储所需的内容。
你能解释一下如何处理平台特定数据吗?
在处理内存时,当读取文件时,我们会避免将文件中的所有数据都加载到内存池中,这样做会浪费内存。相反,我们采用资源流的概念,使得仅在需要时,按需读取文件。这种方法有助于优化内存使用,因为我们只会读取并解压缩那些确实需要的部分,并且把它们直接放入合适的位置。
这种方法最大限度地减少了加载时的内存开销。通过预先处理并将数据转换为一个易于流式处理的格式,我们能够尽可能快速地加载资源,即使数据已经被压缩。这个过程包括仅从文件中读取、解压缩和存储需要的数据块,从而避免了加载时处理不必要的文件内容。这些处理都在一个临时的缓冲区中进行,以便反复使用,从而最大限度地提高资源的加载效率。
在设计平台特定的行为时,我们通常会将平台特定的数据存储在线程环境的一个额外空间中。这些数据在传递给游戏时,会被合并到通用的线程上下文中。因此,当游戏代码回调时,我们可以从这个合并的结构中提取所需的数据。这种方法能够最小化平台数据的传递次数,从而减少代码中的往返调用,并提高代码的效率。
实现一个通用稀疏数组来解决瓦片地图问题会很容易吗(可能通过重载)?
在实现一个通用的稀疏数组来解决这个问题时,可能会比较复杂。使用一个哈希表可能是更合适的选择,因为它提供了更快的索引和访问速度,而稀疏数组可能会显得效率较低,并且需要更多的内存操作。一个哈希表能够更好地处理动态数据的插入和删除,不像稀疏数组那样必须预先分配所有可能的索引空间。
虽然稀疏数组可以用来存储稀疏的数据,但在大多数情况下,这种方法可能会慢于使用哈希表,因为它要求不断地进行内存访问和索引计算。而且,稀疏数组的通用性可能不是必需的,因为它需要一个通用的哈希函数,这可能不是最适合特定需求的。
因此,基于当前情况,我们可能会选择更具体的解决方案,如使用四叉树或类似的数据结构。这些方法虽然可能更复杂,但可以根据特定应用的要求进行优化,提供更高的效率。
总的来说,选择合适的数据结构取决于具体的应用需求。如果内存开销或访问速度是关键因素,哈希表可能是更好的选择。而如果需要更复杂的存储方式,像四叉树这样的结构可能更合适。
所以我们有世界块和瓦片,是不是也有屏幕?
在这个架构中,屏幕并不是一个明确的存储或结构对象,而是一个游戏构造。它们只是游戏中用于管理视角和可见区域的抽象概念。实际上,屏幕只是一种在游戏中进行显示和滚动的方式。摄像头固定在一个屏幕上,使玩家能够在特定区域内移动,而不需要显式地管理或存储这些屏幕对象。它们只是游戏的表现和行为的一部分,通过游戏的机制自动管理和标记。
为什么在前面提到的例子中不使用函数模板?
在开发过程中,如果涉及到使用模板,很多人会感到不适,因为他们的语法被认为很复杂且容易引发编译器问题。模板可能会导致错误消息变得难以理解,使调试过程更加复杂。这种情况尤其普遍,当模板用于复杂的代码结构时,例如在数据处理或抽象层次较深的情况下。代码的可读性和可维护性也会受到影响,特别是当模板泛化程度高时,需要处理各种类型的特殊情况。
从某种程度上说,使用模板可能是一种过度的尝试,用来实现一种弱元编程,而不是利用更强大的语言特性来解决问题。大多数时候,写一些实际的函数或类,更易于理解和调试。这些函数可以根据需要被调用,而不必担心模板所带来的复杂性和潜在的错误消息问题。
综上所述,模板可能在特定情况下节省时间和代码量,但它们引入的复杂性通常不值得。对很多开发者来说,这种复杂性可能远远超过了模板带来的代码简化效果,尤其是在需要处理大量特殊情况时。因此,他们更倾向于使用实际的函数或类来替代模板。
你会编写资源导出/导入工具来将资源转换为高效格式,还是依赖资源创建者的工具?
在编写资产导入程序时,主要关注的是如何将外部资源(如图像或音频文件)打包并转换为游戏所需的格式。虽然直接使用艺术工具生成的数据可能会被忽略,但将这些资源包装成一个统一的文件格式可以方便地集成到游戏中。基本上,这个过程涉及到对资源的整理和优化,以符合游戏的需求。
虽然不会直接依赖艺术工具生成的数据,但在实际开发中,使用一些元编程技术来自动化生成C代码,对于提升效率和简化代码是有帮助的。这种方式可以有效地生成必要的代码片段,从而避免手动编写重复的代码。
对于未来的开发计划,期待能够将资源正确地呈现在屏幕上,并构建出一个吸引人的游戏体验。这包括如何组织地图屏幕和处理游戏中的视觉效果。这些内容将在下一次的直播中继续讨论,并最终实现,使得游戏能够更具吸引力和互动性。
总结来说,编写资产导入程序的关键在于将资源打包和转换,并在游戏中有效利用这些资源,而不是依赖于艺术工具直接输出的数据。