Unity可扩展、灵活的战斗框架实现2 数值和属性

文章讨论了如何在复杂游戏中升级属性管理,从单一的基础类型到UnityGAS中的Attribute和AttributeValue设计,提出了一种更灵活的属性计算方式,包括基础属性、衍生属性和加成机制的改进,以支持动态变化和多英雄配置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在一些简单的游戏中,属性可以直接使用基础类型来定义,比如 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个线性依赖属性

  1. Armour 4 + 0.17 * Agility
  2. Attack Speed 1 * Agility
  3. Max Health 200 + 20 * Strength
  4. Health Regen 1.25 + 0.1 * Strength
  5. Max Mana 75 + 12 * Intelligence
  6. Mana Regen 0.5 + 0.05 * Intelligence

我认为LinearDerivedAttribute这种方式有一个缺点,那就是offset是直接配置保存在SO中,多个单位类型的区别不够方便,假如我需要创建10个英雄,那么就需要给每个英雄单独创建一套SO来存储它的属性,无疑是不方便的。

实际上这些属性的共性只有对所依赖的属性的计算方式,我们的so中不应当存储offset,这个offset值应该由英雄的config来传入。

另外,可以看到,这里属性的值只有一个modifier,里面三个属性分别进行加、乘、覆写,考虑到加成值。这里参考EGAMEPLAY 

m969/EGamePlay: 一个基于Entity-Component模式的灵活、通用、可扩展的轻量战斗(技能)框架,配置可选使用ScriptableObject或是Excel表格. A flexible, generic, easy to extend, lightweight combat (skills) framework based on Entity-Component pattern. Configuration can choose to use ScriptableObject or Excel tables. (github.com)

首先,我们应该有几个基础类

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实例。

编写 Unity 的战斗框架需要考虑多个方面,包括战斗场景设计、角色属性管理、技能系统、伤害计算等等。以下是一个简单的战斗框架实现步骤: 1. 设计战斗场景。根据游戏需求设计出战斗场景,并在 Unity 中创建相应的场景。 2. 创建角色。在 Unity 中创建角色,包括角色的模型、动画属性等。根据游戏需求选择不同的角色类型,例如战斗士兵、法师、弓箭手等。 3. 管理角色属性。为每个角色添加属性组件,包括生命值、攻击力、防御力等。在角色受到攻击时,根据攻击者的属性计算出受到的伤害值,然后减去生命值。 4. 设计技能系统。在 Unity 中创建技能系统,包括技能的触发条件、技能的效果技能的消耗等。根据游戏需求设计出不同的技能类型,例如攻击技能、防御技能、治疗技能等。 5. 触发技能。在角色受到攻击或者主动释放技能时,触发相应的技能效果。根据技能的触发条件效果,在角色身上添加相应的状态,例如冰冻、昏迷、中毒等。 6. 计算伤害。在技能效果中计算伤害值,并减去目标角色的生命值。根据游戏需求设计出不同的伤害计算方式,例如基于攻击力防御力的计算、基于元素属性的计算等。 7. 处理战斗结果。在战斗结束时,根据游戏规则计算出胜负结果,并展示相应的结算界面。 以上是一个简单的战斗框架实现步骤。在实际开发中,还需要考虑各种异常情况的处理优化性能等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值