C++ 层:SkillComponent 与数据资产

数据资产(USkillAsset)

UCLASS(BlueprintType)
class USkillAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill", meta=(ClampMin="0"))
    float Cooldown = 1.f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill", meta=(ClampMin="0"))
    float ManaCost = 10.f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill", meta=(ClampMin="0"))
    float BaseDamage = 20.f;

    // 表现蓝图(软引用:不加载此蓝图也能读数据)
    UPROPERTY(EditAnywhere, Category="Skill", meta=(MetaClass="BP_SkillBase"))
    TSoftClassPtr<AActor> SkillVisualClass;
};

组件(USkillComponent)

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class USkillComponent : public UActorComponent
{
    GENERATED_BODY()

    DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSkillState, int32, Slot, FString, State);

public:
    UPROPERTY(BlueprintAssignable, Category="Skill")
    FOnSkillState OnSkillState;      // UI 订阅刷新按钮状态

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill")
    TArray<USkillAsset*> EquippedSkills;

    UFUNCTION(BlueprintCallable, Category="Skill")
    bool TryCastSkill(int32 Slot);

private:
    TArray<float> CooldownEnds;      // 每个槽位的冷却结束时间
    FTimerHandle CooldownTimer;
};

bool USkillComponent::TryCastSkill(int32 Slot)
{
    if (!EquippedSkills.IsValidIndex(Slot)) return false;
    USkillAsset* S = EquippedSkills[Slot];
    if (!S) return false;

    const float Now = GetWorld()->GetTimeSeconds();
    if (Now < CooldownEnds[Slot]) { OnSkillState.Broadcast(Slot, "CD中"); return false; }

    CooldownEnds[Slot] = Now + S->Cooldown;          // 进入冷却
    OnSkillState.Broadcast(Slot, "施放");            // 通知 UI

    // 生成表现蓝图(软引用加载)
    if (UClass* C = S->SkillVisualClass.LoadSynchronous())
    {
        FActorSpawnParameters P; P.Owner = GetOwner();
        GetWorld()->SpawnActor(C, GetOwner()->GetActorLocation(), FRotator::ZeroRotator, P);
    }
    return true;
}

设计要点回顾

  • 组件只管"状态与规则"(冷却、槽位、广播),不管"火球长什么样";
  • 数值全部从 USkillAsset 读,组件内零硬编码;
  • 软引用 SkillVisualClass——读技能数据时不会把表现蓝图也加载进内存(第 4 章的硬引用规避);
  • 广播 OnSkillState 给 UI:UI 只订阅不决策(第 3 章的通信选型)。

Anim Notify 怎么对接(表现蓝图侧的事件)

技能表现蓝图里订阅 AnimNotify_CastHit 事件( montage 里由美术加的 Notify),把"生效"回调发给组件:

// 在技能表现蓝图基类(C++ 侧)
UFUNCTION(BlueprintNativeEvent, Category="Skill")
void OnCastHit();   // 默认实现:告诉组件做伤害结算

伤害结算在 C++(从 SkillAsset 读 BaseDamage,走第 3 章的 IDamageable 接口发给敌人),特效在蓝图(OnCastHit 的蓝图实现里播 Niagara + 音效)。

本章小结

  • C++ 层三件套:数据资产(数值)、组件(状态机与广播)、表现基类(动画事件对接)。
  • 数值零硬编码;表现蓝图用软引用加载;UI 只订阅广播。
  • 生效时机由美术用 Anim Notify 决定,程序不猜帧。