按需安装游戏

本技术文章介绍使用 Windows Installer 的两种方法:按需安装和后台安装。 游戏可以利用这些安装方法缩短安装时间,从而为玩家提供更优越、更愉快的游戏体验。

概述

长期以来,安装一直是基于计算机的应用程序的一个要素。 目前,大多数应用程序需要先安装在用户的本地硬盘驱动器上,然后才能使用。 游戏也不例外;当消费者购买 Microsoft Windows 游戏后尝试运行游戏时,必须先完成将必要文件从游戏光盘复制到硬盘驱动器的安装过程。 此安装过程通常花费很长时间,可能需要一个小时才能完成。 对于某些玩家来说,安装时间是使主机游戏比电脑游戏更受欢迎的一个因素,因为主机游戏可以在插入游戏光盘后立即进入。本文中介绍的方法将尝试通过大幅减少安装时间来解决此问题。

一直以来,游戏启动之前需要安装全部或大部分文件。 为了实现按需安装,游戏资源需要模块化;也就是说,开发人员必须将应用程序的资源(图形、音频等)划分为组件。 每个组件都是一组可以作为一个单元安装或删除的资源。 完成此操作后,游戏开发人员将定义一个或多个功能,通常是每个级别或区域的一个或多个功能。 应用程序的每个功能都指定运行该特定功能所需的一组组件。 安装应用程序时,其功能可以标记为“安装”(在安装时将组件复制到本地硬盘驱动器)或“已播发”(应用程序使用该功能时,在初始安装后将组件复制到本地硬盘驱动器)。 游戏开发人员可以通过将游戏设计为在安装最少的功能集的情况下启动和运行来减少安装时间。 当应用程序实际需要使用这些功能提供的功能时,其余功能可以标记为“已播发”并按需安装。

游戏可以调用 Windows Installer 来安装可能尚未安装的特定功能。 若要使安装显示在后台,可以使用工作线程调用安装程序,而主线程继续处理游戏逻辑并呈现屏幕。 这样可以最大限度地减少安装导致的游戏中断。 游戏可以随时启动安装。 但是,由于安装会消耗处理器周期,因此当主线程急需处理能力时(例如当用户正在进行操作时),通常不建议执行安装。 例如,执行安装的最佳时机可能是在用户处于游戏菜单中、游戏暂停或最小化时、或者用户正在观看开场动画或过场动画时。

修补支持

目前,大部分游戏即使在发布后也需要更新,以便修复 Bug 及增加新功能。 更新通常需要修补,这对于游戏来说按惯例是一个简单的过程。 由于所有必要的文件都安装在用户的硬盘驱动器上,因此修补游戏涉及将修订的文件复制到硬盘驱动器上,覆盖现有文件。 采用按需安装时,并非所有文件在修补时都会安装和复制。 因此,修补程序无法简单地将更新的文件写入游戏文件夹。

Windows Installer 具有修补采用按需安装的应用程序的功能。 应用修补时,安装程序会将修补缓存到系统上。 此功能适用于增量较小的修补。 修补时,最初发布的文件不再需要位于磁盘上,因此可以播发这些文件。 之后,当应用程序运行并需要访问这些文件时,安装程序会通过从介质(例如 CD)复制最初发布的版本并在读取保存的修补数据后应用修补来安装这些文件的最新版本。

InstallOnDemand SDK 示例

按需安装游戏示例演示了本文中介绍的按需安装方法。 与其他示例不同,按需安装游戏无法直接从示例浏览器运行。 由于该示例使用 Windows Installer 来管理其安装,因此需要包含在安装程序的已安装应用程序数据库中。

启动示例

  1. 使用示例浏览器中的“安装项目”链接将示例的文件复制到文件夹中。
  2. 双击 InstallOnDemand.msi 安装示例。
  3. 选择“典型安装”。
  4. 通过在已安装的文件夹(通常是 Program Files\InstallOnDemand)中启动 InstallOnDemand.exe 或从“开始”菜单\“程序”启动来启动示例。

InstallOnDemand.msi 是安装程序可识别的数据库。 它定义整个安装过程:目录结构、将复制的内容和不会复制的内容、将同时复制哪些资源、要写入的注册表值、要创建的快捷方式等。

启动后,该示例将播放开场动画。 玩家可以通过按 ESC 键结束播放并进入主菜单。 开场动画后,玩家可以通过输入角色名称并滚动浏览统计信息来开始新游戏。 在示例开始播放开场动画之前,示例将调用安装程序函数来检查是否已安装关卡 1 的功能。 如果尚未安装关卡 1 功能,示例将使用后台线程要求安装程序安装游戏,而主线程正在执行其他操作(例如播放开场动画、呈现菜单或与玩家在创建角色时交互)。 这种体验与传统的游戏安装不同,用户在安装过程中已进入游戏(观看开场动画或创建新角色)。 玩家完成创建角色后,示例将加载关卡 1 的资源。

示例屏幕右侧有五个按钮,标记为“进入关卡 1”到“进入关卡 5”。这些按钮模拟玩家通过当前关卡并升级到下一个关卡。 单击其中一个按钮时,将出现一个统计信息屏幕,其中显示玩家刚刚通过的关卡的信息。 示例还需要利用这段时间要求安装程序检查并安装下一个关卡(如果尚未安装)。 安装是在玩家阅读统计信息屏幕时进行的,因此当用户单击“确定”进入下一关卡时,此关卡的资源已全部安装并准备好加载。

示例的功能和组件

一直以来,游戏启动之前需要安装全部或大部分文件。 为了实现按需安装,游戏资源需要模块化;也就是说,开发人员必须将应用程序的资源(图形、音频等)划分为组件。 每个组件都是一组可以作为一个单元安装或删除的资源。 完成此操作后,游戏开发人员将定义一个或多个功能,通常是每个级别或区域的一个或多个功能。 应用程序的每个功能都指定运行该特定功能所需的一组组件。 安装应用程序时,其功能可以标记为“安装”(在安装时将组件复制到本地硬盘驱动器)或“已播发”(应用程序稍后使用该功能时,将组件复制到本地硬盘驱动器)。 游戏开发人员可以通过将游戏设计为在安装最少的功能集的情况下启动和开始运行来减少安装时间。 当应用程序实际需要使用这些功能提供的功能时,其余功能可以标记为“已播发”并按需安装。

下表列出了示例定义的六个顶级功能。

功能名称 功能 组件 文件
核心 包括任何时候所需的资源,不考虑关卡。 这些资源包括:示例可执行文件、开场动画和加载屏幕所需的媒体,以及处理示例中所有呈现的 .fx 文件。 核心 InstallOnDemand.exe、InstallOnDemand.fx、Loading.bmp、Level.x
核心 (同上) CoreUI Media\UI\dxutcontrols.dds、Media\UI\DXUTShared.fx、Media\UI\arrow.x
核心 (同上) CoreMisc Media\Misc\seafloor.x、Media\Misc\seafloor.bmp
核心 (同上) CoreSpeeder Media\PRT Demo\LandShark.x、Media\PRT Demo\speeder_diff.jpg
核心 (同上) CoreReg N/A(注册表值)
Level1 提供关卡 1 使用的资源。 Level1 Level1.jpg
Level1 (同上) L1Skybox Media\Light Probes\galileo_cross.dds
Level2 提供关卡 2 使用的资源。 Level2 Level2.jpg
Level2 (同上) L2Skybox Media\Light Probes\grace_cross.dds
Level3 提供关卡 3 使用的资源。 Level3 Level3.jpg
Level3 (同上) L3Skybox Media\Light Probes\rnl_cross.dds
级别 4 提供关卡 4 使用的资源。 级别 4 Level4.jpg
级别 4 (同上) L4Skybox Media\Light Probes\stpeters_cross.dds
Level5 提供关卡 5 使用的资源。 Level5 Level5.jpg
Level5 (同上) L5Skybox Media\Light Probes\uffizi_cross.dds

 

关卡 1 到关卡 5 功能具有其他子功能,其中包含示例未直接使用的文件。 添加这些子功能文件后将延长安装时间。 这样做是为了演示示例运行时在后台运行的正在进行的安装操作。

下表列出了子功能。

功能 子功能 Files
Level1 L1PH1、L1PH2、L1PH3、L1PH4、L1PH5 Level1 占位符数据\L1PH1.dat Level1 占位符数据\L1PH2.dat Level1 占位符数据\L1PH3.dat Level1 占位符数据\L1PH4.dat Level1 占位符数据\L1PH5.dat
Level2 L2PH1、L2PH2、L2PH3、L2PH4、L2PH5 Level2 占位符数据\L2PH1.dat Level2 占位符数据\L2PH2.dat Level2 占位符数据\L2PH3.dat Level2 占位符数据\L2PH4.dat Level2 占位符数据\L2PH5.dat
Level3 L3PH1、L3PH2、L3PH3、L3PH4、L3PH5 Level3 占位符数据\L3PH1.dat Level3 占位符数据\L3PH2.dat Level3 占位符数据\L3PH3.dat Level3 占位符数据\L3PH4.dat Level3 占位符数据\L3PH5.dat
级别 4 L4PH1、L4PH2、L4PH3、L4PH4、L4PH5 Level4 占位符数据\L4PH1.dat Level4 占位符数据\L4PH2.dat Level4 占位符数据\L4PH3.dat Level4 占位符数据\L4PH4.dat Level4 占位符数据\L4PH5.dat
Level5 L5PH1、L5PH2、L5PH3、L5PH4、L5PH5 Level5 占位符数据\L5PH1.dat Level5 占位符数据\L5PH2.dat Level5 占位符数据\L5PH3.dat Level5 占位符数据\L5PH4.dat Level5 占位符数据\L5PH5.dat

 

在安装期间,核心功能应标记为“安装”,所有其他功能应标记为“已播发”。通过仅安装一项而不是六项功能,玩家必须等待游戏启动的时间就会大幅缩短。

安装

Windows Installer 为应用程序提供了一种请求安装播发功能的机制。 然而,该机制是同步应用程序编程接口 (API) 调用,这意味着应用程序必须在调用内等待,直到安装完成。 若要实现后台安装,需要工作线程,以便主应用程序线程可以自由执行其他重要任务,例如在屏幕上呈现以继续向玩家提供视觉反馈。

在示例中,该示例执行期间有三种安装状态:主动安装、被动安装和不安装。

  • 主动安装是示例在需要访问或加载一个或多个功能提供的资源时发起的请求。 在安装资源之前,示例无法继续执行该操作。
  • 当示例未执行关键任务(例如玩家在菜单中或观看过场动画时)时,将启动被动安装。 在这种情况下,如果示例的任何功能仍播发,工作线程将执行检查。 如果找到安装程序,将调用该安装程序来安装功能。 此过程将重复,直到安装完示例的每个功能。 实质上,被动安装利用额外的处理器周期在后台执行安装,对主示例干扰最小。
  • 当玩家积极参与游戏时,不会发生安装;这可以防止帧率下降,以免干扰用户体验。

在示例中,定义了一个 CMsiUtil 类来处理所有与安装相关的任务。 实质上,CMsiUtil 使用一个工作线程,该线程调用安装程序以在循环中安装示例的功能。 该类有两个用于存储安装请求的队列:一个用于主动安装的高优先级队列,一个用于被动安装的低优先级队列。 在初始化期间,该类将枚举产品的所有功能,并将其添加到被动安装队列。 由于整个产品以这种方式排队,因此如果示例有足够的可用处理器周期,最终将安装整个产品。

当示例需要请求主动安装时,示例可以调用 CMsiUtil::UseFeatureSet(),并传递顶级功能的名称。 UseFeatureSet() 会将请求的功能及其所有子功能排入主动安装队列,以便工作线程执行安装。

执行安装请求时,工作线程将检查主动安装队列和被动安装队列,以查看任一队列是否有任何其他请求。 每次线程发现请求时,都会调用安装程序 API 来执行实际安装。 两个队列都为空后,工作线程将进入睡眠状态,并调用 WaitForSingleObject。 由于整个产品在初始化期间放置在被动安装队列中,因此队列为空表示已安装整个产品。

此示例调用 CMsiUtil::EnablePassiveInstall() 启用或禁用被动安装。 EnablePassiveInstall(true) 增加被动安装的启用计数,EnablePassiveInstall(false) 减少被动安装的启用计数。 如果启用计数大于 0,则类将处理被动安装队列。 如果出现以下任一情况,示例允许被动安装:

  • 用户正在观看初始开场动画。
  • 用户正在示例菜单中导航。
  • 用户正在查看关卡末尾的统计信息。
  • 示例应用程序会失去焦点并转到后台。

下面列出了 CMsiUtil 的方法:

方法 说明
AbortAllRequests 导致当前安装中止并清空主动安装请求队列。
AbortCurrentRequest 导致正在进行的安装中止。 然后,如果队列中存在下一个请求,工作线程将处理该请求。
EnablePassiveInstall 递增或递减被动安装启用计数。 示例使用此调用来控制何时可以进行被动安装以及何时不能进行被动安装。
GetCurrentFeatureName 返回正在主动安装的功能的名称。
GetFeatureProgress 返回正在安装的功能的当前时钟周期位置。
GetFeatureProgressMax 返回正在安装的功能的最大进度时钟周期数。
GetLastError 使用此方法从以前的安装请求中检索返回代码。
GetPassiveProgress 返回被动安装的进度栏时钟周期位置。
GetPassiveProgressMax 返回当前时钟周期位置以及被动安装的最大时钟周期数。 示例可以使用它们一起显示被动安装的总体进度。
GetProgress 返回主动功能集安装的进度栏时钟周期位置。 当示例呈现安装进度栏时,将使用此方法。 由于 Windows Installer 仅提供正在安装的一项功能的进度信息,因此该方法将进度栏按请求的功能划分,以便用户仍将整个安装视为一项任务。
GetProgressMax 返回主动功能集安装的最大进度栏时钟周期计数。 当示例呈现安装进度栏时,将使用此方法。
初始化 使用产品全局唯一标识符 (GUID) 初始化类。 此方法还枚举已播发但尚未安装的应用程序的每个功能,并将其置于被动安装队列中以设置被动安装。
IsInstallInProgress 使用此方法可确定是否正在处理主动安装。
UseFeature UseFeatureSet 调用的私有方法。 检查是否已安装功能。 如果已安装请求的功能,该方法将返回。 如果尚未安装此功能(已播发),该方法会将新的主动安装请求排入工作线程,然后返回。 在请求的安装完成时将发出信号的可选事件句柄。
UseFeatureSet 当示例需要访问特定功能或其任何子功能提供的功能时,示例将调用此方法。 该方法枚举示例的所有功能,并调用 UseFeature() 以获取指定根特征的子功能。 示例可以传入一个事件句柄,该句柄将在安装完整个功能集时发出信号。 由于所有功能都作为一个集安装,因此句柄是为 UseFeature() 排队的最后一个功能指定的,而不是为每个功能指定的,因此在安装所有请求的功能后,示例会收到一次通知。
UseProduct 将主动安装请求排入工作线程,以调用安装程序执行完整的产品安装。 在请求的安装完成时将发出信号的可选事件句柄。

 

限制

当前版本的安装程序不是为同时访问多个线程而设计的。 因此,当工作线程调用安装程序时,主线程不应调用安装程序。 当主线程请求某个功能,然后在工作线程完成安装之前再次请求相同的功能时,示例中会出现此限制的示例。 第二个请求调用 MsiQueryFeatureState() 以确定请求的功能是否已安装,因为安装程序有时可能会在工作线程仍在复制文件时指示功能已完全安装。

幸运的是,有一种简单的解决方法。 CMsiUtil 将在调用 MsiQueryFeatureState() 或 MsiUseFeature() 等函数来询问相关功能的安装状态之前,检查工作线程是否正在安装某个功能。 请注意,此限制也可能成为其他地方的问题。

修补可能会影响按需安装在最终用户计算机上的运行效果。 应用仅包含从以前版本更改的数据的修补可能需要安装更新文件的以前版本才能应用增量。 在这种情况下,修补必须在将修补应用到游戏之前请求安装程序安装受影响的播发功能。

示例是通过启动 InstallOnDemand.msi 来安装的,因为它假定计算机上存在 Windows Installer。 如果安装程序不存在,则无法识别 .msi 文件并且无法启动。 若要解决此问题,应用程序应使用安装程序来执行任务。 此程序应首先检查,以查看安装程序是否存在,如果存在,则查看其版本。 如果版本不符合应用程序的要求,安装程序应安装 Windows Installer,然后启动 .msi 文件。 此过程称为引导。 应用程序通常将其引导安装程序命名为 Setup.exe。 示例不处理引导。 但是,可以在 Windows Installer 中找到有关引导的完整详细信息。

开发人员还应注意游戏中的每个功能的大小。 取消正在进行的安装时,安装程序会将计算机还原到安装前的状态。 这意味着功能安装已完全撤消,并且不存在安装部分功能。 较大功能需要较长的安装时间,这会增加安装被中断和取消的可能性,或者安装会干扰主应用程序。 例如,当用户在游戏过程中调出游戏菜单时启用被动安装。 如果正在安装某个功能并且用户返回游戏中,则游戏可以执行以下其中一种操作:完成被动安装,或取消被动安装。 任何一个模型都不太适合较大的功能。 如果游戏完成大型安装,安装可能会长时间阻碍游戏的呈现性能。 相反,如果游戏取消安装,则用户必须在菜单中停留很长时间才能返回到游戏。 开发人员应找到最适合其个人游戏的平衡功能大小。