前言
聽過遊戲數值策劃崗位的,都知道他們是爲了遊戲數值的平衡和制定,遊戲中各種公式的設計,以及整個系統平衡的搭建。當然策劃可以不必懂編程或需要編程,但配置信息程序猿是不需要干涉的,所以遊戲需要可配置的(爲了熱更新、程序便於維護和分工明確),思考一下如果直接把數值寫死在代碼中感覺好傻的(別的不說了,策劃每次要改數值,還要告訴程序去改),程序和數值分開是很好的習慣,接下來看看GF框架的配置模塊做了些什麼?
1 默認配置輔助器
看過網站上GF架構思路文章的童靴,應該知道學習GF模塊時,可以從默認輔助器人手,畢竟模塊管理器持有模塊輔助器去實現各種功能的,然後模塊組件持有模塊管理器去實際的調用(阿勒,這麼又說一遍了…),所以直接來看看DefaultConfigHelper(自定義的配置輔助器),以後在配置擴展篇裏會考慮實現xml或json的輔助器(個人比較偏向於xml,因爲execl比json可視化軟件更加普遍和便於修改),工程下的默認配置輔助器是txt格式的,接下來先展示一下代碼:
using GameFramework;
using GameFramework.Config;
using System;
using System.IO;
using System.Text;
using UnityEngine;
namespace UnityGameFramework.Runtime
{
/// <summary>
/// 默認全局配置輔助器。
/// </summary>
public class DefaultConfigHelper : ConfigHelperBase
{
private static readonly string[] RowSplitSeparator = new string[] { "\r\n", "\r", "\n" };
private static readonly string[] ColumnSplitSeparator = new string[] { "\t" };
private const int ColumnCount = 4;
private ResourceComponent m_ResourceComponent = null;
private IConfigManager m_ConfigManager = null;
/// <summary>
/// 解析全局配置。
/// </summary>
public override bool ParseConfig(string text, object userData)
{
try
{
string[] rowTexts = text.Split(RowSplitSeparator, StringSplitOptions.None);
for (int i = 0; i < rowTexts.Length; i++)
{
if (rowTexts[i].Length <= 0 || rowTexts[i][0] == '#')
{
continue;
}
string[] splitLine = rowTexts[i].Split(ColumnSplitSeparator, StringSplitOptions.None);
if (splitLine.Length != ColumnCount)
{
Log.Warning("Can not parse config '{0}'.", text);
return false;
}
string configName = splitLine[1];
string configValue = splitLine[3];
if (!AddConfig(configName, configValue))
{
Log.Warning("Can not add raw string with config name '{0}' which may be invalid or duplicate.", configName);
return false;
}
}
return true;
}
catch (Exception exception)
{
Log.Warning("Can not parse config '{0}' with exception '{1}'.", text, exception.ToString());
return false;
}
}
/// <summary>
/// 解析全局配置。
/// </summary>
public override bool ParseConfig(byte[] bytes, object userData)
{
using (MemoryStream memoryStream = new MemoryStream(bytes, false))
{
return ParseConfig(memoryStream, userData);
}
}
/// <summary>
/// 解析全局配置。
/// </summary>
public override bool ParseConfig(Stream stream, object userData)
{
try
{
using (BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8))
{
while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
{
string configName = binaryReader.ReadString();
string configValue = binaryReader.ReadString();
if (!AddConfig(configName, configValue))
{
Log.Warning("Can not add raw string with config name '{0}' which may be invalid or duplicate.", configName);
return false;
}
}
}
return true;
}
catch (Exception exception)
{
Log.Warning("Can not parse config with exception '{0}'.", exception.ToString());
return false;
}
}
/// <summary>
/// 釋放全局配置資源。
/// </summary>
public override void ReleaseConfigAsset(object configAsset)
{
m_ResourceComponent.UnloadAsset(configAsset);
}
/// <summary>
/// 加載全局配置。
/// </summary>
protected override bool LoadConfig(string configName, object configAsset, LoadType loadType, object userData)
{
TextAsset textAsset = configAsset as TextAsset;
if (textAsset == null)
{
Log.Warning("Config asset '{0}' is invalid.", configName);
return false;
}
bool retVal = false;
switch (loadType)
{
case LoadType.Text:
retVal = m_ConfigManager.ParseConfig(textAsset.text, userData);
break;
case LoadType.Bytes:
retVal = m_ConfigManager.ParseConfig(textAsset.bytes, userData);
break;
case LoadType.Stream:
using (MemoryStream stream = new MemoryStream(textAsset.bytes, false))
{
retVal = m_ConfigManager.ParseConfig(stream, userData);
}
break;
default:
Log.Warning("Unknown load type.");
return false;
}
if (!retVal)
{
Log.Warning("Config asset '{0}' parse failure.", configName);
}
return retVal;
}
/// <summary>
/// 增加指定全局配置項。
/// </summary>
protected bool AddConfig(string configName, string configValue)
{
bool boolValue = false;
bool.TryParse(configValue, out boolValue);
int intValue = 0;
int.TryParse(configValue, out intValue);
float floatValue = 0f;
float.TryParse(configValue, out floatValue);
return AddConfig(configName, boolValue, intValue, floatValue, configValue);
}
/// <summary>
/// 增加指定全局配置項。
/// </summary>
protected bool AddConfig(string configName, bool boolValue, int intValue, float floatValue, string stringValue)
{
return m_ConfigManager.AddConfig(configName, boolValue, intValue, floatValue, stringValue);
}
private void Start()
{
m_ResourceComponent = GameEntry.GetComponent<ResourceComponent>();
if (m_ResourceComponent == null)
{
Log.Fatal("Resource component is invalid.");
return;
}
m_ConfigManager = GameFrameworkEntry.GetModule<IConfigManager>();
if (m_ConfigManager == null)
{
Log.Fatal("Config manager is invalid.");
return;
}
}
}
}
看到配置輔助器需要實現兩個函數:AddConfig(解析成功的數值保存到字典中)和LoadConfig(讀取配置文件),也就是說添加xml配置輔助器時主要重寫這兩個方法,打開工程下txt配置文件查看格式是什麼樣子的,DefaultConfig配置文件如下所示:
# 默認配置
# 配置項 策劃備註 配置值
Game.Id Star Force
Scene.Menu 1
Scene.Main 2
如果感覺自定義配置輔助器比較麻煩,想直接使用GF提供的默認配置輔助器就需要按照此格式定義txt內容,這裏空格加Tab或回車鍵表示數據的分割,只不過同行數據不要回車分割(txt沒有xml格式直觀,畢竟xml可以用execl打開),這樣看起來會很累。
2.如何使用配置模塊?
數據保存到字典應該如何取用呢?可以先看一下數據是如何保存就到字典的,就可以知道數據取用的方式(雖然有封裝好的接口),具體保存的代碼如下:
public bool AddConfig(string configName, bool boolValue, int intValue, float floatValue, string stringValue)
{
if (HasConfig(configName))
{
return false;
}
m_ConfigDatas.Add(configName, new ConfigData(boolValue, intValue, floatValue, stringValue));
return true;
}
configName是key,ConfigData(多數據組合類)是value,把讀出來的數據這樣保存感覺怪怪的,思考了一下好像確實沒有更好的解決方案,比如在配置時這個key只是表示int型的,卻多保存了其他數據,取用時還需要告訴它具體調用的函數類型,做法感覺不太智能,比如取用Int數據時需要調用這種代碼:
GameEntry.Config.GetInt("Scene.Menu")
寫框架目的就是提供給使用者大量便捷途徑,但是配置模塊用着確實不太理想(在下不是處女座的,不要誤會。en…處女座的也不要誤會),小節三來考慮一下比較好的處理方式,當然有更好的想法希望各位可以傳授給我(ありがとうございます)。
3.如何修改配置模塊?
首先需要分析問題,知道具體的敵人是誰纔可以擊敗敵人,所以第一個問題就是如何實現保存不同值類型集合的功能?第二個問題就是取用數據時如何統一接口去調用?(不需要使用取用int型時調用getint,取用string型時調用getstring這樣子)。
- 如何保存不同值類型到字典?
首先需要知道的就是使用泛型是不可以的,因爲每個數據都可能不相同(不可能t,t1,t2這樣子去搞,在下也不是這樣的人),所以只能用到拆箱裝箱的方式,也就是object,這樣就解決了第一個問題,具體實現將在進階篇裏實現,字典將調整成這樣:
Dictionary<string,dynamic>
- 如何統一接口?
需要智能識別數據類型的話,就必須在讀取數據時就確定它的類型,這個東西如何確定呢?其實仔細思考以後知道數據類型無非就是字符串(string)、數值(int,double…)、布爾(bool),應該沒有更多了。這樣我們需要制定一個規矩,比如被"“包含起來就是string類型,false或true就是布爾類型的(配置成前面類型中間_後面數值也是可以的,就是太憨憨了,比如int_111111),然後按照規定就可以確定值類型了,簡直完美哎…,但是這樣還是不行的,最後還需要保存每個數據的type取用時可以用到,經過分析後就可以知道字典的value要改成這樣子。
public class ConfigData
{
dynamic data;
Type type;
}
剛接觸編程的可能會有疑問,爲什麼不定義成結構體反而定義成了類?(結構體性能不是比類好嘛),建議少俠去百度字典的value保存成值類型還是引用類型比較好,這裏就不多廢話了,字典的value改成如下所示:
Dictionary<string,ConfigData>
解決問題的思路差不多就這樣了,具體的實現方案將在以後文章完成~