前言
听过游戏数值策划岗位的,都知道他们是为了游戏数值平衡和各种公式设计,以及整个系统平衡。策划可不必懂编程,但配置文件都是靠策划修改的,游戏需要高度配置的(为了热更新、程序便于维护和分工明确),思考如果把数值写在代码中,其实会好傻的,接下来学习下GF的配置模块做了些什么?
1 默认配置辅助器
看过GF架构思路文章的童靴,应该知道学习GF模块时,先从默认辅助器入手,毕竟模块管理器持有模块辅助器实现功能的,模块组件持有模块管理器去实际的调用(阿勒,这么又说一遍了…),所以直接学习下DefaultConfigHelper,以后配置模块文章会考虑实现xlsx、csv、json辅助器(个人偏向于csv),默认配置辅助器是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、回车表示数据分割,同行数据尽量不要使用回车分割,这样看起来会更累。
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>
解决问题的思路差不多就这样了,具体的实现方案将在以后文章完成~