游戲配置表的熱重載設(shè)計(jì)

設(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)論交流鳖谈!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末岁疼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子缆娃,更是在濱河造成了極大的恐慌捷绒,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贯要,死亡現(xiàn)場(chǎng)離奇詭異暖侨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)郭毕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門它碎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人显押,你說我怎么就攤上這事扳肛。” “怎么了乘碑?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵挖息,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我兽肤,道長(zhǎng)套腹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任资铡,我火速辦了婚禮电禀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘笤休。我一直安慰自己尖飞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著政基,像睡著了一般贞铣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沮明,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天辕坝,我揣著相機(jī)與錄音,去河邊找鬼荐健。 笑死酱畅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摧扇。 我是一名探鬼主播圣贸,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼扛稽!你這毒婦竟也來了吁峻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤在张,失蹤者是張志新(化名)和其女友劉穎用含,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帮匾,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡啄骇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘟斜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缸夹。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖螺句,靈堂內(nèi)的尸體忽然破棺而出虽惭,到底是詐尸還是另有隱情,我是刑警寧澤蛇尚,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布芽唇,位于F島的核電站,受9級(jí)特大地震影響取劫,放射性物質(zhì)發(fā)生泄漏匆笤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一谱邪、第九天 我趴在偏房一處隱蔽的房頂上張望炮捧。 院中可真熱鬧,春花似錦惦银、人聲如沸咆课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽傀蚌。三九已至,卻和暖如春蘸吓,著一層夾襖步出監(jiān)牢的瞬間善炫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工库继, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留箩艺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓宪萄,卻偏偏與公主長(zhǎng)得像艺谆,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拜英,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容