GameFramework Demo StarForce详细解析(1)

阅读我的解析之前,强烈建议先阅读木头大佬的博客博客地址,以对GameFramework的基本组件有一定了解。
本篇的目的,是挨个傻瓜式的分析E大的代码,来了解E大的架构思想。(傻瓜式:指由傻瓜来分析。)

流程的入口 ProcedureLaunch

1
流程 (Procedure)是贯穿游戏运行时整个生命周期的有限状态机。而ProcedureLaunch是整个流程的主入口(也就是Entrance Procedure的设置),所以我们从ProcedureLaunch来入手这个Demo。

2

ProcedureBase

可以通过继承ProcedureBase实现自定义的流程。
ProcedureBase继承自有限状态机状态基类FsmState。
会依序调用OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy函数。
3

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)
{
// TODO:这里应该弹出原生对话框,先简化实现为直接按确认按钮
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);

// 构建信息:发布版本时,把一些数据以 Json 的格式写入 Assets/GameMain/Configs/BuildInfo.txt,供游戏逻辑读取
GameEntry.BuiltinData.InitBuildInfo();

// 语言配置:设置当前使用的语言,如果不设置,则默认使用操作系统语言
InitLanguageSettings();

// 变体配置:根据使用的语言,通知底层加载对应的资源变体
InitCurrentVariant();

// 声音配置:根据用户配置数据,设置即将使用的声音选项
InitSoundSettings();

// 默认字典:加载默认字典文件 Assets/GameMain/Configs/DefaultDictionary.xml
// 此字典文件记录了资源更新前使用的各种语言的字符串,会随 App 一起发布,故不可更新
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)
4
值得一提的是,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
/// <summary>
/// 将 JSON 字符串反序列化为对象。
/// </summary>
/// <typeparam name="T">对象类型。</typeparam>
/// <param name="json">要反序列化的 JSON 字符串。</param>
/// <returns>反序列化后的对象。</returns>
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
{
/// <summary>
/// JSON 辅助器接口。
/// </summary>
public interface IJsonHelper
{
/// <summary>
/// 将对象序列化为 JSON 字符串。
/// </summary>
/// <param name="obj">要序列化的对象。</param>
/// <returns>序列化后的 JSON 字符串。</returns>
string ToJson(object obj);

/// <summary>
/// 将 JSON 字符串反序列化为对象。
/// </summary>
/// <typeparam name="T">对象类型。</typeparam>
/// <param name="json">要反序列化的 JSON 字符串。</param>
/// <returns>反序列化后的对象。</returns>
T ToObject<T>(string json);

/// <summary>
/// 将 JSON 字符串反序列化为对象。
/// </summary>
/// <param name="objectType">对象类型。</param>
/// <param name="json">要反序列化的 JSON 字符串。</param>
/// <returns>反序列化后的对象。</returns>
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)
{
// 编辑器资源模式直接使用 Inspector 上设置的语言
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)
{
// 编辑器资源模式不使用 AssetBundle,也就没有变体了
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
/// <summary>
/// 获取或设置声音组静音。
/// </summary>
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);

// 运行一帧即切换到 Splash 展示流程
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
/// <summary>
/// 切换当前有限状态机状态。
/// </summary>
/// <typeparam name="TState">要切换到的有限状态机状态类型。</typeparam>
/// <param name="fsm">有限状态机引用。</param>
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的解读,只是本人才疏学浅的见解,难避免会有错误或者偏差。欢迎大家的指教和讨论。
我们下篇见。