三.技能系统 [Unity_Learn_RPG_1]

三.技能系统

一.技能系统架构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

excel转xml

直接excel就能转= =
在这里插入图片描述

二.技能系统管理类和Unity事件

1.Unity事件

老师说,开发的时候基本不用搞这些 = =。

1).VRTK UI是如何拓展UGUI事件的

在这里插入图片描述

  1. 使用 UIEventListener 继承 UGUI 的各种接口类
  2. 根据接口实现需要的代理。这里是要传递数据,根据接口给的数据,分为几种不同的代理。
  3. 根据接口实现需要的事件
  4. 需要监听的地方注册事件

2).EasyTouch 中是如何实现的

在这里插入图片描述
在这里插入图片描述

  1. 使用不同的功能类继承UnityEvent
  2. 生成不同功能类的对象
  3. 需要监听的地方注册事件

3).为啥Unity用UnityEvent来做,而不是用委托呢?

在这里插入图片描述
主要就是因为UnityEvent可以在编辑器中显示和设置,直接用C#的委托则显示不出来。UnityEvent再深入的代码我们就看到不到了。

个人还是更喜欢用委托的方法。

4).给EasyTouch的某个事件增加返回参数

在这里插入图片描述
在这里插入图片描述
并且我们发现,UnityEvent 这个类最多可以传递4个参数,T0-T3。包括没有参数的,一共又五种可选。

2.可空类型(复习)

C# 可空类型(Nullable)
在这里插入图片描述

3.相关代码

1).SkillData

    [System.Serializable]
    public class SkillData {
        //技能Id
        public int skillId;
        //技能名称
        public string name;
        //技能描述
        public string description;
        //冷却时间
        public int coolTime;
        //冷却剩余
        public int coolRemain;
        //魔法消耗
        public int costSP;
        //攻击距离
        public float attackDistance;
        //攻击角度
        public float attackAngle;
        //攻击目标,通过 tag 分辨
        public string[] attackTargetTags = { "Enemy" };
        //攻击目标对象数组
        [HideInInspector]
        public Transform[] attackTargets;
        //技能影响类型。根据字符串,反射对象
        public string[] impactType = { "CostSP", "Damage" };
        //连击的下一个技能编号
        public int nextBatterId;
        //伤害比率
        public float atkRatio;
        //持续时间
        public float durationTime;
        //伤害间隔
        public float atkInterval;
        //技能所属
        [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;
        //选择类型 扇形(圆形),矩形
        public SelectorType selectorType;
    }
a.SelectorType
[System.Serializable]
public enum SelectorType {
    Sector = 0,
    Rectangle,
}
b.SkillAttackType
[System.Serializable]
public enum SkillAttackType {
    Single = 0,
    Group,
}

三.资源映射表

Q:资源路径需要拼接,如果更改的话怎么修改代码内的路径?

1.生成资源映射表

老师的做法是将资源路径做成一张表,用变量名指代资源路径,并且打包的时候不会随之带走。
在这里插入图片描述
先给这种类型的代码专门新建一个 Editor 文件夹

1).AssetDatabase

Untiy为了方便编译器开发,还提供了一些只在编辑器下可以使用的类(一般都是静态类),打包之后是用不了的。

这里用到的是AssetDatabase
在这里插入图片描述

2).GenerateResConfig

a.功能
  1. 编译器类:继承自Editor类,只需要在Unity编译器中执行的代码

  2. 菜单项,特性[MenuItem(“…”)]:用于修饰需要在Unity编译器中产生菜单按钮的方法

  3. AssetBase:只适用于编译器中执行

  4. StreamingAssets:Unity特殊目录之一,存放需要在程序运行时读取的文件,该目录中的文件不会被压缩。

    1. 适合在移动端读取资源(在PC端可以写入,其他只读)。

    2. Application.persistentDataPath(持久化路径)。

      • 支持运行的时候进行读写操作;
      • 只能在运行的时候操作,在Unity编译器流程下是不行的;
      • Application.persistentDataPath 不是工程内部的路径,外部路径(安装程序时才产生,其实就是看这是什么系统,不同系统有不同的固定路径)

Q1:如果在非PC端要读写 StreamingAssets 下的文件时怎么办?
A1:第一次运行时,把 StreamingAssets 下的文件拷贝到 Application.persistentDataPath,之后只用 Application.persistentDataPath 下的文件即可。

Q2:那么为什么一定要用 Application.persistentDataPath 呢?
A1:

b.代码
	using System.IO;
	using UnityEngine;
	using UnityEditor;

	public class GenerateResConfig : Editor {
    //菜单项。这个方法可以在编辑器的 Tools->Resources->Generate Resoutce Config 直接使用
    [MenuItem("Tools/Resources/Generate Resoutce Config")]
    public static void Generate() {
        //生成资源配置文件
        //1.查找 Resources 目录下所有预制件完整路径
        //resFiles 里是 GUID
        string[] resFiles = AssetDatabase.FindAssets("t:prefab", new string[] { "Assets/Resources"} );
        for(int i = 0;i < resFiles.Length;i++) {
            resFiles[i] = AssetDatabase.GUIDToAssetPath(resFiles[i]);
            //2.生成对应关系
            //  名称 = 路径
            string fileName = Path.GetFileNameWithoutExtension(resFiles[i]);
            string filePath = resFiles[i].Replace("Assets/Resources/", string.Empty).Replace(".prefab", string.Empty);
            resFiles[i] = fileName + "=" + filePath;
        }

        //3.写入文件
        //StreamingAssets 也是Unity中的特殊目录。还有 Resources, Script/Editor
        //如果想运行的时候读取某个文件,且兼容各个平台,得放入 StreamingAssets 里
        File.WriteAllLines("Assets/StreamingAssets/ConfigMap.txt", resFiles);

        //刷新,不写Unity内的资源目录不会立马显示这个新文件
        AssetDatabase.Refresh();
    }
}
c.script/editor 下的所有文件的位置

我们可以很直观的看到,在 script/editor 下的文件和正常文件是不在一起的。
在这里插入图片描述
如下图,他们是放在不同的 dll 里的。并且打包的时候不会把 editor 放进去。
在这里插入图片描述

2.使用资源映射表

ResourceManager.cs

    public class ResourceManager {
        static Dictionary<string, string> configMap;

        static ResourceManager() {

            // 加载文件
            string fileContent = GetConfigFile("ConfigMap.txt");

            // 解析文件(string --> Dictionary<string, string>)
            BuildMap(fileContent);
        }
        
        public static string GetConfigFile(string fileName) {
            string url;

            //if(Application.platform == RuntimePlatform.WindowsEditor)

#if UNITY_EDITOR || UNITY_STANDALONE
            url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IOS
    url = "file://" + Application.dataPath + "/Raw/"+ fileName;
#elif UNITY_ANDROID
    url = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
#endif
            // 本地 new WWW("file:");
            // 网络 new WWW("http:");  http:// https://
            WWW www = new WWW("url");
            // 1.加载文件太大,一次加载不完,需要循环判断,说明www这东西是多线程的
            while (true) {
                if(www.isDone)
                return www.text;
            }
            
        }

        private static void BuildMap(string fileContent) {
            configMap = new Dictionary<string, string>();
            //文件名=路径\r\n文件名=路径
            //fileContent.Split();
            //StringReader 字符串读取器,提供了逐行读取字符串功能
            using (StringReader reader = new StringReader(fileContent)) {
                string line = reader.ReadLine();
                while (line != null) {
                    string[] keyValue = line.Split('=');
                    configMap.Add(keyValue[0], keyValue[1]);
                    line = reader.ReadLine();
                }
                // 文件名 0, 路径 1
            }
        }

        public static T Load<T>(string prefabName) where T:Object {
            //prefabName -> prefabPath
            string prefabPath = configMap[prefabName];
            return Resources.Load<T>(prefabPath);
        }
    }

1).静态构造函数

    public class ResourceManager {
        static ResourceManager() {

            // 加载文件
            string fileContent = GetConfigFile("ConfigMap.txt");

            // 解析文件(string --> Dictionary<string, string>)
            BuildMap(fileContent);
        }

作用:初始化类的静态成员数据
时机:只会调用一次。类在加载时执行一次,就是第一次使用类名的时候。

2).读取 StreamingAssets

StreamingAssets里,只能用以下方式读取

string url = "file:" + Application.streamingAssetsPath + "/ConfigMap.txt";
WWW www = new WWW("url");
// 加载文件太大,一次加载不完,需要循环判断,说明www这东西是多线程的
while (true) {
   if(www.isDone)
   return www.text;
}

好像是说 2019 之后的版本彻底废弃了 WWW, 改为使用 UnityWebRequest

3).不同平台读取 StreamingAssets 下的文件

直接使用 Application.streamingAssetsPath 有可能在不同平台上可能会读不到。

 string url = "file:" + Application.streamingAssetsPath + "/ConfigMap.txt";

不同平台的位置是不一样的

		    string url;
		
		    // 工作当中一般不用这种判断,不然每次都要判断
		    // 用宏来判断,因为打包的时候,就只有属于那个平台的代码
		    //if(Application.platform == RuntimePlatform.WindowsEditor)
		    
#if UNITY_EDITOR || UNITY_STANDALONE
            url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IOS
    url = "file://" + Application.dataPath + "/Raw/"+ fileName;
#elif UNITY_ANDROID
    url = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
#endif

4).StringReader

    private static void BuildMap(string fileContent) {
        configMap = new Dictionary<string, string>();
        //文件名=路径\r\n文件名=路径
        //fileContent.Split();
        //StringReader 字符串读取器,提供了逐行读取字符串功能
        using (StringReader reader = new StringReader(fileContent)) {
            string line = reader.ReadLine();
            while (line != null) {
                string[] keyValue = line.Split('=');
                configMap.Add(keyValue[0], keyValue[1]);
                line = reader.ReadLine();
            }
            // 文件名 0, 路径 1
        }
    }

StringReader 字符串读取器,提供了逐行读取字符串功能。

  1. 当程序调用 using 代码块,将自动调用 reader.Dispose() 方法,否则我们得手动调用一次
  2. 如果异常,则程序会立即中断,那么就执行不了 Dispose 方法了。如果我们使用using,即使代码块异常,也会调用 Dispose 方法

如果读取更复杂的文件,把每行处理的代码更改一下即可。

四.对象池

在这里插入图片描述
GameObjectPool.cs

   /// <summary>
    /// 使用方式:
    /// 1.所有频繁创建/销毁的物体,都通过对象池创建/回收
    /// 2.需要通过对象池创建的物体,如需每次创建时执行,则让脚本实现  IGameObjectPoolReset 接口
    /// </summary>
    public interface IGameObjectPoolReset{
        void OnReset();
    }

    public class GameObjectPool :MonoSingleton<GameObjectPool> {
        //对象池
        private Dictionary<string, List<GameObject>> cache;
        public override void init() {
            base.init();
            cache = new Dictionary<string, List<GameObject>>();
        }

        public GameObject CreateObject(string key, GameObject prefab, Vector3 pos, Quaternion rotate) {
            GameObject go = FindUsableObjectObject(key);

            if(go == null) {
                go = AddObject(key, go);
            }


            UseObject(pos, rotate, go);

            return go;
        }

        private GameObject FindUsableObjectObject(string key) {
            if(cache.ContainsKey(key)) {
                return cache[key].Find(g => !g.activeInHierarchy);
            }

            return null;
        }

        private GameObject AddObject(string key, GameObject prefab) {
            GameObject go = Instantiate(prefab);
            if (!cache.ContainsKey(key)) {
                cache.Add(key, new List<GameObject>());
            }
            cache[key].Add(go);

            return go;
        }

        private static void UseObject(Vector3 pos, Quaternion rotate, GameObject go) {
            go.transform.position = pos;
            go.transform.rotation = rotate;
            go.SetActive(true);
            // 原本是认为只应该使用一个 IGameObjectPool 接口,里面实现 reset,cycle (重置,回收)。
            // 认为充值回收都应该只是一个 GameObject 的。
            // 但是其实不对,GameObject 的 active 不是 IGameObjectPool 的功能,不需要他来执行
            foreach (var item in go.GetComponents<IGameObjectPoolReset>()) {
                item.OnReset();
            }
        }

        public void CollectObject(GameObject go, float delay) {
            //            go.SetActive(false);
            StartCoroutine( CollectObjectDelay(go, delay) );
        }

        private IEnumerator CollectObjectDelay(GameObject go, float delay) {
            yield return new WaitForSeconds(delay);
            go.SetActive(false);
        }

        // System.Object            object int list<string>
        // UnityEngine.Object       Object 模型 贴图 组件

        public void Clear(string key) {
            // 数组类型类型的删除,应该从后往前删。
            // 以为从前往后删除,其实是把后面的所有成员覆盖前一个。会自动减一。
            // 但是 i ++ 会导致 i 多加一次,所以每删一个就会漏一个元素。
            for(int i = cache[key].Count; i >= 0; i--) {
                Destroy(cache[key][i]);
            }

            // foreach 是不能在代码块内部进行增减数组(add,remove)的
            //            foreach(var item in cache[key]) {
            //                Destroy(item);
            //            }


            cache.Remove(key);
        }

        public void ClearAll() {
            // 异常:无效的操作
            // foreach 只读元素
            //foreach(var key in cache.Keys) {
                // 因为这里会移除整个key,而foreach是不允许代码块内部对相关类型进行增减的
            //    Clear(key);
            //}

            // cache.Keys 是只读的
            // 这里的做法就是把keys保存,foreach便利的不是会删减的相关类型
            List<string> keyList = new List<string>(cache.Keys);
            foreach (var key in keyList) {
                Clear(key);
            }
        }
    }

1.为什么能被 foreach

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以发现 keys 的类型是 KeyCollection,而 KeyCollection 继承于 IEnumerable 接口,也就是这个接口,可以使用foreach。

五.释放器

SkillDeployer.cs

    /// <summary>
    /// 技能释放器
    /// </summary>
    public abstract class SkillDeployer : MonoBehaviour {
        private SkillData skillData;
        public SkillData SkillData {
            get {
                return skillData;
            }
            set {
                skillData = value;
                //创建算法对象
            }
        }
        // 选区算法对象
        private IAttackSelector selector;
        // 影响算法对象
        private IImpactEffects[] impactArray;

        //创建算法对象
        private void InitDeployer() {
            // 选区
            selector = DeployeConfigrFactory.CreateAttackSelector(skillData);

            // 影响
            impactArray = DeployeConfigrFactory.CreateAttackImpactEffects(skillData);
        }


        //执行算法对象

        //选区
        public void CalculateTargets() {
            skillData.attackTargets = selector.SelectTarget(skillData, transform);
        }

        //影响
        public void ImpactTargets() {
            for(int i = 0;i < impactArray.Length;i++) {
                impactArray[i].Execute(this);
            }
        }

        //释放方式
        //供技能管理器调用,由子类实现,定义具体释放策略。
        public abstract void DeploySkill();
    }

SkillDeployer .cs

    /// <summary>
    /// 近身释放器
    /// </summary>
    public class MeleeSkillDeployer : SkillDeployer {

        public override void DeploySkill() {
            CalculateTargets();

            ImpactTargets();
        }
    }

DeployeConfigrFactory.cs

   /// <summary>
    /// 释放器配置工厂:提供创建释放器各种算法对象
    /// 作用:将对象的创建与使用分离。
    /// 创建对象在这里,使用放在 SkillDeployer 里。让 SkillDeployer 的职能更单一。
    /// 使用场景:当创建对象的逻辑比较复杂时,可以把创建的代码移出来,原来的代码逻辑只负责使用。
    /// </summary>
    public class DeployeConfigrFactory {
        public static IAttackSelector CreateAttackSelector(SkillData data) {

            // 创建算法对象
            // 选区对象命名规则:
            // xxx.Skill + 枚举名 + AttackSelector
            // 例如扇形选区 xxx.Skill.SectorAttackSelector
            string className = string.Format("xxx.Skill.{0}AttackSelector", data.selectorType);
            return CreateObject<IAttackSelector>(className);
        }

        public static IImpactEffects[] CreateAttackImpactEffects(SkillData data) {
            // 影响效果命名规范:
            // xxx.Skill. + impactType[?] + Impact
            IImpactEffects[] impactArray = new IImpactEffects[data.impactType.Length];         
            for (int i = 0; i < data.impactType.Length; i++) {
                string className = string.Format(".Skill.{0}Impact", data.impactType[i]);
                impactArray[i] = CreateObject<IImpactEffects>(className);
            }

            return impactArray;
        }

        private static T CreateObject<T>(string className) where T : class {
            Type type = Type.GetType(className);
            return Activator.CreateInstance(type) as T;
        }

    }
Q:为什么要用工厂?

A:如果一个“类的对象”的生成逻辑很多,很复杂,那么可以把生成的逻辑剥离出来。
一是,可以减少原来的代码量。二是,让”类“的逻辑更加单一化,将对象的创建与使用分离。

1.选区算法

IAttackSelector .cs

    /// <summary>
    /// 攻击选区的接口
    /// </summary>
    public interface IAttackSelector {
        /// <summary>
        /// 搜索目标
        /// </summary>
        /// <param name="data">技能数据</param>
        /// <param name="skillTF">技能所在物体的变换组件</param>
        /// <returns></returns>
        Transform[] SelectTarget(SkillData data, Transform skillTF);
    }

SectorAttackSelector.cs

    /// <summary>
    /// 圆形选区
    /// </summary>
    public class SectorAttackSelector : IAttackSelector {
        public Transform[] SelectTarget(SkillData data, Transform skillTF) {
            //根据技能数据中的标签,获取所有目标
            //data.attackTargetTags;
            //string[] -> Transform[] 

            List<Transform> targets = new List<Transform>();
            for(int i = 0;i < data.attackTargetTags.Length; i++) {
                GameObject[] tempGoArray = GameObject.FindGameObjectsWithTag("Enemy");
                targets.AddRange(tempGoArray.Select(g => g.transform) );
            }

            //判断攻击范围(扇形/圆形)
            targets = targets.FindAll(t => 
                            Vector3.Distance(t.position, skillTF.position) <= data.attackDistance
                         && Vector3.Angle(skillTF.forward, t.position - skillTF.position) <= data.attackAngle/2);

            //活的目标
            targets = targets.FindAll(t => t.GetComponent<CharacterStatus>().HP > 0);

            //返回目标(单体/群体)
            //data.attackType
            Transform[] result = targets.ToArray();

            if (result.Length <= 0)
                return result;

            if(data.attackType == SkillAttackType.Group) {
                return result;
            }
            //默认找距离最近的敌人
            Transform min = result.GetMin(t => Vector3.Distance(t.position, skillTF.position) );
            return new Transform[] { min };
        }
    }

2.影响算法

IImpactEffects.cs

    /// <summary>
    /// 影响效果算法接口
     /// </summary>
    public interface IImpactEffects {
        void Execute(SkillDeployer deployer);
    }

CostSPEffects .cs

/// <summary>
/// 消耗法力
/// </summary>
public class CostSPEffects : IImpactEffects {
    
    public void Execute(SkillDeployer deployer) {
        CharacterStatus status = deployer.SkillData.owner.GetComponent<CharacterStatus>();
        status.SP -= deployer.SkillData.costSP;
    }
}  

3.造成伤害

DamageImpact.cs

    public class DamageImpact : IImpactEffects {
        private SkillData data;
        public void Execute(SkillDeployer deployer) {
            data = deployer.SkillData;
            deployer.StartCoroutine(RepeatDamge());
        }

        // 重复伤害
        private IEnumerator RepeatDamge() {
            float atkTime = 0;
            do {
                OnceDamage();
                //伤害目标生命
                yield return new WaitForSeconds(data.atkInterval);
                atkTime += data.atkInterval;
            // 攻击时间没到
            } while (atkTime < data.durationTime);
        }

        // 单次伤害
        private void OnceDamage() {
            float atk = data.owner.GetComponent<CharacterStatus>().baseATK * data.atkRatio;
            for(int i = 0;i < data.attackTargets.Length; i++) {
                CharacterStatus status = data.attackTargets[i].GetComponent<CharacterStatus>();
                status.Damage(atk);
            }
        }
    }

代码逻辑很简单,不明白为什么讲了一整节课。

六.技能系统封装(技能系统外观类)

在这里插入图片描述
把技能系统封装起来,内部逻辑和内部逻辑自由交流。但是外部一定要通关外观类来和系统内部交流。

其实就一种设计模式。好像就是叫外观模式,有点忘了。

CharacterSkillSystem.cs

    [RequireComponent(typeof(CharacterSkillManager) ) ]
    /// <summary>
    /// 封装技能系统,提供简单的技能释放功能。
    /// </summary>
    public class CharacterSkillSystem : MonoBehaviour {
        private CharacterSkillManager skillManager;
        private Animator animator;
        public Transform selectedTarget;

        private void Start() {
            skillManager = GetComponent<CharacterSkillManager>();
            animator = GetComponent<Animator>();
            GetComponentInChildren<AnimationEventBehaviour>().attackHandler += DeploySkill;
        }

        private void DeploySkill() {
            //生成技能
            skillManager.GenerateSkil(skill);
        }

        private SkillData skill;
        /// <summary>
        /// 使用技能攻击(为玩家提供)
        /// </summary>
        public void AttackUseSkill(int skillId) {
            if (skillId == null)
                return;

            //准备技能
            skill = skillManager.PrepareSkill(skillId);
            if (skill == null)
                return;

            //播放动画
            animator.SetBool(skill.animationName, true);
            //生成技能

            //如果是目标选中型攻击
            if (skill.attackType != SkillAttackType.Single)
                return;

            // 查找目标
            Transform targetFT = SelectTargets();
            //朝向目标
            transform.LookAt(targetFT);
            //选中目标
            //1.选中目标,间隔指定时间后取消选中.
            //取消上次选中物体
            SetSelectedActiveFx(false);
            //2.选中A目标,再自动取消前又选中B目标,则需手懂将A取消
            selectedTarget = targetFT;
            //选中当前物体
            SetSelectedActiveFx(true);
        }

        private Transform SelectTargets() {
            Transform[] target = new SectorAttackSelector().SelectTarget(skill, transform);
            return target.Length != 0 ? target[0] : null;
        }

        private void SetSelectedActiveFx(bool state) {
            if (selectedTarget == null)
                return;

            var selected= selectedTarget.GetComponent<CharacterSelected>();
            if (selected)
                selected.SetSelectedActive(true);
        }

        /// <summary>
        /// 使用随机技能(为NPC提供)
        /// </summary>
        public void UseRandomSkill() {
            //从管理器中,挑选出随机的技能
            //1.先产生随机数 再判断技能是否可以释放
            //2.先筛选出所有可以释放的技能,再产生随机数
            //我们使用2,1有可能产生的随机数代表的技能无法使用,然后就一直再筛。

            // 筛选所有可以使用的技能
            var usableSkills = skillManager.skills.FindAll(
                s => skillManager.PrepareSkill(s.skillId) != null);

            if (usableSkills.Length == 0)
                return;

            AttackUseSkill( Random.Range(0, usableSkills.Length) );
        }
    }

1.选中功能和标志

在这里插入图片描述
在这里插入图片描述
在某个Character下增加了一个选中的 GameObject ,挂着模型(MeshRenderer)和 CharacterSelected 脚本。
在这里插入图片描述
Q1:这种做法是否正确的呢?如果以后有其他标志位,是否该分类,或者用其他做法呢?
Q2:目前只用于攻击选中,那么其他选中是否可用?比如对话,或者任何其他行为?
Q3:它自动会在 ?秒后把自己 enable,是否正确呢?

CharacterSelected .cs

public class CharacterSelected : MonoBehaviour {
    public GameObject selectedGO;
    [Tooltip("选择器游戏物体名称")]
    public string selectedName = "selected";
    [Tooltip("显示时间")]
    public float displayTime = 3;
    private void Start() {
        selectedGO = transform.Find(selectedName).gameObject;
    }

    private float hideTime;
    public void SetSelectedActive(bool state) {
        //设置选择器物体激活状态
        selectedGO.SetActive(state);
        //设置当前脚本激活状态(enable的开关,直接导致 停止/开启 Update)
        //enabled 关闭后,就不会每帧调用 update 了
        this.enabled = state;
        if (state) {
            hideTime = Time.time + displayTime;
        }
    }

    private void Update() {
        if(hideTime <= Time.time) {
            SetSelectedActive(false);
        }
    }
}

七.技能连击


在这里插入图片描述
做法就是把多次普攻弄成多个技能,比如普通攻击有三段,那么就有三个技能。每个技能的 Next Batter Id 指的是下一个普攻的id。

1.CharacterInputController.cs

修改了攻击按钮的注册事件,改为onPressed

        private void OnEnable() {
            joystick.onMove.AddListener(OnJoystickMove);
            joystick.onMoveStart.AddListener(OnJoystickMoveStart);
            joystick.onMoveEnd.AddListener(OnJoystickMoveEnd);

            for (int i = 0; i < skillButtons.Length; i++) {
                if(skillButtons[i].name == "BaseButton") {
                    skillButtons[i].onPressed.AddListener(OnSkillButtonPressed);
                } else {
                    skillButtons[i].onDown.AddListener(OnSkillButtonDown);
                }
            }
        }

        private float lastPressTime = -1;
        private void OnSkillButtonPressed() {
            //按住间隔如果过小(2)则取消攻击
            //间隔小于5秒视为连击

            //间隔:当前按下时间 - 上次按下时间

            float interval = Time.time - lastPressTime;
            if (interval < 2)
                return;

            bool isBatter = interval <= 5;

            skillSystem.AttackUseSkill(1001, true);

            lastPressTime = Time.time;
        }

2.CharacterSkillSystem.cs

新增一个变量 isBatter,表示是否连击,如果有 skill.nextBatterId 则使用 skill.nextBatterId 代表的那个技能。

    public void AttackUseSkill(int skillId,bool isBatter = false) {
        if (skillId == null)
            return;

        //如果连击,则从上一个释放的技能中获取
        if (skill != null && isBatter)
            skillId = skill.nextBatterId;

        //准备技能
        skill = skillManager.PrepareSkill(skillId);
        if (skill == null)
            return;

        //播放动画
        animator.SetBool(skill.animationName, true);
        //生成技能

        //如果是目标选中型攻击
        if (skill.attackType != SkillAttackType.Single)
            return;

        // 查找目标
        Transform targetFT = SelectTargets();
        //朝向目标
        transform.LookAt(targetFT);
        //选中目标
        //1.选中目标,间隔指定时间后取消选中.
        //取消上次选中物体
        SetSelectedActiveFx(false);
        //2.选中A目标,再自动取消前又选中B目标,则需手懂将A取消
        selectedTarget = targetFT;
        //选中当前物体
        SetSelectedActiveFx(true);
    }

八.总结

1.SkillData

在这里插入图片描述
可以发现在整个技能系统里,SkillData 只是唯一的存在于 CharcterSkillManager 里。除了从表里读取的数据(等级,倍率,效果等)。还有 Skill 的 释放者(owner)的 GameObject 这种游戏实体在里面。也可以说它已经不完全是个常规的data了。

2.DeployerConfigrFactory 增加缓存

就是增加缓存,复用技能,防止无意义的多次创建。

public class DeployeConfigrFactory {
    private static Dictionary<string, System.Object> cache;

    static DeployeConfigrFactory() {
        cache = new Dictionary<string, System.Object>();
    }
	
	......
	......
	......

    private static T CreateObject<T>(string className) where T : class {
        if(!cache.ContainsKey(className)) {
            Type type = Type.GetType(className);
            System.Object instance = Activator.CreateInstance(type);
            cache.Add(className, instance);
        }

        return cache[className] as T;
    }

}

3.DamageImpact

协程+缓存,在上一个逻辑赋值DamageImpact里的私有变量后,未结束逻辑流程,就被下一个 deployer 给重新赋值了。

解决的方法就是不保存这个私有变量,直接闭包调用即可。

看注释掉的这段代码就知道了

private SkillData data;

public class DamageImpact : IImpactEffects {
    //private SkillData data;
    public void Execute(SkillDeployer deployer) {
        //data = deployer.SkillData;
        deployer.StartCoroutine(RepeatDamge(deployer));
    }

    // 重复伤害
    private IEnumerator RepeatDamge(SkillDeployer deployer) {
        float atkTime = 0;
        do {
            OnceDamage(deployer.SkillData);
            //伤害目标生命
            yield return new WaitForSeconds(deployer.SkillData.atkInterval);
            atkTime += deployer.SkillData.atkInterval;
        // 攻击时间没到
        } while (atkTime < deployer.SkillData.durationTime);
    }

    // 单次伤害
    private void OnceDamage(SkillData data) {
        float atk = data.owner.GetComponent<CharacterStatus>().baseATK * data.atkRatio;
        for(int i = 0;i < data.attackTargets.Length; i++) {
            CharacterStatus status = data.attackTargets[i].GetComponent<CharacterStatus>();
            status.Damage(atk);
        }
    }
}

4.多个技能释放导致的bug

就是在按钮哪里判断一下是不是正在攻击,在攻击就不放技能。

讲道理这里教的就感觉很奇怪了。为啥是判断动画状态?为啥不是判断技能是否未释放完?为啥不是判断当前技能动作是否到了可以释放其他动作的时机?

public class CharacterInputController : MonoBehaviour {
	......
	......
	
    private bool IsAtttacking() {
        return anim.GetBool(status.chParams.attack1);
            //|| anim.GetBool(status.chParams.attack2)
    }
}

猜你喜欢

转载自blog.csdn.net/qq_28686039/article/details/123320161