AI 写一个可被 Blueprint 调用的角色技能系统

下面给你一套 轻量版 UE 角色技能系统
C++ 负责技能状态、蓝耗、冷却和校验;Blueprint 负责真正的技能表现 ,比如放动画、生成投射物、冲刺、播特效。这个分工符合 UE 官方推荐的 C++ + Blueprint 混合开发 方式;同时 ActorComponent 本来就是给 Actor 挂载可复用行为用的,很适合拿来做角色技能系统。若后面要做复杂属性、效果堆叠、多人预测/复制,官方有更完整的 Gameplay Ability System(GAS) 。(Epic Games Developers)

先说明边界:这套代码是 非 GAS、可直接上手的最小可用版 ,适合单机、局域网原型、动作游戏原型、RPG 原型。多人正式项目通常会继续加 RPC、属性复制,或者直接切 GAS。UE 官方文档也明确把 GAS 定位成处理技能、属性、效果和技能任务的完整框架。(Epic Games Developers)


1) SkillTypes.h

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "SkillTypes.generated.h"

USTRUCT(BlueprintType)
struct FSkillSpec
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill")
    FName SkillId = NAME_None;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill", meta=(ClampMin="0.0"))
    float ManaCost = 10.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill", meta=(ClampMin="0.0"))
    float Cooldown = 2.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Skill")
    bool bUnlocked = true;
};

2) SkillSystemComponent.h

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SkillTypes.h"
#include "SkillSystemComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnManaChangedSignature, float, NewMana);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkillCastSignature, FName, SkillId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSkillFailedSignature, FName, SkillId, FString, Reason);

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

public:
    USkillSystemComponent();

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Skill|Config")
    float MaxMana = 100.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Skill|State")
    float CurrentMana = 100.0f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Skill|Config")
    TArray<FSkillSpec> Skills;

    // 记录每个技能冷却结束的绝对时间
    UPROPERTY()
    TMap<FName, float> CooldownEndTimes;

public:
    UPROPERTY(BlueprintAssignable, Category="Skill|Events")
    FOnManaChangedSignature OnManaChanged;

    UPROPERTY(BlueprintAssignable, Category="Skill|Events")
    FOnSkillCastSignature OnSkillCast;

    UPROPERTY(BlueprintAssignable, Category="Skill|Events")
    FOnSkillFailedSignature OnSkillFailed;

    UFUNCTION(BlueprintCallable, Category="Skill")
    bool TryCastSkill(FName SkillId);

    UFUNCTION(BlueprintCallable, Category="Skill")
    bool CanCastSkill(FName SkillId, FString& OutReason) const;

    UFUNCTION(BlueprintPure, Category="Skill")
    bool HasSkill(FName SkillId) const;

    UFUNCTION(BlueprintPure, Category="Skill")
    float GetRemainingCooldown(FName SkillId) const;

    UFUNCTION(BlueprintPure, Category="Skill")
    float GetCurrentMana() const { return CurrentMana; }

    UFUNCTION(BlueprintPure, Category="Skill")
    float GetMaxMana() const { return MaxMana; }

    UFUNCTION(BlueprintCallable, Category="Skill")
    void RestoreMana(float Amount);

    UFUNCTION(BlueprintCallable, Category="Skill")
    void SetMana(float NewMana);

    UFUNCTION(BlueprintCallable, Category="Skill")
    void AddOrUpdateSkill(const FSkillSpec& NewSkill);

    // 真正的技能效果交给 Blueprint 实现
    UFUNCTION(BlueprintImplementableEvent, Category="Skill|Events")
    void BP_ExecuteSkill(FName SkillId);

    UFUNCTION(BlueprintImplementableEvent, Category="Skill|Events")
    void BP_OnSkillFailed(FName SkillId, const FString& Reason);

private:
    const FSkillSpec* FindSkillSpec(FName SkillId) const;
};

3) SkillSystemComponent.cpp

cpp 复制代码
#include "SkillSystemComponent.h"

USkillSystemComponent::USkillSystemComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
}

void USkillSystemComponent::BeginPlay()
{
    Super::BeginPlay();

    CurrentMana = FMath::Clamp(CurrentMana, 0.0f, MaxMana);
    OnManaChanged.Broadcast(CurrentMana);
}

const FSkillSpec* USkillSystemComponent::FindSkillSpec(FName SkillId) const
{
    return Skills.FindByPredicate([&](const FSkillSpec& Spec)
    {
        return Spec.SkillId == SkillId;
    });
}

bool USkillSystemComponent::HasSkill(FName SkillId) const
{
    return FindSkillSpec(SkillId) != nullptr;
}

float USkillSystemComponent::GetRemainingCooldown(FName SkillId) const
{
    const UWorld* World = GetWorld();
    if (!World)
    {
        return 0.0f;
    }

    const float* EndTimePtr = CooldownEndTimes.Find(SkillId);
    if (!EndTimePtr)
    {
        return 0.0f;
    }

    return FMath::Max(0.0f, *EndTimePtr - World->GetTimeSeconds());
}

bool USkillSystemComponent::CanCastSkill(FName SkillId, FString& OutReason) const
{
    const FSkillSpec* Skill = FindSkillSpec(SkillId);
    if (!Skill)
    {
        OutReason = TEXT("Skill not found");
        return false;
    }

    if (!Skill->bUnlocked)
    {
        OutReason = TEXT("Skill is locked");
        return false;
    }

    if (CurrentMana < Skill->ManaCost)
    {
        OutReason = TEXT("Not enough mana");
        return false;
    }

    const float RemainingCooldown = GetRemainingCooldown(SkillId);
    if (RemainingCooldown > 0.0f)
    {
        OutReason = FString::Printf(TEXT("Skill is cooling down: %.2f s"), RemainingCooldown);
        return false;
    }

    OutReason = TEXT("");
    return true;
}

bool USkillSystemComponent::TryCastSkill(FName SkillId)
{
    FString FailReason;
    if (!CanCastSkill(SkillId, FailReason))
    {
        OnSkillFailed.Broadcast(SkillId, FailReason);
        BP_OnSkillFailed(SkillId, FailReason);
        return false;
    }

    const FSkillSpec* Skill = FindSkillSpec(SkillId);
    if (!Skill)
    {
        const FString Reason = TEXT("Skill not found");
        OnSkillFailed.Broadcast(SkillId, Reason);
        BP_OnSkillFailed(SkillId, Reason);
        return false;
    }

    CurrentMana = FMath::Clamp(CurrentMana - Skill->ManaCost, 0.0f, MaxMana);

    if (UWorld* World = GetWorld())
    {
        CooldownEndTimes.FindOrAdd(SkillId) = World->GetTimeSeconds() + Skill->Cooldown;
    }

    OnManaChanged.Broadcast(CurrentMana);
    OnSkillCast.Broadcast(SkillId);

    // 把真正效果交给蓝图
    BP_ExecuteSkill(SkillId);

    return true;
}

void USkillSystemComponent::RestoreMana(float Amount)
{
    if (Amount <= 0.0f)
    {
        return;
    }

    CurrentMana = FMath::Clamp(CurrentMana + Amount, 0.0f, MaxMana);
    OnManaChanged.Broadcast(CurrentMana);
}

void USkillSystemComponent::SetMana(float NewMana)
{
    CurrentMana = FMath::Clamp(NewMana, 0.0f, MaxMana);
    OnManaChanged.Broadcast(CurrentMana);
}

void USkillSystemComponent::AddOrUpdateSkill(const FSkillSpec& NewSkill)
{
    if (NewSkill.SkillId.IsNone())
    {
        return;
    }

    if (FSkillSpec* Existing = Skills.FindByPredicate([&](const FSkillSpec& Spec)
    {
        return Spec.SkillId == NewSkill.SkillId;
    }))
    {
        *Existing = NewSkill;
        return;
    }

    Skills.Add(NewSkill);
}

4) 在 Blueprint 里怎么接

这套系统依赖 UE 官方那套 把 C++ 暴露给 Blueprint 的方式:
BlueprintSpawnableComponent 让组件可被蓝图添加;BlueprintCallable 让函数变成蓝图节点;BlueprintImplementableEvent 让你在蓝图里补上最终逻辑。(Epic Games Developers)

按这个流程接:

A. 给角色加组件

在你的 BP_PlayerCharacterBP_EnemyCharacter 里:

  1. Add Component
  2. 选择 SkillSystemComponent

B. 配技能表

在组件 Details 面板里给 Skills 填几个条目,例如:

  • Fireball,ManaCost = 20,Cooldown = 3
  • Dash,ManaCost = 15,Cooldown = 1.5
  • Heal,ManaCost = 25,Cooldown = 5

C. 在蓝图里实现技能表现

在角色蓝图 Event Graph 里实现 BP Execute Skill

text 复制代码
Event BP Execute Skill
    -> Switch on Name (SkillId)
        Fireball:
            播放施法动画
            SpawnActor(BP_FireballProjectile)
        Dash:
            Launch Character
        Heal:
            给自己回血
            播放治疗特效

D. 从输入或 UI 调用

例如按键 Q:

text 复制代码
InputAction Q
    -> Try Cast Skill("Fireball")

按键 Space:

text 复制代码
InputAction Space
    -> Try Cast Skill("Dash")

E. UI 接蓝量/冷却

  • 蓝量条绑定 GetCurrentMana / GetMaxMana
  • 技能按钮灰掉时调用 GetRemainingCooldown
  • 也可以直接监听 OnManaChangedOnSkillCast

5) 这个版本已经有的能力

这套最小版已经支持:

  • 技能列表配置
  • 蓝耗检查
  • 冷却检查
  • 解锁/锁定
  • Blueprint 调用技能
  • Blueprint 实现技能效果
  • UI 事件广播

也就是说,系统层 已经在 C++,表现层完全交给 Blueprint。


6) 一个典型用法

火球技能

BP Execute SkillFireball 分支里:

  • 获取角色前方向
  • SpawnActor(BP_FireballProjectile)
  • 给投射物传伤害值、速度、施法者

冲刺技能

Dash 分支里:

  • 读取角色 Forward Vector
  • LaunchCharacter(ForwardVector * DashStrength, true, true)

治疗技能

Heal 分支里:

  • 调角色身上的 HealthComponent
  • Heal(Amount)
  • 播治疗特效和音效

7) 后面最值得加的 4 个扩展

扩展 1:让技能带更多配置

FSkillSpec 扩展成:

cpp 复制代码
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Damage = 30.0f;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<AActor> SkillActorClass;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UAnimMontage> CastMontage;

这样 Blueprint 实现时可以直接读配置,不用写死。

扩展 2:加公共查询接口

比如:

cpp 复制代码
UFUNCTION(BlueprintPure, Category="Skill")
bool IsSkillReady(FName SkillId) const;

给 UI 判断按钮是否可点击。

扩展 3:加 SkillId -> Enum / GameplayTag

当前 FName 足够轻量,但项目大了以后更建议:

  • 小项目:UENUM
  • 中大项目:GameplayTag

扩展 4:加多人同步

当前版本是本地逻辑版。

如果要多人,需要把施法入口改成 Server RPC ,再复制蓝量、冷却和技能结果。UE 的 Actor/组件体系支持属性和函数复制,但这一步要单独补网络层。(Epic Games Developers)


8) 什么时候该换 GAS

当你开始需要这些能力时,就该考虑 GAS 了:

  • Buff / Debuff
  • 属性集(血、蓝、体力、护甲、暴击)
  • 效果叠层
  • 技能标签互斥/打断
  • 复杂冷却和消耗
  • 多人预测
  • 大量技能的统一框架

这些正是 UE 官方给 GAS 设计的典型场景。(Epic Games Developers)


9) 最后给你一句落地建议

先用这套轻量版把角色技能跑起来,速度最快。

等你把 输入 → 校验 → 冷却 → 蓝耗 → 技能表现 这条链路跑通,再决定要不要升级到 GAS。

下一步我可以直接把它继续补成一套 "火球 + 冲刺 + 治疗"完整示例,包括角色蓝图怎么接、投射物类怎么写、UI 冷却怎么显示。

相关推荐
草原上唱山歌1 小时前
如何理解C语言中的指针?
c语言·开发语言·数据结构
m0_662577971 小时前
C++中的模板方法模式
开发语言·c++·算法
csbysj20201 小时前
PHP 多维数组
开发语言
墨白曦煜1 小时前
RocketMQ 实战:揭秘 @RocketMQMessageListener 的反序列化魔法与“万能”消费策略
开发语言·python·rocketmq
m0_748873553 小时前
C++与Rust交互编程
开发语言·c++·算法
ZTLJQ10 小时前
序列化的艺术:Python JSON处理完全解析
开发语言·python·json
2401_8914821710 小时前
多平台UI框架C++开发
开发语言·c++·算法
88号技师10 小时前
2026年3月中科院一区SCI-贝塞尔曲线优化算法Bezier curve-based optimization-附Matlab免费代码
开发语言·算法·matlab·优化算法