前言

技能系统的数值可以说是五花八门,每种技能的参数可能千奇百怪。

很多技能系统采用excel来配置技能数据。表头会变得很长,并且可能是好几张表,制作一个技能,需要几张表来回跳,个人不太喜欢这种方式。

本技能系统的数值表,采用类似dota2的数据驱动配置方式。

SpecialTable

数据源,键值对,key为string,value为Var。可以扩展csv格式导入导出,以支持批量数值编辑

Var

数值类,泛型实现,有VarSingle、VarInt等等类型,string,是一个类型数据的集合,可以直接填写数据,也可以通过$key来索引SpecialTable中的数据。

配置文件加载时,调用接口解析数据(默认使用反射,无需编写代码),字符串解析使用了Span API,无任何临时字符串,Var保存List对象,后续使用时,无解析、拆箱开销。

Var_T
public abstract class Var<T> : Var
{
    [NonSerialized]
    [JsonIgnore]
    public List<T> values;

    public T this[int index] => GetValue(index);

    [JsonIgnore]
    public int Count => values != null ? values.Count : 0;

    public Var() { }
    public Var(string txt) => this.txt = txt;

    public sealed override object GetRawValue(int index) => GetValue(index);

    public T GetValue(int index = 0)
    {
        if (values == null || values.Count == 0) return default;
        if (index >= values.Count) return values[^1];
        return values[index];
    }

    public sealed override void Parse(IReadOnlyList<VarPair> specials)
    {
        if (raw != null) return;
        if (string.IsNullOrEmpty(txt)) return;
        if (txt.StartsWith(Ref))
            ParseRef(specials);
        else
            ParseValues();
    }

    private void ParseRef(IReadOnlyList<VarPair> specials)
    {
        if (specials == null || specials.Count == 0)
        {
            AbilityLog.ERROR($"ParseValues Error. {nameof(txt)}: {txt} {nameof(specials)} is null or empty");
            return;
        }
        var valueSpan = txt.AsSpan();
        for (int i = 0; i < specials.Count; i++)
        {
            var special = specials[i];
            if (special.name.AsSpan().SequenceEqual(valueSpan[1..])) // 切掉 $ 字符
            {
                values = (List<T>)special.value.raw;
                break;
            }
        }
        Log.Assert(values != null, $"{nameof(ParseRef)} failed. '{txt}'");
    }

    protected abstract void ParseValues();
}
VarInt
[Serializable]
public sealed class VarInt : Var<int>
{
    public VarInt() { }

    public VarInt(string txt) : base(txt) { }

    protected override void ParseValues()
    {
        var valueSpan = txt.AsSpan().Trim();
        if (valueSpan.IsEmpty) return;
        var ranges = txt.SplitFast(' ');
        raw = values = new List<int>();
        foreach (var range in ranges)
        {
            if (range.Start.Value == range.End.Value) continue;
            var item = valueSpan[range].Trim();
            if (item.IsEmpty) continue;
            if (int.TryParse(item, out var val))
                values.Add(val);
            else
                ParseError();
        }
    }
}

IVarMagnitudeCalculation

数值计算类接口,可以自定义扩展数值计算规则,并且可以在editor进行编辑,提供AttributeBasedSingle(基于属性计算)等数值计算类子类

AttributeBasedFloat
[Serializable]
[MovedFrom(true, sourceClassName: "AttributeBasedFloat")]
public sealed class AttributeBasedSingle : IVarMagnitudeCalculation
{
    public enum EAttributeSource : byte
    {
        Caster = 0,
        Target = 1,
    }

    public enum EAttributeCalculateType : byte
    {
        CurrValue = 0,
        BaseValue = 1,
    }

    [FormerlySerializedAs("modifierId")]
    public int attribute;
    public EAttributeSource source;
    public EAttributeCalculateType calculateType;

    // coefficient * (attribute + preMulAddValue) + postMulAddValue
    public VarSingle coefficient = new("1");
    public VarSingle preMulAddValue;
    public VarSingle postMulAddValue;

    public Single CalculateMagnitude(EffectObj effect, int level)
    {
        var set = source == EAttributeSource.Target ?
            effect.TargetAttributeSet :
            effect.CasterAttributeSet;
        Log.Assert(set != null, $"{effect.Config.id} {source}'s AttributeSet is null");
        if (set != null)
        {
            var att = set[attribute];
            Single attributeValue = (Single)0f;
            if (calculateType == EAttributeCalculateType.CurrValue)
                attributeValue = att.CurrValue;
            else if (calculateType == EAttributeCalculateType.BaseValue)
                attributeValue = att.BaseValue;

            return coefficient.GetValue(level) * (attributeValue + preMulAddValue.GetValue(level)) + postMulAddValue.GetValue(level);
        }
        return (Single)0f;
    }

    public void Parse(IReadOnlyList<VarPair> specials)
    {
        coefficient.Parse(specials);
        preMulAddValue.Parse(specials);
        postMulAddValue.Parse(specials);
    }
}

总结

通过上述,可实现配置统一,每个技能一个SpecialTable,数据想怎么定义怎么定义,而不用增加任何代码去解析,运行时性能也不错