三.技能系统

一.技能系统架构




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<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_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<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 -> prefabPathstring 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_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<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.SectorAttackSelectorstring className = string.Format("xxx.Skill.{0}AttackSelector", data.selectorType);return CreateObject<IAttackSelector>(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<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.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

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

三.技能系统 [Unity_Learn_RPG_1]相关推荐

  1. 关于类DOTA游戏多样化技能系统的设计思考

    在游戏里,每一个人物都有很多的技能.像DOTA,英雄联盟一样,技能也不都是单一的直线判断,而是有很多的花样.这类游戏的技能系统是如何设计的呢? 这里想从自动机的角度来抽象这个问题,以期得到一个更泛化的 ...

  2. linux sleeping进程多_一文掌握Linux实战技能系统管理篇

    linux 安装包-yum 的使用 linux 进程管理 ps pstree top 指令的使用 linux 进程之间的通信 linux 守护进程 linux 内存以及硬盘使用查看 linux 防火墙 ...

  3. MMORPG的常规技能系统

    转自: https://www.gameres.com/729192.html 广义的的说,和战斗结算相关的内容都算技能系统,包括技能信息管理.技能调用接口.技能目标查找.技能表现.技能结算.技能创生 ...

  4. 《守望先锋》中网络脚本化的武器和技能系统

    在GDC2017[Networking Scripted Weapons and Abilities in Overwatch]的分享会上,来自暴雪的Dan Reed介绍了<守望先锋>中网 ...

  5. UE战棋游戏的制作流程(使用GAS来制作技能系统)

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 角色基础功能 GAS插件的使用 配置GameplaySystemAbility 角色的基本属性 创建结构体用于储存GE ...

  6. Unity 如何实现一个强大的MMO技能系统!

    1. 如何实现一个强大的MMO技能系统-序章 前言 技能系统可以说是游戏中广泛存在而又最重要的系统了,它是整个游戏战斗体验的核心.一套强大的技能系统可以让游戏的策略性,可玩性得到极大的提升.然而技能系 ...

  7. Unity如何设计一个技能系统

    一.技能系统的设计思路 技能系统是游戏中非常重要的一部分,因此在设计技能系统时需要考虑以下几个方面: 对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白,也有一些正在从事游戏开发的技 ...

  8. 游戏编程干货:游戏技能系统全解析

    广义的的说,和战斗结算相关的内容都算技能系统,包括技能信息管理.技能调用接口.技能目标查找.技能表现.技能结算.伤害结算.buf/法术场模块管理,此外还涉及的模块包括:AI模块(技能调用者).动作模块 ...

  9. 游戏开发笔记-技能系统

    原文地址:http://blog.csdn.net/mooke/article/details/9771545    技能系统是一个对于游戏来说,非常重要,实现起来又有些复杂的模块了.       在 ...

最新文章

  1. mongodb 安装时错误
  2. window.print 固定表头不影响_Excel中的表头,你会处理吗
  3. 100篇精选算法技术文章收藏
  4. 按键精灵saystring无法使用的几种解决方案
  5. libcareplus一个Qemu-6.1.0热补丁示例
  6. maya如何导出ue4_ue4 maya max导入导出问题
  7. CentOS 6.3 samba安装及配置
  8. python urllib下载文件怎么停止_python下载文件的三种方法
  9. linux不同内核驱动移植问题,基于tiny4412的Linux内核移植 -- MMA7660驱动移植(九-2)...
  10. MAC下安装与配置MySQL [转]
  11. 内推 | 高级数据分析师(base:杭州)
  12. 此计算机屏保怎么取消,如何取消屏幕保护
  13. FL studio 20简易入门教程 -- 第六篇 -- 调音台和自动化包络线
  14. JSP+Servlet技术实现分页 首页 下一页 每一页显示10条页码 下一页 尾页 第页/共页 (利用PageBean实现)
  15. VIP邮箱移动办公平台软件,疫情居家办公小助手
  16. android模拟器不玩游戏,安卓模拟器哪个玩游戏最流畅?
  17. 利用Matlab替换图片部分颜色
  18. Visitor----------模式
  19. 七夕王者服务器维护什么时间结束,王者荣耀2020七夕活动什么时候结束?七夕情人节活动结束时间[多图]...
  20. [SAP] 生产订单报错SEQ_NOT_FOUND/OPR_NOT_FOUND问题解决

热门文章

  1. 盘丝洞服务器维护,梦幻西游:测试盘丝洞技能经脉 天罗地网只对任务起效
  2. 对牛皮凉席味道困惑的人群如何选择牛皮凉席
  3. 侧边导航html案例
  4. 腾讯云CDW-ClickHouse云原生实践
  5. 如何在Adobe Illustrator中绘制多肉盆栽插图
  6. .NET Core 3.1发布,支持三年的LTS版本
  7. megacli通过盘符定位物理盘_MegaCli使用
  8. mysql格式是什么文件怎么打开_sql文件怎么打开,SQL格式是什么文件?
  9. 初识python语言微课_Python语言程序设计(微课版)
  10. React项目中引入图片