Jun 18, 2025
11 mins read
当角色进入到一个触发范围内会在角色位置上生成特效
使用两个定时器分别是FPlayVfxTimerHandle和FCheckTimerHandle
删除Tick函数设置bCanEverTick = false 因为不使用到Tick所以删除会提升性能
构造函数中初始化初始化一个可见的无碰撞球体,为了布置场景时看到范围。在BeginPlay中直接获取球体半径作为DistanceToPlayer的值。
FCheckTimerHandle定时器用于每秒判断角色是否进入到范围内
FPlayVfxTimerHandle定时器用于不断播放特效,并设置一个bIsVFXPlaying用于判断是否正在播放特效,如果已经在播放特效了并且超出距离就会清空FPlayVfxTimerHandle特效播放定时器;如果没有在播放特效并且在距离范围内则会开启定时器。
为什么一定需要bIsVFXPlaying,因为否则人物在距离内时第一个FCheckTimerHandle会不断地调用CheckDistance函数导致无法播放特效。
APMS_VFXTriggerActor::APMS_VFXTriggerActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
DetectionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("DetectionSphere"));
RootComponent = DetectionSphere;
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void APMS_VFXTriggerActor::BeginPlay()
{
Super::BeginPlay();
DistanceToPlayer = DetectionSphere->GetScaledSphereRadius();
if(GetWorld()->GetNetMode() == NM_Standalone || GetWorld()->GetNetMode() == NM_Client)
{
GetWorld()->GetTimerManager().SetTimer(FCheckTimerHandle, this, &APMS_VFXTriggerActor::CheckDistance, 1.0f, true);
}
}
void APMS_VFXTriggerActor::CheckDistance()
{
APMS_ZombiePlayerCharacter* PlayerCharacter = Cast<APMS_ZombiePlayerCharacter>(GetWorld()->GetFirstPlayerController()->GetPawn());
if (PlayerCharacter)
{
float Distance = PlayerCharacter->GetDistanceTo(this);
if (Distance <= DistanceToPlayer && !bIsVFXPlaying)
{
bIsVFXPlaying = true;
PlayVFXEffect(PlayerCharacter->GetActorLocation());
GetWorld()->GetTimerManager().SetTimer(FPlayVfxTimerHandle,
[this, PlayerCharacter](){PlayVFXEffect(PlayerCharacter->GetActorLocation());},
VFXDuration,
true);
}
else if (Distance > DistanceToPlayer && bIsVFXPlaying)
{
bIsVFXPlaying = false;
GetWorld()->GetTimerManager().ClearTimer(FPlayVfxTimerHandle);
}
}
}
void APMS_VFXTriggerActor::PlayVFXEffect(FVector Location)
{
if(!VFXEffect) return;
UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), VFXEffect, Location);
}
空投落下后,空投有一定生命值可被子弹破坏,当生命值为0时,补给箱炸飞,空投子物体隐藏。
SupplyBox中补给箱需要有ProjectileMovementComponent,作用是自己被炸飞。
在构造函数中设置球形碰撞体为根组件,原因是让补给箱落地很平稳,不会因为形状不规则而旋转的很奇怪。一开始将ProjectileMovementComponent设为失活,等待爆炸时再启用。
在BeginPlay中绑定自带的投射物落地触发OnSupplyBoxStop事件的委托OnProjectileStop,让组件失活。
// 空投爆炸补给箱抛射设置
UPROPERTY(EditAnywhere)
float MinLaunchSpeed = 800.f;
UPROPERTY(EditAnywhere)
float MaxLaunchSpeed = 1200.f;
UPROPERTY(EditAnywhere)
float MinUpwardStrength = 800.f;
UPROPERTY(EditAnywhere)
float MaxUpwardStrength = 1000.f;
UPROPERTY(EditAnywhere)
float RandomCircleRadius = 1.f;
UPROPERTY(EditAnywhere)
TObjectPtr<UProjectileMovementComponent> ProjectileMovementComponent;
APMS_SupplyBox::APMS_SupplyBox()
{
PrimaryActorTick.bCanEverTick = true;
SphereCollision = CreateDefaultSubobject<USphereComponent>(TEXT("SphereCollision"));
RootComponent = SphereCollision;
if(SphereCollision)
{
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
MeshComponent->SetupAttachment(SphereCollision);
}
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
if(ProjectileMovementComponent)
{
ProjectileMovementComponent->bAutoActivate = false;
}
}
void APMS_SupplyBox::BeginPlay()
{
Super::BeginPlay();
if(HasAuthority())
{
ProjectileMovementComponent->OnProjectileStop.AddUniqueDynamic(this, &APMS_SupplyBox::OnSupplyBoxStop);
}
}
void APMS_SupplyBox::OnSupplyBoxStop(const FHitResult& ImpactResult)
{
if(ProjectileMovementComponent)
{
ProjectileMovementComponent->Deactivate();
}
}
ExplodeSupplyBox函数是爆炸后补给箱随机飞射的函数
其中LaunchSpeed是飞得远近,UpwardStrength是飞的高低,RandomCircleRadius是飞的发散程度。原理是取圆的随机二维点,变成一个三维向量,X, Y 值是向量归一化后 * LaunchSpeed,向量的Z值是UpwardStrength。再去同步碰撞设置,使箱子不会穿过地面。
void APMS_SupplyBox::ExplodeSupplyBox()
{
if (ProjectileMovementComponent)
{
FVector2D Rand2D = FMath::RandPointInCircle(RandomCircleRadius);
FVector HorizontalDir = FVector(Rand2D.X, Rand2D.Y, 0.f);
HorizontalDir.Normalize();
float UpwardStrength = FMath::FRandRange(MinUpwardStrength, MaxUpwardStrength);
FVector LaunchVelocity = HorizontalDir * FMath::FRandRange(MinLaunchSpeed, MaxLaunchSpeed);
LaunchVelocity.Z = UpwardStrength;
ProjectileMovementComponent->Velocity = LaunchVelocity;
ProjectileMovementComponent->Activate();
MulticastConfigureCollision();
}
}
void APMS_SupplyBox::MulticastConfigureCollision_Implementation()
{
if(SphereCollision)
{
SphereCollision->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
}
}
AirDrop中重写ReceiveDamage、CanTakeDamage和ServerTakeDamage函数
其中PreDead是判断生命值是否<=0。如果空投被破坏或者空投结束就不会再进到ServerTakeDamage函数中。
在空投爆炸函数中将SupplyBox与空投分离,因为原来它是作为空投的ChildActor。如果空投正常工作时是可以被破坏的,如果补给箱被取走了空投就结束工作了相当于EAirDropState::EndPlay,这个时候就不能再执行重复执行AirDropEndPlay的逻辑了。并且广播爆炸特效。
void APMS_AirDrop::ReceiveDamage(const FPMS_DamageInfo& DamageInfo)
{
Super::ReceiveDamage(DamageInfo);
ServerTakeDamage(DamageInfo);
}
bool APMS_AirDrop::CanTakeDamage(const FPMS_DamageInfo& DamageInfo)
{
return true;
}
void APMS_AirDrop::ServerTakeDamage(const FPMS_DamageInfo& DamageInfo)
{
if(AirDropState == EAirDropState::Destroyed || AirDropState == EAirDropState::EndPlay) return;
if(PreDead(DamageInfo))
{
if(HasAuthority())
{
AirDropExplode();
}
}
}
void APMS_AirDrop::AirDropExplode()
{
TArray<UChildActorComponent*> ChildActorComponents;
GetComponents(UChildActorComponent::StaticClass(), ChildActorComponents);
for(int32 Index = 0; Index <FMath::Min(ChildActorComponents.Num(),DropClassList.Num()); Index++)
{
APMS_SupplyBox* SupplyBox = Cast<APMS_SupplyBox>(ChildActorComponents[Index]->GetChildActor());
if (IsValid(SupplyBox))
{
SupplyBox->ExplodeSupplyBox();
SupplyBox->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
SupplyBox->SetOwner(nullptr);
}
}
if(AirDropState == EAirDropState::Play)
{
AirDropState = EAirDropState::Destroyed;
AirDropEndPlay();
}
MulticastExplosionEffect();
}
这个函数将空投的Box设置为NoCollision,只留下空投底座是有碰撞的,并且如果空投是被破坏的,那么就将所有设置为HideComponentTag的子组件SetHiddenInGame。
MARK_PROPERTY_DIRTY_FROM_NAME是手动修改了AirDropState枚举变量标记为Dirty,可以触发同步。
void APMS_AirDrop::AirDropEndPlay()
{
TArray<UShapeComponent*> ShapeComponents;
GetComponents(UShapeComponent::StaticClass(), ShapeComponents);
for(auto ShapeComponent:ShapeComponents)
{
if(ShapeComponent->ComponentHasTag(TEXT("EndWork")))
{
ShapeComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
ShapeComponent->SetCollisionProfileName(UCollisionProfile::BlockAllDynamic_ProfileName,false);
}
else
{
ShapeComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
}
if(AirDropState == EAirDropState::Destroyed)
{
TArray<UActorComponent*> PrimitiveComponentList = GetComponentsByTag(UPrimitiveComponent::StaticClass(), HideComponentTag);
for(UActorComponent* PrimitiveComponent : PrimitiveComponentList)
{
Cast<UPrimitiveComponent>(PrimitiveComponent)->SetHiddenInGame(true);
}
}
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, AirDropState, this);
}
void APMS_AirDrop::MulticastExplosionEffect_Implementation()
{
if(GetNetMode() != NM_DedicatedServer)
{
if(DestroyEffect !=nullptr)
{
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, DestroyEffect, GetActorLocation(), GetActorRotation(), FVector(1,1,1), true, true, ENCPoolMethod::None, true);
}
}
}
以攀爬为例,通过自定义的PMS_GameDelegate_WorldSubsystem向UI层(TS脚本)进行广播,再由UI接收到消息后执行切换图片
在PMS_GameDelegate_WorldSubsystem中定义无参数委托类型
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FNotifyWithNoParam);
UPROPERTY(BlueprintAssignable)
FNotifyWithNoParam OnJumpStart;
UPROPERTY(BlueprintAssignable)
FNotifyWithNoParam OnJumpEnd;
FNotifyWithNoParam& GetOnJumpStart() { return OnJumpStart; }
FNotifyWithNoParam& GetOnJumpEnd() { return OnJumpEnd; }
然后广播事件,其中BroadcastEvent是自定义封装的函数
static void BroadcastEvent(UWorld* World, DelegateType Delegate, Args&&... args)
{
if (World != nullptr)
{
if (UPMS_GameDelegate_WorldSubsystem* GameDelegate = World->GetSubsystem<UPMS_GameDelegate_WorldSubsystem>())
{
if ((GameDelegate->*Delegate)().IsBound())
{
(GameDelegate->*Delegate)().Broadcast(std::forward<Args>(args)...);
}
}
}
}
UPMS_GameDelegate_WorldSubsystem::BroadcastEvent(GetWorld(), &UPMS_GameDelegate_WorldSubsystem::GetOnJumpStart);
UPMS_GameDelegate_WorldSubsystem::BroadcastEvent(GetWorld(), &UPMS_GameDelegate_WorldSubsystem::GetOnJumpEnd);
然后在TypeScript\HUD\GameInputSystem.ts中
将函数绑定到OnJumpStart中
@D.UEGameDelegateBind("OnJumpStart")
OnJumpStart() {
console.log("OnJumpStart called!");
this.UEWidget.GameButton_Jump.NormalBorder.SetBrush(
UE.WidgetBlueprintLibrary.MakeBrushFromTexture(this.UEWidget.GameButton_Jump.EnableSprite)
);
}
@D.UEGameDelegateBind("OnJumpEnd")
OnJumpEnd() {
this.UEWidget.GameButton_Jump.NormalBorder.SetBrush(
UE.WidgetBlueprintLibrary.MakeBrushFromTexture(this.UEWidget.GameButton_Jump.NormalSprite)
);
}
Sharing is caring!