C#Unity3D与游戏开发

游戏配置表的热重载设计

2021-11-07  本文已影响0人  Aodota

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

前言

在制作一款游戏中,有大量的配置表。比如游戏中有装备的配置表,它定义了装备的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();
    }
}

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

三、热重载配置表

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

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

那我们直接看代码吧

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.Instance.Reload(loader);

结语

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

上一篇 下一篇

猜你喜欢

热点阅读