游戏引擎学习第26天

编程实际游戏的第一天

在第一天的编程中,目标是开始编写游戏的实际部分。虽然之前有一个原型层已经完成,但目前这个层次只是为了快速开发和验证游戏的基本功能,并不适合直接发布给终端用户。虽然这个原型层足够让游戏开发顺利进行,但它仍然只是原型设计,接下来的工作将是进一步完善和调整。

在这个阶段,虽然有些平台相关的工作是必要的,但这些工作往往是枯燥的重复性劳动,涉及到如何将已知的功能成功实现到平台上。尽管与平台打交道有时能带来一些乐趣,但更多时候它只是试图解决平台特性或文档不充分的问题,这可能导致开发过程变得不太令人满意。

此外,制作这样一个教育性质的流媒体节目比单纯的编程更具挑战性,尤其是在讲解一些完全自己编写的代码时,可以更清楚地解释为什么做出这样的设计选择。当涉及到平台层时(比如 Linux 或 macOS),就需要依赖这些平台提供的接口或系统,而并非自己完全控制。到了某些阶段,开发者会不得不接受这些外部系统的限制和特性,即使这并不总是令人满意的过程。

我们将了解平台层之上的一切是如何发生的

在这段话中,重点是谈论游戏开发过程的乐趣和挑战,尤其是在从零开始编程的情况下。首先,开发者明确表示他们的目标是完全控制游戏的开发,不依赖任何外部平台或工具,这样就能了解每一个像素是如何渲染到屏幕上的,甚至可以深入了解每一个渲染细节和计算过程。这种做法让开发者对游戏的控制力更强,同时也能从中学到很多,尤其是在教育意义上。

开发过程中的一些实用细节。例如,开发者希望通过直接从零开始构建游戏,而不是使用预制的游戏引擎,这样能够更好地理解每一层技术细节,进而掌握整个游戏开发的管道。这不仅仅是为了让游戏能运行,而是为了了解和学习每一部分是如何实现的。通过这个过程,开发者希望能向其他游戏程序员展示如何从基础开始完成整个游戏的开发。

此外,一个重要的方面------源代码的分享和学习。在这个过程中,源代码会以压缩文件的形式发布,观众可以下载并跟随开发者的进度进行开发,了解每一天的进展。这种做法能够让有兴趣的观众直接参与进来,从而在实践中学习。

总的来说,这段话传达了开发者对控制游戏开发全过程的兴趣,以及通过从零开始的方式来学习和理解游戏开发的复杂性。

软件架构

有很多关于游戏和软件架构的误解,尤其是在编程领域,许多人对这些概念的理解并不深刻。软件架构,尤其是架构设计,在很多情况下被误解为一套复杂的规则和教条,很多这些教条对大多数人来说可能是非常违反直觉的,甚至对个人生产力造成负面影响。结果是,这些误解可能导致更糟糕的产品产出。

很多关于架构的观点可能并不适用于所有情况,尤其是在复杂的项目中。更糟糕的是,过于依赖这些固定的框架和理论,反而可能会束缚开发过程,让最终的产品质量下降。

关于架构的讨论常常忽视了实际情况,许多看似合理的规范和设计原则,可能并不是最佳选择,尤其在快速变化和多样化的项目需求中。游戏架构,作为一种特殊的架构形式,也面临着类似的问题。设计游戏架构时,思考方式和通用的软件架构有所不同,需要更灵活、更加注重效率和性能的解决方案。

因此,理解什么是真正有效的架构原则和思维方式,特别是在游戏开发领域,至关重要。要关注的重点应该是如何通过合理的架构设计来提高效率、保持灵活性,同时避免陷入僵化的规则或不必要的复杂性。

与真实架构的对比

建筑这个词的起源可以追溯到建筑领域,尤其是在建造建筑物时所涉及的过程。当建造一座建筑物时,通常会有一个建筑师负责设计和绘制建筑的蓝图。这些蓝图不仅包含了建筑物的外观设计,还包括了诸如水管、电力、电气系统、地板布置等各个方面的详细信息。在复杂的建筑项目中,可能会有数百份不同的蓝图,每一份蓝图专注于建筑的一个方面,例如水管布局、地板结构或电力布线等。

随着技术的发展,现在的建筑设计往往依赖于CAD(计算机辅助设计)工具来绘制这些蓝图。一旦这些蓝图完成,它们会被交给承包商,承包商会雇佣木匠、石匠、起重机操作员等工人,使用各种建筑材料来实现蓝图上的设计。

在这个过程中,建筑师负责设计和规划,承包商则负责实际的建设工作。建筑师设计的蓝图提供了建设建筑的详细指导,而承包商则通过实际操作,如钉钉子、砌砖、使用起重机等手段,完成建筑物的建造。

然而,将这个比喻直接应用到编程领域并不完全合适。虽然软件架构和建筑设计有一些相似之处,但编程中的"建筑师"并不总是能像建筑师在建造建筑时那样精确地指引过程。建筑蓝图在施工时几乎可以保证无论哪个承包商负责建造,最终结果都应该是相同的。但在编程中,尽管两个不同的团队可能使用相同的设计和架构,最终的代码实现可能会因为不同的细节处理、代码风格或实现方式而有所不同。因此,尽管有相似的概念,软件架构的比喻并不能完全等同于建筑设计,它面临着更复杂且多变的实现过程。

认真看待比喻:UML作为蓝图

很多人谈论软件架构时,会将其比喻为建筑设计的过程,试图将这个比喻字面化,甚至使用类似ML图这样的工具来描述架构。这样做的想法是通过提前规划、绘制复杂的图表或蓝图来尽可能复制建筑设计中的规划过程。然而,这种方式可能会导致架构在实际应用中出现问题,甚至产生误解。

这类方法试图将软件架构的工作框定为一种固定的过程,即:在你设计架构时,假设最终的系统就像建筑一样,可以通过一个精确的蓝图来实现。换句话说,架构师设计的蓝图应该能够传递给任何其他团队成员,且他们在没有任何额外信息的情况下,能按图纸实现出相同的最终程序。这种方式假设,两个不同的人或团队只要根据相同的"蓝图"来工作,最终的结果就应该一致。

这种思维方式基本上将架构视为一组定义或规格,就像建筑的蓝图一样,企图通过预先的规划来定义整个程序的结构和细节。在硬核的面向对象设计中,尤其是那些强调预先设计、系统图表和详细规划的架构风格中,往往采用这种方法。

然而,这种对架构的看法假设了架构的过程是完全可预测和标准化的。实际上,软件架构设计往往比建筑设计更为复杂和动态,细节和实际需求常常会随着开发进程的推进发生变化。因此,这种过于依赖前期规划和图表的方法,可能无法灵活应对开发中的各种变化和挑战。

这样做的问题

在实际的编程中,特别是游戏编程中,传统的建筑比喻并不适用。即使可以绘制出程序的蓝图,准确地描述程序的各个部分,这个过程依然充满了复杂性,因为程序涉及大量的活动部件和不确定性。在这种情况下,理论上,若能够绘制出详细的蓝图并精确指定程序的每个细节,可能就能得到一个最终结果,但实际上这是不太可能的。程序的复杂性远超简单的蓝图设计,尤其是在开发过程中,很多变化和调整是不可预见的。

如果真能创建这样一个完整的蓝图,并将程序的所有细节规定清楚,那么实际上你已经完成了程序的编码工作------你直接写出了程序的代码。尽管一些先进的工具可能能够帮助绘制出这种"蓝图",但这些工具并不能解决实际开发中的不确定性和复杂性。通常情况下,这种方法只是导致了大量前期的工作,却未必能带来预期的结果,特别是在软件开发过程中,很多细节和调整是难以预见的。

很多人,尤其是游戏编程中的从业者,已经意识到这种方法的缺陷。通过观察和经验,他们发现过于依赖前期的详细设计和规划往往不会带来实际的回报,反而可能延误项目进度,导致过多的前期投入却没有显著的回报。

基于这些经验,传统的"建筑师"或"建筑"概念被认为是误导性的。将软件架构比作建筑设计可能并不合适,特别是在实际的编程实践中,这种方法往往不适用,可能导致低效和质量不高的结果。因此,可能需要重新思考和定义软件架构的概念,避免将其与建筑设计过度对比。

替代的观点:软件设计师作为城市规划师

在讨论软件架构时,传统的建筑师比喻可能不太合适。更合适的类比是将软件架构视为城市规划,而非建筑设计。城市规划者负责设计城市的布局和基础设施,这个角色更接近于软件架构师的工作。就像城市规划者需要考虑如何布置道路、公共设施和交通系统,软件架构师也需要设计系统的整体结构,确保各个组件能够有效地协同工作。

建筑师设计具体的建筑物,而软件架构师并不是在设计一个具体的"程序"或"产品",而是在设计一种"结构",为程序的开发和运行提供基础。这种结构就像城市规划中的道路和建筑的布置,它为程序提供了发展的框架。

在这个过程中,编程就像是实际的建筑工作,程序员是将架构转化为实际功能的"承包商"。编译器则像是将程序员写的代码(蓝图)转化为可以实际运行的软件。也就是说,程序员并不是从架构师那里拿到蓝图后直接构建程序,而是通过编译器将代码(架构的蓝图)转化为最终可用的软件。

这种比喻帮助理解了软件架构的角色------它并不局限于设计出具体的应用,而是提供了一种框架和结构,程序员在此基础上进行实现,最终通过编译器将代码转化为运行中的程序。

软件的自由:可塑性架构

在软件开发中,编程的过程与建筑设计有显著的不同,主要在于灵活性和可塑性。与建筑师在现实世界中无法轻易改变建筑蓝图不同,程序员在编写代码时拥有更大的灵活性。建筑师在建造房屋时不能不断修改蓝图,因为修改设计会导致高昂的成本和时间浪费。而在编程中,程序是高度可塑的,可以随时调整和改进。

在建筑行业,预先规划并固定建筑蓝图的做法是基于现实世界的约束,因为一旦建筑开始建造,修改其结构变得非常昂贵和不可行。然而,编程的灵活性使得提前制定"固定蓝图"并不总是明智的。程序设计应当保留灵活性,就像建筑师理想中可以在建筑施工过程中不断调整设计那样。通过这种方式,程序可以随着开发过程的进展不断优化和改进,避免了过早锁定设计而带来的限制。

这一思维模式类似于"变异代码"的概念,即在编程过程中允许不断修改和调整,直到程序结构和功能达到最佳状态。实际上,这种灵活的、可反复修改的编程方式可能会比早期固定框架的方式产生更好的程序,就像建筑师如果可以持续修改设计,最终可能建造出更好的建筑。

总的来说,编程中的灵活性让开发人员能够不断调整和改进代码,最终实现更高质量的成果,而不是仅仅依赖于过早的"预规划"。这种动态和渐进的调整过程,有助于应对实际开发中的变化和需求,而不必像建筑行业那样受到固定预算和时间的约束。

软件设计:城市如何运作

在讨论软件架构时,重要的是要理解它与传统建筑设计的不同之处。与建筑师在设计建筑时所做的事情不同,软件架构更类似于城市规划。在城市规划中,规划师不关心具体建筑的蓝图,而是关注城市中各个区域的分区、交通流动、以及不同区域之间如何有效连接。在软件架构中,这种思维方式体现在设计系统的高层结构和各个部分如何协同工作,而不仅仅是关注具体的代码实现。

软件架构师通常会处理系统的整体结构和模块化,类似于规划一个虚拟城市的布局。他们关注的是系统中的各个组件如何协同工作,它们之间的交互如何设计,以确保系统高效运行。这些设计包括决定哪些部分应该在哪个层级,如何确保数据流动顺畅,如何在不同模块之间分配责任等等。

然而,很多时候当人们谈论"软件架构"时,他们常常把这个概念误解为具体的代码结构和程序设计蓝图,而实际上,这种误解忽视了软件架构的更高层次设计------即系统的整体规划。真正的软件架构并不涉及每一行代码的具体实现,而是决定系统的基本框架、结构和各部分如何连接与协作。这一点非常重要,特别是对于初学者来说,因为在学习软件架构时,可能会遇到很多不同的概念和术语,这些术语有时并不指代相同的东西。

因此,软件架构应该更像是一个"城市规划"的过程,而不是单纯的"建筑设计"。它关注的是如何让不同的系统模块和组件互相配合,如何通过设计来最大化系统的效率和可扩展性。通过这种方式,软件架构不仅仅是关于代码本身,更是关于如何规划和设计一个系统的结构,使其能在面对复杂和动态的需求时仍然保持灵活和高效。

定义边界的艺术

在软件架构中,核心目标是对系统进行策略性的设计,主要通过分离边界来组织和优化代码。基本上,架构就是决定系统中各个组件如何分离和如何连接,这种设计不仅仅是为了组织代码,还涉及到如何确保系统高效运行、可扩展和易于维护。

架构设计的一个常见比喻是城市规划。在城市规划中,规划者会决定不同区域的功能,并设置它们之间的交通流线,使得每个区域能够独立运作并且有效地连接到其他区域。在软件架构中,设计者做的就是定义哪些部分属于系统的哪一部分,如何通过接口或通信机制将它们连接起来,确保各个组件之间没有不必要的重叠或耦合。

这种架构设计的目的在于定义系统的边界和关系,明确每个部分应该如何运作。架构的关键任务是定义这些边界和连接模式,从而确保系统的不同部分可以协同工作,而不会因为过度的耦合或不明确的责任分配而导致问题。

总结来说,架构设计的本质是对系统的组织和规划,确保每个组件有明确的职责和适当的边界,这种设计能够使系统更容易维护、扩展,并且能够随着需求变化而做出调整。这与城市规划非常相似,都是通过合理的布局和设计来提高整体的效率和功能。

架构的度量标准

当考虑软件架构时,重用性是一个关键的考量因素。能够设计出容易重用的组件,使得代码可以被拆解并在不同的上下文中使用,是开发中非常重要的目标。通过分离代码,特别是将系统分成多个模块,可以显著提高代码的重用性。例如,如果游戏的底层系统能够被设计成通用的平台层,未来如果需要将该系统应用到其他项目中,重用这些已分离出来的模块会非常简单。

此外,良好的软件架构有助于团队中的劳动分工。在一个大团队中,程序员需要专注于各自负责的模块,如果没有明确的分离和结构,团队成员可能会互相干扰,导致项目进展缓慢。因此,架构设计不仅仅是为了代码的重用,还为了更好的团队协作和分工。

当代码变得越来越庞大时,保持清晰的架构同样至关重要。随着项目规模的扩大,开发者需要在更高层次上管理和组织代码,以避免"耦合"问题。没有明确的分离,不同模块的代码可能会互相依赖,导致更改一个部分时需要考虑整个系统的其他部分,这种情况会随着项目复杂度的增加变得难以管理,甚至会导致灾难性的后果。

另外,程序员在处理大规模项目时,往往面临的是"思维的清晰度"。如果架构设计得当,开发人员不需要在处理每一行代码时都考虑整个系统的每个细节。这种分离和结构化能够减轻大脑的负担,使得开发工作更加高效和可控。

为了进一步提升架构的可维护性,考虑不同类型的耦合至关重要。时间耦合、布局耦合等不同类型的耦合关系,反映了模块之间在执行时的依赖性。一个模块可能需要等待另一个模块的处理结果才能继续执行,这种时间上的依赖关系需要在架构设计中明确规划。此外,不同模块之间的数据格式、执行顺序等也会影响整个系统的性能和可维护性。

架构设计还应考虑到流动性。流动性指的是架构的灵活性和可修改性,能够在需求变更时快速做出调整。这一点对于初期的项目尤为重要,因为初期开发人员可能不确定最终的架构设计是什么,流动性较好的架构允许团队在后续开发中进行必要的调整和改进,从而避免早期过于死板的设计给项目带来的阻碍。

最后,软件架构不仅仅是关于如何处理复杂的代码和模块,还是关于如何规划系统的各个部分能够无缝协作。在设计架构时,必须时刻考虑到系统的可扩展性、可维护性以及开发效率,确保架构既能满足当前需求,也能应对未来可能的变化和扩展。

我们已经做了什么

在游戏开发中,架构的设计是一个逐步积累的过程,特别是当涉及到平台层和原型层时。架构的设计不仅仅是单一阶段的任务,它是一个随着时间推移、根据经验逐渐成熟的过程。

首先,游戏架构的底层通常包括平台层,它与硬件和操作系统进行交互。平台层与图形库等系统组件对接,处理像渲染、更新等任务,但在早期阶段,它可能只是一个简单的原型版本。这个平台层的设计虽然不会达到最终的完善程度,但它能为开发团队提供一个起点,帮助他们在后续的开发中逐步优化和完善。

当设计这个平台层时,首先要了解其最基本的组成部分,包括底层的硬件和系统组件。平台层通常需要处理一些较低级的操作,比如渲染图像、游戏更新以及与声音系统的交互。这些操作是数据流动的关键,数据从游戏逻辑流向渲染模块和声音模块,然后返回游戏模块。在这个过程中,游戏更新、渲染和声音处理是核心的功能模块,虽然这些模块在初期阶段可能仅仅是粗略的实现,但它们为后续的完善提供了一个框架。

值得注意的是,虽然初期的架构设计可能看起来相对简单或甚至是"作弊"的方式,但这并不是因为缺乏设计深度,而是因为开发者对这种架构有非常明确的理解和经验。通过多次的开发经验,开发者已经能够迅速识别出最合适的架构方式,并能够在初期阶段就实现一个可行的原型,而不需要从零开始不断摸索。

最终,随着开发的深入,平台层将变得更加复杂和优化,可能会涉及到硬件加速、复杂的渲染管线等问题,但在早期阶段,这些问题可能并不会立即需要解决。开发者的目标是创建一个有效的原型,使得游戏能够快速迭代和测试,而后续的优化和细节完善将随着项目的深入而逐步进行。

这种架构设计过程更多的是关于如何将自己的经验和技巧应用到开发中,而不是一开始就需要做到完美。在实际开发中,很多决策都是基于经验的积累和对现有技术栈的熟悉程度。

接下来会做什么

在游戏开发中,设计架构时需要考虑到目标和需求,尤其是当涉及到游戏的规模和复杂性时。开发者已经有了明确的目标,并且在构建游戏时会更加幸运,因为他们有一定的经验和框架可以依赖。

然而,即使有过开发经验,每款游戏的架构都是独特的,取决于游戏的需求和目标。不同的游戏需要不同的架构设计,这也意味着即使是有经验的开发者,也不能预先完全确定一款游戏的架构应该如何设计。每个项目都会带来不同的挑战,需要在开发过程中根据具体需求逐步调整和优化架构。

在接下来的几个月里,开发者将遵循自己通常的步骤来改进和优化游戏的架构。这个过程是逐步的,随着游戏开发的深入,架构设计也会逐渐演变,逐步完善。这些步骤的核心目标是展示如何在实践中设计和调整游戏架构,并根据项目的实际需求进行改进。

总体而言,游戏架构设计是一个动态的过程,需要在不同阶段根据游戏的规模、复杂性以及目标逐步改进。尽管有经验,但每个新游戏仍然需要从基础开始,根据特定的需求来调整和优化架构设计。

原则

在接下来的讨论中,主要关注的是游戏架构的一些核心原则,特别是基于经验得出的设计方法和技巧。这些原则将帮助构建一个更加稳定和高效的游戏架构。接下来的讨论将逐步展开,并探索如何在实际编程过程中应用这些设计思想。

首先,游戏架构的设计通常从一些基本的技术开始,例如如何更新游戏状态、渲染图形和处理用户输入。游戏更新和渲染是游戏架构的核心组成部分,这两个步骤通常会紧密相连,并通过一定的调用链完成。例如,更新游戏状态时,通常需要处理用户的输入,并在此基础上修改游戏的状态,然后进行渲染。

在实际的架构设计中,游戏更新和渲染往往不会被视作单独的调用,而是作为整体流程的一部分来处理。这种方式不仅简化了架构设计,还能够提高执行效率。在此过程中,声音输出和图形渲染也紧密相关,并且通常在相似的过程中进行处理,尤其是在复杂度较高的图形渲染任务中。

另外,游戏架构设计中的一些基本概念,例如如何高效地管理游戏资源和操作系统层的交互,都会在这个阶段得到充分的讨论和实践。平台层的工作是支撑整个游戏架构的重要部分,所有的游戏逻辑和操作系统交互都通过平台层来完成。

总体来说,游戏架构设计涉及到很多细节,尤其是如何处理游戏更新、渲染以及用户输入的过程。这些设计原则在实际编程过程中将不断被调整和优化,以确保游戏能够在不同平台和硬件上稳定运行。

输入 -> 更新 -> 渲染

在游戏架构设计中,处理用户输入、更新全局状态和渲染图形是核心的三大环节,且这三者的顺序和执行方式对游戏的流畅度和响应性至关重要。首先,必须尽量减少输入延迟,因此用户输入应该在游戏更新之前尽早处理。如果输入延迟较大,可能会导致玩家的操作与游戏反应之间产生滞后感,从而影响游戏体验。

理想的游戏循环是将用户输入直接集成到游戏更新过程中,而不是先更新游戏状态再回过头来处理输入。如果这样操作,就会导致一个游戏帧的延迟,使得游戏未能及时响应玩家的操作。因此,游戏架构通常设计成先处理输入,再更新世界状态,并最终进行渲染。

对于更新和渲染的独立性,一些游戏架构可能会选择将它们分开进行。这种做法在意识形态上是合理的,特别是在某些情况下,开发者可能希望更新游戏状态和渲染它的画面是独立的操作。例如,可能会在不渲染游戏的情况下进行多次更新,或者反之,只进行一次更新和多次渲染。这样的架构能够为开发者提供更多灵活性,尤其是在需要进行复杂操作或优化时。

总的来说,游戏架构中的输入处理、状态更新和渲染之间的关系需要根据游戏的需求进行合理设计,尤其是在多线程和优化方面,需要特别注意它们的执行顺序和如何减少延迟。

关联性和缓存

在游戏架构设计中,更新和渲染的关系一直是一个关键问题。一般来说,有些架构将更新和渲染分开执行,但这种分界并不总是最有效的。事实上,分开这两个环节可能会导致效率低下,尤其是在处理复杂的游戏元素时,比如粒子系统。举个例子,当一个游戏需要同时处理大量粒子时,每一颗粒子都需要进行物理更新,并且更新后的数据会被发送到图形卡进行渲染。如果更新和渲染是分开进行的,那么每颗粒子可能会在更新和渲染阶段分别被加载到缓存中,这样就会增加缓存的丢失和重新加载的时间,从而降低效率。

因此,在设计游戏架构时,尽量避免将更新和渲染分开进行,而是将它们视为一个统一的步骤。这样可以减少缓存缺失的情况,并提高性能。实际上,许多现代计算机系统都非常依赖缓存,因此尽量避免不必要的数据重复加载是优化性能的关键。如果可以在一次更新过程中同时完成物理计算和渲染准备,那么整体性能会大大提升。

此外,渲染本身不仅仅是图形显示,还包括准备图形数据以供最终渲染的过程。在游戏的初期开发阶段,这一过程通常被称为"渲染准备",而不仅仅是图形显示本身。因此,虽然"渲染"在许多文档中通常指的是最终显示图像的步骤,但在实际开发中,它可能涉及多个步骤和缓冲区处理,这些都属于"渲染准备"而非最终显示。

总结来说,现代游戏架构设计应避免不必要的更新和渲染分界,而应将这两个步骤紧密结合,优化性能和缓存使用。这种方法有助于提高游戏的流畅度,减少延迟,并实现更高效的资源管理。

资源:加载 vs 流式传输

在游戏开发中,架构设计需要考虑多个方面,尤其是如何处理输入、更新和渲染。通常,在游戏的框架中,输入是一个独立的步骤,通常不涉及性能关键的计算,只是为了设置一些状态,供后续更新使用。这使得输入处理成为一个与更新和渲染分开的部分,类似于玩家控制代码。

然而,在架构设计中,除了输入处理,另一个关键问题是如何管理游戏中的资源。对于游戏来说,资源的加载是至关重要的,这可能会采取不同的架构方式。常见的两种方式是加载屏幕架构和流媒体架构。

加载屏幕架构是一种比较传统的方式,通常在游戏切换关卡或发生大规模事件时使用。玩家会进入一个加载屏幕,游戏系统会在后台加载所有必要的资源,直到加载完成后才继续游戏。这个过程中,游戏的主要架构就是分成两个独立的系统:加载资源的系统和实际运行的游戏系统。资源通常从存储介质(如硬盘)加载,而这个过程与游戏其他部分是完全分开的,因此架构设计上没有太多需要关注的复杂性,主要问题在于如何优化资源加载的速度。

与此不同,流媒体架构则是更为现代的方式,它允许游戏在不间断的情况下继续运行,游戏的资源是在后台持续加载的。这种架构支持更大的游戏世界,玩家在游戏过程中并不需要等待加载屏幕的出现,而是通过流媒体的方式,在需要时动态加载资源。例如,玩家可以在虚拟世界中自由漫游,而背景中的资源则会根据玩家的需求逐步加载。

流媒体架构要求更加复杂的通信机制,因为游戏系统和资源加载系统需要通过双向通信保持同步,确保资源在需要时能够及时加载并且被游戏系统使用。这个过程需要高效的设计,保证资源加载不干扰游戏的运行,而玩家也不应感受到加载的延迟。

总结来说,加载屏幕架构适用于那些对资源加载有明确需求且不要求完全无缝体验的游戏,而流媒体架构则适用于需要大规模动态加载资源且要求无缝体验的现代游戏。在架构设计时,必须考虑如何管理这些资源的加载和同步,以便在不同类型的游戏中获得最佳的性能和玩家体验。

即时模式 vs 保留模式

在开发系统时,设计的架构通常有两种模式:即时模式(Immediate Mode)和保留模式(Retained Mode)。即时模式指的是在调用时立即进行处理,调用者不需要关心系统中对象的生命周期或管理它们的存在。换句话说,调用者只关心当前的操作,不需要记住和管理对象的状态或生命周期。例如,在图形渲染中,程序可以即时设置和调整渲染参数,而不需要关心这些参数在何时被释放。

保留模式则不同。在这种模式下,系统会持续保留对象的状态和生命周期,调用者需要管理对象的生命周期,并且系统会在适当的时候执行清理或释放工作。例如,在图形渲染中,开发者可能会创建一个纹理绑定,并需要在之后明确释放它,这意味着程序必须管理对象的存续时间,确保对象不在不再需要时仍然存在。

两种模式各有利弊,选择哪种模式取决于具体的需求。即时模式通常简化了编程,因为调用者不需要管理生命周期,但也可能导致内存使用上的不效率或重复的操作。保留模式虽然能更好地管理资源和对象,但开发者需要额外的工作来确保生命周期管理得当,从而避免资源泄漏或不必要的复杂度。

在设计系统时,理解这两种模式的差异并选择合适的方式至关重要。每个决定都会对系统的性能和可维护性产生深远影响,因此需要根据具体需求来决定使用哪种模式。

即时模式/保留模式像是IoC(控制反转)/依赖注入吗?[注:不是的 -ed]

虽然涉及到两种不同的设计方法,但并未深入讨论它们如何在某些框架或系统中实现。提到 iOS 中的依赖注入(Dependency Injection)和控制反转(Inversion of Control),这些技术常用于解耦和管理类之间的依赖关系。依赖注入是一种将外部依赖传递给类的方式,而控制反转则是指将控制流程从代码中移除,交给外部容器或框架来处理。

尽管这些概念在某些开发框架中常见,但并没有详细解释它们与即时模式或保留模式的关系。由于对这些术语不熟悉,因此无法提供更深入的解释。

当用户可能无法达到30fps时,我们使用固定时间步长可以吗?

在讨论固定时间步进式更新渲染时,提到的一个问题是如何应对错过目标帧速率。如果终端用户的设备足够慢,无法达到目标帧率(如30帧每秒或60帧每秒),该系统如何反应。这提出了一个关于稳定性和流畅度的问题,特别是当帧速率低于30帧时,游戏画面会显得卡顿或不稳定。因此,设计中应避免让游戏在低于30帧每秒的帧速率下运行。

如果无法保持30帧每秒,建议的做法是将渲染过程放在一个单独的线程中,或者采用其他优化方法,使图形更新的频率低于输入更新的频率,从而在设备性能较低时依然保持一定的流畅度。然而,输入操作不应低于30帧每秒,因为这会导致游戏交互不流畅,严重影响玩家体验。

总的来说,理想的做法是确保游戏至少在30帧每秒的帧率下运行,特别是在涉及图形渲染时,避免因图形帧数过低导致的显著画面卡顿。

游戏不会在固定时间步长下运行:我们将把 t 作为参数传入

在讨论游戏架构时,指出了一点:游戏并不依赖于固定的时间步进更新,而是可以在任何帧速率下运行。这意味着,游戏的更新机制并不是围绕固定的时间步长设计的,而是基于实际的帧速率进行更新。

游戏能够适应不同的帧速率,这就意味着可以在较低的帧速率下运行,只要系统能够适应。关键是在于确保图形质量不会影响游戏帧速率的稳定性。如果图形的复杂度或渲染需求导致帧速率无法达到30帧每秒,那么应该通过降低图形质量或缩减图形的复杂度来确保游戏仍然能够稳定地以30帧每秒运行。

这种灵活的架构设计使得游戏可以在不同的硬件上运行,不受固定帧率的限制。

你们会使用虚拟文件系统来存储资源吗?

虚拟文件系统(VFS)这个术语常常被使用,但实际上它有些过于宽泛和模糊,尤其是在游戏或应用开发中。通常人们将它与一些包装形式(如 ZIP 文件)混为一谈。当我们谈到"虚拟文件系统"时,很多时候它并不真正代表一个具备完整文件系统功能的实现,而只是指某种封装的资源存储方式。例如,使用压缩文件(如 ZIP)来打包数据,这种方法并没有实现一个真正的文件系统,虽然它可能看起来像是"文件系统"一样的功能------如存储和访问文件。

真实的文件系统不仅仅是一个简单的打包结构,它应该具备许多特性和能力,比如目录结构、权限管理和动态访问等功能。简单地将数据打包成一个文件并通过简单的存取方式处理,并不能称为一个完整的虚拟文件系统。

在更新时渲染:如果在更新底部我们知道的某些东西会改变,如何处理渲染顺序问题?

当处理粒子的更新和渲染时,通常的建议是不要将更新和渲染过程混合。原因在于,若在渲染前进行更新,可能会出现一些不一致的情况,例如,子弹击中玩家的检测可能会由于更新顺序不同而导致动画不同步。举个例子,如果先渲染了玩家的状态,然后再更新子弹和玩家之间的碰撞检测,玩家的反应动画可能会显示延迟一帧,导致不自然的效果。

这个问题往往源于更新循环的设计,如果循环是基于遍历实体列表并按顺序逐个更新,那么可能会出现不符合预期的行为,尤其是在多个实体之间存在依赖关系时。虽然这种方法可能是许多游戏的常见做法,但它并不是最佳实践。为了避免这种问题,更好的方法是通过一种结构化的方式来确保每个更新步骤正确地处理相关实体的状态,而不是依赖于简单的顺序遍历。

将更新和渲染分开,并确保每个步骤的顺序和依赖性得到妥善处理,是构建稳定且流畅游戏更新循环的关键。

更新依赖性是一个通用问题,应该由架构师来解决

在游戏的更新和渲染过程中,处理依赖关系是非常重要的,尤其是在多个对象之间存在相互依赖时。例如,如果玩家和子弹的更新顺序不对,可能会出现玩家在子弹击中后仍然可以继续移动的情况。假设子弹击中了一个按钮,打开了门,而玩家正在朝着那扇门走,如果在更新中没有正确处理这些依赖关系,玩家可能会穿过门时被停住,导致游戏逻辑不一致。

因此,更新的依赖关系需要在更新循环中被适当处理。这些依赖关系与渲染无关,它们本质上与游戏状态的变化和更新顺序相关。即使将渲染完全从更新中分离出来,依赖关系仍然是不可忽视的。正确处理这些依赖关系是保证游戏更新正确性和一致性的关键,无论是否涉及渲染。

解释一下这个变量

更新和渲染步骤在游戏架构中不一定需要强烈区分。其实,理想的做法是,更新和渲染可以并行进行,而不必预先规定它们不能同时发生。游戏架构设计中不做这种强制性的区分,可以使得更新和渲染的操作更灵活,允许它们在适当的条件下共同进行。

在某些情况下,更新和渲染的顺序可能会有所不同。例如,某些操作的更新可能需要在渲染之前完成,但这种顺序依赖关系并不意味着必须在架构上做出严格的区分。如果可以同时执行更新和渲染,且两者都能够正确进行,那么就没有必要强行分开它们。最终,是否分开这些步骤不应该基于架构上的强制要求,而应更多地取决于性能和需求的考量。

关于变量的存在和作用,提到了一点是,很多时候游戏引擎中的变量实际上是在极短的时间内执行的,我们很难直接测量和观察它们。即使它们确实存在,我们只能通过间接的方式感知它们的影响,而无法直接学习或解释这些变量如何在游戏引擎中有效运作。

如何处理更新/渲染在不同帧率下的问题?

关于如何处理游戏的更新和渲染,尤其是在面对不同的帧率(如60fps或20fps)时,这并不构成一个特别复杂的问题。如果关注的是让游戏始终在相同的时间步长下运行,而不关心渲染阶段的具体时间,那么问题的关键是如何计算和处理更新。

如果游戏的更新与渲染需要同步,可以通过调整时间步长来实现。例如,更新的时间步长可以设置为每秒60帧(1/60),或者如果帧率较低(如20fps),更新的时间步长则为1/20。这种方式并不会要求改变游戏的核心架构,只是调整更新的频率和渲染的时间间隔。

从架构的角度来看,最大的区别在于如何处理时间变量(如t变量)。无论是60fps还是20fps,更新的核心逻辑保持不变,只需要根据当前帧率调整相应的时间增量。这样就能确保游戏的行为与渲染的时间步长相匹配,而不需要对整个系统进行大的修改。

当你说将更新/渲染一起做时,是指每个实体都这样做吗?

当谈到更新和渲染时,并不是每个更新都是在单一对象或实体的层面上进行的。更新的粒度可以根据系统的需求来确定。对于某些系统,如粒子系统,可以选择一次性更新整个系统,而不需要逐个对象处理。

对于对象本身,可能存在一个依赖关系图,更新时需要遍历这些依赖。不同的系统可能有不同的更新要求,具体的更新逻辑可能与对象的关系或依赖有关。重点在于不将更新和渲染强行分开,而是在更新的粒度上提供灵活性。系统的每个部分都有可能选择将渲染操作绑定在更新过程中,或者延迟渲染到稍后的步骤。

本质上,这种方法避免了在架构上人为地划分更新和渲染的步骤,而是允许根据需要灵活调整。

我们计划做网络功能吗?

网络处理和游戏循环的设计可以视作独立的内容。网络可以被视为一个单独的流,在游戏结束后进行处理。不过,在讨论游戏循环的相关内容时,网络并不是一个主要议题,因为它涉及的范围和复杂度不同。因此,不打算将网络集成到当前的讨论中,因为网络是一个完全独立且复杂的主题,不容易在有限的时间内深入探讨。

使用内存映射的虚拟文件系统是个坏主意吗?

内存映射虚拟文件系统(Memory-mapped virtual file systems)有一些问题,特别是在32位系统上。如果使用超过4GB的音视频数据,无法有效地处理文件,因为32位系统无法访问超过4GB的内存。即便在64位系统上,内存映射文件也有其局限性,尤其是当文件内容需要分块映射时,这种做法会增加复杂度。为了避免这种问题,通常更倾向于使用传统的文件加载方式,而不是依赖内存映射文件。

尽管内存映射在某些应用场景下有效,比如录音中的数据存储,但在游戏开发中,它的问题在于无法精确控制数据的分页更新,因此不被推荐用于频繁的数据访问或游戏资源加载。

能解释一下流式传输是如何适应 GUAR 吗?

内存映射文件系统的问题之一是失去了对操作系统读取操作的控制,这可能会导致不必要的复杂性。为了避免这一问题,使用加载屏幕或资源流式加载(streaming)是更常见的做法,尤其是当需要处理大量数据时。通过这种方式,可以在后台进行资源加载,同时确保游戏流畅运行,而不是依赖于内存映射。

在具体实现中,资源加载是通过一个队列来处理的,资源加载器会在后台线程中运行,允许游戏按需加载资源。比如,在游戏中一个物体(如立方体)可以向这个队列写入数据,启动资源加载过程。这个方法确保了游戏的流畅性,同时也提供了后台加载的机制,使得资源的加载不会阻塞主线程。

结合更新和渲染会限制多线程吗?

在讨论游戏引擎的线程设计时,存在一个关键的问题:如何利用硬件并行性。游戏引擎通常会将渲染准备工作放在单独的线程中,这样做可以利用多核CPU的并行计算能力。然而,这种分割并不总是最佳的,特别是因为缓存一致性问题(coherency problem)。这意味着,如果渲染和更新工作分开处理,可能会导致缓存失效,从而影响性能。

在某些场景下,像流媒体这种场景,可能不会受到太大影响,但通常而言,渲染和更新最好还是能够统一处理。如果考虑到性能问题,设计时应该谨慎选择是否分割这些任务。

有两种方式可以实现并行处理任务。一种方式是将所有任务(如更新和渲染)放在不同的线程中执行,另一种方式是将它们按照某种顺序组织成矩阵结构,可以选择在更新和渲染之间实现并行。理解如何在代码中实现这些多线程性能,特别是从单线程代码转换到多线程处理,是优化游戏引擎性能的关键步骤。

为了更好地理解这一点,可以将其看作是一个矩阵的结构,任务可以按更新、渲染的顺序进行划分,确保并行处理时的资源和性能分配最优。

关于线程的论述

在多线程处理游戏更新和渲染时,有不同的设计方式,具体取决于如何划分任务和处理依赖关系。首先,如果按照单线程方式处理,所有的更新和渲染都按顺序执行,这种方式虽然简单,但无法利用硬件并行性,效率较低。

在多线程设计中,可以选择将任务分成多个线程,比如将更新和渲染分别分配给不同的线程。这样做的优点是可以并行执行任务,但存在一些问题。首先,更新和渲染之间往往存在依赖关系,更新必须先完成,渲染才能进行。因此,即使任务分配到不同的线程,更新和渲染依然需要按顺序执行,无法完全并行。

此外,这种分割方法可能导致数据缓存的多次访问。如果更新和渲染使用相同的数据,线程需要多次加载相同的数据到缓存中,浪费了内存带宽和CPU时间。特别是当更新过程中产生的中间结果被渲染线程需要时,可能会造成不必要的重复计算和数据传输。

另一种可能的方案是将任务分解成更细粒度的部分,按行(row-wise)来处理。通过这种方式,依赖关系依然保留在代码内,但不同任务的执行顺序和依赖性更容易控制。这样可以避免缓存的多次访问和不必要的数据重组,同时保持任务划分的并行性和高效性。

总的来说,虽然多线程设计可以提高性能,但必须考虑任务之间的依赖性和缓存一致性问题。如果任务分割过于复杂,可能导致性能下降,因此要根据具体情况灵活选择处理方式。

由于人眼只能看到70fps [需要引用 -ed],我们应该设置最大帧率吗?

人眼的感知能力并不是固定的,一般来说,人眼能感知的帧率在70帧/秒左右,但实际上,它能够察觉到的帧率可以高达90帧/秒。因此,在某些情况下,即使游戏的帧率超过70帧/秒,提升帧率仍然有意义,尤其是在平滑度和响应性方面。

然而,设置最大帧率时,并不仅仅是为了匹配人眼的感知极限。最大帧率的设置更多是出于技术上的考虑,特别是在浮点数的精度问题。当帧率达到一定高度时,浮点值可能会出现误差,影响计算的准确性,因此需要为帧率设置上限,以确保时间步长的计算不会出现精度问题。

尽管如此,70帧/秒并非硬性上限,只是一个普遍认为的最大值,实际帧率可能会更高,尤其是在高刷新率显示器和优化良好的硬件上。因此,帧率上限的设置更多是为了避免浮点精度问题,而不是简单地为了适应人眼的感知能力。

我们将如何处理垂直同步(vsync)?

为了处理垂直同步(v-sink)的问题,代码已经以一种特定的结构进行设置,以确保在切换到使用OpenGL时能够自然地工作。具体来说,代码实现了自旋锁(spin lock),通过等待来确保在理想的时间步长内完成计算,通常是在每秒的16分之一秒(即每帧的理想时间)。

未来的计划是在代码中加入名为""的机制,专门用于处理垂直同步问题。这将确保图形渲染与显示器的刷新率同步,避免画面撕裂等问题。

由于代码已经为此做好了结构上的准备,未来启用垂直同步时,不需要进行大量的额外工作。可以直接在现有架构下实现,确保流畅的渲染和游戏体验。

你不能将组存放在一起以便在分开更新/渲染时节省些时间吗?

为了避免缓存未命中的问题,渲染和物理组件的数据需要分开管理。虽然物理和渲染使用的组件是相同的,但它们的功能不同,因此需要分别处理。在物理计算中,会遍历并更新物理组件,确保物体的速度、位置等状态得到更新。而渲染组件则会使用这些信息来决定如何渲染这些物体。

例如,物理组件需要更新物体的位置和速度,而渲染组件需要知道物体的最终位置,以便正确显示。如果涉及运动模糊或其他视觉效果,可能还需要使用物理计算中的一些额外数据。不过,物理和渲染数据集并不是独立的,而是共享同一套数据。

但是,在实际操作中,物理计算和渲染并不总是完全共享相同的数据,某些物理数据渲染系统不会使用,反之亦然。因此,在设计时需要注意区分和优化数据的使用。

尽管这些数据是共享的,但如果不小心管理,可能会导致缓存重载,从而浪费性能资源。为了提高效率,避免不必要的缓存重载是非常重要的,尤其是在频繁更新物理和渲染数据时。如果能有效地管理和共享这些数据,系统将更加高效。

异步任务系统?

任务系统(job system)并不一定是每个游戏都需要的,尤其是在特定硬件平台上。对于一个目标平台如树莓派,由于只有一个核心,游戏不适合过度依赖多核处理,因此不能使用复杂的多核任务系统。对于这样的平台,通常会选择一些更加合理和简化的方式来处理任务,避免复杂的多核处理带来的性能问题。

在这种情况下,游戏可能不需要一个完整的基础工作系统,而是依赖于一些基本的任务调度机制。主要的工作负载(如渲染)仍然可能是性能瓶颈,但在其他更强大的平台上,可能会使用更深度的多线程和并行处理能力。此外,资产流(asset streaming)也并不完全依赖于多线程,而更多是关注于如何在后台管理数据流和加载资源的策略,避免在单个线程中发生资源阻塞。

因此,游戏的任务和资源管理方式会根据目标平台的硬件限制和需求进行调整,避免过度设计一个复杂的多核任务系统,确保在低性能平台上运行流畅。

我们会做物理引擎吗?

开发过程会从明天开始,重点是编写渲染代码,虽然游戏的核心代码还需要一段时间才能开始开发。渲染和游戏代码的工作会交替进行,目前还没有开始编写渲染相关的代码。如果计划中包括物理引擎的开发,虽然物理引擎需要处理游戏中的运动,但不会涉及复杂的三维物理。游戏本身是二维的,所需要的物理处理相对简单,类似于基础的物理计算,并不会涉及像"混乱"之类的复杂物理效果。因此,物理引擎的开发会根据游戏的需求进行,而不是做过多的额外工作。

相关推荐
XZHOUMIN几秒前
MFC中如何在工具条动态增加菜单
c++·mfc
R6bandito_13 分钟前
Qt几何数据类型:QLine类型详解(基础向)
c语言·开发语言·c++·经验分享·qt
禊月初三19 分钟前
C++面试突破---C/C++基础
c语言·c++·面试
程序猿阿伟1 小时前
《平衡之策:C++应对人工智能不平衡训练数据的数据增强方法》
前端·javascript·c++
CodeGrindstone1 小时前
Muduo网络库剖析 --- 架构设计
网络·c++·网络协议·tcp/ip
9毫米的幻想1 小时前
【C++】—— set 与 multiset
开发语言·c++·rpc
想成为高手4991 小时前
深入理解AVL树:结构、旋转及C++实现
开发语言·数据结构·c++
£suPerpanda1 小时前
P3916 图的遍历(Tarjan缩点和反向建边)
数据结构·c++·算法·深度优先·图论
码农老张Zy1 小时前
【PHP小课堂】学习PHP中的变量处理相关操作
android·开发语言·学习·php
IT古董1 小时前
【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法
人工智能·学习·算法·决策树·机器学习·分类