FeastSC 发表于 2022-6-3 10:36

在Unity中制作完整的技能系统(代码篇)

哈喽~又是我暴躁老哥酒九。
上期文章向大家介绍了一下技能系统的思路和使用方法。那么话不多说,接下来就是有关这些功能都是如何实现的。整整整。


场景搭建和角色素材这些操作大家就自己去选择准备吧。素材准备工作做好之后,我们就可以开始编写具体的脚本了。
新建项目,新建一个文件夹命名为SkillSystem,之后我们技能系统所有的脚本就放在这里面了(养成收拾脚本的好习惯)。
PS:接下来的所有脚本我们都统一放进一个命名空间中,我这里命名为MOBASkill,后面代码中就不单独写出来了。
1、系统总体规划

我们再来捋一捋技能系统的各个模块之间是怎么互相作用的。
每一个拥有技能的角色身上,都会有一个技能管理器负责该角色的技能数据存放和生成对应技能的预制体。在我们生成技能预制体(释放技能)的同时,对应的技能预制体身上挂载好的技能释放器就会在释放器配置工厂中找到对应的效果算法和目标选择算法,并且执行算法中的代码,完成查找敌人并对敌人产生对应的效果,最后销毁预制体本身。
这就是一次完整的技能释放流程。
2、技能数据类(SkillData)

我们新建脚本命名为SkillData,将技能中需要的数据都写在这个数据类中。大家可以根据自己的设计想法来添加,我就不过多啰嗦,看着代码讲。
    public enum SkillAttackType
    {
      single,
      aoe,
    }
    public enum SelectorType
    {
      none,
      Sector,
      Rectangular,
    }   
    public enum DisappearType
    {
      TimeOver,
      CheckOver,
    }

   
    public class SkillData
    {
      public int skillId;//技能ID
      public string name;//技能名称
      public string description;//技能描述
      public int skillCd;//技能冷却时间
      public int cdRemain;//技能剩余冷却时间
      public int costMp;//法力值消耗
      public float attackDistance;//技能距离
      public float attackAngle;//技能攻击角度
      public string[] attackTargetTags = { "Enemy" };//能作用的目标Tag
      
      public Transform[] attackTargets;//作用目标对象数组
      public string[] impactType = { "CostMP", "Damage" };//技能影响类型
      public int nextBatterld;//连击的技能ID
      public float attackNum;//伤害数值
      public float durationTime;//持续时间
      public float attackInterval;//伤害间隔
      
      public GameObject owner;//技能所属的角色
      public string prefabName;//技能预制体名称
      
      public GameObject skillPrefab;//预制体对象
      public string animationName;//动画名称
      public string hitFxName;//受击特效名称
      
      public GameObject hitFxPrefab;//受击特效预制体
      public int level;//技能等级
      public SkillAttackType attackType;//AOE或者单体
      public SelectorType selectorType;//释放范围类型(圆形,扇形,矩形)
      public string skillIndicator;//技能指示器名字
      public string skillIconName;//技能显示图标名字
      
      public Image skillIcon;//技能事件图标
      public DisappearType disappearType;//技能预制体消失方式
    }
这里有几个变量需要单独说一说作用:
1.attackTargetTags和attackTargets,咋一看这两个变量的作用似乎是一样的,但前者表示的是在释放技能之前该技能可以作用于哪些物体上(敌人,队友,建筑等),后者则会在执行技能范围选择算法之后返回在技能范围中的敌人(实际攻击到的目标)。
2.owner这其实就是挂载管理器的物体自己,在后续算法中可以更方便的调用本身的数据,方法或者组件等。
3.skillIndicator是成功释放技能之前显示出来的辅助技能释放的工具。
PS:通常我们定义了公有变量之后就会在Inspector窗口中生成对应的输入框,而当在变量前加上之后就会该公有变量就不会在Inspector中显示。
3、技能管理器(SkillManager)

技能管理器的作用就是存好人物的所有技能数据,初始化技能数据中未填写的部分(owner,skillPrefab,hitFxPrefab),准备技能,生成技能等。因此,首先我们需要准备一个数组来存放人物的全部技能,根据数据中的预制体名字(prefabName,hitFxName)给对应的GameObject赋值的方法,生成技能时各种内容等。来看看代码。
public class CharacterSkillManager : MonoBehaviour
    {
      public SkillData[] Skills;//技能列表

      private void Awake()
      {
            foreach (var s in Skills)
            {
                InitSkill(s);
            }
      }
      //初始化技能
      private void InitSkill(SkillData data)
      {
            if (data.prefabName != null)
            {
                data.skillPrefab = Resources.Load<GameObject>("SkillPrefab/"+data.prefabName);
                data.owner = this.gameObject;
            }
      }
      //技能释放条件判断
      public SkillData PrepareSkill(int id)
      {
            SkillData data = new SkillData();
            foreach (var s in Skills)
            {
                if (s.skillId == id)
                {
                  data = s;
                }
            }
            if (data != null && data.cdRemain <= 0)//这里还有技能消耗值的判断
            {
                return data;
            }
            else
            {
                return null;
            }
      }
      //生成技能
      public void GenerateSkill(SkillData data)
      {
            //创建技能预制体
            GameObject skillgo = GameObjectPool.instance.CreateObject(data.prefabName, data.skillPrefab, transform.position, transform.rotation);
            //传递技能数据给技能释放器
            SkillDeployer deployer = skillgo.GetComponent<SkillDeployer>();
            deployer.SkillData = data;
            //释放器释放技能
            deployer.DeploySkill();
            StartCoroutine(CoolTimeDown(data));//开启冷却
      }
      //协程实现技能冷却
      private IEnumerator CoolTimeDown(SkillData data)
      {
            data.cdRemain = data.skillCd;
            while (data.cdRemain > 0)
            {
                yield return new WaitForSeconds(1f);
                data.cdRemain -= 1;
            }
      }
    }
这里对于能否释放技能的条件有:
该技能ID是否存在;
技能的是否完成冷却(利用剩余冷却时间是否为0来判断);
技能所需的消耗值玩家的对应数值是否足够。
大伙儿可以根据自己的需求来添加删除条件。举个例子:一个技能拥有三段释放,那么就可以将“上一段技能是否释放”来作为条件实现。生成技能这里我使用了对象池,大家也可以改成普通的实例化。后面的技能冷却则是通过协程实现的。
4、释放器配置工厂(DeployerConfigFactory)

根据之前的技能流程图,我们可以发现:在技能管理器中释放技能的操作其实只有“实例化预制体”和“将技能数据传入释放器中”,而实际的选取敌人、执行技能效果都是在释放器中来完成的。那技能释放器是怎么知道要用哪些算法的呢?我们可以看到技能数据中有两个变量impactType和selectorType。这两个变量中都存放的是字符串内容,那么我们就通过这些字符串内容来获取到对应的算法。这些操作由释放器配置工厂来完成。
先准备好技能范围选择算法和效果算法的接口:
    public interface IImpactEffect //效果算法接口
    {
      void Execute(SkillDeployer deployer);
    }   
    public interface ISkillSelector //范围选择算法接口
    {
      Transform[] SelectTarget(SkillData data, Transform skillTF);//skillTF是技能预制体
    }
接下来所有的相关算法都是先继承这两个接口,再编写具体的逻辑。
现在来看释放器配置工厂。
public class DeployerConfigFactory//反射来实现
    {
      public static ISkillSelector CreateSkillSelector(SkillData data)//范围选择算法
      {
            string className = string.Format("MOBASkill.{0}SkillSelector", data.selectorType);
            return CreateObject<IAttackSelector>(className);
      }
      public static IImpactEffect[] CreateImpactEffects(SkillData data) //效果算法
      {
            IImpactEffect[] impacts = new IImpactEffect;
            for (int i = 0; i < data.impactType.Length; i++)
            {
                string classname = string.Format("MOBASkill.{0}Impact", data.impactType);
                impacts = CreateObject<IImpactEffect>(classname);
            }
            return impacts;
      }
      private static T CreateObject<T>(string className) where T : class//创建对应算法
      {
            Type type = Type.GetType(className);
            return Activator.CreateInstance(type) as T;
      }
    }
在类中我们用CreateObject方法来根据字符串变量找到对应的算法,主要操作就是使用泛型(T)并且将我们的泛型约束为引用类型(where T : class),根据前文提到的算法的字符串变量的Type来创建对应类型的方法。而CreateAttackSelector和CreateImpactEffects两个方法就是把算法名补全之后返回指定接口类型的方法。
PS:这里的Activator.CreatInstance作用是用与指定参数匹配程度最高的构造函数来创建指定类型的实例。通俗的讲就是按照参数给出方法,不过它返回的是object类型,所以要转化成我们需要的类型(as T)。
5、技能释放器(SkillDeployer)

解决了根据数据生成算法的问题之后,在技能释放器中就只需要初始化释放器算法并且调用相应的算法就好了,代码如下:
public abstract class SkillDeployer : MonoBehaviour//技能释放器
    {
      private SkillData skillData;
      public SkillData SkillData //技能管理器提供
      {
            get { return skillData; }
            set { skillData = value; InitDeplopyer(); }
      }
      //范围选择算法
      private IAttackSelector selector;
      //效果算法对象
      private IImpactEffect[] impactArray;
      //初始化释放器
      private void InitDeplopyer()//初始化释放器
      {
            //范围选择
            selector = DeployerConfigFactory.CreateAttackSelector(skillData);
            //效果
            impactArray = DeployerConfigFactory.CreateImpactEffects(skillData);
      }
      //范围选择
      public void CalculateTargets()
      {
            skillData.attackTargets = selector.SelectTarget(skillData, this.transform);
      }
      //效果
      public void ImpactTargets()
      {
            for (int i = 0; i < impactArray.Length; i++)
            {
                impactArray.Execute(this);
            }
      }
      public abstract void DeploySkill();//供技能管理器调用,由子类实现,定义具体释放策略
      
    }
这里释放器就可以作为所有释放器的父类,如果有释放情况的不同,就继承他在子类中进行重写方法。比如前文提到的:近战技能释放是如何释放,远程技能释放又是怎样的等等,这里就交给大家自己完成啦。
public class MeleeSkillDeployer : SkillDeployer//近战技能释放例子
    {
      public override void DeploySkill()
      {
            //执行选区算法
            CalculateTargets();
            //执行影响算法
            ImpactTargets();
      }
    }
6、技能范围选择算法(Selector)

顾名思义,这里要完成的内容就是将在规定范围中的敌人全部选择出来。给大家演示一下扇形范围的敌人检测。上代码。
public class SectorSkillSelector : ISkillSelector
    {
      GameObject[] tempGOArray;
      public Transform[] SelectTarget(SkillData data, Transform skillTF)
      {
            //根据技能数据中得标签 获取所有目标
            List<Transform> taragets = new List<Transform>();
            for (int i = 0; i < data.attackTargetTags.Length; i++)
            {
                tempGOArray = GameObject.FindGameObjectsWithTag(data.attackTargetTags);
            }
            for (int i = 0; i < tempGOArray.Length; i++)
            {
                taragets.Add(tempGOArray.GetComponent<Transform>());
            }
            //判断攻击范围
            taragets = taragets.FindAll(t =>
            Vector3.Distance(t.position, skillTF.position) <= data.attackDistance &&
            Vector3.Angle(skillTF.forward, t.position - skillTF.position) <= data.attackAngle / 2
            );
            //返回目标
            Transform[] result = taragets.ToArray();
            if (result.Length == 0)
            {
                Debug.Log("没有敌人");
                return result;
            }
            else
            {
                for (int i = 0; i < result.Length; i++)
                {
                  Debug.Log(result.name);
                }
                return result;
            }
      }
    }
说明一下整体的流程。首先我们根据技能数据中的attackTargetTags中的Tag找到所有带有该Tag的物体,再通过一个列表的FindAll方法来找符合条件的敌人,再返回一个数组到技能数据的attackTargets中去。这样就完成了敌人的查找。
PS:1.脚本名是一定要与释放器配置工厂中的命名规则一致,否则无法找到。在这里范围选择算法名为xxxSkillSelector,而效果算法名应该为xxxImpactEffect。2.在FindAll中使用了Lamda表达式来完成条件的限制,Vector3.Distance负责在攻击距离内,Vector3.Angle负责在攻击角度内。3.这里还需要大家自己添加上敌人是否存活的选择条件判断、技能目标为单个或多个的返回情况等。
7、技能效果算法(Impact)

技能的效果就有很多了。可以是回复血量,消耗法力值,减少血量等等,大家可以根据自己的需求来自行编写。这里的例子是给敌人添加一个击飞Buff。
public class BlowFlyImpact : IImpactEffect
    {
      private SkillData data;
      public void Execute(SkillDeployer deployer)
      {
            data = deployer.SkillData;
            deployer.StartCoroutine(ContinuousBlowFly(deployer));
      }
      public void BlowFly(Transform transform)//给敌人添加Buff
      {
            CharacterData cd = transform.GetComponent<CharacterData>();
            BuffManager.instance.AllBuffs.currentTarget = cd;
            cd.AddBuff(BuffManager.instance.AllBuffs);
      }
      IEnumerator ContinuousBlowFly(SkillDeployer deployer) //每隔0.05秒检测一次敌人
      {
            float time = 0;
            do
            {
                yield return new WaitForSeconds(0.05f);
                time += 0.05f;
                deployer.CalculateTargets();
                if (data.attackTargets.Length != 0)
                {
                  foreach (var t in data.attackTargets)
                  {
                        BlowFly(t);
                  }
                }
            } while (time < 0.4f);
      }
    }
为什么要用协程来持续检测敌人呢?
因为这个技能是一个冲撞技能,在玩家向前位移的整个时间范围内检测到的敌人都会被添加击飞的Buff。BuffManager是一个简单Buff系统中的管理器,这里不必深究。当释放技能时就会调用Execute中的内容。
好了,今天的教程就暂时讲到这里~
<hr/>皮皮关与网易联合开发了完备的游戏开发线上课程。想要进军游戏开发领域的童鞋,可戳下面链接了解课程详情:
如果有任何问题,还可以通过以下入口与网易小伙伴取得联系,有什么问题直接正面怼他(战术后退
同时,欢迎加入游戏开发群欢乐搅基:1082025059

XGundam05 发表于 2022-6-3 10:40

这也太难了

TheLudGamer 发表于 2022-6-3 10:48

这套 做不了闪电链吧 [害羞]

yukamu 发表于 2022-6-3 10:55

可以,可以,必须点赞!
页: [1]
查看完整版本: 在Unity中制作完整的技能系统(代码篇)