游戏引擎学习第27天
仓库:https://gitee.com/mrxiao_com/2d_game
欢迎
项目的开始是从零开始构建一款完整的游戏,完全不依赖任何库或引擎。这样做有两个主要原因:首先,因为这非常有趣;其次,因为它非常具有教育意义。了解游戏开发的低层次工作原理是非常有益的,这不仅可以帮助开发者更有控制力,还能让开发者能在需要时直接进入引擎的源代码,进行必要的调整或解决问题,而不是依赖于别人做好的工具。这样能够掌控每个细节,避免陷入使用其他工具时可能遇到的问题。
至今为止,项目进展非常顺利,已经进入了更加有趣的部分。大约花了 25 小时进行原型开发,以确保基本的服务能正常工作,比如地图绘制、播放声音、获取用户输入等。这些工作完成后,就可以开始制作游戏,尽管目前还只是原型版本,未达到发布级别,但足以满足开发游戏的基本需求。虽然平台层并不是完美无缺,但它已经足够支持游戏的开发工作。
接下来,将正式开始进入游戏开发的阶段,这比之前的工作更加有趣,因为这意味着真正开始制作游戏本身,而不再仅仅是打磨平台层。对于那些提前预定游戏的人,他们会收到一封包含下载源代码链接的电子邮件。这个源代码是一个巨大的 zip 文件,包含了每天的状态快照,因此可以清楚地看到开发进度。如果想要跟进,可以从第 27 天的源代码开始,解压并从前一天的代码开始工作,这样就能紧跟项目的最新进展。
项目的演变
今天的讨论重点是如何进行项目架构设计。首先,目标是让大家理解架构的基本概念,以及如何从思维角度接近和框定架构的问题。昨天主要是让大家对项目架构的基础有一个大致的了解,并没有深入讨论具体的架构设计方法。今天的目标是更深入地介绍架构思维,并开始实际构建架构。
架构设计是一个持续的过程,不是一开始就确定好的,它需要根据开发过程中不断出现的需求进行调整和改进。随着项目进展,架构会不断变化和演化。这是一个反复思考和修正的过程,不同的阶段可能会涉及到不同的架构调整。
在项目的初期,尤其是游戏开发的早期阶段,很多人可能对架构还不太明确。为了帮助理解,项目被形象化为一个“项目状态空间”,这个空间包含了项目从起始到完成的整个过程。项目的第0天,大家从零开始,一无所有,这时候的目标是逐步实现从无到有的过程,逐步构建出一个可用的游戏引擎和游戏本身。
项目的进展并不像我们想象的那样是一条直线。虽然从第0天开始,理想的状态是每天做一点工作,逐渐接近最终的发布日期,但现实情况往往是项目在开发过程中会发生很多曲折。项目常常需要调整方向,回退到某些步骤或重新设计。这种非线性的进展是项目开发中的常态,尤其是当涉及复杂的产品时,开发者在第0天通常无法完全预见最终的产品样貌,只有模糊的概念。
因此,项目开发过程充满了迂回曲折的变化,而不是一条从起点到终点的直线路径。开发者并不会从一开始就有一个完美的、清晰的愿景,而是不断通过调整和修正来实现最终目标。
如何朝着一个未知的目标前进
在项目开发过程中,架构和代码之间没有明确的界限,而是一个连续的过程。开发过程中,不仅仅是架构的设计会随着时间变化,单个代码的实现也会不断地调整和演变。因此,架构和代码之间的关系是模糊的,它们是相互交织的。架构的设计会影响代码的结构,而代码的实现又可能反过来影响架构的设计。
项目的开发过程常常不像最初设想的那样是一个直接的线性进程。每个阶段的开发都像是在一个虚拟的“项目状态空间”中移动。在某个特定的时间点,可能并不知道最终的目标是什么。即使有一些模糊的目标概念,开发人员也无法清晰地看到最终的完成形态。通常,开发者只能依靠对当前阶段的模糊理解,逐步朝着一个可能的目标前进。
这种进程有点像是通过“死航测定”(dead reckoning)来规划前进的方向。在每个开发阶段,开发人员都会根据当前的需求和目标,选择某个方向前进。这时,并不能确定最终的目的地是什么,甚至目标可能会随着开发的推进发生变化。当开发者根据现有的代码和需求做出调整时,可能会意识到,之前的方向并不是最终目标,可能需要回退或者重新调整。
这种开发过程往往充满了曲折与迂回,最终目标可能并不在最初的路径上,而是随着开发的进展逐渐明晰。在开发过程中,随着每个阶段的推进,开发者逐渐清晰地知道自己在做什么,并且从模糊的构思逐步走向更明确的方向。
总结来说,开发是一个不断调整和修正的过程。每个开发步骤都推动着项目向前发展,尽管目标并不总是明确,但随着进度的推进,开发者逐步接近最终的目标。
预先规划的危险
在软件开发中,前期设计通常假设开发者已经对最终目标有清晰的认识,这样的假设导致了一个问题:在不清楚目标的情况下进行设计和规划是不切实际的。前期设计的核心思想是将整个开发过程划分为一系列的“路径点”,即开发者在不同阶段设置目标并按照预定路线推进。然而,这种方法存在缺陷,尤其是在目标不明确时,无法确保设计的准确性。开发过程中,目标往往会发生变化,开发者逐渐明晰最终的形态,而预设的路径点往往无法适应这种变化。
如果项目的目标是完全未知的,那么预先规划的路径点就没有意义,因为根本无法确定目标的具体位置。在这种情况下,开发者可能会做出错误的设计决策,导致开发过程偏离最初设想。当开发者继续推进时,往往会发现问题的出现,需要重新调整方向并进行修正。最终的结果可能是开发出的产品与最初的目标相去甚远,或者根本无法实现预期的效果。
在实际开发过程中,真正的架构设计挑战在于如何应对不确定性,并在目标不明确时有效地推进开发。相比于已经有明确目标的开发任务,面对未知的设计需求才是真正的架构工作,因为这需要在不完全了解最终结果的情况下进行决策和调整。
开发者需要培养的关键技能是在这种不确定性中保持灵活性,学会如何在缺乏完全了解的情况下进行架构设计。这包括不断地对当前的进展进行评估和调整,避免仅仅依赖预设的路径点,而是要根据实际情况和需求进行灵活应对。这种方法不仅可以提高开发过程的效率,还能避免过度依赖先入为主的设计思路,确保项目能够在发展过程中适应变化,最终达到一个更好的结果。
探索的两步法
在软件开发过程中,常常会面临目标不明确的情况。这时,开发者需要采取一种更灵活的方式来推进工作,而不是依赖事先设定好的目标和路径。具体来说,开发者应该像探险家一样,探索当前的开发空间,而不是预设一条固定的路线。这个过程充满了试探与调整,开发者需要根据实际的发现不断调整方向,锁定有效的路径,并在此基础上进行下一步的探索。
这种方法不依赖于已经设定好的“路径点”,而是通过对当前代码和设计的不断实验,找到可行的方向。一旦发现某个设计或代码片段有效,就将其作为新的起点,继续前进。这个过程是循环的,每一次探索和发现都会为下一步的设计提供新的信息。
在这种探索过程中,开发者逐步培养一种“感知梯度”的能力,即通过经验判断当前设计是否接近理想的方向。随着时间的积累,开发者能够更好地识别设计中的“好”与“坏”,并不断调整自己的工作方向,保持在“良好设计”的区间内,从而最终避免过多的错误和偏离目标。
这种方法强调的是适应变化和灵活应对,而不是依赖固定的设计模式或预设的目标。即使在面对从未遇到过的架构问题时,开发者也能凭借已有的编程经验,通过不断的探索和调整,找到合适的解决方案。
总体而言,这种方法要求开发者在面对复杂或不确定的任务时,保持开放和敏捷的心态,随时准备根据实际情况进行调整和优化,而不是死守预设的设计路线。这种灵活的探索方式是应对不确定性和复杂问题的有效策略。
游戏引擎目标
在开发游戏引擎的过程中,首先需要明确的是,目标并不是完全设计好游戏本身,而是建立一个能够支持游戏设计的引擎框架。尽管具体的游戏设计和规则会在之后详细介绍,并且可能会有一些意外的惊喜,但在此时,需要先概括性地了解游戏引擎所需支持的基本内容。
具体来说,开发者首先要思考游戏的外观和位置,以及这些元素如何与引擎的功能相结合。理解这些基本构成后,才能更清楚地知道如何设计和实现引擎的各个模块,以便能够有效地支持未来的游戏设计。这意味着,开发者需要以一种支持性的方式构建引擎,确保它能够灵活应对各种游戏规则和需求。
《塞尔达传说》的影响
在谈到游戏设计时,特别是以《塞尔达传说》为例,核心的理念是探索自由和没有强制指导。最初的《塞尔达传说》带来了这样一种感觉,玩家被直接投入到一个丰富的世界中,几乎没有约束,玩家可以自由探索而不被游戏中的角色或任务指引束缚。例如,游戏并没有强迫玩家去某些地方,像是让玩家在去特定区域前先完成某些任务。这样的设计给予了玩家极大的自由度和未知的探索感,这正是吸引人的地方。
这种探索的自由与思考的空间是游戏设计的核心要素。设计者不想在游戏中加入过多的引导,避免过度干预玩家的决策过程,而是希望玩家可以自行探索并发现世界。尽管这是一个个人偏好的问题,但这一点对游戏体验至关重要。
当考虑将这一探索和自由感融入现代游戏设计时,重点是如何保留这种无手持引导的自由感觉,同时现代化游戏元素,比如利用现代技术更新古老的游戏机制。例如,像《塞尔达传说》中的瓷砖地图和角色移动系统,这些经典的要素需要在不牺牲原有游戏体验的前提下进行现代化处理。
因此,游戏设计不仅仅是关于游戏内容本身,还包括如何通过引擎技术支持这种自由探索的体验。目标是创造一个支持这种探索感的引擎,同时避免过多干预,保持玩家的自由度。
瓦片地图——什么需要现代化,什么需要保留
目标是创建一个游戏引擎,它能支持在平铺地图上创建可玩的游戏,灵感来源于像《塞尔达传说》这样的经典游戏。这个游戏设计希望保留那种经典的探索感觉,其中玩家自由探索世界而不被过多指引。瓷砖地图(tile map)被认为是一个有效的工具,尽管它可能看起来过时,特别是在现代游戏中。传统的瓷砖地图形式虽然在技术上有局限,但它能帮助玩家理解空间布局和与环境互动的规则,例如,当某个瓷砖被阻塞时,玩家就知道自己的路径受限。
然而,虽然传统的瓷砖地图有其优势,现代游戏设计可以对其进行一定的“现代化”处理。目标是让瓷砖地图看起来更灵活,不再呈现出明显的重复和机械感。例如,可以通过某种方式使得游戏画面看起来更自然,而不仅仅是平铺地图的机械拼接。虽然地图元素会保持与传统瓷砖地图的游戏逻辑一致,但在视觉呈现上会更加多样化和动态化,避免了过去那种每个对象都显得过于死板和重复的情况。
此外,某些元素如敌人和主角可以在这个灵活的地图上自由漂浮,而不必严格地与瓷砖对齐,增加游戏的自由度和灵活性。游戏的基础逻辑依然会基于瓷砖地图,确保游戏的规则和物体交互保持一致性,但在视觉层面上则会通过一些现代渲染技术来增强表现力,使得玩家体验到一种既经典又现代的感觉。
这种方法的核心目标是让玩家能够清晰理解游戏世界的结构和互动规则,同时又能感受到视觉上的创新和变化,从而提升整体的游戏体验。
程序化生成关卡的目标
计划是创建一个完全程序化的游戏世界,其中所有的内容都由算法生成,而不是硬编码的地图或区域。每次进入游戏时,玩家将看到一个全新的世界。这个世界的各个部分,包括地牢和城镇,都将根据算法自动生成,而不是手动设计或预设。
在具体的设计上,地图将保持一致性,所有的瓷砖(tile)都应该在相同的尺寸和位置上,不会有不合理的缩放或变化,除非是通过特殊的魔法环境或其他特定情况。即便是地牢,也将作为世界的一部分生成,而不是通过不同的屏幕或分开独立的区域来呈现。玩家可以进入地牢,通过某种方式(如梯子或隧道)到达地牢的地图,但这一切都将在同一个空间中,保持一致性。
整个世界的地图将没有尺寸限制,允许生成极其巨大的世界,甚至可以包含玩家永远无法完全探索的区域。没有固定的“屏幕大小”或区域尺寸限制,这样可以创建出完全开放、没有边界的世界。这种方式保证了世界的生成不会受到硬性限制,而是尽可能地开放和灵活。
地牢和城镇等地点将作为世界的一部分而存在,而不是单独的“关卡”。城镇的地图将不再是传统的瓷砖地图,而是通过一些更自然、更加灵活的渲染方式进行展示,保持视觉上的一致性,但又避免了传统瓷砖地图的重复感。
这种设计的目标是让所有的内容都具有高度的程序化生成能力,同时保持足够的灵活性和美感,使得玩家在游戏中每次进入时都能体验到一个全新的世界。
丰富的组合互动
在游戏的开发中,重点是创造一个非常丰富的程序化系统,使得所有世界中的事物都能通过组合学(combinatorics)进行交互和变动。这意味着,游戏中的所有元素,无论是静态的瓷砖,还是在瓷砖上移动的对象,都应该能够通过一致的方式进行互动,且每个属性都能与其他属性产生影响。
例如,如果有一个怪物,其具有“恐惧”这一属性,当它被击中时,应产生恐惧的效果,甚至可以与其他因素产生联动效果。如果怪物本身有“弱点”属性,那么恐惧和弱点的结合可能导致怪物逃跑或表现出其他反应。这种交互不仅限于怪物,还可以扩展到游戏中的任何元素,比如道具、魔法、环境因素等。
该游戏设计的目标是创造一个极为复杂且多层次的组合系统,以确保每个元素都有与其他元素相互作用的潜力。这种程序化的组合性设计虽然非常具有挑战性,但将使游戏更加有趣和复杂,并且能提供丰富的编程体验。
实现这一目标,首先需要确保游戏世界本身是可以自由生成的,能够容纳这些复杂的交互逻辑。一旦基础的世界生成系统能够顺利运行,就可以逐步引入更为复杂的元素和交互设计。随着游戏开发的推进,将逐渐实现这个组合学驱动的世界,使其不仅具有高度的程序化生成能力,还能在细节上表现出丰富的逻辑和动态。
这项技术将是游戏开发的核心部分,虽然尚未实现,但将是游戏后期设计中的关键要素。当前的开发重点是确保基础的世界生成系统能够顺利运行,为之后更复杂的游戏机制打下基础。在这之后,开发将转向真正的游戏设计和开发阶段,花费大量时间完善这些系统。
编码开始
添加我们忘记的东西
在接下来的开发过程中,需要关注几个技术点。首先,回顾了之前提到的游戏循环目标和帧率更新的概念,之前并没有将计算的“TargetSecondsPerFrame”传递到游戏循环中。这个值对于游戏的更新和运行至关重要,能够使游戏能够在不同的帧率下流畅运行,因此需要确保将其传递给游戏引擎。
此外,游戏的输入处理。虽然此前讨论过如何处理游戏输入,但还没有实际将“NewInput”与目标帧率的更新相结合。目标是确保输入和更新逻辑在每一帧的目标秒数下同步进行。
最终的步骤是将这些更新和输入处理结合起来,确保它们在游戏引擎中顺利传递和应用。这将为游戏提供更加稳定的运行基础,并确保开发者能够在接下来的开发中实现更复杂的逻辑和交互。
清理旧的调试可视化/声音
开始时,目标是清理一些不再需要的测试代码,尤其是那些与渲染和梯度相关的部分。之前的一些测试模式产生了不需要的视觉效果,如奇怪的梯度,已经不再需要这些功能。
首先,决定去除这些不必要的渲染调用和测试输出,尤其是与控制器和音效输出相关的部分。通过禁用音效输出,确保游戏在运行时保持安静,避免出现不必要的声音。接下来,清理掉多余的调试信息和不再需要的测试代码,比如与鼠标测试相关的部分,这些都是为了调试的临时代码,已经不适用于当前开发阶段。
清理过程中,注意移除那些显示不必要的图形和信息的代码,如之前用于显示的梯度和视觉标记。虽然有些部分可能仍然是测试代码,但现在的目标是保持简洁并去除不再使用的内容。
在调整和清理这些代码后,测试了游戏是否正常运行,确认没有再生成不必要的测试输出。清理掉不必要的函数调用和调试信息后,确认游戏可以以正确的方式运行,确保游戏的基本结构和控制功能依然有效。
最终,代码已清理干净,不再需要的部分被移除或注释掉,确保游戏逻辑和显示都回归到基本状态,准备好进入更高级的开发阶段。这些改动使得游戏运行更加简洁和高效,为后续开发打下基础。
清理掉相关调试信息
分辨率目标
现在的重点是考虑游戏的分辨率,并决定适合的目标分辨率,尤其是针对二维游戏的需求。与3D游戏不同,二维游戏的分辨率设计需要特别关注背景元素的尺寸,确保它们在屏幕上呈现时清晰可见,符合艺术家设定的要求。考虑到这一点,决策的分辨率为1920x1080,标准的高清分辨率,但为了适应软件渲染的性能限制,建议暂时使用较低的分辨率,例如960x540。
考虑到软件渲染的效率较低,比GPU渲染要慢得多,因此,理想情况下可以选择将渲染分辨率设为GPU分辨率的1/8,这样可以保证游戏的表现平衡在可接受的范围内。在这个过程中,目标是确保游戏在低性能渲染下能够顺利运行,同时维持较为清晰的视觉效果,确保图形和纹理不会过于模糊。
为了测试和开发,决定将游戏的分辨率设定为960x540,并以此进行开发和优化,直到能够达到目标的1920x1080分辨率。此决策考虑到在优化过程中,减少性能瓶颈,保持每秒60帧的流畅度,并且根据不同的硬件环境,确保游戏可以在多种设备上运行。
此外,讨论了重新启动应用程序的必要性,因为一些修改涉及到平台层的设置,只有在应用重新启动后才能应用这些更改。最终,游戏的目标显示分辨率大致为960x540,这是当前最适合进行开发和测试的分辨率。
这一过程确保游戏在设计和性能方面都能朝着合适的方向发展,并做好了准备来应对未来的优化和发布。
画一个矩形
为了顺利进行开发,首先需要实现一个简单的函数来绘制矩形。这个矩形将通过传递颜色值,并且需要考虑到正确的裁剪,以确保即使矩形超出了预定的区域也不会崩溃。目标是让这个函数能够绘制出一个矩形,并且确保它不会出界。
这个矩形绘制功能会使用现有的渲染播放器,而这个播放器之前已经能够绘制矩形。目标是将其改造为一个正式的绘制矩形的函数。为了支持更多的灵活性,该函数将接受浮点坐标而不是整数坐标,虽然这在一开始可能看起来不必要,但其好处将在后续显现,特别是在做像素定位时,能够通过插值来实现更细致的控制。
通过接受浮点坐标,未来可以更加精确地调整图形的显示,尤其是在实现紫外线照射、颜色插值等效果时,这种精度显得尤为重要。当前的代码目标是接受 x、y 坐标及矩形的最大宽度和高度。
在绘制矩形时,有时会遇到一些困扰,尤其是在处理矩形的坐标和尺寸时。虽然有时可能会考虑到一些更复杂的算法和方法,例如流式处理,但由于实际情况的复杂性,选择逐步调整和解决问题,而不是一次性解决所有问题。
画几何图形时需要了解的边界
在开发过程中,需要注意一些复杂的细节,特别是在处理渲染时。绘制矩形时,精确的定义和坐标非常重要。对于像素网格的操作,必须有一个明确的方式来指定和填充像素,但这变得相对复杂,特别是在确定是否包括某些像素时。比如,当填充一个区域时,需要明确是填充该区域内的所有像素,还是排除一些边界像素。这个“包含”与“排除”之间的区别很容易让人混淆。
当处理像素坐标时,有时需要考虑到精确的坐标系统。例如,如果需要填充从(1,1)到(3,3)的区域,可能会面临问题:到底是填充边界上的像素,还是不包括最外侧的像素?这些问题在设计和实现时必须特别小心,以确保渲染的正确性。
此外,在处理精灵或其他对象的渲染时,坐标的偏移会带来更大的复杂性。如果精灵没有精确对齐到像素网格,它可能会涉及到“子像素”的问题,即渲染在像素之间的部分。这需要有明确的惯例来决定每个像素如何贡献颜色以及如何处理这些边缘情况。
在实际实现时,矩形绘制操作的具体细节会涉及到坐标的舍入和调整。例如,通过选择最小值并将坐标四舍五入到整数,可以确保填充区域的正确性。然而,最终的填充区域可能会根据具体的规则进行裁剪,以避免超出预定的区域。
这些细节的处理会影响到渲染的精确度,尤其是在绘制多个矩形或者复杂图形时。为了简化调试过程,采用一些粗略的规则来处理填充区域,可以避免一开始就追求完美,而是在后续优化时进行精细调整。
编写 drawRectangle
上述内容描述了如何在图形上下文中处理内存和像素数据,具体步骤如下:
主要概念
-
缓冲区和像素表示:
- 内存缓冲区用于存储像素数据,每个值代表一个像素的颜色。可以通过访问这个缓冲区来进行绘制或操作像素。
- 目标是确保在绘制矩形(或像素区域)时,不会有写入超出有效内存或屏幕边界的情况。
-
矩形裁剪:
- 主要任务是将矩形裁剪到有效的屏幕区域内。
- 如果矩形超出了有效的边界(例如,超出了左边界、右边界或屏幕的宽度/高度),需要进行调整(裁剪),确保写入的内容只在有效的内存位置内。
-
坐标限制:
- 如果矩形的x或y值超出了允许的边界(例如小于0或大于屏幕的宽度或高度),需要进行限制。
- 这个过程包括检查并调整最小值和最大值,确保它们是有效的,从而防止崩溃或无效的写入操作。
-
内存布局和效率:
- 假设内存是连续的,也就是说,像素数据存储在连续的内存位置中。
- 为了提高内存访问效率,建议按行写入相邻的像素,这是更符合缓存友好性的做法。
- 使用指针来访问内存,从指定的最小x/y坐标开始,按行填充矩形。
-
指针管理:
- 使用指针来遍历内存,首先水平地(在行内)移动,然后垂直地(向下移动到下一行)。这样可以确保像素按照正确的顺序进行写入,从最左边的像素开始,向右移动,再向下移动到下一行。
- 内存访问通过“行指针”和“像素指针”进行管理,行指针处理垂直移动,像素指针处理水平移动。
-
颜色处理:
- 初步使用一个占位颜色(白色)来填充矩形,实际的颜色逻辑将稍后实现。
- 颜色通过RGB值传递,任务是确保在写入每个像素时应用正确的颜色。
-
像素写入:
- 最终的任务是将颜色写入像素内存中。需要遍历像素数组,必要时调整指针,直到整个矩形区域被填充完毕。
-
四舍五入函数:
- 提到需要一个四舍五入函数,用于处理某些操作,例如将值四舍五入到符合某些限制或网格大小的范围,尽管该函数的具体实现细节稍后会补充。
-
效率与内存边界:
- 特别关注确保写入像素时不会超出有效内存或缓冲区的边界。这包括进行逻辑检查(边界判断)和内存管理技巧(如确保只写入有效的内存块)。
总结
整个过程描述了如何结构化地设置和管理像素数据,确保渲染操作不会越界到无效内存,并且符合内存布局和性能优化的要求。
将浮点数四舍五入为整数
上面描述的是一个实现四舍五入函数的过程,主要通过模拟传统的四舍五入行为来改进截断方法。具体来说,以下是步骤和思路的总结:
问题的背景
在C语言中,默认的类型转换操作(如将浮动点数转化为整数)通常会进行截断,即去掉小数部分,而不是执行四舍五入。这导致了对于需要四舍五入的情况,直接进行转换可能不符合期望。
截断和四舍五入的区别
- 截断(Truncation):将浮动点数转换为整数时,C会直接去掉小数部分,不考虑四舍五入的规则。例如,
3.7
会被截断为3
,而-3.7
会被截断为-3
。 - 四舍五入(Rounding):四舍五入是指,如果小数部分大于或等于0.5,整数部分应增加1;如果小数部分小于0.5,保持整数部分不变。
实现四舍五入
-
截断的行为:假设有一个浮动点数,直接将其转换为整数会丢掉小数部分。如果想实现四舍五入,可以通过在截断之前对浮动点数加上一个偏移量(比如0.5),这样可以让小数部分推到下一个整数。
-
通过加0.5实现四舍五入:为了让数值“向上”四舍五入,可以加上
0.5
到浮动点数。如果原始值是3.2
,那么加上0.5
后得到3.7
,然后截断会得到3
,这是我们期望的结果。如果原始值是3.7
,加上0.5
后变为4.2
,截断后得到4
,同样符合四舍五入的规则。 -
示例:对于数值
3.7
,加上0.5
后变为4.2
,截断后得到4
,表示四舍五入到1。因此,使用加上0.5
的方式实现了四舍五入。
需要注意的细节
-
负数的情况:对于负数,像
-3.7
,加上0.5
后变为-3.2
,截断后得到-3
。虽然截断时会移除小数部分,但由于加入了偏移量,负数也会正确地处理为四舍五入。 -
特殊情况:四舍五入并非总是仅仅加
0.5
就能完全满足需求,尤其是在涉及浮动点数的各种情况(如精度问题)时,可能需要更多的考虑和调整,尤其是当我们涉及更复杂的数据类型或特殊数学处理时。 -
将截断操作改为四舍五入:通过在数值上加上一个适当的偏移量(如
0.5
),可以将简单的截断操作转变为四舍五入操作,从而解决普通的四舍五入需求。
总结
上述方法通过简单的数值偏移加截断的组合,提供了一种基础的四舍五入机制。这种方式在许多情况下是有效的,尤其是对于简单的浮动点数值。对于更复杂的四舍五入规则,可能还需要进一步的调整和处理,尤其是在涉及更高精度浮动点数或多维数据时。
测试 drawRectangle
-
矩形绘制函数的测试:
- 初步测试通过
DrawRectangle
函数绘制了一个矩形。起初,只是简单测试其能否正常绘制一个矩形。 - 进行了异常情况测试,比如将矩形移动到屏幕之外,检查程序是否会崩溃。经过测试,没有发现崩溃,验证了矩形绘制函数的稳定性。
- 初步测试通过
-
清屏功能的实验:
- 测试了将屏幕清除为特定颜色,例如紫色和青色,并尝试在此基础上绘制矩形。这些测试验证了颜色的传递和正确性。最初,尝试的颜色传递方式并没有正常工作,但后来调整后得以实现。
-
前进的计划:
- 最终确认矩形渲染功能基本有效,并为下一步任务做好准备,即构建一个平铺地图(tile map)。虽然当前已完成了初步的矩形绘制功能,但为了继续开发,下一步将朝着更复杂的任务(如地图构建)前进。
- 结束了本次开发过程,并计划在未来的工作中继续测试和调整。
对于将所有东西都写成完全函数式的想法有什么看法?
在讨论中,首先提到了一种对函数式编程的看法。对某些人来说,将整个游戏重构为函数式编程可能有些过于教条化。尽管函数式编程可以作为一种练习来进行,但如果从生产力的角度来看,强制将整个项目改为函数式编程可能并不是最佳选择。
有些人可能会将函数式编程视为一种理念,并在项目中尝试尽可能采用这种方法,但这并不意味着必须完全遵循这种方式。在实际开发中,函数式编程在某些地方是有意义的,特别是当它能够提供更清晰、更简洁的代码结构时。通过减少副作用并提高代码的可重用性,函数式编程能让开发者更容易理解和操作代码。
然而,虽然倾向于在可能的地方使用函数式编程,但并不意味着要在所有地方都严格执行。如果编程任务并不适合函数式编程,那就没有必要强行使用。函数式编程应该根据实际情况来应用,只有在它确实能带来清晰和可维护性的提升时,才值得使用。总体而言,开发者认为应该根据需求来合理使用函数式编程,而不是让其成为一种过度的编程风格。
如何在大团队中进行压缩编程?
在讨论压缩编程和大团队时,首先提到了一些关于团队合作和设计的观点。对于大型团队中的压缩编程,涉及的核心问题是如何协调不同团队成员间的工作,特别是当不同的人在处理不同的代码块时,如何确保压缩和设计工作能够顺利进行。
一个要点是,在团队中做压缩编程时,团队成员通常会在自己的模块中进行设计和开发。就像在游戏开发过程中,团队成员会在实现某些功能或优化时,进行压缩编程的工作。当这些设计被整合并达到一定的稳定性时,团队成员会将设计输出到其他团队成员,允许他们继续构建系统。这种做法的核心是将工作拆分并逐步完善,通过团队合作推进项目进展。
此外,还提到了一些关于团队工作和压缩编程的经验,例如,团队成员在设计过程中会经历探索阶段,寻找一个合适的设计方案,当他们认为已经达到了一个稳定、满意的状态时,就会将设计分享给其他团队成员。这种方式并不复杂,核心是在于团队成员之间的协调合作,保证每个环节的设计和实现能够互相兼容。
总的来说,压缩编程和团队合作的关键在于充分的沟通、协调和设计的逐步完善,而不是简单地在技术层面解决问题。
你认为写像向量这样的结构体好吗?
在讨论编程的实践时,重点强调了代码的写作过程和如何逐步引导出有意义的结构。首先,指出了在团队工作中,编写代码的方式不应该过早地使用复杂的抽象或结构,而是应该从简单、明确的写作开始,避免一开始就引入过多的样板代码。这是因为当程序员还不完全确定如何处理某些概念时,过早的抽象可能会使代码变得不易理解,甚至阻碍后续开发。
在实际编程中,首先是通过探索性的方式编写代码,观察代码中出现的模式,进而根据这些模式决定是否引入一些常见的功能或者工具类(如向量类或颜色类)。这种方式强调了先写清楚代码,避免过度设计,以便在代码编写的过程中更好地理解需求和结构。当程序员逐渐理解了代码的意图和功能后,才会根据实际情况抽象出必要的工具类。
此外,如果频繁使用某些功能,开发者自然会逐步形成一些规范或工具类,但在这之前,重要的是要理解实际的需求和场景,而不是先入为主地创建复杂的结构。通过这种方式,开发者可以避免过早地形成结构或设计,确保最终的工具类能够真正解决问题并符合开发需要。
总结来说,这种方法强调了探索性编程和渐进式设计,鼓励开发者从明确、简单的实现开始,通过不断的实践和理解,逐渐引入复杂的抽象和结构。
你会使用线性纹理布局还是一些花哨的布局?
是否使用线性纹理布局或者更复杂的布局方法,例如一种类似于瓷砖布局的方式,其中瓷砖被嵌套,并且在屏幕的右侧进行一些操作。对于这些布局的选择,答案是暂时不做决定,建议等到渲染阶段时再讨论相关的实现细节和布局选项。
如果没有 C,您会使用什么语言?
讨论中提到,如果没有某个特定的工具或语言存在,可能会转向使用汇编语言进行编程。假设没有现成的工具集,可能会自己开发一套工具来处理汇编代码,特别是在低级编程方面,可能会在没有现有语言支持的情况下使用汇编。
虽然有时会思考是否回归到汇编语言,尤其是在感到自己在这一领域的知识还不够扎实时,这种想法也并不完全新鲜。过去的编程背景并没有强烈的依赖于汇编语言,随着技术的进步,汇编的应用逐渐减少,但对汇编的理解仍然有一定兴趣。
我们在哪里能找到有关引擎架构的信息?
讨论中提到,关于引擎架构的附加信息比较难以获得。由于不再花很多时间阅读入门级的书籍或材料,因此也不太了解现有的资源。在早期学习游戏开发时,市面上几乎没有关于游戏引擎架构的书籍,且当时的游戏相关书籍质量较差,尤其是90年代出版的一些作品。
当时有一些关于图形编程的书籍,如《微型计算机图形显示》等,但这类书籍主要集中在图形显示方面,而并非游戏引擎架构。作者没有阅读过游戏引擎架构的书籍,尽管他假设如今可能会有一些更好的资源存在。
程序化生成的具体内容是什么?地下城是否会在每次进入时重新生成?
在讨论游戏世界是否会在存储并保存状态时重新生成时,提到有一个可能的设计方案是使用种子(seed)来确保每个玩家获得相同的游戏世界。通过保存游戏的状态并以种子的形式导出,玩家可以分享游戏状态,允许其他玩家加载并继续相同的游戏体验。这样,地牢等内容在玩家重新访问时不会随机改变,从而保持世界的一致性。
然而,尽管设计中考虑了世界的一致性,依然希望在游戏设计上做一些特殊的调整,使得它不完全是那种像《以撒的结合》(Roguelite) 那样通过种子直接打印出可复现世界的设计。这是因为某些设计的需求意味着仅依靠种子并不足以完全重建游戏世界。
游戏会根据进度生成吗?
在讨论游戏世界生成和状态保存时,提到了一种可能的方案:游戏世界在开始时生成,而不是每次进入时重新生成。这样,生成过程可以在后台进行,以避免玩家在等待生成时看着加载屏幕。希望尽量避免像某些游戏那样,玩家不得不长时间等待游戏生成内容的情况。为了实现这一点,可能会将生成过程移至独立线程,这样玩家可以在第一个或前几个屏幕上继续游戏,而无需等待。
此外,提到现代计算机拥有大量内存,这意味着游戏世界的生成过程应该能够在内存中完成,而不需要保存到磁盘。这样可以节省磁盘空间,因为生成的世界不太可能达到需要持久存储的规模。最终,游戏世界的数据大部分可以通过内存管理,而不需要依赖磁盘存储。
你会使用定点数吗?
在讨论使用固定点(fixed point)与浮点数(float)时,指出固定点数通常会导致计算变慢。因此,虽然可能在某些情况下使用固定点数的方式会看起来像是固定点数的操作,但大多数情况下会选择使用浮点数,因为浮点数运算更为高效和快速。
玩家会真的注意到这个游戏中的声音延迟 1-2 帧吗?
关于游戏开发中的声音处理,提到降低延迟的问题,尤其是当涉及到复杂的声音循环时,降低声音延迟是一个关键目标。如果想要减少游戏中的延迟,尤其是涉及玩家交互的情况下,声音和图像输出的同步性非常重要。尽管当前的游戏可能不会面临过多的延迟问题,但对于有节奏要求的游戏,像节奏游戏等,降低延迟尤为关键。因此,在开发过程中,可能会花费一些时间专注于优化声音代码,确保游戏体验更加流畅。
如果游戏是基于矢量的,最初的渲染会有多不同?
关于“初始渲染代码”的问题,提到的初始渲染代码目前其实并不重要,因为目前的工作重点只是调试。开发的目标是在屏幕上显示矩形,以便测试并开始运行游戏。此时,并不需要关注最终的渲染代码,而是着重于初步的调试和测试。通过这些测试,可以逐步明确渲染系统需要提供的功能。
如果讨论到最终的渲染代码,那么它将与基于矢量的渲染和基于点的(或像素)渲染有显著的区别。基于矢量的渲染方法和基于点的渲染方法在实现和效果上有很大的不同,最终的渲染代码会在这两者之间有明显的差异。因此,在当前阶段,讨论初始渲染代码的具体实现并不重要,主要目的是测试和验证渲染系统的基本功能。
面向对象编程有什么不好的地方?
关于面向对象编程(OOP),对其使用的观点并不是完全否定,而是强调在特定情况下的使用价值。虽然面向对象的抽象对于某些应用是有用的,但它并不是万能的,且并非所有时候都能带来最优的解决方案。面向对象的编程方法有时会导致代码变得过于复杂,而这并不总是能提高性能或者代码的可维护性。特别是当将编程模式强制应用到所有场景时,可能会带来冗余或者效率问题。
核心的论点是,面向对象的编程(OOP)方法通常会过度聚焦于“对象”本身,但这些对象与代码如何运行的性能无关。实际上,代码的结构更应该由算法决定,而不是由对象的抽象层次所决定。面向对象编程把焦点放在了“对象”上,而忽视了算法和数据布局对性能的影响。代码的结构应更多地基于性能需求和算法的设计,而不是简单地围绕对象构建。
相比之下,通过“压缩”或“算法导向”的思维方式进行编程会带来更高效、清晰的代码。这种方式强调在理解数据和算法的过程中,通过提取和重用代码来简化结构,而不是单纯地将复杂性加在对象的层次上。
这并不是说在系统中没有对象,而是指开发者在设计代码时的思维方式。最终的代码质量取决于如何思考和设计这些对象以及它们之间的交互,而不仅仅是对象本身的定义。使用压缩或算法导向的编程方式,能够更好地关注性能和简化逻辑,从而产生更好的代码结构和系统设计。
你将如何使用游戏内存块?
讨论中提到的是关于游戏开发过程中的内存管理和资源使用的情况。具体来说,计划很快开始使用游戏内存块(game memory block)来优化游戏资源的管理和存储。此时,需要对开发过程保持一些控制和耐心,避免过快推进。开发者比喻性地表示,“控住你的马”,意思是在开发的初期阶段保持节奏,不要过于急于推进,因为系统的构建和资源管理通常需要一些时间才能稳定运行。
尽管内存块的使用计划已经在进展中,但目前还需要一些额外的时间来调整和完善,因此建议在推进项目时保持谨慎,稍微“控制”进度,以确保游戏的各个部分在两天内能够顺利运行。在此过程中,开发者应保持对系统的灵活性,准备好随时根据进展调整计划,但仍需注意不要过于急躁。
如果 MinX 和 MaxX 都小于 0 会怎样?
讨论中涉及到的是一个关于条件判断和程序流的逻辑。如果在 Linux 和 Mac 系统上某些值低于零,程序将会把这些值调整为零,以避免进入错误的循环或导致崩溃。当检查到这些值小于零时,程序会确保它们被移到零,不会影响后续逻辑。
具体来说,代码设计了一个保护机制,确保即使某些输入值不符合预期(例如小于零),程序也不会崩溃。通过设置这些值的边界(比如设置为零),避免了进入不必要的循环,也保证了程序的稳定性。最后,尽管这种情况看似已被妥善处理,但建议进行测试以确保没有遗漏的潜在问题。