設(shè)計(jì)一個(gè)可靠的配置表可重載系統(tǒng)
前言
在制作一款游戲中籍嘹,有大量的配置表碾褂。比如游戲中有裝備的配置表法褥,它定義了裝備的icon,名稱会烙,屬性等等负懦。有關(guān)卡配置表,配置了關(guān)卡信息柏腻,怪物信息纸厉,怪物數(shù)據(jù)等等。對(duì)于一款實(shí)時(shí)在線游戲五嫂,我們需要這些配置表可以被熱更新颗品,可以快速fix一些配置錯(cuò)誤,而無需停機(jī)維護(hù)游戲沃缘。那如何做到可靠有效的熱更新呢躯枢?正是這篇文章要介紹給大家的。
一槐臀、理解配置表
不同的游戲配置表的實(shí)際形式也是多種多樣的锄蹂,有的配置表采用Excel
,有的配置表采用csv文件
峰档,有的直接使用數(shù)據(jù)庫表
败匹。不管是那種形式,游戲的配置表不是那種單純配置參數(shù)的key讥巡,value配置文件掀亩。它是結(jié)構(gòu)化的!不管它的載體是什么欢顷,我把它當(dāng)做一個(gè)數(shù)據(jù)表
去維護(hù)槽棍,表與表之間可以有關(guān)聯(lián)關(guān)系。
對(duì)于一張實(shí)體配置表抬驴,我新建一個(gè)對(duì)應(yīng)的Domain實(shí)體類
與之對(duì)應(yīng)炼七。這是不是像是管理數(shù)據(jù)表類似的,是的布持!我就是把配置表當(dāng)做一個(gè)數(shù)據(jù)庫表豌拙,在代碼上有一個(gè)對(duì)應(yīng)的實(shí)體類。但不同的是题暖,它是一張內(nèi)存只讀表
下面我就用裝備配置表為例來一場(chǎng)載入到安全可重載之旅按傅!
裝備Id | 裝備名稱 | 裝備的品質(zhì) | 裝備的圖像 |
---|---|---|---|
1001 | 暗影戰(zhàn)斧 | 精良 | anyingzhanfu |
1002 | 鐵劍 | 普通 | tiejian |
1003 | 破曉 | 傳說 | pojie |
備注:這里只是舉個(gè)例子,實(shí)際游戲中的裝備配置表要比這個(gè)復(fù)雜許多
對(duì)應(yīng)的Domain類
public class Equip
{
public int Id { get; set; }
public string Name { get; set; }
public string Quality { get; set; }
public string Pic { get; set; }
}
二胧卤、載入配置表
對(duì)于上面定義的配置表唯绍,我們?nèi)绻d入到內(nèi)存并且管理起來呢?我想到的是用一個(gè)Dictionary
枝誊,Key是表的主鍵况芒,Value是對(duì)應(yīng)的實(shí)體類。我定義了一個(gè)配置表的Cache管理器叶撒,它的接口定義如下:
public interface ConfigCache<K, V>
{
/// <summary>
/// 從緩存中獲取Key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
V Get(K key);
/// <summary>
/// 將對(duì)象放入緩存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
void Put(K key, V value);
/// <summary>
/// 載入所有Models
/// </summary>
/// <returns></returns>
List<V> GetModels();
}
- 你可以通過主鍵獲取指定配置
- 你可以放入指定配置到緩存表绝骚,實(shí)際的操作一般是啟動(dòng)時(shí)載入所有配置到內(nèi)存
- 獲取所有的配置信息
我們有了配置表管理類耐版,那如何載入呢?如何將配置信息載入進(jìn)管理類呢皮壁?上面我提到配置表有很多不同的載體椭更,比如Excel,數(shù)據(jù)庫表蛾魄,CSV文件等虑瀑。同樣我也設(shè)計(jì)了一個(gè)載入API,不同的載體去實(shí)現(xiàn)不同的實(shí)現(xiàn)類即可滴须,定義如下:
public interface IConfigLoader
{
/// <summary>
/// 獲取所有配置信息
/// </summary>
/// <typeparam name="V"></typeparam>
/// <returns></returns>
List<V> GetModels<V>();
}
public interface ConfigLoadable
{
/// <summary>
/// 載入緩存
/// </summary>
void Load();
/// <summary>
/// 設(shè)置配置載入器
/// </summary>
/// <param name="loader"></param>
void SetConfigLoader(IConfigLoader loader);
/// <summary>
/// 獲取配置載入器
/// </summary>
/// <returns></returns>
IConfigLoader GetConfigLoader();
}
有了載入器舌狗,你就可以在系統(tǒng)啟動(dòng)時(shí)候載入對(duì)應(yīng)配置,放入緩存管理類了扔水。那我們看看實(shí)際的EquipCache吧痛侍。
// 配置表的基類,子類只需要實(shí)現(xiàn)盡量少的代碼
public abstract class BaseConfigCache<K, V> : ConfigCache<K, V>, IConfigLoader
{
/// <summary>
/// 緩存Map
/// </summary>
protected readonly Dictionary<K, V> CacheMap = new ();
/// <summary>
/// 緩存載入器
/// </summary>
protected IConfigLoader ConfigLoader;
public V Get(K key)
{
return CacheMap.TryGetValue(key, out var value) ? value : default;
}
public void Put(K key, V value)
{
CacheMap[key] = value;
}
public List<V> GetModels()
{
return CacheMap.Values.ToList();
}
public void Load()
{
CacheMap.Clear();
Init();
}
public void SetConfigLoader(IConfigLoader loader)
{
this.ConfigLoader = loader;
}
public IConfigLoader GetConfigLoader()
{
return this.ConfigLoader;
}
public abstract void Init();
}
// 實(shí)際的Equip配置表Cache
public class EquipCache : BaseConfigCache<int, Equip>
{
public override void Init()
{
var modelList = GetConfigLoader().GetModels<Equip>();
foreach(var model in modelList)
{
Put(model.Id, model);
}
}
}
看看目前我們做到了什么魔市?
- 定義了配置表的管理方式
- 定義了配置表的載入方法主届,采用接口的設(shè)計(jì),你可以根據(jù)自己的需要擴(kuò)展自己的載入器即可
- 每個(gè)配置表實(shí)際管理類只需要實(shí)現(xiàn)少量代碼
- 由于每個(gè)配置表都有一個(gè)管理器待德,你實(shí)際上擁有了自由定義配置表管理的能力君丁。比如我想裝備配置表可以按照品質(zhì)檢索。
public class EquipCache : BaseConfigCache<int, Equip>
{
public override void Init()
{
var modelList = GetConfigLoader().GetModels<Equip>();
foreach(var model in modelList)
{
Put(model.Id, model);
}
}
/// <summary>
/// 通過品質(zhì)獲取裝備配置信息
/// </summary>
public List<Equip> GetEquipByQuality(string quality)
{
return GetModels().Where(v => v.Quality == quality).ToList();
}
}
- 如果非常高頻将宪,你還可以通過在Init里面提前構(gòu)建索引表用來加速查詢
目前我們已經(jīng)做到如果載入配置表绘闷,以及怎么管理和使用它,在游戲的其他模塊使用它已經(jīng)很稱手了较坛,接下去是時(shí)候給它添加熱重載
支持了印蔗!
三、熱重載配置表
熱重載本質(zhì)是重新載入丑勤,可以是通過指定讓系統(tǒng)重新載入华嘹,或者是自動(dòng)檢測(cè)到配置表變化實(shí)現(xiàn)重新載入。對(duì)于上述的設(shè)計(jì)法竞,熱重載不就是重新調(diào)用一下Load
方法即可嗎除呵?其實(shí)是的,但當(dāng)如果只是這樣做它是不夠安全的爪喘。它有以下問題:
- 配置表的管理類的
Dictionary
不是線程安全的,如果在其他線程查詢的時(shí)候纠拔,又觸發(fā)了重新載入秉剑,這是不可預(yù)期的 - 還有就是配置表之間一般是有
關(guān)聯(lián)
關(guān)系的,重新載入的過程可能破壞這種關(guān)聯(lián)關(guān)系稠诲,導(dǎo)致依賴關(guān)聯(lián)關(guān)系邏輯出現(xiàn)不可預(yù)期的錯(cuò)誤侦鹏。比如關(guān)卡配置表依賴怪物配置表诡曙,重載發(fā)生時(shí)候,關(guān)卡表重新載入了略水,依賴了新的怪物价卤。但怪物表還未載入完成,如果玩家此時(shí)讀取到了新的關(guān)卡配置表就會(huì)出現(xiàn)錯(cuò)誤 - 重載時(shí)必須清理和重建自己自定義的索引緩存
那我是怎么設(shè)計(jì)的渊涝,我是通過以下設(shè)計(jì)避免上述問題呢慎璧?
- 雙buff機(jī)制
- 配置管理類每次重載時(shí)重新生成的
- 通過實(shí)體Type查詢具體的配置管理類
那我們直接看代碼吧
public class ConfigManager
{
/// <summary>
/// 內(nèi)部實(shí)例
/// </summary>
private static readonly ConfigManager ConfigInstance = new ConfigManager();
/// <summary>
/// 內(nèi)部CacheMap
/// </summary>
private readonly Dictionary<Type, object>[] _cacheMaps;
/// <summary>
/// 游標(biāo)cursor
/// </summary>
private volatile int _cursor;
/// <summary>
/// 游標(biāo)local標(biāo)志
/// </summary>
private readonly ThreadLocal<int> _cursorLocal;
/// <summary>
/// 被cache的類
/// </summary>
private readonly List<Type> _cacheList;
private ConfigManager()
{
_cacheMaps = new Dictionary<Type, object>[2];
_cacheMaps[0] = new();
_cacheMaps[1] = new();
_cursorLocal = new(() => -1);
_cacheList = new();
}
public static ConfigManager Instance()
{
return ConfigInstance;
}
public void Register(Type type)
{
_cacheList.Add(type);
}
public ConfigCache<K, V> GetCache<K, V>(Type type)
{
var index = _cursorLocal.Value;
if (index == -1)
{
_cursorLocal.Value = _cursor;
index = _cursorLocal.Value;
}
else
{
index = _cursorLocal.Value;
}
return _cacheMaps[index][type] as ConfigCache<K, V>;
}
public void Reload(IConfigLoader loader)
{
var index = 1 - _cursor;
try
{
Init(index, loader);
_cursor = index;
_cursorLocal = new(() => -1);
}
catch (Exception e)
{
Log.Error(e, "reload Config error");
}
}
private void Init(int index, IConfigLoader loader)
{
foreach (var type in _cacheList)
{
var obj = Activator.CreateInstance(type);
if (obj is ConfigLoadable configLoader)
{
configLoader.SetConfigLoader(loader);
configLoader.Load();
_cacheMaps[index][type] = loader;
}
}
}
}
- 設(shè)計(jì)了一個(gè)
ConfigManager
統(tǒng)一管理所有ConfigCache
- 設(shè)計(jì)了雙buff
_cacheMaps
用于安全的重載ConfigCache
- 采用了
ThreadLocal
保證,單個(gè)線程訪問的一致性 - 一次重新載入過程是重建
ConfigCache
避免了自建索引忘記重建的bug - 提供了一個(gè)通過
Type
獲取ConfigCache
的統(tǒng)一入庫
那剩下的重載配置表就是這行代碼了
ConfigManager.Instance.Reload(loader);
結(jié)語
通過以上設(shè)計(jì)跨释,就完成了一個(gè)安全可靠的配置表重載系統(tǒng)胸私,對(duì)此你怎么看?歡迎評(píng)論交流鳖谈!