游戏开发论坛

 找回密码
 立即注册
搜索
查看: 5602|回复: 0

《炉石传说》架构设计分析:Gameplay与Asset管理

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2016-3-2 16:05:57 | 显示全部楼层 |阅读模式
U1782P115DT20131109230557.jpg

  GameRes游资网授权发布 文 / 燕良@游戏开发

  经过前面的分析,我们已经找到了两个关键的类Gameplay和GameState(当然还有我最感兴趣的Spell和SpellController,这两个还要在后面分析)。

  首先我们看一下Gameplay这个类的Awake方法,它完成的主要工作是:

  1、调用“ GameState.Initialize()”创建一个新的GameState实例;

  2、注册CreateGame事件:在Gameplay.OnCreateGame()中响应,主要是

  初始化卡背;(本地玩家和远程玩家的卡背ID都通过Player类来读取);

  启动一个Coroutine:NotifyPlayersOfBoardLoad,它做的主要工作是

  • 等待BoardStandardGame对象加载完成;
  • 然后调用所有Player的OnBoardLoaded(),它的主要工作是初始化法力水晶相关的管理逻辑;

  3、使用AssetLoader加载AttackSpellController、SecretSpellController、TurnStartManager等;

  这些类看上去都很重要,我们后续分析游戏逻辑时肯定用得到。

  接下来我们要看一下Gameplay.Start()方法,它主要是注册了一些自己关心的网络消息,然后调用

  Network.StartCountdown()——发送网络消息“BeginPlaying”;

  Network.GetGameState()——发送网络消息“GetGameState”;

  我们在看一下Gameplay.Update(),里面似乎正常情况只是调用GameState.Update()。

  OK,以上就是从MonoBehavior继承来的三个被自动调用的函数。对于游戏逻辑来说,还是没有什么头绪。

  再往下分析,遇到的一个最大的困难是很多操作应该是通过网络交互完成的,例如【认输】操作,分析它是从GameMenu.ConcedeButtonPressed()开始的,一直调用到ConnectAPI.Concede()向服务器发了一个GiveUp消息,但是无法确定它对应的服务器返回消息是什么。

  接下来我们先分析一下游戏的回合的流转,还是先看一下相关的类图:

1.jpg

  回合结束是由玩家点击右侧的【End Trun】操作来触发的,其对应的代码为:InputManager.DoEndTurnButton(),这个函数的逻辑有些费解,目前只能是猜测如下:

  首先判断当前是否允许访问GameState的OptionsPacket,以及EndTurnButton是否可以操作;

  然后根据GameState.GetResponseMode()来分两种情况处理:

  GameState.ResponseMode.OPTION——初步猜测为游戏回合中的正常操作:

  • 从GameState中取出所有的Network.Options,然后遍历,找到“OptionType.END_TURN”或者OptionType.PASS的对象,然后调用GameState.SetSelectedOption(i);GameState.SendOption();
  • GameState.ResponseMode.CHOICE——初步猜测为游戏回合开始时,选择初始手牌的相关操作;
  • 服务器端的行为就比较难以猜测了,只能等到客户端行为分析比较完整时再说了。

  服务器端相关的返回大致是这样的,在Gameplay的Start中有这样一句:

  1. network.RegisterNetHandler(Network.PacketID.ALL_OPTIONS, new Network.NetHandler(this.OnAllOptions));
复制代码

  想象中客户端使用Gameplay.OnAllOptions()处理网络层接收到的所有玩家操作,此函数主要是将Network.GetOptions()取出的数据发送到GameState.OnAllOptions()去处理,后者主要会触发事件GameState.FireOptionsReceivedEvent()。

  我们通过对应的GameState.RegisterOptionsReceivedListener()成员函数,可以分析一下哪些对象会响应此事件。找到EndTurnButton.OnOptionsReceived()。

  这阶段的分析难度越来越大了,这次分析算是有小小的收获,但是整个回合流转的流程还没有清晰。总结如下:

  • 玩家的操作是在InputManager中处理的,重点的成员函数包括DoNetworkResponse()、DoEndTurnButton();
  • 玩家的操作和网络发送来的操作都保存在GameState.m_options中;

  其中有另外一个Entity类体系也需要进一步分析。

  Asset管理

  话说,经过这段时间的学习和摸索,对于Unity3D的开发思路已经基本清晰了。唯独还剩下一个AssetBundle机制还没有搞透,这个涉及到前期项目的资源规划、资源管理代码的写法,以及自动更新机制的实现。

  所以,还是想先把游戏逻辑的进一步分析押后,先来看一下《炉石传说》Asset管理。必须得说一下的是,目前分析都是PC版的程序集,对于移动端不一定完全合适,且当做一个案例分析吧。

  本文主要讲述《炉石传说》的AssetBundle的管理机制。它的机制比较简单清晰,中规中矩,中间的分析过程就不讲了,直接展现其架构设计和代码逻辑组织。先从Asset管理相关的类讲起。

  class Asset :资源信息描述

2.jpg

  Asset类,并不管理直接的资源对象,而是保存的一个Asset相关的信息,具体请看上图。

  另外,它还有一个“paths”变量,这是一个Dictionary,key是AssetFamily枚举,value是Assetbundle的路径和资源路径。下面的AssetFamily一节详细解释。

  enum AssetFamily - 资源分类

3.png

  如上图所示:

  炉石根据资源的不同类型进行分别的AssetBundle打包,一类资源对应一个或者多个资源包;(一类资源分多个包的规则不得而知);

  有的资源包真的本地化单独打包,例如“fonts0.unity3d”==》“fontszhCN0.unity3d”;

  在程序中,资源包的分类对应枚举类型“AssetFamily”;

  资源包的具体路径信息,存储在Asset.paths,这是一个静态变量;在初始化时,手动填写必要的信息,类似这样:

  1. Dictionary<AssetFamily, AssetFamilyPathInfo> dictionary = new Dictionary<AssetFamily, AssetFamilyPathInfo>();  
  2. AssetFamilyPathInfo info = new AssetFamilyPathInfo {  
  3.     format = "Data/Actors/{0}.unity3d",  
  4.     sourceDir = "Assets/Game/Actors"  
  5. };  
  6. info.exts = new string[] { "prefab" };  
  7. dictionary.Add(AssetFamily.Actor, info);  
复制代码

  另外,还有一个class AssetBundleInfo是记录了每种AssetBundle对应的主文件名,以及包文件的个数、对应的对象类型等信息;详见下图:

4.png

  class AssetLoader :资源加载

5.png

  游戏运行时需要加载各种资源,基本上都是通过AssetLoader(也有个别情况适用了Resources.Load())。接下来我们就重点看一下AssetLoader的实现思路。

  AssetLoader对上层提供资源对象加载接口,对于每种类型的资源都提供一组函数,例如LoadCardPrefab,LoadActor等等。对于对象加载完成、加载进度等提供回调函数。这些函数只是一些简单的包装,其内部都调用到LoadCachedGameObject()或LoadCachedObject()这两个核心函数。

  从这两个函数的流程可以看到,资源加载使用到了Cache机制:

  首先从AssetCache中查找,如果找到了,则更新Cache项的时间戳,并调用回调;

  如果没有找到,则向AssetCache添加一个Request,然后启动Coroutine:CreateCachedAsset(),它的调用步骤是:

  • 调用AssetCache.StartLoading();
  • 启动Coroutine:CreateCachedAsset_FromBundle<RequestType>():
  • 使用AssetLoader.GetBundleForAsset()找到资源所属的AssetBundle;
  • 调用AssetBundle.LoadAsync()来真正加载资源;
  • 在加载的过程中,根据处理的结果调用:AssetCache.CacheRequest的OnLoadFailed()、OnLoadSucceeded()、OnProgressUpdate()等函数;
  • 在AssetCache查找此资源,如果找到了,则加载成功,调用回调函数;
  • 调用AssetCache.StopLoading();

  我们都知道在开发过程中,不能使用AssetBundle(每次启动都要打包,肯定收不了)。怀疑它的Editor模式相关的代码是用预编译宏处理来实现的,所以未出现在发布出来的程序集当中,类似这样:

  1. #if UNITY_EDITOR  
  2.         Obj = Resources.LoadAssetAtPath(assetPath, typeof(T));  
  3.         if (Obj == null)  
  4.             Debug.LogError ("Asset not found at path: " + assetPath);  
  5.         yield break;  
  6. #else  
复制代码

  class AssetCache :资源的Cache机制

  前面在AssetLoader一节我们已经讲到了AssetCache机制,这里再做一个详细的阐述。

6.png

  前面我们已经讲到:

  • AssetCache中的资源项的时间戳,由AssetLoader在资源加载请求时维护;
  • AssetCache主要负责管理Cache数据,而真正的资源加载动作还是在AssetLoader中执行;
  • AssetCache的资源淘汰主要由外部的各个模块根据自己认为需要的时机去调用,例如:
  • SceneMgr.ClearCachesAndFreeMemory()
  • LoadingScreen.ClearAssets()
  • SoundMgr.UnloadSoundBundle()
  • 等等

  另外,程序启动时会自动更新资源包(在Login.OnAssetsVersion()中启动),主要是通过UpdateManager和Downloader两个类来处理。

  OK,总结一下炉石的资源管理机制:

  对游戏资源按照类型分包,每一类资源包可以有多个;

  在游戏运行时使用Cache机制。

  相关阅读《炉石传说》架构设计分析:启动流程与Scene管理

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-5-20 05:58

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表