三.技能系统
一.技能系统架构
excel转xml
直接excel就能转= =
二.技能系统管理类和Unity事件
1.Unity事件
老师说,开发的时候基本不用搞这些 = =。
1).VRTK UI是如何拓展UGUI事件的
- 使用 UIEventListener 继承 UGUI 的各种接口类
- 根据接口实现需要的代理。这里是要传递数据,根据接口给的数据,分为几种不同的代理。
- 根据接口实现需要的事件
- 需要监听的地方注册事件
2).EasyTouch 中是如何实现的
- 使用不同的功能类继承UnityEvent类
- 生成不同功能类的对象
- 需要监听的地方注册事件
3).为啥Unity用UnityEvent来做,而不是用委托呢?
主要就是因为UnityEvent可以在编辑器中显示和设置,直接用C#的委托则显示不出来。UnityEvent再深入的代码我们就看到不到了。
个人还是更喜欢用委托的方法。
4).给EasyTouch的某个事件增加返回参数
并且我们发现,UnityEvent 这个类最多可以传递4个参数,T0-T3。包括没有参数的,一共又五种可选。
2.可空类型(复习)
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.功能
-
编译器类:继承自Editor类,只需要在Unity编译器中执行的代码
-
菜单项,特性[MenuItem(“…”)]:用于修饰需要在Unity编译器中产生菜单按钮的方法
-
AssetBase:只适用于编译器中执行
-
StreamingAssets:Unity特殊目录之一,存放需要在程序运行时读取的文件,该目录中的文件不会被压缩。
-
适合在移动端读取资源(在PC端可以写入,其他只读)。
-
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 字符串读取器,提供了逐行读取字符串功能。
- 当程序调用 using 代码块,将自动调用 reader.Dispose() 方法,否则我们得手动调用一次
- 如果异常,则程序会立即中断,那么就执行不了 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)
}
}