|
GameRes游资网授权发布 文 / 燕良@游戏开发
炉石传说的游戏内容的非常丰富多彩,所以我花了一些时间分析了其程序集,将一些设计思路记录下来,与大家分析。
做这些分析的主要目的是:
- 看看炉石如何组织游戏逻辑,以支撑复杂的技能逻辑、表现等;
- 看看炉石是如何使用Unity的,其结构设计和技巧上有什么值得学习的地方;
- 向暴雪的程序员好好学习一下英语。
下面我们就正式开始。我习惯先分析一下游戏的启动流程,这中间就涉及到了游戏基础数据的管理&初始化,各种管理器级别的类,以及相互引用关系也会初步显现。
首先看一下我找到的一些游戏启动过程相关的类,下面是他们的类图:
接下来我们分析一下游戏启动的操作流程。
- ApplicationMgr对象应该绑定到了一个场景对象中,这个场景应该在游戏启动中加载;
- ApplicationMgr:Awake()被Unity引擎自动调用;
- 调用ApplicationMgr:Initialize(),在这个函数中顺序调用了以下成员函数来进行初始化:
- InitializeMode():设置模式为ApplicationMode.PUBLIC;
- InitializeUnity():设置了UnityEngine. Application的一些属性;
- InitializeGame():看来核心的内容在这里,初始化Network,GameMgr等;
- InitializeWindowTitle();
- InitializeOptionValues();
游戏启动应该不只这点东西。由于我们纯靠程序集的动态分析,无法知道它的场景编辑、对象的脚本绑定,也不能跟踪调试,所以只能靠猜测了。另外,一些事件是通过网络消息触发的,这也给静态分析带来了一些难度。
OK,我们继续。我注意到了class Login,它从Scene派生。查看了一下Scene的派生类还有不少,我猜测每个派生类,作为特定Scene逻辑处理的脚本。而Login应该是在第0个场景中被激活运行。我们看一下Login:Start(),这属于MonoBehavior自动调用,他主要做了这样几件事:
- 注册了一些资源版本检测、Login相关的网络消息回调;
- 通知SceneMgr场景加载完成;
- 调用成员函数:AutoLogin();此函数调用Network.AutoLogin();
- 从配置文件中找到User Name,然后调用ConnectAPI.AutoLogin()——奇怪的是发现这个函数只是简单的返回false,并没有进行实际的操作。
我们在来看一下Login:Update(),这个也是属于被自动调用的脚本函数。在这里它检测了Login的状态,并调用了成员函数LoginOk(),而它有主要调用了AssetsVersionCheckCompleted(),这个函数内容很丰富:
- 通知其他模块,已经登录成功,包括:BaseUI、 InactivePlayerKicker、HealthyGamingMgr、GameMgr;
- 调用一些模块的Initialize函数,包括:DefLoader、CollectionManager、AdventureProgressMgr、Tournament、StoreManager等;
- 我们前面看到了Login从Scene派生,并且还有一个SceneMgr类。我们可以断定游戏根据不同的逻辑划分成了一些scene,接下来我们就探索一下Scene切换的流程。还是从Login入手。
以下流程都是在Login类中完成,下面描述的过程都是Login成员函数的调用:
- 首先我找到了Login:OnNetCacheReadyStep2()函数,这个应该是login流程中某一步的网络消息回调函数;
- 它会调用 WaitForAchievesThenInit()这个Coroutine函数,这个函数检测了是否需要播放视频,然后调用ReconnectOrChangeMode();
- 此函数处理重新连接,一般的话应该是调用了ChangMode();
- ChangMode()处理了新手教程相关的启动逻辑,一般的话会调用ChangeMode_Hub();
- 这个地方貌似是调用了一个技能特效,特效播放完成之后调用回调函数:OnStartupHubSpellFinished();
- 此函数调用ShowUnAckedRewardsAndQuests();
- 这里面主要是调用了HandleUnAckedRewardsAndCompletedQuests(),哦~,这应该是游戏启动的时候显示当前任务还有未领取的奖励的那个界面;
- 它会调用ShowNextUnAckedRewardOrCompletedQuest();
- 其中主要调用了ShowWelcomeQuests();
- 当幸亏显示的任务为0时,则调用了这一句:SceneMgr.Get().SetNextMode(SceneMgr.Mode.HUB),这是关键的一步了;
接下来就我们跳转到SceneMgr类中,继续探索Scene切换流程的实现。以下都是指的SceneMgr的成员函数:
- SetNextMode()函数主要就是把“m_nextMode”成员变量设置为了指定值;
- 接下来看一下Update(),这个函数主要是检测了是否需要切换Mode,然后调用了:
- SwitchMode():这个是一个Coroutine,它主要是调用了LoadModeFromModeSwitch(),它的核心也是调用LoadMode();
- 直接调用LoadMode()
- LoadMode()函数,主要是根据当前的Mode,调用LoadScene();
- LoadScene()函数主要是调用了: Application.LoadLevelAdditiveAsync(this.sceneName);
- 这样就完成了场景的切换。
OK。通过以上分析,我们大体了解了游戏启动过程是这样的:
- 进行账户验证;
- 账户验证完毕之后,显示未领取的奖励和任务;
- 然后切换到SceneMgr.Mode.HUB模式,即加载了相应的Scene。
Scene管理
接下来我们主要分析一下炉石这款游戏中一共有哪些Scene,他们各自负责什么,以及它内部的逻辑、UI的处理方式。
在正式开始之前,我来对前文中提到的Scene切换再做一些补充分析。前文中我们看到SceneMgr是调用了“ Application.LoadLevelAdditiveAsync(this.sceneName);”,那内存中的东西岂不是越搞越多吗?我们再仔细看一下SceneMgr:SwitchMode()函数,它是一个Coroutine,他主要进行了下面这几个步骤的操作:
1.调用当前Scene的Scene:PreUnload()函数
- 发送FireScenePreUnloadEvent事件;
- 等待直到Unload过程走完(通过检测LoadingScreen的阶段);
2.调用Scene:Unload()函数
- 发送FireScenePreUnloadEvent事件;
- 调用成员函数PostUnloadCleanup()函数,它调用了两个关键的函数:
- 首先是成员函数:DestroyAllObjectsOnModeSwitch(),这个函数查找到所有的GameObject(Object.FindObjectsOfType(typeof(GameObject))),然后进行了筛选(通过成员函数ShouldDestroyOnModeSwitch),除了一些全局对象之外(主要是SceneMgr、PegUI、Box、DefLoader),全都删除了(通过调用Object.DestroyImmediate())。
- 然后调用了:Resources.UnloadUnusedAssets();
- 然后是调用前文提到过的成员函数:LoadModeFromModeSwitch(),进行了LoadLevelAdditiveAsync()操作;
3.综上所述,炉石的Scene切换主要是包含两步:1删除所有非全局对象,卸载未引用的Asset;2加载新的Scene。(我倒是想到另外一个土鳖一点的替代方案:创建一个完全空的scene,调用LoadLevel加载它,那么所有没有设置"DontDestroyOnLoad"的对象就都被删除了。)
除了前文提到的Login,我们可以看到Scene还有很多派生类,详见下图:
这是我猜测的这些类和游戏内容的对应关系,没有太仔细分析,可能有些对应是错误的:
下面我们就挑选一个简单的Scene来分析一下它的内部运作机制,我们来看一下AdventureScene吧。Adventure相关的Class很多,我们只做一个粗略的分析,只涉及到下面这几个类和接口:
首先我推测,在Hub屏幕中点击中间的【Solo Adventure】(冒险模式)按钮之后,通过我们前文分析的LoadScene流程,加载了一个冒险模式相关的Scene。它里面有一个GameObject绑定了“AdventureScene”这个脚本,我们可以看到AdventureScene:Awake()方法中主要是注册了很多事件的回调。
我们可以看到有一个“AdventrueSubScenes”枚举,它基本上对应了下图中的按钮:
- public enum AdventureSubScenes
- {
- Chooser,
- Practice,
- MissionDeckPicker,
- NormalHeroic,
- ClassChallenge
- }
复制代码
接下来还有一个"AdventureSubScene"是处理子场景对应的一些逻辑的。
我们看到有“AdventureChooserTray”这个类:
我推测这个类就是用来处理上面这个游戏画面的UI交互操作的;
这个类在Awake时,通过调用“CreateAdventureChooserButton()”方法创建了上图中的上部分那几个冒险游戏内容模式相关的按钮;
这些按钮都绑定了事件回调:AdventureChooserTray.ButtonModeSelected();当这些按钮被点击时,主要是调用:
- AdventureConfig:SetSelectedAdventureMode(),此函数修改内部数据之后触发事件:FireSelectedModeChangeEvent()
- AdventureChooserTray通过OnSelectedModeChange()响应此事件,这也就是点击上面那几个按钮之后要做的一些处理:包括更新左侧的画面、设置Choose按钮状态等等;
- 其中调用了PlayMakerFSM,主要是向其发送事件“Burst”;通过这里,我们可以确定炉石使用了PlayerMaker插件。
AdventureScene也通过OnSelectedModeChanged()相应了此事件;
它里面还有一个“PlayButton m_ChooseButton”成员变量,并把为它添加了EventListener,用来调用ChangeSubScene()方法。这就和游戏实际的操作对应上来:在上面选择模式,然后点击下面的【Choose】按钮,就进行到下一步的选择了。
AdventureChooserTray:ChangeSubScene()通过Coroutine的方式调用了AdventureConfig:ChangeSubSceneToSelectedAdventure(),然后调用了AdventureConfig:ChangeSubScene();它主要触发两个事件:
- FireSubSceneChangeEvent:AdventureScene通过OnSubSceneChange()函数响应它,主要是调用AdventureScene:LoadSubScene(),内部主要是调用AssetLoader.LoadUIScreen();
- FireAdventureModeChangeEvent:AdventureScene通过OnAdventureModeChanged()响应它。
通过上面的分析,我们大致了解了上面这个游戏截图中的操作实现逻辑。
这次的分析算是一次热身,接下来重点分析有两个方面:
- 游戏逻辑的组织,特别是技能的数据、逻辑组织;这可能需要经过多次尝试,慢慢接近;
- 游戏的Asset资源管理、加载机制;
OK,今天的分析就到这里,欢迎大家拍砖。后续分析敬请期待!
顺便来秀一下我的鱼人部队,别看这些1点学的小东西,加在一起还蛮欢乐的!
相关阅读:游戏框架设计Ⅰ—— 游戏中的事件机制
|
|