在一些简单的游戏中,属性可以直接使用基础类型来定义,比如 int hp=100,float speed=50等等,但当战斗的复杂性提升,加入buff、装备等变量后,属性要根据buff的增加、删除等进行频繁的改变,又假如有些计算过程需要对基础值或附加值的百分比进行修正,那么简单的数值--属性系统就难以实现这些功能。
在dota2中,属性我们可以区分为基础属性(力量,敏捷,智力),以及由基础属性计算得到的属性(最大生命值,最大法力值,生命恢复,法力恢复,攻击力,攻击速度等等)
在UNITY GAS 的示例中,作者的设计是分为两个类Attribute和AttributeValue
AttributeValue是最基本的数值,里面四个字段,Attribute存储自己所属的属性的引用,Modifier用于添加临时的修正(如装备、buff等)
public struct AttributeValue
{
public AttributeScriptableObject Attribute;
public float BaseValue;
public float CurrentValue;
public AttributeModifier Modifier;
}
[Serializable]
public struct AttributeModifier
{
public float Add;
public float Multiply;
public float Override;
public AttributeModifier Combine(AttributeModifier other)
{
other.Add += Add;
other.Multiply += Multiply;
other.Override = Override;
return other;
}
}
AttributeScriptableObject
属性的基类,继承ScriptableObject,用于生成属性配置的实例存储。
public class AttributeScriptableObject : ScriptableObject
{
/// <summary>
/// Friendly name of this attribute. Used for dislpay purposes only.
/// </summary>
public string Name;
public event EventHandler PreAttributeChange;
public void OnPreAttributeChange(object sender, PreAttributeChangeEventArgs e)
{
EventHandler handler = PreAttributeChange;
PreAttributeChange?.Invoke(sender, e);
}
public virtual AttributeValue CalculateInitialValue(AttributeValue attributeValue, List<AttributeValue> otherAttributeValues)
{
return attributeValue;
}
public virtual AttributeValue CalculateCurrentAttributeValue(AttributeValue attributeValue, List<AttributeValue> otherAttributeValues)
{
attributeValue.CurrentValue = (attributeValue.BaseValue + attributeValue.Modifier.Add) * (attributeValue.Modifier.Multiply + 1);
if (attributeValue.Modifier.Override != 0)
{
attributeValue.CurrentValue = attributeValue.Modifier.Override;
}
return attributeValue;
}
}
我们可以看到这个基类提供了name字段来区别属性(比如力量,敏捷,智力等),并提供了两个虚方法供重写,CalculateInitialValue和CalculateCurrentAttributeValue,分别提供属性的初始化和计算当前属性值功能
UNITY-GAS也提供了可扩展的属性,比如在dota2中,HP是由基础值+力量*20来计算的,所以提供了一个示例LinearDerivedAttribute,即线性计算属性
public class LinearDerivedAttributeScriptableObject : AttributeScriptableObject
{
public AttributeScriptableObject Attribute;
[SerializeField] private float gradient;
[SerializeField] private float offset;
public override AttributeValue CalculateCurrentAttributeValue(AttributeValue attributeValue, List<AttributeValue> otherAttributeValues)
{
// Find desired attribute in list
var baseAttributeValue = otherAttributeValues.Find(x => x.Attribute == this.Attribute);
// Calculate new value
attributeValue.BaseValue = (baseAttributeValue.CurrentValue * gradient) + offset;
attributeValue.CurrentValue = (attributeValue.BaseValue + attributeValue.Modifier.Add) * (attributeValue.Modifier.Multiply + 1);
if (attributeValue.Modifier.Override != 0)
{
attributeValue.CurrentValue = attributeValue.Modifier.Override;
}
return attributeValue;
}
}
重写了计算方式,增加了依赖的属性Attribute,倍数gradient和偏移值offset三个属性
所以获取的当前值就是 ((基础属性当前值*倍数+偏移值)+加法加成)*(1+乘法加成)
作者的示例属性也是按照dota2中来举例,分为力量敏捷智力三个基础属性和6个线性依赖属性
- Armour
4 + 0.17 * Agility
- Attack Speed
1 * Agility
- Max Health
200 + 20 * Strength
- Health Regen
1.25 + 0.1 * Strength
- Max Mana
75 + 12 * Intelligence
- Mana Regen
0.5 + 0.05 * Intelligence
我认为LinearDerivedAttribute这种方式有一个缺点,那就是offset是直接配置保存在SO中,多个单位类型的区别不够方便,假如我需要创建10个英雄,那么就需要给每个英雄单独创建一套SO来存储它的属性,无疑是不方便的。
实际上这些属性的共性只有对所依赖的属性的计算方式,我们的so中不应当存储offset,这个offset值应该由英雄的config来传入。
另外,可以看到,这里属性的值只有一个modifier,里面三个属性分别进行加、乘、覆写,考虑到加成值。这里参考EGAMEPLAY
首先,我们应该有几个基础类
1: AttributeValue 提供baseValue, 第一次加成add和第一次百分比加成pctAdd,第二次加成finalAdd和finalPctAdd,以及获取基础值basevalue,加成值addValue和最终值currentvalue的方法,两次的加成计算可以让游戏的数值系统更灵活,让类似英雄联盟中的基础加成、额外加成更好实现。
2:AttributeValueModifier 对属性的加成值。
3:AttributeValueModifierCollection 多个属性增减效果的集合,方便在临时BUFF增加、消失或装备穿脱时对属性的修饰修改
4:AttributeValueModifierType 枚举Modifier的类型,使AttributeValue中的加成数字可随时根据Modifier类型来进行更新
代码如下
/// <summary>
/// 属性值修饰器
/// </summary>
public class AttributeValueModifier
{
public float Value;
}
/// <summary>
/// 属性修饰器集合
/// </summary>
public class AttibuteValueModifierCollection
{
public float TotalValue { get; private set; }
private List<AttributeValueModifier> _modifiers =new List<AttributeValueModifier>();
public void Update()
{
TotalValue = 0;
foreach (AttributeValueModifier modifier in _modifiers)
{
TotalValue += modifier.Value;
}
}
public float AddModifier(AttributeValueModifier modifier)
{
_modifiers.Add(modifier);
Update();
return TotalValue;
}
public float RemoveModifier(AttributeValueModifier modifier)
{
_modifiers.Remove(modifier);
Update();
return TotalValue;
}
}
public enum AttibuteValueModifierType
{
Add,
PctAdd,
FinalAdd,
FinalPctAdd
}
public class AttributeValue
{
public Attribute Attribute; //所属的属性
public float BaseValue { get; private set; } //基础值
public float CurrentValue{ get; private set; }//当前总数值
public float AddValue{ get; private set; }// 加成值
private float _add;
private float _pctAdd;
private float _finalAdd;
private float _finalPctAdd;
private Dictionary<AttibuteValueModifierType, AttibuteValueModifierCollection> _typeModifiers;
private void Update()
{
float firstCal = (BaseValue + _add) * (1 + _pctAdd);
float secondCal = (firstCal + _finalAdd) * (1 + _finalPctAdd);
CurrentValue = secondCal;
AddValue = CurrentValue - BaseValue;
}
public float SetBaseValue(float value)
{
BaseValue = value;
Update();
return BaseValue;
}
public float AddBaseValue(float value)
{
BaseValue += value;
if (BaseValue < 0) BaseValue = 0;
Update();
return BaseValue;
}
public float AddModifier(AttributeValueModifier modifier, AttibuteValueModifierType type)
{
float value=_typeModifiers[type].AddModifier(modifier);
if (type == AttibuteValueModifierType.Add) _add = value;
if (type == AttibuteValueModifierType.PctAdd) _pctAdd = value;
if (type == AttibuteValueModifierType.FinalAdd) _finalAdd = value;
if (type == AttibuteValueModifierType.FinalPctAdd) _finalPctAdd = value;
Update();
return CurrentValue;
}
public float RemoveModifier(AttributeValueModifier modifier, AttibuteValueModifierType type)
{
float value = _typeModifiers[type].RemoveModifier(modifier);
if (type == AttibuteValueModifierType.Add) _add = value;
if (type == AttibuteValueModifierType.PctAdd) _pctAdd = value;
if (type == AttibuteValueModifierType.FinalAdd) _finalAdd = value;
if (type == AttibuteValueModifierType.FinalPctAdd) _finalPctAdd = value;
Update();
return CurrentValue;
}
}
和UNITY-GAS不同,这里将数值的的加成计算放在数值类中,但在属性中仍留好虚方法以便继承重写
那么Attribute类是这样
[CreateAssetMenu(fileName = "基础属性配置",menuName = "属性配置/基础属性配置")]
public class Attribute : SerializedScriptableObject
{
/// <summary>
/// 属性名
/// </summary>
public string Name;
public virtual AttributeValue GetCurrentAttributeValue(AttributeValue attributeValue,List<AttributeValue> attributeValues)
{
return attributeValue;
}
}
次级属性就重写GetCurrentAttributeValue方法,来修改属性的计算方法
在之后的战斗实体中,都有一个AttributeSystemComponent来负责属性的刷新和计算,Attribute只是属性的计算方法的配置类,使用scripteObject存储在本地,所有实体的相同属性AttributeValue实例都仅引用一个Attibute实例。