游戏配置表的热重载设计

设计一个可靠的配置表可重载系统

前言

在制作一款游戏中,有大量的配置表。比如游戏中有装备的配置表,它定义了装备的icon,名称,属性等等。有关卡配置表,配置了关卡信息,怪物信息,怪物数据等等。对于一款实时在线游戏,我们需要这些配置表可以被热更新,可以快速fix一些配置错误,而无需停机维护游戏。那如何做到可靠有效的热更新呢?正是这篇文章要介绍给大家的。

一、理解配置表

不同的游戏配置表的实际形式也是多种多样的,有的配置表采用Excel ,有的配置表采用csv文件 ,有的直接使用数据库表 。不管是那种形式,游戏的配置表不是那种单纯配置参数的key,value配置文件。它是结构化的!不管它的载体是什么,我把它当做一个数据表去维护,表与表之间可以有关联关系。

对于一张实体配置表,我新建一个对应的Domain实体类 与之对应。这是不是像是管理数据表类似的,是的!我就是把配置表当做一个数据库表,在代码上有一个对应的实体类。但不同的是,它是一张内存只读表 下面我就用装备配置表为例来一场载入到安全可重载之旅!

装备Id 装备名称 装备的品质 装备的图像
1001 暗影战斧 精良 anyingzhanfu
1002 铁剑 普通 tiejian
1003 破晓 传说 pojie

备注:这里只是举个例子,实际游戏中的装备配置表要比这个复杂许多

对应的Domain类

public class Equip
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Quality { get; set; }
    public string Pic { get; set; }
}

二、载入配置表

对于上面定义的配置表,我们如果载入到内存并且管理起来呢?我想到的是用一个Dictionary ,Key是表的主键,Value是对应的实体类。我定义了一个配置表的Cache管理器,它的接口定义如下:

public interface ConfigCache<K, V>
{
    /// <summary>
    /// 从缓存中获取Key
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    V Get(K key);

    /// <summary>
    /// 将对象放入缓存
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    void Put(K key, V value);

    /// <summary>
    /// 载入所有Models
    /// </summary>
    /// <returns></returns>
    List<V> GetModels();
}
  • 你可以通过主键获取指定配置
  • 你可以放入指定配置到缓存表,实际的操作一般是启动时载入所有配置到内存
  • 获取所有的配置信息

我们有了配置表管理类,那如何载入呢?如何将配置信息载入进管理类呢?上面我提到配置表有很多不同的载体,比如Excel,数据库表,CSV文件等。同样我也设计了一个载入API,不同的载体去实现不同的实现类即可,定义如下:

public interface IConfigLoader
{
    /// <summary>
    /// 获取所有配置信息
    /// </summary>
    /// <typeparam name="V"></typeparam>
    /// <returns></returns>
    List<V> GetModels<V>();
}

public interface ConfigLoadable
{
    /// <summary>
    /// 载入缓存
    /// </summary>
    void Load();

    /// <summary>
    /// 设置配置载入器
    /// </summary>
    /// <param name="loader"></param>
    void SetConfigLoader(IConfigLoader loader);

    /// <summary>
    /// 获取配置载入器
    /// </summary>
    /// <returns></returns>
    IConfigLoader GetConfigLoader();
}

有了载入器,你就可以在系统启动时候载入对应配置,放入缓存管理类了。那我们看看实际的EquipCache吧。

// 配置表的基类,子类只需要实现尽量少的代码
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();
}

// 实际的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);
        }
    }
}

看看目前我们做到了什么?

  • 定义了配置表的管理方式
  • 定义了配置表的载入方法,采用接口的设计,你可以根据自己的需要扩展自己的载入器即可
  • 每个配置表实际管理类只需要实现少量代码
  • 由于每个配置表都有一个管理器,你实际上拥有了自由定义配置表管理的能力。比如我想装备配置表可以按照品质检索。
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>
    /// 通过品质获取装备配置信息
    /// </summary>
    public List<Equip> GetEquipByQuality(string quality)
    {
        return GetModels().Where(v => v.Quality == quality).ToList();
    }
}
  • 如果非常高频,你还可以通过在Init里面提前构建索引表用来加速查询

目前我们已经做到如果载入配置表,以及怎么管理和使用它,在游戏的其他模块使用它已经很称手了,接下去是时候给它添加热重载支持了!

三、热重载配置表

热重载本质是重新载入,可以是通过指定让系统重新载入,或者是自动检测到配置表变化实现重新载入。对于上述的设计,热重载不就是重新调用一下Load方法即可吗?其实是的,但当如果只是这样做它是不够安全的。它有以下问题:

  • 配置表的管理类的Dictionary 不是线程安全的,如果在其他线程查询的时候,又触发了重新载入,这是不可预期的
  • 还有就是配置表之间一般是有关联 关系的,重新载入的过程可能破坏这种关联关系,导致依赖关联关系逻辑出现不可预期的错误。比如关卡配置表依赖怪物配置表,重载发生时候,关卡表重新载入了,依赖了新的怪物。但怪物表还未载入完成,如果玩家此时读取到了新的关卡配置表就会出现错误
  • 重载时必须清理和重建自己自定义的索引缓存

那我是怎么设计的,我是通过以下设计避免上述问题呢?

  • 双buff机制
  • 配置管理类每次重载时重新生成的
  • 通过实体Type查询具体的配置管理类

那我们直接看代码吧

public class ConfigManager
{
    /// <summary>
    /// 内部实例
    /// </summary>
    private static readonly ConfigManager ConfigInstance = new ConfigManager();

    /// <summary>
    /// 内部CacheMap
    /// </summary>
    private readonly Dictionary<Type, object>[] _cacheMaps;

    /// <summary>
    /// 游标cursor
    /// </summary>
    private volatile int _cursor;

    /// <summary>
    /// 游标local标志
    /// </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;
            }
        }
    }
}
  • 设计了一个ConfigManager 统一管理所有ConfigCache
  • 设计了双buff_cacheMaps 用于安全的重载ConfigCache
  • 采用了ThreadLocal保证,单个线程访问的一致性
  • 一次重新载入过程是重建ConfigCache 避免了自建索引忘记重建的bug
  • 提供了一个通过Type 获取ConfigCache 的统一入库

那剩下的重载配置表就是这行代码了

ConfigManager.Instance.Reload(loader);

结语

通过以上设计,就完成了一个安全可靠的配置表重载系统,对此你怎么看?欢迎评论交流!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容