草庐IT

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

酒九5 2023-07-11 原文

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

素材准备工作做好之后我们就可以开始编写具体的脚本了。新建好我们的项目,场景搭建和角色素材这些操作就大家自己去选择准备吧。新建一个文件夹命名为SkillSystem,之后我们技能系统所有的脚本就放在这里面了(养成收拾脚本的习惯)。

PS:接下来的所有脚本我们都统一放进一个命名空间中,我这里命名为MOBASkill,后面代码中就不单独写出来。

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

2、技能数据类(SkillData)
我们新建脚本命名为SkillData,将我们技能中需要的数据都写在这个数据类中,这里可以大家根据自己的设计想法来进行添加,这里我就不过多啰嗦我们看着代码讲。

   public enum SkillAttackType
    {
        single,
        aoe,
    }
    public enum SelectorType
    {
        none,
        Sector,
        Rectangular,
    }   
    public enum DisappearType 
    {
        TimeOver,
        CheckOver,
    }

    [Serializable]
    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
        [HideInInspector]
        public Transform[] attackTargets;//作用目标对象数组
        public string[] impactType = { "CostMP", "Damage" };//技能影响类型
        public int nextBatterld;//连击的技能ID
        public float attackNum;//伤害数值
        public float durationTime;//持续时间
        public float attackInterval;//伤害间隔
        [HideInInspector]
        public GameObject owner;//技能所属的角色
        public string prefabName;//技能预制体名称
        [HideInInspector]
        public GameObject skillPrefab;//预制体对象
        public string animationName;//动画名称
        public string hitFxName;//受击特效名称
        [HideInInspector]
        public GameObject hitFxPrefab;//受击特效预制体
        public int level;//技能等级
        public SkillAttackType attackType;//AOE或者单体
        public SelectorType selectorType;//释放范围类型(圆形,扇形,矩形)
        public string skillIndicator;//技能指示器名字
        public string skillIconName;//技能显示图标名字
        [HideInInspector]
        public Image skillIcon;//技能事件图标
        public DisappearType disappearType;//技能预制体消失方式
    }

这里有几个变量的作用需要单独说一说他的作用:

1.attackTargetTags和attackTargets,看上去这两个变量的作用是一样的,但是前者是在释放技能之前该技能可以作用于那些物体上(敌人,队友,建筑等),后者是在执行技能范围选择算法之后返回的在技能范围中的敌人(实际攻击到的目标)。

2.owner在这里其实就是挂载管理器的物体自己,在后续算法中可以更方便的调用本身的数据,方法或者组件等。

3.skillIndicator是我们成功释放技能之前显示出来的辅助技能释放的工具。

PS:通常我们定义了公有变量之后就会在Inspector窗口中生成对应的输入框,而当我们在变量前加上[HideInInspector]之后就会该公有变量就不会在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[data.impactType.Length];
            for (int i = 0; i < data.impactType.Length; i++)
            {
                string classname = string.Format("MOBASkill.{0}Impact", data.impactType[i]);
                impacts[i] = 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[i].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[i]);
            }
            for (int i = 0; i < tempGOArray.Length; i++)
            {
                taragets.Add(tempGOArray[i].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[i].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[0].currentTarget = cd;
            cd.AddBuff(BuffManager.instance.AllBuffs[0]);
        }
        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中的内容啦。

好啦,本篇文章的内容就到这里啦,大家伙下篇文章见哦。拜拜~

有关在Unity中制作完整的技能系统(代码篇)的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  3. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  4. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  5. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  6. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  7. 程序员如何提高代码能力? - 2

    前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源

  8. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  9. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  10. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

随机推荐