unity最简buff系统,附带技能系统,分模块完全解耦

前言

alex教程观后感

关于耦合

我认为耦合分两种,一种是代码的耦合,一种是逻辑的耦合

代码的耦合源于依赖关系。比如说相互依赖,玩家依赖于技能的释放,技能又依赖于玩家的角色属性,不可能手动调用每个技能的接口,所以将技能抽象,让调用技能只依赖于抽象而不是实现。技能方面通过设置owner调用者,可以方便的获取属性,从而解除依赖(也算是依赖注入了)

逻辑的耦合大多不可避免,但也有例外,比如在攻击时通过管理类调用武器的特效,如果换成广播事件,由武器自行订阅,就可以完全解耦。有时换个思路也可以减少耦合

在我看来优质的代码,最理想的情况是一个改动只需要改一处地方,因为要加枚举避免字符串,改两个地方也能接受,超过三个地方都是为了快速开发没时间去设计框架。

正文

因为buff系统比较简单,就放在前面

Buff系统

buff系统一般分为两种,一种是Manager全局管理,一种是依附于角色自己管自己,我的是后者

核心是用反射实例化buff,然后根据类型填充数据,添加新的buff类只需要增加对应枚举和对应buff数据

buff数据

使用so管理buff数据,数据和buff执行逻辑分离,这里因为不想每次调用都在前面加buffData,所以写在一起

[CreateAssetMenu(menuName = "Buff/BuffDataSO")]
public class BuffDataSO : ScriptableObject
{
    public List<BuffBase> buffs;

    public BuffBase GetBuff(BuffType buffType)
    {
        return buffs.Find(x => x.buffType == buffType);
    }

    public void OnEnable()
    {
        foreach (var buff in buffs)
        {
            buff.timer = 0;
            buff.currentTarget = null;
        }
    }
}

[System.Serializable]
public class BuffBase
{
    public static BuffType realBuffType;
    public BuffType buffType;
    public BuffOverlap buffOverlap;
    public BuffCloseType buffCloseType;
    public BuffCalculateType buffCalculateType;

    public int effectValue;//效果值
    public int maxLimit;//最大次数
    public int level;//层数
    public float interalTime;//间隔时间
    public float durationTime;//持续时间
    [HideInInspector] public float timer;//计时器
    [HideInInspector] public CharacterStats currentTarget;
    [HideInInspector] public CharacterStats deployStats;

    public virtual void OnStart()
    {
    }
    public virtual void OnEffect()
    {
        if (timer >= durationTime)
        {
            currentTarget.RemoveBuff(this);
        }
        timer += Time.fixedDeltaTime;
    }
    public virtual void OnEnd()
    {
        timer = 0;
    }

    #region  简写
    public EntityFX Fx => currentTarget.fx != null ? currentTarget.fx : null;
    #endregion
}
对应枚举
public enum BuffType
    {
        None,
        Ignited,//被点燃
        Frozen,//被冻结
        Shocked,//被震撼
    }
    public enum BuffOverlap //叠加类型
    {
        None,
        StackedTime,//增加时间
        StackedLayer,//增加层数
        ResterTime,//重置时间
    }
    public enum BuffCloseType
    {
        All,//全部关闭
        Layer,//逐层关闭
    }
    public enum BuffCalculateType//执行类型 
    {
        Once,//一次
        Loop,//每次
    }

Buff工厂

这里将数据赋值给buff实例,虽然用了反射,但是做了缓存也影响不大

public class BuffFactory : SingletonMono<BuffFactory>
    {
        // 类型缓存
        private static Dictionary<string, Type> typeCache = new Dictionary<string, Type>();
        // 属性缓存
        private static ConcurrentDictionary<Type, FieldInfo[]> PropertyCaches = new();

        public BuffDataSO buffData;
        private Dictionary<BuffType, BuffBase> buffBases = new();

        public BuffBase CreateBuff(BuffType buffType)// 创建对应算法
        {
            string className = string.Format("Platform.Buff.Buffs.{0}Buff", buffType.ToString());
            if (!typeCache.TryGetValue(className, out Type type))
            {
                // 如果缓存中没有该类型,反射获取并缓存
                type = Type.GetType(className);
                if (type != null)
                {
                    typeCache[className] = type;
                }
                else
                {
                    Debug.LogError($"Type {className} not found.");
                    return null;
                }
            }

            //给buff填充数据
            var BuffData = buffData.GetBuff(buffType);

            BuffBase buff = Activator.CreateInstance(type) as BuffBase;

            if (buff != null) CopyProperties(BuffData, buff);

            return buff;
        }
        public static void CopyProperties<T>(T source, T target)
        {
            if (source == null || target == null)
                throw new ArgumentNullException("Source or target cannot be null.");

            var properties = PropertyCaches.GetOrAdd(typeof(T), t => t.GetFields());
            foreach (var property in properties)
            {
                var value = property.GetValue(source);
                property.SetValue(target, value);
            }
        }

    }

在角色类添加buff

 public void AddBuff(BuffType buffType, CharacterStats sourceStats = null)
        {
            var buff = buffs.FirstOrDefault(b => b.buffType == buffType);
            if (buff != null)
            {
                //处理覆盖情况
                switch (buff.buffOverlap)
                {
                    case Buff.BuffOverlap.None:
                        //无视
                        break;
                    case Buff.BuffOverlap.StackedTime:
                        buff.timer = buff.interalTime;
                        break;
                    case Buff.BuffOverlap.StackedLayer:
                        buff.level++;
                        break;
                    case Buff.BuffOverlap.ResterTime:
                        RemoveBuff(buff);
                        AddBuff(buffType);
                        break;
                }
            }
            else
            {
                buff = BuffFactory.Instance.CreateBuff(buffType);
                buff.currentTarget = this;
                buff.deployStats = sourceStats;
                buffs.Add(buff);
                buff.OnStart();
            }
        }
        public void ReFreshBuff()
        {
            // 后加入先执行
            for (int i = buffs.Count - 1; i >= 0; i--)
            {
                buffs[i].OnEffect();
            }
        }
        public void RemoveBuff(BuffType buffType)
        {
            var buff = buffs.FirstOrDefault(b => b.buffType == buffType);
            if (buff != null)
            {
                RemoveBuff(buff);
            }
        }
        public void RemoveBuff(BuffBase buff)
        {
            buffs.Remove(buff);
            buff.OnEnd();
        }
燃烧buff案例
 public class IgnitedBuff : BuffBase
    {
        private float fireTimer = 0;

        public override void OnStart()
        {
            base.OnStart()  ;
            ZTimer.SetInterval(0.2f, Fx.FireColorFx);
        }
        public override void OnEnd()
        {
            base.OnEnd();
            ZTimer.ClearTimer(Fx.FireColorFx);
            Fx.CancelColorChange();
        }

        public override void OnEffect()
        {
            fireTimer += Time.fixedDeltaTime;
            if (fireTimer >= interalTime)
            {
                currentTarget.CurrentHealth -= effectValue;
                currentTarget.OnHealthChange?.Invoke();
                fireTimer = 0;
            }
            base.OnEffect();
        }
    }

技能系统

主要参考:https://zhuanlan.zhihu.com/p/513705768
建议先看原文,讲的比我更好,这里受限时间也只能放代码了。主要是根据实际情况增加了一点内容,改的最大的可能就是给反射加了个缓存和对象池:)
还有就是闪电链,原来的架构感觉少了一层不好实现,SO的特性刚好补上了
大致逻辑:
在这里插入图片描述

技能数据

因为SO文件相当于全局变量,所以多个释放器也可以通过共有的那一份数据管理,比如闪电链同时有多个实例,又需要记录一个共有的最后命中的敌人。好处是释放器上面不用再加一层管理释放器的类
因此最好怪物和角色技能用不同的SO存储
说是释放器,准确的讲是控制器更合适

[CreateAssetMenu(menuName = "skill/SkillDataSO")]
public class SkillDataSO : ScriptableObject
{
    public List<SkillData> skills;

    public void OnEnable()
    {
    foreach (var skill in skills)
        {
            var fields = typeof(SkillData).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
            foreach (var field in fields)
            {
                if (System.Attribute.IsDefined(field, typeof(HideInInspector)))
                {
                    switch (field.FieldType.Name)
                    {
                        case "Single": // float
                            field.SetValue(skill, 0f);
                            break;
                        case "Int32": // int
                            field.SetValue(skill, 0);
                            break;
                        case "GameObject":
                        case "Image":
                        case "Transform":
                            field.SetValue(skill, null);
                            break;
                        case "HashSet`1":
                            field.SetValue(skill, new HashSet<Transform>());
                            break;
                        case "List`1":
                            field.SetValue(skill, new List<GameObject>());
                            break;
                        case "Vector3":
                            field.SetValue(skill, Vector3.zero);
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
}

[System.Serializable]
public class SkillData
{
    [HideInInspector] public GameObject owner;//技能所属的角色
    public int skillId;
    public string name;//技能名称
    public float cooldown;
    [HideInInspector] public float cdRemain;
    public int level;//技能等级
    public int costMp;//法力值消耗
    public float range;
    public float Distance;
    public int attackDamage;
    [Header("攻击间隔")]
    public float attackInterval;//伤害间隔
    [Header("持续时间")]
    public float durationTime;//持续时间

    [Header("攻击类型")]
    public DamageType damageType;

    [Header("攻击目标")]
    public SkillAttackType attackType;//攻击目标

    /// <summary>
    /// 攻击的冻结时间
    /// </summary>
    [Header("冻结时间")]
    public float freezeTime;

    [Header("技能状态")]
    public States[] stateNames;//状态名称

    [Header("技能影响类型")]
    public ImpactEnum[] impactType;//技能影响类型
    [Header("释放范围类型")]
    public SelectorTypeEnum selectorType;//释放范围类型(圆形,扇形,矩形)

    [Header("技能指示器")]
    public SkillIndicatorEnum skillIndicator;//技能指示器名字

    //  public int nextBatterld;//连击的技能ID

    [Header("技能预制体")]
    public string prefabName;//技能预制体名称
    [HideInInspector] public GameObject skillPrefab;//预制体对象

    public LayerMask attackTargetLayers;//能作用的目标Tag
    [HideInInspector] public HashSet<Transform> attackTargets;//作用目标对象数组

    // public string skillIndicator;//技能指示器名字|成功释放技能之前显示出来的辅助技能释放的工具

    [Header("技能图标")]
    public string skillIconName;//技能显示图标名字
    [HideInInspector] public Image skillIcon;//技能事件图标
    [Header("攻击持续类型")]
    public CalculDamageType disappearType;

    //技能生成位置, 处理重复在指定位置生成
    [HideInInspector] public Vector3 targetPos;

    [Header("被动属性")]
    public float chanceActive;

    /// <summary>
    /// 连击技能 处理传送
    /// </summary>
    public SkillEnum nextSkill;

    [Header("远程攻击属性")]
    public Vector2 launchForce;
    public float gravity;

    [Header("多段攻击属性")]
    public int comboCount;
    [HideInInspector] public int combo;
    [Header("穿透属性")]
    public int pierceCount;
    [HideInInspector] public int pierced;


    [Header("装填类技能")]
    public int backfillCount;
    [HideInInspector] public List<GameObject> backfills;


    //给闪电链用的
    [HideInInspector] public Transform lastTarget;

}
对应枚举
 // 数字对应id  也可以直接用枚举
    public enum SkillEnum
    {
        None = 0,
        Dash = 1,
        Clone = 2,
        ReBoundSword = 3,
        PierceSword = 4,
        SpinSword = 5,
        flySword = 6,
        Blackhole = 7,
        Crystal = 8,
        ExplodeCrstal = 9,
        ThunderStrike = 10,
    }

    public enum DamageType
    {
        Normal,
        Magic,
        OwnerAttack,
        OwnerMagic,
    }


    public enum ImpactEnum
    {
        Dash,
        Clone,
        ReBound,
        Pierce,
        Spin,
        Insert,
        Transfer,
    }

    public enum SkillAttackType
    {
        single,
        aoe,
    }

    public enum SelectorTypeEnum
    {
        None,
        Circle,
        Nearly,
    }

    public enum CalculDamageType
    {
        Once,
        Continue,
    }

    public enum SkillIndicatorEnum
    {
        None,
        Dotted,
    }

核心管理器

public class SkillMgr : MonoBehaviour
    {
        const string SKILL_PATH = "SkillPrefab/";
        public SkillDataSO skillDataList;
        public bool isSkillChoose;
        public SkillData currentSkill;
        private Entity entity;

        private BaseSkillIndicator skillIndicator;

        // 避免填充技能重复执行冷却
        private bool isColldown;


        void Start()
        {
            if (skillDataList != null)
            {
                // skillData.ResetSkills();
                Init();
            }
            entity = GetComponent<Entity>();
        }

        void Init()
        {
            foreach (var skillData in skillDataList.skills)
            {
                if (!skillData.prefabName.IsNullOrEmpty())
                {
                    // PoolMgr.Instance.CreatePool(SKILL_PATH + skill.prefabName, 5);
                    // skill.skillPrefab = Resources.Load<GameObject>("/SkillPrefab" + skill.prefabName);
                    skillData.owner = gameObject;
                    skillData.attackTargets = new HashSet<Transform>();

                    // 提前填充  技能预制体
                    if (skillData.backfillCount > 0)
                    {
                        skillData.backfills = new List<GameObject>();
                        for (int i = 0; i < skillData.backfillCount; i++)
                        {
                            BackfillObj(skillData);
                        }
                    }
                }
            }
        }

        #region  按键触发
        void OnEnable()
        {
            PlayerInput.Instance.inputActions.Player.UseSkill.performed += OnUseSkill;
            PlayerInput.Instance.inputActions.Player.CloseSkill.performed += OnCloseSkill;
        }
        void OnDisable()
        {
            PlayerInput.Instance.inputActions.Player.UseSkill.performed -= OnUseSkill;
            PlayerInput.Instance.inputActions.Player.CloseSkill.performed -= OnCloseSkill;
        }

        private void OnCloseSkill(UnityEngine.InputSystem.InputAction.CallbackContext context)
        {
            if (isSkillChoose)
            {
                CloseSkillIndicator();
            }
        }

        /// <summary>
        /// 技能通过动画调用就不会走这里
        /// </summary>
        /// <param name="context"></param>
        private void OnUseSkill(UnityEngine.InputSystem.InputAction.CallbackContext context)
        {
            if (isSkillChoose)
            {
                if (currentSkill.stateNames.Length > 0)
                {
                    StartCoroutine(ChangeStateAsync(currentSkill.stateNames));
                }
                else
                    UseSkill(currentSkill.skillId);
            }


            IEnumerator ChangeStateAsync(States[] states)
            {
                foreach (var state in states)
                {
                    yield return new WaitForEndOfFrame();
                    entity.ChangeState(state);
                }
            }
        }
        #endregion

        #region  使用技能
        public SkillData PrepareSkill(SkillEnum skillEnum)
        {
            SkillData skillData = skillDataList.skills.Find(x => x.skillId == (int)skillEnum);
            if (skillData.backfillCount > 0)
            {
                return skillData;
            }
            if (skillData != null && skillData.cdRemain <= 0)//这里还有技能消耗值的判断
                return skillData;
            else
                return null;
        }

        /// <summary>
        /// 生成技能
        /// </summary>
        /// <param name="skillEnum"></param>
        /// <param name="targetPos">技能生成和目标位置</param>
        /// <returns></returns>
        public SkillData UseSkill(SkillEnum skillEnum, Vector3 targetPos = default)
        {
            // todo 暂时写在这里,统一的处理
            if (isSkillChoose) CloseSkillIndicator();

            var skillData = PrepareSkill(skillEnum);
            if (skillData == null) return null;
            currentSkill = skillData;

            if (!skillData.prefabName.IsNullOrEmpty())
            {
                GameObject skillPrefab = null;
                // 自指多段技能,不重复创建预制体
                // 传送的需求是自身被再次创建时触发下一段, 依赖于自身 无法使用事件
                if (skillEnum == skillData.nextSkill)
                {
                    if (skillData.skillPrefab != null)
                    {
                        skillPrefab = skillData.skillPrefab;
                    }
                    else
                    {
                        var prefab = Resources.Load<GameObject>(SKILL_PATH + skillData.prefabName);
                        skillPrefab = Instantiate(prefab, transform.position, transform.rotation);
                    }
                    skillPrefab.SetActive(true);
                }
                // 填充式技能
                else if (skillData.backfillCount > 0)
                {
                    if (skillData.backfills.Count > 0)
                    {
                        skillPrefab = skillData.backfills[0];
                        skillPrefab.SetActive(true);
                        skillData.backfills.RemoveAt(0);
                    }
                }
                else
                {
                    //创建技能预制体
                    skillPrefab = PoolMgr.Instance.GetObj(SKILL_PATH + skillData.prefabName, transform.position, transform.rotation);
                }
                skillData.skillPrefab = skillPrefab;

                if (targetPos != default)
                {
                    skillPrefab.transform.position = targetPos;
                    skillData.targetPos = targetPos;
                }

                //传递技能数据给技能释放器
                SkillDeployer deployer = skillPrefab != null ? skillPrefab.GetComponent<SkillDeployer>() : null;

                if (deployer != null)
                {
                    deployer.SkillData = skillData;
                    //释放器释放技能
                    deployer.DeploySkill();
                }
            }

            //防止装填的冷却重复触发
            if (!isColldown)
                StartCoroutine(CoolTimeDown(skillData));
            return skillData;
        }
        public SkillData UseSkill(int id)
        {
            if (Enum.TryParse(id.ToString(), out SkillEnum skillEnum))
            {
                return UseSkill(skillEnum);
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// 装填技能预制体
        /// </summary>
        private void BackfillObj(SkillData skillData)
        {
            Debug.Log("装填技能预制体");
            var obj = PoolMgr.Instance.GetObj(SKILL_PATH + skillData.prefabName);
            obj.SetActive(false);
            skillData.backfills.Add(obj);
        }


        //协程实现技能冷却  
        private IEnumerator CoolTimeDown(SkillData skillData)
        {
            isColldown = true;
            skillData.cdRemain = skillData.cooldown;
            while (skillData.cdRemain > 0)
            {
                yield return new WaitForSeconds(0.1f);
                skillData.cdRemain -= 0.1f;
            }
            isColldown = false;

            //冷却完毕,如果是填充技能,递归
            if (skillData.backfillCount > 0)
            {
                if (skillData.backfills.Count < skillData.backfillCount)
                {
                    BackfillObj(skillData);
                    StartCoroutine(CoolTimeDown(skillData));
                }
            }
        }
        #endregion


        #region  技能指示器  
        public bool OpenSkillIndicator(SkillEnum skillEnum)
        {
            if (isSkillChoose) return false;
            var skill = PrepareSkill(skillEnum);
            currentSkill = skill;
            if (skill == null || skill.skillIndicator == SkillIndicatorEnum.None)
            {
                Debug.Log("不存在技能指示器");
                return false;
            }
            else
            {
                Debug.Log("打开技能指示器");
                isSkillChoose = true;
                SelectSpellIndicator(skill.skillIndicator);
                return true;
            }
        }

        public void CloseSkillIndicator()
        {
            isSkillChoose = false;
            entity.ChangeState(Entity.States.Idle);
            CancelSpellIndicator();
        }

        // 此处耦合,考虑反射或映射
        private void SelectSpellIndicator(SkillIndicatorEnum skillIndicatorType)
        {
            switch (skillIndicatorType)
            {
                case SkillIndicatorEnum.Dotted:
                    skillIndicator = GetComponent<DottedIndicator>() ?? gameObject.AddComponent<DottedIndicator>();
                    break;
                default:
                    skillIndicator = null;
                    break;
            }
            if (skillIndicator != null)
                StartCoroutine(ShowIndicator());

            IEnumerator ShowIndicator()
            {
                skillIndicator.skillData = currentSkill;
                skillIndicator.entity = entity;
                yield return null; // 等待下一帧
                skillIndicator?.Show();
            }
        }


        private void CancelSpellIndicator()
        {
            skillIndicator?.Hide();
        }

        #endregion

技能释放器

释放器工厂
public class DeployerConfigFactory : MonoBehaviour
    {
        // 一个字典用以缓存类型
        private static Dictionary<string, Type> typeCache = new Dictionary<string, Type>();

        public static ISkillSelector CreateSkillSelector(SkillData data) // 范围选择算法 
        {
            if (data.selectorType == SelectorTypeEnum.None) return null;

            // 根据选择类型构建类名
            string className = string.Format("GardeningSkeleton.Platform.SkillSpace.{0}SkillSelector", data.selectorType.ToString());
            return CreateObject<ISkillSelector>(className);
        }

        public static BaseImpactEffect[] CreateImpactEffects(SkillData data) // 效果算法
        {
            BaseImpactEffect[] impacts = new BaseImpactEffect[data.impactType.Length];
            for (int i = 0; i < data.impactType.Length; i++)
            {
                string className = string.Format("GardeningSkeleton.Platform.SkillSpace.{0}Impact", data.impactType[i].ToString());
                impacts[i] = CreateObject<BaseImpactEffect>(className);
            }
            return impacts;
        }

        private static T CreateObject<T>(string className) where T : class // 创建对应算法
        {
            if (!typeCache.TryGetValue(className, out Type type))
            {
                // 如果缓存中没有该类型,反射获取并缓存
                type = Type.GetType(className);
                if (type != null)
                {
                    typeCache[className] = type;
                }
                else
                {
                    Debug.LogError($"Type {className} not found.");
                    return null;
                }
            }

            return Activator.CreateInstance(type) as T;
        }
    }
 public class SkillDeployer : MonoBehaviour
    {
        [HideInInspector] public SpriteRenderer sr;
        [HideInInspector] public Animator anim;
        [HideInInspector] public Rigidbody2D rb;

        protected SkillData skillData;
        public SkillData SkillData //技能管理器提供
        {
            get { return skillData; }
            set { skillData = value; InitDeplopyer(); }
        }
        //范围选择算法
        private ISkillSelector selector;
        //效果算法对象 
        private BaseImpactEffect[] impactArray;

        /// <summary>
        /// 初始化释放器  在设置技能时调用
        /// </summary>
        private void InitDeplopyer()//初始化释放器 
        {
            //范围选择
            selector = DeployerConfigFactory.CreateSkillSelector(skillData);
            //效果
            impactArray = DeployerConfigFactory.CreateImpactEffects(skillData);
        }
        //范围选择
        public void chooseTargets()
        {
            if (selector != null) skillData.attackTargets = selector?.SelectTarget(skillData, this.transform);
        }
        /// <summary>
        /// 执行所有效果, 自动赋值常用变量
        /// </summary>
        public void ImpactTargets()
        {
            for (int i = 0; i < impactArray.Length; i++)
            {
                impactArray[i].skillData = SkillData;
                impactArray[i].Execute(this);
            }
        }

        /// <summary>
        /// 结束效果, 实际是传递给所有效果
        /// </summary>
        public virtual void EndEffect()
        {
            for (int i = 0; i < impactArray.Length; i++)
            {
                impactArray[i].EndEffect();
            }
        }

        //供技能管理器调用,由子类实现,定义具体释放策略
        public virtual void DeploySkill()
        {
            //范围选择
            chooseTargets();
            //执行影响算法
            ImpactTargets();
        }

        #region  检测范围绘制
        public float rangeTemp;
        protected virtual void OnDrawGizmos()
        {
            // 绘制圆形
            Gizmos.color = Color.red; // 设置圆形颜色
            Gizmos.DrawWireSphere(transform.position, rangeTemp); // 绘制半径为 data.range 的圆形
        }

        #endregion

        #region  物理碰撞
        protected virtual void OnTriggerEnter2D(Collider2D other)
        {
            for (int i = 0; i < impactArray.Length; i++)
            {
                impactArray[i].OnImpactTriggerEntry(other);
            }
        }

        protected virtual void OnTriggerExit2D(Collider2D other)
        {
            for (int i = 0; i < impactArray.Length; i++)
            {
                impactArray[i].OnImpactTriggerExit(other);
            }
        }

        #endregion

        #region  触发伤害
        private bool isAttack = false;
        // 伤害的触发时机差异很大,不适合作为效果
        public virtual void AttackStart()
        {
            switch (skillData.disappearType)
            {
                case CalculDamageType.Once:
                    Attack();
                    break;
                case CalculDamageType.Continue:
                    if (isAttack) return;
                    isAttack = true;
                    ZTimer.SetInterval(skillData.attackInterval, Attack);
                    break;
            }
        }
        /// <summary>
        /// 敌人选取两种形式: 碰撞 、 射线检测
        /// </summary>
        /// <param name="other"></param>
        public virtual void AttackStart(Collider2D other)
        {
            AttackStart(other.transform);
        }
        public virtual void AttackStart(Transform other)
        {
            if (Tool.IsInLayerMask(other.gameObject, skillData.attackTargetLayers))
            {
                //这里区分单体攻击
                if (skillData.attackType == SkillAttackType.single)
                {
                    skillData.attackTargets.Clear();
                }
                skillData.attackTargets.Add(other);
            }
            AttackStart();
        }
        public virtual void AttackEnd()
        {
            isAttack = false;
            ZTimer.ClearTimer(Attack);
        }
        private void Attack()
        {
            if (skillData.attackTargets?.Count > 0)
            {
                foreach (var entity in skillData.attackTargets.ToArray())
                {
                    var enemyEntity = entity.GetComponent<Enemy>();
                    if (enemyEntity != null) enemyEntity.StartCoroutine(enemyEntity.FreezeTimeFor(skillData.freezeTime));

                    switch (skillData.damageType)
                    {
                        case DamageType.Normal:
                        case DamageType.Magic:
                            entity.GetComponent<CharacterStats>().BeDamage(OwnerEntity.stats, skillData.attackDamage, skillData.damageType);
                            break;
                        case DamageType.OwnerAttack:
                            //使用原始攻击力
                            entity.GetComponent<CharacterStats>().BeDamage(OwnerEntity.stats);
                            break;
                        case DamageType.OwnerMagic:
                            //使用原始魔法攻击+技能攻击
                            entity.GetComponent<CharacterStats>().BeMagicalDamage(OwnerEntity.stats, skillData.attackDamage);
                            break;
                    }
                }
            }
        }

        #endregion


        #region 简写
        public Entity OwnerEntity => skillData.owner.GetComponent<Entity>();
        public Vector3 OwnerPos => skillData.owner.transform.position;
        public int OwnerDamage => OwnerEntity.stats.damage.Value;

        public void Release()
        {
            PoolMgr.Instance.Release(this.gameObject);
        }


        #endregion
案例

以抛剑为例,剑需要回收,但又有投掷物特性,所以有继承

/// <summary>
    /// 抛物线技能释放器
    /// </summary>
    public class ParabolicSkillDeployer : SkillDeployer
    {
        public Player player;

        protected Collider2D cd;

        protected Vector2 finalDir;


        void Awake()
        {
            anim = GetComponentInChildren<Animator>();
            rb = GetComponent<Rigidbody2D>();
            cd = GetComponent<Collider2D>();
            // cd = GetComponent<CircleCollider2D>();
            player = PlayerMgr.Instance.player;
        }

        void Start()
        {
            player = PlayerMgr.Instance.player;
        }

        void Update()
        {

        }

        void OnEnable()
        {
            cd.enabled = true;
            rb.isKinematic = false;
            rb.constraints = RigidbodyConstraints2D.None;
        }

       
       
        // 由效果决定是否冻结
        public virtual void StopRigidbody()
        {
            cd.enabled = false;
            rb.isKinematic = true;
            // 冻结所有 自由度(位置和旋转)
            rb.constraints = RigidbodyConstraints2D.FreezeAll;
        }


        public override void DeploySkill()
        {
            finalDir = new Vector2(Tool.GetMouseDirection().normalized.x * skillData.launchForce.x,
                   Tool.GetMouseDirection().normalized.y * skillData.launchForce.y);
            rb.velocity = finalDir;
            rb.gravityScale = skillData.gravity;

            base.DeploySkill();
        }

        /// <summary>
        /// 让效果只依赖此接口
        /// </summary>
        public virtual void ReturnSword() { }
        public virtual void SetRotate(bool value) { }
    }
 /// <summary>
    /// 抛剑 释放器 
    /// </summary>
    public class ThrowSwordSkillDeployer : ParabolicSkillDeployer
    {
        private float returnSpeed = 20;
        public bool isReturning;
        /// <summary>
        /// 能够转向
        /// </summary>
        public bool canRotate = true;

        void Update()
        {
            if (canRotate)
                transform.right = -rb.velocity;

            if (isReturning)
            {
                transform.position = Vector2.MoveTowards(transform.position
                , player.transform.position, returnSpeed * Time.deltaTime);

                var distance = Vector2.Distance(transform.position, player.transform.position);
                if (distance < 0.1f)
                {
                    player.ChangeState(Entity.States.CatchSword);
                    DestroySword();
                }
            }
        }
        void DestroySword()
        {
            isReturning = false;
            if (player.sword = null) return;
            //再次关闭效果
            // EndEffect();  
            player.sword = null;
            // 回收预制体
            PoolMgr.Instance.Release(skillData.skillPrefab);
        }


        protected override void OnTriggerEnter2D(Collider2D other)
        {
            if (isReturning) return;
            base.OnTriggerEnter2D(other);
        }


        public override void DeploySkill()
        {
            base.DeploySkill();
            canRotate = true;
            isReturning = false;
            player.sword = gameObject;

            // 距离过远超过时间自动销毁
            // 创建太快多次调用会 有重复销毁bug
            ZTimer.ClearTimer(DestroySwordByDistance);
            ZTimer.SetTimeout(5f, DestroySwordByDistance);
        }

        private void DestroySwordByDistance()
        {
            var distance = Vector2.Distance(transform.position, player.transform.position);
            if (distance > 15f)
            {
                DestroySword();
            }
        }

        public override void EndEffect()
        {
            ZTimer.ClearTimer(DestroySwordByDistance);
            base.EndEffect();
        }

        /// <summary>
        /// 返回剑
        /// </summary>
        public override void ReturnSword()
        {
            // deployer.rb.isKinematic = false;//受物理控制
            rb.constraints = RigidbodyConstraints2D.FreezeAll;
            transform.parent = null;
            isReturning = true;
        }

        public override void SetRotate(bool value)
        {
            base.SetRotate(value);
            canRotate = value;
        }


        public override void StopRigidbody()
        {
            base.StopRigidbody();
            canRotate = false;
        }
    }

技能效果

 public interface IImpactEffect //效果算法接口
    {
        void Execute(SkillDeployer baseDeployer);
        void EndEffect();
        
    }
 public abstract class BaseImpactEffect : IImpactEffect
    {
        public SkillData skillData;

        public abstract void EndEffect();

        public abstract void Execute(SkillDeployer baseDeployer);

        public virtual void OnImpactTriggerEntry(Collider2D other){}
        public virtual void OnImpactTriggerExit(Collider2D other){}

        void AddUpdateEvent(UnityAction action)
        {
            MonoMgr.Instance.AddUpdateEvent(action);
        }

        void RemoveUpdateEvent(UnityAction action)
        {
            MonoMgr.Instance.RemoveUpdateEvent(action);
        }


        #region 简写
        public Entity OwnerEntity => skillData.owner.GetComponent<Entity>();
        public Vector3 OwnerPos => skillData.owner.transform.position;
        public int OwnerDamage => OwnerEntity.stats.damage.Value;
        #endregion
    }
以穿刺效果为例
 public class PierceImpact : BaseImpactEffect
    {
        ParabolicSkillDeployer deployer;

        public override void EndEffect()
        {
            deployer.ReturnSword();
        }

        public override void Execute(SkillDeployer baseDeployer)
        {
            deployer = (ThrowSwordSkillDeployer)baseDeployer;
            // MonoMgr.Instance.AddUpdateEvent(PierceUpdate);
        }

        public override void OnImpactTriggerEntry(Collider2D other)
        {
            deployer.AttackStart(other);
            if (skillData.pierced < skillData.pierceCount && other.gameObject.layer == LayerMask.NameToLayer("Enemy"))
            {
                skillData.pierced++;
                return;
            }
            skillData.pierced = 0;

            deployer.StopRigidbody();
            deployer.transform.parent = other.transform;
        }

        private void PierceUpdate()
        {

        }

技能选择器

以圆形范围为例

 public interface ISkillSelector //范围选择算法接口
    {
        HashSet<Transform> SelectTarget(SkillData skillData, Transform skillPrefab);//skillTF是技能预制体
    }

public class CircleSkillSelector : ISkillSelector
    {
        public HashSet<Transform> SelectTarget(SkillData data, Transform skillTF)
        {
            HashSet<Transform> taragets = new();
            Collider2D[] colliders = Physics2D.OverlapCircleAll(skillTF.position, data.range, data.attackTargetLayers);

            for (int i = 0; i < colliders.Length; i++)
            {
                taragets.Add(colliders[i].transform);
            }

            if (taragets.Count == 0)
            {
                // Debug.Log("没有敌人");
                return taragets;
            }
            else
            {
                for (int i = 0; i < taragets.Count; i++)
                {
                    // Debug.Log("敌人" + res[i].name);
                }
                return taragets;
            }
            // return res;
        }
#####

技能指示器

打开指示器和释放技能的逻辑是分开的
案例是绘制原点虚线

 public class BaseSkillIndicator : MonoBehaviour
    {
        public SkillData skillData;
        public Entity entity;

        public virtual void Show()
        {
        }
        public virtual void Hide(){}

    }

public class DottedIndicator : BaseSkillIndicator
    {


        [Header("点抛物线")]
        [SerializeField] private int numberOfDots = 20;
        [SerializeField] private float spaceBetweenDots = 0.07f;
        // [SerializeField] private GameObject dotPrefab;
        // [SerializeField] private Transform dotsParent;
        private GameObject[] dots;

        void Awake()
        {
            GeneraeteDots();
        }

        void Update()
        {
            // if (PlayerInput.Instance.HasUseSkillInput)
            // {
            for (int i = 0; i < numberOfDots; i++)
            {
                dots[i].transform.position = DotsPosition(i * spaceBetweenDots);
            }
            // }
        }

做一个简单的Unity Buff(增益效果)系统,首先需要考虑的是设计模块化和可复用的结构。以下是步骤和关键点: 1. 定义Buff接口:创建一个`IBuff`接口,包含基础属性如`ID`, `Name`, 和生命周期管理函数`Apply()`和`Remove()`。 ```csharp public interface IBuff { int ID { get; } string Name { get; } void Apply(); void Remove(); } ``` 2. 实现基础buff类:比如`BaseBuff`作为所有buff的基础类,继承自`MonoBehaviour`并实现`IBuff`接口。 ```csharp public class BaseBuff : MonoBehaviour, IBuff { public int ID; public string Name; public virtual void Apply() { // 简单的添加状态提示或数据变化 } public virtual void Remove() { // 清除状态或数据恢复 } } ``` 3. 具体增益类:例如`HealBuff`、`SpeedBoostBuff`等,继承自`BaseBuff`并在`Apply()`和`Remove()`中提供具体的增强功能。 ```csharp public class HealBuff : BaseBuff { public float HealAmount; //... } ``` 4. 使用和管理buff:创建一个`BuffManager`类负责配、存储和更新buff,可以将`IBuff`实例添加到列表或字典中,并在需要的地方应用和移除。 ```csharp public class BuffManager : MonoBehaviour { private Dictionary<int, IBuff> buffs = new Dictionary<int, IBuff>(); public void AddBuff(IBuff buff) { buffs.Add(buff.ID, buff); } public void ApplyBuff(int id) { if (buffs.ContainsKey(id)) buffs[id].Apply(); } public void RemoveBuff(int id) { if (buffs.ContainsKey(id)) buffs[id].Remove(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值