程序执行的入口函数(主函数)

int WINAPI WinMain(_In_ HINSTANCE hInstance, Inopt_ HINSTANCE hPrevInstance, In LPSTR lpCmdLine, In int nCmdShow);---44E8F0

即为PVZ的主函数,程序的运行都在此函数内

函数先通过gLawnApp = new LawnApp(); 创建gLawnApp[6A9EC0],其中LawnApp::LawnApp(LawnApp * this)---44EAA0是LawnApp的构造函数,new LawnApp时在汇编代码上会执行此函数,即设置gLawnApp的成员的数值,包括但不限于mProdName=PopCap\\PlantsVsZombies (产品名称)
aTitleName = "Plants vs. Zombies"(窗口标题)
mGameScene = GameScenes::SCENE_LOADING;(游戏场景)

设置工作文件夹

gLawnApp->mChangeDirTo记录工作文件夹的相对路径,工作文件夹即为程序读取资源的文件夹,所以将所有资源挪至exe上一目录也可正常运行程序

下面是检索过程:

检索exe同一目录内properties\resources.xml的存在情况

若存在,则工作文件夹为exe所在目录

若不存在则查找上一目录

在上一目录存在,则工作文件夹为exe上一目录

若上一目录也不存在,则设置为exe所在目录(会导致后续游戏出现Resource file not found: properties\resources.xml弹窗)

游戏初始化

设置完工作文件夹后会进行游戏的初始化,使用的是函数LawnApp::Init(ecx = LawnApp * this)---451880

内部包括:

首先通过Sexy::SexyAppBase::DoParseCmdLine(ecx = SexyAppBase * this)---552D70获取命令行字符串并从中提取出包含所有命令行参数的字符串交由自身的 ParseCmdLine 函数处理。这一步骤通常用于根据用户在启动游戏时传递的参数来设置游戏的不同运行模式或选项。

然后初始化

  • mSessionID +87C//确保会话唯一性,值为1970年1月1日午夜(UTC)以来的秒数。

  • mPlayTimeActiveSession +880//设置为0,用处暂时未知,疑似一直为零

  • mPlayTimeInactiveSession +884//同+880

  • mBoardResult +888//记录BoardResult枚举值(即战斗状态),初始化为BoardResult::BOARDRESULT_NONE

  • mSawYeti +[bool]88C//是否见过雪人僵尸,初始化为false

另附enum BoardResult

enum BoardResult
{
    BOARDRESULT_NONE = 0,//未进入战斗,或战斗正常
    BOARDRESULT_WON = 1,//赢得战斗
    BOARDRESULT_LOST = 2,//输掉战斗
    BOARDRESULT_RESTART = 3,//重启战斗
    BOARDRESULT_QUIT = 4,//退出战斗
    BOARDRESULT_QUIT_APP = 5,//退出程序
    BOARDRESULT_CHEAT = 6//使用作弊
};

使用函数Sexy::SexyApp::Init(ecx = SexyApp * this)---5D54E0对应用程序初始化,负责初始化SexyApp类的各个组件和变量,并输出一些调试信息。包括但不限于报错弹窗的文字

An unexpected error has occured! Pressing 'Send Report' will send us helpful debugging information that may help us resolve this issue in the future.You can also contact us directly at feedback@popcap.com.

此外,大部分初始化信息在Sexy::SexyAppBase::Init(ecx = SexyAppBase * this)---553A70函数内,并在Sexy::SexyApp::Init内部会调用。

SexyAppBase::Init内部会进行以下初始化

设置mPrimaryThreadId(主线程ID)与DirectX库加载检测。

使用Sexy::SexyApp::InitPropertiesHook(ecx = SexyApp * this)---5D5010初始化属性,加载 partner.xml 并从中获取程序名、窗口标题、是否默认窗口化等部分信息。同时,对程序自身的网络广告管理器和内测支持系统进行初始化。 通过Sexy::SexyAppBase::ReadFromRegistry(ecx = SexyAppBase * this)---54A470读取注册表,获取所有基础设定数据(音量、窗口模式、窗口坐标等)

检查操作系统版本是否为Windows Vista,如果是,则获取公共应用程序数据路径(CSIDL_COMMON_APPDATA),并设置应用程序数据文件夹路径。如果mDemoFileName不是绝对路径,则将其设置为相对于应用程序数据文件夹的路径。解析命令行参数,如果命令行参数尚未解析,则通过Sexy::SexyAppBase::DoParseCmdLine(ecx = SexyAppBase * this)---552D70方法解析命令行参数,以便根据命令行选项进行相应的配置。

通过Sexy::SexyAppBase::IsScreenSaver(eax = SexyAppBase * this)---5456C0判断是否为屏保,如果当前应用程序作为屏幕保护程序运行,则将mOnlyAllowOneCopyToRun设置为false,允许同时运行多个实例。

获取模块句柄如果gHInstance为NULL,则调用GetModuleHandle(NULL)获取当前模块的句柄,并将其赋值给gHInstance。gHInstance通常用于后续的资源加载和窗口创建。调用LawnApp::ChangeDirHook(const char* theIntendedPath, ecx = LawnApp * this)---4522A0尝试更改工作目录。如果ChangeDirHook失败,则使用chdir函数更改工作目录。

调用PakInterface::AddPakFile(ecx = const std::string& theFileName)---5D7D90向资源包接口中添加一个新的资源包文件,会立即加载指定资源包内的所有文件。;使用资源包接口提供的相关函数可以像读写正常文件一样读写所有已添加的资源包内的文件。出现字符串Notify和Mute,疑似注册一个唯一的窗口消息,用于进程间通信。并创建一个全局唯一的互斥锁,用于确保只有一个实例的程序在运行。

通过Sexy::SRand(ecx = unsigned long theSeed)---5AF420来初始化随机数生成器(gMTRand 数组),其中theSeed与为KERNEL32.GetTikCount()的结果。设

置演示缓冲区,注册窗口类,如果操作系统是Windows 98/Me,则使用WNDCLASSA和RegisterClassA注册窗口类;否则,使用WNDCLASS和RegisterClass注册窗口类。

注册窗口类后,使用CreateWindowExA或CreateWindowEx创建一个不可见的窗口mInvisHWnd,并将其用户数据设置为当前SexyAppBase实例的指针。创建两个自定义光标:mHandCursor和mDraggingCursor。这些光标用于在游戏的不同状态下显示不同的鼠标光标。

  • mHandCursor: 用于表示手指光标,在用户点击某些交互元素时显示。

  • mDraggingCursor: 用于表示拖动光标,在用户拖动某些元素时显示。

调用LawnApp::PreDisplayHook(ecx = LawnApp * this)---4531D0方法,允许子类在显示窗口之前执行一些自定义操作(程序初始化结束、切换全屏模式并显示窗口之前的钩子)调用Sexy::WidgetManager::Resize; (edx = const Rect& theMouseSourceRect, ecx = const Rect& theMouseDestRect, eax = WidgetManager * this)---538D20方法,调整窗口管理器的大小以适应当前窗口的尺寸。mWidgetManager管理游戏中的各种界面元素和控件。检查窗口大小和分辨率,如果应用程序设置为窗口模式(mIsWindowed为true),但不是全屏窗口模式(mFullScreenWindow为false),则检查窗口尺寸是否大于或等于屏幕全屏尺寸。如果是,则将窗口模式设置为全屏模式(mIsWindowed设置为false,mForceFullscreen设置为true),以防止窗口尺寸过大导致显示问题,并以此设置分辨率。

设置完成后通过Sexy::SexyAppBase::MakeWindow(ecx = SexyAppBase * this)---5509E0创建程序窗口,并进行相关的初始化,设置声音和音乐管理器。并根据是否为屏保处理光标,初始化钩子。

SexyAppBase::Init初始化结束后会通过Sexy::ResourceManager::ParseResourcesFile(ecx = const std::string& theFilename, ResourceManager * this)---5B6A20创建 XML 解析器,解析指定文件的大框架,确认是资源描述文件后,调用具体的解析过程。;当文件不以 ResourceManifest 标签开头时,也会尝试进行解析,但是会先产生一个错误,如果这一步失败会出现错误窗口。通过TodResourceManager::TodLoadResources(eax = const std::string& theGroup, ecx = TodResourceManager * this)---513140加载Init组资源。加载玩家数据,通过Sexy::SexyAppBase::RegistryReadString; (esi = std::string theString, ecx = const std::string& theValueName, edx = SexyAppBase this)---54A360从注册表中读取当前用户信息,并通过 ProfileMgr::GetProfile(ecx = const std::string& theName, edi = ProfileMgr * this)---46B290 获取相应的玩家信息。如果仍然没有找到玩家信息,则通过 ProfileMgr::GetAnyProfile(esi = ProfileMgr * this)---46A7C0 获取首个节点信息。通过Sexy::SexyAppBase::GetInteger(int theDefault, ecx = const std::string& theId, eax = SexyAppBase * this)---5528B0来程序的整数属性容器中寻找 theId 对应的整数值以此设定:

  • MaxExecutions

  • MaxPlays

  • MaxTime

创建一个 TitleScreen 对象,并将其大小调整为窗口的宽度和高度,将标题屏幕添加到 mWidgetManager 中,并设置焦点到这个标题屏幕。这确保用户在启动游戏时看到的是游戏的标题屏幕。创建mSoundSystem(音效)、mMusic(音乐)和mEffectSystem(特效)系统对象(后面会出对应文章专门记录),并对特效系统进行初始化,确保游戏的音效、音乐和效果系统在启动时正确初始化,以便后续使用。创建多个 TypingCheck 对象,用于检查玩家是否输入了特定的作弊码。通过ReanimatorLoadDefinitions()---473750将 gLawnReanimationArray 指定为全局动画参数数组(后面会出对应文章专门记录),并根据动画数量新建相应大小的动画定义数组。再通过ReanimatorEnsureDefinitionLoaded(bool theIsPreloading, ReanimationType theReanimType)---4735E0来确保theReanimType 动画的定义数据已加载完成。

至此初始化结束(期间有很多加载用到了性能计时器)

游戏的开始

游戏初始化结束后会用函数LawnApp::Start(ecx = LawnApp * this)---4522B0顺便检测下资源是否加载成功,失败则退出程序。

成功则进入定义:Sexy::SexyAppBase::Start(ecx = SexyAppBase * this)---5522C0显示程序窗口并开始程序的主循环。

启动独立的线程处理光标动画或状态更新(不详细赘述),并启动资源加载线程(后面会出对应文章专门记录),显示窗口并聚焦,并对高精度计时器和计时变量进行初始化,进行主循环执行(后面会出对应文章专门记录)安全删除列表处理,即清理需要延迟释放的对象(如跨线程资源),阻塞主线程,直到资源加载线程完成,进行性能统计并输出,结束高精度计时器,行关闭前的自定义清理操作(如释放资源、保存状态)。将当前配置(如窗口位置、音量设置)保存到注册表。

最后关闭游戏

若游戏退出会步入函数LawnApp::Shutdown(ecx = LawnApp * this)---44F200从而退出程序(若处于关卡内则尝试保存用户存档和关卡存档。删除程序内各个系统和对象,具体如下:

检测资源加载线程,若为完成,直接标记为失败,确保关闭。遍历所有对话框类型(如设置、存档、错误弹窗),调用 KillDialog(i) 关闭并释放它们。mBoardResult标记为BoardResult::BOARDRESULT_QUIT_APP。尝试保存关卡存档(后面会出对应文章专门记录)。包括创建存档文件、暂停背景音乐、写入存档文件、保存生存模式记录等事件。安全删除列表处理,即清理需要延迟释放的对象(如跨线程资源)。释放池效果对象(如粒子效果池),先销毁附件数据数组中的所有附件并回收其占用的内存区域,再删除对象。释放禅境花园模块(ZenGarden)及其关联资源(植物、装饰物数据)。释放全局特效系统(如粒子效果、动画控制器),确保先销毁附件数据数组中的所有附件并回收其占用的内存区域。释放动画(Reanimation)缓存,避免动画资源内存泄漏。通过以下函数清理所有静态或全局资源:

  • FilterEffectDisposeForApp()---446F00//删除并释放每种滤镜效果作用下的、原贴图向滤镜贴图的映射容器,以及容器中的滤镜贴图

  • TodParticleFreeDefinitions()---515E30//根据粒子系统定义结构图释放粒子系统定义数组,清除全局粒子系统参数数组指针。

  • ReanimatorFreeDefinitions()---473870//释放动画定义数组,清除全局动画参数数组指针。

  • TrailFreeDefinitions()---51BAB0//根据轨迹定义结构图释放轨迹定义数组,清除全局轨迹参数数组指针。

  • FreeGlobalAllocators()---513600//释放所有全局内存申请器中所有已申请的内存区块。

(以上后面均会出对应文章专门记录)

更新注册表信息,调用Sexy::SexyAppBase::ShutDown(ecx = SexyAppBase * this)---54B810关闭程序,删除程序窗口

具体为:

检测当前线程是否主线程(通过 mPrimaryThreadId 判断),标记加载失败并直接返回。标记关闭状态,并通过钩子函数允许子类实现自定义关闭逻辑,通过轮询 mCursorThreadRunning 标志,等待光标线程退出,停止所有正在播放的音乐和音效。如果应用以全屏模式运行(mIsPhysWindowed为false),调用 DirectDraw 接口恢复原始显示模式。隐藏应用窗口,如果是全屏模式,恢复屏幕分辨率。如果应用配置(如窗口位置、音量)需要从注册表读取(mReadFromRegistry为true),则将当前设置写入注册表。释放 JPEG2000 图像解码库的资源,此函数结束。

然后检测gLawnApp是否存在,若存在则delete gLawnApp;然后return 0;关闭程序。程序彻底结束。