首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-02-18 12:01
  • 4676
  • 11750
blog.csdn.net

前言

alex教程观后感

关于耦合

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

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

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

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

正文

因为buff系统比较简单,就放在前面
2024.12.1补充:过于简单仅适合参考或者原型开发

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
对应枚举
public enum BuffType
    {
        None,
        Ignited,//被点燃
        Frozen,//被冻结
        Shocked,//被震撼
    }
    public enum BuffOverlap //叠加类型
    {
        None,
        StackedTime,//增加时间
        StackedLayer,//增加层数
        ResterTime,//重置时间
    }
    public enum BuffCloseType
    {
        All,//全部关闭
        Layer,//逐层关闭
    }
    public enum BuffCalculateType//执行类型 
    {
        Once,//一次
        Loop,//每次
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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);
            }
        }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

在角色类添加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();
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
燃烧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();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

技能系统

主要参考: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;//攻击目标

    /// 
    /// 攻击的冻结时间
    /// 
    [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;

    /// 
    /// 连击技能 处理传送
    /// 
    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;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
对应枚举
 // 数字对应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,
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

核心管理器

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("/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();
            }
        }

        /// 
        /// 技能通过动画调用就不会走这里
        /// 
        /// 
        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;
        }

        /// 
        /// 生成技能
        /// 
        /// 
        /// 技能生成和目标位置
        /// 
        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;
            }
        }

        /// 
        /// 装填技能预制体
        /// 
        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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289

技能释放器

释放器工厂
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;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
 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;

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

        /// 
        /// 结束效果, 实际是传递给所有效果
        /// 
        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;
            }
        }
        /// 
        /// 敌人选取两种形式: 碰撞 、 射线检测
        /// 
        /// 
        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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
案例

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

/// 
    /// 抛物线技能释放器
    /// 
    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();
            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();
        }

        /// 
        /// 让效果只依赖此接口
        /// 
        public virtual void ReturnSword() { }
        public virtual void SetRotate(bool value) { }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
 /// 
    /// 抛剑 释放器 
    /// 
    public class ThrowSwordSkillDeployer : ParabolicSkillDeployer
    {
        private float returnSpeed = 20;
        public bool isReturning;
        /// 
        /// 能够转向
        /// 
        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();
        }

        /// 
        /// 返回剑
        /// 
        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;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

技能效果

 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
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
以穿刺效果为例
 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()
        {

        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

技能选择器

以圆形范围为例

 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;
        }
#####
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

技能指示器

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

 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);
            }
            // }
        }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
注:本文转载自blog.csdn.net的_时侍的文章"https://blog.csdn.net/zhuanggenhua/article/details/142358215"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

134
游戏
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top