游戏引擎学习第221天:(实现多层次过场动画)

发布于:2025-04-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

资产: intro_art.hha 已发布

在下载页面,你会看到一个新的艺术包。你将需要这个艺术包来进行接下来的开发工作。这个艺术包是由一位艺术家精心制作并打包成我们设计的格式,旨在将这些艺术资源直接应用到游戏中。它包含了许多我们会在接下来的直播中使用的艺术资源。

艺术包的大小约为100MB,如果还没有下载的话,建议你尽快下载。这个包里包含了许多图形资源,我已经下载并解压了它。在我的机器上,我有这个压缩文件,它约为108MB,解压后会变成约500MB左右。解压后,资源文件被放入了项目的data目录中,里面包含了之前用来测试系统的HHA文件。
我是放这个目录下来
在这里插入图片描述

在这里插入图片描述

cmake 会自动拷贝到执行文件的目录
在这里插入图片描述

https://download.csdn.net/download/TM1695648164/90603893

介绍这个艺术包

原本正在进行关于zib的一些代码开发,但决定先把那部分内容暂时放一边,先来聊一聊艺术资源包的事情。

所以这次主要是想专注于艺术资源包的介绍。现在既然这个资源包已经发布了,我们就来看看如何使用它,特别是为了那些想要自己尝试玩一下这些资源的人,让他们了解具体的操作和流程。

接下来会构建游戏并运行一下。按我们之前的实现逻辑,所有的 HHA 文件——也就是我们资源打包的格式——会被游戏引擎自动加载。所以,只要运行游戏,程序就会自动去读取数据目录下的这些 HHA 文件,并将它们全部加载进来,无需我们再写任何额外的代码。所有资源会被识别并视为可用内容,自动生效。

当然,要做到这一点,我之前在制作这个数据包的时候也做了不少工作。我们接下来要看看这些资源是如何组织的,并展示怎样才能利用它们进行实际开发和测试。这个过程不仅方便理解系统如何处理资源,也为之后自定义和扩展内容打下基础。

game_file_formats.h: 将新的资产类型和标签添加到 asset_type_id 和 asset_tag_id

我们现在处理的这个艺术资源包中,包含了一些我们尚未命名的新内容。回忆一下我们在资源系统中命名内容的方式,我们会为每个资源指定“资源类型”和“资源标签”。这些信息存储在文件格式的定义中,正是通过这些资源类型(asset types)和标签(asset tags),我们才能在游戏中识别和加载对应的资源。

为了处理这次新的资源包,我们新增了一些资源类型,以便准确描述新加入的内容。例如,添加了一个用于“开场动画”的资源类型,这个类型专门用于存储和开场动画相关的图像或数据。

此外,还增加了两个新的标签:一个叫做“镜头索引(shot index)”,另一个是“图层索引(layer index)”。这样做的目的是为了在开场动画中标识每一帧或每一组画面中的位置与图层结构。通过这种方式,我们可以明确地知道某个资源对应的是哪个镜头,以及它在画面中属于哪一层。

总之,整个设计思路是通过扩展资源系统的类型与标签体系,使其能够支持更复杂的资源组织结构,尤其是动画或过场画面这类需要层次和时间轴管理的内容。这样不仅方便资源加载,也为后续的编辑与播放提供了清晰的结构支持。

在这里插入图片描述

在这里插入图片描述

Blackboard: 开场动画艺术

我们这次的资源整理,是围绕着一个“开场动画”的概念进行的。具体来说,我们设想的是一个具有专业品质的游戏应该在吸引模式(Attract Mode)下有一个开场动画,这种动画能够在玩家开始游戏之前营造氛围,传递故事背景或情绪。而我们目前的任务,就是先把这个开场动画做出来。

从开发角度来说,开场动画是一个相对简单的切入点。相比之下,游戏中实体与实体之间的交互需要更多复杂的逻辑,而动画更像是按脚本播放的一系列画面。这样我们可以先进行一些基础的动画开发练习,为后续更复杂的互动系统开发打好基础。

为了实现这个动画,我们设计了一套资源结构。动画由一系列“镜头”(Shots)组成,每个镜头可以理解为一个画面场景。镜头的数量不定,从镜头1到镜头N,每一个镜头都包括若干“图层”(Layers),这样我们就可以实现更丰富的画面效果。

图层的存在使我们可以构建出类似“视差滚动”的视觉效果。例如,一个镜头可能由前景的人物、中景的建筑、远景的山丘组成,这些图层可以独立移动,从而在简单动画中增加深度感与专业感。这比单纯地展示一张静态图像更有视觉冲击力,画面层次感更强,看起来也更有制作水准。

为支持这一结构,我们在资源系统中做了以下处理:

  • 为所有属于开场动画的资源统一指定了一个新的资源类型,例如 opening_cutscene
  • 每个资源还被附加两个标签:一个是 shot_index(镜头索引),另一个是 layer_index(图层索引)。

有了这些类型与标签之后,我们在程序中就可以通过指定镜头编号与图层编号,快速定位和提取我们想要展示的图像素材。比如,如果想要获取第3个镜头中的第2层素材,只需查找 opening_cutscene 类型资源,标签中包含 shot_index=3layer_index=2 的即可。

此外,当我们首次运行游戏并加载这个资源包时,系统会进行一些断言检查,比如资源数量是否超过了之前定义的上限。由于我们新增了许多素材,资源总数超过原定值,因此触发了断言错误。我们需要根据新的资源规模调整系统配置,以容纳这些新增的内容。

总之,这次的资源包为我们建立了一个完整的开场动画结构:镜头分段,图层分层,具备视差效果,所有资源都通过类型与标签精准分类,为后续动画播放与扩展打下了清晰的基础。这不仅有利于后续的程序设计,也提升了整体的专业感和可维护性。

重新编译并检查运行是否正常

我们重新编译之后,程序可以正常运行,并且新的 .hha 资源包也已经加载进来了。虽然这个资源包解压后有接近 500MB 的体积,但我们并不是一次性加载所有数据,而是采取了按需加载的机制。

目前的系统是运行在传统 7200 转的机械硬盘上,不是现代的固态硬盘,数据吞吐并不快。如果真要一次性加载 500MB 数据,是绝对做不到启动瞬间完成的。但从实际情况来看,运行游戏的瞬间加载速度非常快,几乎是立即就完成了,因此我们可以确定程序并没有一次性读取全部图像数据。

实际上,我们只是读取了 .hha 文件的头部信息,并合并到现有的资源表中,使得游戏知道有哪些资源是可以用的,但并没有真正把图像或其他内容载入内存。只有在需要使用具体资源时(比如图像要显示到屏幕上时),才会真正去加载对应的数据。

接下来的任务,就是演示如何真正加载并使用这些多图层的内容。我们会在流的后续部分尝试读取这些资源并进行展示,实现在前面提到的那种“视差图层”的视觉效果。

要进行资源加载,首先需要检查当前资源管理器系统的工作空间设置。在我们的代码中,有一个资源管理器的初始化函数,我们给它传入了一个用于存储资源的预期内存大小。

从当前代码来看,我们给它配置了 16MB 的资源内存上限。这个数值适合测试阶段,尤其在资源系统尚未完全稳定的情况下,用较小的内存分配可以更容易暴露 bug。但现在我们的资源包变得更大,加载图层数量也变多,16MB 显然不够用了。

我们接下来要把这个值调大一些,使其足以支撑我们加载实际使用的图层资源。

另外,我们之前制作这些艺术资源的时候,考虑的是以 1080p 分辨率为目标平台来绘制和导出图片。这也意味着单张图像可能就已经相当大了,分辨率越高,占用的内存自然越多。因此我们在分配资源空间时,也要考虑实际图像资源的尺寸,以避免运行时内存不足。

总结来说,这部分的核心内容包括:

  • 使用按需加载机制,避免一次性读取大资源包;
  • 初始加载仅解析 .hha 文件头,构建统一资源表;
  • 资源数据真正使用时才加载;
  • 当前资源系统的内存配额太小,需要根据图像分辨率和资源规模调整;
  • 后续将开始尝试加载图层资源,展示开场动画中的多图层效果。

这将为我们正式开始构建更完整的开场动画提供技术基础。
在这里插入图片描述

Blackboard: 计算剧情动画所需的空间

我们使用的是 1080p 分辨率,也就是 1920x1080 的画面尺寸。每一个像素包含 RGB 和 Alpha 四个通道,因此每个像素占用 4 个字节。这意味着一张完整分辨率的图层,单张图像就需要消耗的内存量可以这样计算:

1920 × 1080 × 4 = 8,294,400 字节,也就是大约 8MB

所以,我们每一个图层的大小大约是 8MB。如果这个图层还有一些超出屏幕范围的部分,比如做出平移(pan)效果时要展示额外内容,那么它的尺寸就可能会更大,内存需求也会随之增加。

而我们在前面资源系统中只为资源预留了 16MB 的内存,这意味着什么?这意味着,我们连加载 一组图层都不够用。因为仅一个图层已经接近 8MB,如果有多个图层,例如一个镜头画面中有三层甚至更多图像,内存就立刻爆表。换句话说,一个完整的镜头画面都无法被加载进内存,资源系统就无法正常运行。

这也清楚地说明了,之前的配置只适用于测试阶段或非常小的资源,现在实际进入图像资源加载阶段之后,必须根据图像分辨率和图层数量合理分配资源内存,否则系统无法工作。

所以总结:

  • 1080p 图像每张约占 8MB 内存;
  • 多图层结构意味着每组画面远超 8MB;
  • 之前配置的 16MB 资源空间过小,不足以加载任何完整镜头;
  • 为了能正确运行,需要提高资源管理系统的内存上限;
  • 图层有可能因为动态视差或移动效果而比屏幕更大,进一步增加内存需求。

接下来调整资源空间的分配,就可以为后续的图层加载和动态渲染做好准备。

在这里插入图片描述

game.cpp: 增加分配给资产的内存

现在我们确实需要将资源系统的工作集内存设置得更合理一些。之前的配置太小,不足以支撑我们当前对素材的使用需求。因此我们现在设置一个更实际的内存分配,例如 256MB,就能保证系统在加载多个图层时不会频繁进行资源置换(thrashing)。

举个例子,如果每个图层占用 8MB,256MB 大约可以容纳 32 个图层,这对一个过场动画(cutscene)来说基本可以满足需求,虽然将来可能还会需要更多空间,但目前已经足够使用。

这种改动不会影响游戏的实际运行流程,它只是让资源管理系统在运行过程中更从容,不必因为内存不足而不断清理和重新加载资源,从而提高运行效率。

当然,将来我们还是会把内存配置调小,回到像 16MB 这样的数值,来测试资源系统在低内存环境下的表现,这样能帮助我们发现和修复资源系统中的潜在问题。现在暂时不这么做,是因为我们当前的重心是完成其他系统的开发,不希望资源系统的不稳定影响进展。我们先假设资源系统工作正常,等以后再做细致测试和优化。

目前资源接口已经搭建好了,我们已经可以围绕它进行其他系统的开发,比如接下来要实现的过场动画(cutscene)支持功能。我们调整好资源内存之后,就准备进入正式的 cutscene 编码阶段了。下一步,我们将开始编写相关代码,尝试加载多个图层,并展示具有分层效果的动画画面。

game_cutscene.cpp: 开始编写剧情动画代码

我们现在准备创建一个独立的用于处理开场动画(cutscene)的代码文件。这样做的原因是,这部分功能暂时不会影响游戏中其他模块,我们希望它保持独立,避免干扰主逻辑的开发与维护。

对于文件划分,我们并不打算进行复杂的模块拆分,而是出于个人编码习惯和管理方便,选择将这部分临时内容单独存放。在整个项目中,所有代码最终还是会编译成一个统一的翻译单元(translation unit),除非迫不得已才会拆分成多个。换句话说,这种拆分更多是为了保持主结构清晰,不让试验性的代码混乱主线逻辑。

接下来我们将从最简单的步骤入手,模拟从未实现过开场动画的情况,一步一步搭建其基本框架。我们不打算一开始就设计出完整复杂的系统,而是采用渐进式开发方式,逐步添加结构和功能。

第一个目标就是完成最基础的显示逻辑:加载开场动画中的第一幕(shot),并将其所有**图层(layer)**渲染出来,仅此而已。这个简单目标作为切入点,方便我们理清楚资源加载和图像渲染的基本流程。

随着功能逐步完善,我们后续还会逐步将其中的一些共通逻辑提取出来进行封装,便于未来复用。总之,初期保持思路清晰、逻辑简单,是我们构建复杂系统前非常重要的准备阶段。

game_cutscene.cpp: 引入 RenderCutScene

我们需要开始实现一个“过场动画”的渲染功能。这个功能的目标是像我们在其他地方那样进行渲染——也就是说,我们希望将一些资源加载出来,然后把它们推入渲染系统中显示在屏幕上。

为了实现这个功能,我们需要准备几个关键的系统资源:

  • 渲染组(Render Group):用于实际执行渲染操作,是我们渲染数据的主要目标。
  • 临时状态(Transient State):保存运行时临时状态。
  • 游戏资源(Game Assets):提供渲染所需的各种图片、动画等内容。
  • Draw Buffer(绘制缓冲):从中获取画布的宽度和高度等信息,通常是一个位图。

接下来,我们要设置渲染参数,比如视角(lens)等信息,这决定了我们从什么角度、以什么方式观察画面。

然后,我们需要明确如何从资产系统中获取我们想要显示的资源。为此,我们通过一个“匹配向量(match vector)”来描述我们想要获取的资源类型,并调用资产系统来进行查询。我们可以使用一个匹配权重向量(weight vector)来控制匹配的重要性,比如强烈要求某个镜头索引(shot index)必须匹配,而图层索引(layer index)可以灵活一些,尽管目前我们总是要求完全匹配,所以这个权重向量的作用有限。

镜头索引和图层索引是我们用来组织资源的两个维度。比如,我们可能有一个开场镜头(shot index 为 1),这个镜头下有多个图层资源。我们打算从第一个镜头中加载它的第一个图层,然后将其渲染出来。我们在这一步暂时不考虑动画、时间轴等动态表现,仅仅是静态地渲染图层。

在渲染之前,还需要设定“米到像素”的转换比例(meters to pixels),这是为了确保图像按正确比例显示在屏幕上。但目前这块逻辑存在一些不统一的地方,我们可能会加一个 TODO,提示未来要统一相关的处理方式。

我们先获取显示设备的尺寸(以米为单位)和绘制缓冲的像素宽度,然后计算出米到像素的比例系数,来控制缩放比例,这部分在调试和不同设备测试时会调整。

接下来我们会进入一个简单的循环,从资产系统中依次获取不同图层的资源。具体实现上是构造匹配向量与权重向量,然后用它们去资产系统中做查询,获取对应的资源,比如一个 bitmap 图像资源。然后调用绘制函数,将图像绘制在屏幕正中央。

在这整个过程中,我们可以暂时先只绘制一个图层,用于验证渲染逻辑是否正确。这个图层来自我们指定的镜头和图层索引。将来我们可能需要遍历所有图层,进行批量绘制,甚至引入一些模糊匹配逻辑或更复杂的动画控制。但当前阶段,我们的目标只是打通基本流程,确保能够正确地加载并显示一个过场镜头的画面。

另外,资产系统的匹配算法目前是通过权重控制优先级,我们也意识到未来可能需要优化这部分,比如对向量信息进行压缩加速匹配过程,尤其是在资产标签数量增加之后。

整体而言,这一部分功能的开发是在为后续的剧情演出、过场动画等视觉内容的呈现打基础。我们先以最简单的形式做出基本能力,之后再逐步丰富和优化。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game.cpp: #include “game_cutscene.cpp”

我们完成了过场动画渲染函数的编写后,还需要将它实际编译进项目中。由于我们当前采用的是“单一翻译单元(single translation unit)”的构建方式,也就是说所有代码都集中编译在一个主文件中,因此我们只需要在主文件中手动引入这个新的 .cpp 文件即可。

我们通过在主文件中加入一行代码,将 cutscene.cpp 文件直接包含进来,这样它就会被编译器一并处理。在完成这一步之后,项目重新编译,并且没有出现任何编译错误,这说明语法部分已经完全没有问题,逻辑上可以继续推进。我们对自己的语法熟练度感到满意。

但是,虽然渲染函数已经被编译进程序,我们实际上还没有在任何地方真正调用这个渲染函数。因此,即便现在运行程序,画面上也不会有任何变化,完全不会执行刚刚写的渲染代码。

如果我们希望看到效果,就必须手动在程序的某处调用我们刚刚实现的函数。只有这样,渲染流程才会真正执行,相关的画面才会被渲染到屏幕上。接下来的任务就是选择合适的时机和位置,把这个过场渲染调用插入到主流程中,使其能够正常运行和展示。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game.cpp: 调用 RenderCutScene 并引入 UpdateAndRenderGame

我们现在要真正开始渲染过场动画。为了实现这个目标,我们的思路是:让过场动画的渲染函数去处理所有和它相关的图层和图像资源渲染,而我们其他的正常游戏画面渲染逻辑就暂时先不执行了,避免消耗多余的渲染资源。

因为过场动画可能涉及大量图层叠加,渲染开销很大,为了性能考虑,我们不想在播放过场时再去额外渲染其他东西。所以我们决定将现有的渲染逻辑进行拆分,分成两个独立的部分:

  1. 渲染过场动画(RenderCutscene)
  2. 渲染正常游戏内容(RenderGame)

我们把原本集中在一起的渲染代码进行结构上的拆分。在进入主渲染流程之后,首先调用 RenderCutscene() 函数,将与过场动画相关的资源进行绘制。之后,原本用于游戏渲染的代码则会被提取出来,单独放进一个新的函数,比如 RenderGame() 或者更准确地叫 UpdateAndRenderGame(),以便后续管理和控制。

在执行这项拆分时,我们观察到原来的代码中存在一个奇怪的行为:有部分渲染逻辑竟然是在刷新渲染系统之前执行的。理论上,这种行为不太合理,因为通常应先刷新当前状态再开始下一轮渲染。我们对此感到疑惑,认为这段逻辑可能需要调整。但考虑到可能存在我们未曾理解的上下文或排序依赖,我们决定先观察运行情况,之后再确认是否可以真正调整。

我们继续对 RenderGame() 函数进行结构化重构,开始提取它需要的各种输入参数。包括:

  • 游戏状态(GameState)
  • 临时状态(TransientState)
  • 输入信息(GameInput)
  • 鼠标位置(MouseP)

鼠标位置可以直接从 GameInput 中获取,不需要单独传参。至于世界状态(World),我们发现它其实是内嵌在 GameState 里面的,因此也不需要再单独传递。我们顺便清理了一些不再需要的变量。

最后,我们还遇到了一个小错误:RenderMemory 没有定义。这是因为它原本定义在外层,在重构过程中遗漏了传递。我们修正了这一点,并确保 RenderGame()RenderCutscene() 都可以独立运行,不依赖对方。

完成这一系列结构优化后,代码逻辑就更加清晰,渲染流程也更加可控。我们可以根据当前是否处于过场动画状态来决定只调用 RenderCutscene(),或者继续运行常规的 RenderGame(),从而避免重复或无用的渲染操作,提升整体性能和灵活性。接下来只需要运行程序验证是否一切正常即可。

BeginRender(RenderGroup); 后面代码单独放在一个函数中
在这里插入图片描述

编译并运行,触发 CheckArena 中的断言

我们现在暂时不处理过场动画的渲染,先只调用 UpdateAndRenderGame(),目的是确保我们刚刚重构提取出来的游戏渲染逻辑是否还能正常运行。这个阶段的重点是验证提取是否正确,避免在拆分代码时出现遗漏或逻辑破坏。

由于整个引擎的系统越来越复杂,涉及的模块和逻辑越来越多,我们必须格外小心。稍有不慎,就有可能打破某个模块之间的依赖关系或生命周期管理,尤其是像内存管理和临时状态这类底层系统,任何小问题都可能导致严重后果。因此我们先保持过场动画未启用,仅使用常规渲染路径,确保游戏场景仍然能正常渲染。

在运行后,我们观察到一些和临时内存清理相关的现象。显然,在渲染逻辑结束之后的某个阶段,我们会对临时内存或临时资源进行清理操作。而现在的问题是,出现了内存相关的清理逻辑触发时,似乎和我们重构的代码部分发生了某种不协调。

我们开始追踪这一问题,思考内存管理到底是在哪里被处理的,特别是“临时内存(temporary memory)”相关的清理,是在什么地方进行的,以及它的生命周期与我们的渲染流程是否协调。因为这种内存通常由一帧生命周期内的堆栈式分配器或类似结构管理,如果重构时打乱了它们的开始和结束调用,就有可能破坏整个状态系统。

因此,我们当前的任务是检查:

  • 临时内存分配和释放是否仍然在正确的位置调用。
  • 渲染流程中是否提前或错位使用了这些内存资源。
  • 是否遗漏了某些初始化或清理操作。

这一步非常关键,确保系统稳定运行的同时,也为后续重新引入过场动画渲染做好准备。等确认一切正常后,再切换到过场动画相关的渲染路径,从而实现系统的双模式支持(游戏与动画)。
在这里插入图片描述

在这里插入图片描述

game.cpp: 在 CheckArena 前执行 EndTemporaryMemory

我们注意到临时渲染内存的管理存在时序上的问题,具体来说,临时内存的释放发生在错误的位置,导致可能在释放之前就提前检查了内存使用情况,进而产生异常或错误判断。

分析内存的使用流程:

  • 首先,我们在渲染开始时调用获取临时渲染内存的函数(例如 BeginTemporaryMemory),这一步将一个临时内存块压入某种类似栈结构的内存管理系统中,供接下来的渲染操作使用。
  • 然后我们执行各种渲染相关逻辑,比如过场动画渲染、游戏内容渲染等,这些操作都会临时申请和使用那块内存。
  • 但问题在于:临时内存的“释放”操作(比如 EndTemporaryMemory 或等效逻辑)实际上是在 UpdateAndRenderGame() 函数内部完成的

这意味着,如果我们在 UpdateAndRenderGame() 之外的地方(比如主流程中)提前检查或者处理与临时内存相关的状态,可能就会出现内存未释放或状态不一致的问题。

因此,我们意识到一个逻辑错误:

  • 临时内存释放应该发生在检查内存状态或Arena之前,而不是之后。
  • 所以内存释放语句的位置必须提前,确保在进入任何下一阶段操作或状态验证之前,所有与当前帧相关的临时内存都已经正确释放。

我们最终将释放内存的逻辑上移到内存状态检查之前,确保内存生命周期和逻辑处理顺序完全匹配。这样才能避免一些隐藏的内存污染、重复释放或使用已释放内存的问题。

这一步调整完成之后,内存系统的生命周期才算真正闭环,从获取临时内存、使用它,到在适当时机释放,流程清晰且安全,也为后续复杂渲染流程的引入打下了稳定基础。

编译并确认现在已经正确

我们本以为内存的嵌套关系是正确的,应该可以正常运行,但实际上并没有生效。最终发现问题的根本原因是:CheckArena 相关的调用发生在临时内存释放之前,这导致了错误。

回顾流程:

  • 临时内存的分配和释放是成对出现的,通常通过某种临时内存栈(arena)进行管理。
  • 渲染开始前分配临时内存,用于本帧的各种图像、状态等临时数据处理。
  • 渲染结束后应立即释放对应的临时内存,确保生命周期闭环。
  • 然而我们在释放临时内存之前,就已经开始调用 CheckArena 之类的检测函数,用于检查当前内存状态或完整性。

这种顺序是错误的,因为临时内存尚未释放时,内存状态还处于“未完成”或“临时占用”中,检测这时的状态会导致结果不准确,甚至误判。

所以,问题的核心是临时内存的释放必须在所有检测或状态验证之前进行,否则就会因为状态不完整而产生逻辑错误。

最终确认,这一错误是由于操作顺序写反造成的,而不是内存系统本身有问题。修复方法也很简单:将检测调用放到临时内存释放之后,逻辑就能恢复正常。

这也提醒我们,在处理临时内存等具有明确生命周期的资源时,操作顺序至关重要,稍有疏忽就可能带来难以察觉的隐患。现在修正之后,系统应该能按照预期工作。

game.cpp: 从 UpdateAndRenderGame 切换到 RenderCutScene

现在我们已经完成了前面的设置,如果我们想要从普通游戏渲染切换到过场动画渲染,理论上来说应该是可以直接完成的。接下来运行程序后,应该就会进入到我们写好的过场渲染逻辑中。

接下来的目标是开始正式编写过场动画的渲染逻辑。初步目标是在屏幕上渲染出一段基础的过场动画画面,也就是让一个简单的过场镜头能够出现在画面中。这是一个核心步骤,意味着整个过场动画系统的最初渲染路径已经准备完毕。

现在为此我们大概有三十分钟左右的时间,希望这个过程不会太困难,但也不能排除中途可能遇到的问题。总之,下一步就是实际去推进这个初始镜头渲染的过程,争取尽快让一个基本的画面出现在屏幕上,完成初始的可视化渲染。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试器: 进入 RenderCutScene 并确保我们得到了位图

首先要做的第一件事是确认当前是否确实获取到了一个可用于绘制的位图资源。这一步非常关键,因为如果没有拿到有效的位图,就无法进行后续的渲染操作。

接着进入调试步骤,手动单步执行代码,观察渲染过场动画的函数逻辑,确认我们是否真正从资源系统中拿回了一个合法的 bitmap

这次拿回的结果是 251,这意味着我们确实成功地获取到了一个有效的位图资源(通常 0 或负数表示失败),说明资源加载系统工作正常。

接下来,继续将这个位图资源推送到渲染系统中,也就是实际开始绘制它。为确保这一过程没有问题,我们手动设置断点并进入绘制调用,查看整个绘制流程是否顺利进行。

理论上来说,只要当前流程不被意外的构造函数或者异常调用中断,渲染系统应该能够正常地将这个位图绘制到屏幕上。

最后,准备验证绘制的最终效果,看是否画面上能正常显示出过场画面内容。这是关键的验证步骤,决定我们是否可以继续推进更复杂的过场逻辑。整体来看,目前一切都在预期内进行。

看一下为什么没有获得bitmap_id

在这里插入图片描述

在这里插入图片描述

读取资产时
在这里插入图片描述

在这里插入图片描述

这应该是这Asset_LastSound Asset_FirstSound两个导致后面ID对不上
在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 将高度传递给 PushBitmap,看到剧情动画生效

现在的问题出在高度的设置上。因为当前传入的高度值为 0,所以理论上是无法在屏幕上看到任何内容的。为了暂时验证渲染逻辑是否正常,先随便指定一个非零高度,比如传入一个高度为 10 的值(这个高度是随意设定的),用于临时测试。

设置好之后,重新运行程序。可以看到,屏幕上成功渲染出了位图内容。虽然位图非常小,需要将窗口尺寸放大才能清楚看到,但这说明整个加载资源并进行绘制的流程已经正确执行。

此时屏幕上看到的只是单独的一层图像。由于现在仅绘制了一个图层,所以画面内容看起来还没有什么意义。这也是符合预期的,因为目前只加载并渲染了一个单独图层,整个过场画面仍处于初步阶段。

这一步的意义在于完成了资源读取、位图匹配和基本绘制的闭环,验证了整个管线的最基本功能是可以正常工作的,接下来可以在这个基础上继续扩展更完整的过场动画渲染内容。

在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 调整大小并遇到调试中的 bug

当前还不确定最终希望过场图像在屏幕上显示的尺寸,但由于是实时热重载的代码编辑流程,因此并不需要专门开发太多的工具来进行调试和调整。可以直接通过快速修改代码,比如调整位图的高度数值,来观察效果并进行迭代。

本次尝试中,直接修改了渲染的高度值,以测试不同尺寸的显示效果。这种方式非常灵活,能够立即在画面中看到修改后的结果,提升了开发效率。

在过程中发现一个有趣的问题:当修改了高度数值之后,调试信息显示系统(debug UI)似乎出现了异常。看起来字符串比较的部分出现了错误,导致调试面板输出内容异常。这可能是因为调试系统中存在一个未曾修复的 bug。

这个问题并不令人意外,因为当前的调试功能是与热重载系统集成的,而热重载系统本身还处于开发中的状态,因此出现小问题是可以接受的。接下来需要排查调试输出字符串的处理逻辑,以解决这个显示异常的问题。整体来看,尽管出现了一个小 bug,但并未影响主线开发流程,仍可以继续推进过场动画的渲染工作。

game_cutscene.cpp: 调整大小直到图像适应屏幕,并向场景添加图层

我们在继续进行渲染过场动画的工作,当前主要目标是调整图层的尺寸和位置,使其更合理地占据画面空间。

首先我们尝试将背景图层放大一些,让它能覆盖更多的屏幕区域,使视觉效果更协调。通过不断地调整高度参数,逐渐让图层达到理想的尺寸和屏幕占比。当前这种方式只是手动试错,把图层尺寸设为例如 14 这样的值,然后观察实际显示效果是否合适。

调整好第一个图层后,我们开始加载多个图层并将它们叠加组合。尝试了三层图之后,发现画面过大,说明图层的大小和位置还需进一步校准。于是继续通过尝试叠加更多图层,逐步还原一个完整的过场镜头。当前这个镜头一共大概有八个图层,加载完成后可以看到最终完整的场景效果。

这个场景与之前使用的背景版本类似,但经过了进一步润色和细节完善,所以看起来更精致。我们只是单纯地将每一层通过渲染器进行绘制叠加,并没有执行任何复杂逻辑或特效。

下一步,我们将开始处理图层的定位和移动,也就是让画面具备基础的动画过场行为,包括图层的运动、过渡等效果,这是构建过场动画的核心部分。通过这些准备工作,我们已经打下了基础,可以进一步构建动态表现。
在这里插入图片描述

在这里插入图片描述

今天做的事情

我们原本预计今天的工作内容是将过场动画的图层成功渲染到屏幕上,但这个过程比预想中要顺利得多,图像已经正常显示。因此,仅仅完成这个目标显然不足以作为今天全部的进度,毕竟花了不到三十分钟,实在显得不够充实,也不够有意义。

于是我们决定直接进入接下来的阶段——深入探讨当前这个具体的过场动画场景的实现细节和渲染机制。我们会重点分析这个过场动画的结构组成,以及它在系统中是如何被管理和渲染的,包括图层的加载顺序、显示逻辑,以及接下来需要处理的动画过渡、交互触发等问题。

总之,当前已经完成的只是一个静态展示的基础,而我们会进一步展开工作,让这个场景不仅能显示,还能表现出完整的动画流程和动态特效。接下来就是聚焦在如何让这个过场动画“动起来”,并实现我们想要的演出效果。

Blackboard: 在空间中定位图层

目前我们已经成功将一个包含八个图层的过场动画渲染到了屏幕上。这些图层依次从 layer1 到 layer8,全部被正确地叠加在一起,组成完整的视觉场景。虽然暂时还没有查看帧率的具体情况,但从实际运行来看,渲染器可以处理这么多图层并保持良好的性能,这本身已经超出预期。

接下来的任务是为这些图层添加空间位置感,也就是模拟出一种透视效果。我们的目标是构建出一个具有深度层次的画面,使前后图层在视角移动或缩放时产生大小变化,形成类似真实场景中的“近大远小”的视觉效果。

从概念上来说,可以将当前观察场景的方式理解为从上往下看的俯视图。在这个视图中,屏幕就是一个矩形显示区域,而观察者的位置就是屏幕前的“眼睛”。各个图层按照不同的“Z深度”(z-depth)依次排布在眼睛与屏幕之间,前景图层离观察者更近,背景图层更远。

我们需要做的事情是:给每个图层分配一个不同的 Z 轴深度值。这样,在相机移动或进行缩放操作时,由于视差效果,不同深度的图层会以不同的缩放速率变化,从而呈现出层次感强烈的视觉透视效果。

实际上,之前我们已经在系统中实现了 Z 深度处理机制,在角色上下楼层、镜头推进拉远时都用到了它。现在我们要将这一机制进一步扩展到过场动画系统中,让每一个图层都能根据其所在深度自动产生透视缩放变化,从而实现更自然、更有空间感的动画表现。

总结起来,当前阶段的核心目标就是:为每个图层设定合理的 Z 深度,并使镜头变动时图层根据其深度自动缩放,最终实现具有立体透视感的动态分层场景表现。

game.h: 引入 CutSceneTime

我们接下来的目标是为过场动画引入一个相机随时间移动的机制。为此,首先需要实现一个用于驱动镜头变动的时间变量,从而使整个镜头在播放过程中能够按照时间动态变化,逐渐推进、拉远或平移视角。

当前阶段我们先不考虑具体的结构设计或系统架构,只需要临时实现一套基础的机制来验证可行性。我们已经有一个 game_state(游戏状态)结构体,可以直接在这个结构体中临时增加一个变量,比如 cutting_time,用来记录过场动画当前已经进行的时间。

cutting_time 就是一个简单的计时器,会随着程序运行不断递增。它将作为过场动画渲染过程的核心驱动参数,影响相机的位置、角度,乃至于图层的缩放等。

这意味着之后我们可以根据这个 cutting_time 来控制相机的运动轨迹,例如:

  • 在前几秒内逐渐推进镜头
  • 随时间淡入淡出某些图层
  • 通过函数映射 cutting_time 到 z-depth 缩放,实现动态视差效果

虽然目前还没有设计更复杂的“镜头系统”或“镜头序列”,但随着后续对过场动画的需求增多,我们将会把这部分代码独立出来,形成一个更清晰、可扩展的系统。现在我们只需要关注一个目标:让相机随着时间推移发生变化,并对渲染效果产生可见影响。

总结:

  • 引入一个 cutting_time 变量,用作相机动画的时间基准
  • 后续通过该变量控制镜头变化,驱动图层视差与画面运动
  • 目前以临时方案落地,后续再逐步系统化设计
    在这里插入图片描述

game_cutscene.cpp: 将 CutSceneTime 传递给 RenderCutScene,并让相机缓慢缩放

在当前的开发阶段,我们的目标是为过场动画实现一个随时间变化的相机缩放效果,模拟出视觉透视的动感变化。

首先,我们在 game_state 中添加了一个变量 tCutScene,作为过场动画的计时器,它记录了动画当前的运行时间,并随着帧的推进不断递增。然后在 RenderCutscene 函数中引入了该时间参数,让渲染逻辑可以根据当前时间动态调整镜头视角。

我们设定了一个相机起始高度为 10 的值,代表相机最初离“地面”有 10 个单位的距离。在接下来的 5 秒时间内,我们希望相机逐渐向场景拉近 5 个单位,最终变为 5 的高度,从而实现一个平滑的相机“推进”效果。

为此,引入了两个边界变量:

  • t_cosine_start = 0.0
  • t_cosine_end = 5.0

表示过场动画的时间范围。接着,我们调用现有的工具函数 clamp01_map_to_range,将 tCutScene 映射到 [0, 1] 区间,得出一个 t 值。这个 t 代表当前动画所处的进度百分比。

然后我们使用这个 t 来插值计算相机的当前位置,比如:

camera_z = 10.0 - (5.0 * t);

这个表达式表示:随着 t 从 0 增加到 1,camera_z 从 10 缓慢变化为 5,实现逐帧的相机推进。

为方便调试和反复播放这个过场动画,我们暂时实现了一个简单的循环逻辑:当 tCutScene > 5.0 时,将其重置为 0,让动画循环进行。

虽然过程中曾尝试使用模运算 fmod 来实现值循环,但发现当前系统内没有相关实现,因此放弃该做法,改用更直接的重置机制。

总结关键点如下:

  • game_state 中添加 tCutScene 变量,作为动画计时器
  • 在渲染函数中传入该时间值,用于驱动视觉变换
  • 使用映射函数将时间归一化后计算插值系数
  • 控制相机的 z 坐标根据该系数平滑推进
  • 设定超过 5 秒后重置时间,实现动画循环

通过以上实现,成功完成了一个基础但有效的相机缩放机制,为后续复杂的过场动画效果打下了基础。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,查看剧情动画生效

当我们将这个过场动画机制与游戏主循环连接起来之后,整体效果开始显现。

当前渲染系统正在处理多个图层堆叠的画面输出,而这一过程居然表现得非常稳定。尽管多个图像层被叠加绘制,并进行了线性缩放处理,渲染器依然能够流畅运行,这让人颇感惊喜。

虽然帧率并没有达到每秒 60 帧的水平,但考虑到当前并未以 Release 模式编译,这个表现已经相当出色。也就是说,在未进行任何优化的前提下,系统依然可以实时计算多个图层的缩放与合成操作,并维持良好的运行状态,这表明渲染管线设计具备不错的性能基础。

总结要点如下:

  • 当前过场动画成功集成到了主游戏逻辑中,渲染效果可见。
  • 多图层堆叠、线性缩放与动态视角变化并未显著拖慢渲染速度。
  • 虽未达到满帧表现,但在 Debug 模式下仍然运行稳定,说明底层渲染系统具有较强处理能力。
  • 暂未出现内存瓶颈、渲染阻塞等明显问题,整体效果“出乎意料地好”。

这一现象展示了当前引擎架构在处理复杂视觉效果时的潜力,也为后续引入更多动画表现和动态控制打下了良好的基础。
在这里插入图片描述

临时编译时使用 release

debug release 差别的确很大
计算机的性能依然令人惊叹,即使只是简单地运行当前的渲染流程,也总能带来一些震撼。我们已经将当前的构建设置为发布模式,所以在不做进一步优化的情况下,运行速度基本已经达到上限。尽管还有许多可以做的优化,但即使在目前这种“原始”的状态下,效果已经非常不错。

在渲染多个图层叠加的场景中,每一层都进行了缩放与位置变化的计算,系统依然能够稳定输出,说明底层渲染管线的性能非常扎实。这让人意识到现代计算平台的处理能力已经非常强大,甚至不需要特别优化的情况下,也能完成相当复杂的视觉处理任务。

当前的状态总结如下:

  • 当前渲染是以发布模式构建运行,性能已经接近当前代码结构下的上限。
  • 多图层线性缩放叠加并未引起性能瓶颈,系统稳定性非常好。
  • 目前还有大量优化空间(如图层合并、遮挡剔除、缓存策略等),但尚未实施。
  • 在完全未优化的条件下,仍能运行复杂的动画过场流程,凸显现代硬件处理能力的强大。
  • 已成功实现相机视角平滑变化与图层缩放联动,视觉效果连贯。

接下来准备继续调整并完善视觉层的表现方式,进一步打磨图层深度与视角动态之间的逻辑关联,使得整个画面具有更强的空间感和临场感。
在这里插入图片描述

注意到图像没有深度

当前的问题在于,虽然我们已经成功将多个图层渲染到画面中,并实现了相机的平滑缩放功能,但由于所有图层都处于相同的 Z 深度(z-depth),导致整个画面完全没有层次感或空间感。

具体来说:

  • 所有图层的 Z 深度相同,意味着它们在三维空间中的距离是一样的。
  • 在相机进行缩放时,所有图层会以相同的速度变大或变小,因此彼此之间没有相对位移。
  • 这种情况下无法产生视差(parallax)效果,即远处的图层移动慢、近处图层移动快的那种空间感。
  • 结果就是画面看起来非常“平”,虽然叠加了多个图层,但视觉上没有任何深度的变化,也没有那种真实场景的动态感。

因此,接下来的关键目标是:

  • 将各个图层设置在不同的 Z 深度。
  • 利用相机移动或缩放时,深度差异带来的缩放和位置变化速率差异,制造出真实的视差效果。
  • 通过视差,让图层之间在相机移动过程中产生相对滑动,从而增强整体的空间感和沉浸感。

一旦实现这一点,每个图层将不再是简单的平面堆叠,而是有明确远近关系的“景深”结构,使整个画面更加生动、自然,并能真实地模拟三维空间中镜头推进或拉远的效果。这也是实现动态镜头和复杂场景切换(如过场动画)中的重要基础。
在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 根据图层索引定位图层的 Z 轴位置

我们目前面临的任务是为每个图层赋予不同的 Z 深度,以便在相机缩放或移动时产生真实的视差效果。但由于当前渲染系统使用的是一种“二维半三维”(2.5D)结构,变换的方式比较特殊,因此在实现这一目标时需要处理一些非标准的变换逻辑。

以下是目前的工作内容和具体细节总结:

  • 渲染系统中使用 transform offset p 来设置每个图层的位置变换,包括 Z 轴上的偏移。
  • 为了实现图层的空间分布,我们通过设置 transform offset p 的 Z 值,让每个图层有不同的深度。
  • 我们的图层编号是从 1 到 8,但图层编号 1 实际上是最远的背景图层,而图层编号 8 是最靠近摄像机的前景图层。
  • 初始尝试是使用 -layer_index 作为偏移,但导致图层缩放方向反了(前景图层缩放更少,背景图层反而变化更大)。
  • 为了纠正这个问题,我们反转了偏移的逻辑:从一个固定的负值(例如 -10)开始,每个后续图层的 Z 值加 1,这样图层 1 会在 -10,图层 2 会在 -9,依此类推,直到图层 8 位于 -3。
  • 这样设置之后,在相机缩放时,近处图层(Z 值较大)会以更快的速度放大或缩小,远处图层(Z 值更负)则变化较慢,从而实现了真实的透视缩放效果。
  • 通过这个方式,每一层图像在三维空间中获得了深度位置,使得画面在运动中有了明显的空间层次感,不再是“贴在一起”的平面效果。

这一调整为后续的场景镜头推进、过场动画模拟、甚至交互式视差滚动打下了基础。现在我们可以根据需求灵活控制每个图层的深度,从而实现更复杂且具有沉浸感的视觉表现。

在这里插入图片描述

在这里插入图片描述

看到视差效果

现在我们开始能够观察到视差效果的产生。图层在画面中随着相机的缩放或移动表现出不同程度的变化,产生了一种深度感,这是之前所有图层处于相同深度时无法实现的。

接下来我们进入循环编辑模式,能够在程序运行时动态调整代码并立即观察效果。这样可以更直观地调试和微调视差设置,从而精确控制每个图层的视觉表现。

具体操作流程如下:

  • 现在图层已经按照不同的 Z 值排列,远近关系清晰,视差现象开始显现。
  • 为了便于调试和调整,我们进入了循环编辑模式(live code editing),可以在程序运行中即时修改代码并实时查看变化效果。
  • 考虑到编辑界面的排布,我们做了一些调整:删除了一个窗口并重新布置界面,在底部预留出一个窄小的代码编辑栏,用于快速修改参数。这是为了让主渲染窗口尽可能保持清晰、直观的显示空间,同时又不影响代码调试。
  • 窗口调整过程中,系统自动调整了一些窗口位置,不过这些可以手动修正。

通过这一方式,我们可以在不停止程序运行的情况下,不断调节图层深度参数、缩放速率等细节,直到获得理想的空间视差效果。这种工作流非常适合用于视觉调试和镜头效果打磨,效率高且反馈即时。我们会在此基础上进一步优化图层布局,增强视觉体验。
在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 调整图层位置

我们现在可以明显看到,视差效果能够被大幅夸张,通过加大图层之间的深度差异,可以制造出更强烈的空间感。这种分层方式可以根据需要进行灵活调整,比如将图层在 Z 轴上的间距拉得更远,从而产生更夸张的前后移动效果。

在尝试不同深度设置时,我们逐步调整每一层的位置,比如通过负数偏移量将某些图层放得更远,从而增强背景层缩小、前景层放大的动态效果。我们发现,图层越远,视觉上的缩放效应就越明显,但这也带来了一个问题:图层之间没有统一的比例模型,导致远近图层的尺寸不一致,缺乏真实感。

基于当前这种视觉模拟方式,我们意识到如果想要更严谨、准确地制作一段完整的过场动画,就必须为每一个图层明确指定它的几个关键参数:

  1. Z轴位置(深度):决定该图层在空间中离“相机”的远近,用于产生视差。
  2. 缩放比例(大小):确保图层在不同深度下不会因为缩放导致画面失衡。
  3. 具体位置(坐标):确保每个图层摆放的位置符合画面构图需求。
  4. 渲染顺序:根据深度正确排序,保证遮挡关系合理。

接下来要做的,就是将所有图层的这些参数拆分出来,制定成明确的配置方式,使系统在渲染时可以精确地根据设定摆放和缩放每一层。我们将从之前那种简单的循环叠加逻辑过渡到一种更具结构化和表达力的方式,用更具描述性的配置来控制画面中的每一个图层,从而为后续的动态镜头控制和画面切换打下基础。
在这里插入图片描述

game_cutscene.cpp: 引入 LayerPlacement

我们接下来要做的是,为每一层图层指定一个四维向量(Vector4),用来表示该图层在空间中的位置和缩放比例。这个向量包含四个分量:x、y、z 和一个高度值(其实就是缩放因子)。这样,每一层就可以有独立的摆放和缩放控制。

我们为所有的图层(共八层)初始化了一个这样的参数表格,初始值统一设置为 (0, 0, 0, 14),其中 (0, 0, 0) 表示图层位置,14 是缩放系数。这个数值目前是根据之前的设置临时决定的,后续可以调整优化。

图层命名为:layer1, layer2, …, layer8,我们将为每一层保留一份对应的 placement 数据。在渲染过程中,每次处理一个图层时,我们从这张 placement 表中读取对应图层的 x、y、z 和 height 数据,然后据此对图层进行定位和缩放:

  • xy 控制图层的屏幕平移;
  • z 控制图层的深度,即与镜头的远近关系;
  • height 控制图层的缩放大小。

这样做的好处是:我们可以为每一个图层单独指定它在画面中的位置、距离和缩放比例,而不再是所有图层共用统一的参数。后续我们可以很方便地去微调每一层的效果,比如增强某一层的远近感、调整某一层的大小或位置,从而更精细地控制整体画面的层次感和动态表现。

为了测试设置是否生效,我们暂时切换为只渲染其中一个图层,确保基础逻辑没问题。目前这个单独图层并没有任何特殊行为,但已经正确地按照其 placement 参数进行渲染。这为我们后续的动态切换和视差调整打下了稳定的基础。
在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 特别处理天空背景

我们目前要处理的是最远的背景层,也就是“天空背景”,这个图层代表的是远处的天空。因为现实中,无论我们怎么走动,天空的距离都不会因为人的移动而发生明显变化,可以视作“无限远”,所以在画面中我们希望让这一层始终固定不动,不会因为相机的缩放或推进而产生任何视觉上的变化。

为实现这个目标,我们想采取一种“投机取巧”的方法:
让天空背景的 z 值始终等于当前相机的高度,也就是说它始终和相机在同一个距离层级上,这样无论相机怎么变动,天空都不会有视觉上的位移或缩放。

实现这个思路时,我们设置了这个图层的 z 值为相机当前的“distance above ground”,并希望这样它就会保持静止不动。但一开始设置的时候效果并不对,表现上看起来天空背景依然有移动的趋势。

检查后发现,我们把 t_normal(也就是时间变化相关的缩放比)错误地设置在了图层的缩放槽位上,而不是 z 值的槽位。这是一个参数顺序上的小失误。更正之后,将 t_normal 用在了 z 坐标上,并把缩放保持不变,效果就立刻改善了。

此外我们还注意到一开始我们处理的是数组的第一个元素(下标为 0),而不是第二个(下标为 1),这也导致了逻辑上的错位,修正为访问正确图层后,结果才如预期一样稳定下来。

最终结果是:天空背景图层保持固定,不随相机移动而发生变化,如我们最初设想那样,成功模拟了天空“无限远”的视觉效果。这个修正也为其他图层的动态缩放和视差铺平了道路,确保背景在移动中有深度感,但最远的背景不会产生不合理的运动。
在这里插入图片描述

在这里插入图片描述

“嘿,看!它做到了我预期的!”

我们设定的天空背景图层现在终于表现得如预期那样稳定了——也就是当我们查看实际值并将其正确地设置为相机上方的距离时,图层位置终于不再随相机移动而发生变化。

首先,我们将背景图层的位置设置为了相机的“distance above ground”,也就是相机与地面的距离。但一开始,这导致图层与相机完全重合,造成了一些显示上的问题。因为这样一来,天空背景就像贴在相机镜头上一样,失去了应有的远景效果。为了解决这个问题,我们决定将它稍微向后移动一点,比如设置在相机位置后方100米处。

接着,为了让这个图层能够填满整个画面,我们调整了它的缩放比例。最开始的缩放还不足以覆盖全屏,于是我们不断尝试更大的缩放数值,从100开始递增,尝试到120、140,最终找到了一个足够大的数值使其完整覆盖整个屏幕范围,呈现一个完整而不动的背景图像。

完成这些调整后,这个天空背景图层已经完全固定在画面中,不再随相机移动而产生视觉偏移,实现了天空“无限远”的视觉错觉。

现在可以继续处理第二层图层。这个第二层看起来像是一些奇特的“天窗”或“高空景观”图像,相比天空来说,它应当稍微近一些,并且具有轻微的视差效果。我们接下来会为这些图层设定不同的深度和缩放比例,从而逐层构建出具有空间感的背景系统。

在这里插入图片描述

在这里插入图片描述

game_cutscene.cpp: 放置图层

为了构建出正确的视差效果,图层逐步调整并变得越来越精细,接下来的步骤是调整各个图层的位置、缩放和视差效果。

首先,我们调整了第二个图层,类似于一个轻微的天窗效果,这个图层应该非常远离镜头,并且几乎不动。为了让它略微有一些动感,只需微调其位置和缩放,确保它和背景保持一定的距离。接着,调整了第三个图层,图层的大小最初设置得太大,因此需要将它往后移动并重新调整其大小。通过这次调整,可以看到该图层的运动更加适当,且与其他图层的差异更加明显。

随着这些调整的进行,我们逐渐通过更细致的调整来完善每个图层的层次感。每个图层的深度、位置和大小都被细致地控制,以确保它们在移动时产生预期的视差效果。对于每个图层的缩放,我们不断尝试新的值,确保它们在屏幕上的显示符合预期。例如,第三层的树木经过了多次调整,确保它们与背景有合适的距离,同时也让树木的规模适中,符合预期的视觉效果。

同时,我们也不断调整图层的垂直位置,以确保画面中的元素排列整齐,填补了背景和前景之间的空白。对于一些图层,甚至还进行了横向的移动,使它们与画面其他部分更加协调,避免出现不自然的错位。

在完成这些图层的调整后,接下来的任务是逐层添加更多元素并调整它们的比例和位置。每添加一个新图层,都要确保它与其他图层的对比和运动效果得到优化。例如,第四层的规模和位置调整,使得画面中的元素能够更自然地过渡。

最终,经过一系列的微调,视差效果得到了完善,图层之间的深度感和运动效果开始显现,整体画面的层次感也变得更加丰富。每个图层的细节都被精确控制,以确保它们在相机运动时产生合适的视觉效果,达到了预期的背景和前景交替的效果。

就是一顿挨着图层依次实时调节位置

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注意到我们唯一的问题是我们正在缩放到虚无之中

我们现在只剩下一个问题要解决。可以看到画面中的镜头在“向前推进”的过程中,其实是朝着“空无一物”的方向推进。作为游戏开发者和程序员,虽然我们可能并不是电影摄影方面的专家,但至少应该能够察觉到这种奇怪的视觉体验。

我们本应该将镜头推进到“有动作的地方”——也就是孤儿院的位置,而不是推进到远方的空白区域。所以,我们需要修正这种“推进向虚空”的效果。

为了实现这个,我们希望在现有基础上快速添加一些代码,做一个小的改动:让镜头在推进时,不仅仅只是跟随地面的高度(distance above ground),而是能够添加一个偏移量。这个偏移量将用于控制摄像机朝向的位置,使它能够对准画面中实际发生事件的地方,比如孤儿院。

接下来,我们将定义一个三维向量(vector3)来表示这个偏移,这样在计算摄像机位置时,就可以在原有基础上进行调整,使推进方向更加自然、符合视觉叙事逻辑。这样一来,整个镜头运动就不再是“推进到空无一物”,而是推进到我们希望观众关注的焦点区域。

game_cutscene.cpp: 引入 CameraOffset 并设置焦点

我们为了解决“镜头推进时对准空无一物”的问题,进一步添加了一个三维向量 cameraOffset,用于控制镜头的偏移,使其在运动过程中可以更自然地对准我们想要突出展示的画面焦点。

我们首先设置了一个 cameraStart 的初始位置,比如(0,0,0),作为所有图层和镜头的起点。接着设置一个 cameraEnd,表示我们希望镜头最终推进到的位置,比如往下、往左,以及略微拉近一些。这个推进方向是通过 t_normal 插值得到的,从 cameraStartcameraEnd 的平滑过渡。

为了解决在插值中缺少函数的问题,我们本来以为之前实现的是 linearBlend,结果发现实际命名是 lerp,这是一个线性插值函数,用来平滑地计算起点与终点之间的中间值。

通过 cameraOffset 的计算,我们可以将每一个图层的位置 placement 减去 cameraOffset,从而实现视觉上的“相机在动,场景在跟随”的效果。这个逻辑其实是“将世界往反方向移动,制造出相机移动的错觉”。

接下来,我们可以设定镜头在每一帧的偏移,比如每一帧向左、向下、向前移动一个单位。这种设置让我们可以灵活控制镜头最终聚焦的位置。

调整好偏移之后,我们将偏移放置到主逻辑的顶部,使得每一帧画面渲染都能体现这个偏移带来的变化。效果非常明显——我们不再是单纯地“推进到远方”,而是随着镜头的推进,自然地聚焦到我们关心的画面重点区域,比如孤儿院窗户附近。

通过这些调整,我们终于实现了理想的视觉效果:镜头推进的过程中始终对准“事件发生的地方”,整个视角感觉更有戏剧性、更具沉浸感。我们可以进一步微调偏移量,让画面更好地聚焦到目标区域。总的来说,这种方式让我们只用了一个小小的逻辑,就提升了画面的表现力和观看体验,令人非常满意。

在这里插入图片描述

调节推镜头
在这里插入图片描述

通常你会想从艺术图那里获取每个图层的位置/缩放信息吗?如果是的话,你会怎么做?

我们在获取每一层图像的实际缩放比例时,通常希望从美术那边直接得到这些数值,但实际操作中会受到多方面因素的影响,关键在于美术是否熟悉一些能够搭建完整场景预览的工具。

如果美术只是纯绘图型的插画师,那他们往往只会使用 Photoshop 之类的静态绘图软件,不具备使用 After Effects 等动态场景工具的能力。在这种情况下,他们通常不会事先设定图层缩放和景深效果,也没办法在他们的工作流中预览出分层动画的效果。所以在这种情形下,我们基本无法直接获得每层的合适缩放比例,只能在项目中自己一边尝试一边调整,并与美术沟通进行来回确认。

如果希望提供给美术一些辅助工具,让他们能参与缩放比例的预设,可以考虑为他们开发一个简单的小工具,比如一个可以调节数值、即时预览图层距离和缩放的 UI 界面,这样他们就能通过操作界面直接设置各层的缩放或者位移数据。这当然需要我们提前开发好这样的工具,并且提供给美术使用。

另一种情况是,如果美术比较熟悉 Flash、After Effects 或者其他可以设置分层动画和相机视角的工具,那就容易很多。他们可以在这些工具中自己搭建动画场景,设定好每一层的移动范围和缩放比例,然后我们可以根据他们导出的数据来实现相应的视觉效果。这时需要我们根据具体工具的导出格式编写对应的数据导入模块,比如 JSON、XML 或是某种自定义格式。

当然,每个工具的导出方式不一样,处理方式也会完全不同。例如,如果使用的是 Flash,可能需要从动画时间轴中提取图层位置;如果是 After Effects,则可能是从合成文件中提取图层的空间坐标。

如果坚持要用 Photoshop,也有一个可行的、但不太理想的办法,就是让美术通过创建两个图层组合(Layer Comps),分别表示缩放开始和结束的状态。这样我们可以通过两者的视觉对比去估算比例。但 Photoshop 本身并不支持三维定位功能,因此即便通过图层组合去表示变化,也缺乏纵深信息(z 轴),不容易还原准确的空间场景。

虽然 Photoshop 的确有一些三维工具,比如基本的三维物体和视角调整,但它不是为这种场景构建和摄像机动画设计的,大多数美术也并不熟悉这些三维功能,所以实际应用起来仍然非常困难。

综上所述,是否能顺利获取每一层正确的缩放比例,完全取决于美术是否掌握具备场景动画能力的工具。如果不能,那就只能靠我们自己搭建逻辑场景,并不断调整图层缩放和位置,最终通过测试得到合适效果。整个过程会是技术和美术之间来回协作的结果。

我是编程新手,只会中级 Java。如果你用 Visual Studio,能解释一下你为什么还要使用 Emacs 吗?

我们在使用 Visual Studio 的时候,其实只用到了它的调试功能,仅此而已。我们并不依赖它来编写代码或者进行构建操作。

代码的编写工作是完全在 Visual Studio 之外完成的,我们使用的是 Emacs(一个强大的文本编辑器)来编辑源代码,而不是依赖 Visual Studio 的编辑器界面。

构建项目也不是通过 Visual Studio 的编译按钮或者图形界面来完成的,而是通过命令行来手动执行构建流程。也就是说,我们直接在终端或命令行工具中输入编译命令来完成构建,而不是使用集成开发环境(IDE)提供的自动化按钮或功能。

因此,我们整个开发流程的核心其实是围绕 Emacs 和命令行构建系统进行的,Visual Studio 仅在调试阶段才会被启动,用来设置断点、检查变量、追踪程序执行流程等。这种方式意味着我们只把它当作一个“调试器”工具使用,而不是一个完整的开发环境。

总的来说,开发过程非常轻量化和灵活,编辑器用的是 Emacs,构建靠命令行完成,只有调试时才临时调用 Visual Studio。这样做的好处是可以更好地控制开发流程,也不被某个集成环境所限制。对于熟悉命令行和编辑器操作的程序员来说,这种方式效率更高,也更自由。

我觉得你应该稍微把极光抬高一点

我们可以根据实际效果判断是否需要将天空光(skylight)稍微上移一些。如果感觉它的位置略显偏低,那确实可以考虑稍微调整它的垂直位置。不过这种调整完全取决于具体项目的需求和最终想要呈现的画面感觉。

当前的布局基本已经接近完成,整体效果也已经成型,但后续还打算继续进行一些微调,比如把前景的围栏部分再提升一点位置,同时也有一些其它的小细节需要完善。

虽然整体场景已经基本就绪,但这还不是最终版本,仍会继续进行优化。整体思路是确保每一个图层在画面中呈现出合理的位置和深度感,特别是在执行平移或者镜头推进(如缩放或镜头移动)时,能产生自然的视差效果,增强画面的沉浸感。这个过程中每一层的位置、缩放比例以及相对于摄像机的偏移都是可以调试和微调的,从而达到最理想的视觉效果。

我觉得我们应该有一些深沉的声音旁白,“在山谷中……”

我们觉得 Galina Bloom 这个角色可能需要一种低沉的叙述音色,这样能够更符合角色的气质和场景氛围。确实,加入那种低沉、富有情感的配音会显得更有张力,也能更好地渲染整体气氛。

不过目前的重点还是要先把余弦插值(cosine interpolation)相关的系统完善好,也就是场景过渡、视差滚动和镜头动画的基础工作。在这些功能就位之后,才会开始着手处理配音的同步问题。只有等整个视听流程的核心部分稳定之后,才会去考虑如何将配音自然地嵌入到画面中,从而保证整体的沉浸感和节奏一致性。

配音虽然重要,但需要在技术基础打牢之后再来安排,这样可以更有把握地实现真正契合画面的声音表现。到时候,低沉叙述的声音一定会派上用场。

你在学习编程的时候遇到过困难吗?我现在在 Java 课上遇到的难度有点大。

确实,学习编程的过程中我们也曾遇到过很多困难。虽然没有上过正式的编程课程,因为很小的时候(大概七岁)就开始自学编程了,所以没有经历过在课堂上“被教训”的情况,但遇到技术难题却是家常便饭。

小时候很多时候完全不知道该怎么解决问题,也会因为某些小错误卡很久。那时候只能求助家里懂编程的亲人,比如父亲就是程序员,就经常被叫来帮忙调试。那种“我太笨了,帮帮我吧”的感觉非常真实。

虽然没有课堂学习的经历,但在自学和实践的过程中确实深刻体会到编程是一个需要长期积累和不断练习的技能。成为一名真正优秀的程序员需要时间,需要不断碰壁、不断练习。

路上会遇到很多让人头疼的难点和障碍,但只要坚持练习,多做项目,多写代码,慢慢就会突破瓶颈,能力也会逐渐提高。所以现在遇到困难完全正常,咬牙坚持下去,时间会给出回报。

我们以后会加载 PNG 文件吗?如果会,我们是自己写加载器还是用 stb_image.h?我听说加载 PNG 有点棘手?

我们不会加载 PNG 文件,我们始终使用的是 AJ 文件格式。这是我们唯一支持和加载的格式,不会使用 SVG 图像,也不会在引擎中处理 PNG。

关于 libpng 或其他第三方库解析 PNG 的问题,我们完全不涉及,因为我们不使用这些格式。我们选择 AJ 文件格式是为了更好地控制图像数据的处理方式,避免标准图像格式带来的复杂性和额外依赖。这样可以确保图像的加载过程简洁、高效,并且与我们整体的渲染系统高度兼容。

你的剧情动画是这样编码的吗,还是会作为数据保存在单独的文件里?

我们的过场动画(cuttings)会直接以代码的形式编写在程序中,不会作为独立的数据文件存储。

这是因为目前我们没有用于编辑这些动画的外部工具,也就没有将它们拆分成外部数据文件的意义。游戏本身就是我们目前唯一的预览工具,所以将动画逻辑直接写在代码中更加直观、方便调整和调试。这样做也简化了工作流程,避免了额外的资源管理和格式转换问题,使开发过程更高效可控。

做这些事时需要深入了解线性代数吗?

不需要掌握深入的线性代数知识才能完成这些工作。

我们确实不需要知道如何手动解矩阵这类高深的内容,但至少需要掌握一些基础的向量知识。像是向量的加减、缩放、插值、归一化等基本操作,以及在二维或三维空间中如何表示位置、方向、移动等概念,这些都是必要的。因为在图形编程、动画控制和摄像机移动等环节中,基本的向量计算是日常频繁用到的核心工具。

所以我们并不追求学术意义上的“线性代数深入理解”,而是掌握足够的、实用的向量知识,能够理解和运用它们去实现具体的图形逻辑和动画效果。这样就已经足够应对大多数实际开发中的需求了。

你提到你的代码库大部分是独立的:这是否也包括随机数生成,还是你使用 C 库中的方法?

代码库是完全独立的,不依赖于任何外部库,包括 C 库。实际上,整个代码库除了与操作系统的内核进行交互外,不会链接任何其他库。对于随机数生成,也是在代码库内独立实现的,而不是依赖于 C 库中的随机数生成功能。因此,所有的功能和实现都在自己的代码库中处理,完全不依赖外部的第三方库。

你是怎么处理你其他引擎和游戏中的 3D 音频的?用 OpenAL 还是自己实现?

对于三维音频的处理,从未涉及过这方面的工作,也没有特别的兴趣。通常来说,更倾向于制作音频已被精心设计和创作好的游戏,而不是需要根据玩家位置或环境动态生成音效的类型。三维音频通常与虚拟现实(VR)游戏相关,但这种类型的游戏并不是自己最感兴趣的方向。如果以后有机会制作 VR 游戏,可能会考虑加入三维音频的功能,但这并不是目前的主要关注点。

极光的上方有一个间隙

这个问题是说顶部有个空隙,可以通过调整尺寸来解决。如果担心这个空隙的话,可以考虑将其上移或者增加大小,选择哪种方式更合适。

game_cutscene.cpp: 稍微把天光位置移近一点

如果将它稍微上移一点,靠近一些,可能会更好。这样做可以让它看起来有更多的动感。

在这里插入图片描述

这段代码是用来制作游戏中的电影镜头吗,还是只是测试功能?

这是游戏中的一段电影画面,是游戏开场的第一个镜头,或者是开场的介绍部分。

你怎么看待在剧情动画中加入光照效果,比如欢迎标志上的烛光闪烁或脉动?

如果想要添加灯光效果,比如蜡烛光的闪烁或脉动效果,这是完全可以的。不过,根据当前的艺术设计方式,灯光并不是单独绘制的,所以无法单独调节这些灯光。可以尝试一些简单的办法,例如通过遮罩来模拟闪烁效果,但这种做法可能不会特别引人注目或效果太好。

我的意思是图像格式,在打包成资产之前我们会使用什么格式?我知道现在使用的是 BMP 格式。

目前使用的是BMP格式,正如之前多次提到的,资产预处理并不是流程的一部分。我们目前的资产预处理只是为了定义文件格式,这样就可以像我之前做的那样,将资产上传并打包成指定格式。以后我们不会再涉及这个问题了。我们的资产文件采用AJ格式,遵循我们定义的文件格式,我们只从这些文件中加载资产,其他部分不再做更改。

如果要添加更多剧情动画或支持 mod,是否应该将视差常量保存在 hha 文件中?

是否将更多的过场动画和模型内容保存在HA文件中,取决于具体需求。一般来说,建议不要这么做。如果希望支持完整的模型,可能会需要支持动态加载(MOD加载)。通过这种方式,可以通过提供代码来渲染新的过场动画,从而实现更大的灵活性,例如可以随时启动动画,增加动态效果等,而不是尝试将这些内容固定在某种格式中。通常来说,只有在拥有外部编辑器的情况下,才适合使用像艺术格式或数据格式这样的方式来处理内容。如果没有外部编辑器,那么使用代码来提交这些内容会更加高效。

你不需要链接 user32、gdi32 和 opengl32 吗?

不需要链接到某些库,只需动态加载它们。唯一需要链接的是kernel32库,但实际上并不需要直接链接到它,而是需要获取它的基本地址,因为没有GetProcAddress,就无法动态加载任何东西。所以,虽然可以通过一些技巧避免直接链接,但这样做没有太大意义。

如果你写 OpenGL 代码,是用 GLEW 还是自己写扩展加载器并手动加载函数指针?

在处理OpenGL相关内容时,通常会有两种方式来加载扩展:一种是使用已有的库,如GLU(OpenGL Utility Library),另一种则是自行编写扩展加载器,直接处理功能指针的加载与卸载。从技术角度来看,后一种方法更具灵活性与控制力,允许更精细地管理扩展的加载过程,避免了额外的依赖与开销。

通过自己编写扩展加载器,可以更直接地与OpenGL的底层接口交互,这样一方面能确保对函数指针的精确控制,另一方面也能让程序员避免使用诸如GLU这类可能过时或笨重的工具库。这种做法虽然可能更复杂、需要额外的调试与维护,但它为性能和可扩展性提供了更大的空间。如果想要在性能上获得绝对的优势,那这种方法无疑是值得的。

不过,得承认,手动管理扩展加载器并非没有挑战——毕竟,掌握了这种技巧的程序员通常也得具备一颗不怕折磨的心。就像自行织造网兜,虽然能装更多东西,但过程可能会有点……有趣。

你可以做一个 2D VR 游戏,里面有一个巨大的视频墙。

确实,可以制作一个虚拟现实(VR)游戏,内容仅限于一个房间和一个巨大的视频墙。这样的设计构思有一定的吸引力,能够带来独特的沉浸感和视觉冲击。实际上,这种设想并不陌生,过去曾有过类似的思考。通过将VR与大屏幕视频墙结合,可以创造出一个充满视觉张力的空间,让玩家在其中感受到更为强烈的代入感和互动体验。

有没有计划为路灯等事物添加简单的光照和阴影支持?

目前的计划是为游戏中的光照和阴影支持做一些基础工作,尤其是像路灯这样的光源。我们确实打算在游戏中实现光照效果,这也是渲染系统中剩下的一个重要任务。光照将会是游戏中的一个关键部分,能够增强场景的真实性和氛围感。目前的工作重点是完成这部分的实现。

你会在直播中展示所有的内容创作吗?看你花几个小时做剧情动画可能会不太有趣。

制作游戏的过程本质上是工作,而非娱乐。尽管对于观看制作过程的人来说,长时间的剪辑制作可能不太有趣,但这部分内容会完整地呈现在直播中,作为一种教育性质的展示。我们会在直播中展示所有代码的编写,包括涉及游戏中剪辑的部分。如果观看这些内容不感兴趣,可以选择跳过相关的直播。然而,对于那些有兴趣学习编程的人来说,观看这个过程对于职业发展和技能提升是有意义的。

有兴趣的话,如果你想实现一个沿着山丘轮廓曲线缩放(前进,下降,前进),你会考虑在代码中实现,还是会选择一个视觉工具来做这种事?

对于像你提到的这种需求,使用视觉工具的决定通常取决于时间投入的平衡。如果制作视觉工具所需的时间少于直接完成剪辑的时间,那么就可以考虑使用视觉工具。然而,目前制作的剪辑数量不多,且相对简单,因此没有必要使用更复杂的工具。

如果在制作过程中,发现某个任务变得过于复杂,单纯依靠手工代码实现效率低下,且难以完成时,才需要考虑使用视觉工具。这时,可能需要评估是否值得花时间开发一个工具,或者是否选择使用现有的工具(如Flash或Maya等)来辅助工作。关键是要判断开发一个导入器或工具所需的时间与直接完成任务所需时间之间的差异。如果开发一个导入器需要一周时间,而直接做剪辑也需要一周时间,那么开发工具就没有实际意义。

总之,是否使用视觉工具需要根据工作量和时间的权衡,做出合适的决策。

在你学习编程时,你是否深入研究过算法,还是大多通过实践经验学会的?

在学习算法的过程中,虽然有读过相关的书籍和了解过一些经典的算法,但大多数学习还是通过实际编程经验来完成的。在编程时,遇到的实际问题常常促使学习和应用这些算法。例如,虽然排序算法我很少用到,因此在写排序代码时并不擅长,但对于像链表、数组、哈希表这些常用的数据结构,我的理解和运用更加熟练,因为它们是我日常编程中经常用到的。

可以说,某些经典的数据结构和算法,比如排序树和排序算法,因为使用频率较低,所以并不如链表、数组和哈希表那样成为我的第二天性。因此,我的算法知识并不是一开始就学得很深,而是通过不断的实践和使用中逐步积累的。

VSYNC 是渲染器的一部分吗?至少在我的观看中,直播有一些画面撕裂,不确定是不是直播问题。

目前的渲染方式并没有进行同步处理,主要是因为在Windows平台上,当前的渲染方法是直接将位图复制到屏幕上,而没有使用维渲染管线(3D rendering pipeline)。维渲染管线(3D rendering pipeline)会自动处理垂直同步(V-Sync)。未来,可能会切换到三维渲染管线,这样就会自动处理同步问题。

但现在的渲染只是简单地将缓冲区内容复制到屏幕,没有进行任何同步操作。而且由于我们使用的是Windows的Stretch Blit,这实际上是通过Windows的Aero进行合成,这可能是导致你看到画面撕裂的原因。我的屏幕上并没有出现撕裂,因此撕裂可能是出现在你这边的。

总的来说,我们目前并没有尝试在渲染端进行任何垂直同步操作,而撕裂问题更有可能是出现在视频播放端,而不是我们的渲染端。

对问题的回应有没有影响你处理某些问题的方法?

回答问题时,一些对问题的回应让人对某些问题的处理方式产生了兴趣。虽然目前还没有涉及相关的问题,但曾对某人使用索引处理艺术系统的方法感到惊讶。他的做法出乎意料且有很强的合理性,背后有清晰的思考过程。虽然这不会立刻改变自己的处理方法,但这一点会被记住,成为思考时的参考。

雪人故意低头是为了表示悲伤吗,还是我想多了?

关于雪人的姿势是否故意低头来表达悲伤的情感,实际上这个问题可以向负责艺术创作的人员询问。大部分时间,工作重心集中在主游戏的艺术指导上,尽量避免过多干涉的细节。创作过程中,设计人员通常会根据自己的判断决定事物的呈现方式。对于这类项目,自己主要做的是构思画面,提供一些基础的草图和构图,然后让设计人员根据这些框架自由发挥,除非有特别明确的需求,才会提出修改意见。对于雪人是否悲伤的表达,通常不做过多的干预,除非有特别的要求。

有没有编程问题让你卡住过?即使是 Google 搜索或合作也没能解决?

编程问题是否曾经让人感到困惑并无法解决,这个问题本身的表述存在一些误解。编程本身并不像有固定解答的问题,因为编程问题的解决方案是多种多样的。每个解决方案都有不同的标准,可能是执行速度、内存效率、鲁棒性、代码量、或者与其他系统的集成度等。所以,通常并不存在“解不开”的编程问题。

在处理编程问题时,通常会有一种感觉,就是可能通过花更多时间来研究问题,能够找到更好的解决方案。每次编写代码时,总是觉得自己能够做得更好,只要愿意投入更多时间去深入思考问题。因此,并没有真正意义上“被难住”的时候,只是总觉得自己可以通过更多努力做得更好。

那是个不错的引用

看起来,似乎大家建议将其作为引用添加进去。不过,这对我来说似乎是显而易见的,我并不觉得真的需要特别说明这一点。

关于使用指标来确定给定情境下最合适的实现方式

在计算机科学中,确实有一些特定问题的解决方案,比如我们可以回答某个算法的最坏情况下运行时间等问题,这类问题有一个明确的数学答案。然而,写实际代码时并没有“解决方案”这种说法,甚至都不能称之为问题,更多的是“这段代码需要完成什么任务”。假设你会编程,那么你应该能够让代码完成这些任务。

所以,唯一的问题是,我们如何衡量这个解决方案的好坏。所谓的“解决方案”其实更应该称之为“实现”。每个实现都能解决问题,或者它就不是一个有效的实现。不同的实现有不同的特点,我们通过衡量这些特点,能够对比各种实现,进而选择最合适的实现。

有些实现可能比其他实现严格来说更好,而有些实现则在某些方面更好,在其他方面则更差。最终,取决于我们需要做什么,我们可以选择一个更加适合的实现。

当你还是中级程序员时,你有多经常使用 Google 查找解决方案?还是你总是先自己尝试解决?

这是一个有趣的问题。当我还是一个中级程序员时,谷歌还没有成立。

这可能显而易见,但确实有一类程序员似乎认为并非如此。

这显然是个遗憾的情况,但也没办法。

你注意到窗户里的类似三角形的形状了吗?

注意到窗户上有类似三角形的形状。实际上,并不是完全的三角形结构,因为顶部和中间部分并没有被分割开。虽然两侧的部分可以看作是三角形,但整体上并不是一个递归的三角形结构。虽然没特别留意,但也确实注意到了这个设计。

我的一个导师曾经说过,应该有一本书叫做《算法、数据结构和权衡》。

学生们常常认真地听老师说,应该有一本书叫做《算法、数据结构与权衡》。这个观点非常好,我完全同意。
没找到这本书


网站公告

今日签到

点亮在社区的每一天
去签到