GameAbilitySystem|05 GameAbility 技能/能力
GameAbility
简称GA,GA其实应该叫做 能力 而不是 技能,在GAS中,挨打也是一种GA,所以应该是:“角色具有挨打的能力”,任何行为都可以是一个GA,但还是不建议把基础的移动控制作为一种GA…
能力其实比技能更加抽象,比如我拥有释放火焰的能力,只约定了你在什么时候可以释放火焰,具体怎么释放,动作是什么,效果是什么,这些都不由GA决定,GA只是纯粹的约定,你可以有这个能力,而已
抽象类,是能力的基类,只带有属性,不带有逻辑,我们需要继承这个类,去实现自己的能力
当然,其实这个基类的中的属性,和具体的实现类中的逻辑,是强相关的,也就是说,某个字段对应了什么功能,是约定俗成的(这一点设计上不太好,当然你也可以搞自己的抽象用法。。)
namespace GameAbilitySystem
{
public abstract class GameAbility : ScriptableObject
{
// 这个技能自身的标签
public GameTag assetTag;
// 取消带有这些标签的能力
// 举例,玩家A有两个技能,一个火魔法,一个冰魔法,当我们释放火魔法时,想要打断自身的所有冰魔法,那么这里就需要加上冰魔法的标签
public GameTag[] cancelAbilityWithTags;
// 约定的基本用法:持有这些标签时该能力无法激活
public GameTag[] blockAbilityWithTags;
// 约定的基本用法:激活能力时的附带标签
public GameTag[] activationOwnedTags;
// 约定的基本用法:当技能持有者不满足标签条件时,无法释放技能
public GameTagRequireIgnoreContainer ownerTags;
// 约定的基本用法:当技能释放者不满足标签条件时,无法释放技能,只有通过事件触发技能时(后文说明),会设置这个标签
public GameTagRequireIgnoreContainer sourceTags;
// 约定的基本用法:当技能目标不满足标签条件时,无法释放技能,只有通过事件触发技能时(后文说明),会设置这个标签
public GameTagRequireIgnoreContainer targetTags;
// 冷却时间,其实GE不放在这里也行,但是一般来说技能都是有冷却的,所以直接在这里加了一个GE
public GameEffect coolDown;
// 技能消耗,其实GE不放在这里也行,但是一般来说技能都是有消耗的,所以直接在这里加了一个GE
public GameEffect cost;
// 技能激活时能否再次激活
public bool canActiveWhenActive = false;
// 激活时再次激活,是否需要取消自身
public bool endSelfWhenActiveInActive = false;
// 技能蓝图,后文说明
public FlowScript blueprint;
// 创建一个GAS
public abstract GameAbilitySpec CreateSpec(AbilitySystemComponent owner,FlowScriptController blueprintController);
}
}
GameAbilitySpec
可以发现,上面的GA就是一个纯数据类,基本不带有任何逻辑,与GE一样,GA是一个纯配置类,在运行时,我们会主动创建GA对应的GAS
但不一样的地方在于,GAS,同样是一个抽象类
也就是说,我们不但要继承GA实现自己的能力,同时,每一类GA对对应了一个GAS,这个也需要我们继承然后实现,可以说GAS才是真正对能力进行逻辑处理的地方
AbilityCooldownTime
一个辅助类,用来存储冷却时间的
namespace GameAbilitySystem
{
public struct AbilityCooldownTime
{
public float timeRemaining;
public float totalDuration;
}
}
显卡一下GAS的基础属性,和构造方法
namespace GameAbilitySystem
{
public abstract class GameAbilitySpec
{
// 该GAS对应的GA
public GameAbility ability;
// ACS,技能处理中心,后文细说
public AbilitySystemComponent owner;
// 等级
public float level;
// 是否激活
public bool isActive;
public GameAbilitySpec(GameAbility ability, AbilitySystemComponent owner)
{
this.ability = ability;
this.owner = owner;
}
}
}
然后我们详细看一下GAS有哪些方法,只有一个入口,那就是尝试激活能力
namespace GameAbilitySystem
{
public abstract class GameAbilitySpec
{
public void TryActivateAbility()
{
if (!CanActivateAbility())
return;
ActivateAbility();
}
}
}
首先是判断能否激活能力
namespace GameAbilitySystem
{
public abstract class GameAbilitySpec
{
// 有多个条件
// 1.没有激活,或者激活中但是这个能力可以重复激活
// 2.符合标签要求
// 3.符合消耗要求
// 4.符合冷却要求
private bool CanActivateAbility()
{
return (!isActive || (isActive && ability.canActiveWhenActive))
&& CheckGameTags()
&& CheckCost()
&& CheckCooldown().timeRemaining <= 0;
}
// 符合标签要求,这是一个抽象方法,需要我们在我们自己的能力里判断
protected abstract bool CheckGameTags();
// 检查技能消耗
private bool CheckCost()
{
// 这里的cost是一个GE,上面说过了
if (ability.cost == null)
return true;
// 创建一个GES,这里的GES,他的时间类型,只能是即时的(用完就删除),否则直接返回true
// 注意,这里只是创建,没有应用,具体应用消耗的逻辑,在后文里
var spec = owner.MakeGameEffectSpec(ability.cost, level);
if (spec.gameEffect.durationPolicy != EDurationPolicy.Instant)
return true;
// 这里就是遍历这个GES的所有修饰器
foreach (var modifier in spec.gameEffect.modifiers)
{
if (modifier.modifierOperator != EModifierOperator.Add)
continue;
// 然后挨个计算消耗,对比数值,这里也说明,如果你的消耗是法力-40,那么你只能有一个修饰器是法力-40,而不能有两个法力-20
var cost = modifier.modifierMagnitude.CalculateMagnitude(spec) * modifier.multiplier;
if (owner.attributeSystemComponent.TryGetAttributeValue(modifier.attribute, out var attributeValue))
{
if (attributeValue.currentValue + cost < 0)
return false;
}
else
{
return false;
}
}
return true;
}
// 检查冷却时间
public AbilityCooldownTime CheckCooldown()
{
if (ability.coolDown == null)
return new AbilityCooldownTime();
var maxDuration = 0f;
var longestCooldown = 0f;
var coolDownTags = ability.coolDown.grantedTags;
// 遍历ASC的所有GE,找到所有的CD GE,找到最长的CD时间
foreach (var appliedGameEffect in owner.appliedGameEffects)
{
foreach (var grantedTag in appliedGameEffect.spec.gameEffect.grantedTags)
{
foreach (var coolDownTag in coolDownTags)
{
if (grantedTag.IsDescendantOf(coolDownTag))
{
if (appliedGameEffect.spec.gameEffect.durationPolicy == EDurationPolicy.Infinite)
{
return new AbilityCooldownTime
{
timeRemaining = float.MaxValue,
totalDuration = 0
};
}
var durationRemaining = appliedGameEffect.spec.durationRemaining;
if (durationRemaining > longestCooldown)
{
longestCooldown = durationRemaining;
maxDuration = appliedGameEffect.spec.totalDuration;
}
}
}
}
}
return new AbilityCooldownTime
{
timeRemaining = longestCooldown,
totalDuration = maxDuration
};
}
}
}
我们看一下具体的配置,先看消耗,可以发现,这个GE,基本没有任何要求,也不带什么标签,单纯只是附加了一个修饰器
在看冷却时间的GE,这个稍微不太一样,这个GE会附加一个标签,这个标签,就是冷却标签
没有其他任何效果,也没有消耗,但他有一个持续时间,当持续时间结束后,这个GE就会被删除
所以很明显,第一次释放技能时,我们身上没有这个冷却标签,自然可以释放,第二次释放时,因为我们身上已经有这个标签了,自然不符合冷却时间要求,就不能释放了
而当时序时间结束后,这个GE就会被删除,标签自然也就没了(标签是附加在GE身上的)
其次是激活/取消激活的两个方法
namespace GameAbilitySystem
{
public abstract class GameAbilitySpec
{
public virtual void ActivateAbility()
{
isActive = true;
// 如果当前技能正在释放中,判断需不需要取消
var index = owner.currentAbilitySpecs.IndexOf(this);
if (index > 0)
{
var current = owner.currentAbilitySpecs[index];
if (current.ability.endSelfWhenActiveInActive)
current.EndAbility();
owner.currentAbilitySpecs.Remove(this);
}
owner.currentAbilitySpecs.Add(this);
owner.CancelAbilityWithTags(ability.cancelAbilityWithTags);
}
public virtual void EndAbility()
{
isActive = false;
}
}
}
很明显,好像整个GAS里也没什么关于技能的逻辑,都是一些最基础的判断
因为真正的技能逻辑需要我们继承GAS去实现
实践
千万要记住,GA/GAS,代表的是能力,能力意味着某一类的技能,千万不要每一个技能都写一个GA,大多数技能是可以通过某一个GA配置实现的
以普通攻击为例,我们实现一个最简单的能力
SimpleAbility
GA其实是没什么东西的,就是返回一个对应的GAS就行
namespace GASExample
{
[CreateAssetMenu(menuName = "GASExample/Ability/Simple Ability")]
public class SimpleAbility : GameAbility
{
public override GameAbilitySpec CreateSpec(AbilitySystemComponent owner,FlowScriptController blueprintController)
{
var spec = new SimpleAbilitySpec(this, owner, blueprintController);
return spec;
}
}
}
SimpleAbilitySpec
主要来看一下这个GAS,这是个内部类
先看一下属性和构造方法
namespace GASExample
{
[CreateAssetMenu(menuName = "GASExample/Ability/Simple Ability")]
public class SimpleAbility : GameAbility
{
public class SimpleAbilitySpec : GameAbilitySpec
{
// 蓝图控制器(后文细说)
private readonly FlowScriptController blueprintController;
// 玩家控制器(后文细说)
private readonly PlayerController controller;
// 对应的GA
private readonly SimpleAbility simpleAbility;
// 构造方法就是属性初始化,没什么特殊逻辑
public SimpleAbilitySpec(GameAbility ability, AbilitySystemComponent owner, FlowScriptController blueprintController) : base(ability, owner)
{
simpleAbility = ability as SimpleAbility;
this.blueprintController = blueprintController;
controller = owner.GetComponent<PlayerController>();
level = owner.level;
}
}
}
}
然后再看他的一些接口方法
namespace GASExample
{
[CreateAssetMenu(menuName = "GASExample/Ability/Simple Ability")]
public class SimpleAbility : GameAbility
{
public class SimpleAbilitySpec : GameAbilitySpec
{
// 重写GAS的抽象方法
// HasAllTags和HasNoTags都是ACS的接口方法,用法和名字一样,就是判断玩家身上有某些标签,或者没有某些标签
// 这里就是最基础的标签检测,如果玩家要能够释放这个技能,那么必须满足的标签要求
protected override bool CheckGameTags()
{
return owner.HasAllTags(ability.ownerTags.requireTags)
&& owner.HasNoTags(ability.ownerTags.ignoreTags)
&& owner.HasAllTags(ability.sourceTags.requireTags)
&& owner.HasNoTags(ability.sourceTags.ignoreTags)
&& owner.HasAllTags(ability.targetTags.requireTags)
&& owner.HasNoTags(ability.targetTags.ignoreTags);
}
// 重写激活能力的接口
public override void ActivateAbility()
{
// 必须要先走base
base.ActivateAbility();
// 这里才是真正应用消耗和冷却GE的地方
if (simpleAbility.coolDown)
{
var cdSpec = owner.MakeGameEffectSpec(simpleAbility.coolDown, level);
owner.ApplyGameEffectSpecToSelf(cdSpec);
}
if (simpleAbility.cost)
{
var costSpec = owner.MakeGameEffectSpec(simpleAbility.cost, level);
owner.ApplyGameEffectSpecToSelf(costSpec);
}
// 蓝图控制器派发事件(后文细说)
blueprintController.SendEvent("OnAbilityStart", (GameAbilitySpec)this, owner.GameObject());
}
public override void EndAbility()
{
if (!isActive)
return;
base.EndAbility();
}
}
}
}
这就是最基础的一个GAS了,其实也不复杂, 就多了两步:
- 检查释放标签要求
- 应用消耗GES,冷却GES
- 派发蓝图事件开始技能流程
最后,提前放一个完整版的蓝图编辑流程在这里: