前言
alex教程观后感
关于耦合
我认为耦合分两种,一种是代码的耦合,一种是逻辑的耦合
代码的耦合源于依赖关系。比如说相互依赖,玩家依赖于技能的释放,技能又依赖于玩家的角色属性,不可能手动调用每个技能的接口,所以将技能抽象,让调用技能只依赖于抽象而不是实现。技能方面通过设置owner调用者,可以方便的获取属性,从而解除依赖(也算是依赖注入了)
逻辑的耦合大多不可避免,但也有例外,比如在攻击时通过管理类调用武器的特效,如果换成广播事件,由武器自行订阅,就可以完全解耦。有时换个思路也可以减少耦合
在我看来优质的代码,最理想的情况是一个改动只需要改一处地方,因为要加枚举避免字符串,改两个地方也能接受,超过三个地方都是为了快速开发没时间去设计框架。
正文
因为buff系统比较简单,就放在前面
Buff系统
buff系统一般分为两种,一种是Manager全局管理,一种是依附于角色自己管自己,我的是后者
核心是用反射实例化buff,然后根据类型填充数据,添加新的buff类只需要增加对应枚举和对应buff数据
buff数据
使用so管理buff数据,数据和buff执行逻辑分离,这里因为不想每次调用都在前面加buffData,所以写在一起
[CreateAssetMenu(menuName = "Buff/BuffDataSO")]
public class BuffDataSO : ScriptableObject
{
public List<BuffBase> buffs;
public BuffBase GetBuff(BuffType buffType)
{
return buffs.Find(x => x.buffType == buffType);
}
public void OnEnable()
{
foreach (var buff in buffs)
{
buff.timer = 0;
buff.currentTarget = null;
}
}
}
[System.Serializable]
public class BuffBase
{
public static BuffType realBuffType;
public BuffType buffType;
public BuffOverlap buffOverlap;
public BuffCloseType buffCloseType;
public BuffCalculateType buffCalculateType;
public int effectValue;//效果值
public int maxLimit;//最大次数
public int level;//层数
public float interalTime;//间隔时间
public float durationTime;//持续时间
[HideInInspector] public float timer;//计时器
[HideInInspector] public CharacterStats currentTarget;
[HideInInspector] public CharacterStats deployStats;
public virtual void OnStart()
{
}
public virtual void OnEffect()
{
if (timer >= durationTime)
{
currentTarget.RemoveBuff(this);
}
timer += Time.fixedDeltaTime;
}
public virtual void OnEnd()
{
timer = 0;
}
#region 简写
public EntityFX Fx => currentTarget.fx != null ? currentTarget.fx : null;
#endregion
}
对应枚举
public enum BuffType
{
None,
Ignited,//被点燃
Frozen,//被冻结
Shocked,//被震撼
}
public enum BuffOverlap //叠加类型
{
None,
StackedTime,//增加时间
StackedLayer,//增加层数
ResterTime,//重置时间
}
public enum BuffCloseType
{
All,//全部关闭
Layer,//逐层关闭
}
public enum BuffCalculateType//执行类型
{
Once,//一次
Loop,//每次
}
Buff工厂
这里将数据赋值给buff实例,虽然用了反射,但是做了缓存也影响不大
public class BuffFactory : SingletonMono<BuffFactory>
{
// 类型缓存
private static Dictionary<string, Type> typeCache = new Dictionary<string, Type>();
// 属性缓存
private static ConcurrentDictionary<Type, FieldInfo[]> PropertyCaches = new();
public BuffDataSO buffData;
private Dictionary<BuffType, BuffBase> buffBases = new();
public BuffBase CreateBuff(BuffType buffType)// 创建对应算法
{
string className = string.Format("Platform.Buff.Buffs.{0}Buff", buffType.ToString());
if (!typeCache.TryGetValue(className, out Type type))
{
// 如果缓存中没有该类型,反射获取并缓存
type = Type.GetType(className);
if (type != null)
{
typeCache[className] = type;
}
else
{
Debug.LogError($"Type {className} not found.");
return null;
}
}
//给buff填充数据
var BuffData = buffData.GetBuff(buffType);
BuffBase buff = Activator.CreateInstance(type) as BuffBase;
if (buff != null) CopyProperties(BuffData, buff);
return buff;
}
public static void CopyProperties<T>(T source, T target)
{
if (source == null || target == null)
throw new ArgumentNullException("Source or target cannot be null.");
var properties = PropertyCaches.GetOrAdd(typeof(T), t => t.GetFields());
foreach (var property in properties)
{
var value = property.GetValue(source);
property.SetValue(target, value);
}
}
}
在角色类添加buff
public void AddBuff(BuffType buffType, CharacterStats sourceStats = null)
{
var buff = buffs.FirstOrDefault(b => b.buffType == buffType);
if (buff != null)
{
//处理覆盖情况
switch (buff.buffOverlap)
{
case Buff.BuffOverlap.None:
//无视
break;
case Buff.BuffOverlap.StackedTime:
buff.timer = buff.interalTime;
break;
case Buff.BuffOverlap.StackedLayer:
buff.level++;
break;
case Buff.BuffOverlap.ResterTime:
RemoveBuff(buff);
AddBuff(buffType);
break;
}
}
else
{
buff = BuffFactory.Instance.CreateBuff(buffType);
buff.currentTarget = this;
buff.deployStats = sourceStats;
buffs.Add(buff);
buff.OnStart();
}
}
public void ReFreshBuff()
{
// 后加入先执行
for (int i = buffs.Count - 1; i >= 0; i--)
{
buffs[i].OnEffect();
}
}
public void RemoveBuff(BuffType buffType)
{
var buff = buffs.FirstOrDefault(b => b.buffType == buffType);
if (buff != null)
{
RemoveBuff(buff);
}
}
public void RemoveBuff(BuffBase buff)
{
buffs.Remove(buff);
buff.OnEnd();
}
燃烧buff案例
public class IgnitedBuff : BuffBase
{
private float fireTimer = 0;
public override void OnStart()
{
base.OnStart() ;
ZTimer.SetInterval(0.2f, Fx.FireColorFx);
}
public override void OnEnd()
{
base.OnEnd();
ZTimer.ClearTimer(Fx.FireColorFx);
Fx.CancelColorChange();
}
public override void OnEffect()
{
fireTimer += Time.fixedDeltaTime;
if (fireTimer >= interalTime)
{
currentTarget.CurrentHealth -= effectValue;
currentTarget.OnHealthChange?.Invoke();
fireTimer = 0;
}
base.OnEffect();
}
}
技能系统
主要参考:https://zhuanlan.zhihu.com/p/513705768
建议先看原文,讲的比我更好,这里受限时间也只能放代码了。主要是根据实际情况增加了一点内容,改的最大的可能就是给反射加了个缓存和对象池:)
还有就是闪电链,原来的架构感觉少了一层不好实现,SO的特性刚好补上了
大致逻辑:
技能数据
因为SO文件相当于全局变量,所以多个释放器也可以通过共有的那一份数据管理,比如闪电链同时有多个实例,又需要记录一个共有的最后命中的敌人。好处是释放器上面不用再加一层管理释放器的类
因此最好怪物和角色技能用不同的SO存储
说是释放器,准确的讲是控制器更合适
[CreateAssetMenu(menuName = "skill/SkillDataSO")]
public class SkillDataSO : ScriptableObject
{
public List<SkillData> skills;
public void OnEnable()
{
foreach (var skill in skills)
{
var fields = typeof(SkillData).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
foreach (var field in fields)
{
if (System.Attribute.IsDefined(field, typeof(HideInInspector)))
{
switch (field.FieldType.Name)
{
case "Single": // float
field.SetValue(skill, 0f);
break;
case "Int32": // int
field.SetValue(skill, 0);
break;
case "GameObject":
case "Image":
case "Transform":
field.SetValue(skill, null);
break;
case "HashSet`1":
field.SetValue(skill, new HashSet<Transform>());
break;
case "List`1":
field.SetValue(skill, new List<GameObject>());
break;
case "Vector3":
field.SetValue(skill, Vector3.zero);
break;
default:
break;
}
}
}
}
}
}
[System.Serializable]
public class SkillData
{
[HideInInspector] public GameObject owner;//技能所属的角色
public int skillId;
public string name;//技能名称
public float cooldown;
[HideInInspector] public float cdRemain;
public int level;//技能等级
public int costMp;//法力值消耗
public float range;
public float Distance;
public int attackDamage;
[Header("攻击间隔")]
public float attackInterval;//伤害间隔
[Header("持续时间")]
public float durationTime;//持续时间
[Header("攻击类型")]
public DamageType damageType;
[Header("攻击目标")]
public SkillAttackType attackType;//攻击目标
/// <summary>
/// 攻击的冻结时间
/// </summary>
[Header("冻结时间")]
public float freezeTime;
[Header("技能状态")]
public States[] stateNames;//状态名称
[Header("技能影响类型")]
public ImpactEnum[] impactType;//技能影响类型
[Header("释放范围类型")]
public SelectorTypeEnum selectorType;//释放范围类型(圆形,扇形,矩形)
[Header("技能指示器")]
public SkillIndicatorEnum skillIndicator;//技能指示器名字
// public int nextBatterld;//连击的技能ID
[Header("技能预制体")]
public string prefabName;//技能预制体名称
[HideInInspector] public GameObject skillPrefab;//预制体对象
public LayerMask attackTargetLayers;//能作用的目标Tag
[HideInInspector] public HashSet<Transform> attackTargets;//作用目标对象数组
// public string skillIndicator;//技能指示器名字|成功释放技能之前显示出来的辅助技能释放的工具
[Header("技能图标")]
public string skillIconName;//技能显示图标名字
[HideInInspector] public Image skillIcon;//技能事件图标
[Header("攻击持续类型")]
public CalculDamageType disappearType;
//技能生成位置, 处理重复在指定位置生成
[HideInInspector] public Vector3 targetPos;
[Header("被动属性")]
public float chanceActive;
/// <summary>
/// 连击技能 处理传送
/// </summary>
public SkillEnum nextSkill;
[Header("远程攻击属性")]
public Vector2 launchForce;
public float gravity;
[Header("多段攻击属性")]
public int comboCount;
[HideInInspector] public int combo;
[Header("穿透属性")]
public int pierceCount;
[HideInInspector] public int pierced;
[Header("装填类技能")]
public int backfillCount;
[HideInInspector] public List<GameObject> backfills;
//给闪电链用的
[HideInInspector] public Transform lastTarget;
}
对应枚举
// 数字对应id 也可以直接用枚举
public enum SkillEnum
{
None = 0,
Dash = 1,
Clone = 2,
ReBoundSword = 3,
PierceSword = 4,
SpinSword = 5,
flySword = 6,
Blackhole = 7,
Crystal = 8,
ExplodeCrstal = 9,
ThunderStrike = 10,
}
public enum DamageType
{
Normal,
Magic,
OwnerAttack,
OwnerMagic,
}
public enum ImpactEnum
{
Dash,
Clone,
ReBound,
Pierce,
Spin,
Insert,
Transfer,
}
public enum SkillAttackType
{
single,
aoe,
}
public enum SelectorTypeEnum
{
None,
Circle,
Nearly,
}
public enum CalculDamageType
{
Once,
Continue,
}
public enum SkillIndicatorEnum
{
None,
Dotted,
}
核心管理器
public class SkillMgr : MonoBehaviour
{
const string SKILL_PATH = "SkillPrefab/";
public SkillDataSO skillDataList;
public bool isSkillChoose;
public SkillData currentSkill;
private Entity entity;
private BaseSkillIndicator skillIndicator;
// 避免填充技能重复执行冷却
private bool isColldown;
void Start()
{
if (skillDataList != null)
{
// skillData.ResetSkills();
Init();
}
entity = GetComponent<Entity>();
}
void Init()
{
foreach (var skillData in skillDataList.skills)
{
if (!skillData.prefabName.IsNullOrEmpty())
{
// PoolMgr.Instance.CreatePool(SKILL_PATH + skill.prefabName, 5);
// skill.skillPrefab = Resources.Load<GameObject>("/SkillPrefab" + skill.prefabName);
skillData.owner = gameObject;
skillData.attackTargets = new HashSet<Transform>();
// 提前填充 技能预制体
if (skillData.backfillCount > 0)
{
skillData.backfills = new List<GameObject>();
for (int i = 0; i < skillData.backfillCount; i++)
{
BackfillObj(skillData);
}
}
}
}
}
#region 按键触发
void OnEnable()
{
PlayerInput.Instance.inputActions.Player.UseSkill.performed += OnUseSkill;
PlayerInput.Instance.inputActions.Player.CloseSkill.performed += OnCloseSkill;
}
void OnDisable()
{
PlayerInput.Instance.inputActions.Player.UseSkill.performed -= OnUseSkill;
PlayerInput.Instance.inputActions.Player.CloseSkill.performed -= OnCloseSkill;
}
private void OnCloseSkill(UnityEngine.InputSystem.InputAction.CallbackContext context)
{
if (isSkillChoose)
{
CloseSkillIndicator();
}
}
/// <summary>
/// 技能通过动画调用就不会走这里
/// </summary>
/// <param name="context"></param>
private void OnUseSkill(UnityEngine.InputSystem.InputAction.CallbackContext context)
{
if (isSkillChoose)
{
if (currentSkill.stateNames.Length > 0)
{
StartCoroutine(ChangeStateAsync(currentSkill.stateNames));
}
else
UseSkill(currentSkill.skillId);
}
IEnumerator ChangeStateAsync(States[] states)
{
foreach (var state in states)
{
yield return new WaitForEndOfFrame();
entity.ChangeState(state);
}
}
}
#endregion
#region 使用技能
public SkillData PrepareSkill(SkillEnum skillEnum)
{
SkillData skillData = skillDataList.skills.Find(x => x.skillId == (int)skillEnum);
if (skillData.backfillCount > 0)
{
return skillData;
}
if (skillData != null && skillData.cdRemain <= 0)//这里还有技能消耗值的判断
return skillData;
else
return null;
}
/// <summary>
/// 生成技能
/// </summary>
/// <param name="skillEnum"></param>
/// <param name="targetPos">技能生成和目标位置</param>
/// <returns></returns>
public SkillData UseSkill(SkillEnum skillEnum, Vector3 targetPos = default)
{
// todo 暂时写在这里,统一的处理
if (isSkillChoose) CloseSkillIndicator();
var skillData = PrepareSkill(skillEnum);
if (skillData == null) return null;
currentSkill = skillData;
if (!skillData.prefabName.IsNullOrEmpty())
{
GameObject skillPrefab = null;
// 自指多段技能,不重复创建预制体
// 传送的需求是自身被再次创建时触发下一段, 依赖于自身 无法使用事件
if (skillEnum == skillData.nextSkill)
{
if (skillData.skillPrefab != null)
{
skillPrefab = skillData.skillPrefab;
}
else
{
var prefab = Resources.Load<GameObject>(SKILL_PATH + skillData.prefabName);
skillPrefab = Instantiate(prefab, transform.position, transform.rotation);
}
skillPrefab.SetActive(true);
}
// 填充式技能
else if (skillData.backfillCount > 0)
{
if (skillData.backfills.Count > 0)
{
skillPrefab = skillData.backfills[0];
skillPrefab.SetActive(true);
skillData.backfills.RemoveAt(0);
}
}
else
{
//创建技能预制体
skillPrefab = PoolMgr.Instance.GetObj(SKILL_PATH + skillData.prefabName, transform.position, transform.rotation);
}
skillData.skillPrefab = skillPrefab;
if (targetPos != default)
{
skillPrefab.transform.position = targetPos;
skillData.targetPos = targetPos;
}
//传递技能数据给技能释放器
SkillDeployer deployer = skillPrefab != null ? skillPrefab.GetComponent<SkillDeployer>() : null;
if (deployer != null)
{
deployer.SkillData = skillData;
//释放器释放技能
deployer.DeploySkill();
}
}
//防止装填的冷却重复触发
if (!isColldown)
StartCoroutine(CoolTimeDown(skillData));
return skillData;
}
public SkillData UseSkill(int id)
{
if (Enum.TryParse(id.ToString(), out SkillEnum skillEnum))
{
return UseSkill(skillEnum);
}
else
{
return null;
}
}
/// <summary>
/// 装填技能预制体
/// </summary>
private void BackfillObj(SkillData skillData)
{
Debug.Log("装填技能预制体");
var obj = PoolMgr.Instance.GetObj(SKILL_PATH + skillData.prefabName);
obj.SetActive(false);
skillData.backfills.Add(obj);
}
//协程实现技能冷却
private IEnumerator CoolTimeDown(SkillData skillData)
{
isColldown = true;
skillData.cdRemain = skillData.cooldown;
while (skillData.cdRemain > 0)
{
yield return new WaitForSeconds(0.1f);
skillData.cdRemain -= 0.1f;
}
isColldown = false;
//冷却完毕,如果是填充技能,递归
if (skillData.backfillCount > 0)
{
if (skillData.backfills.Count < skillData.backfillCount)
{
BackfillObj(skillData);
StartCoroutine(CoolTimeDown(skillData));
}
}
}
#endregion
#region 技能指示器
public bool OpenSkillIndicator(SkillEnum skillEnum)
{
if (isSkillChoose) return false;
var skill = PrepareSkill(skillEnum);
currentSkill = skill;
if (skill == null || skill.skillIndicator == SkillIndicatorEnum.None)
{
Debug.Log("不存在技能指示器");
return false;
}
else
{
Debug.Log("打开技能指示器");
isSkillChoose = true;
SelectSpellIndicator(skill.skillIndicator);
return true;
}
}
public void CloseSkillIndicator()
{
isSkillChoose = false;
entity.ChangeState(Entity.States.Idle);
CancelSpellIndicator();
}
// 此处耦合,考虑反射或映射
private void SelectSpellIndicator(SkillIndicatorEnum skillIndicatorType)
{
switch (skillIndicatorType)
{
case SkillIndicatorEnum.Dotted:
skillIndicator = GetComponent<DottedIndicator>() ?? gameObject.AddComponent<DottedIndicator>();
break;
default:
skillIndicator = null;
break;
}
if (skillIndicator != null)
StartCoroutine(ShowIndicator());
IEnumerator ShowIndicator()
{
skillIndicator.skillData = currentSkill;
skillIndicator.entity = entity;
yield return null; // 等待下一帧
skillIndicator?.Show();
}
}
private void CancelSpellIndicator()
{
skillIndicator?.Hide();
}
#endregion
技能释放器
释放器工厂
public class DeployerConfigFactory : MonoBehaviour
{
// 一个字典用以缓存类型
private static Dictionary<string, Type> typeCache = new Dictionary<string, Type>();
public static ISkillSelector CreateSkillSelector(SkillData data) // 范围选择算法
{
if (data.selectorType == SelectorTypeEnum.None) return null;
// 根据选择类型构建类名
string className = string.Format("GardeningSkeleton.Platform.SkillSpace.{0}SkillSelector", data.selectorType.ToString());
return CreateObject<ISkillSelector>(className);
}
public static BaseImpactEffect[] CreateImpactEffects(SkillData data) // 效果算法
{
BaseImpactEffect[] impacts = new BaseImpactEffect[data.impactType.Length];
for (int i = 0; i < data.impactType.Length; i++)
{
string className = string.Format("GardeningSkeleton.Platform.SkillSpace.{0}Impact", data.impactType[i].ToString());
impacts[i] = CreateObject<BaseImpactEffect>(className);
}
return impacts;
}
private static T CreateObject<T>(string className) where T : class // 创建对应算法
{
if (!typeCache.TryGetValue(className, out Type type))
{
// 如果缓存中没有该类型,反射获取并缓存
type = Type.GetType(className);
if (type != null)
{
typeCache[className] = type;
}
else
{
Debug.LogError($"Type {className} not found.");
return null;
}
}
return Activator.CreateInstance(type) as T;
}
}
public class SkillDeployer : MonoBehaviour
{
[HideInInspector] public SpriteRenderer sr;
[HideInInspector] public Animator anim;
[HideInInspector] public Rigidbody2D rb;
protected SkillData skillData;
public SkillData SkillData //技能管理器提供
{
get { return skillData; }
set { skillData = value; InitDeplopyer(); }
}
//范围选择算法
private ISkillSelector selector;
//效果算法对象
private BaseImpactEffect[] impactArray;
/// <summary>
/// 初始化释放器 在设置技能时调用
/// </summary>
private void InitDeplopyer()//初始化释放器
{
//范围选择
selector = DeployerConfigFactory.CreateSkillSelector(skillData);
//效果
impactArray = DeployerConfigFactory.CreateImpactEffects(skillData);
}
//范围选择
public void chooseTargets()
{
if (selector != null) skillData.attackTargets = selector?.SelectTarget(skillData, this.transform);
}
/// <summary>
/// 执行所有效果, 自动赋值常用变量
/// </summary>
public void ImpactTargets()
{
for (int i = 0; i < impactArray.Length; i++)
{
impactArray[i].skillData = SkillData;
impactArray[i].Execute(this);
}
}
/// <summary>
/// 结束效果, 实际是传递给所有效果
/// </summary>
public virtual void EndEffect()
{
for (int i = 0; i < impactArray.Length; i++)
{
impactArray[i].EndEffect();
}
}
//供技能管理器调用,由子类实现,定义具体释放策略
public virtual void DeploySkill()
{
//范围选择
chooseTargets();
//执行影响算法
ImpactTargets();
}
#region 检测范围绘制
public float rangeTemp;
protected virtual void OnDrawGizmos()
{
// 绘制圆形
Gizmos.color = Color.red; // 设置圆形颜色
Gizmos.DrawWireSphere(transform.position, rangeTemp); // 绘制半径为 data.range 的圆形
}
#endregion
#region 物理碰撞
protected virtual void OnTriggerEnter2D(Collider2D other)
{
for (int i = 0; i < impactArray.Length; i++)
{
impactArray[i].OnImpactTriggerEntry(other);
}
}
protected virtual void OnTriggerExit2D(Collider2D other)
{
for (int i = 0; i < impactArray.Length; i++)
{
impactArray[i].OnImpactTriggerExit(other);
}
}
#endregion
#region 触发伤害
private bool isAttack = false;
// 伤害的触发时机差异很大,不适合作为效果
public virtual void AttackStart()
{
switch (skillData.disappearType)
{
case CalculDamageType.Once:
Attack();
break;
case CalculDamageType.Continue:
if (isAttack) return;
isAttack = true;
ZTimer.SetInterval(skillData.attackInterval, Attack);
break;
}
}
/// <summary>
/// 敌人选取两种形式: 碰撞 、 射线检测
/// </summary>
/// <param name="other"></param>
public virtual void AttackStart(Collider2D other)
{
AttackStart(other.transform);
}
public virtual void AttackStart(Transform other)
{
if (Tool.IsInLayerMask(other.gameObject, skillData.attackTargetLayers))
{
//这里区分单体攻击
if (skillData.attackType == SkillAttackType.single)
{
skillData.attackTargets.Clear();
}
skillData.attackTargets.Add(other);
}
AttackStart();
}
public virtual void AttackEnd()
{
isAttack = false;
ZTimer.ClearTimer(Attack);
}
private void Attack()
{
if (skillData.attackTargets?.Count > 0)
{
foreach (var entity in skillData.attackTargets.ToArray())
{
var enemyEntity = entity.GetComponent<Enemy>();
if (enemyEntity != null) enemyEntity.StartCoroutine(enemyEntity.FreezeTimeFor(skillData.freezeTime));
switch (skillData.damageType)
{
case DamageType.Normal:
case DamageType.Magic:
entity.GetComponent<CharacterStats>().BeDamage(OwnerEntity.stats, skillData.attackDamage, skillData.damageType);
break;
case DamageType.OwnerAttack:
//使用原始攻击力
entity.GetComponent<CharacterStats>().BeDamage(OwnerEntity.stats);
break;
case DamageType.OwnerMagic:
//使用原始魔法攻击+技能攻击
entity.GetComponent<CharacterStats>().BeMagicalDamage(OwnerEntity.stats, skillData.attackDamage);
break;
}
}
}
}
#endregion
#region 简写
public Entity OwnerEntity => skillData.owner.GetComponent<Entity>();
public Vector3 OwnerPos => skillData.owner.transform.position;
public int OwnerDamage => OwnerEntity.stats.damage.Value;
public void Release()
{
PoolMgr.Instance.Release(this.gameObject);
}
#endregion
案例
以抛剑为例,剑需要回收,但又有投掷物特性,所以有继承
/// <summary>
/// 抛物线技能释放器
/// </summary>
public class ParabolicSkillDeployer : SkillDeployer
{
public Player player;
protected Collider2D cd;
protected Vector2 finalDir;
void Awake()
{
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
cd = GetComponent<Collider2D>();
// cd = GetComponent<CircleCollider2D>();
player = PlayerMgr.Instance.player;
}
void Start()
{
player = PlayerMgr.Instance.player;
}
void Update()
{
}
void OnEnable()
{
cd.enabled = true;
rb.isKinematic = false;
rb.constraints = RigidbodyConstraints2D.None;
}
// 由效果决定是否冻结
public virtual void StopRigidbody()
{
cd.enabled = false;
rb.isKinematic = true;
// 冻结所有 自由度(位置和旋转)
rb.constraints = RigidbodyConstraints2D.FreezeAll;
}
public override void DeploySkill()
{
finalDir = new Vector2(Tool.GetMouseDirection().normalized.x * skillData.launchForce.x,
Tool.GetMouseDirection().normalized.y * skillData.launchForce.y);
rb.velocity = finalDir;
rb.gravityScale = skillData.gravity;
base.DeploySkill();
}
/// <summary>
/// 让效果只依赖此接口
/// </summary>
public virtual void ReturnSword() { }
public virtual void SetRotate(bool value) { }
}
/// <summary>
/// 抛剑 释放器
/// </summary>
public class ThrowSwordSkillDeployer : ParabolicSkillDeployer
{
private float returnSpeed = 20;
public bool isReturning;
/// <summary>
/// 能够转向
/// </summary>
public bool canRotate = true;
void Update()
{
if (canRotate)
transform.right = -rb.velocity;
if (isReturning)
{
transform.position = Vector2.MoveTowards(transform.position
, player.transform.position, returnSpeed * Time.deltaTime);
var distance = Vector2.Distance(transform.position, player.transform.position);
if (distance < 0.1f)
{
player.ChangeState(Entity.States.CatchSword);
DestroySword();
}
}
}
void DestroySword()
{
isReturning = false;
if (player.sword = null) return;
//再次关闭效果
// EndEffect();
player.sword = null;
// 回收预制体
PoolMgr.Instance.Release(skillData.skillPrefab);
}
protected override void OnTriggerEnter2D(Collider2D other)
{
if (isReturning) return;
base.OnTriggerEnter2D(other);
}
public override void DeploySkill()
{
base.DeploySkill();
canRotate = true;
isReturning = false;
player.sword = gameObject;
// 距离过远超过时间自动销毁
// 创建太快多次调用会 有重复销毁bug
ZTimer.ClearTimer(DestroySwordByDistance);
ZTimer.SetTimeout(5f, DestroySwordByDistance);
}
private void DestroySwordByDistance()
{
var distance = Vector2.Distance(transform.position, player.transform.position);
if (distance > 15f)
{
DestroySword();
}
}
public override void EndEffect()
{
ZTimer.ClearTimer(DestroySwordByDistance);
base.EndEffect();
}
/// <summary>
/// 返回剑
/// </summary>
public override void ReturnSword()
{
// deployer.rb.isKinematic = false;//受物理控制
rb.constraints = RigidbodyConstraints2D.FreezeAll;
transform.parent = null;
isReturning = true;
}
public override void SetRotate(bool value)
{
base.SetRotate(value);
canRotate = value;
}
public override void StopRigidbody()
{
base.StopRigidbody();
canRotate = false;
}
}
技能效果
public interface IImpactEffect //效果算法接口
{
void Execute(SkillDeployer baseDeployer);
void EndEffect();
}
public abstract class BaseImpactEffect : IImpactEffect
{
public SkillData skillData;
public abstract void EndEffect();
public abstract void Execute(SkillDeployer baseDeployer);
public virtual void OnImpactTriggerEntry(Collider2D other){}
public virtual void OnImpactTriggerExit(Collider2D other){}
void AddUpdateEvent(UnityAction action)
{
MonoMgr.Instance.AddUpdateEvent(action);
}
void RemoveUpdateEvent(UnityAction action)
{
MonoMgr.Instance.RemoveUpdateEvent(action);
}
#region 简写
public Entity OwnerEntity => skillData.owner.GetComponent<Entity>();
public Vector3 OwnerPos => skillData.owner.transform.position;
public int OwnerDamage => OwnerEntity.stats.damage.Value;
#endregion
}
以穿刺效果为例
public class PierceImpact : BaseImpactEffect
{
ParabolicSkillDeployer deployer;
public override void EndEffect()
{
deployer.ReturnSword();
}
public override void Execute(SkillDeployer baseDeployer)
{
deployer = (ThrowSwordSkillDeployer)baseDeployer;
// MonoMgr.Instance.AddUpdateEvent(PierceUpdate);
}
public override void OnImpactTriggerEntry(Collider2D other)
{
deployer.AttackStart(other);
if (skillData.pierced < skillData.pierceCount && other.gameObject.layer == LayerMask.NameToLayer("Enemy"))
{
skillData.pierced++;
return;
}
skillData.pierced = 0;
deployer.StopRigidbody();
deployer.transform.parent = other.transform;
}
private void PierceUpdate()
{
}
技能选择器
以圆形范围为例
public interface ISkillSelector //范围选择算法接口
{
HashSet<Transform> SelectTarget(SkillData skillData, Transform skillPrefab);//skillTF是技能预制体
}
public class CircleSkillSelector : ISkillSelector
{
public HashSet<Transform> SelectTarget(SkillData data, Transform skillTF)
{
HashSet<Transform> taragets = new();
Collider2D[] colliders = Physics2D.OverlapCircleAll(skillTF.position, data.range, data.attackTargetLayers);
for (int i = 0; i < colliders.Length; i++)
{
taragets.Add(colliders[i].transform);
}
if (taragets.Count == 0)
{
// Debug.Log("没有敌人");
return taragets;
}
else
{
for (int i = 0; i < taragets.Count; i++)
{
// Debug.Log("敌人" + res[i].name);
}
return taragets;
}
// return res;
}
#####
技能指示器
打开指示器和释放技能的逻辑是分开的
案例是绘制原点虚线
public class BaseSkillIndicator : MonoBehaviour
{
public SkillData skillData;
public Entity entity;
public virtual void Show()
{
}
public virtual void Hide(){}
}
public class DottedIndicator : BaseSkillIndicator
{
[Header("点抛物线")]
[SerializeField] private int numberOfDots = 20;
[SerializeField] private float spaceBetweenDots = 0.07f;
// [SerializeField] private GameObject dotPrefab;
// [SerializeField] private Transform dotsParent;
private GameObject[] dots;
void Awake()
{
GeneraeteDots();
}
void Update()
{
// if (PlayerInput.Instance.HasUseSkillInput)
// {
for (int i = 0; i < numberOfDots; i++)
{
dots[i].transform.position = DotsPosition(i * spaceBetweenDots);
}
// }
}