游戏配置表的热重载设计

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

前言

在制作一款游戏中,有大量的配置表。比如游戏中有装备的配置表,它定义了装备的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
{
    /// 
    /// 从缓存中获取Key
    /// 
    /// 
    /// 
    V Get(K key);

    /// 
    /// 将对象放入缓存
    /// 
    /// 
    /// 
    void Put(K key, V value);

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

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

public interface IConfigLoader
{
    /// 
    /// 获取所有配置信息
    /// 
    /// 
    /// 
    List GetModels();
}

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

    /// 
    /// 设置配置载入器
    /// 
    /// 
    void SetConfigLoader(IConfigLoader loader);

    /// 
    /// 获取配置载入器
    /// 
    /// 
    IConfigLoader GetConfigLoader();
}

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

// 配置表的基类,子类只需要实现尽量少的代码
public abstract class BaseConfigCache : ConfigCache, IConfigLoader
{
    /// 
    /// 缓存Map
    /// 
    protected readonly Dictionary CacheMap = new ();

    /// 
    /// 缓存载入器
    /// 
    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 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
{
    public override void Init()
    {
        var modelList = GetConfigLoader().GetModels();
        foreach(var model in modelList)
        {
            Put(model.Id, model);
        }
    }
}

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

  • 定义了配置表的管理方式
  • 定义了配置表的载入方法,采用接口的设计,你可以根据自己的需要扩展自己的载入器即可
  • 每个配置表实际管理类只需要实现少量代码
  • 由于每个配置表都有一个管理器,你实际上拥有了自由定义配置表管理的能力。比如我想装备配置表可以按照品质检索。
public class EquipCache : BaseConfigCache
{
    public override void Init()
    {
        var modelList = GetConfigLoader().GetModels();
        foreach(var model in modelList)
        {
            Put(model.Id, model);
        }
    }

    /// 
    /// 通过品质获取装备配置信息
    /// 
    public List GetEquipByQuality(string quality)
    {
        return GetModels().Where(v => v.Quality == quality).ToList();
    }
}
  • 如果非常高频,你还可以通过在Init里面提前构建索引表用来加速查询

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

三、热重载配置表

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

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

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

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

那我们直接看代码吧

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

    /// 
    /// 内部CacheMap
    /// 
    private readonly Dictionary[] _cacheMaps;

    /// 
    /// 游标cursor
    /// 
    private volatile int _cursor;

    /// 
    /// 游标local标志
    /// 
    private readonly ThreadLocal _cursorLocal;

    /// 
    /// 被cache的类
    /// 
    private readonly List _cacheList;

    private ConfigManager()
    {
        _cacheMaps = new Dictionary[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 GetCache(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;
    }

    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);

结语

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

你可能感兴趣的:(游戏配置表的热重载设计)