UE5纯干货C++ 您所在的位置:网站首页 tool类 UE5纯干货C++

UE5纯干货C++

2023-04-12 05:02| 来源: 网络整理| 查看: 265

最近做一个动作Demo时,觉得可以把常用的基本战斗功能整合成一个组件。于是就开始整理。

下文链接

源码链接:

提取码:1234

状态管理

对于整体的战斗系统,首先需要一个统一的状态管理。基于插件的通用性,我们可以把相关状态分为以下几类

Idle :不解释Attack:攻击状态Defense:防御状态Dodge:闪避状态SuperArmor:霸体状态BPHit:受击状态

每个状态的具体转换关系如下,其中不同条件的触发机制差别很大。比如有些条件是蒙太奇动画结束,有些是动画通知或者用户输入,这些都要一一处理。

很乱,凑合看吧

由上,组件里必须有一个统一的当前状态变量,以及对应的统一状态转换函数。

//AttackComp.h //先声明枚举类 namespace Combat_State { enum State { IDLE = 0, ATTACK = 1, DEFENSE = 2, BE_HIT = 3, SPATTACK = 4, //人物技能,模拟霸体 DODGE = 5, }; } UCLASS(Blueprintable , ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class SS_ATTACKCOMPONENT_API UAttackComp : public UActorComponent { GENERATED_BODY() public: // Sets default values for this component's properties UAttackComp(); ... private: typedef int32 F_Combat_State; F_Combat_State nowState = Combat_State::IDLE; //统一处理状态转换 bool ChangeCombatState(F_Combat_State toState); ... } //AttackComp.cpp bool UAttackComp::ChangeCombatState(F_Combat_State toState) { //后续补充各状态处理逻辑 ... //默认改变 nowState = toState; return true; } 攻击模块

当玩家不停的连击A键时,他们在期待什么?控制的角色播放攻击动画,进行命中判定,然后尽可能快的进入下一段攻击动画,这就是攻击模块要做的事。

连击蒙太奇

从最简单开始考虑,需要指定一段蒙太奇,然后再玩家进行攻击操作并可以攻击时播放它。然后,这个蒙太奇应该有多个分段,对应多段连击动画。当玩家进行攻击操作时,有如下几种情况

由 CanAttak() 函数判定当前状态不允许进行攻击,返回。允许攻击且角色未在攻击状态,播放第一段动画。允许攻击且角色在攻击状态且无下一段蒙太奇,返回。允许攻击且角色在攻击状态且有下一段蒙太奇,设置 bContinueAttack 为 true,等待回调处理。//AttackComp.h UPROPERTY(EditAnywhere,Category="AttackMontage") UAnimMontage* AttackMontage; UFUNCTION(BlueprintCallable,Category="AttackComponent") virtual void Attack(); //是否继续攻击 bool obWantContinueAttack; //当前播放攻击蒙太奇片段的索引 int NowAttackSectionNum; //根据当前状态判定能否攻击 bool CanAttack(); //AttackComp.cpp bool UAttackComp::CanAttack() { return nowState != Combat_State::BE_HIT && nowState != Combat_State::DODGE && nowState != Combat_State::SPATTACK; } void UAttackComp::Attack() { if(!CanAttack()) return; if(!AttackMontage) { UE_LOG(LogTemp, Warning, TEXT("No Attack Montage")); return; } ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; if(nowState != Combat_State::ATTACK) { NowAttackSectionNum = 0; OwnerCharacter->PlayAnimMontage(AttackMontage,1.0f,AttackMontage->GetSectionName(NowAttackSectionNum)); ChangeCombatState(Combat_State::ATTACK); } else { if(NowAttackSectionNum GetNumSections() - 1) { bWantContinueAttack = true; } } } //在每个蒙太奇攻击片段结束时的回调函数 void UAttackComp::AttackEnd() { ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; if(bWantContinueAttack) { bWantContinueAttack = false; if(NowAttackSectionNum == AttackMontage->GetNumSections() - 1) { //防止越界,不会到达 return; } NowAttackSectionNum += 1; OwnerCharacter->PlayAnimMontage(AttackMontage,1.0f,AttackMontage->GetSectionName(NowAttackSectionNum)); } else { ChangeCombatState(Combat_State::IDLE); } }

对于回调时间,不能选择在整个动画结束时,因为单个攻击动画一般带有完整的后摇,在进行连击操作时必须跳过这些后摇。只能手动加入动画通知,忍痛增加操作量。

//Notify_AttackEnd.h UCLASS() class SS_ATTACKCOMPONENT_API UNotify_AttackEnd : public UAnimNotify { GENERATED_BODY() public: virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override; }; //Notify_AttackEnd.cpp void UNotify_AttackEnd::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) { Super::Notify(MeshComp, Animation); AActor* OwnerActor = MeshComp->GetOwner(); if(!OwnerActor) return; UAttackComp* AttackComponent = Cast(OwnerActor->GetComponentByClass(UAttackComp::StaticClass())); if(!AttackComponent) return; //组件内回调函数 AttackComponent->AttackEnd(); }

使用方法如图

忽略其他通知攻击线条检测

在播放攻击动画时如何判定攻击是否命中?这里我们采取武器插槽定位+线条检测的方式。

首先在武器上创建用于检测的定位插槽

这里我设置了5个插槽并统一以"SS_AttackComponent"开头,用于在组件中获取插槽位置,免去了繁琐的一一设置。

在组件中,使用三个变量对线条检测进行设置。

//Weapon Detect part //插槽开头统一命名,用于获取检测插槽数组 UPROPERTY(EditAnywhere,Category="WeaponTraceDetect") FString TraceSocketNameStartWith = "SS_AttackComponent"; //检测间隔时间 UPROPERTY(EditAnywhere,Category="WeaponTraceDetect") float TraceDetectTickTime = 0.02f; //指定武器Mesh命名,如果直接在人物上设置的插槽,则留空 UPROPERTY(EditAnywhere,Category="WeaponTraceDetect") FName WeaponMeshComponentName = "";

实现线条攻击检测的逻辑如下

在 BeginPlay() 初始化插槽命名数组在 StartTraceDetect() 中获得各插槽位置,并通过定时器循环触发 TickTraceDetect() 在 TickTraceDetect() 中获得新的各插槽位置,绘制与原有位置线条,检测碰撞在 EndTraceDetect() 中结束定时器

代码如下

//AttackComp.h //各插槽对应的位置 TMap TraceLocationMap; FTimerHandle TraceDetectTimerHandle; //记录攻击线条检测的数组 TArray TraceSocketNameArr; //AttackComp.cpp //在 BeginPlay() 中调用 void UAttackComp::InitWeaponSocketNameArr() { ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; //判断武器是否为单独的骨骼 WeaponComponent = nullptr; if(WeaponMeshComponentName == "") { //无单独骨骼,设置为人物骨骼 WeaponComponent = OwnerCharacter->GetMesh(); } else { TArray SkeletalMeshComponents; OwnerCharacter->GetComponents(SkeletalMeshComponents,true); for(auto Mesh : SkeletalMeshComponents) { if(Mesh->GetName() == WeaponMeshComponentName.ToString()) { WeaponComponent = Mesh; break; } } if(!WeaponComponent) { WeaponComponent = OwnerCharacter->GetMesh(); } } //从指定骨骼上获得所有插槽 TArray AllSocketName = WeaponComponent->GetAllSocketNames(); for(auto SocketName : AllSocketName) { //是否以指定开头命名 if(SocketName.ToString().StartsWith(TraceSocketNameStartWith)) { //加入插槽数组 TraceSocketNameArr.Add(SocketName); } } } //更新并记录当前位置 void UAttackComp::GetSocketLocationFromNameArr() { // ACharacter* OwnerCharacter = Cast( GetOwner()); // if(!OwnerCharacter) return; //get socket location for(auto SocketName : TraceSocketNameArr) { FVector SocketLocation = WeaponComponent->GetSocketLocation(SocketName); TraceLocationMap.Add({SocketName,SocketLocation}); } } void UAttackComp::StartTraceDetect() { //以指定的间隔调用TickTraceDetect进行检测 GetWorld()->GetTimerManager().SetTimer(TraceDetectTimerHandle,this,&UAttackComp::TickTraceDetect,TraceDetectTickTime,true,-1.0f); if(GetOwner()) { //不检测自己 QParams.AddIgnoredActor(GetOwner()); //初始化插槽位置 GetSocketLocationFromNameArr(); } } void UAttackComp::TickTraceDetect() { //未开启线条检测,范围 if(!bTraceDetect) return; ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; for(auto SocketName : TraceSocketNameArr) { FVector oldLocation = TraceLocationMap.FindRef(SocketName); FVector newLocation = WeaponComponent->GetSocketLocation(SocketName); if(bUseDebug) { DrawDebugLine(GetWorld(), oldLocation,newLocation,FColor::Red,false,3,0,1); } // FHitResult HitRes; //进行通过新旧位置进行轨迹检测 bool bHit = GetWorld()->LineTraceSingleByChannel(HitRes,oldLocation,newLocation,ECC_Pawn,QParams); if(bHit) { //每次攻击仅检测一次 QParams.AddIgnoredActor(HitRes.GetActor()); //需要检测到的Actor也有AttackComp组件 if(auto victimsAttackComp = Cast(HitRes.GetActor()->GetComponentByClass(UAttackComp::StaticClass()))) { ACharacter* victims = Cast(HitRes.GetActor()); if(victimsAttackComp->CanBeHit()) { //进行击中和受击处理 Hit(victims,(oldLocation + newLocation)/2); victimsAttackComp->BeHit(OwnerCharacter); } } } //更新插槽位置 TraceLocationMap.Add({SocketName,newLocation}); } } void UAttackComp::EndTraceDetect() { //结束定时器 GetWorld()->GetTimerManager().ClearTimer(TraceDetectTimerHandle); //关闭尾迹,在攻击表现模块 if(bUseTrailParticle && TrailParticle) { TarilParticleSystemComponent->EndTrails(); } //重置忽略Actor QParams.ClearIgnoredActors(); }

同样的,线条检测的开始和结束也需要动画通知触发,其与AttackEnd通知基本一致,不再细谈

void UNotify_TraceBegin::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) { Super::Notify(MeshComp, Animation); // UE_LOG(LogTemp, Warning, TEXT("TraceBegin")); AActor* OwnerActor = MeshComp->GetOwner(); if(!OwnerActor) return; UAttackComp* AttackComponent = Cast(OwnerActor->GetComponentByClass(UAttackComp::StaticClass())); if(!AttackComponent) return; AttackComponent->StartTraceDetect(); } 攻击表现

改功能用于增强攻击时的打击感,具体有以下几个部分

镜头晃动卡肉攻击音效和命中音效命中时的Niagara系统粒子尾迹

在头文件中实现用户设置

//Attack performance part UPROPERTY(EditAnywhere,Category= "AttackPerformance") bool bUseAttackPerformance; UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) TSubclassOf AttackCameraShake; //卡肉时间 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) float AnimPauseTime = 0.01f; //攻击音效 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) TArray AttackCueArr; //命中音效 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) TArray HitCueArr; //命中时触发的粒子系统 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) UNiagaraSystem* HitNiagara; UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) bool bUseTrailParticle; //尾迹 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseTrailParticle")) UParticleSystem* TrailParticle; //附着尾迹的插槽命名 UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseTrailParticle")) FName FirstSocketName = "Trail_Sta"; UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseTrailParticle")) FName SecondSocketName = "Trail_End"; UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseTrailParticle")) float InWidth = 2.0f; private: //尾迹粒子系统 UParticleSystemComponent* TarilParticleSystemComponent; void AttachParticleSystem();

按照逻辑顺序逐步实现

BeginPlay阶段:初始化尾迹系统

//在BeginPlay中调用 void UAttackComp::AttachParticleSystem() { ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; //AttackComp私有变量 TarilParticleSystemComponent = NewObject(OwnerCharacter); TarilParticleSystemComponent->SetupAttachment(WeaponComponent); if(TrailParticle) { TarilParticleSystemComponent->SetTemplate(TrailParticle); } TarilParticleSystemComponent->RegisterComponent(); }

攻击开始时:镜头晃动,攻击音效,尾迹

直接用引擎自带的CameraShake,随机播放攻击音效数组中的一个,开始播放尾迹。

// .h UPROPERTY(EditAnywhere,Category="AttackPerformance",meta=(EditCondition = "bUseAttackPerformance")) TSubclassOf AttackCameraShake; // .cpp void UAttackComp::StartTraceDetect() { GetWorld()->GetTimerManager().SetTimer(TraceDetectTimerHandle,this,&UAttackComp::TickTraceDetect,TraceDetectTickTime,true,-1.0f); if(GetOwner()) { QParams.AddIgnoredActor(GetOwner()); GetSocketLocationFromNameArr(); } ACharacter* OwnerCharacter = Cast( GetOwner()); if(AttackCameraShake) { APlayerController* Controller = Cast(OwnerCharacter->GetController()); if(Controller) { APlayerCameraManager* CameraManager = Controller->PlayerCameraManager; if(CameraManager) { CameraManager->StartCameraShake(AttackCameraShake); } } } if(bUseAttackPerformance && AttackCueArr.Num() > 0) { //随机播放音效 USoundCue* SoundCue = AttackCueArr[FMath::RandRange(0,AttackCueArr.Num()-1)]; FVector Location = GetOwner()->GetActorLocation(); UGameplayStatics::PlaySoundAtLocation(GetWorld(), SoundCue, Location); } if(bUseTrailParticle && TrailParticle) { //开始尾迹 TarilParticleSystemComponent->BeginTrails(FirstSocketName,SecondSocketName,ETrailWidthMode_FromCentre,InWidth); } }

命中时:卡肉,播放命中音效,生成Niagara特效

//HitLocation在线条检测时确定 //平均位置 Hit(victims,(oldLocation + newLocation)/2); void UAttackComp::Hit(ACharacter* victims,const FVector& HitLocation) { ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; if(bUseAttackPerformance) { if(AnimPauseTime > 0.0f) { //设置时间膨胀 GetWorld()->GetWorldSettings()->SetTimeDilation(0.1); FTimerHandle TmpTimerHandle; //直接lambda回调 GetWorld()->GetTimerManager().SetTimer(TmpTimerHandle, [this]() { GetWorld()->GetWorldSettings()->SetTimeDilation(1); }, AnimPauseTime, false); } if( HitCueArr.Num() > 0) { // 随机播放命中音效 USoundCue* SoundCue = HitCueArr[FMath::RandRange(0, HitCueArr.Num()-1)]; FVector Location = GetOwner()->GetActorLocation(); UGameplayStatics::PlaySoundAtLocation(GetWorld(), SoundCue, Location); } if(HitNiagara) { //生成粒子系统 UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(),HitNiagara,HitLocation); } } }

结束时:结束拖尾

void UAttackComp::EndTraceDetect() { GetWorld()->GetTimerManager().ClearTimer(TraceDetectTimerHandle); if(bUseTrailParticle && TrailParticle) { TarilParticleSystemComponent->EndTrails(); } QParams.ClearIgnoredActors(); } 敌方锁定

最好的方法应该是球体检测+角度判定,获取指定长度和夹角范围内的敌人。但直接用线条检测不判定角度也能实现。

这里使用LockView() 处理锁定操作,TickComponet() 中处理视角逻辑。

// .h UPROPERTY(EditAnywhere,Category = "ViewLock") bool bUseViewLock = false; //最长距离 UPROPERTY(EditAnywhere,Category = "ViewLock",meta=(EditCondition = "bUseViewLock")) float MaxViewLength = 1000.0f; //球体检测半径 UPROPERTY(EditAnywhere,Category = "ViewLock",meta=(EditCondition = "bUseViewLock")) float ViewHalfSize = 500.0f; private: bool bViewLock = false; //锁定的Actor AActor* ViewLockActor; // .cpp void UAttackComp::LockView() { if(!bUseViewLock) { return; } ACharacter* OwnerCharacter = Cast( GetOwner()); if(!OwnerCharacter) return; //调用一次锁定,两次取消 bViewLock = !bViewLock; if(bViewLock) { //获得范围内最近的敌人 ViewLockActor = GetNearestActor(); if(ViewLockActor == nullptr) { //没有敌人,视为未操作 bViewLock = false; } } else { ViewLockActor = nullptr; } } AActor* UAttackComp::GetNearestActor() { // TArray HitResArr; FVector StaLocation = GetOwner()->GetActorLocation(); FVector EndLocation = StaLocation + GetOwner()->GetActorForwardVector() * MaxViewLength; //调用从组件中抽离的工具函数 TArray ResActorArr = AttackComponentPluginUtilities::SphereTraceActorsWithComponent( GetWorld(), StaLocation, EndLocation, ViewHalfSize, bUseDebug ); AActor* ResActor = nullptr; for(AActor* HitActor: ResActorArr) { if(HitActor == GetOwner()) { continue; } //比较距离,获得最近的Actor if(!ResActor) { ResActor = HitActor; } else { float HitDistance = FVector::Dist(HitActor->GetActorLocation(),GetOwner()->GetActorLocation()); float ResDistance = FVector::Dist(ResActor->GetActorLocation(),GetOwner()->GetActorLocation()); if(HitDistance GetComponentByClass(ComponentType::StaticClass())) { resActors.Add(HitActor); } } return resActors; }

获得目标Actor后,在Tick中确定人物朝向

void UAttackComp::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); //锁定目标Actor if(bViewLock && ViewLockActor != nullptr) { FRotator LookAtRoation = UKismetMathLibrary::FindLookAtRotation(GetOwner()->GetActorLocation(),ViewLockActor->GetActorLocation()); LookAtRoation.Pitch = LookAtRoation.Roll = 0.0f; GetOwner()->SetActorRotation(LookAtRoation); if(bUseDebug) { DrawDebugLine(GetWorld(),GetOwner()->GetActorLocation(),ViewLockActor->GetActorLocation(), FColor::Red,false,0.1,0,1); } } }

以上是完整的攻击模块实现,其它模块会在下一篇文章里更新。

任何问题欢迎评论区讨论!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有