1.技能系统
拿到技能说明文档,我们的分析过程如下:
首先把每一个技能单独写一个类是可以的,只是这只适用于技能较少且不会改动的情况下,如果技能较多且后续可能会一直修改的话就不合适了,改动量会非常大。
此时可以想到,每一个技能都是由不同的影响效果结合产生的,如LOL里诺手的大杀四方就有这么几个影响效果:降低自身蓝量,减少敌人血量,对英雄单位增加一层流血被动。其他技能也有类似影响效果。
此时我们就可以将每一种影响效果写成一个类,然后不同技能挑选几个效果结合即可,且所有影响继承同一个父类IImpactEffect(接口类),在父类里提供一个抽象方法,由子类实现不同效果例如CostSPImpactEffect(减少蓝量),DamageImpactEffect(造成伤害)等,然后通过反射创建影响效果对象,在使用多态通过调用父类的接口实现不同的影响效果类型产生对应的效果。
此时技能已经有了,我们还需要一个技能数据类SkillData来存储技能数据例如技能的ID,技能的名字,技能所需蓝量,技能有哪些影响效果(反射创建具体影响效果类时就通过这里给的名称进行反射创建)等。
当存在技能数据类型后,角色还需要一个存储管理所有技能的技能管理类(CharacterSkillManager),这个类会根据技能ID创建对应技能并存储起来。还可以释放技能,释放技能时会判断技能是否准备完成(冷却结束,有做够的法力值),准备完成时就会释放技能了。
释放技能时会创建技能的选区(技能的影响范围,也就是获取技能目标),创建技能的具体影响效果对象(多个),创建完成选区与效果对象后就会调用其父类的接口获取技能目标与产生影响效果。技能的释放是由技能释放器实现,然后管理器调用创建的方法即可。选区与效果对象的创建则由一个工厂(DeployerconfigFactory)创建(当然不用工厂创建直接写在技能释放器内也可以,但是分离之后更好),然后释放器调用他的创建方法就行。不同的技能有着不同的释放方法,比如有远程释放,有近身释放,所以也需要一个技能释放器的父类,并提供一个 释放技能的抽象方法,然后不同的释放器子类分别实现具体的释放方式。
上面我们提到了一个技能的选区(如何获取技能目标),一般而言很多技能的释放范围时不同的,有圆弧,有长方形,有三角形等等,此时可以使用技能影响效果的方法,将不同的选区分别做一个类,并继承同一个父类IAttackSelector(接口类) ,在父类里提供一个抽象方法,由子类具体实现不同的选区方法,然后通过反射创建对象,在使用多态通过调用父类的接口实现不同的选区。
上述的影响效果类与选区类我们只需先完成父类这个接口,然后慢慢实现具体的影响效果类与选区类。
图解如下
对象池:
以一个射击游戏为例,一把枪发射子弹去打目标,子弹这个游戏物体在每一次开枪时都会被创建使用,然后打击到目标后会被销毁。子弹这个游戏物体会被重复多次的创建然后销毁掉,如果一直用旧方法的 Instantiate创建与Destroy销毁会特别消耗性能。
所以我们可以用一个字典,字典的key是游戏物体的类型,值为一个List存储这个类的所有GameObject,当我们需要创建某个类型的游戏物体时只需要去字典里找有没有这个类型的游戏物体是关闭显示的,有的话直接拿到需要的位置上,然后SetActive(True)使用即可,如果没有再创建这个游戏物体拿去使用并存到字典内,需要销毁时只需将物体SetActive(False)就行。
对象池是一种通过空间换取时间的思想,虽然字典占用了更多的内存空间,但是降低了性能的消耗。
接下来就是技能系统与对象池的具体实现代码
首先是技能数据类SkillData
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 技能数据类
/// </summary>
[Serializable]
public class SkillData
{
/// <summary>
/// 技能ID
/// </summary>
public int skillID;
/// <summary>
/// 技能名称
/// </summary>
public string name;
/// <summary>
/// 技能描述
/// </summary>
public string description;
/// <summary>
/// 冷却时间
/// </summary>
public int coolTime;
/// <summary>
/// 冷却剩余
/// </summary>
public int coolRemain;
/// <summary>
/// 法力值消耗
/// </summary>
public int costSP;
/// <summary>
/// 攻击距离
/// </summary>
public float attackDistance;
/// <summary>
/// 攻击角度
/// </summary>
public float attackAngle;
/// <summary>
/// 攻击目标的Tags
/// </summary>
public string[] attackTargetTags = { "Enemy" };
/// <summary>
/// 攻击目标对象数组
/// </summary>
[HideInInspector]
public Transform[] attackTargets;
/// <summary>
/// 技能影响类型
/// </summary>
public string[] impactype = { "CostSP", "Damage" };
/// <summary>
/// 连击的下一个技能编号
/// </summary>
public int nextBatterID;
/// <summary>
/// 伤害比率/伤害数值
/// </summary>
public float atkRatio;
/// <summary>
/// 持续时间
/// </summary>
public float durationTime;
/// <summary>
/// 伤害间隔时间
/// </summary>
public float atkInterval;
/// <summary>
/// 技能所属
/// </summary>
[HideInInspector]
public GameObject owner;
/// <summary>
/// 技能预制体名称
/// </summary>
public string prefabName;
/// <summary>
/// 技能预制体对象
/// </summary>
[HideInInspector]
public GameObject skillPrefab;
/// <summary>
/// 动画名称
/// </summary>
public string animationName;
/// <summary>
/// 受击特效名称
/// </summary>
public string hitFxName;
/// <summary>
/// 受击特效预制体
/// </summary>
[HideInInspector]
public GameObject hitFxPrefab;
/// <summary>
/// 技能等级
/// </summary>
public int level;
/// <summary>
/// 攻击类型 单攻,群攻
/// </summary>
public SkillAttackType attackType;
/// <summary>
/// 技能范围类型 扇形,圆形,矩形......
/// </summary>
public SelectorType selectorType;
//其他数据
}
}
然后是选区类型 SelectorType
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
public enum SelectorType
{
//扇形
Sector,
//圆形
Rectangle,
}
}
然后是攻击方式SelectorType:单攻/群攻
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
public enum SkillAttackType
{
//单攻
Single,
//群攻
Group,
}
}
然后是选区的接口IAttackSelector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 攻击选区接口
/// 技能攻击的范围区域,例如诺手无情铁手的三角区域
/// </summary>
public interface IAttackSelector
{
/// <summary>
/// 获取攻击区域内的目标
/// </summary>
/// <param name="skill">技能</param>
/// <param name="SkillTF">技能的实体</param>
/// <returns></returns>
Transform[] GetTargets(SkillData skill, Transform skillTF);
}
}
然后是影响效果的接口IImpactEffect
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 技能造成的影响
/// 例如减少蓝量,掉血等
/// </summary>
public interface IImpactEffect
{
/// <summary>
/// 影响效果
/// 例如:伤害敌人生命(HP)
/// 可能伤害多次
/// </summary>
/// <param name="deployer">释放器脚本</param>
void Execute(SkillDeployer deployer);
}
}
然后是技能管理器CharacterSkillManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common;
namespace RPG.Skill
{
public class CharacterSkillManager : MonoBehaviour
{
public List<SkillData> skillDatas = new List<SkillData>();
private void Start()
{
foreach (var data in skillDatas)
{
InitSkills(data);
}
}
/// <summary>
/// 初始化技能数据
/// </summary>
/// <param name="data"></param>
private void InitSkills(SkillData data)
{
/*
资源映射表
资源名称 资源完整路径
Test = Skill/Test
*/
//由data.prefabName 生成data.skillPrefab
//data.skillPrefab = Resources.Load<GameObject>("路径/" + data.prefabName);
data.skillPrefab = ResourceManager.Load<GameObject>(data.prefabName);
data.owner = this.gameObject;
}
/// <summary>
/// 准备技能,判断技能是否可以释放
/// 技能释放条件:冷却结束+有多余蓝量
/// </summary>
/// <param name="skillID"></param>
/// <returns></returns>
public SkillData PrepareSkill(int skillID)
{
//根据id查找技能
SkillData skill = skillDatas.Find(s => s.skillID == skillID);
//判断条件
if (skill != null && skill.coolRemain <= 0 && skill.costSP <= this.transform.GetComponent<CharacterStatus>().SP)
return skill;
//返回技能数据
else
return null;
}
/// <summary>
/// 生成技能
/// </summary>
public void GenerateSkill(SkillData data)
{
//创建技能预制体
//GameObject skillObj = Instantiate(data.skillPrefab, transform.position, transform.rotation);
GameObject skillObj = GameObjectPool.Instance.CreateObject(data.prefabName, data.skillPrefab, transform.position, transform.rotation);
//播放攻击动画,此处为测试代码
//this.transform.GetComponent<Animator>().SetTrigger("IsAttack");
//传递技能数据
SkillDeployer deployer = skillObj.GetComponent<SkillDeployer>();
deployer.SkillData = data;
deployer.DeploySkill();
//延迟销毁预制体
//Destroy(skillObj, data.durationTime);
GameObjectPool.Instance.CollectObject(skillObj, data.durationTime);
//技能冷却 开启一个协程
StartCoroutine(CoolTimeDown(data));
}
/// <summary>
/// 计算技能冷却
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private IEnumerator CoolTimeDown(SkillData data)
{
//data.coolTime ---> data.coolRemain
data.coolRemain = data.coolTime;
while (data.coolRemain > 0)
{
yield return new WaitForSeconds(1);
data.coolRemain--;
}
}
}
}
然后是技能释放器SkillDeploy
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 技能释放器
/// </summary>
public abstract class SkillDeployer : MonoBehaviour
{
//技能数据
private SkillData skillData;
public SkillData SkillData
{
get
{
return skillData;
}
set
{
skillData = value;
//创建算法对象
InitDeployer();
}
}
//选区效果对象
private IAttackSelector selector;
//影响效果对象
private List<IImpactEffect> impactEffects/* = new List<IImpactEffect>()*/;
/// <summary>
/// 初始化释放器
/// </summary>
private void InitDeployer()
{
//创建算法对象
//1.创建技能区域对象(圆形/矩形***)
selector = DeployerconfigFactory.CreateAttackSelector(skillData);
//2.创建影响效果对象
impactEffects = DeployerconfigFactory.CreateImpactEffects(skillData);
}
//执行算法对象
/// <summary>
/// 1.执行选区算法
/// 获取技能目标对象
/// </summary>
public void CalculateTargets()
{
skillData.attackTargets = selector.GetTargets(skillData, this.transform);
//foreach (var item in skillData.attackTargets)
//{
// Debug.LogError(item.name);
//}
}
/// <summary>
/// 2.执行影响算法
/// 造成技能效果
/// </summary>
public void ImpactTargets()
{
foreach (var item in impactEffects)
{
//item.接口方法
item.Execute(this);
}
}
//释放方式
/// <summary>
/// 供技能管理器调用,有子类实现,定义具体释放策略
/// </summary>
public abstract void DeploySkill();
#region 此处为创建释放器各种算法对象,已移至释放器工厂内实现,分离的算法对象的创建与实用,该脚本只使用各种算法对象
/// <summary>
/// 初始化释放器
/// </summary>
//private void InitDeployer()
//{
// //创建算法对象
// //1.技能区域(圆形/矩形***)
// //skillData.selectorType
// //使用反射的方法
// //选区对象命名规则:
// //(命名空间名)RPG.Skill.+枚举名+AttackSelector
// //例如扇形选区:RPG.Skill.SectorAttackSelector
// string typeName = string.Format("RPG.Skill.{0}AttackSelector", skillData.selectorType);
// //Type type = Type.GetType(typeName);
// //selector = Activator.CreateInstance(type) as IAttackSelector;
// selector = CreateObject<IAttackSelector>(typeName);
// //2.影响
// //skillData.impactype
// //同样使用反射的方法
// //影响的命名规范:
// //(命名空间名)RPG.Skill.+具体名称+ImpactEffect
// //例如蓝量的影响:RPG.Skill.CostSPImpactEffect
// foreach (var impactType in skillData.impactype)
// {
// string impactName = string.Format("RPG.Skill.{0}ImpactEffect", impactType);
// //Type impact = Type.GetType(name);
// //impactEffects.Add(Activator.CreateInstance(impact) as IImpactEffect);
// impactEffects.Add(CreateObject<IImpactEffect>(impactName));
// }
//}
/// <summary>
/// 通过反射创建对象
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="typeName"></param>
/// <returns></returns>
//private T CreateObject<T>(string typeName)where T:class
//{
// Type type = Type.GetType(typeName);
// return Activator.CreateInstance(type) as T;
//}
#endregion
}
}
然后是技能配置工厂DeployerconfigFactory
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 技能释放器工厂类,生成算法对象
/// </summary>
public class DeployerconfigFactory
{
//缓存使用反射创建的对象
private static Dictionary<string, object> cache = new Dictionary<string, object>();
/// <summary>
/// 生成技能区域对象
/// 提供创建释放器各种算法对象的功能
/// 作用:将对象的创建于使用分离,此处只创建,用还是在释放器内
/// </summary>
/// <param name="skillData"></param>
/// <returns></returns>
public static IAttackSelector CreateAttackSelector(SkillData skillData)
{
//创建算法对象
//1.技能区域(圆形/矩形***)
//skillData.selectorType
//使用反射的方法
//选区对象命名规则:
//(命名空间名)RPG.Skill.+枚举名+AttackSelector
//例如扇形选区:RPG.Skill.SectorAttackSelector
string typeName = string.Format("RPG.Skill.{0}AttackSelector", skillData.selectorType);
//Type type = Type.GetType(typeName);
//selector = Activator.CreateInstance(type) as IAttackSelector;
return CreateObject<IAttackSelector>(typeName);
}
/// <summary>
/// 生成影响效果对象
/// </summary>
/// <param name="skillData"></param>
/// <returns></returns>
public static List<IImpactEffect> CreateImpactEffects(SkillData skillData)
{
//2.影响
//skillData.impactype
//同样使用反射的方法
//影响的命名规范:
//(命名空间名)RPG.Skill.+具体名称+ImpactEffect
//例如蓝量的影响:RPG.Skill.CostSPImpactEffect
List<IImpactEffect> impactEffects = new List<IImpactEffect>();
foreach (var impactType in skillData.impactype)
{
string impactName = string.Format("RPG.Skill.{0}ImpactEffect", impactType);
//Type impact = Type.GetType(name);
//impactEffects.Add(Activator.CreateInstance(impact) as IImpactEffect);
impactEffects.Add(CreateObject<IImpactEffect>(impactName));
}
return impactEffects;
}
/// <summary>
/// 通过反射创建对象
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="typeName"></param>
/// <returns></returns>
private static T CreateObject<T>(string typeName) where T : class
{
由于反复使用反射创建对象消耗性能较高,所有使用缓存来解决此问题
if (!cache.ContainsKey(typeName))
{
//Debug.LogError("反射");
Type type = Type.GetType(typeName);
object instance = Activator.CreateInstance(type);
cache.Add(typeName, instance);
}
return cache[typeName] as T;
//Type type = Type.GetType(typeName);
//return Activator.CreateInstance(type) as T;
}
}
}
到这那些核心的代码(父类与接口)就写完了,剩下的就是一些具体的影响效果类与选区
例如减少自身蓝量CostSPImpactEffect(此处要注意命名规范,因为在技能工厂内是根据此处命名反射创建对象的)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 消耗蓝量的影响
/// </summary>
public class CostSPImpactEffect : IImpactEffect
{
/// <summary>
/// 具体影响效果的实现
/// 消耗蓝量的影响:减少自身蓝量
/// </summary>
/// <param name="deployer"></param>
public void Execute(SkillDeployer deployer)
{
//减少蓝量
//deployer.SkillData.owner.GetComponent<CharacterStatus>().SP -= deployer.SkillData.costSP;
deployer.SkillData.owner.GetComponent<CharacterStatus>().ExpendSP(deployer.SkillData.costSP);
}
}
}
减少自身蓝量,造成伤害等这些影响效果还需要用到角色身上的状态数据(HP,SP等),这些数据都在一个角色状态类里CharacterStatus,这里的代码可以根据实际需要修改
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
public class CharacterStatus : MonoBehaviour
{
/// <summary>
/// 血量
/// </summary>
public float HP;
/// <summary>
/// 蓝量
/// </summary>
public float SP;
/// <summary>
/// 基础攻击力
/// </summary>
public float BaseAttack;
/// <summary>
/// 法强
/// </summary>
public float Spellpower;
/// <summary>
/// 护甲
/// </summary>
public float Armor;
/// <summary>
/// 魔抗
/// </summary>
public float MagicResistance;
/// <summary>
/// 移速
/// </summary>
public float Speed;
/// <summary>
/// 韧性
/// </summary>
public float rx;
/// <summary>
/// 冷却
/// </summary>
public float lq;
/// <summary>
/// 攻速
/// </summary>
public float gjSpeed;
/// <summary>
/// 暴击几率
/// </summary>
public float bjjl;
/// <summary>
/// 最高血量
/// </summary>
public float MaxHP;
/// <summary>
/// 最高蓝量
/// </summary>
public float MaxSP;
/// <summary>
/// 受伤
/// 真实的伤害值需要考虑伤害来源的攻击力/法强,以及破甲/法穿,自身的护甲/魔抗等众多因素来计算
/// 此方法只是简单的扣除血量,可做具体修改
/// </summary>
/// <param name="damage"></param>
public void ExpendHP(float damage)
{
HP -= damage;
}
/// <summary>
/// 消耗法力值
/// </summary>
/// <param name="costSp"></param>
public void ExpendSP(float costSp)
{
SP -= costSp;
}
/// <summary>
/// 回复血量
/// </summary>
public void RestoreHP(float value)
{
}
/// <summary>
/// 回复法力值
/// </summary>
public void RestoreSP(float value)
{
}
//等等其他的改变认为基础数据的方法
//还有增加攻击力,改变双抗,改变韧性,改变移速等
}
}
造成伤害DamageImpactEffect
using System.Collections;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 造成伤害
/// </summary>
public class DamageImpactEffect : IImpactEffect
{
//private SkillData skillData;
/// <summary>
/// 实现造成伤害效果
/// </summary>
/// <param name="deployer"></param>
public void Execute(SkillDeployer deployer)
{
//skillData = deployer.SkillData;
//由于有些技能是持续的,会多次重复造成伤害,所以使用协程写一个重复扣血的方法
//因为文件代码不是挂在实体上,所以无法开启协程,所以使用具体的技能释放器开启协程
deployer.StartCoroutine(RepeatDamage(deployer));
}
/// <summary>
/// 重复造成伤害
/// </summary>
/// <returns></returns>
private IEnumerator RepeatDamage(SkillDeployer deployer)
{
//计时:攻击已经持续的时间
float attackTime = 0;
do
{
//单次造成伤害
OnceDamage(deployer.SkillData);
yield return new WaitForSeconds(deployer.SkillData.atkInterval);
attackTime += deployer.SkillData.atkInterval;
//因为技能持续时间内目标可能会离开攻击范围或者死亡,所以目标需要实时计算
deployer.CalculateTargets();
} while (attackTime < deployer.SkillData.durationTime);//时间小于技能持续时间则再次造成伤害
}
/// <summary>
/// 单次造成伤害
/// </summary>
private void OnceDamage(SkillData skillData)
{
if (skillData.attackTargets == null) return;
foreach (var target in skillData.attackTargets)
{
//计算造成的伤害量:需要结合技能释放者的基础攻击力,破甲/法穿,目标的护甲/魔抗等众多因素
//此处只做简单计算
//如果角色状态类中存在详细的扣血方法,只需简单计算出该方法所需参数即可
float damage = skillData.atkRatio * skillData.owner.GetComponent<CharacterStatus>().BaseAttack;
target.GetComponent<CharacterStatus>().ExpendHP(damage);
}
//创建攻击特效
}
}
}
扇形/圆形攻击选区SectorAttackSelector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 扇形/圆形攻击选区
/// </summary>
public class SectorAttackSelector : IAttackSelector
{
public Transform[] GetTargets(SkillData skill, Transform skillTF)
{
//根据技能数据中的标签,获取所有目标
//skill.attackTargetTags
//string[] --> Transform[]
List<Transform> targets = new List<Transform>();
foreach (var tag in skill.attackTargetTags)
{
List<Transform> transforms = new List<Transform>();
foreach (var item in GameObject.FindGameObjectsWithTag(tag))
{
transforms.Add(item.transform);
}
targets.AddRange(transforms);
}
//判断攻击范围(扇形/圆形)
targets = targets.FindAll(t =>
Vector3.Distance(t.position, skillTF.position) <= skill.attackDistance &&
Vector3.Angle(skillTF.forward, t.position - skillTF.position) <= skill.attackAngle / 2);
//筛选出活着的敌人(HP>0)
targets = targets.FindAll(t => t.GetComponent<CharacterStatus>().HP > 0);
//返回目标,判断技能是单攻还是群攻
//如果是单攻则返回目标中距离最近的
//skill.attackType
if (skill.attackType == SkillAttackType.Group)
return targets.ToArray();
if (targets.Count == 0)
return null;
//判断单攻是,距离最近的目标
float min = Vector3.Distance(skillTF.position, targets[0].position);
Transform value = null;
foreach (var target in targets)
{
float dis = Vector3.Distance(skillTF.position, target.position);
if(dis <= min)
{
min = dis;
value = target;
}
}
return new Transform[] { value };
}
}
}
然后还有具体的技能释放器例如近身释放MeleeSkillDeployer
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
/// <summary>
/// 具体的释放器:近身释放器
/// </summary>
public class MeleeSkillDeployer : SkillDeployer
{
/// <summary>
/// 具体的释放方式
/// </summary>
public override void DeploySkill()
{
//执行选区算法
CalculateTargets();
//执行影响算法
ImpactTargets();
}
}
}
然后我们还可以提供一个技能释放系统CharacterSkillSystem,用于封装技能系统,提供简单的技能释放功能方法,以后要释放技能时只需要在要释放技能的地方,调用此系统里的方法并给一个技能ID,然后系统就会自行释放技能,播放动画,朝向目标等
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Skill
{
[RequireComponent(typeof(CharacterSkillManager))]//效果:实体添加该脚本是自动添加CharacterSkillManager脚本
/// <summary>
/// 角色技能系统类
/// 封装技能系统,提供简单的技能释放功能
/// </summary>
public class CharacterSkillSystem : MonoBehaviour
{
private CharacterSkillManager skillManager;
private Animator animator;
private SkillData skillData;
private void Start()
{
skillManager = GetComponent<CharacterSkillManager>();
animator = GetComponent<Animator>();
}
/// <summary>
/// 生成技能
/// 由于有时播放技能动画时,需要指定在播到某一时刻在生成技能,此处提供生成技能的方法,结合动画事件调用即可
/// </summary>
private void DeploySkill()
{
skillManager.GenerateSkill(skillData);
}
/// <summary>
/// 使用技能攻击(为玩家提供)
/// </summary>
/// <param name="skillID">技能ID</param>
/// <param name="isBatter">是否连击,根据要求实现,也可没有连击</param>
public void AttackUseSkill(int skillID, bool isBatter = false)
{
//设置技能ID为当前技能的下一个连击技能ID
if (skillData != null && isBatter)
skillID = skillData.nextBatterID;
//准备技能
skillData = skillManager.PrepareSkill(skillID);
if (skillData == null) return;
//播放技能动画,此处为测试代码,后续根据实际动画系统的设置进行修改
animator.SetTrigger(skillData.animationName);
//animator.SetBool(skillData.animationName, true);
//生成技能,由于有时播放技能动画时,指定在播到某一时刻在生成技能,所以需要结合动画事件实现,此处不生成
//skillManager.GenerateSkill(skillData);
//如果单攻(skill.attackType == SkillAttackType.Single)
if (skillData.attackType != SkillAttackType.Single) return;
//--查找目标
Transform target = SearchTarget();
//--朝向目标
this.transform.LookAt(target);
//--选中锁定目标等(可不做,根据实际要求)
// 1.选中目标,间隔指定时间后取消选中
// 2.选中A目标,在自动取消选中钱又选中B目标,则需要手动将A目标取消选中
}
private Transform SearchTarget()
{
Transform[] target = new SectorAttackSelector().GetTargets(skillData, this.transform);
//if (target == null) return null;
return target.Length != 0 ? target[0] : null;
}
/// <summary>
/// 使用随机技能攻击(为NPC提供)
/// </summary>
public void UseRandomSkill()
{
//需求 从管理器中挑选随机的技能
//--先产生随机数,在判断技能是否可以释放(不好,可能产生的随机数技能不能释放,然后再次产生的随机数技能依旧不能释放,如此循环)
//--先筛选出所有可以释放的技能,在产生随机数。
var usableSkills = skillManager.skillDatas.FindAll(s => skillManager.PrepareSkill(s.skillID) != null);
if (usableSkills.Count == 0) return;
int index = Random.Range(0, usableSkills.Count);
AttackUseSkill(usableSkills[index].skillID);
}
}
}
释放技能时,会生成一些游戏物体(就是技能特效),这些特效需要频繁的创建销毁,所以使用对象池来存储创建的游戏物体。然后创建游戏物体时我选择的是用Resources.Load<GameObject>方法,需要一个路径,所以我这还写了一个创建资源映照表(键值对形式,key为预制体名称,value为预制体在Resources文件夹下的路径)的方法GenerateResConfig以及解析映照表的方法ResourceManager,还有一个读取配置文件获取文件内容的方法ConfigurationReader
读取配置文件获取文件内容ConfigurationReader
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace Common
{
/// <summary>
/// 配置文件读取器
/// </summary>
public class ConfigurationReader
{
/// <summary>
/// 通过文件名称读取文件内容
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string GetConfigFile(string fileName)
{
string url;
#region 分平台判断StreamingAssets路径
//string url = "file://" + Application.streamingAssetsPath + "/" + fileName;
//如果在编译器或单机中
//if(Application.platform == RuntimePlatform.Android)
//Unity宏标签
#if UNITY_EDITOR || UNITY_STANDALONE
url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
//否则如果在Iphone下
#elif UNITY_IPHONE
url = "file://" + Application.dataPath + "/Raw/" + fileName;
//否则如果在android下
#elif UNITY_ANDROID
url = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
#endif
#endregion
WWW www = new WWW(url);
while(true)
{
if (www.isDone)
return www.text;
}
}
/// <summary>
/// 解析文件内容
/// </summary>
/// <param name="fileContent"></param>
/// <param name="handler"></param>
public static void Reader(string fileContent, Action<string> handler)
{
//StringReader字符串读取器,提供了逐行读取字符串功能
using (StringReader reader = new StringReader(fileContent))
{
//一行一行的读取
//当内容不为空时,使用特定方法解析单行信息
string line;
while((line = reader.ReadLine()) != null)
{
handler(line);
}
}//当程序退出using代码块,会主动调用reader.Dispose(),无论是正常退出还是异常退出
}
}
}
创建资源映照表GenerateResConfig,如果没有Resources文件夹可以手动创建并随便给一些游戏物体
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
/*
1.编译器代码:继承自Editor类,只需要在unity编译器中执行的代码
2.菜单项 特性 [MenuItem("****")]:用于修饰需要在unity编译器中产生菜单按钮的方法
3.AssetDatabase:包含了只适用于unity编译器中操作资源的相关功能
4.StreamingAssets:unity特殊目录之一,该目录中的文件不会被压缩,适合在移动端读取资源(在PC端还可以写入)
持久化路径:Application.persistentDataPath(软件安装时才产生) 该路径可以在运行时进行读写操作,unity外部目录
*/
/// <summary>
/// 生成资源映射表
/// </summary>
public class GenerateResConfig : Editor
{
[MenuItem("Tools/Resources/Generate ResConfig File")]
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.写入文件
File.WriteAllLines("Assets/StreamingAssets/ResConfig.txt", resFiles);
//手动刷新工程
AssetDatabase.Refresh();
}
}
解析映照表ResourceManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
/// <summary>
/// 资源管理器
/// </summary>
public class ResourceManager
{
private static Dictionary<string, string> configMap;
/// <summary>
/// 作用:初始化类的静态数据成员
/// 时机:类被加载时执行一次
/// </summary>
static ResourceManager()
{
//加载文件
string fileContent = ConfigurationReader.GetConfigFile("ResConfig.txt");
configMap = new Dictionary<string, string>();
ConfigurationReader.Reader(fileContent, BuildMap);
}
/// <summary>
/// 解析文件内容
/// </summary>
/// <param name="line">每一行的数据</param>
private static void BuildMap(string line)
{
string[] key_Value = line.Split('=');
configMap.Add(key_Value[0], key_Value[1]);
}
/// <summary>
/// 加载资源
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="prefabName"></param>
/// <returns></returns>
public static T Load<T>(string prefabName) where T : Object
{
//prefabName --> prefabPath
return Resources.Load<T>(configMap[prefabName]);
}
}
}
最后就是存储技能特效的对象池GameObjectPool,对象池是一个单例模式,所以还有一个脚本单例工具MonoSingleton
脚本单例工具MonoSingleton
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
//namespace 域名.项目名称.模块
/// <summary>
/// 脚本单例工具类
/// 特点:
/// 1.唯一
/// 2.常用
/// 功能:
/// 1.保持一个游戏对象的单一,整个进程只有这一个对象
/// 2.对单一游戏对象的控制和调用
/// 解释:
/// 泛型类<T>,为了子类可以传递类型
/// 泛型约束:类型参数必须是指定的基类或派生自指定的基类。
/// 子类需要继承此单例类
/// 如何使用:
/// 1.继承时,必须传递子类类型
/// 2.在任意脚本生命周期中,通过子类类型访问Instance属性, XXXX.Instance.XXX
/// </summary>
public class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
// T 代表子类型 通过 where T : MonoSingleton<T> 来体现
//按需加载
private static T instance;
//只能读取get
public static T Instance
{
get
{
if (instance == null)
{
//在场景中根据类型来查找引用
//只执行一次
instance = FindObjectOfType<T>();
//场景中没这个类型==游戏对象未挂载脚本
if (instance == null)
{
//创建一个脚本对象(立即执行Awake)
new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
}
else
{
//如果找到了子类,立即初始化
instance.Init();
}
}
return instance;
}
}
protected void Awake()
{
//脚本自行挂载到游戏物体上了,调Awake
if (instance == null)
{
//子类 = 父类 as(强转成) 子类
//泛型约束 Where T : MonoSingleton<T>
instance = this as T;
Init();
}
}
/// <summary>
/// 因为继承Unity的类不是依靠构造函数实例化的,所以要新建一个Init函数,通过重写Override实现一些单例的初始化。
/// 后代做初始化,不需要用awake,自行初始化
/// </summary>
public virtual void Init()
{
}
}
}
对象池GameObjectPool
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
/*
使用方法:
1.所有频繁创建/销毁的物体,都通过对象池创建/回收
GameObject go = GameObjectPool.Instance.CreateObject("类别", 预制体, 位置, 旋转);
GameObjectPool.Instance.CollectObject(游戏物体);
2.需要通过对象池创建的物体,如果需要每次创建时执行某些计算或其他操作,则让其脚本实现IAwakeAble接口
*/
/// <summary>
/// 对象池
/// </summary>
public class GameObjectPool : MonoSingleton<GameObjectPool>
{
/// <summary>
/// 每次显示可执行,接口
/// </summary>
public interface IAwakeAble
{
void OnAwake();
}
//对象池
private Dictionary<string, List<GameObject>> cache;
public override void Init()
{
base.Init();
cache = new Dictionary<string, List<GameObject>>();
}
/// <summary>
/// 通过对象池,创建对象
/// </summary>
/// <param name="key">类别</param>
/// <param name="prefab">预制体</param>
/// <param name="position">物体位置</param>
/// <param name="rotation">物体旋转</param>
/// <returns></returns>
public GameObject CreateObject(string key, GameObject prefab, Vector3 position, Quaternion rotation)
{
GameObject go = null;
//查找指定类中可以使用的对象
if(cache.ContainsKey(key))
go = cache[key].Find(s => !s.activeInHierarchy);
//如果对象池中没有对象,则创建对象并加入池中
if(go == null)
{
go = Instantiate(prefab);
//如果池中没有key,则添加记录
if (!cache.ContainsKey(key))
cache.Add(key, new List<GameObject>());
cache[key].Add(go);
}
//使用对象
go.transform.position = position;
go.transform.rotation = rotation;
go.SetActive(true);
//当对象创建时需要执行某些计算子类的方法,可以此时执行,对象实现OnAwake方法即可
foreach (var awakwAble in go.GetComponents<IAwakeAble>())
{
awakwAble.OnAwake();
}
return go;
}
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">需要被回收的对象</param>
/// <param name="delay">延迟时间</param>
public void CollectObject(GameObject go, float delay = 0)
{
StartCoroutine(CollectObjectDelay(go, delay));
}
/// <summary>
/// 延迟回收对象
/// </summary>
/// <param name="go"></param>
/// <param name="delay"></param>
/// <returns></returns>
private IEnumerator CollectObjectDelay(GameObject go, float delay)
{
yield return new WaitForSeconds(delay);
go.SetActive(false);
}
/// <summary>
/// 清空key对应的对象池数据
/// </summary>
/// <param name="key"></param>
public void Clear(string key)
{
if(cache.ContainsKey(key))
{
foreach (var obj in cache[key])
{
Destroy(obj);
}
cache.Remove(key);
}
}
/// <summary>
/// 清空对象池所有对象
/// </summary>
public void ClearAll()
{
//应为Clear方法内有remove移除字典的数据,所以需要用一个临时的列表存储keys
//foreach (var key in new List<string>(cache.Keys))
//{
// Clear(key);
//}
//效果同上
List<string> keys = new List<string>(cache.Keys);
foreach (var key in keys)
{
Clear(key);
}
cache.Clear();
}
}
}