
처음에는:
“어차피 애님 몽타주는 모든 클라이언트에서 재생되니까,
그냥 Notify에서 각자 파티클 재생하면 되는 거 아닌가?”
라는 생각도 했다.
하지만 이 방식은 문제가 있었다.
특히 중간에 접속한 플레이어는 이미 Notify 시점이 지나버렸다면 폭발 이펙트를 못 볼 수도 있었다.
반면 GameplayCue는:
하다는 장점이 있었다.
결국:
“폭발 여부는 서버가 결정하고,
연출은 GameplayCue로 동기화한다”
는 방향이 더 맞다고 판단했다.


GameplayCueNotify_Static_Burst 기반 BP 생성구조로 정리했다.
추가로 GameplayCue Tag는 반드시:
GameplayCue.*
형태로 시작해야 정상 인식되기 때문에,
Cue와 Notify 모두 동일 태그를 사용하도록 구성했다.
void UAnimNotify_Boom::Notify(/**/)
{
FGameplayCueParameters Params;
Params.Instigator = BaseEnemyCharacter;
Params.Location = MeshComp->GetSocketLocation(SocketName);
Params.Normal = MeshComp->GetSocketRotation(SocketName).Vector();
ASC->ExecuteGameplayCue(GameplayCueBoomTag, Params);
}

적을 대량으로 스폰하다 보니:
문제가 발생했다.
기존에는 단순히 Z값을 위로 올려 스폰했지만, 지형이 복잡해지면서 한계가 있었다.
그래서 스폰 위치를 결정할 때 실제 지형을 먼저 검사하도록 수정했다.
우선 랜덤 XY 위치를 정한 뒤, 위쪽에서 아래로 LineTrace를 발사해 실제 바닥 위치를 찾도록 구성했다.
다만 이 경우 아파트 옥상 같이 높은 곳도 감지될 수 있어서 Trace 결과의 Z값을 Core 기준 높이와 비교해 너무 높은 위치는 제외하도록 처리했다.
이후 실제 스폰 가능 여부는 Capsule 충돌 검사를 사용해
검사했고, Hit이 발생하지 않을 때만 스폰하도록 구성했다.
즉:
역할까지 함께 담당한다.
결과적으로:
단순 랜덤 스폰이 아니라,
실제 지형과 충돌까지 고려한 안전한 스폰 구조
로 개선할 수 있었다.
void USpawnManagerSubsystem::SpawnEnemies()
{
for (int32 j = 0; j < Count; j++)
{
if (CanSpawnEnemy(SpawnLocation))
{
ABaseEnemyCharacter* Enemy = GetWorld()->SpawnActor<ABaseEnemyCharacter>(
EnemyClass,
SpawnLocation,
FRotator::ZeroRotator,
SpawnParams
);
// ... //
}
bool USpawnManagerSubsystem::CanSpawnEnemy(FVector& InSpawnLocation)
{
FHitResult HitResult;
FCollisionQueryParams QueryParams;
bool bHit = World->LineTraceSingleByChannel(HitResult,
TraceStart,
TraceEnd,
ECC_WorldStatic,
QueryParams);
if (bHit)
{
FVector FinalSpawnLocation = HitResult.ImpactPoint;
if (FinalSpawnLocation.Z > TargetCore->GetActorLocation().Z + 100)
{
return false;
}
FCollisionShape Capsule = FCollisionShape::MakeCapsule(CapsuleRadius, CapsuleHalfHeight);
FHitResult SweepHit;
bool bIsBlocked = World->SweepSingleByChannel(
SweepHit,
FinalSpawnLocation + FVector(0, 0, CapsuleHalfHeight + 50),
FinalSpawnLocation + FVector(0, 0, CapsuleHalfHeight + 50),
FQuat::Identity,
ECC_Pawn,
Capsule
);
if (!bIsBlocked)
{
InSpawnLocation = FinalSpawnLocation + FVector(0, 0, CapsuleHalfHeight);
return true;
}
else
{
return false;
}
}
return false;
}