游戏引擎学习第225天

发布于:2025-04-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

只能说太难了

回顾当前的进度

我们正在进行一个完整游戏的开发,并在直播中同步推进。上周我们刚刚完成了过场动画系统的初步实现,把开场动画基本拼接完成,整体效果非常流畅。看到动画顺利呈现,令人十分满意,整个系统逐渐成型的过程也非常激动人心。

在上周五的问答环节中,有人提出了一个问题:是否能够在游戏启动前实现一个淡出桌面的效果。我们当时尝试临时快速搭建了一个简单的方案来实现这个功能,但由于时间限制,并没有来得及完成。现在计划把这个功能继续研究一下,看看能否实现这个淡出桌面的视觉效果。

虽然这个功能不会被正式纳入游戏系统当中,但这个问题本身非常合理,而且对方长期以来为这个项目做出了很多贡献,因此我们觉得最起码可以尝试做个示范,回馈一下这种支持。

目前的目标并不是实现一个高强度、完美无瑕的工业级解决方案,而是演示一个思路,一个简单直接的实现方式。之前在论坛上常见的解答者通过社交媒体提醒我们,其实很早以前我们已经讲解过一个更简单的做法,当时我们完全忘了这件事。

现在我们决定,不再使用复杂的 UpdateLayeredWindow API,而是采用更加简单清晰的方法,快速实现窗口淡出桌面的视觉效果,并验证其可行性。整体方向就是尽量简洁有效,集中演示淡出的原理,而不是构建一个完整系统。
在这里插入图片描述

网络查找:SetLayeredWindowAttributes 函数

我们回忆起其实可以使用 SetLayeredWindowAttributes 来实现淡入淡出的效果。早期我们已经演示过这个方法,但因为很久没用,这次开始的时候竟然一时忘记了。幸好后来想起来这个函数的存在,它比我们一开始打算使用的 UpdateLayeredWindow 要简单得多,也更适合快速实现目标。

我们的思路是:为了实现淡出桌面的效果,最兼容的方式可能就是创建一个全屏窗口,这个窗口初始是完全透明的,颜色为黑色。然后我们逐渐增加它的透明度(即减少其透明程度),这样它就会慢慢遮盖住后台的桌面,产生一个“淡出到黑”的视觉效果。

虽然还有其他实现方式,但这种方式的好处是兼容性强,操作相对简单。我们准备尝试实现这种全屏透明黑色窗口的方案,看看是否能达到我们想要的视觉效果。接下来的任务就是用代码快速构建这个窗口,动态调整它的透明度,从而模拟淡出效果。这个方案虽然是临时的测试实现,但能帮助我们验证可行性,并为后续类似的处理提供参考基础。

在这里插入图片描述

在这里插入图片描述

奇怪了

在这里插入图片描述

按道理应该是黑色的才对

晕死show的位置不对

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并观察淡出效果

为了使渐变效果更适当,我们需要做一些调整。具体而言,在实现渐变效果时,需要确保以下几点:

  1. 渐变的实现:通过控制窗口的透明度来实现渐变效果。透明度可以通过SetLayeredWindowAttributes函数进行调整,利用其LWA_ALPHA标志来控制透明度的值,从0(完全透明)到255(完全不透明)。在逐渐增加透明度的过程中,可以使用Sleep来控制渐变的速度。

  2. 设置窗口背景色:如果需要改变渐变窗口的背景色,可以使用CreateSolidBrush来创建一个特定颜色的画刷。通过WindowClass.hbrBackground指定窗口的背景色,常见的做法是使用RGB值来设置颜色,比如使用红色RGB(255, 0, 0)、绿色RGB(0, 255, 0)或蓝色RGB(0, 0, 255)

  3. 控制窗口样式:窗口样式的设置非常重要,特别是需要使用WS_EX_LAYERED样式来启用分层窗口,从而使透明度控制生效。此外,SetLayeredWindowAttributes函数需要指定窗口的颜色键(可以为0)以及透明度的字节值。通过这种方式,可以调整窗口的透明度,从而实现渐变效果。

  4. 内存管理:在设置颜色和透明度时,确保创建的资源(例如画刷)在窗口销毁后被正确释放,避免内存泄漏。这通常在窗口的WM_DESTROY消息中通过调用DeleteObject来完成。

通过这些步骤,可以实现一个渐变淡出的效果,同时根据需要调整背景颜色和透明度。

win32_game.cpp 中将淡出逻辑实现为可逐步调用的函数

为了让渐变效果更为合理,我们考虑了使用固定的时间间隔来替代当前的 Sleep 函数,因为 Sleep 函数并不是一种精确的计时方式,它可能会根据不同机器的性能产生不同的延迟,无法确保在每秒 60 帧的情况下正确控制时间。因此,应该把渐变效果和游戏的主时间轴同步,使其更符合我们实际需要的效果。

步骤和思路:

  1. 避免使用 Sleep 函数

    • 由于 Sleep 不稳定,无法确保每次都延迟相同的时间,导致不准确的效果。因此我们考虑将渐变操作和游戏的主时钟同步,这样每一帧都会精确执行而不是依赖不稳定的等待时间。
  2. 创建一个永久性的渐变窗口

    • 可以在游戏初始化时注册一个新的窗口类,并创建一个窗口来处理渐变效果。这个窗口可以在整个游戏过程中存在,而不是每次调用时重新创建,避免频繁地创建和销毁窗口。
  3. 渐变过程的修改

    • 为了让渐变效果更精确,可以通过传递一个参数来控制窗口的透明度(Alpha值),而不是依赖循环来不断修改透明度。我们将透明度值传递给窗口,然后根据需要进行更新。
  4. 透明度值处理

    • 通过计算一个合适的透明度值(例如通过 Alpha * 255 来将其转换为Windows所需的透明度值),然后使用 SetLayeredWindowAttributes 来控制窗口的透明度。如果透明度为0,窗口将被隐藏;如果透明度大于0,窗口则会显示。
  5. 代码实现细节

    • 在代码中,首先注册一个窗口类并创建一个窗口。然后,在主循环中逐步更新该窗口的透明度。每次调用时更新透明度值,而不是依赖固定的 Sleep 间隔,确保渐变更加平滑和稳定。

总结:

通过将渐变窗口的透明度与游戏的主时钟同步,避免了不稳定的 Sleep 函数,可以实现更加稳定且精确的渐变效果。这种方法不仅能保证渐变效果的流畅性,还能使得透明度的变化更加可控和一致,适应不同机器的性能。
在这里插入图片描述

运行游戏,发现没有任何反应

目前什么都没有发生,因为渐变效果还没有与任何其他操作绑定,所以程序看起来和之前完全一样,没有渐变效果,也没有任何变化。但是,我们可以通过将渐变效果和游戏的主循环或时间系统绑定起来,使其逐步实现。

在当前状态下,代码只是创建了一个窗口并设置了渐变的相关参数,但它并没有在实际运行时与游戏的主时间轴连接。因此,虽然窗口被创建并设置了透明度的变化,但是它没有和游戏的进程同步,因此不会看到实际的渐变效果。

接下来,可以通过将渐变效果的更新逻辑与游戏的主循环或者帧更新机制结合,确保每一帧都会更新透明度,这样渐变才能正常工作并表现出效果。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

win32_game.cpp 中,延迟调用 ShowWindow,直到 FadeAlpha 达到 1.0f

目前在程序中尝试实现渐变效果时,已经尝试将渐变的透明度(fade alpha)作为一个变量,并通过每一帧的时间增量来更新它。目标是让渐变效果平滑地进行,直到达到完全不透明,之后切换到另一个窗口显示游戏内容。

具体的做法是,每一帧根据前一帧的fade alpha值与当前的时间增量(dt)来调整透明度。透明度会逐步增加,直到fade alpha值达到1.0,即完全不透明。此时,窗口的显示状态会切换,从而显示游戏的内容。

然而,程序当前的表现并未如预期,渐变过程显得非常突兀,速度也比预期的要快,显示效果不够平滑。这可能是由于透明度更新的逻辑、时间增量的计算,或者窗口的显示切换发生得太快导致的。

接下来的调试工作将集中在找出问题的根源,可能需要进一步调整透明度的计算方法、时间增量的处理,或是确认是否在适当的时机进行窗口显示切换。

在这里插入图片描述

在这里插入图片描述

启用时间控制循环

为了调试和确保渐变效果的平滑性,决定临时启用实际的帧时间循环。目的是通过正确的时间控制来改善透明度变化的表现,避免之前出现的过快或不平滑的效果。通过这样做,可以更好地看到每一帧的实际时间,并确保透明度更新的过程中,渐变效果更符合预期。
在这里插入图片描述

在这里插入图片描述

生效了
窗口的WS_EX_TOPMOST不能打开不然一直不能退出导致不能操作

运行游戏,发现切换时有轻微闪烁

在调试过程中,发现透明度渐变到最大时,出现了短暂的闪烁现象。具体来说,当透明度达到1时,窗口的切换过程并不如预期那样平滑,可能是因为没有强制更新窗口,导致显示效果不连续。为了调试这个问题,尝试去除“顶层窗口”标志(topmost),这样就可以通过Alt+Tab切换到调试窗口来查看问题的根源。

此外,还需要改进帧率的控制,使其保持稳定在30Hz或60Hz,以确保渐变的流畅性。在调试的过程中,还需要确认是否是窗口管理或系统设置的问题,尤其是在使用全屏模式时可能遇到的问题。

使用调试器进入 ShowWindow 函数

在调试过程中,发现窗口在透明度变化到最大时,存在切换不及时的问题。具体表现为当透明度达到1时,窗口的切换似乎没有立即生效,导致新的窗口未能及时显示。通过调试,发现窗口的隐藏逻辑在透明度达到最大时确实按预期触发了 SW_HIDE,但是新的窗口没有立即显示。

为了避免多次执行相同的操作,加入了一个 fading complete 标志,确保在完成淡出效果后不再重复执行相关操作。尽管这样做后,问题有所缓解,但仍然存在一些小的闪烁现象,尤其是在隐藏窗口后,新的窗口没有及时显示。

为了排查问题,尝试通过检查窗口是否是顶层窗口来确定是否存在问题。通过逐步调试和设置断点,确认了在切换窗口时,新的窗口的显示存在延迟。

可能的解决方案是强制刷新或通过消息循环确保窗口的显示及时更新,避免由于某些延迟导致窗口切换失败。尽管问题并不总是重现,但通过调试确保每个步骤都按预期执行,逐步解决了部分问题。
在这里插入图片描述

在这里插入图片描述

还是会闪一下
在这里插入图片描述

现在不闪不过有两个窗口
在这里插入图片描述

win32_game.cpp 中尝试在显示后执行 ToggleFullscreen,并尝试强制重绘

在尝试调试窗口切换和透明度过渡时,遇到了一些问题,特别是在窗口的切换过程中,新的窗口没有及时显示,导致界面出现闪烁现象。为了强制触发窗口的重新绘制,尝试使用了 InvalidateRectUpdateWindow 方法,这些方法的目的是告诉 Windows 强制重新绘制窗口。但在测试后发现,这并没有解决问题,依然存在切换不流畅的现象。

具体操作是,调用 InvalidateRect 来标记窗口需要重新绘制,然后使用 UpdateWindow 来强制刷新窗口。即使这样做后,问题依然存在,窗口的切换依旧不够平滑,且没有达到预期效果。

进一步检查时,确认了 alpha 值并未出现异常,比如没有发生回绕(wrap-around),透明度变化看起来是正常的。但问题依旧存在,且主要集中在窗口切换的“过渡”阶段,具体原因依然不明。

整体来说,尝试了一些标准的 Windows 窗口操作方法来强制更新和重绘窗口,但这些方法似乎并未完全解决问题。问题可能与 Windows 自身的处理机制有关,特别是在高频率更新窗口状态时,可能存在一些不易察觉的延迟或同步问题。

在这里插入图片描述

还是会闪一下

设置 WindowsAlpha = 255 并测试 IsWindowVisible 是否生效

在处理窗口透明度和显示时,遇到了一些问题,尤其是在窗口的“淡出”效果过程中。具体来说,问题出现在透明度达到 0 时,窗口没有完全隐藏,导致仍然能看到一些残留的效果,甚至出现了闪烁现象。为了排查和解决这个问题,尝试了几种不同的方法。

首先,尝试将窗口的 alpha 值固定为 255(完全不透明),并通过这种方式检查是否会出现闪烁。测试结果显示,即使在完全不透明的情况下,问题依旧存在,因此可以确认不是由于透明度计算错误或 alpha 值异常导致的。
在这里插入图片描述

在这里插入图片描述

接着,试图优化窗口的显示和隐藏逻辑,避免每次都调用 ShowWindowUpdateWindow,以减少不必要的窗口刷新。通过判断窗口是否可见来决定是否调用 ShowWindow,从而避免不必要的操作。尽管这种方式让代码看起来更简洁一些,但仍未能解决核心问题。
在这里插入图片描述

在这里插入图片描述

进一步调试时,考虑到 Windows 系统的特殊性,进行了一些实验性的调整,例如尝试在窗口淡出完成后,通过在消息循环中强制更新窗口的显示状态,看看能否解决窗口未及时重绘的问题。经过尝试,发现给窗口一个“消息循环”的机会似乎有所帮助,窗口在透明度变化后能够正确刷新并显示新的内容。

此外,发现一个新的问题,即窗口顶部的光标依然可见。为了解决这个问题,可以通过在窗口回调函数中禁用光标显示来避免光标影响用户体验。通过调用 SetCursor 方法来隐藏光标,确保在窗口淡出过程中光标也不会干扰显示。
在这里插入图片描述

最后,关于窗口的完全隐藏问题,尽管调用了 ShowWindow 并传入了 SW_HIDE 参数来隐藏窗口,但似乎在某些情况下窗口依然会处于“顶层”状态,导致它在其他窗口之上可见。经过检查,发现可能是传递给 ShowWindow 的参数不正确或缺少必要的标志,导致窗口没有完全隐藏。尽管传递了 SW_HIDE 标志,窗口仍然能够被选中并与之交互,显示为顶层窗口。

通过这些调试步骤,逐步排除了一些潜在的问题,最终发现给窗口一个更新的时间,并通过控制透明度和窗口的显示状态,能够使窗口切换更加平滑。对于 Windows 编程中的一些特殊行为和底层细节,解决方案需要不断尝试和调整,因为 Windows 系统的行为有时难以预测和调试。
在这里插入图片描述

在这里插入图片描述

使用调试器:在 SetFadeAlpha 中打断点,确认 ShowWindow 没有被调用

在调试过程中,遇到了一个问题,即 IsWindowVisible 函数对分层窗口(layered window)返回 false。这导致无法正确判断窗口的可见性,进而影响了窗口的显示和隐藏逻辑。此时,设定了一个断点来检查是否正确调用了相关函数,但并未得到预期的结果。

通过进一步的分析,发现分层窗口似乎不被 IsWindowVisible 函数正确识别为可见窗口,尽管它实际上是可见的。这种行为可能与 Windows 的内部处理方式有关,特别是对于透明或半透明的窗口,Windows 系统可能会把它们视作“不可见”的窗口,即便它们实际上在屏幕上显示。

为了解决这个问题,需要重新评估窗口的可见性检查逻辑,或者使用其他方法来手动控制窗口的显示和隐藏状态。由于 IsWindowVisible 对于分层窗口的处理方式并不符合预期,可以考虑使用其他 API 或方法来判断窗口的状态,确保在操作窗口的显示和隐藏时能够得到正确的反馈。

有被调用的

使用 Spy++ 工具检查窗口样式

我们使用了 Windows 系统下的 Spy++ 工具来检查一个窗口的状态,重点是想确认这个窗口在某些时候是否被标记为“可见”。虽然肉眼能看到这个窗口在显示,但在 Spy++ 工具中检查其窗口样式时,发现并没有设置 WS_VISIBLE 标志,这很奇怪。说明窗口实际上在系统层面上并没有被视作“可见”,但它却仍然显示了出来,这与预期不符。

于是我们设置了一个更早的断点,尝试在窗口正在逐渐淡出、alpha 值约为 0.6 的时候观察这个状态。我们刷新状态后,继续查看这个窗口的属性,果然如之前所见,它没有 WS_VISIBLE 样式位。然而,我们确信自己并没有调用 SW_HIDE 来主动隐藏这个窗口,所以不清楚这个标志是何时被移除的。

进一步回顾了相关代码,检查了所有可能影响窗口可见性的操作,例如 ShowWindowValidateRectSetFadeAlpha 等方法,确认这些逻辑并没有触碰窗口的隐藏流程。

为了查明真相,我们反复打断点并观察行为,最终发现是逻辑上的一个小 bug:窗口的 alpha 值在某些情况下本应设置为 0(即完全透明、应隐藏),但由于一个状态判断逻辑错误(例如 if (!showing_complete)),这个路径并没有执行,因此窗口的可见性没有被更新。

具体来说,在第一次设置 alpha 值时,窗口为 0,状态合理;但在之后的某个流程中,本该再次调用以确保隐藏窗口,却由于条件判断不准确,导致这一步未被触发,窗口实际上依旧保留在桌面上,尽管我们认为它应当已经隐藏。这也解释了为什么 Spy++ 中没有可见标志,而我们却仍然看到它显示出来——本质是状态没有同步更新,系统层面仍将其当作一个“存在但不可视”的窗口处理。

综上,我们找到了问题的根源:状态机逻辑中对窗口淡出完成与是否需要继续处理的判断存在漏洞,导致某些流程没有被调用,从而造成窗口状态与期望不符的情况。需要修正的是对 alpha = 0 时的逻辑处理,确保每次都正确执行隐藏操作。

修正 FadingComplete / ShowingComplete 的判断逻辑

我们在调试过程中发现一个明显的逻辑错误,代码中某个判断条件写得很糟糕,导致某段逻辑被反复无意义地调用,这完全不是我们想要的行为。我们本意是要根据淡入淡出的状态来决定窗口是否应被隐藏或显示,但实际逻辑却写得不清不楚,导致程序行为不符合预期。

具体来说,我们想实现的逻辑是这样的:如果“淡出”已经完成,那么才去检查“淡入”是否尚未完成。如果淡入还没完成,那就执行设置窗口可见的逻辑;否则,如果淡入已经完成而淡出也完成了,那就应该把窗口隐藏起来。结果原本应该是一个分支判断,却变成了每一帧都执行无意义的状态更新,导致窗口状态一直不稳定。

为了修复这个问题,我们计划重新理清状态机的流程,明确每一个状态之间的关系。目标是让程序在“淡出完成但淡入未完成”这个明确条件下执行显示操作,否则在两个过程都完成的前提下执行隐藏。这样做可以保证窗口的行为是清晰、稳定、预期内的,避免出现不断重复调用、窗口状态混乱等问题。

接下来需要整理的就是这部分状态控制逻辑,确保只在状态变更时更新窗口,而不是在每一帧都进行无效操作。这种状态判断错误虽然看起来小,但在实际运行中却可能引发很大的显示逻辑混乱,尤其是在使用了 alpha 淡入淡出、分层窗口、窗口显示/隐藏切换等机制的情况下。我们正在逐步理清这类问题,使窗口逻辑更加可靠。
在这里插入图片描述

在这里插入图片描述

运行游戏,确认之前的 bug 不再出现

我们最终定位并修复了一个关键的 bug,这个问题在于之前那个窗口并没有被正确地销毁或隐藏掉,导致它在某些时候仍然会显示出来,这可能是造成视觉残影或者“闪烁”问题的根源。现在我们通过调整逻辑,确保在适当的时候正确移除该窗口,验证后已经确认它不会再出现在屏幕上,这说明该部分逻辑终于如预期工作了。

虽然之前修复后还担心是否会再次出现之前那个闪屏的问题,但实际测试下来情况良好,目前没有发现那个烦人的窗口重新出现,一切表现都非常正常,这是一个非常积极的信号。窗口管理部分的行为终于变得清晰、干净了。

现在整个窗口切换的行为看起来更加符合预期:我们在淡入淡出完成之后可以准确控制窗口的可见状态,避免了之前因判断逻辑错误而导致的状态混乱或者显示异常。此外,通过这次修复,我们的状态管理流程也变得更稳定,UI 方面也更流畅了。

总的来说,目前阶段的结果令人满意,窗口状态干净利落,没有遗留问题。接下来可以继续考虑一些优化,比如进一步提升用户体验,或者添加一些额外的细节特效来完善整体表现。但从功能性层面来看,这一阶段的问题算是成功解决了。

引入 dFadeAlpha,用于从游戏淡出返回桌面

我们计划实现一个反向的淡入淡出过程,也就是在退出时使用和进入时相反的视觉效果来过渡,提升用户体验。

首先,我们尝试在原有的淡出逻辑中加入一个 dFadeAlpha 参数,用来控制淡出速度,并通过乘以当前时间增量等方式平滑过渡。我们会用 clamp 来限制淡出值在合理范围内,确保视觉上始终保持一致。

然后我们引入一个标志位 QuitOnFade,用于判断当前是否是在退出过程中执行淡出逻辑。如果该标志被设置,我们在淡出结束时不立即关闭程序,而是进行一系列后续处理。比如在开始退出时,我们将 FadeAlpha 设置为 1.0,也就是完全黑,然后逐步降低到 0.0,达到“从黑屏淡出到桌面”的视觉效果。

为了正确支持这种行为,我们需要在退出流程中添加一系列状态设置:

  • FadingCompleteshowing_complete 都设置为 false,表示一个新的动画开始;
  • 显示淡出窗口;
  • 隐藏主窗口;
  • 开始反向更新 FadeAlpha 的值(比如设置 dFadeAlpha = -1.0);
  • FadeAlpha == 0.0 时标记为 FadingComplete = true,然后根据 QuitOnFade 判断是否真正退出程序。

这套机制的关键在于正确管理 FadeAlpha 的方向以及何时真正触发退出。我们还注意到一些顺序问题,比如在执行 SetFadeAlpha() 前必须确保窗口是正确显示的状态,因此我们在调用前要确保淡出窗口被激活,而主窗口被隐藏。

最后我们测试该逻辑,发现第一次执行时并没有按预期工作,退出时动画没有触发。于是我们回头检查 QuitOnFade 的处理逻辑和状态设置的先后顺序,确认是否正确设置了 FadingComplete = false 以及是否正确激活了淡出窗口。通过调整顺序,尝试将关键设置放到状态更改之前,来确保窗口正确响应。

整体上,这段逻辑虽然比较繁琐,但为平台层代码中的视觉过渡提供了结构合理的处理路径。当前实现还有细节需要打磨,但主流程逻辑已经初步成型,能够支持程序在退出时通过动画淡出到桌面,并保持状态清晰且可控。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

按ESC键退出有逐渐退出的画面

win32_game.h 中引入 win32_fader_statewin32_fader

此时整体代码逻辑已经变得过于凌乱和零散,为了让结构更加清晰、可维护,我们决定将这部分功能从当前的流程中抽离出来,进行更合理的封装。

我们设想把与窗口淡入淡出相关的功能封装成一个独立的结构,比如 Win32FadeWindow,其中包含:

  • 窗口句柄;
  • 当前的 fade_alpha 值;
  • 当前状态(state),如正在淡入、正在淡出、显示中、等待显示、非活动等。

通过这种方式,我们能够将大量冗长、状态分散的处理逻辑集中到一个统一的更新函数中,比如 UpdateFade(),这个函数的职责就是:

  • 根据当前状态判断如何更新 fade_alpha
  • 控制窗口的可见性;
  • 在淡入或淡出完成时更新状态为“完成”;
  • 管理窗口的显示与隐藏等。

我们会定义几个枚举状态,表示淡入淡出的不同阶段,包括:

  1. FadingIn(正在淡入);
  2. WaitingForShow(等待主窗口显示);
  3. Showing(淡入完成,正常显示);
  4. FadingOut(正在淡出);
  5. Inactive(不活跃,不显示);

这样,每次进入更新函数时,只需判断当前状态,然后根据具体逻辑做出相应操作,例如:

  • 如果是 FadingIn,则递增 fade_alpha
  • 如果是 FadingOut,则递减 fade_alpha
  • fade_alpha 达到阈值(0.0 或 1.0)时切换状态;
  • 当从 FadingOut 到达 Inactive,可触发程序退出;
  • FadingIn 到达 Showing,则标记为“显示完成”。

这一改动使得窗口动画的更新不再是依赖一堆零散变量和逻辑判断的混乱流程,而是成为一个结构化、状态驱动的过程。这种方式在架构上更加健壮,后期也方便扩展或调整效果(比如增加淡入淡出的时间、插值方式或其他视觉过渡),也更符合平台层的职责与风格。

总的来说,我们从一堆混乱的控制逻辑出发,成功走向一个清晰、模块化、可维护的状态机模型,为整套窗口管理提供了更高质量的基础。

在这里插入图片描述

在这里插入图片描述

win32_game.cpp 中引入 UpdateFade 函数

我们将之前用于控制窗口淡入淡出的混乱代码逻辑,统一整理到一个新的模块中,并称这个模块为 Win32Fader,也可以简单称为 Fader。这个模块的作用就是管理一个特定的窗口,通过控制其透明度 (fade_alpha) 实现淡入淡出动画效果。这个结构不仅清晰地表示出其职责,还让原先复杂、分散的逻辑变得集中和可维护。

我们打算将之前所有关于淡入淡出的控制代码整合进这个结构中,把它实现成一个独立组件,具体步骤如下:

  1. 结构设计:

    • 定义一个 Win32Fader 对象,其中包含:
      • 一个窗口句柄;
      • 当前的透明度值 fade_alpha
      • 当前淡入淡出所处的状态(如正在淡入、正在淡出、等待显示、非活动等);
      • 一个目标透明度变化速度值(如每帧淡入/淡出量);
      • 一些控制标志(比如是否请求退出等);
  2. 迁移旧逻辑:

    • 将原先分散的代码集中拷贝出来,作为基础;
    • 整理成模块内部函数,尤其是提取出一个 Update()UpdateFade() 函数;
    • 将逻辑根据状态机进行整理,不再依赖多个外部判断;
    • 每次更新时,根据当前状态自动推进动画进程,例如:
      • 状态为 FadingIn 时,增加 fade_alpha
      • 状态为 FadingOut 时,减少 fade_alpha
      • 到达透明或不透明时切换状态;
  3. 简化外部调用:

    • 外部不再需要手动控制透明度或隐藏窗口;
    • 只需设置目标状态或触发“淡出后退出”等行为,由 Win32Fader 内部自动处理。

通过这种封装,原本散落在代码各处的冗长处理逻辑被整合成一个明确职责的模块。接下来,我们只需要继续细化这个结构,分离初始化函数、状态更新函数、绘制函数(如有),并将旧逻辑逐步转移进来,整个流程就能变得更具可控性和可扩展性。最终,这将大幅提高窗口动画相关代码的可维护性与整体结构清晰度。
在这里插入图片描述

在这里插入图片描述

win32_game.cpp 中制作淡入淡出的有限状态机

我们重新整理并完善了淡入淡出窗口(Win32Fader)的状态机逻辑,实现了清晰的状态转移和行为执行流程,目标是让代码更模块化、可读性更强,减少之前混乱、分散的控制流程。当前的设计主要包含如下内容:


状态分类与含义

我们定义了若干状态,每种状态下行为明确:

  1. Fading In(淡入中)

    • 每帧将 fade_alpha 增加一定值(比如 +dt);
    • 如果 fade_alpha >= 1.0,则将其钳制为 1.0,并转入 Waiting For Show(等待显示) 状态;
  2. Fading Out(淡出中)

    • 每帧将 fade_alpha 减去一定值(比如 -dt);
    • 如果 fade_alpha <= 0.0,钳制为 0.0,并转入 Waiting For Close(等待关闭) 状态;
  3. Waiting For Show(等待显示)

    • 执行一次性逻辑,例如显示主窗口,或设置其他显示相关参数;
    • 然后立即转入 Inactive(非活动) 状态;
  4. Waiting For Close(等待关闭)

    • 同样无需主动行为,只是静候系统关闭或者下一步外部指令;
    • 通常此状态用于确保淡出动画播放完成后才执行关闭动作;
  5. Inactive(非活动)

    • 无操作,表示 Fader 当前不在动画状态;

动画更新逻辑(伪代码逻辑如下)

switch (state) {
  case FadingIn:
    fade_alpha += dt;
    if (fade_alpha >= 1.0) {
      fade_alpha = 1.0;
      state = WaitingForShow;
    }
    break;

  case FadingOut:
    fade_alpha -= dt;
    if (fade_alpha <= 0.0) {
      fade_alpha = 0.0;
      state = WaitingForClose;
    }
    break;

  case WaitingForShow:
    // 执行显示主窗口的逻辑
    state = Inactive;
    break;

  case WaitingForClose:
    // 等待外部关闭程序或进一步处理
    break;

  case Inactive:
    // 什么都不做
    break;
}

其他细节

  • 设置 fade_alpha 时确保调用 SetLayeredWindowAttributes() 来更新窗口可视度;
  • 在适当状态切换时调用 ShowWindow()HideWindow()
  • 使用 quit_on_fade 标志标记程序是否在淡出后需要退出;
  • 用统一的 UpdateFade() 函数处理状态机推进逻辑,让每帧都保持一致处理流程。

通过这套改进,我们将原来分散、混杂在各处的淡入淡出代码统一整合,构造出清晰的状态管理系统。每种状态只负责一件事,使程序逻辑更简洁易懂,也为后续扩展(比如加入更多动画类型)打下良好基础。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

简要说明这个状态机的结构

我们目前已经初步完成了一个小型状态机的构建,这其实就是在将之前由多个变量共同决定行为的逻辑,转化为明确的“模式”(或称状态),并为每种模式命名,然后在这些状态之间建立清晰的转换流程。

虽然我们还没有系统性地深入讲解有限状态机(FSM)的全部内容,但这个过程算是一个简洁的初步介绍。核心思想如下:


状态机的基本转化过程

  1. 原状
    原本的逻辑是靠一些布尔变量、标志位和条件判断分散控制流程,代码显得杂乱且不直观。

  2. 重构目标
    将所有运行中可能出现的“运行模式”提炼为一组“状态”,并为每一个状态起一个明确的名字,例如:

    • FadingIn(淡入中)
    • FadingOut(淡出中)
    • WaitingForShow(等待显示)
    • WaitingForClose(等待关闭)
    • Inactive(非活动)
  3. 明确转换逻辑
    状态之间不再由杂乱无章的布尔判断控制,而是通过统一的状态转移条件控制,比如:

    • 如果 fade_alpha >= 1.0,就从 FadingIn 转换为 WaitingForShow
    • 如果 fade_alpha <= 0.0,就从 FadingOut 转换为 WaitingForClose

状态机带来的好处

  • 可读性提高:通过状态名称,可以直观地理解当前代码所处的运行阶段;
  • 可维护性增强:每个状态负责单一行为,逻辑更清晰,容易调试和修改;
  • 逻辑更健壮:减少了因状态变量交叉控制导致的边界错误或未定义行为。

接下来的工作

当前状态机框架已经完成,现在我们只需将它与主逻辑正式“接线”使用,即:

  • 在主程序的更新循环中调用状态机的 Update 函数;
  • 确保每帧根据当前状态执行合适的动画推进或状态转移;
  • 对过渡条件做测试,确保如 fade_alpha 到达阈值时能够正确切换状态;
  • 继续调试和优化动画流程,防止边缘状态或视觉残影。

这个改造使整个代码结构更加模块化和系统化。虽然状态机在这个例子里是简单版本,但它是构建复杂逻辑行为的基础,也是大型程序组织的重要手段。我们后续可能还会进一步扩展和深入这类模式。

win32_game.h 中添加 Win32Fade_FadingGame 状态

我们在现有的淡入淡出流程基础上,准备增加一个新的状态逻辑:淡出游戏画面。这个新增的状态将命名为 FadingGame,其作用是在从游戏切换到桌面前,让游戏界面也逐渐淡出至黑色,增强过渡的视觉体验。


新增状态:FadingGame

我们新增的状态 FadingGame 主要逻辑如下:

  1. 目的
    在游戏退出前,让游戏画面也以淡出的方式消失,视觉上更加自然统一。

  2. 实现方式

    • 状态检测中加入 FadingGame 这个选项;
    • fade_alpha >= 1.0 时,说明淡出动画已完成;
    • 此时,隐藏游戏窗口,调用相应函数隐藏主游戏窗口;
    • 然后立刻将状态切换至 FadingOut,进入黑屏;
    • 之后继续进入原有逻辑流程,即从黑屏淡入桌面界面。
  3. 窗口传递
    因为需要操作游戏窗口,所以在调用 update_fade 相关函数时,要额外传入 game_window 参数,方便控制隐藏操作。


整体流程梳理(完整状态机变更后)

  1. FadingIn:从黑屏渐变到游戏界面;
  2. WaitingForShow:一次性等待消息循环,随后立即转为 Inactive
  3. Inactive:无动画,无需更新;
  4. FadingGame:游戏画面渐变至黑色,完成后隐藏游戏窗口并转为 FadingOut
  5. FadingOut:从黑色界面渐变回桌面;
  6. WaitingForClose:等待关闭处理;
  7. QuitOnFade(相关标志):表示整个流程结束后应退出程序。

当前的进度与目标

  • 代码逻辑已经逐步清晰并模块化;
  • 状态机结构完善,便于拓展和管理;
  • 添加 FadingGame 增强过渡体验,提升视觉连贯性;
  • 后续仅需把窗口控制逻辑整理归类,并把这些状态条件在调用层级中正确串联,即可达到预期效果。

整体来说,这一步是为了让用户从游戏回到桌面时,过程更加自然顺畅,同时使状态管理更具弹性和拓展性,为未来更多过渡效果打好基础。
在这里插入图片描述

在这里插入图片描述

win32_game.cpp 中添加 InitFaderBeginFadeinBeginFadeOut

我们对现有的淡入淡出系统进行了结构性整合与增强,使其具备更清晰、更具模块化的状态管理逻辑,以下是详细说明:


淡入淡出系统整体架构升级

主要改动内容:
  1. 引入初始化函数 init_fader

    • 集中处理初始化 fader 的构造逻辑;
    • 提取原有分散的初始化代码块;
    • 将其独立成函数,提升复用性和代码可读性。
  2. 状态重命名更清晰

    • 原本的 begin_fade_inbegin_fade_out 命名不够准确,重命名为:
      • begin_fade_to_game:从桌面黑屏渐变至游戏;
      • begin_fade_to_desktop:从游戏界面淡出,最终显示桌面。
  3. 统一使用 fader 状态结构体管理流程

    • fader_state 作为枚举,用于标识当前处于哪种动画状态;
    • fade_alpha 用于表示当前淡入淡出的透明度进度值;
    • 所有状态之间的跳转由 update_fade() 函数统一处理,变得更直观、清晰。

逻辑流程梳理

启动淡入至游戏:
  • begin_fade_to_game() 会:
    • 设置 fader_state = FadingIn
    • fade_alpha = 0.0
    • 初始化相关窗口句柄。
游戏退出并切换到桌面:
  • begin_fade_to_desktop() 会:
    • 检测退出标志是否已置位;
    • 若尚未触发淡出动画,则开始 fader_state = FadingGame
    • 之后按 fade_alpha 递增,达到 1.0 后隐藏游戏窗口;
    • 切换状态至 FadingOut,淡入桌面。
状态轮询与动画更新:
  • 在主循环中调用 update_fade()
    • 每帧检查当前 fader_state 并执行对应逻辑;
    • 返回新的状态;
    • 若进入 WaitingForClose,则将 global_running 设置为 false,触发程序退出。

遇到的Bug及修复

  1. 退出键重复触发初始化

    • begin_fade_to_desktop() 被多次调用,持续重置 fade_alpha = 0.0
    • 导致动画永远无法完成,程序停留在黑屏状态;
    • 解决方案:增加判断,只有当当前状态不为任意淡出状态时,才重新初始化 fader
  2. Alt+F4 vs ESC 键冲突

    • Alt+F4 本应立即关闭窗口,但此处观察到 ESC 键无效;
    • 需确认键盘事件处理逻辑,将 ESC 正确绑定为触发淡出;
    • Alt+F4 行为应绕开动画系统直接关闭。

模块结构整理建议

  • fader.h / fader.cpp:用于封装整个状态机逻辑,包括初始化、状态更新、状态标识;
  • main.cpp:负责处理消息循环和调用 update_fade()
  • game_window.cpp:负责实际的窗口隐藏、显示等操作;
  • input.cpp:键盘事件逻辑中应检测 ESC 键触发 begin_fade_to_desktop()

当前阶段成果

  • 状态驱动的淡入淡出系统已建立;
  • 渐变动画、窗口隐藏、退出流程逻辑基本完整;
  • 主循环中状态更新统一处理,便于后续扩展与维护;
  • 已定位关键Bug并进行逻辑完善。

整体来看,整个系统从最初的字符串驱动、分散逻辑逐步演进为具备清晰状态结构的模块化框架,增强了可维护性、调试能力及后续拓展性。接下来只需完善键盘输入绑定及窗口状态同步即可进入稳定阶段。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

奇怪运行没反应

对UpdateFader 打断点
在这里插入图片描述

Fader没有初始化
在这里插入图片描述

进入可以发现不能退出很奇怪
在这里插入图片描述

在这里插入图片描述

仅在状态为 Win32Fade_Inactive 时才执行 BeginFadeToDesktop

我们进一步完善了窗口淡出流程中的细节逻辑,以下是本阶段的调整与修复内容:


状态判断优化

  • 在触发从游戏窗口返回桌面淡出的 begin_fade_to_desktop() 调用中,增加条件判断
    • 只有当前 fader_stateInactive(即空闲)时,才允许发起新的淡出请求;
    • 防止重复触发 fade_alpha 重置的问题,避免动画逻辑被不断覆盖;
    • 虽然目前处理方式较为“临时”或“hacky”,但作为功能防御措施已可起到基本保护作用;
    • 后续可考虑通过状态机设计更明确的状态跳转函数,让状态过渡更规范和显式。

窗口未隐藏Bug修复

问题:
  • 淡出动画结束后,窗口未按预期隐藏,导致窗口残影/未清除现象;
  • 初步排查认为是窗口 Alpha 值未被重置导致;
调试过程:
  • set_fader_alpha(0.0) 操作未在正确时机执行;
  • 原本设定为在进入 WaitingForShow 状态时执行此重置;
  • 实际逻辑遗漏了这个调用或调用顺序错误。
修复方案:
  • 明确在状态跳转至 WaitingForShow 时执行:
    • set_fader_alpha(0.0):立即设置窗口完全透明;
    • 并在此阶段将窗口置于隐藏状态或可自动销毁状态;
  • 此步骤设计的初衷是为了解决窗口在桌面上“闪现一帧”的视觉残留 Bug,所以非常关键。

状态说明回顾

状态 说明 特别操作
FadingIn 从黑屏进入游戏 fade_alpha 递增到 1.0 后切换状态
WaitingForShow 动画完成但未完全展示窗口前的一帧 此处执行 set_fader_alpha(0.0) 以隐藏淡出窗口
Inactive 无动画进行中 可触发下一阶段动画
FadingGame 游戏开始淡出 fade_alpha 增至 1.0 后隐藏游戏窗口并切换至 FadingOut
FadingOut 桌面淡入过程 fade_alpha 减至 0.0 后转入 WaitingForClose
WaitingForClose 准备退出阶段 主循环检测到后关闭程序

当前阶段成效总结

  • 增加了状态判断保护,避免多次触发初始化;
  • 修复了窗口淡出后残留问题,通过精确状态切换点执行 fade_alpha=0.0
  • 淡入淡出流程的状态衔接更加可靠,整体可控性提高;
  • 虽部分实现尚未完全规范(如状态跳转可更显式封装),但逻辑已逐步趋于健壮。

下一阶段我们可以考虑将状态切换抽象为函数或事件,减少状态管理分散性,提高系统的模块化和可测试性。同时,完善窗口行为与 Alpha 值控制的绑定机制,确保视觉效果的统一与自然。
在这里插入图片描述

可以了
在这里插入图片描述

运行游戏,确认一切正常

我们完成了整个淡入淡出机制的搭建和验证,最终效果已经达到了预期,以下是详细总结:


Alt+Tab 层级表现正常

  • 淡出窗口在 Alt+Tab 的窗口列表中显示方式正确;
  • 表明窗口句柄与可见状态管理已符合系统标准;
  • 此行为验证了 Fade Window 的创建和显示逻辑没有破坏操作系统的窗口层级机制。

⌨ESC 键触发退出流程

  • 按下 ESC 键后,窗口成功执行 淡出动画,从游戏画面过渡至黑屏;
  • 然后再从黑屏 淡回桌面
  • 流程稳定,效果平滑无跳帧,符合用户对过渡的视觉预期。

游戏执行与退出切换逻辑确认

  • 游戏窗口启动 → 淡入显示 → ESC 退出 → 淡出隐藏 → 返回桌面;
  • 所有状态机切换稳定运行:
    • FadingInWaitingForShowInactive
    • FadingGameFadingOutWaitingForClose
  • 整体逻辑闭环无冗余操作或逻辑错误;
  • 状态同步与 fade_alpha 的更新节奏一致,确保动画流畅。

当前成效汇总

  • 完成了游戏与桌面之间的双向淡入淡出系统,并实现了状态驱动的视觉过渡;
  • 窗口在系统层级中表现良好,包括任务栏、Alt+Tab、Z序等;
  • 核心状态机逻辑稳定可靠,涵盖多个子状态,过渡逻辑清晰;
  • 按键响应与窗口控制统一集成,符合正常操作习惯;
  • 当前系统具备良好的可扩展性,未来可继续拓展例如淡入时播放音效、黑屏渐现LOGO等更多细节。

这个阶段的开发目标已经全面达成,后续可以继续在交互性、动画细节以及资源管理等方面优化,使其更加精致与完善。

Fading 窗口没有隐藏呢
在这里插入图片描述

在Fading 窗口没有活动的状态隐藏

回顾状态机

在这里插入图片描述

在这里插入图片描述

plantuml

@startuml
skinparam monochrome false
skinparam shadowing true
skinparam roundcorner 10
skinparam state {
  BackgroundColor LightBlue
  BorderColor DarkBlue
  FontColor Black
  FontSize 14
  ArrowColor DarkBlue
  ArrowFontColor Black
  ArrowFontSize 12
}
skinparam note {
  BackgroundColor LightYellow
  BorderColor DarkGoldenrod
  FontColor Black
}

[*] --> FadingIn : BeginFadeToGame()\nFaderAlpha = 0.0f
note left of FadingIn
  Triggered by BeginFadeToGame()
  Starts fade-in to show GameWindow
end note

state FadingIn : FaderAlpha += dt\nSetFaderAlpha(FaderAlpha)
FadingIn --> FadingIn : FaderAlpha < 1.0f
FadingIn --> WaitingForShow : FaderAlpha >= 1.0f\nSetFaderAlpha(1.0f)\nShowWindow(GameWindow, SW_SHOW)\nInvalidateRect\nUpdateWindow
note right of FadingIn
  Fader window fades in (Alpha 0→1)
  Prepares to show GameWindow
end note

state WaitingForShow : SetFaderAlpha(0.0f)
WaitingForShow --> Inactive : Immediate
note right of WaitingForShow
  Resets fader to transparent
  Transitions to idle state
end note

state Inactive : ShowWindow(Fader->Window, SW_HIDE)
Inactive --> Inactive : No action
Inactive --> FadingGame : BeginFaderToDesktop()\nFaderAlpha = 0.0f
note right of Inactive
  Fader window hidden
  Waits for fade-to-desktop trigger
end note

state FadingGame : FaderAlpha += dt\nSetFaderAlpha(FaderAlpha)
FadingGame --> FadingGame : FaderAlpha < 1.0f
FadingGame --> FadingOut : FaderAlpha >= 1.0f\nSetFaderAlpha(1.0f)\nShowWindow(GameWindow, SW_HIDE)
note right of FadingGame
  Fader window fades in (Alpha 0→1)
  Prepares to hide GameWindow
end note

state FadingOut : FaderAlpha -= dt\nSetFaderAlpha(FaderAlpha)
FadingOut --> FadingOut : FaderAlpha > 0.0f
FadingOut --> WaitingForClose : FaderAlpha <= 0.0f\nSetFaderAlpha(0.0f)
note right of FadingOut
  Fader window fades out (Alpha 1→0)
  Prepares to close
end note

state WaitingForClose : No action
WaitingForClose --> WaitingForClose : Waits for external close
note right of WaitingForClose
  Fader window fully transparent
  Awaits program termination
end note

@enduml

在这里插入图片描述

提问:你可能需要一个新状态来等待 alpha 开启窗口显示,否则可能还会遇到同样的 bug

我们之所以认为在从“激活态”切换到“FadingGame”(淡出游戏)时不会出现“窗口未显示”的bug,主要是基于对 Windows 分层窗口机制的理解。具体来说,我们认为不会出错的原因有以下几点:


核心推理逻辑

  1. SetLayeredWindowAttributes 立即生效:
    我们调用 SetFaderAlpha 的时候,其内部实现会调用 SetLayeredWindowAttributes,这是直接操作窗口的透明度,不依赖消息循环。当我们设置 alpha 值时,窗口立即进入 Windows 的 Alpha 合成流程。

  2. Alpha 合成与消息队列无关:
    这个 Alpha 合成过程是系统层面处理的,它不会等待消息泵处理队列,不会因为缺乏消息处理而阻止透明效果的更新。

  3. ShowWindow 与 SetLayeredWindow 的区别:

    • ShowWindow 是一个触发行为,它依赖窗口消息机制(如 WM_SHOWWINDOW 等)来生效。
    • SetLayeredWindowAttributes 是立即生效的属性更改,不通过消息队列。

    所以,淡入时需要等待 ShowWindow 成功地显示窗口,因此需要 WaitingForShow 状态以确保其完成。而淡出过程中,是我们手动设置透明度至 1.0,再执行 ShowWindow(GameWindow, SW_HIDE),这个行为是明确地关闭窗口,因此不需要等待“显示完成”的过程。

  4. 从 Inactive 到 FadingGame 不会依赖消息循环:
    因为我们在进入 FadingGame 状态前就已经设置了分层窗口的透明度为非零值,它立即生效,不存在同步问题,也就避免了窗口未完全激活却被隐藏的 bug。


经验与观测支持推论:

  • 在测试过程中,并未观察到该 bug 出现。
  • 游戏窗口淡出及返回桌面过程流畅,未发生“窗口一闪而逝”或“残影”等异常现象。
  • 因此,我们认为当前的实现路径是安全的。

🔄若未来出现该类问题的可能应对方式:

如果将来发现 fade 到桌面过程中确实存在类似“窗口未显示完成就被隐藏”的问题,可以考虑添加额外的同步点,例如:

  • 加入 WaitingForHide 状态来确保 SW_HIDE 完成。
  • 或在 SetFaderAlpha(1.0f) 后延迟一定时间,确认视觉过渡完成。

总结:

我们之所以认为淡出(Fade to Desktop)过程无需额外等待窗口激活,是因为 SetLayeredWindowAttributes 是立即生效的系统调用,不依赖消息队列。这与 ShowWindow 完全不同,因此没有引发 bug 的同源条件。目前从逻辑推理和实际运行来看,都支持这个判断,后续也可以继续观察验证这一点。

提问:你的淡出设置似乎太复杂了,为什么不直接截图、渲染到后备缓冲区然后做淡出?这在什么平台上不可行?

我们不选择“截一张桌面图贴到后台缓冲区再进行淡出”的方式来实现淡出动画,原因非常多,具体如下:


技术层面的问题

  1. 无法可靠地获取后台缓冲区内容
    Windows 在某些场景下会禁止读取后台缓冲区的内容。此时我们尝试截取桌面时,只会截取到黑屏或无效数据。淡出动画在这种场景下就会直接显示错误画面或视觉闪烁,非常不稳定。

  2. 图像叠加层(Alpha Overlay)问题
    桌面可能存在由显卡硬件合成的 YUV 或颜色键叠加层,这些叠加内容是由 GPU 直接渲染的,不能通过软件截图捕捉。这意味着在某些场景下,截到的桌面图像是不完整的,会在淡出时造成“撕裂”或“闪烁”的视觉问题。

  3. 桌面分辨率未知或不一致
    我们无法提前知道用户当前桌面的分辨率。假设桌面是 2440x1440,而我们游戏是 1280x1024,如果我们直接贴图淡出,那么:

    • 要么截图拉伸导致画面模糊;
    • 要么淡出时会看到分辨率突然改变(窗口变形),造成明显的视觉割裂;
    • 甚至系统可能先将桌面强行切换分辨率后才淡出,过渡体验会非常差。

替代方案优势**:

  1. 需要复杂的图像拉伸与资源切换
    为了避免上述分辨率问题,必须先创建一个与桌面分辨率一致的缓冲区贴图,再进行淡出,然后销毁,再创建游戏专用分辨率的缓冲区。这个流程看似简单,实则涉及多个资源的申请与释放,状态管理复杂,非常容易出错。

  2. 这种方式反而更复杂、更不可靠
    本质上,看似“简单”的贴图方式,背后隐藏着许多系统层面的陷阱与兼容性问题。对比之下,我们采用的做法是直接创建一个分层窗口,然后通过 Windows 自带的 Alpha 合成机制来实现淡入淡出,整个流程在系统层面完成,兼容性好,效果稳定,也不会被保护内容影响。


总结

我们之所以避免使用“桌面截图并淡出”的方式,是因为该方式存在严重的兼容性问题和复杂的边界条件,例如无法捕捉受保护内容、无法处理硬件叠加层、桌面分辨率不一致等。同时,反而需要做更多额外的工作来补救这些问题。相比之下,直接利用系统提供的 Alpha 混合机制构建一个透明的分层窗口进行淡入淡出,是更稳健、更通用、也更易维护的方式。

提问:为什么用第二个窗口来做淡出,而不是直接对主窗口做?

我们之所以使用第二个窗口来实现淡入淡出效果,而不是直接对主窗口进行淡入淡出,原因其实是出于性能考虑。

原本我们的想法确实是直接在主窗口上进行淡入淡出,这在逻辑上也更合理。但我们在最近调试 Layered Window(分层窗口) 与透明度时发现了一个关键问题:

当我们在主窗口上设置 WS_EX_LAYERED 这个窗口样式标志后,程序的性能会严重下降。我们不便用太粗俗的词来形容,但可以理解为性能“极其糟糕”。

虽然目前我们使用的是 StretchBlt 来进行窗口内容的绘制,但将来我们计划使用 OpenGL,而这个问题在使用 OpenGL 时尤为明显。OpenGL 的渲染路径和 Layered Window 不兼容,性能瓶颈极大。原因可能是在驱动层或与 Windows 的合成器协同机制中存在问题,导致 OpenGL 无法走上真正的高性能渲染通道。

因此,绝对不应该在主窗口上启用 Layered Window 属性,尤其是在渲染性能至关重要的场景中。我们进行了实测验证,确认只要使用了这个标志,性能就会显著下降。

基于此,我们采取了现在的方案:创建一个专门用于淡入淡出的第二窗口,该窗口设置为分层窗口,用于执行 Alpha 淡入淡出的视觉过渡,而主窗口则始终保持性能最佳的设置。这种方式虽然结构上稍显复杂,但带来的性能收益和可控性远远优于直接操作主窗口。

总的来说,为了在 Windows 上实现平滑的淡入淡出过渡效果,同时保持渲染效率,这是我们能找到的最可靠做法。

提问并致谢:能否让窗口的某些部分透明,比如某个怪物把窗口撕开看到桌面?

我们原本也设想过是否可以让窗口的某些部分变得透明,比如模拟一种效果——游戏窗口被怪物撕开了一个洞,通过那个洞可以看到桌面上的树叶等内容。但实际上,这是无法实现的,至少在性能可接受的前提下是不可行的。

从技术角度来说,确实可以通过 Layered Window(分层窗口)机制来实现部分透明区域的窗口渲染,比如设置窗口区域的 alpha 值,让某些区域显示为透明。然而,真正的问题并不在于能否做到,而在于性能开销非常严重

我们亲自进行了测试,发现仅仅是将窗口设置为 Layered Window,即使不启用实际透明度变化,只是设置了这个标志位,OpenGL 的渲染性能就会大幅下降。哪怕窗口仍然是完全不透明的,仅仅启用了分层窗口功能,性能也出现了非常明显的下降,下降的幅度以毫秒计,非常可观,已经足以对帧率产生负面影响。

因此,从性能角度来看,这种做法完全不可接受。我们希望游戏的图形处理能够走操作系统提供的高性能路径,而分层窗口显然与这一目标相悖。

也许在某些特定版本的 Windows,比如 Windows 10 或更高版本,这种性能问题已经被修复,但我们没有理由假设所有用户都运行在最佳环境下。我们不可能在数百台机器上去逐一验证,因此在开发机上测试确认该方式性能低下后,我们选择放弃这种做法。

总结来说,虽然分层窗口可以实现窗口局部透明,但由于对 OpenGL 图形性能的严重影响,这种方法在高性能需求的游戏中并不可行。我们必须确保所有图形操作尽可能地避免触发系统的慢路径,以维持稳定且流畅的渲染表现。

提问:假设你是微软,但目标不是赚尽天下钱,而是靠卖操作系统谋生,那你是否可以提供一种更好方式让用户调试 API 相关代码?

我们认为,如果目标只是靠销售操作系统来维持生计,而不是谋求世界上所有的财富,那么完全可以在不影响商业模式的前提下,为平台开发者提供良好的调试手段。

从我们的判断来看,即便微软将除了内核级别之外的所有系统组件源代码都公开,比如 user32.dll 相关的源代码,也完全不会对其销量造成任何负面影响,甚至有可能反而促进销售

这是因为:

  1. 开发者将更容易调试使用系统 API 的程序,从而能更高效地开发和维护 Windows 上的软件。
  2. 安全研究人员将能更快发现系统漏洞并协助修复,整体提升操作系统的安全性和稳定性。
  3. 真正对系统底层有研究需求的人极少,普通开发者根本不会去调试内核级别的代码,除非他们配有完整的内核调试环境,这种人非常少,所以保留内核代码的封闭性已经足够。

此外,微软本身已经提供了符号服务器(Symbol Server),可以用来查看栈帧对应的函数和调用位置,开发者在崩溃时也能定位到具体的模块和偏移。但目前没有源代码,只能看到汇编层面的东西,这对排查问题非常不便。

我们认为,如果微软直接公开这些高层模块的源代码,将会极大改善开发者调试体验,而微软的核心商业模式不会因此受损。反而,Windows 可能因此对开发者变得更加友好,从而进一步增强其平台的吸引力。

因此,我们结论是:微软完全可以这么做,而且没有理由不这么做,除非他们就是不想开放。我们强烈倾向于认为,在不牺牲系统安全或商业秘密的前提下,开放高层系统库的源代码是一种双赢的做法。

继续问:做完淡出后不能移除 layered window 标志吗?

我们非常确定,仅仅在淡出后取消设置 Layered Window(分层窗口)标志,是无法恢复 OpenGL 性能的。虽然不能百分百保证,但几乎可以肯定这一点已经被验证过。

我们曾尝试这样做:先启用分层窗口来实现透明淡出效果,之后再取消这个标志,结果发现OpenGL 性能并没有恢复,仍然显著下降。也就是说,光是取消 Layered 标志,不足以让窗口恢复到未设置之前的高性能状态

不过,或许可以尝试一些更复杂的做法,比如:

  • 取消 Layered 标志后,销毁当前的 OpenGL 上下文(context),再创建一个新的上下文
  • 或者,在取消 Layered 标志之后再延迟初始化 OpenGL 上下文;
  • 又或者,在淡出阶段使用透明渲染,但在恢复阶段完全关闭透明效果、重新初始化上下文,避免性能受损。

这些方案理论上有可能奏效,但我们尚未验证,因为之前测试的目的只是发现问题并避免,而不是深入研究如何修复或绕过。

简而言之:

  • 直接取消 Layered 标志并不能恢复性能
  • 如果性能恢复非常重要,可以考虑彻底销毁并重建 OpenGL 上下文
  • 我们并未对这类复杂操作做过彻底测试,有兴趣的人可以深入研究验证可行性
  • 对我们而言,既然 Layered 标志导致严重性能下降,我们就避免使用它,特别是在高性能图形渲染中。

提问:为什么我用 Alt+PrintScreen 可以捕获 HDCP 内容,但手动截图却不行?我有很多通过截图工具捕到 PS3 播放器画面的图

我们发现,是否能通过截图方式捕获 HDCP(High-bandwidth Digital Content Protection)保护的内容,完全取决于内容本身是否启用了保护机制,而非操作系统本身是否允许。

例如,我们确实曾经通过 Alt + Print Screen 成功截过一些播放设备(如某些视频播放器)画面中的内容,即使这些内容看起来应该受到保护。但这并不意味着所有受 HDCP 保护的内容都可以如此截取。真正受到 HDCP 严格保护的内容,在尝试截图或屏幕采集时,操作系统或者硬件会直接阻止你获取画面帧,要么得到的是一片黑屏,要么是空白内容,甚至截图文件中根本没有任何相关像素信息。

这背后其实与 HDCP 规范的实现细节 有关。比如:

  • 某些设备或操作系统层面的截图操作,可能并不会走到真正的 GPU 合成管线,所以规避了保护机制;
  • 某些平台对普通游戏画面或 UI 并不启用 HDCP,只有在播放实际受保护的媒体(如蓝光、加密视频流)时才会启动 HDCP;
  • PS3 这类设备也只有在播放受保护内容时才启用 HDCP,如果只是普通游戏画面,HDCP 不会自动开启。

所以,我们必须明确几点:

  1. 不是所有内容都启用了 HDCP,只有真正需要保护的媒体内容才会开启;
  2. 是否能截图成功,完全取决于具体情境中的 HDCP 是否处于激活状态
  3. HDCP 的目的是防止未经授权的复制,而操作系统和硬件平台也有责任确保这一点,所以在完全激活的情况下,截图行为将被系统底层封锁;
  4. 某些截图方式之所以“有效”,只是因为当时捕获的并不是实际受保护的数据帧。

因此,即使曾经成功截图,也不能说明 HDCP 失效了,而只是当时的内容不在保护范围内。对于真正受保护的视频帧,截图行为很可能直接被系统拦截,无法获得有效图像。

提问:这次都在做启动和关闭时的淡入淡出,那如果是游戏中场景切换的淡出,是否就不应该用 Windows API 实现?

在游戏中进行场景之间的淡入淡出时,实际上并不需要依赖Windows API。这是因为淡入淡出的处理完全可以通过游戏内的渲染系统来完成,而不必依赖外部系统。具体来说,淡入淡出黑色屏幕是一种常见的方法,它的实现非常简单,只需要通过调节透明度的值(通常是20)来达到逐渐从黑色过渡到游戏场景的效果。

这种方法不涉及操作系统层面的处理,完全可以在游戏渲染的过程中控制。当需要实现淡入或淡出效果时,只需要修改渲染引擎中相应的参数,无需关心操作系统的具体实现。

如果我们需要在不同的游戏场景之间进行平滑的过渡,主要的工作是通过调节渲染的透明度来实现这一目标。例如,当从一个场景切换到另一个场景时,可以让屏幕的透明度逐渐变为完全透明或完全不透明,从而实现视觉上的平滑过渡。

game_cutscene.h 中添加 tFadeInlayered_scene

在游戏优化的渲染系统中,颜色调制是一种常见的技术。当我们进行快速的渲染操作时,渲染的过程中会使用颜色调制来改变显示内容的外观。例如,在渲染过程中,可以通过传入一个颜色参数来调节正在绘制的纹理或位图。这个颜色参数会影响图像的每个像素,将它们按比例调节成特定颜色。

如果我们想要实现淡出到黑色的效果,其实非常简单。只需调节颜色调制参数,就能让某个场景的内容逐渐过渡到黑色。例如,在一个过场动画中,若需要让画面从黑色逐渐显现出来,可以通过这种方式来实现。

具体操作是,通过设置一个参数,比如“fade from black”,来控制该场景的过渡效果。然后,可以进一步细化这个操作,比如设置“fade in”效果,让画面逐渐从黑色过渡到正常显示的场景。通过这种方式,我们可以实现平滑的过渡效果,而无需复杂的代码,只需调节相应的颜色值即可完成。

在这里插入图片描述

game_cutscene.cpp 中实现不同过场镜头间的淡出黑屏效果

在创建分层场景时,我们可以通过设置每个场景的淡入淡出效果来实现细致的过渡。例如,在某个分层场景的渲染过程中,如果希望某一场景的淡入效果在进度的10%处就完成,可以通过设置相应的“淡入时间”来实现。这样,其他场景则保持默认设置,不进行任何变化。

具体来说,在渲染时,系统会遍历每个场景,检查是否有设置了特定的分层场景。如果需要渲染一个特定的分层场景,我们会计算当前的透明度值(Alpha值),以及该场景的全局淡入值。默认情况下,场景的淡入值为1,这表示场景完全可见。然后,我们根据当前的时间进度,调整该场景的淡入值。

如果当前的进度(tNormal)小于场景设置的淡入时间(tFadeIn),那么就会根据进度计算出新的场景淡入值。这是通过一个映射函数来完成的,该函数将当前时间进度(从0到1)映射到淡入的值上。这个过程已经在其他地方实现过了,具体是通过一个函数将时间值映射到一个范围内,以此来得到当前的淡入值。

然后,将得到的淡入值应用到场景的颜色中。注意,这里的颜色调整并不涉及透明度(Alpha值),而是通过将颜色调制为黑色来实现淡入效果。如果将RGB值都设为0,那么颜色就是黑色;如果将其设置为小于1的值,则实现的是从黑色到正常色彩的过渡。因此,通过调整这个值,可以实现从黑色到最终颜色的平滑过渡。

最后,每次渲染一个分层场景时,只需要将计算得到的颜色(根据当前的淡入值)应用到位图上即可。这样,渲染出来的场景就会根据设置的淡入值实现从黑色逐渐过渡到最终颜色的效果。
在这里插入图片描述

运行游戏并查看淡出效果

在游戏中,如果想要实现从黑色淡出然后再淡入的效果,可以通过控制淡入淡出的进度值来调整速度。比如,我们可以通过一个从0到1的值来控制这个过渡的进度。默认情况下,可能会设置一个较快的淡入速度,例如0.1的进度值。如果希望淡入的速度更慢,可以将这个值调整得更小,甚至设定一个较长的时间,例如10秒,来完成淡入效果。

这种方式让我们可以精确控制淡入淡出的速度,首先实现从黑色渐变出图像,然后再通过调整进度值,逐渐将场景恢复到正常状态。这是一个非常简单且有效的方法。

然而,这种做法可能会有一些问题。

注意:Alpha 值必须进行 gamma 校正

在处理淡入淡出效果时,可能会遇到一个问题,那就是透明度(Alpha)空间的处理不正确。为了确保效果的准确性,我们需要确保透明度值是经过伽马校正的,即在适当的颜色空间中进行操作。否则,渲染的结果可能不符合预期,表现为过渡过程中的渐变不平滑,甚至出现不符合预期的效果。

在渲染过程中,颜色和透明度通常是在线性空间中进行处理的,而图像的纹理本身通常是以sRGB(标准RGB)格式加载的。因此,如果在这些不同的颜色空间之间没有适当地进行转换,可能会导致最终的颜色调制出现问题,尤其是在淡入淡出的过渡过程中。

在这里,有一个重要的步骤是将sRGB颜色空间的纹理转换为线性颜色空间,这样做的目的是为了确保图像的亮度计算是正确的。如果这个转换步骤没有正确执行,淡入淡出效果就会看起来不自然,表现为一个非线性的渐变,远离我们期望的线性过渡。

经过检查,虽然在加载纹理时进行了适当的解码,并将其转换为线性空间,但实际的淡入淡出过程可能没有正确应用伽马校正,导致了渲染出来的效果显得不对劲。通过对照gamma曲线的正常行为,可以发现过渡效果实际上是以非线性的方式变化的,这表明可能存在伽马校正的问题。

因此,下一步的任务是检查伽马校正的实现是否正确,确保在进行颜色调制和透明度处理时,所有的操作都发生在正确的颜色空间中,避免出现不正确的渲染效果。这是一个值得进一步检查的问题,可能需要在接下来的开发过程中予以修正。