GameFramework Demo StarForce详细解析(1) 阅读我的解析之前,强烈建议先阅读木头大佬的博客博客地址 ,以对GameFramework的基本组件有一定了解。 本篇的目的,是挨个傻瓜式的分析E大的代码,来了解E大的架构思想。(傻瓜式:指由傻瓜来分析。)
流程的入口 ProcedureLaunch 流程 (Procedure)是贯穿游戏运行时整个生命周期的有限状态机。而ProcedureLaunch是整个流程的主入口(也就是Entrance Procedure的设置),所以我们从ProcedureLaunch来入手这个Demo。
ProcedureBase 可以通过继承ProcedureBase实现自定义的流程。 ProcedureBase继承自有限状态机状态基类FsmState。 会依序调用OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy函数。
UseNativeDialog StarForce的ProcedureBase还增加了一个抽象bool值UseNativeDialog,用来获取流程是否使用原生对话框。
1 2 3 4 5 6 7 8 9 public abstract class ProcedureBase : GameFramework.Procedure.ProcedureBase { public abstract bool UseNativeDialog { get ; } }
让我们看看UseNativeDialog调用的地方。(静态类UIExtension中的拓展函数)
1 2 3 4 5 6 7 8 9 10 11 public static void OpenDialog (this UIComponent uiComponent, DialogParams dialogParams ) { if (((ProcedureBase)GameEntry.Procedure.CurrentProcedure).UseNativeDialog) { OpenNativeDialog(dialogParams); } else { uiComponent.OpenUIForm(UIFormId.DialogForm, dialogParams); } }
拓展了UIComponent的OpenDialog函数,如果当前的Procedure的UseNativeDialog为true的话,会调用OpenNativeDialog方法,反之,会调用UIComponent的OpenUIForm方法。 再去看看用OpenNativeDialog方法。
1 2 3 4 5 6 7 8 private static void OpenNativeDialog (DialogParams dialogParams ) { if (dialogParams.OnClickConfirm != null ) { dialogParams.OnClickConfirm(dialogParams.UserData); } }
好的,e大还没做。(这不是尴尬了不是。)所以目前,UseNativeDialog为true的话,会直接调用确认按钮的回调事件。 实际使用: 如:ProcedureCheckVersion流程中,如果需要强制更新游戏应用,会打开一个原生对话框,然后执行GotoUpdateApp函数,进行更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (m_VersionInfo.ForceUpdateGame) { GameEntry.UI.OpenDialog(new DialogParams { Mode = 2 , Title = GameEntry.Localization.GetString("ForceUpdate.Title" ), Message = GameEntry.Localization.GetString("ForceUpdate.Message" ), ConfirmText = GameEntry.Localization.GetString("ForceUpdate.UpdateButton" ), OnClickConfirm = GotoUpdateApp, CancelText = GameEntry.Localization.GetString("ForceUpdate.QuitButton" ), OnClickCancel = delegate (object userData) { UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); }, }); return ; }
没想到一个小小的bool参数就写了这么多,e大的框架真是深不可测啊!(明明是你自己废话多!) 好的,那我们按照ProcedureLaunch的流程函数继续研究吧。
OnEnter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected override void OnEnter (ProcedureOwner procedureOwner ) { base .OnEnter(procedureOwner); GameEntry.BuiltinData.InitBuildInfo(); InitLanguageSettings(); InitCurrentVariant(); InitSoundSettings(); GameEntry.BuiltinData.InitDefaultDictionary(); }
GameEntry.BuiltinData.InitBuildInfo(); OnEnter中先是调用了Customs下Builtin Data挂载的BuiltinDataComponent(:GameFrameworkComponent(:MonoBehaviour))中的InitBuildInfo函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void InitBuildInfo ( ) { if (m_BuildInfoTextAsset == null || string .IsNullOrEmpty(m_BuildInfoTextAsset.text)) { Log.Info("Build info can not be found or empty." ); return ; } m_BuildInfo = Utility.Json.ToObject<BuildInfo>(m_BuildInfoTextAsset.text); if (m_BuildInfo == null ) { Log.Warning("Parse build info failure." ); return ; } }
利用GF的Utility将本地的BuildInfo.txt序列化为对应的类,存储在BuiltinDataComponent中。 (后文会将命名空间简化GameFrameWork = GF;GameFrameWorkUnity = GFU;GameFrameWorkEditor = GFE) 值得一提的是,GameFramework中很多操作都会利用到对应的Helper类。所以如果想自定义相应的操作,可以构建自己的Helper类并实现接口即可。 如: Utility.Json.ToObject 将 JSON 字符串反序列化为对象的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static T ToObject <T >(string json ) { if (s_JsonHelper == null ) { throw new GameFrameworkException("JSON helper is invalid." ); } try { return s_JsonHelper.ToObject<T>(json); } catch (Exception exception) { if (exception is GameFrameworkException) { throw ; } throw new GameFrameworkException(Text.Format("Can not convert to object with exception '{0}'." , exception), exception); } }
UGF的BaseComponent会在Awake对时游戏框架组件进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void InitJsonHelper ( ){ if (string .IsNullOrEmpty(m_JsonHelperTypeName)) { return ; } Type jsonHelperType = Utility.Assembly.GetType(m_JsonHelperTypeName); if (jsonHelperType == null ) { Log.Error("Can not find JSON helper type '{0}'." , m_JsonHelperTypeName); return ; } Utility.Json.IJsonHelper jsonHelper = (Utility.Json.IJsonHelper)Activator.CreateInstance(jsonHelperType); if (jsonHelper == null ) { Log.Error("Can not create JSON helper instance '{0}'." , m_JsonHelperTypeName); return ; } Utility.Json.SetJsonHelper(jsonHelper); }
使用反射,利用m_JsonHelperTypeName创建对应的jsonHelper,利用SetJsonHelper设置到Json中。 自定义的jsonHelper需要实现IJsonHelper接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 namespace GameFramework { public static partial class Utility { public static partial class Json { public interface IJsonHelper { string ToJson (object obj ) ; T ToObject <T >(string json ) ; object ToObject (Type objectType, string json ) ; } } } }
(天,一个函数就写了这么多…….开始担心有没有人愿意看了。)
InitLanguageSettings(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private void InitLanguageSettings ( ) { if (GameEntry.Base.EditorResourceMode && GameEntry.Base.EditorLanguage != Language.Unspecified) { return ; } Language language = GameEntry.Localization.Language; if (GameEntry.Setting.HasSetting(Constant.Setting.Language)) { try { string languageString = GameEntry.Setting.GetString(Constant.Setting.Language); language = (Language)Enum.Parse(typeof (Language), languageString); } catch { } } if (language != Language.English && language != Language.ChineseSimplified && language != Language.ChineseTraditional && language != Language.Korean) { language = Language.English; GameEntry.Setting.SetString(Constant.Setting.Language, language.ToString()); GameEntry.Setting.Save(); } GameEntry.Localization.Language = language; Log.Info("Init language settings complete, current language is '{0}'." , language.ToString()); }
通过GameEntry.Setting.HasSetting(Constant.Setting.Language)检查是否存在指定游戏配置项。 类似于IJsonHelper,读取游戏设置也有对应的辅助器,PlayerPrefsSettingHelper继承自SettingHelperBase实现了ISettingHelper接口。 本例的PlayerPrefsSettingHelper直接调用的PlayerPrefs.HasKey(settingName)检查是否存在指定配置项。 本例中若是暂不支持的语言,则使用英语,并调用GameEntry.Setting.SetString设置对应键值对;然后再调用Save函数,保存所有修改的参数。(PlayerPrefs.Save()默认在Unity退出时调用) 最后将Localization组件的Language设置为我们设置的Language。
InitCurrentVariant 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private void InitCurrentVariant ( ) { if (GameEntry.Base.EditorResourceMode) { return ; } string currentVariant = null ; switch (GameEntry.Localization.Language) { case Language.English: currentVariant = "en-us" ; break ; case Language.ChineseSimplified: currentVariant = "zh-cn" ; break ; case Language.ChineseTraditional: currentVariant = "zh-tw" ; break ; case Language.Korean: currentVariant = "ko-kr" ; break ; default : currentVariant = "zh-cn" ; break ; } GameEntry.Resource.SetCurrentVariant(currentVariant); Log.Info("Init current variant complete." ); }
InitCurrentVariant会在非编辑器资源模式下,通过不同的Language,调用Resource组件的SetCurrentVariant()来设置当前变体。 设置的变体信息,会在ResourceManager的OnLoadPackageVersionListSuccess回调中派上用场。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void OnLoadPackageVersionListSuccess (string fileUri, byte [] bytes, float duration, object userData ) { …… foreach (PackageVersionList.ResourceGroup resourceGroup in resourceGroups) { ResourceGroup group = m_ResourceManager.GetOrAddResourceGroup(resourceGroup.Name); int [] resourceIndexes = resourceGroup.GetResourceIndexes(); foreach (int resourceIndex in resourceIndexes) { PackageVersionList.Resource resource = resources[resourceIndex]; if (resource.Variant != null && resource.Variant != m_CurrentVariant) { continue ; } group .AddResource(new ResourceName(resource.Name, resource.Variant, resource.Extension), resource.Length, resource.Length); } } …… }
当某个资源设置了Variant,加载资源时只会将对应资源的变体加入ResourceGroup,来实现不同语言的变体功能。
InitSoundSettings 1 2 3 4 5 6 7 8 9 10 11 private void InitSoundSettings ( ) { GameEntry.Sound.Mute("Music" , GameEntry.Setting.GetBool(Constant.Setting.MusicMuted, false )); GameEntry.Sound.SetVolume("Music" , GameEntry.Setting.GetFloat(Constant.Setting.MusicVolume, 0.3f )); GameEntry.Sound.Mute("Sound" , GameEntry.Setting.GetBool(Constant.Setting.SoundMuted, false )); GameEntry.Sound.SetVolume("Sound" , GameEntry.Setting.GetFloat(Constant.Setting.SoundVolume, 1f )); GameEntry.Sound.Mute("UISound" , GameEntry.Setting.GetBool(Constant.Setting.UISoundMuted, false )); GameEntry.Sound.SetVolume("UISound" , GameEntry.Setting.GetFloat(Constant.Setting.UISoundVolume, 1f )); Log.Info("Init sound settings complete." ); }
GameEntry.Sound.Mute是静态类SoundExtension的一个拓展函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void Mute (this SoundComponent soundComponent, string soundGroupName, bool mute ){ if (string .IsNullOrEmpty(soundGroupName)) { Log.Warning("Sound group is invalid." ); return ; } ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); if (soundGroup == null ) { Log.Warning("Sound group '{0}' is invalid." , soundGroupName); return ; } soundGroup.Mute = mute; GameEntry.Setting.SetBool(Utility.Text.Format(Constant.Setting.SoundGroupMuted, soundGroupName), mute); GameEntry.Setting.Save(); }
通过soundGroupName找到对应的soundGroup并设置他是否静音(bool值:Mute) SoundGroup (: ISoundGroup)在Mute变换时,会遍历存储的所有SoundAgent(声音代理)来更新他们的Mute的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public bool Mute { get { return m_Mute; } set { m_Mute = value ; foreach (SoundAgent soundAgent in m_SoundAgents) { soundAgent.RefreshMute(); } } }
GameEntry.Sound.SetVolume与Mute也是同理,一个是setBool,另一个则是setFloat,大同小异就不赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void SetVolume (this SoundComponent soundComponent, string soundGroupName, float volume ) { if (string .IsNullOrEmpty(soundGroupName)) { Log.Warning("Sound group is invalid." ); return ; } ISoundGroup soundGroup = soundComponent.GetSoundGroup(soundGroupName); if (soundGroup == null ) { Log.Warning("Sound group '{0}' is invalid." , soundGroupName); return ; } soundGroup.Volume = volume; GameEntry.Setting.SetFloat(Utility.Text.Format(Constant.Setting.SoundGroupVolume, soundGroupName), volume); GameEntry.Setting.Save(); }
OnUpdate 接下来我们来到OnUpdate函数。(写完OnEnter血条已经空了大半,GF框架果然牛逼。我们现在先初步的理解,后面再做深入的研究。)
1 2 3 4 5 6 7 protected override void OnUpdate (ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds ){ base .OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); ChangeState<ProcedureSplash>(procedureOwner); }
还好procedureLaunch的OnUpdate比较简单,直接调用ChangeState(procedureOwner)切换到下一个流程。 ChangeState是切换状态机的通用函数,比较简单就不过多分析了。 大概就是从状态机字典m_States取出对应状态机,执行当前状态机的OnLeave函数,将m_CurrentStateTime设置为0,m_CurrentState设置为当前状态机,并执行新的状态机的OnEnter函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void ChangeState <TState >(IFsm<T> fsm ) where TState : FsmState<T> { Fsm<T> fsmImplement = (Fsm<T>)fsm; if (fsmImplement == null ) { throw new GameFrameworkException("FSM is invalid." ); } fsmImplement.ChangeState<TState>(); }
顺带一提OnUpdate的三个参数分别是:procedureOwner流程持有者,可以用来传递一些参数;elapseSeconds 逻辑流逝时间;realElapseSeconds 真实流逝时间。
写博客真的是一个体力活啊,累死个人。 对StarForce的解读,只是本人才疏学浅的见解,难避免会有错误或者偏差。欢迎大家的指教和讨论。 我们下篇见。