三.技能系统 [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 {//技能Idpublic 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 里是 GUIDstring[] 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 configMap;static ResourceManager() {// 加载文件string fileContent = GetConfigFile("ConfigMap.txt");// 解析文件(string --> Dictionary)BuildMap(fileContent);}public static string GetConfigFile(string fileName) {string url;//if(Application.platform == RuntimePlatform.WindowsEditor)#if UNITY_EDITOR || UNITY_STANDALONEurl = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IOSurl = "file://" + Application.dataPath + "/Raw/"+ fileName;
#elif UNITY_ANDROIDurl = "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();//文件名=路径\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(string prefabName) where T:Object {//prefabName -> prefabPathstring prefabPath = configMap[prefabName];return Resources.Load(prefabPath);}}

1).静态构造函数

    public class ResourceManager {static ResourceManager() {// 加载文件string fileContent = GetConfigFile("ConfigMap.txt");// 解析文件(string --> Dictionary)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_STANDALONEurl = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IOSurl = "file://" + Application.dataPath + "/Raw/"+ fileName;
#elif UNITY_ANDROIDurl = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
#endif

4).StringReader

    private static void BuildMap(string fileContent) {configMap = new Dictionary();//文件名=路径\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

   /// /// 使用方式:/// 1.所有频繁创建/销毁的物体,都通过对象池创建/回收/// 2.需要通过对象池创建的物体,如需每次创建时执行,则让脚本实现  IGameObjectPoolReset 接口/// public interface IGameObjectPoolReset{void OnReset();}public class GameObjectPool :MonoSingleton {//对象池private Dictionary> cache;public override void init() {base.init();cache = new Dictionary>();}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());}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()) {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// 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 keyList = new List(cache.Keys);foreach (var key in keyList) {Clear(key);}}}

1.为什么能被 foreach

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

五.释放器

SkillDeployer.cs

    /// /// 技能释放器/// 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

    /// /// 近身释放器/// public class MeleeSkillDeployer : SkillDeployer {public override void DeploySkill() {CalculateTargets();ImpactTargets();}}

DeployeConfigrFactory.cs

   /// /// 释放器配置工厂:提供创建释放器各种算法对象/// 作用:将对象的创建与使用分离。/// 创建对象在这里,使用放在 SkillDeployer 里。让 SkillDeployer 的职能更单一。/// 使用场景:当创建对象的逻辑比较复杂时,可以把创建的代码移出来,原来的代码逻辑只负责使用。/// public class DeployeConfigrFactory {public static IAttackSelector CreateAttackSelector(SkillData data) {// 创建算法对象// 选区对象命名规则:// xxx.Skill + 枚举名 + AttackSelector// 例如扇形选区 xxx.Skill.SectorAttackSelectorstring className = string.Format("xxx.Skill.{0}AttackSelector", data.selectorType);return CreateObject(className);}public static IImpactEffects[] CreateAttackImpactEffects(SkillData data) {// 影响效果命名规范:// xxx.Skill. + impactType[?] + ImpactIImpactEffects[] 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(className);}return impactArray;}private static T CreateObject(string className) where T : class {Type type = Type.GetType(className);return Activator.CreateInstance(type) as T;}}
Q:为什么要用工厂?

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

1.选区算法

IAttackSelector .cs

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

SectorAttackSelector.cs

    /// /// 圆形选区/// public class SectorAttackSelector : IAttackSelector {public Transform[] SelectTarget(SkillData data, Transform skillTF) {//根据技能数据中的标签,获取所有目标//data.attackTargetTags;//string[] -> Transform[] List targets = new List();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().HP > 0);//返回目标(单体/群体)//data.attackTypeTransform[] 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

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

CostSPEffects .cs

/// 
/// 消耗法力
/// 
public class CostSPEffects : IImpactEffects {public void Execute(SkillDeployer deployer) {CharacterStatus status = deployer.SkillData.owner.GetComponent();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().baseATK * data.atkRatio;for(int i = 0;i < data.attackTargets.Length; i++) {CharacterStatus status = data.attackTargets[i].GetComponent();status.Damage(atk);}}}

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

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

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

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

CharacterSkillSystem.cs

    [RequireComponent(typeof(CharacterSkillManager) ) ]/// /// 封装技能系统,提供简单的技能释放功能。/// public class CharacterSkillSystem : MonoBehaviour {private CharacterSkillManager skillManager;private Animator animator;public Transform selectedTarget;private void Start() {skillManager = GetComponent();animator = GetComponent();GetComponentInChildren().attackHandler += DeploySkill;}private void DeploySkill() {//生成技能skillManager.GenerateSkil(skill);}private SkillData skill;/// /// 使用技能攻击(为玩家提供)/// 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();if (selected)selected.SetSelectedActive(true);}/// /// 使用随机技能(为NPC提供)/// 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 cache;static DeployeConfigrFactory() {cache = new Dictionary();}..................private static T CreateObject(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().baseATK * data.atkRatio;for(int i = 0;i < data.attackTargets.Length; i++) {CharacterStatus status = data.attackTargets[i].GetComponent();status.Damage(atk);}}
}

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

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

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

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部