前言

很多技能系统都是基于事件的,不然代码耦合严重,扩展困难

本技能系统的事件类似于c#的事件模型EventHandler

事件定义

GameplayTag,使用tag来定义各种事件,易扩展,可读性高,性能也不错

partial class Events
{
    [Tooltip("Effect创建时\n(args=null)")]
    public readonly static GameplayTag Effect_OnCreated = new("Event.Effect.OnCreated");
    [Tooltip("Effect销毁时\n(args=null)")]
    public readonly static GameplayTag Effect_OnDestroy = new("Event.Effect.OnDestroy");
    [Tooltip("Effect周期触发时\n(args=null)")]
    public readonly static GameplayTag Effect_OnPeriod = new("Event.Effect.OnPeriod");
    [Tooltip("Effect堆叠溢出时\n(args=null)")]
    public readonly static GameplayTag Effect_OnStackOverflow = new("Event.Effect.OnStackOverflow");
    [Tooltip("Effect堆叠层数变化时\n(args=EventArgs_EffectStackCountChanged)")]
    public readonly static GameplayTag Effect_OnStackCountChanged = new("Event.Effect.OnStackCountChanged");
    [Tooltip("Effect持续时间刷新时\n(args=null)")]
    public readonly static GameplayTag Effect_OnDurationRefresh = new("Event.Effect.DurationRefresh");
}

事件发起者

IGameplayEventSender,用于抽象技能系统里一些对象,例如Effect、Ability、Projectile、AoE

事件参数

GameplayEventArgs,针对不同类型的事件,需要的参数也不同,通过子类来实现

事件调用模型

Map<GameplayTag, f(sender, args)>,sender为事件发起者,args为事件参数。在不同的事件点,调用Map的事件即可。

internal static void HandleGameplayEvent(ref this AoEObj cAoe, GameplayTag eventTag, EcsEntity eAoe, GameplayEventArgs args)
{
    var script = cAoe.eventScript;
    if (script != null)
    {
        script.Invoke(eventTag, eAoe, args);
    }

    if (cAoe.data.eventContainer != null)
    {
        DataDrivenUtility.HandleDataDrivenEvents(cAoe.data.eventContainer, cAoe, eventTag, args, cAoe.level);
    }

    EffectSystem.HandleGameplayEvent(cAoe.caster, eventTag, args);
}

事件注册

  • 代码注册,用于自定义脚本,脚本类包含Map<GameplayTag, f(sender, args)>,代码填充即可
public interface IEventScript<TSender> : IScript
{
    Dictionary<GameplayTag, GameplyEventHandler<TSender>> EventMap { get; }

    void Invoke(GameplayTag eventTag, TSender sender, GameplayEventArgs args)
    {
        Log.Assert(EventMap != null, $"{this.GetType().Name} {eventTag}'s EventMap is null");

        if (EventMap.TryGetValue(eventTag, out var func))
        {
            Log.Assert(func != null, $"{this.GetType().Name} {eventTag}'s func is null");

            func.Invoke(sender, args);
        }
    }

    bool ContainsEvent(GameplayTag eventTag)
    {
        Log.Assert(EventMap != null, $"{this.GetType().Name} {eventTag}'s EventMap is null");

        return EventMap.ContainsKey(eventTag);
    }
}
  • 数据驱动注册,编辑器编辑EventContainer
  • 编写数据驱动Action
/// <summary>
/// 施加Effect
/// </summary>
[MovedFrom(true, sourceClassName: "GAction_Effect")]
[Serializable]
public sealed class GAction_ApplyEffect : GAction
{
    [HideInInspector]
    [ReadOnly]
    public int effectId;
#if UNITY_EDITOR
    [Tooltip("施加的effectId")]
    [SerializeField]
    [JsonConverter(typeof(UObjectToGUIDConverter))]
    [JsonProperty]
    private Effect m_EffectId;
#endif

    [Tooltip("施加的effect层数")]
    public VarInt stackCount = new("1");

    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
    public bool queued = true;

    [SerializeReference]
    public ITargetType_DataDriven targetType;

    public override GActionObj CreateObj(int level, bool poolable)
    {
        var obj = poolable ? SharedPool.Rent<GActionObj_ApplyEffect>() : new();
        obj.Poolable = poolable;

        obj.effectId = effectId;
        obj.stackCount = stackCount.GetValue(level);
        obj.queued = queued;

        if (targetType is TargetType _targetType)
            obj.targetType = TargetTypeObj.CreateObj(_targetType) as ITargetTypeObj_DataDriven;

        return obj;
    }

#if UNITY_EDITOR
    public override void OnValidate()
    {
        base.OnValidate();
        effectId = AbilityUtility.StringToHash(m_EffectId);
    }
#endif
}

public sealed class GActionObj_ApplyEffect : GActionObj
{
    public int effectId;
    public int stackCount;
    public bool queued;
    public ITargetTypeObj_DataDriven targetType;

    private void Act(EcsEntity caster, List<EcsEntity> targets, int level)
    {
        for (int i = 0; i < targets.Count; i++)
        {
            var target = targets[i];

            ref var ctx = ref target.Get<EffectCtx>();
            if (queued)
                GameplayUtility.ApplyEffectQueued(ref ctx, effectId, caster, target, level, stackCount);
            else
                GameplayUtility.ApplyEffect(ref ctx, effectId, caster, target, level, stackCount);
        }
    }

    public override void InvokeAction(IGameplayEventSender sender, GameplayEventArgs args)
    {
        using var _ = ListPool<EcsEntity>.Rent(out var targets);
        targetType.GetTargets(sender, args, targets);
        if (targets.Count > 0)
            Act(sender.Caster, targets, sender.Level);
    }

    protected override void IReferenceClear()
    {
        base.IReferenceClear();

        TargetTypeObj.ReleaseObj(targetType);
    }
}
  • Unity Visual Scripting(实验性质,UVS目前性能较差,并且存在bug)

总结

通过上述简单的抽象,统一了脚本和数据驱动的事件分发,大大提高了可技能系统的可扩展性。