feat(M4): enemy AI - behavior tree, patrol, chase, attack, spawner
M4 — Enemy AI Foundation - MMEnemyBase: enemy base class (aggro/deaggro/attack ranges, death event) - MMAIController: BT driver with TargetActor/SpawnLocation/IsInAttackRange blackboard keys - BTTask_Patrol: random reachable point patrol around spawn location - BTTask_ChaseTarget: move-to-actor with tick-based range check - BTTask_Attack: face target + activate attack ability - MMEnemySpawner: timed spawn with max alive count, death tracking - Build.cs: added AIModule + NavigationSystem Files: 44 total, 2381 lines
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# MM 项目开发进度报告
|
||||
|
||||
> 最后更新:2026-05-02 06:35
|
||||
> 最后更新:2026-05-02 07:10
|
||||
|
||||
## 已完成里程碑
|
||||
|
||||
@@ -11,50 +11,61 @@
|
||||
|
||||
### M1 ✅ — 项目骨架
|
||||
- commit: `266e817`
|
||||
- 14 文件,602 行新增代码
|
||||
- MMAbilitySystemComponent(ASC 骨架)
|
||||
- MMAttributeSet(7 核心属性 + PostGameplayEffectExecute 日志)
|
||||
- MMCharacterBase(ASC + 俯视角摄像机 + Enhanced Input 移动/面向)
|
||||
- MMPlayerController(鼠标光标)
|
||||
- MMGameMode(默认 Pawn/Controller)
|
||||
- SaberCharacter(Saber 入口类,空)
|
||||
- 14个文件,500行代码
|
||||
- MMCharacterBase(ASC + 摄像机 + Enhanced Input 移动)
|
||||
- MMAbilitySystemComponent(ASC 骨架,实现 IAbilitySystemInterface)
|
||||
- MMAttributeSet(7个核心属性:Health/MaxHealth/Mana/MaxMana/AttackPower/Defense/MoveSpeed)
|
||||
- MMPlayerController(鼠标光标 + 默认输入)
|
||||
- MMGameMode(默认 Pawn + Controller)
|
||||
- SaberCharacter(Saber 子类占位)
|
||||
|
||||
## 目录结构(当前)
|
||||
### M2 ✅ — 战斗基础
|
||||
- commit: `c08f18f`
|
||||
- 24个文件,1197行代码
|
||||
- MMGameplayAbility(技能基类,工具方法)
|
||||
- MMWeaponComponent(武器碰撞检测 + 命中列表 + ApplyDamage)
|
||||
- DamageCalculation(GE Execution Calc:伤害公式 = Atk × (1 + Atk*0.01) - Def × 0.5)
|
||||
- MMGameplayEffect_Damage(瞬时伤害 GE)
|
||||
- MMAttack_Melee(Saber 普攻 GA,0.3s 挥砍窗口)
|
||||
- 角色基类扩展:攻击输入、受击事件、死亡处理
|
||||
|
||||
```
|
||||
Source/MM/
|
||||
├── Core/
|
||||
│ ├── MMCharacterBase.h/.cpp # 角色基类
|
||||
│ ├── MMGameMode.h/.cpp # GameMode
|
||||
│ └── MMPlayerController.h/.cpp # PlayerController
|
||||
├── GAS/
|
||||
│ └── AbilitySystem/
|
||||
│ ├── MMAbilitySystemComponent.h/.cpp # ASC
|
||||
│ └── MMAttributeSet.h/.cpp # AttributeSet
|
||||
├── Player/
|
||||
│ └── SaberCharacter.h/.cpp # Saber 入口
|
||||
├── MM.h / MM.cpp # 模块入口
|
||||
└── MM.Build.cs # 构建配置
|
||||
```
|
||||
### M3 ✅ — 技能系统 & HUD
|
||||
- commit: `a18c23b`
|
||||
- 32个文件,1721行代码
|
||||
- MMGameplayTags(Native Tag 注册:Cooldown/State/Combat)
|
||||
- MMSkill_Slash(Saber 横斩,3s CD,2x 伤害倍率)
|
||||
- MMHUD + MMHealthBarWidget(纯 C++ UGM 生命条)
|
||||
- MMCharacterBase 扩展:Skill1Action 输入 + StartupAbilities 授予
|
||||
- SaberCharacter 授予 Slash 技能
|
||||
- Build.cs 添加 Slate/SlateCore/UMG 模块
|
||||
|
||||
## 下一步:M2 — 战斗基础
|
||||
## 当前代码统计
|
||||
|
||||
**目标**:碰撞命中、普攻、伤害 GE、受击、死亡
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 总文件数 | 32 (.h + .cpp) |
|
||||
| 总代码行数 | 1721 |
|
||||
| 目录结构 | Core/GAS/Combat/UI/Player |
|
||||
|
||||
### M2 计划任务
|
||||
## 下一步目标 — M4
|
||||
|
||||
| 任务 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| T001 | Combat/MMDamageType.h | 伤害类型定义 |
|
||||
| T002 | Combat/DamageCalculation.h/.cpp | 伤害计算(无随机命中/闪避) |
|
||||
| T003 | GAS/Effects/MMGameplayEffect_Damage.h | 伤害 GameplayEffect(C++ DataAsset) |
|
||||
| T004 | Core/MMCharacterBase 扩展 | 添加受击/死亡逻辑、Health clamp |
|
||||
| T005 | Combat/MMWeaponComponent.h/.cpp | 武器碰撞检测组件(CapsuleOverlap) |
|
||||
| T006 | GAS/Abilities/MMGameplayAbility.h/.cpp | 技能基类(GA 基类) |
|
||||
| T007 | GAS/Abilities/Saber/MMAttack_Melee.h/.cpp | Saber 普攻 GA |
|
||||
| T008 | Core/MMCharacterBase 扩展 | 普攻输入绑定(LMB) |
|
||||
### M4 — 怪物 AI 基础
|
||||
目标:创建怪物基类、简单 AI(巡逻+追击+攻击)、生成系统
|
||||
|
||||
### 设计约束(来自 game-design.md)
|
||||
- 命中/闪避不做随机面板检定
|
||||
- 用碰撞、走位、翻滚无敌帧、格挡窗口、技能前后摇表达操作性
|
||||
- 伤害公式:最终伤害 = (攻击力 - 防御力) × 技能倍率
|
||||
**任务清单:**
|
||||
1. T001: MMEnemyBase.h/.cpp — 怪物基类(继承 CharacterBase,AI 控制)
|
||||
2. T002: MMAIController.h/.cpp — AI 控制器(行为树 + 黑板)
|
||||
3. T003: BT_EnemyBase — 行为树资产(巡逻→追击→攻击)
|
||||
4. T004: BTTask_Patrol.h/.cpp — 巡逻任务节点
|
||||
5. T005: BTTask_ChaseTarget.h/.cpp — 追击目标节点
|
||||
6. T006: BTTask_Attack.h/.cpp — 攻击任务节点
|
||||
7. T007: MMEnemySpawner.h/.cpp — 怪物生成器
|
||||
8. T008: 更新 MM.Build.cs — 添加 AIModule
|
||||
|
||||
**依赖链:**
|
||||
T001 + T002 → T003 → T004+T005+T006 → T007
|
||||
T008 独立(Build.cs 更新)
|
||||
|
||||
**完成后项目状态:**
|
||||
玩家可以操控 Saber 在地图上移动、攻击、使用技能,
|
||||
怪物会自动生成、巡逻、追击玩家并攻击。
|
||||
|
||||
49
Source/MM/AI/BTTask_Attack.cpp
Normal file
49
Source/MM/AI/BTTask_Attack.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "BTTask_Attack.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "AI/MMAIController.h"
|
||||
#include "Core/MMCharacterBase.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
|
||||
UBTTask_Attack::UBTTask_Attack()
|
||||
{
|
||||
NodeName = TEXT("Attack");
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
AAIController* AIController = OwnerComp.GetAIOwner();
|
||||
if (!AIController)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
AMMCharacterBase* Character = Cast<AMMCharacterBase>(AIController->GetPawn());
|
||||
if (!Character || Character->IsDead())
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// 面向目标
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
{
|
||||
if (AActor* Target = Cast<AActor>(BB->GetValueAsObject(AMMAIController::TargetActorKeyName)))
|
||||
{
|
||||
const FVector Direction = (Target->GetActorLocation() - Character->GetActorLocation()).GetSafeNormal2D();
|
||||
if (!Direction.IsNearlyZero())
|
||||
{
|
||||
Character->SetActorRotation(Direction.Rotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发攻击
|
||||
if (Character->GetAbilitySystemComponent() && Character->GetDefaultAttackAbility())
|
||||
{
|
||||
Character->GetAbilitySystemComponent()->TryActivateAbilityByClass(
|
||||
Character->GetDefaultAttackAbility());
|
||||
}
|
||||
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
34
Source/MM/AI/BTTask_Attack.h
Normal file
34
Source/MM/AI/BTTask_Attack.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "BTTask_Attack.generated.h"
|
||||
|
||||
/// 攻击任务节点。
|
||||
///
|
||||
/// 触发怪物的默认攻击能力。
|
||||
/// 攻击完成后有短暂冷却。
|
||||
///
|
||||
/// # 黑板键
|
||||
///
|
||||
/// | 键名 | 类型 | 说明 |
|
||||
/// |------|------|------|
|
||||
/// | TargetActor | Object | 攻击目标 |
|
||||
///
|
||||
/// - SeeAlso: UBTTaskNode
|
||||
UCLASS()
|
||||
class UBTTask_Attack : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBTTask_Attack();
|
||||
|
||||
/// 执行攻击任务。
|
||||
virtual EBTNodeResult::Type ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
|
||||
/// 攻击后冷却时间(秒)。
|
||||
UPROPERTY(EditAnywhere, Category = "Attack")
|
||||
float AttackCooldown = 1.5f;
|
||||
};
|
||||
66
Source/MM/AI/BTTask_ChaseTarget.cpp
Normal file
66
Source/MM/AI/BTTask_ChaseTarget.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "BTTask_ChaseTarget.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "AI/MMAIController.h"
|
||||
#include "Enemy/MMEnemyBase.h"
|
||||
|
||||
UBTTask_ChaseTarget::UBTTask_ChaseTarget()
|
||||
{
|
||||
NodeName = TEXT("Chase Target");
|
||||
bNotifyTick = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UBTTask_ChaseTarget::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
AAIController* AIController = OwnerComp.GetAIOwner();
|
||||
if (!BB || !AIController)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(AMMAIController::TargetActorKeyName));
|
||||
if (!Target)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// 开始追击
|
||||
AIController->MoveToActor(Target, AttackRange * 0.8f);
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UBTTask_ChaseTarget::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
AAIController* AIController = OwnerComp.GetAIOwner();
|
||||
if (!BB || !AIController)
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(AMMAIController::TargetActorKeyName));
|
||||
if (!Target)
|
||||
{
|
||||
// 目标丢失
|
||||
BB->SetValueAsObject(AMMAIController::TargetActorKeyName, nullptr);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
APawn* Pawn = AIController->GetPawn();
|
||||
if (!Pawn)
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
const float Distance = Pawn->GetDistanceTo(Target);
|
||||
if (Distance <= AttackRange)
|
||||
{
|
||||
BB->SetValueAsBool(AMMAIController::IsInAttackRangeKeyName, true);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
}
|
||||
38
Source/MM/AI/BTTask_ChaseTarget.h
Normal file
38
Source/MM/AI/BTTask_ChaseTarget.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "BTTask_ChaseTarget.generated.h"
|
||||
|
||||
/// 追击目标任务节点。
|
||||
///
|
||||
/// 向黑板中的 TargetActor 移动,直到进入攻击范围或目标丢失。
|
||||
///
|
||||
/// # 黑板键
|
||||
///
|
||||
/// | 键名 | 类型 | 说明 |
|
||||
/// |------|------|------|
|
||||
/// | TargetActor | Object | 追击目标 |
|
||||
/// | IsInAttackRange | Boolean | 是否已进入攻击范围 |
|
||||
///
|
||||
/// - SeeAlso: UBTTaskNode
|
||||
UCLASS()
|
||||
class UBTTask_ChaseTarget : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBTTask_ChaseTarget();
|
||||
|
||||
/// 执行追击任务。
|
||||
virtual EBTNodeResult::Type ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
|
||||
/// 每帧检查距离。
|
||||
virtual void TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
|
||||
/// 攻击范围(用于判断是否切换到攻击行为)。
|
||||
UPROPERTY(EditAnywhere, Category = "Chase")
|
||||
float AttackRange = 150.0f;
|
||||
};
|
||||
42
Source/MM/AI/BTTask_Patrol.cpp
Normal file
42
Source/MM/AI/BTTask_Patrol.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "BTTask_Patrol.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "AI/MMAIController.h"
|
||||
#include "NavigationSystem.h"
|
||||
|
||||
UBTTask_Patrol::UBTTask_Patrol()
|
||||
{
|
||||
NodeName = TEXT("Patrol");
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UBTTask_Patrol::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
const FVector SpawnLoc = BB->GetValueAsVector(AMMAIController::SpawnLocationKeyName);
|
||||
AAIController* AIController = OwnerComp.GetAIOwner();
|
||||
if (!AIController)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// 在生成点附近生成随机可达点
|
||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(AIController);
|
||||
if (!NavSys)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
FNavLocation RandomPoint;
|
||||
if (NavSys->GetRandomReachablePointInRadius(SpawnLoc, PatrolRadius, RandomPoint))
|
||||
{
|
||||
AIController->MoveToLocation(RandomPoint.Location);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
34
Source/MM/AI/BTTask_Patrol.h
Normal file
34
Source/MM/AI/BTTask_Patrol.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "BTTask_Patrol.generated.h"
|
||||
|
||||
/// 巡逻任务节点。
|
||||
///
|
||||
/// 在生成点附近的随机位置移动。
|
||||
/// 到达后等待一段随机时间,再选择下一个巡逻点。
|
||||
///
|
||||
/// # 黑板键
|
||||
///
|
||||
/// | 键名 | 类型 | 说明 |
|
||||
/// |------|------|------|
|
||||
/// | SpawnLocation | Vector | 生成位置(巡逻中心点)|
|
||||
///
|
||||
/// - SeeAlso: UBTTaskNode
|
||||
UCLASS()
|
||||
class UBTTask_Patrol : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBTTask_Patrol();
|
||||
|
||||
/// 执行巡逻任务。
|
||||
virtual EBTNodeResult::Type ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
|
||||
/// 巡逻半径(从黑板 SpawnLocation 偏移的最大距离)。
|
||||
UPROPERTY(EditAnywhere, Category = "Patrol")
|
||||
float PatrolRadius = 500.0f;
|
||||
};
|
||||
38
Source/MM/AI/MMAIController.cpp
Normal file
38
Source/MM/AI/MMAIController.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#include "MMAIController.h"
|
||||
#include "BehaviorTree/BehaviorTree.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Enemy/MMEnemyBase.h"
|
||||
#include "Perception/AIPerceptionComponent.h"
|
||||
#include "Perception/AISenseConfig_Sight.h"
|
||||
|
||||
const FName AMMAIController::TargetActorKeyName(TEXT("TargetActor"));
|
||||
const FName AMMAIController::SpawnLocationKeyName(TEXT("SpawnLocation"));
|
||||
const FName AMMAIController::IsInAttackRangeKeyName(TEXT("IsInAttackRange"));
|
||||
|
||||
AMMAIController::AMMAIController()
|
||||
{
|
||||
CachedBlackboard = nullptr;
|
||||
}
|
||||
|
||||
void AMMAIController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
AMMEnemyBase* Enemy = Cast<AMMEnemyBase>(InPawn);
|
||||
if (!Enemy || !BehaviorTreeAsset)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动行为树
|
||||
if (UBlackboardComponent* BB = GetBlackboardComponent())
|
||||
{
|
||||
BB->InitializeBlackboard(*BehaviorTreeAsset->BlackboardAsset);
|
||||
BB->SetValueAsVector(SpawnLocationKeyName, Enemy->GetActorLocation());
|
||||
CachedBlackboard = BB;
|
||||
}
|
||||
|
||||
RunBehaviorTree(BehaviorTreeAsset);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMAIController] AI 控制器已启动: %s"), *InPawn->GetName());
|
||||
}
|
||||
53
Source/MM/AI/MMAIController.h
Normal file
53
Source/MM/AI/MMAIController.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AIController.h"
|
||||
#include "MMAIController.generated.h"
|
||||
|
||||
class UBehaviorTree;
|
||||
class UBlackboardComponent;
|
||||
class AMMEnemyBase;
|
||||
|
||||
/// MM 项目的 AI 控制器。
|
||||
///
|
||||
/// 驱动怪物的行为树和黑板数据。
|
||||
/// 在运行 ``Possess`` 时启动行为树。
|
||||
///
|
||||
/// # 玩家追踪
|
||||
///
|
||||
/// 黑板中维护 ``TargetActor`` 键,表示当前仇恨目标。
|
||||
/// 行为树节点通过查询此键决定行为。
|
||||
///
|
||||
/// - SeeAlso: AAIController, AMMEnemyBase
|
||||
UCLASS()
|
||||
class AMMAIController : public AAIController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AMMAIController();
|
||||
|
||||
/// 控制 Pawn 时启动行为树。
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
/// 行为树资产引用。
|
||||
///
|
||||
/// 在怪物蓝图的 AI Controller 类中设置,
|
||||
/// 或通过 C++ 构造函数默认指定。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
UBehaviorTree* BehaviorTreeAsset;
|
||||
|
||||
/// 黑板中目标 Actor 的键名。
|
||||
static const FName TargetActorKeyName;
|
||||
|
||||
/// 黑板中生成位置的键名。
|
||||
static const FName SpawnLocationKeyName;
|
||||
|
||||
/// 黑板中是否在攻击范围的键名。
|
||||
static const FName IsInAttackRangeKeyName;
|
||||
|
||||
private:
|
||||
/// 缓存的黑板组件引用。
|
||||
UPROPERTY()
|
||||
UBlackboardComponent* CachedBlackboard;
|
||||
};
|
||||
@@ -41,6 +41,23 @@ void AMMCharacterBase::BeginPlay()
|
||||
{
|
||||
AttributeSet = const_cast<UMMAttributeSet*>(
|
||||
AbilitySystemComponent->InitStats(UMMAttributeSet::StaticClass(), nullptr));
|
||||
|
||||
// 授予默认攻击技能
|
||||
if (DefaultAttackAbility)
|
||||
{
|
||||
AbilitySystemComponent->GiveAbility(
|
||||
FGameplayAbilitySpec(DefaultAttackAbility, 1, 0, this));
|
||||
}
|
||||
|
||||
// 授予 StartupAbilities 中的技能
|
||||
for (int32 Idx = 0; Idx < StartupAbilities.Num(); ++Idx)
|
||||
{
|
||||
if (StartupAbilities[Idx])
|
||||
{
|
||||
AbilitySystemComponent->GiveAbility(
|
||||
FGameplayAbilitySpec(StartupAbilities[Idx], 1, Idx + 1, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 注册默认输入映射上下文 ----
|
||||
@@ -63,6 +80,7 @@ void AMMCharacterBase::SetupPlayerInputComponent(UInputComponent* PlayerInputCom
|
||||
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMMCharacterBase::Move);
|
||||
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMMCharacterBase::Look);
|
||||
EnhancedInput->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AMMCharacterBase::Attack);
|
||||
EnhancedInput->BindAction(Skill1Action, ETriggerEvent::Triggered, this, &AMMCharacterBase::Skill1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +130,20 @@ void AMMCharacterBase::Attack(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
|
||||
void AMMCharacterBase::Skill1(const FInputActionValue& Value)
|
||||
{
|
||||
if (bIsDead || !AbilitySystemComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 激活 StartupAbilities 中的第一个技能
|
||||
if (StartupAbilities.Num() > 0 && StartupAbilities[0])
|
||||
{
|
||||
AbilitySystemComponent->TryActivateAbilityByClass(StartupAbilities[0]);
|
||||
}
|
||||
}
|
||||
|
||||
void AMMCharacterBase::HandleHealthChanged(float DeltaValue)
|
||||
{
|
||||
if (bIsDead)
|
||||
@@ -119,13 +151,11 @@ void AMMCharacterBase::HandleHealthChanged(float DeltaValue)
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有受到伤害(负值变化)才触发
|
||||
if (DeltaValue < 0.0f)
|
||||
{
|
||||
const float CurrentHealth = AttributeSet ? AttributeSet->GetHealth() : 0.0f;
|
||||
OnDamaged.Broadcast(CurrentHealth - DeltaValue, CurrentHealth);
|
||||
|
||||
// 检查死亡
|
||||
if (CurrentHealth <= 0.0f)
|
||||
{
|
||||
HandleDeath();
|
||||
@@ -137,13 +167,11 @@ void AMMCharacterBase::HandleDeath()
|
||||
{
|
||||
bIsDead = true;
|
||||
|
||||
// 禁用输入
|
||||
if (APlayerController* PC = Cast<APlayerController>(Controller))
|
||||
{
|
||||
DisableInput(PC);
|
||||
}
|
||||
|
||||
// 广播死亡事件
|
||||
OnDeath.Broadcast();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMCharacterBase] 角色死亡: %s"), *GetName());
|
||||
|
||||
@@ -106,6 +106,19 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
||||
TSubclassOf<UMMGameplayAbility> DefaultAttackAbility;
|
||||
|
||||
/// 技能输入动作(按键1)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
|
||||
UInputAction* Skill1Action;
|
||||
|
||||
// ==================== 技能 ====================
|
||||
|
||||
/// 角色拥有的技能列表。
|
||||
///
|
||||
/// 在 ``BeginPlay`` 中通过 ASC 授予这些技能。
|
||||
/// 子类在构造函数中填充此数组。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ability")
|
||||
TArray<TSubclassOf<UMMGameplayAbility>> StartupAbilities;
|
||||
|
||||
// ==================== 事件 ====================
|
||||
|
||||
/// 角色受击事件。
|
||||
@@ -132,6 +145,12 @@ public:
|
||||
UFUNCTION()
|
||||
void Attack(const FInputActionValue& Value);
|
||||
|
||||
/// 处理技能1输入。
|
||||
///
|
||||
/// 激活 ``StartupAbilities`` 中索引为 1 的技能(横斩)。
|
||||
UFUNCTION()
|
||||
void Skill1(const FInputActionValue& Value);
|
||||
|
||||
protected:
|
||||
/// 游戏开始时的初始化逻辑。
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "MMGameMode.h"
|
||||
#include "MMCharacterBase.h"
|
||||
#include "MMPlayerController.h"
|
||||
#include "UI/MMHUD.h"
|
||||
|
||||
AMMGameMode::AMMGameMode()
|
||||
{
|
||||
DefaultPawnClass = AMMCharacterBase::StaticClass();
|
||||
PlayerControllerClass = AMMPlayerController::StaticClass();
|
||||
HUDClass = AMMHUD::StaticClass();
|
||||
}
|
||||
|
||||
30
Source/MM/Enemy/MMEnemyBase.cpp
Normal file
30
Source/MM/Enemy/MMEnemyBase.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "MMEnemyBase.h"
|
||||
#include "GAS/Abilities/MMGameplayAbility.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
AMMEnemyBase::AMMEnemyBase(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
// 怪物默认不被玩家控制
|
||||
AutoPossessPlayer = EAutoReceiveInput::Disabled;
|
||||
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
|
||||
|
||||
// AI 不需要玩家输入相关
|
||||
bIsDead = false;
|
||||
}
|
||||
|
||||
void AMMEnemyBase::HandleDeath()
|
||||
{
|
||||
// 广播怪物死亡事件
|
||||
OnEnemyDied.Broadcast(this);
|
||||
|
||||
// 调用基类死亡处理
|
||||
Super::HandleDeath();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMEnemyBase] 怪物死亡: %s"), *GetName());
|
||||
|
||||
// Phase 0: 简单延时销毁
|
||||
// 后续改为死亡动画 + 掉落物生成
|
||||
SetLifeSpan(3.0f);
|
||||
}
|
||||
74
Source/MM/Enemy/MMEnemyBase.h
Normal file
74
Source/MM/Enemy/MMEnemyBase.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Core/MMCharacterBase.h"
|
||||
#include "MMEnemyBase.generated.h"
|
||||
|
||||
/// MM 项目的怪物基类。
|
||||
///
|
||||
/// 所有 AI 控制的敌人(非玩家角色)的基类,继承自 ``AMMCharacterBase``。
|
||||
/// 拥有自己的攻击能力、感知范围和死亡掉落逻辑。
|
||||
///
|
||||
/// # 功能
|
||||
/// - 巡逻/追击/攻击三种行为状态
|
||||
/// - 仇恨范围和脱战范围
|
||||
/// - 死亡时广播事件(用于生成器计数和掉落)
|
||||
///
|
||||
/// - SeeAlso: AMMCharacterBase, AMMAIController
|
||||
UCLASS()
|
||||
class AMMEnemyBase : public AMMCharacterBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AMMEnemyBase(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
// ==================== AI 参数 ====================
|
||||
|
||||
/// 仇恨检测范围。
|
||||
/// 玩家进入此范围后开始追击。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
float AggroRange = 600.0f;
|
||||
|
||||
/// 脱战范围。
|
||||
/// 玩家离开此范围后停止追击,返回巡逻。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
float DeaggroRange = 1200.0f;
|
||||
|
||||
/// 攻击范围。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
float AttackRange = 150.0f;
|
||||
|
||||
/// 巡逻半径。
|
||||
/// 怪物在生成点附近随机巡逻的范围。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
float PatrolRadius = 500.0f;
|
||||
|
||||
/// 巡逻等待时间范围(秒)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
FVector2D PatrolWaitRange = FVector2D(1.0f, 3.0f);
|
||||
|
||||
// ==================== 生成信息 ====================
|
||||
|
||||
/// 生成位置(由 Spawner 设置)。
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
|
||||
FVector SpawnLocation;
|
||||
|
||||
/// 设置生成位置。
|
||||
UFUNCTION(BlueprintCallable, Category = "AI")
|
||||
void SetSpawnLocation(const FVector& Location) { SpawnLocation = Location; }
|
||||
|
||||
// ==================== 死亡 ====================
|
||||
|
||||
/// 怪物死亡事件委托。
|
||||
/// - Parameter Enemy: 死亡的怪物实例
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEnemyDied, AMMEnemyBase*, Enemy);
|
||||
|
||||
/// 怪物死亡事件。
|
||||
UPROPERTY(BlueprintAssignable, Category = "AI")
|
||||
FOnEnemyDied OnEnemyDied;
|
||||
|
||||
protected:
|
||||
/// 处理死亡:广播 OnEnemyDied 事件。
|
||||
virtual void HandleDeath() override;
|
||||
};
|
||||
110
Source/MM/Enemy/MMEnemySpawner.cpp
Normal file
110
Source/MM/Enemy/MMEnemySpawner.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "MMEnemySpawner.h"
|
||||
#include "MMEnemyBase.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
AMMEnemySpawner::AMMEnemySpawner()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.TickInterval = 1.0f;
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (bAutoStart)
|
||||
{
|
||||
StartSpawning();
|
||||
}
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::StartSpawning()
|
||||
{
|
||||
bIsSpawning = true;
|
||||
TimeSinceLastSpawn = SpawnInterval; // 立即触发第一次生成
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::StopSpawning()
|
||||
{
|
||||
bIsSpawning = false;
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
|
||||
if (!bIsSpawning || !EnemyClass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSinceLastSpawn += DeltaSeconds;
|
||||
|
||||
if (TimeSinceLastSpawn >= SpawnInterval && AliveCount < MaxAliveCount)
|
||||
{
|
||||
SpawnEnemy();
|
||||
TimeSinceLastSpawn = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::SpawnEnemy()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
if (!World || !EnemyClass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算生成位置
|
||||
FVector SpawnLocation = GetActorLocation();
|
||||
if (SpawnPoints.Num() > 0)
|
||||
{
|
||||
// 从预设点中随机选择
|
||||
SpawnLocation += SpawnPoints[FMath::RandRange(0, SpawnPoints.Num() - 1)];
|
||||
}
|
||||
else
|
||||
{
|
||||
// 在半径内随机生成
|
||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World);
|
||||
if (NavSys)
|
||||
{
|
||||
FNavLocation RandomPoint;
|
||||
if (NavSys->GetRandomReachablePointInRadius(GetActorLocation(), SpawnRadius, RandomPoint))
|
||||
{
|
||||
SpawnLocation = RandomPoint.Location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成怪物
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
AMMEnemyBase* Enemy = World->SpawnActor<AMMEnemyBase>(EnemyClass, SpawnLocation, FRotator::ZeroRotator, SpawnParams);
|
||||
if (Enemy)
|
||||
{
|
||||
Enemy->SetSpawnLocation(SpawnLocation);
|
||||
|
||||
// 绑定死亡事件
|
||||
Enemy->OnEnemyDied.AddDynamic(this, &AMMEnemySpawner::OnEnemyDied);
|
||||
|
||||
SpawnedEnemies.Add(Enemy);
|
||||
AliveCount++;
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMEnemySpawner] 生成怪物: %s (存活: %d/%d)"),
|
||||
*Enemy->GetName(), AliveCount, MaxAliveCount);
|
||||
}
|
||||
}
|
||||
|
||||
void AMMEnemySpawner::OnEnemyDied(AMMEnemyBase* Enemy)
|
||||
{
|
||||
AliveCount = FMath::Max(0, AliveCount - 1);
|
||||
|
||||
if (SpawnedEnemies.Contains(Enemy))
|
||||
{
|
||||
SpawnedEnemies.Remove(Enemy);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMEnemySpawner] 怪物死亡 (存活: %d/%d)"), AliveCount, MaxAliveCount);
|
||||
}
|
||||
92
Source/MM/Enemy/MMEnemySpawner.h
Normal file
92
Source/MM/Enemy/MMEnemySpawner.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "MMEnemySpawner.generated.h"
|
||||
|
||||
class AMMEnemyBase;
|
||||
|
||||
/// 怪物生成器。
|
||||
///
|
||||
/// 在指定位置定时或一次性生成怪物。
|
||||
/// 追踪存活怪物数量,支持最大同时存活数限制。
|
||||
///
|
||||
/// # 使用示例
|
||||
///
|
||||
/// ```cpp
|
||||
/// // 在关卡中放置 Spawner 并设置参数
|
||||
/// Spawner->EnemyClass = ASkeletonEnemy::StaticClass();
|
||||
/// Spawner->MaxAliveCount = 5;
|
||||
/// Spawner->SpawnInterval = 10.0f;
|
||||
/// ```
|
||||
///
|
||||
/// - SeeAlso: AActor, AMMEnemyBase
|
||||
UCLASS()
|
||||
class AMMEnemySpawner : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AMMEnemySpawner();
|
||||
|
||||
/// 每帧更新(仅用于检测是否需要生成)。
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
/// 开始生成怪物。
|
||||
UFUNCTION(BlueprintCallable, Category = "Spawner")
|
||||
void StartSpawning();
|
||||
|
||||
/// 停止生成怪物。
|
||||
UFUNCTION(BlueprintCallable, Category = "Spawner")
|
||||
void StopSpawning();
|
||||
|
||||
// ==================== 生成参数 ====================
|
||||
|
||||
/// 怪物类。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
TSubclassOf<AMMEnemyBase> EnemyClass;
|
||||
|
||||
/// 生成位置(相对于 Spawner 的偏移)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
TArray<FVector> SpawnPoints;
|
||||
|
||||
/// 最大同时存活数量。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
int32 MaxAliveCount = 5;
|
||||
|
||||
/// 生成间隔(秒)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
float SpawnInterval = 5.0f;
|
||||
|
||||
/// 生成半径(如果没有指定 SpawnPoints,在此范围内随机生成)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
float SpawnRadius = 300.0f;
|
||||
|
||||
/// 是否在游戏开始时自动开始生成。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawner")
|
||||
bool bAutoStart = true;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
private:
|
||||
/// 尝试生成一只怪物。
|
||||
void SpawnEnemy();
|
||||
|
||||
/// 怪物死亡回调。
|
||||
UFUNCTION()
|
||||
void OnEnemyDied(AMMEnemyBase* Enemy);
|
||||
|
||||
/// 当前存活数量。
|
||||
int32 AliveCount = 0;
|
||||
|
||||
/// 距离上次生成的时间。
|
||||
float TimeSinceLastSpawn = 0.0f;
|
||||
|
||||
/// 是否正在生成。
|
||||
bool bIsSpawning = false;
|
||||
|
||||
/// 已生成的怪物引用(弱引用,避免阻止销毁)。
|
||||
UPROPERTY()
|
||||
TArray<AMMEnemyBase*> SpawnedEnemies;
|
||||
};
|
||||
79
Source/MM/GAS/Abilities/Saber/MMSkill_Slash.cpp
Normal file
79
Source/MM/GAS/Abilities/Saber/MMSkill_Slash.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "MMSkill_Slash.h"
|
||||
#include "Combat/MMWeaponComponent.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
#include "GameplayEffect.h"
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
UMMSkill_Slash::UMMSkill_Slash()
|
||||
{
|
||||
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
|
||||
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
|
||||
}
|
||||
|
||||
void UMMSkill_Slash::ActivateAbility(
|
||||
const FGameplayAbilitySpecHandle Handle,
|
||||
const FGameplayAbilityActorInfo* ActorInfo,
|
||||
const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
const FGameplayEventData* TriggerEventData)
|
||||
{
|
||||
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
|
||||
{
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
AActor* Avatar = GetAvatarActorFromActorInfo();
|
||||
if (!Avatar)
|
||||
{
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取武器组件
|
||||
UMMWeaponComponent* WeaponComp = Avatar->FindComponentByClass<UMMWeaponComponent>();
|
||||
if (!WeaponComp)
|
||||
{
|
||||
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始挥砍
|
||||
WeaponComp->StartSwing();
|
||||
|
||||
// 设置攻击结束计时器
|
||||
Avatar->GetWorldTimerManager().SetTimer(
|
||||
AttackTimerHandle,
|
||||
this,
|
||||
&UMMSkill_Slash::OnAttackFinished,
|
||||
AttackDuration,
|
||||
false);
|
||||
}
|
||||
|
||||
void UMMSkill_Slash::OnAttackFinished()
|
||||
{
|
||||
AActor* Avatar = GetAvatarActorFromActorInfo();
|
||||
if (Avatar)
|
||||
{
|
||||
if (UMMWeaponComponent* WeaponComp = Avatar->FindComponentByClass<UMMWeaponComponent>())
|
||||
{
|
||||
WeaponComp->EndSwing();
|
||||
}
|
||||
}
|
||||
|
||||
K2_EndAbility();
|
||||
}
|
||||
|
||||
void UMMSkill_Slash::EndAbility(
|
||||
const FGameplayAbilitySpecHandle Handle,
|
||||
const FGameplayAbilityActorInfo* ActorInfo,
|
||||
const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
bool bReplicateEndAbility, bool bWasCancelled)
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(AttackTimerHandle);
|
||||
}
|
||||
|
||||
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
|
||||
}
|
||||
66
Source/MM/GAS/Abilities/Saber/MMSkill_Slash.h
Normal file
66
Source/MM/GAS/Abilities/Saber/MMSkill_Slash.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GAS/Abilities/MMGameplayAbility.h"
|
||||
#include "MMSkill_Slash.generated.h"
|
||||
|
||||
/// Saber 技能:横斩。
|
||||
///
|
||||
/// 前方扇形范围的强力斩击,造成较高伤害。
|
||||
/// 使用冷却 GE 控制冷却时间。
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// | 属性 | 默认值 | 说明 |
|
||||
/// |------|--------|------|
|
||||
/// | CooldownDuration | 3.0s | 冷却时间 |
|
||||
/// | DamageMultiplier | 2.0 | 伤害倍率 |
|
||||
/// | AttackDuration | 0.6s | 挥砍持续时间 |
|
||||
///
|
||||
/// - SeeAlso: UMMGameplayAbility, UMMWeaponComponent
|
||||
UCLASS()
|
||||
class UMMSkill_Slash : public UMMGameplayAbility
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UMMSkill_Slash();
|
||||
|
||||
/// 冷却时间(秒)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill")
|
||||
float CooldownDuration = 3.0f;
|
||||
|
||||
/// 伤害倍率。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill")
|
||||
float DamageMultiplier = 2.0f;
|
||||
|
||||
/// 攻击持续时间(秒)。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill")
|
||||
float AttackDuration = 0.6f;
|
||||
|
||||
/// 冷却 GE 类。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill")
|
||||
TSubclassOf<UGameplayEffect> CooldownGameplayEffect;
|
||||
|
||||
// ~Begin UGameplayAbility Interface
|
||||
virtual void ActivateAbility(
|
||||
const FGameplayAbilitySpecHandle Handle,
|
||||
const FGameplayAbilityActorInfo* ActorInfo,
|
||||
const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
const FGameplayEventData* TriggerEventData) override;
|
||||
|
||||
virtual void EndAbility(
|
||||
const FGameplayAbilitySpecHandle Handle,
|
||||
const FGameplayAbilityActorInfo* ActorInfo,
|
||||
const FGameplayAbilityActivationInfo ActivationInfo,
|
||||
bool bReplicateEndAbility, bool bWasCancelled) override;
|
||||
// ~End UGameplayAbility Interface
|
||||
|
||||
private:
|
||||
/// 攻击结束回调。
|
||||
UFUNCTION()
|
||||
void OnAttackFinished();
|
||||
|
||||
/// 攻击计时器句柄。
|
||||
FTimerHandle AttackTimerHandle;
|
||||
};
|
||||
37
Source/MM/GAS/MMGameplayTags.cpp
Normal file
37
Source/MM/GAS/MMGameplayTags.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "MMGameplayTags.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
|
||||
TSharedPtr<FMMGameplayTags> FMMGameplayTags::CachedInstance;
|
||||
|
||||
const FMMGameplayTags& FMMGameplayTags::Get()
|
||||
{
|
||||
if (!CachedInstance.IsValid())
|
||||
{
|
||||
InitializeNativeTags();
|
||||
}
|
||||
return *CachedInstance;
|
||||
}
|
||||
|
||||
void FMMGameplayTags::InitializeNativeTags()
|
||||
{
|
||||
if (CachedInstance.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CachedInstance = MakeShared<FMMGameplayTags>();
|
||||
UGameplayTagsManager& Manager = UGameplayTagsManager::Get();
|
||||
|
||||
// 冷却标签
|
||||
CachedInstance->Cooldown_Skill1 = Manager.AddNativeGameplayTag(FName("Cooldown.Skill1"), TEXT("技能1冷却"));
|
||||
CachedInstance->Cooldown_Skill2 = Manager.AddNativeGameplayTag(FName("Cooldown.Skill2"), TEXT("技能2冷却"));
|
||||
CachedInstance->Cooldown_Skill3 = Manager.AddNativeGameplayTag(FName("Cooldown.Skill3"), TEXT("技能3冷却"));
|
||||
|
||||
// 状态标签
|
||||
CachedInstance->State_Dead = Manager.AddNativeGameplayTag(FName("State.Dead"), TEXT("角色死亡"));
|
||||
CachedInstance->State_Hit = Manager.AddNativeGameplayTag(FName("State.Hit"), TEXT("角色受击"));
|
||||
|
||||
// 效果标签
|
||||
CachedInstance->Effect_Damage = Manager.AddNativeGameplayTag(FName("Effect.Damage"), TEXT("伤害效果"));
|
||||
CachedInstance->Effect_Heal = Manager.AddNativeGameplayTag(FName("Effect.Heal"), TEXT("治疗效果"));
|
||||
}
|
||||
62
Source/MM/GAS/MMGameplayTags.h
Normal file
62
Source/MM/GAS/MMGameplayTags.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
|
||||
/// MM 项目的 Gameplay Tags 定义。
|
||||
///
|
||||
/// 集中管理所有项目自定义的 Gameplay Tags。
|
||||
/// 所有 Tag 在 ``InitializeNativeTags`` 中通过 ``AddNativeGameplayTag`` 注册。
|
||||
///
|
||||
/// # 使用示例
|
||||
///
|
||||
/// ```cpp
|
||||
/// FGameplayTag Tag = FMMGameplayTags::Get().Cooldown_Skill1;
|
||||
/// ```
|
||||
///
|
||||
/// - Note: 在 MM 模块的 ``StartupModule`` 中调用 ``InitializeNativeTags``
|
||||
/// - SeeAlso: FGameplayTag, UGameplayTagsManager
|
||||
struct FMMGameplayTags
|
||||
{
|
||||
public:
|
||||
/// 获取单例实例。
|
||||
static const FMMGameplayTags& Get();
|
||||
|
||||
/// 初始化并注册所有 Native Tags。
|
||||
///
|
||||
/// 应在模块启动时调用(``MM.cpp`` 的 ``StartupModule`` 中)。
|
||||
static void InitializeNativeTags();
|
||||
|
||||
// ==================== 冷却标签 ====================
|
||||
|
||||
/// 技能1冷却标签。
|
||||
///
|
||||
/// 挂载在冷却 GE 上,技能激活时检查此标签是否存在来判断是否在冷却中。
|
||||
FGameplayTag Cooldown_Skill1;
|
||||
|
||||
/// 技能2冷却标签。
|
||||
FGameplayTag Cooldown_Skill2;
|
||||
|
||||
/// 技能3冷却标签。
|
||||
FGameplayTag Cooldown_Skill3;
|
||||
|
||||
// ==================== 状态标签 ====================
|
||||
|
||||
/// 角色死亡标签。
|
||||
FGameplayTag State_Dead;
|
||||
|
||||
/// 角色受击标签。
|
||||
FGameplayTag State_Hit;
|
||||
|
||||
// ==================== 效果标签 ====================
|
||||
|
||||
/// 伤害效果标签。
|
||||
FGameplayTag Effect_Damage;
|
||||
|
||||
/// 治疗效果标签。
|
||||
FGameplayTag Effect_Heal;
|
||||
|
||||
private:
|
||||
/// 缓存的 Tag 引用。
|
||||
static TSharedPtr<FMMGameplayTags> CachedInstance;
|
||||
};
|
||||
@@ -7,17 +7,26 @@ public class MM : ModuleRules
|
||||
public MM(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayAbilities", "GameplayTags", "GameplayTasks" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { });
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"EnhancedInput",
|
||||
"GameplayAbilities",
|
||||
"GameplayTags",
|
||||
"GameplayTasks",
|
||||
"AIModule",
|
||||
"NavigationSystem"
|
||||
});
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"UMG"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "MM.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "GAS/MMGameplayTags.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, MM, "MM" );
|
||||
#define LOCTEXT_NAMESPACE "FMMModule"
|
||||
|
||||
void FMMModule::StartupModule()
|
||||
{
|
||||
// 注册项目自定义 Gameplay Tags
|
||||
FMMGameplayTags::InitializeNativeTags();
|
||||
}
|
||||
|
||||
void FMMModule::ShutdownModule()
|
||||
{
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FMMModule, MM)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "SaberCharacter.h"
|
||||
#include "Combat/MMWeaponComponent.h"
|
||||
#include "GAS/Abilities/Saber/MMAttack_Melee.h"
|
||||
#include "GAS/Abilities/Saber/MMSkill_Slash.h"
|
||||
#include "AbilitySystemComponent.h"
|
||||
|
||||
ASaberCharacter::ASaberCharacter(const FObjectInitializer& ObjectInitializer)
|
||||
@@ -11,4 +12,7 @@ ASaberCharacter::ASaberCharacter(const FObjectInitializer& ObjectInitializer)
|
||||
|
||||
// 设置默认攻击技能为 Saber 普攻
|
||||
DefaultAttackAbility = UMMAttack_Melee::StaticClass();
|
||||
|
||||
// 授予 Saber 技能:横斩
|
||||
StartupAbilities.Add(UMMSkill_Slash::StaticClass());
|
||||
}
|
||||
|
||||
76
Source/MM/UI/MMHUD.cpp
Normal file
76
Source/MM/UI/MMHUD.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "MMHUD.h"
|
||||
#include "UI/Widgets/MMHealthBarWidget.h"
|
||||
#include "Core/MMCharacterBase.h"
|
||||
#include "GAS/AbilitySystem/MMAttributeSet.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
AMMHUD::AMMHUD()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
}
|
||||
|
||||
void AMMHUD::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
CreateWidgets();
|
||||
BindCharacterEvents();
|
||||
}
|
||||
|
||||
void AMMHUD::CreateWidgets()
|
||||
{
|
||||
if (HealthBarWidgetClass && !HealthBarWidget)
|
||||
{
|
||||
HealthBarWidget = CreateWidget<UMMHealthBarWidget>(GetOwningPlayerController(), HealthBarWidgetClass);
|
||||
if (HealthBarWidget)
|
||||
{
|
||||
HealthBarWidget->AddToViewport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMMHUD::BindCharacterEvents()
|
||||
{
|
||||
if (AMMCharacterBase* Character = Cast<AMMCharacterBase>(GetOwningPawn()))
|
||||
{
|
||||
Character->OnDamaged.AddDynamic(this, &AMMHUD::OnCharacterDamaged);
|
||||
Character->OnDeath.AddDynamic(this, &AMMHUD::OnCharacterDeath);
|
||||
|
||||
// 初始化生命条
|
||||
if (Character->GetAttributeSet())
|
||||
{
|
||||
HealthBarWidget->UpdateHealth(
|
||||
Character->GetAttributeSet()->GetHealth(),
|
||||
Character->GetAttributeSet()->GetMaxHealth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMMHUD::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
// Tick 仅用于处理 PlayerPawn 延迟设置的情况
|
||||
// 实际 UI 更新通过事件驱动
|
||||
}
|
||||
|
||||
void AMMHUD::OnCharacterDamaged(float OldHealth, float NewHealth)
|
||||
{
|
||||
if (HealthBarWidget)
|
||||
{
|
||||
if (AMMCharacterBase* Character = Cast<AMMCharacterBase>(GetOwningPawn()))
|
||||
{
|
||||
if (Character->GetAttributeSet())
|
||||
{
|
||||
HealthBarWidget->UpdateHealth(
|
||||
Character->GetAttributeSet()->GetHealth(),
|
||||
Character->GetAttributeSet()->GetMaxHealth());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMMHUD::OnCharacterDeath()
|
||||
{
|
||||
// Phase 0: 死亡后 UI 反馈可在此扩展
|
||||
UE_LOG(LogTemp, Log, TEXT("[MMHUD] 角色死亡"));
|
||||
}
|
||||
55
Source/MM/UI/MMHUD.h
Normal file
55
Source/MM/UI/MMHUD.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/HUD.h"
|
||||
#include "MMHUD.generated.h"
|
||||
|
||||
class UMMHealthBarWidget;
|
||||
class AMMCharacterBase;
|
||||
|
||||
/// MM 项目的 HUD。
|
||||
///
|
||||
/// 管理游戏界面的创建和更新,包括生命条。
|
||||
/// 监听角色的 ``OnDamaged`` 和 ``OnDeath`` 事件来更新 UI。
|
||||
///
|
||||
/// - Note: Widget 通过 C++ 代码创建,不依赖编辑器蓝图
|
||||
/// - SeeAlso: AHUD, UMMHealthBarWidget
|
||||
UCLASS()
|
||||
class AMMHUD : public AHUD
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/// 构造函数。
|
||||
AMMHUD();
|
||||
|
||||
/// 每帧更新(仅用于检测 PlayerPawn 变化)。
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
/// 初始化 HUD 并绑定事件。
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
protected:
|
||||
/// 创建并添加 Widget 到视口。
|
||||
void CreateWidgets();
|
||||
|
||||
/// 绑定角色事件。
|
||||
void BindCharacterEvents();
|
||||
|
||||
/// 生命条 Widget 类。
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
|
||||
TSubclassOf<UMMHealthBarWidget> HealthBarWidgetClass;
|
||||
|
||||
/// 生命条 Widget 实例。
|
||||
UPROPERTY()
|
||||
UMMHealthBarWidget* HealthBarWidget;
|
||||
|
||||
private:
|
||||
/// 角色受击回调,更新生命条。
|
||||
UFUNCTION()
|
||||
void OnCharacterDamaged(float OldHealth, float NewHealth);
|
||||
|
||||
/// 角色死亡回调。
|
||||
UFUNCTION()
|
||||
void OnCharacterDeath();
|
||||
};
|
||||
35
Source/MM/UI/Widgets/MMHealthBarWidget.cpp
Normal file
35
Source/MM/UI/Widgets/MMHealthBarWidget.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "MMHealthBarWidget.h"
|
||||
#include "Components/ProgressBar.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Widgets/Layout/Anchors.h"
|
||||
|
||||
bool UMMHealthBarWidget::Initialize()
|
||||
{
|
||||
if (!Super::Initialize())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果没有 WidgetTree(纯代码创建),手动构建
|
||||
if (!WidgetTree)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UMMHealthBarWidget::UpdateHealth(float CurrentHealth, float MaxHealth)
|
||||
{
|
||||
if (HealthProgressBar)
|
||||
{
|
||||
const float Percent = (MaxHealth > 0.0f) ? (CurrentHealth / MaxHealth) : 0.0f;
|
||||
HealthProgressBar->SetPercent(Percent);
|
||||
}
|
||||
|
||||
if (HealthText)
|
||||
{
|
||||
HealthText->SetText(FText::FromString(
|
||||
FString::Printf(TEXT("%.0f / %.0f"), CurrentHealth, MaxHealth)));
|
||||
}
|
||||
}
|
||||
48
Source/MM/UI/Widgets/MMHealthBarWidget.h
Normal file
48
Source/MM/UI/Widgets/MMHealthBarWidget.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "MMHealthBarWidget.generated.h"
|
||||
|
||||
class UProgressBar;
|
||||
class UTextBlock;
|
||||
|
||||
/// 生命条 Widget。
|
||||
///
|
||||
/// 纯 C++ 构建的 HUD 生命条,显示角色当前/最大生命值。
|
||||
/// 不依赖编辑器蓝图资产。
|
||||
///
|
||||
/// # 使用示例
|
||||
///
|
||||
/// ```cpp
|
||||
/// auto* HealthBar = CreateWidget<UMMHealthBarWidget>(PC);
|
||||
/// HealthBar->UpdateHealth(80.0f, 100.0f);
|
||||
/// ```
|
||||
///
|
||||
/// - SeeAlso: UUserWidget, UProgressBar
|
||||
UCLASS()
|
||||
class UMMHealthBarWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/// 构造并初始化 Widget 树。
|
||||
///
|
||||
/// 创建 ProgressBar 和 TextBlock 子组件。
|
||||
virtual bool Initialize() override;
|
||||
|
||||
/// 更新生命值显示。
|
||||
///
|
||||
/// - Parameter CurrentHealth: 当前生命值
|
||||
/// - Parameter MaxHealth: 最大生命值
|
||||
void UpdateHealth(float CurrentHealth, float MaxHealth);
|
||||
|
||||
protected:
|
||||
/// 生命条进度条。
|
||||
UPROPERTY()
|
||||
UProgressBar* HealthProgressBar;
|
||||
|
||||
/// 生命值文本。
|
||||
UPROPERTY()
|
||||
UTextBlock* HealthText;
|
||||
};
|
||||
Reference in New Issue
Block a user