回顾游戏运行并指出一个明显的图像问题。
回顾一下之前那个算法
我们今天要做一点预加载的处理。上周刚完成了游戏序章部分的所有剪辑内容。在运行这一部分时,如果观察得足够仔细,就会注意到一个问题。虽然因为视频流压缩质量较低,很难清楚地看到,但如果是在本地亲自运行代码,或者通过论坛上的种子下载高清版本的录像来看,这个问题就会很明显。
这个问题不是系统或代码出现了错误,而是存在一个我们希望消除的视觉瑕疵。切换场景时,在转场的瞬间会出现一个明显的闪屏,不是干净利落地从一个画面跳到另一个,而是中间出现了突兀的"闪一下"。这是由于我们使用了按需加载的资源系统,在后台从硬盘动态加载所需的素材。
尤其是在我们使用的 7200 转的机械硬盘上,这样的加载速度会相对较慢。虽然很多资源可能已经在缓存中了,但还是存在一帧或两帧的延迟。这意味着素材线程需要时间从硬盘读取图像资源,再放入内存中用于渲染。而这个延迟,就导致了场景切换时的闪屏现象。
我们希望解决这个问题,避免这种中间空白的情况。一种传统的做法是使用加载画面,在每段内容前插入一个大的加载界面,并提前将所有资源一次性读入。这虽然可行,但也有明显缺点,比如浪费内存、体验不流畅等。
但我们不希望使用加载画面,目标是实现即时进入的体验,一旦用户点击,就立即开始内容。因此,我们正在寻找替代方案,希望在不依赖加载画面的前提下,实现平滑的场景过渡,这正是我们今天的目标。
调试器:进入 RenderCutScene
,发现资源状态未设置。
我们先从问题的实际表现开始说起。最直接的方式就是在调试器里观察这个问题,因为它会在第一次调用 RenderCutscene
时就出现。
进入 RenderCutscene
,其中调用了 RenderLayeredScene
,在这个函数中,我们会按顺序处理每一层图像,并尝试获取每一层对应的位图资源。在处理的过程中,通过资源系统的查询机制,我们成功得到了对应资源的 ID,例如返回的是资源 251。这些步骤本身没有问题,因为通过数据表(资源系统的核心结构)可以即时获取这些信息。
接下来是问题的关键,我们尝试通过 GetBitmap
函数根据这个 ID 获取位图内容。这时问题出现了:资源并没有真正被加载到内存中。调用 GetBitmap
后,资源系统尝试去访问该资源,但失败了,因为它当前还没有被加载。查看这个资源的状态,会发现它的状态值是 0,说明它尚未初始化或加载。
虽然系统会在检测到资源未加载时,发出加载请求,把这个资源列入加载队列中,但这一帧的渲染已经来不及等这个加载过程完成。这就意味着这帧中我们将缺失这个位图,从而造成场景切换时出现视觉上的跳帧或闪屏,这就是我们观察到的问题的全部来源。
即便硬盘很快返回数据,只要我们在本帧中做出"资源未准备好"的判断,我们也已经跳过了绘制这个资源的机会,所以这一帧的渲染内容就是不完整的。
要解决这个问题,其实办法非常简单------预取(prefetch)。我们的目标是提前通知资源系统:马上就要用到哪些资源了,请先开始加载它们,争取在真正需要它们进行渲染之前,就把它们加载到内存中。
特别是在过场动画中,这个策略非常合理,因为过场动画的内容是完全已知且固定的,我们完全知道什么时候需要用到哪些资源,所以没有理由不提前告诉资源系统"准备好这些资源"。
总结起来,解决思路就是:
- 明确即将使用的资源
- 在渲染前通知资源系统进行加载
- 给资源系统留出时间完成加载
- 避免首帧渲染缺失资源导致的闪烁问题
通过这个方法,就能保证即使是第一帧播放动画,也能顺利且完整地呈现内容。
在 game.cpp
中以人为加快的速度回放过场动画。
我们现在需要做的其实并不复杂。首先,我们希望能以人为加快的速度播放过场动画,这样能更清楚地观察问题的表现。
所以我们先将动画加速播放,这样就可以快速地看到动画推进的过程。当动画以更快速度播放时,我们能清晰地看到在某些时间点会出现闪烁的现象。这些小小的闪烁就是我们想要消除的画面瑕疵。
因此,接下来的目标非常明确:去除这些闪烁。
要做到这一点,最关键的问题就是如何进行"预取"(prefetch)操作,也就是在资源真正需要被使用之前,提前告诉资源系统去加载它们。
这些闪烁的根本原因是资源在被请求时还没有准备好,而渲染这一帧时又必须依赖这些资源,但它们还在硬盘中,或者正在加载过程里,导致这一帧无法正常渲染全部内容,从而出现闪屏。
我们知道这个问题只会在第一次尝试使用某个资源时发生,所以只要能在它真正进入渲染流程前,提前一两帧把它加载到内存,就可以完全避免闪烁。
由于在过场动画中,所有资源的使用顺序是完全已知的,我们完全可以在动画开始前,就将即将使用的资源列表提前交给资源系统处理。资源系统接收到这个信息后,就可以开始异步加载它们,即使加载过程稍慢一些,也可以在真正需要用到前完成。
因此,我们的思路是:
- 通过人为加速的方式确认问题点,更清楚地看到问题所在;
- 识别出过场动画中即将用到的资源;
- 在动画播放前或初始化阶段,调用资源系统对这些资源进行预取;
- 确保在正式渲染时,所有资源都已在内存中;
- 最终实现顺滑播放,无任何闪烁或缺失。
这就是我们要解决闪烁的具体做法和实施方案。目标明确,手段直接,可控性强,是一个高效且合理的优化步骤。
第三个场景好像有点问题

在 game_cutscene.cpp
中引入 RenderCutsceneAtTime
。
我们这里其实已经有一个很方便的机制,可以用来处理这个问题。我们有一个现成的功能,只需要传入当前的过场动画时间,就能渲染出当下的场景。整个过程逻辑非常简单,没有太多复杂操作。
所以,解决方案也很直接:我们完全可以让这个过场动画渲染函数执行两次。第一次是真正用于画面的渲染,第二次则是在未来的某个时间点提前渲染,但结果不需要显示,只是为了触发资源加载。关键是我们不需要让第二次渲染的结果进入画面,也不需要提交它生成的图像。只需要触发渲染流程中对资源的引用,这样资源系统就会预先加载这些资源。
这是因为我们在执行渲染函数时,其内部会构建一个"渲染组",这个过程里会触发资源系统的加载请求。而最终是否真正渲染到屏幕,并不影响资源加载请求的产生。所以只要在构建完渲染组之后把它丢弃,就能达到预取的效果,而且不影响实际画面。
因此我们做了如下调整:
- 把渲染过场动画的逻辑单独提取出来,封装成一个内部函数,例如叫做
RenderCutsceneAtTime
; - 这个函数只在系统内部使用,不对外暴露,命名也不用太在意;
- 新函数接收一个特定时间点,然后基于该时间点渲染一次场景;
- 对外部调用而言,依然保留原本的
RenderCutscene
接口; - 使用新函数进行额外一次提前渲染,这次不提交渲染结果,只触发资源预加载。
这样一来,我们就获得了一个简单但有效的资源预取机制,能够在真正渲染前把所有将要用到的资源加载进内存,确保在实际渲染时不会有闪烁或资源缺失。这个方法不增加额外的加载界面,也不会影响玩家体验,同时对现有结构几乎没有干扰,是一个高效且优雅的改进方式。
在 RenderCutscene
中额外调用一次 RenderCutsceneAtTime
,传入 0
作为 RenderGroup,以进行位图预加载。
现在我们有了一个新的选项,可以选择让渲染函数执行两次。第一次是正常渲染当前时刻的过场动画,第二次是往前看大约半秒甚至一秒,用于提前准备即将需要的资源。这样做虽然可以解决之前资源未及时加载导致的闪烁问题,但也带来了一个新问题:如果我们直接调用两次渲染函数,会生成两套绘制命令,而我们其实并不想真正渲染第二次,只是希望借此触发资源加载。
为了解决这个问题,有两种思路:
- 在资源加载层级,直接调用预取函数,而不依赖渲染流程生成命令。这种做法结构清晰,但需要改动比较底层的接口;
- 继续使用现有的渲染流程,但把第二次的"渲染命令"直接丢弃,不写入任何缓冲区,也不输出到屏幕,只是借助它的访问逻辑来触发资源加载。
考虑到简单性和复用已有逻辑的便利,我们尝试第二种做法:传入一个空的渲染组(Render Group 为 0),作为"空渲染"的标志信号。
具体实现如下:
- 对渲染函数做了改造,使其支持 Render Group 为空;
- 在生成渲染命令的地方添加条件判断:只有在 Render Group 存在时才真正构建渲染命令;
- 否则,直接走资源访问的逻辑,并在不生成任何绘制数据的前提下执行 Prefetch 操作;
- 在执行 Perspective 变换等处理时,也加了条件保护,防止空指针问题;
- 这种方式最大程度避免了内存浪费,因为根本没有生成临时数据结构,也不会往渲染列表中推送无用内容。
最终,完整的逻辑就是:
- 先执行一次正常的过场动画渲染,输出画面;
- 紧接着执行一次预取渲染,将时间提前约一秒;
- 这次调用不产生任何绘制效果,只用于提前触发资源加载;
- 渲染函数内部自动识别传入的 Render Group 是否为空,来决定是预取还是正常渲染。
这样,我们就实现了一个零成本、低侵入的资源预取机制,有效解决了场景切换时的资源缺失问题,而且不会引入额外加载界面或内存开销,整个逻辑既高效又优雅。


运行过场动画,查看其效果。
我们现在来看一下最新的修改是如何起作用的,确认是否已经解决了之前的问题。现在可以看到,在所有场景切换过程中,已经没有任何"穿插"或"穿帮"的情况发生了。所有的过渡都变得非常平滑,这是因为我们已经给予流媒体系统足够的时间来完成加载和准备。
虽然在最开始的部分,似乎还是有一次轻微的异常------有一个画面闪了一下,只有一个眼睛的内容------但这是有明确原因的。这一点我们需要单独讨论一下,因为虽然这不影响循环播放的部分(就像使用椭圆机时那样,并不需要完美循环),但从另一个角度来看,我们还是需要去修复这个问题,原因稍微有点不同。
总的来说,除了开头的那一处小问题外,其余部分已经完全按照预期运行,过渡流畅,没有视觉上的错误,这证明了我们对系统加载节奏的调整是有效的。但为了完整性,我们仍需解决最开始那一帧的问题。
提问:"你们知道为什么开头会出现那个图像故障吗?"
首先我们需要弄清楚,为什么只有在最开始的部分会出现那个异常情况,而其他地方都没有。如果仔细思考一下,这个问题的原因其实是可以推断出来的。
我们之所以在所有场景切换中都没有问题,唯独在开头部分出现了那个问题,是因为在进行预加载或"向前看"(look-ahead)时,并没有进行循环处理。也就是说,当播放进度走到最后一个片段时,系统并不会自动回到第一个片段进行预加载。循环逻辑的相关代码其实存在,但它只在循环机制真正启动的时候才会被调用,而在渲染播放的过程中,这段代码并没有被触发。
因此,当渲染到最后一个片段时,系统并不会预判接下来要显示第一个片段的内容,从而导致在首次渲染的过程中出现了那个画面缺失或闪一下的问题。
不过,这种情况在正常情况下是可以接受的,因为通常我们并不需要让整个片段循环播放。也就是说,如果不是打算循环,理论上这个小问题是可以忽略的。
但我们现在真正需要解决的,其实是首次出现那个异常画面的问题,也就是第一次播放时的视觉闪烁。尽管不需要处理循环问题,但我们仍希望第一次播放时就能是平滑、无缝的,没有任何突兀或加载不及时的现象,这才是我们真正想要修复的部分。
在 game_cutscene.cpp
中创建一个初始黑色画面,持续 20 秒(加速播放)。

我们意识到,为了确保初始化阶段流媒体系统有足够的时间进行预加载,我们需要一个真正"空"的场景。这个场景中没有任何实际内容------没有画面,没有图层,没有对象,甚至可以说就是一堆"零",完全空白。
我们可以为这个空场景设置一个明确的持续时间。例如,在进行资源初始化(像 asset none 这样的操作)时,将所有内容都设为零:坐标、图层、状态等全为空,然后再指定一个时间长度,用来作为这个空场景存在的时间。这段时间不需要太长,足够流媒体系统完成一次向前预加载即可,也可以根据需要延长一些,比如加入一个较大的延迟,以确保它确实能够起作用。
我们也发现,虽然这个场景存在,但如果没有显式使用或没有在渲染流程中引用,它其实是不会被执行的。因此,我们还需要确保这个空场景确实被加入到了播放队列或初始化流程中。
最后一部分的确认是,我们发现之前定义的某个空场景并没有被使用,这是为什么延迟没有生效的原因。一旦正确地将这个空场景插入流程,并赋予一个适当的持续时间,比如几百毫秒的"黑屏",那么整个系统在真正进入可视内容播放前就能完成预加载,从而避免开头出现画面闪烁或未加载完整的问题。
运行过场动画,观察延迟效果。
现在,我们已经在启动阶段加入了一个延迟,这样就能保证系统在正式加载内容之前有足够的时间进行准备。目前来看这个处理是有效的,已经能够看到这段延迟被正确应用了。
接下来需要确保这段延迟时间足够长,以便系统有足够时间完成资源加载。理论上应该是没问题的,但实际情况中我们并不完全确定这些资源的加载耗时究竟是多少。这也是我们需要进一步观察和测试的重点。
为了更准确地判断所需的延迟时长,我们打算查看系统在初始化阶段到底花了多少时间加载资源。这样可以确保延迟不会过短导致加载不全,也不会太长造成不必要的等待。
为了防止画面在启动时出现快速闪烁的情况(类似闪现),我们已经将某些设置调整回来,使其更平稳自然。
接下来我们会尝试查找相关部分的代码或日志,具体分析资源加载的时长是多少,然后根据这个信息进一步微调延迟时间,确保整个启动流程在视觉和性能上都达到理想状态。我们正在定位和查找那一部分,继续分析下去。
在 game.cpp
中以正常速度播放过场动画。
我们将启动延迟时间调整回正常水平进行测试,发现如果设置为二十秒,显然会太长,不实际。即使只是一秒钟,体感上也显得偏长,不过从实际效果来看确实就是一秒左右。
好处是,这样处理之后,启动时那个闪屏的情况已经完全消除,说明我们给流媒体系统提供了足够的时间完成初始资源的加载,达到了预期目的。
当然,这个时间长度还需要不断微调优化。我们也可以采用另一种方式:在启动时显式等待所有资源加载完成后再继续执行。但考虑到性能和体验上的平衡,可能没有必要做到完全同步等待,只要提供略微充足的延迟时间即可。
现在的问题是,虽然加载逻辑已经顺利运行,但有个现象比较奇怪:延迟时间虽然设置了,但主观上感觉启动过程仍然很快,几乎在还没来得及最大化窗口时就已经开始播放了。这可能是因为系统在初始化流程中的某些操作比我们预期更快,或是资源提前就被加载好了。
接下来我们还考虑是否能让程序默认以全屏模式启动,目前不确定是否已经具备这个功能。如果可以设置全屏启动,那在视觉和用户体验上会更统一,尤其在消除启动延迟感和加载痕迹方面会更加自然。
目前我们正在查看是否支持全屏切换的逻辑,例如是否有 toggle fullscreen 这样的功能调用可以实现这一点。接下来将继续探索和测试这部分的功能完善。

在 win32_game.cpp
中切换到全屏并观察效果。
现在我们尝试在创建窗口之后切换到全屏模式,如果需要的话,可以在创建窗口后立即调用 toggle fullscreen
来实现这一功能。
不过,尽管已经添加了全屏切换操作,启动时仍然存在一些小问题,特别是在启动画面上,我们能看到一个白色的闪烁区域,这种闪烁显得有些烦人,可能需要进一步优化去除这个现象。
尽管如此,重要的是,一旦开始播放过场动画后,已经没有出现那些视觉闪烁的小问题了。过场动画播放时非常顺畅,完全没有任何加载或图像中断的现象。这表明流媒体系统有足够的时间预加载内容,避免了之前的闪烁或不连续问题。
不过,我们也注意到,即使给系统预留了一秒钟的时间,这段时间似乎还是有些短。虽然现在已经能保证动画播放时流畅无阻,但如果能给系统更多的预加载时间,可能会更有助于提高稳定性和体验感。所以,考虑到这一点,可能需要提供更长的延迟时间来确保一切都能加载完毕,而不仅仅是一个秒级的预留时间。


在 game_cutscene.cpp
中定义宏 CUTSCENE_WARMUP_SECONDS
。
我们可以考虑给系统更长的预加载时间,比如设置一个"过场动画预热时间",这个时间可以用来确保系统完全准备好。具体来说,可以定义一个变量,比如"warmup seconds",表示预热时间的长度,并将其应用到多个地方。
在启动时,可以先显示一个空白画面,持续一定的时间,这段时间就是预热的时间。与此同时,我们也可以让系统在这个时间内进行前置加载,确保所有资源都已准备好。通过这种方式,我们确保在过场动画开始播放之前,系统已经充分加载并准备就绪。
这样一来,我们就可以保证预加载的流畅性,避免出现任何加载不完全的情况,确保画面在播放时是连续的,没有任何闪烁或卡顿的问题。通过适当调整"预热时间",可以确保一切都能够在最佳状态下运行,让动画播放时始终保持平滑和无缝。
在 win32_game.cpp
中添加 LayerCount 为 0 时清屏逻辑。
为了去除启动时的烦人问题,我们决定采取两个步骤来解决。首先,检查渲染组是否得到了正确清理。我们不确定渲染组是否有清理操作,可能存在某些清除机制,也可能没有。为了确保清理正常执行,我们计划在渲染过程中显式调用清理操作。
通常情况下,渲染时并不需要手动清理,因为我们会渲染整个场景,所有内容会被覆盖,没必要额外处理清理。但如果某些场景没有任何图层(即图层计数为零),就会出现问题。为了避免这种情况,我们打算在渲染前检查场景的图层数量,如果图层计数为零,就强制进行清理操作。
为了验证这一操作是否生效,我们将清理颜色设置为灰色,这样可以确保在清理过程中,屏幕显示为灰色,便于观察是否成功执行清理。通过这种方式,确保在"黑屏"启动期间,屏幕内容被清理干净,避免出现残留的无效内容。
完成这一处理后,我们就能确保在启动时画面被正确清理,避免任何不必要的显示问题,进而确保后续内容顺利渲染。
在 win32_game.cpp
中排查闪屏问题。
为了去除启动时那个恼人的白色闪烁,我决定回到平台层,仔细查看问题出在哪里。在我们的 WM_PAINT
代码中,确实有一个调用 window to display buffer
的操作,但可以看到,这个过程使用了某种策略,这可能导致了初始化不完全的情况,进而产生了闪烁。
我怀疑这个显示缓冲区并没有被正确初始化。经过一些检查,发现确实存在未初始化的情况,因此需要确保在调用 Win32ResizeDIBSection
时,将缓冲区清空,确保所有内容都被清理为零。
为了实现这一点,我认为 Windows 系统中可能存在一个类似 zero memory
的调用方法,用于将内存清零。虽然不完全记得具体的实现,但我猜测这可能是通过 C 运行时库中的宏来实现的。具体可以使用 fill memory
函数来将内存区域清零,确保显示缓冲区在每次刷新前都是干净的。
这样一来,可以确保显示缓冲区的内容被完全清除,避免出现任何未初始化的闪烁现象,确保在启动阶段一切都能顺利进行。

上网查询 VirtualAlloc
相关内容。
在使用虚拟内存时,理论上应该可以请求操作系统自动返回一个已清零的内存区域。实际上,操作系统提供的 VirtualAlloc
函数会自动为我们分配并初始化为零的内存。所以我们不需要手动将内存清零,它本身就已经是清空的(即黑色的)。这个过程应该是自动完成的。
基于这一点,我认为我们无需再手动清理缓冲区,特别是清空为黑色的操作应该不再需要执行。内存初始化为零意味着它的内容已经是干净的,理论上不需要额外的清除操作。
尽管如此,我仍然想确认这一点是否准确。尽管 MSDN(微软文档)上提到分配的内存会被自动初始化为零,但我并不完全信任文档的描述,因此决定亲自检查一下,确认是否确实如文档所说,内存已经初始化为零。
这时,我检查了相关内容,尽管我之前可能误解了一些地方,实际测试显示,分配的内存确实已经是零初始化的,这也就意味着我们不需要再额外进行清除操作了。
调试器:进入 Win32ResizeDIBSection
并查看 Buffer->Memory
。
接下来,我决定通过检查缓冲区的内存内容来确认它是否符合预期。首先,我打开了一个内存窗口,并将其移动到屏幕的上方,以便更清晰地查看内存的具体内容。
查看内存时,发现缓冲区的内存内容确实看起来像是已经被初始化为零。我将它粘贴出来并仔细检查,确认它的内容大部分都是零,没有看到很多非零的值。根据这一点,可以确定缓冲区的内存确实已经是零初始化的,这符合预期。
但是,尽管缓冲区内存看起来已经是零,问题依然存在。启动时,仍然会出现那个讨厌的白色闪烁现象。这就引发了一个新的问题:为什么即使内存已经是零,屏幕上还是会出现这种白色的干扰?这个问题需要进一步排查,找出真正的原因。

上网查询 GetStockObject
函数。
为了找出白色闪烁的原因,需要进一步检查一些可能的情况。特别是在切换全屏时,可能会涉及到窗口类中的一些设置。首先,我想检查窗口类的定义,因为有时候在窗口类中可能会不小心设置了某些 hbrush
,导致了不期望的效果。例如,我们可能设置了 CS_HREDRAW
和 `CS_VREDRAW,这些设置可能会导致一些问题。此时,我不确定是否真的需要这些设置。
在此情况下,我们希望的是窗口背景始终保持黑色。为了确保这一点,我们可以尝试用一个黑色的画刷来初始化窗口类,这样每次绘制窗口时,背景都会自动填充为黑色。这样就不需要担心其他意外的颜色问题了。
为了实现这一目标,我们可以使用 hbrBackGround
,它是一个窗口类的背景画刷句柄。通过设置一个黑色画刷(例如使用 GetStockObject
函数获取一个标准的黑色画刷),我们可以确保窗口背景始终是黑色的,从而避免白色闪烁的现象。
当我回忆起 Windows 编程的细节时,突然意识到通过 GetStockObject
函数可以很方便地获取预设的黑色画刷,这正是我所需要的。通过使用这个黑色画刷,我们可以验证是否能够解决问题。

在 win32_game.cpp
中使用 GetStockObject(BLACK_BRUSH)
绘制窗口背景。
接下来,我们可以尝试使用一个黑色画刷来解决问题。具体来说,就是在窗口类中指定一个黑色画刷,让它作为背景来使用。我们可以通过调用 GetStockObject
获取一个黑色画刷,并将其赋值给 hbr background
,这样窗口背景就会被填充为黑色。
只需设置这个黑色画刷后,窗口的背景应该会保持黑色,确保没有意外的颜色变化,也就避免了白色闪烁的问题。这个操作看起来很简单,不需要太多复杂的设置。只要确保窗口使用的是黑色画刷,就可以观察它是否有效解决了启动时的闪烁问题。
运行过场动画,确认启动体验不错。
现在,通过使用黑色画刷,成功去除了那个烦人的白色闪烁问题。这个问题之前可能是因为 Windows 系统没有正确清理窗口内容,或者默认清理为白色背景,导致出现了不必要的白色区域。无论原因是什么,现在背景已经是黑色了,用户点击运行后,启动体验变得更加顺畅,界面看起来很不错。
然而,仍然偶尔能看到一点窗口擦除的痕迹,这虽然比之前的白色闪烁好很多,但依然不完美。这种现象并不令人满意,仍然需要进一步检查和优化,确保窗口的过渡和绘制过程更加平滑。
在 win32_game.cpp
中创建窗口但初始不可见。
接下来,计划做一些优化,首先我打算在窗口创建时先不显示它,这样可以避免用户看到窗口的闪烁。然后,在窗口准备好后,再让它变为可见。通过这种方式,窗口的显示过程会更加平滑,不会让用户看到不必要的闪烁。
我检查了一下 set ws_visible
,并没有发现这些命令会让窗口立刻变为可见。所以,我决定在切换全屏时使用 show window
和 sw show
命令来控制窗口的可见性。具体做法是先让窗口进入全屏模式,然后再显示它,这样就避免了启动时出现的任何闪烁现象。
通过这种方法,效果比之前好多了。整体体验变得更加流畅,没有看到之前的那些干扰现象。现在的效果让我比较满意,我觉得这样就可以了。

探讨后续可能的开发方向。
现在我们的过场动画播放效果已经相当不错了,接下来有两个可能的方向可以推进。
第一个方向是将动画与背景音乐进行同步,但目前我们还没有背景音乐轨道,因此暂时无法进行这一部分。要实现这一步,首先需要修复 Win32 的计时逻辑,确保其具有良好的一致性。之后,就可以将动画按照某个时间基准进行同步,比如设定每个片段、每次镜头移动与音乐中的某些关键点对齐。但由于我们目前还没有音轨,所以这一部分暂时不做。
因此更实际的做法是回到游戏代码中,对整体结构进行规划,使其可以支持过场动画系统。我们需要构建一种逻辑系统,用于管理当前游戏的运行状态,比如:当前是否在播放过场动画?是否进入了游戏?是否处于角色选择界面?等等。
接下来我们会回到核心游戏代码中进行调整和结构搭建,以支持这些状态切换的逻辑。虽然听起来复杂,但由于我们在之前的 game cutscenes 系统中已经做了很多准备,所以这部分实现起来其实并不难。
举个例子,我们只需要调整每段过场动画的持续时间,就可以达到我们想要的节奏效果。现在设置的持续时间可能是默认的,比如每段动画播放 20 秒。但等我们有了音乐轨之后,只要参考音乐的长度与自然断点,调整这些时间值就可以了。
我们的动画只是静态图像,不涉及口型同步,也不是节奏游戏,因此对时间精度的要求不高。只要大致对得上即可,不需要每毫秒精确匹配,这样也大大简化了实现的复杂度。
总结来说,下一步我们会回到游戏主逻辑中,构建一个状态管理系统,为后续整合动画、游戏流程及音乐做准备。至于同步音轨,只需在未来补上音乐后,微调每段动画的持续时间即可。整体流程清晰,执行起来也相对简单。
在 game.cpp
中切回游戏,解释游戏开始的概念。
我们现在回到 game.cpp
中,开始思考如何处理游戏主逻辑,特别是从吸引模式(Attract Mode)过渡到正式游戏开始的流程。
当前的游戏结构已经有了"游戏开始"的基本概念。例如在某个状态下,虽然画面已经切换到游戏画面,但主角并不会立即出现,只有在玩家按下某个按键(比如空格键)后,主角才会生成并加入游戏。这是为了预先支持多人控制的机制。虽然目前游戏不是一个真正的多人游戏,但系统已经设计为可以支持多个玩家的逻辑,体现出一种模块化与可拓展性。
目前初始状态下呈现的是一个"空白的游戏画面",接下来我们希望将这个部分替换为一个"过场动画",也就是让游戏初始不再是等待玩家操作的空场景,而是播放吸引眼球的动态内容,直到玩家准备开始游戏。这样一来体验更加自然和完整。
这一改动还有助于设计其他系统,尤其是外部资源的预加载(pre-fetching)。在吸引模式播放期间,系统可以在后台异步加载接下来游戏所需的关键资源,比如贴图、音频、关卡信息等,以避免后续切换时出现卡顿。
接下来的目标:
- 替换默认初始状态:让初始进入游戏时不是空白画面,而是进入吸引模式,播放过场动画。
- 完善"开始游戏"触发逻辑:仍然保留通过玩家按键进入游戏的逻辑,只不过视觉上更具吸引力。
- 设计合理的资源加载机制:在播放吸引动画期间启动预加载流程,为即将开始的游戏做准备。
- 统一游戏状态管理架构:通过状态枚举或状态机等方式清晰管理游戏当前处于哪一阶段(吸引、加载、游戏中、暂停等),以支持后续功能扩展。
这种改动虽然在代码结构上不复杂,但在游戏流程体验、系统性能优化、资源调度逻辑等方面都能带来显著提升。之后我们可以在此基础上,进一步添加更多如菜单界面、场景切换、音效同步等逻辑。这个阶段的调整对后续整个游戏架构的发展具有重要的指导意义。

在 game.h
中添加切换过场动画与游戏的能力。
当前的目标是实现吸引模式(cutscene/attract mode)和正式游戏之间的切换。为此,需要引入一个动态变量用于判断当前是否应处于播放吸引动画状态,或者已经进入了正式游戏流程。
考虑到游戏中主角的生成是玩家主动触发的,因此可以利用是否存在"受控主角"(controlled heroes)作为判断依据。游戏已有相关结构体或数组来记录这些受控主角,通过遍历这个数组并检测是否有有效索引或对象存在,即可判断是否已有玩家加入游戏。
逻辑思路如下:
- 定义一个布尔值变量,比如
heroes_exist
,初始为false
。 - 遍历当前记录的受控主角数据,检测是否至少存在一个有效主角对象(代表玩家已经开始游戏)。
- 如果存在,就将
heroes_exist
设置为true
。 - 使用
heroes_exist
作为关键判断变量,在渲染逻辑或状态判断中切换:- 若为
false
,则播放吸引模式的画面和动画; - 若为
true
,则切入正式的游戏逻辑和画面。
- 若为
这种结构可以清晰地分离"未开始游戏"和"游戏已开始"的两种状态,从而保持状态转换的整洁性。
还需要注意控制流程的顺序与状态初始化的逻辑,确保判断在主角真正生成之后才能触发状态切换。同时,这种方式也为后续状态扩展(如暂停状态、菜单界面、结束画面等)提供了基础框架。整体思路简洁清晰,可扩展性强。

按下空格键
运行游戏,加入游戏时注意资源预加载需求。
目前的逻辑是从播放场景动画(cutscene)开始,直到玩家加入游戏,一旦加入,系统就切换到游戏状态。这一过程基本逻辑已经实现,状态切换也是可控的。
不过,目前也暴露出另一个潜在问题,即资源加载的延迟。由于在玩家进入游戏的那一刻,有些游戏资源可能尚未加载完毕,因此可能会出现部分画面加载不完整或黑屏等现象。
虽然这并不是当前优先解决的问题------因为现在的游戏逻辑尚处于测试阶段,还没有真正加入敌人、行走、交互等完整功能------但这确实是后期必须处理的问题。尤其是在游戏玩法逐渐完善之后,需要确保进入游戏的那一刻资源已经就绪,体验才不会被破坏。
相较而言,cutscene 的资源是可以预加载完再播放的,因为它是线性的,不需要实时交互,因此已经提前做了资源准备。而游戏本身因为是实时交互的复杂状态机,所以在资源加载时机上要更为谨慎和设计合理。
当前阶段的处理策略是先不动游戏部分的资源加载机制,专注确保 cutscene 部分可以完整、顺畅地播放,并通过按键触发来切换状态。等到游戏内容更完善时,再将资源预加载系统引入到游戏主逻辑中,确保所有必要资源在进入游戏前都已经加载完成,从而避免切入后出现的残缺画面问题。
总结:
- 当前从 cutscene 切入游戏的逻辑已经跑通;
- 游戏资源的加载延迟会造成初始切换时的不完整画面;
- cutscene 部分已做资源预加载处理;
- 游戏资源加载问题留待游戏架构更完整后统一解决;
- 后续将考虑加入更完整的资源预取机制(例如进入游戏前预读必要贴图、模型等)。
在 game.cpp
中引入 DeleteLowEntity
,按下 Esc 键时调用。
现在已经完成了片头动画(cutscene)的播放逻辑,游戏可以顺利启动并进入游戏状态。接下来需要开始考虑一些更深入的功能,比如:
进入游戏之后,目前没有任何方式能够退出游戏。按下 Escape 键(Back 按钮)不能返回、不能退出、也不能重新回到菜单状态。因此,需要设计一个机制让玩家可以从游戏中退出。
首先想到的方案是利用 Escape 键来作为"返回"或"退出"操作的触发点。通过检查按键绑定,确认 Back 按钮确实是绑定在 Escape 键上的,因此可以用它来触发退出逻辑。
目前控制器系统中通过 AddPlayer
的方式将玩家加入游戏,但没有设计任何对应的"撤销"或"删除"机制。控制器添加之后便一直保留,无法移除,所以需要引入删除玩家实体的能力。
查看现有逻辑后发现:
- 已有
AddGroundedEntity
等类似的添加接口; - 但尚未有对应的删除(DeleteEntity)接口;
- 也没有实现类似的回收机制(如 freelist);
- 目前实体都是不断追加到状态里的。
于是决定为实体系统加入一个简单的 freelist(空闲链表)机制来实现"伪删除"。也就是说,不真正删除内存中的对象,而是将其加入一个可复用的空闲列表中,以便下次重用。这种方式既简单又符合现有架构思路,已在其他系统中使用过类似方法。
具体实现逻辑:
- 每当按下 Escape 键,查找对应的玩家控制器;
- 若找到,则移除其对应的游戏实体;
- 在逻辑上将该实体从游戏状态中删除;
- 后续可通过 freelist 机制复用这些实体,不再重复分配;
- 同时在调用删除接口时,需要传递当前的 game state 以便正确管理状态。
这个处理逻辑是可扩展的,后续也可以在基础上扩展为暂停菜单、重开游戏、返回主菜单等功能。
总结:
- 增加了从游戏中退出(或移除控制器)的初步机制;
- 设计了删除实体的方法,通过 freelist 实现回收;
- 以 Escape 键作为退出操作的触发;
- 后续可继续拓展为菜单导航和完整生命周期管理;
- 整体逻辑保持简洁、清晰,方便后续维护和扩展。



运行游戏并尝试让主角死亡。
目前系统已经基本完成了角色加入、退出、以及从片头动画和游戏之间切换的逻辑测试。
现在的流程是:启动游戏时默认播放片头动画,玩家通过输入操作(如按下空格)创建一个角色并进入游戏世界。当按下退出键(Escape)后,可以将角色从游戏中移除,并回到片头动画状态。
在这个测试过程中发现,虽然角色被逻辑上移除了,但实体并没有真正被删除,而是依然保留在世界状态中。因此,通过反复创建和退出角色,可以看到所有生成的角色实体仍然存在,说明当前只是"假删除",并没有彻底清理掉数据。
此外,还发现多个角色会重叠在一起,这是因为所有角色都被生成在同一个位置,彼此的碰撞体积重叠,导致互相卡在一起,这种行为并不理想。
当前结果的几点总结如下:
- 角色的添加和删除流程已基本通畅,可以从片头动画切入游戏,也可以退出回到片头;
- 删除逻辑采用的是简单的"标记回收"方式,角色虽然不再控制,但实体还在游戏世界中存在;
- 重复进入游戏会不断创建新角色,未进行实体位置处理,导致重叠问题;
- 该机制已可作为原型使用,后续可以进一步改进实体管理(真正清理内存或优化复用逻辑);
- 可以考虑添加角色生成位置的逻辑,避免多个角色重叠;
- 整体上,交互流畅度和体验有明显提升,已有较完整的开始-进入-退出流程。
总体上,这一阶段的目标基本完成,下一步可以针对实体管理精度和生成逻辑做进一步优化。
在 game.cpp
和 game_platform.h
中添加退出游戏功能。
当前的目标是完善游戏的退出机制,让游戏在没有任何角色且玩家主动请求退出时能够正确关闭。
首先的设计思路是:当玩家在游戏中按下特定按键(如 Escape),触发"退出请求"。此时,如果当前没有任何活跃的英雄角色存在,则可以认为游戏应当终止运行,进入"游戏结束"状态。
为此,计划进行以下几步:
-
捕捉退出请求
在控制器处理逻辑中添加判断:如果检测到退出按键被按下(如 Escape 键),则设置一个
quitRequested
标志为true
,表示用户想要退出游戏。 -
检查是否还有角色存在
添加逻辑判断当前是否有任何已激活的英雄角色(controlled heroes)。如果列表中没有任何角色,并且
quitRequested == true
,就意味着可以安全退出。 -
修改游戏主循环退出标志
在调用游戏更新和渲染逻辑之后,对结果进行检查。如果检测到
quitRequested
被设置为 true,则将全局运行标志globalRunning
设为false
,使平台层可以终止主循环。 -
平台层支持退出信号
在平台层添加处理逻辑,允许从游戏层接收退出请求信号,并据此中止运行。
-
内存初始化确认
确认在初始化
gameMemory
时将其全部清零,以确保quitRequested
以及其他布尔标志在游戏启动时为默认值false
,防止误触发。
这套机制的意义在于:
- 让游戏的生命周期更加清晰,从"初始状态(片头动画)"到"角色加入"再到"退出"具备完整的流程;
- 在退出时不依赖平台层强制终止,而是由游戏逻辑自主判断是否应退出,提升模块解耦;
- 为后续菜单系统或更多游戏状态控制打下基础;
- 实现平台和游戏模块之间的简单通信桥梁(例如
quitRequested
标志),从游戏主动传递状态到外部平台。
接下来的工作主要集中在具体代码实现上,包括在控制器输入处理逻辑中加入判断、在游戏主循环中加入状态检查,以及在平台层主函数中处理 globalRunning
变化并优雅终止程序。整体方向和结构已清晰可行。


运行游戏并尝试退出。
当前逻辑的问题出现在退出游戏的判断条件上:即使仍然存在英雄角色,游戏依然退出了。这显然不符合预期,因为应该只有在没有任何英雄角色存在,并且玩家明确请求退出的前提下,游戏才应该真正结束。
我们需要详细分析逻辑流程并指出当前可能存在的问题和修正思路:
当前预期逻辑(设计目标):
- 玩家按下"退出键"(如 Escape) → 设置
quitRequested = true
- 游戏中检查:如果
controlledHeroes
(受控英雄列表)中没有任何角色 并且quitRequested == true
→ 游戏退出(globalRunning = false
)
实际情况:
- 玩家在游戏过程中按下"退出键"
- 游戏直接退出,即使还有英雄存在
- 说明
controlledHeroes
检测逻辑未能正确拦截退出条件
Escape 检查的不对
在 game.cpp
中检查 Esc 键是否被按下。
现在的退出逻辑之所以出现问题,是因为没有对键盘输入进行"去抖动"处理(debounce),导致程序在连续的两帧中先删除了英雄角色,然后立即检测到"没有英雄 + 请求退出",从而直接关闭了游戏。这种逻辑在没有适当判断按键状态变化的情况下很容易出现。
当前行为的问题概述:
- 第一次按下 ESC 键 → 英雄被移除
- 下一帧继续检测 ESC 键仍处于"按下"状态 → 满足"没有英雄 + 请求退出"的条件 → 游戏退出
这个过程发生得非常快,玩家几乎没有反应时间,游戏就意外退出了。
原因详解:
-
按键持续为"按下"状态
系统逻辑中并没有区分"按下的一瞬间"和"持续按下",所以 ESC 按键在连续帧中被视为仍然按下,导致逻辑被多次触发。
-
缺少去抖动处理(Debounce)
没有检测按键是否是"新按下的",即没有从"未按下 → 按下"的转变判断。
修正策略:
需要改为判断键是否是**"刚刚按下"**的一帧,而不是当前是否处于按下状态。可以使用 wasPressed
类型的接口或布尔标志来实现这一点。
实现建议:
- 使用
wasPressed(KEY_ESCAPE)
或类似方式判断是否是"刚按下"状态:
c
if (wasPressed(KEY_ESCAPE)) {
// 只在按下瞬间触发的逻辑
}
这样就可以避免因 ESC 长时间按住而导致多帧触发的副作用。
- 可选优化逻辑流程:
c
if (wasPressed(KEY_ESCAPE)) {
if (heroExists) {
removeHero();
} else {
requestQuit();
}
}
确保只有当按键被"新触发"时才执行后续操作。
总结:
当前游戏在处理 ESC 键退出逻辑时没有进行去抖动处理,导致在两帧内连续触发"删除角色"和"退出游戏"的操作,造成了误判退出。通过添加 wasPressed
检查,仅在按键从未按下变为按下的一瞬间触发逻辑,即可有效解决这个问题,避免误退出的情况发生。这样也可以确保操作的可控性和用户体验。

运行游戏并测试退出功能。
现在的退出逻辑分为两种情况:常规退出和两步退出。通过这两种方式,至少现在可以顺利进入游戏并且顺利退出,提供了一个更稳固的游戏流程。
退出流程概述:
- 常规退出: 直接按下退出键,游戏结束。
- 两步退出: 需要进行两次操作才能退出,增加了确认步骤,避免误退出。
这种结构的实现,确保了玩家可以在游戏中自由进出,体验更加稳定。
在 game_cutscene.h
中引入 struct playing_cutscene
,规范过场动画状态。
现在我们开始思考如何更正式地处理过场动画(cut scenes),而不仅仅是随机地将其插入游戏中。目标是让过场动画成为游戏的一部分,给玩家一个更清晰的体验和理解。
过场动画的结构:
-
过场动画定义: 创建一个更正式的"过场动画"结构,它将包含所有与过场动画相关的信息。当前,这个过场动画的内容不会特别复杂,但它为将来增加更多的功能提供了基础。
-
需要的额外信息: 当前,过场动画的一些数据(例如镜头索引等)还比较硬编码(即直接写死在代码里)。因此,除了过场动画的基础信息外,还可以加入更详细的内容,比如具体的过场镜头,场景的编号等。
-
场景数量: 可以设置一个场景的数量字段,用于控制有多少个过场镜头或场景,并确保过场动画能够按顺序或预定的方式播放。
设计思路:
- 为了使过场动画看起来更有条理,可以在代码中设计一个"过场动画"类或结构体(比如"playing_cutscene"),它不仅能存储场景索引、镜头等数据,还能进一步扩展,随着需求的增加,可以更灵活地添加新功能。
通过这些方式,过场动画将不再是游戏中的随机插入部分,而是变成有目的、有结构的游戏内容。
在 game_cutscene.cpp
中引入 MakeIntroCutscene
。
现在的目标是将过场动画(cut scene)做得更加正式和结构化,以便在游戏中能够更清晰地控制过场动画的播放。
设计思路:
-
创建过场动画的生成函数:
- 创建一个函数,比如
makeIntroCutScene
,用来初始化和构建过场动画。这个函数将负责生成一个过场动画对象(plainCutScene
),并将相关的参数或信息封装到这个对象中。
- 创建一个函数,比如
-
初始化过场动画:
- 在初始化时,设置过场动画的场景数量(
sceneCount
)和场景列表(scenes
)。这些数据可以从外部提供,也可以在函数内部默认生成。 - 对于过场动画对象的一些其他初始化参数,如
time
,不需要显式赋值,因为它们会自动处理。
- 在初始化时,设置过场动画的场景数量(
-
返回过场动画对象:
- 初始化完过场动画后,函数会返回该对象。这样,就可以在游戏中调用这个函数来获取当前播放的过场动画对象。
-
具体使用:
- 在游戏启动时,可以通过调用这个函数来获取一个"具体的"过场动画对象,进而确定当前正在播放哪个过场动画。这为后续更复杂的过场动画控制打下基础。
通过这种方式,可以在游戏中更加清晰地管理和控制过场动画,而不是简单地插入一些随机的过场。

将 Cutscene 作为参数传入 RenderCutscene
。
现在的目标是对渲染过场动画(cut scene)的方式进行一些调整和优化,使其更加灵活和清晰。具体步骤如下:
-
调整
renderCutScene
函数:- 修改
renderCutScene
函数,使其不再直接使用tCutScene
,而是接收一个具体的过场动画(cut scene)对象。这意味着,在渲染时,会将具体的过场动画数据传递给函数。
- 修改
-
传递过场动画数据:
- 在调用
renderCutScene
时,需要传递具体的过场动画对象cutScene
,而不是之前的tCutScene
。这样,函数就能根据传入的具体过场动画对象来渲染不同的场景。
- 在调用
-
更新
renderCutScene
内部处理:- 在渲染过程中,确保过场动画对象的各项数据能够正确地传递和使用。更新函数,使其可以处理不同的过场动画对象并显示相应的内容。
-
清理和调整:
- 做一些必要的清理工作,确保代码整洁,过场动画的传递和渲染流程更加顺畅。需要确保传入的过场动画数据可以在渲染过程中正确使用,而不产生任何错误。
通过这样的调整,过场动画的控制变得更加灵活,能够根据不同的需求进行渲染,而不是单纯依赖固定的方式。这也使得后续可能的扩展和调整变得更加容易。


在 game.h
中初始化 CurrentCutscene
。
我们现在的目标是进一步整理和完善过场动画(cutscene)系统的结构,使其更加模块化、清晰易用,便于后续扩展和维护。主要做了以下几方面的工作:
架构调整
-
替换旧的
tCutScene
:将原来硬编码的
tCutScene
替换为新的playingCutScene
,并通过currentCutScene
变量来跟踪当前正在播放的过场动画。 -
游戏初始化时指定初始过场动画:
在游戏初始化阶段,如果游戏状态还未初始化,则调用
makeIntroCutScene()
创建一个初始的引导过场动画,并赋值给currentCutScene
。这样一来,游戏启动时就有明确的过场动画状态。
渲染逻辑重构
-
renderCutScene
接口调整:修改
renderCutScene
函数,使其不再使用固定的结构,而是接收gameState.currentCutScene
作为参数,确保渲染的是当前活跃的过场动画内容。 -
消除类型错误:
在尝试访问
gameState.tCutScene
时出现了类型错误,因为新结构中没有这个成员。于是改用advanceCutScene()
等辅助函数去处理当前过场动画,避免直接访问不存在的字段。
错误处理和类型定义
-
类型缺失处理:
编译器报错提示"currentCutScene"类型缺失,这是因为结构体中没有预先定义它。为解决此问题,手动将其加入结构定义,确保编译器能够识别和使用该字段。
-
合理忽略默认值:
对某些未初始化值不做强制默认值处理,例如
t
时间字段,依靠结构体初始化自动设置为0.0
,减少冗余代码。
下一步逻辑准备
- 准备
advanceCutScene
函数:
为当前过场动画推进添加advanceCutScene
,用于控制进度、切换镜头等功能。这种方式更可控,也为后续设计更复杂的场景逻辑打下基础。
整体效果
目前已经实现以下目标:
- 游戏启动即有明确的过场动画状态。
- 渲染逻辑不再依赖硬编码字段,使用结构体引用更加灵活。
- 构建了可扩展的系统,后续可以自由添加新的过场动画逻辑。
- 消除了关键的类型错误,使代码结构更稳定可靠。
整体上,这一轮改动为过场动画系统打下了干净、清晰的基础,便于未来添加更多复杂行为或多阶段演出效果。



注意:过场动画不再支持热重载。
现在我们遇到一个潜在问题,这种做法在未来可能会给我们带来一些麻烦,特别是在热重载(hot loading)方面。
热重载的局限性
-
结构体数据可能会移动:
由于我们将数据结构直接写在函数外面,这些数据在程序运行期间有可能被重新加载或移动,而在热重载过程中,这些变化可能无法被正确保留或更新。
-
缺乏 C 语言层面的支持:
当前使用的语言环境(例如 C)对热重载的支持较弱,无法像一些高级语言或引擎那样在不重启的情况下安全、灵活地重新加载代码和数据。
-
数据"烘焙"导致静态绑定:
我们将数据"烘焙"(bake)进结构中,这使得相关数据变得更静态,不容易动态替换或重载。这种处理方式在数据频繁变动或开发阶段需要热更新的情况下会带来不便。
潜在应对方法
-
将数据写入函数内部:
如果我们担心这些结构体在热重载时位置或状态丢失,可以选择将这些数据放进具体的函数体中,而不是静态存储。这样可以在重新加载函数时重新初始化这些数据,从而避免因外部定义带来的失效问题。
-
手动管理热重载:
可以通过某种方式为这些结构体或资源手动添加一个"重载钩子"或者在重载后主动刷新这些结构数据。虽然繁琐,但可以解决部分热重载失效的问题。
实际影响分析
-
目前影响不大但值得关注:
当前的系统可能还没有频繁依赖热重载特性,所以短期内不会有太大问题。但长期来看,如果希望频繁使用热重载提升开发效率,这种写法的确会成为潜在障碍。
-
属于语言设计限制:
这是由于语言本身在运行时处理数据的方式决定的,并非代码逻辑问题。C 本身在运行时对热更新支持就非常有限,静态定义的数据结构在模块重载过程中容易失效或不一致。
总结
我们当前的实现方式虽然功能正确,但可能会在将来因热重载需求而面临一些挑战,主要体现在:
- 数据不再热重载安全;
- 结构体定义静态化,难以动态更新;
- 缺乏语言级支持导致难以解决根本问题。
尽管暂时不影响开发,但如果未来要做大规模热更新优化,可能需要重新考虑数据和函数的组织方式。这是一个值得提前注意和记录下来的技术点。
在 game_cutscene.cpp
中添加 AdvanceCutscene
,以支持多个过场动画。
我们现在需要一个机制来让过场动画(cutscene)随时间推进。之前是直接在代码中硬编码的,但现在我们把它做得更正式一些,这样我们就能支持多个不同的过场动画,而且不会全部都被固定写死在程序中。
过场动画时间推进逻辑整理
-
引入更正式的数据结构:
我们把过场动画抽象成了一个结构体,现在不再是简单地用某些硬编码的变量来控制,而是可以更灵活地定义多个动画。
-
实现推进函数:
创建了一个推进函数,用来根据
dt
(delta time,帧间隔时间)来更新过场动画当前时间t
。逻辑很简单,就是把dt
加到当前的t
上:ccutting.t += dt;
当前阶段,这个函数还不处理其他事情,仅仅更新时间,这为后续拓展动画效果(比如切换镜头、过渡等)打下基础。
-
没有添加其他行为:
目前只做了时间推进的基础框架,没有处理播放进度、切换状态、转场等高级逻辑。
当前结果
- 现在的系统已经能够支持多个不同的过场动画实例,而不是只能播放一个固定的动画。
- 过场动画状态能随着游戏时间正确推进。
- 没有对现有流程造成影响,系统运行回到了正常状态,具备良好扩展性。
未来可以拓展的方向
- 支持动画状态切换(比如播放完自动进入下一个)。
- 加入动画触发机制,例如特定事件触发某段动画。
- 结合剧情或 UI 反馈,让动画与游戏逻辑更加紧密结合。
- 添加控制动画暂停、跳过、回放等功能。
总之,这一阶段主要是将原本临时硬编码的过场动画处理方式转化为一个更灵活、可扩展的系统,虽然目前功能简单,但已经为后续开发打好了基础。

运行游戏,测试新功能,并考虑让过场动画重置。
目前已经可以顺利进入游戏并从游戏中返回,但存在一个新的问题:过场动画(cutscene)在中途进入游戏再退出时,并不会重启。也就是说,动画状态被保留了下来,这可能不是我们期望的行为。
现阶段的表现与问题
- 进入游戏再退出后,过场动画不会从头开始播放,而是从上次停留的时间点继续。
- 这会导致像"欢迎"字样的元素已经移出屏幕,回来时无法再次看到。
- 例如在退出时,画面中"welcome"已经飘到画面右侧快看不见了,回来时它仍然是那个状态,用户错过了内容。
理想的行为目标
- 每次返回主界面或重新启动动画时,应当让动画状态归零,从头开始播放。
- 将来如果实现标题画面(title screen),可以在播放标题几秒后自动开始动画,形成自然的过渡。
- 关键是要有一个机制,在状态从"非过场动画"切换回"播放过场动画"时,重新初始化动画。
需要补充的功能点
- 当前的结构中没有明确标记"状态转换",也就是说,系统无法识别何时从"游戏状态"返回到了"播放动画状态"。
- 需要添加一个状态转变检测机制,例如通过上一次状态与当前状态的比较来判断是否需要重启动画。
- 也可以引入一个状态机机制,使动画播放逻辑更清晰,并方便管理进入/退出游戏、开始/停止动画等切换。
当前的系统优点
- 当前实现方式非常干净,避免了因状态不一致而产生的问题,例如"游戏以为自己在运行中,实际上并没有角色存在"这种错误状态。
- 系统不会出现混乱的状态判断,逻辑清晰、稳定。
当前的限制与接下来的工作
- 尚未实现"状态过渡检测"逻辑,所以动画不会根据场景跳转而重启。
- 接下来的任务就是添加这种检测机制,以便在切换到动画播放状态时正确地重新初始化动画。
当前时间紧张(只剩大约五分钟开发时间),暂时不展开这部分的实现,但已经明确了问题所在与解决方向,之后可以直接着手构建过渡检测机制,实现动画状态的合理重启逻辑。
提问:是否可以添加 BlockUntilLoaded
类函数,确保较慢的电脑在过场动画开始前加载完所有图层?
目前正在考虑是否有必要在过场动画或游戏开始前加入一个"阻塞直到加载完成"的机制,以确保即使在低性能设备上也能顺利加载所有渲染层(layers),避免画面缺失或卡顿。
已有的加载控制机制
- 实际上系统中已经存在类似的机制,可以控制渲染内容在准备好前不进行显示。
- 这个机制的实现是通过
game_render_group
来完成的。 game_render_group
在某种程度上起到了"阻塞直到资源准备好"的作用,可以确保所有需要的图层、图像等都加载完成后再开始渲染。
使用时机与优化方向
- 是否启用这样的阻塞机制,要视具体优化目标而定。
- 如果优先考虑用户体验,避免初始动画卡顿或贴图缺失,使用该机制是有益的。
- 如果更关注启动速度,可能希望跳过某些资源的等待,让它们异步加载。
- 当前结构已经为这种需求提供了良好的基础,只要适当调用
game_render_group
,即可实现阻塞等待功能。
后续考虑
- 如果决定更正式地引入加载检测逻辑,可以扩展当前系统,例如:
- 在进入特定状态(如播放过场动画)前,调用一个专门的"加载屏幕"或"资源准备确认"模块。
- 为
game_render_group
增加明确的加载状态判断,比如is_ready
标志。
- 这种设计可以进一步提升跨平台表现,尤其对性能较低的设备更为友好。
总结来说,已经具备了一个可用的机制来处理"加载前阻塞"的问题,未来只需根据具体的运行需求决定是否启用它,并可按需扩展其能力。
在 game_cutscene.cpp
中关闭预加载逻辑。
当前的讨论集中在如何通过已有机制处理资源加载缓慢时的视觉问题,特别是在没有启用预加载(prefetch)功能的情况下,可能出现的画面撕裂或资源缺失现象。
已有机制说明
- 系统内部已有一套预加载(prefetch)机制,能提前加载资源,防止游戏初始阶段或切换场景时出现贴图、图层丢失等现象。
- 在启用预加载的状态下,画面渲染非常平滑,不会出现明显的加载故障。
- 如果手动关闭预加载机制,例如模拟在一台性能很低的设备上运行,会立即看到场景中资源缺失造成的可视化"撕裂"或"空白"。
当前改进思路
- 为了让这种情况下的体验更合理,可以在检测到资源尚未加载完成时,暂时不渲染画面,从而避免显示错误的中间状态。
- 实现方式是在每一帧渲染前检查资源状态,如果发现有必要资源未准备就绪,就直接跳过该帧的渲染,等资源加载完成后再开始渲染。
效果与意义
- 这样做的好处是,即便在慢速或老旧设备上运行,也不会看到"闪烁"、"丢贴图"等问题,而是看到一个平稳等待的过程。
- 本质上这等于用时间换体验:与其展示半成品的画面,不如干脆延迟显示,保证完整性。
- 这一策略尤其适用于开场动画、过场演出等对视觉完整性要求高的内容。
后续扩展可能
- 可以在"等待资源加载完成"的过程中添加提示信息,例如加载动画、文本提示等,提升用户感知上的流畅性。
- 还可以细化资源状态的判断逻辑,例如只等待当前帧需要的资源,而非全部资源,提高加载效率。
总结来说,已经具备一个可以利用的判断与控制机制,只需简单逻辑改动即可实现"资源未加载完则暂停渲染"的效果,从而有效提升整体视觉体验,特别适用于低性能环境下的过渡场景处理。
在 game.cpp
中检查 AllResourcesPresent
后再进行 TiledRenderGroupToOutput
。
当前内容围绕优化资源加载与渲染逻辑展开,尤其是当资源尚未完全加载完毕时,如何避免画面撕裂、错位或贴图缺失等问题,并提供更平滑的过渡体验。
当前处理方式说明
- 在进行
tab
相对于输出位置的操作时,加入了一步判断逻辑:如果所有资源都已加载,则进行渲染,否则暂停。 - 这种处理方式在运行时会导致一些非常短暂的"卡顿"或"停顿",但由于持续时间极短,基本不会被察觉,对体验影响非常小。
- 实际运行中已经观察到,在资源未加载完成前不会渲染任何内容,成功避免了资源缺失带来的可视化异常。
特殊场景测试
- 在按下空格键触发新场景或内容加载时,同样的机制也会生效,系统会等资源准备就绪后才渲染,从而确保画面完整性。
- 唯一的例外是**地面瓦片(ground tiles)**的加载,这部分是作为一个单独过程进行的,因此会在主资源准备完后略微延迟显示。这属于结构层面不同步的问题。
改动小结
- 当前的实现逻辑非常简洁,只需简单判断
all assets present
即可控制渲染流程,不影响主逻辑结构。 - 该策略默认兼容未来的性能较低设备,使其也能流畅过渡,哪怕在加载时稍有停顿。
预加载机制的回归与兼容性
- 一旦恢复启用预加载(prefetch)机制,整个流程将恢复为完全无停顿状态,因为所有资源在正式渲染前已被加载完毕。
- 即便禁用预加载,对于慢速系统而言,也不会出现明显视觉缺陷,因为此方案通过主动等待机制掩盖了资源加载过程。
最终结论
- 该改动是稳妥且有益的,兼顾性能与视觉体验。
- 代码结构简洁,逻辑清晰,未来可轻松扩展为带提示或加载动画的形式。
- 当前实现已考虑到最坏情况,无需进一步特殊处理,属于可以保留并长期使用的通用优化方案。
整体来看,这是一次从用户体验角度出发的细致优化,兼顾了系统性能差异的适配性,同时保持了代码的灵活性与简洁性。
提问:有没有办法使用 SIMD 清除后备缓冲区?
目前我们讨论的是使用 SIMD 指令(如 rep
指令或其他清屏方式)清空后备缓冲区(back buffer)的问题。总结如下:
关于清空后备缓冲区的方式
- 确实可以使用 SIMD(Single Instruction Multiple Data)指令来清除后备缓冲区,例如使用
memset
等基于 SIMD 的优化方法。 - 但这并不一定总是最快的方式。例如使用
rep stos
(重复存储指令)在某些平台上可能性能更优。 - 实际上,哪种方式更快通常取决于具体硬件架构与 CPU 优化情况,必须通过实际测试来验证。
当前逻辑中的考虑
- 在本项目中,清除后备缓冲区的操作仅在未渲染任何内容的情况下发生。
- 因此,该操作性能影响非常小,不构成瓶颈,不存在对清除效率的高要求。
- 即便清除操作较慢,也几乎不会对整体帧率或用户体验产生明显影响。
总体结论
- 是的,确实可以使用 SIMD 优化清除操作,但是否使用要看实际情况。
- 对当前场景而言,清除性能并不是核心问题,因为它只在没有其他渲染内容时执行,且执行频率极低。
- 预计未来也不会出现对清除速度有严格要求的场景,因此目前的实现方案已足够稳定可靠,无需进一步优化。
这一点体现出在游戏或图形系统开发中,性能优化应该是按需进行的,不必为极少数场景提前过度优化。
提问:是否会有动画过场,还是仅仅是缩放镜头?
目前的过场动画主要只是一些镜头推进(zooming shots),没有真正意义上的敌人出场。如果非要说有的话,也就是在某一两个镜头中,画面上有些静态元素,例如沙地上的脚印或帽子的位置,这些可能被视作敌人留下的痕迹,但它们本身并不具备动作或互动性。
详细总结如下:
- 当前过场动画的内容以镜头推进为主,即通过画面拉近、平移等方式展示场景。
- 没有设计带有行为的敌人登场,敌人并不直接参与过场动画。
- 如果一定要从中找出与"敌人"有关的元素,可能只有一两个画面:
- 比如沙地上的痕迹或物品(如帽子)可能暗示敌人的存在。
- 这些仅是静态线索,用于营造氛围或铺垫剧情,而非实际的敌对互动。
- 整体而言,过场动画在目前阶段更偏向于视觉表现与情绪烘托,而非叙事冲突或战斗展示。
换句话说,这些镜头是为了让场景更具戏剧性或沉浸感而存在的,而不是用来展示敌人或敌对事件的。
提问:是否可以从桌面淡入游戏初始黑屏?如果做不到,淡入淡出过场也很酷。
目前确实可以实现从桌面渐变过渡到游戏初始的黑屏画面,但是否采用这种方式,则取决于具体的设计目的。
详细总结如下:
-
技术上是可行的,可以实现从桌面渐变过渡到黑屏的效果。
- 一种方法是获取当前桌面背景的内容,然后通过在其上方创建一个**分层窗口(layered window)**来实现渐变效果。
- 或者简单地在游戏启动初期先展示一个窗口,然后控制其透明度逐渐降低,最终过渡到游戏画面。
-
设计层面并不推荐轻易使用"淡出"效果:
- 个人风格上不太喜欢"fade to black"(淡出到黑),因为这种过渡效果在视觉语言中有特定用途,不应滥用。
- 在现代电影中,"淡出"或"画面擦除"(wipe)这类过渡手法都非常少见,通常只在特定情绪或情节场合使用。
- 比如表达时间流逝、场景切换的情感缓冲等。
- 若没有强烈的情绪驱动或叙事理由,这种过渡会显得老派甚至多余。
-
对当前场景来说:
- 如果游戏初始是黑屏,那其实已经具备了过渡的"纯净感",不一定需要从桌面"渐变"进来。
- 除非有特别明确的风格化需求 或技术演示目的,否则没必要添加这类过渡。
总结来说,技术上完全可以实现桌面到黑屏的淡出过渡,但是否采用,取决于对游戏叙事节奏、风格表达的权衡。在多数情况下,这样的视觉效果并不必要,甚至可能会影响节奏或显得多余。
在 win32_game.cpp
中添加 FadeOut
。
要实现桌面到黑屏的淡出效果,可以通过创建一个全屏窗口并逐渐调整其透明度来实现。以下是步骤和思路的总结:
1. 创建一个全屏窗口:
- 首先,创建一个新的窗口,并将其调整为覆盖整个屏幕。这可以通过设置窗口大小为屏幕的尺寸来实现。
- 使用窗口类创建窗口,可能需要为窗口指定合适的类,以确保窗口可以正确地显示。
2. 设置窗口层叠和透明度:
- 创建一个分层窗口(layered window)。这种窗口允许设置透明度,可以通过调整其透明度来实现从桌面到黑屏的渐变效果。
- 使用UpdateLayeredWindow函数来更新窗口的透明度。通过这种方式,窗口会逐渐变得透明,直到最终消失,达到淡出效果。
3. 避免复杂的窗口捕获:
- 可以通过不捕获背景或其他窗口的内容,而是简单地创建一个覆盖全屏的窗口来避免操作系统不兼容的问题。
- 捕获屏幕内容并在窗口中渲染可能会因为操作系统的不同而不稳定,使用全屏覆盖窗口的方式更为简便且可靠。
4. 实现思路:
- 通过这种方法,可以创建一个全屏窗口,然后逐渐降低其透明度,使其看起来像是从桌面过渡到黑屏。
- 这种方式的好处是实现简单,不依赖操作系统的具体细节(比如屏幕捕获),且适用于较旧的操作系统(如XP及其以上)。
5. 使用这种方法的潜在问题:
- 这种方法在某些情况下可能不适用于游戏开发,因为它涉及到的窗口管理和层叠效果可能影响游戏的其他方面,特别是对于更复杂的图形和渲染系统。
- 尽管这种方法技术上可行,但在实际游戏开发中,通常不建议使用,因为它可能会导致不必要的视觉过渡,并且可能不符合现代游戏设计的流畅性需求。
总结来说,技术上是可以通过创建全屏窗口并调整其透明度来实现淡出效果,但这不一定是最好的解决方案,特别是在游戏开发中,可能会有更合适的方式来处理场景切换。
上网查询 UpdateLayeredWindow
用法。
为了实现淡出效果,可以通过创建一个分层窗口(layered window)并更新它的透明度来逐步实现窗口的淡出。然而,实际操作中遇到了一些问题,下面总结了整个过程以及问题分析:
1. 创建和更新分层窗口:
- 使用UpdateLayeredWindow函数来实现窗口的透明度渐变。这个函数允许在操作系统层面更新窗口,使其成为透明的。
- 为了让窗口过渡到黑屏,可以通过不断调整窗口的透明度来实现。
- 在操作系统中调用该函数时,需要传递窗口的句柄、设备上下文(DC)、大小等信息。
2. 使用透明度来实现淡出效果:
- 通过逐步改变窗口的透明度,创建一个从桌面背景到黑色屏幕的淡出效果。透明度是通过alpha通道控制的,逐渐降低透明度来模拟淡出效果。
- 具体做法是通过设置AlphaBlend,指定透明度的逐步变化。
3. 问题与错误排查:
- 在实际操作中,遇到了一些问题,比如窗口没有显示出来,尽管代码中已经设定了透明度更新。
- 调用UpdateLayeredWindow时传递的参数可能没有正确配置,导致窗口无法更新。尤其是在**设备上下文(DC)**的传递和处理上,可能需要传递正确的设备上下文,而不是简单地传递0。
- 尝试过多种方法,包括使用零作为设备上下文的参数,然而没有成功显示窗口。
4. 其他可能的解决方案:
- 可能需要创建一个有效的设备上下文(DC),或者通过设置更具体的图形上下文来绘制窗口的内容。使用兼容的DC对象来确保窗口能够正确渲染。
- 另一个思路是,创建一个新的绘图区域,并确保使用正确的DC来绘制。这可以通过设置兼容的DC来实现,从而避免透明度更新失败。
5. 最终的计划:
- 由于时间限制,无法在当前会话中完成所有操作,决定将这个问题推迟到下次继续解决。
- 计划进一步思考是否有更简单的方式来处理窗口的淡出效果,例如不需要创建复杂的DC或直接用更简单的方式来填充黑色背景,避免过多的复杂计算和操作。
总结来说,尽管代码本身没有报错,但实际操作中由于设备上下文的处理和透明度的配置问题,导致窗口未能正确显示。接下来将继续进行调试,尝试更合适的方式来实现黑屏淡出的效果。
