일단 야구공처럼 날아가게 변경 + 조준점만 표시하게 변경
UpdateTrajectory에서 선 그리는 루프 지우기
void UFDItemInventoryComponent::UpdateTrajectory()
{
FVector StartLoc;
FVector LaunchVelocity;
GetThrowStartAndVelocity(StartLoc, LaunchVelocity);
if (LaunchVelocity.IsNearlyZero()) return;
FPredictProjectilePathParams PathParams;
PathParams.StartLocation = StartLoc;
PathParams.LaunchVelocity = LaunchVelocity;
PathParams.ProjectileRadius = 20.f;
PathParams.OverrideGravityZ = GetWorld()->GetGravityZ() * ThrowGravityScale;
PathParams.bTraceWithCollision = true;
PathParams.TraceChannel = ECC_Visibility;
PathParams.MaxSimTime = 3.f;
PathParams.SimFrequency = 15.f;
PathParams.DrawDebugType = EDrawDebugTrace::None;
PathParams.ActorsToIgnore.Add(GetOwner());
FPredictProjectilePathResult PathResult;
const bool bHit = UGameplayStatics::PredictProjectilePath(this, PathParams, PathResult);
// 착지 지점만 표시
if (bHit)
{
DrawDebugSphere(
GetWorld(),
PathResult.HitResult.Location,
30.f, // 반지름
16, // 세그먼트
FColor::Cyan,
false,
-1.f); // 매 프레임 다시 그림
}
}
값 변경
1) BP_FDThrowItem -> ProjectileMovement 컴포넌트
- Initial Speed: 2500
- Max Speed: 2500
- Projectile Gravity Scale: 0.5
2) BP_FDHiderCharacter -> ItemInventoryComp
- Throw Speed: 2500
- Throw Gravity Scale: 0.5
술래 스턴 효과 만들기
AFDThrowItem::OnHit 호출
술래를 StunDuration(5초)간 이동 불가로
5초 뒤 자동 해제
투사체는 맞자마자 사라짐 -> 타이머를 투사체에 걸면 안 됨
이동만 막는 건 스턴이 아님 -> 포획도 같이 막아야 함
1. 타이머
투사체(AFDThrowItem)는 Hit 즉시 Destroy()
-> 투사체에 5초 타이머를 걸면 액터가 사라지면서 타이머도 같이 사라짐 -> 해제 함수가 안 불림 -> 술래가 계속 못 움직임
지속되는 상태는 그 상태를 겪는 액터가 관리해야 함
AFDHiderCharacter::SetInvincible이 무적 상태와 타이머를 자기가 들고 있는 것과 같은 구조
-> 스턴 로직 + 타이머를 AFDTaggerCharacter에 둠 -> 투사체는 ApplyStun(StunDuration) 한 번 부르고 사라지기만 함
2. 이동과 포획 둘다 막기
Client_LockMovement() = SetIgnoreMoveInput(true)
-> WASD만 막힘 -> 마우스 좌클릭(포획)은 그대로 먹힘 -> 술래가 제자리에서 계속 하이더 잡을 수 있음 = 스턴 의미 없음
포획도 막아야 함
- 입력 단계(Input_Attack)에서 막기 -> 클라이언트 조작으로 우회 가능
- 서버 판정(Server_TryCapture)에서 막기 <- 이걸로
3. 중복 스턴은 무시
하이더 여럿이 연속으로 던지면 술래가 15초, 20초 계속 묶임
-> 이미 스턴 중이면 무시 (연장 없음)
FDTaggerCharacter.h
public:
// 투사체에 맞았을 때 스턴 적용 (서버 전용)
void ApplyStun(float Duration);
// 현재 스턴 상태인지 (복제되므로 클라/서버 모두 사용 가능)
bool IsStunned() const { return bIsStunned; }
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
private:
// 스턴 상태 - 지금은 이펙트 없지만 나중에 연출 위해 복제해둠
UPROPERTY(ReplicatedUsing = OnRep_bIsStunned)
bool bIsStunned = false;
UFUNCTION()
void OnRep_bIsStunned();
// 스턴 해제 타이머
FTimerHandle StunExpireTimerHandle;
void OnStunExpired();
FDTaggerCharacter.cpp
#include "Net/UnrealNetwork.h"
void AFDTaggerCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AFDTaggerCharacter, bIsStunned);
}
void AFDTaggerCharacter::ApplyStun(float Duration)
{
if (!HasAuthority()) return;
// 이미 스턴 중이면 무시 (연장 안 됨)
if (bIsStunned)
{
UE_LOG(LogTemp, Log, TEXT("[술래] 이미 스턴 중 - 중복 스턴 무시"));
return;
}
bIsStunned = true;
OnRep_bIsStunned(); // 서버 수동 호출
// 이동 입력 차단
if (AFDPlayerController* FDPC = Cast<AFDPlayerController>(GetController()))
{
FDPC->Client_LockMovement();
}
GetWorldTimerManager().SetTimer(
StunExpireTimerHandle,
this,
&AFDTaggerCharacter::OnStunExpired,
Duration,
false);
UE_LOG(LogTemp, Warning, TEXT("[술래] 스턴 적용 - %.1f초"), Duration);
}
void AFDTaggerCharacter::OnStunExpired()
{
if (!HasAuthority()) return;
bIsStunned = false;
OnRep_bIsStunned();
// 이동 입력 복구
if (AFDPlayerController* FDPC = Cast<AFDPlayerController>(GetController()))
{
FDPC->Client_UnlockMovement();
}
GetWorldTimerManager().ClearTimer(StunExpireTimerHandle);
UE_LOG(LogTemp, Warning, TEXT("[술래] 스턴 해제"));
}
void AFDTaggerCharacter::OnRep_bIsStunned()
{
// TODO: 스턴 이펙트(별 빙빙, 머티리얼 변화 등) 여기에 추가
}
포획 차단 - Server_TryCapture 수정
void AFDTaggerCharacter::Server_TryCapture_Implementation()
{
// 스턴 중엔 포획 불가
// 이동만 막으면 제자리에서 계속 잡을 수 있어서 스턴 의미가 없어짐
if (bIsStunned)
{
UE_LOG(LogTemp, Log, TEXT("[술래] 스턴 중 - 포획 시도 무시"));
return;
}
CaptureCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
FTimerHandle TempHandle;
GetWorldTimerManager().SetTimer(TempHandle, [this]()
{
CaptureCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}, 0.2f, false);
}
FDThrowItem.cpp - OnHit
void AFDThrowItem::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
AFDTaggerCharacter* Tagger = Cast<AFDTaggerCharacter>(OtherActor);
if (Tagger)
{
// 스턴 적용은 술래 자신이 관리 (타이머 포함)
// 투사체는 바로 Destroy되니까 타이머를 여기서 걸면 같이 사라져버림
Tagger->ApplyStun(StunDuration);
UE_LOG(LogTemp, Warning, TEXT("[투사체] 술래 명중 - 스턴 %.1f초"), StunDuration);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[투사체] 지형/기타 대상에 맞음"));
}
Destroy();
}
알게 된 것
"이 로직을 어디에 둘까"는 취향이 아니라 생명주기 문제
투사체는 즉시 사라지는 일회용 액터 -> 5초짜리 상태를 관리할 자격이 없음 상태를 겪는 쪽이 상태를 소유해야 함
캡슐화가 실제로 값어치를 발휘한 순간
포획으로 들어가는 통로가 Server_TryCapture 하나뿐이라 if (bIsStunned) return; 한 줄이면 끝 포획 로직이 여러 군데 흩어져 있었다면 전부 찾아서 막아야 했을 것
만들어둔 스텁이 쓰인 순간
Client_UnlockMovement는 "아직 쓰이는 곳은 없음"이라는 주석과 함께 만들어뒀던 함수 대칭성 지키려고 짝으로 만들어둔 게 이번에 실제로 쓰임
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| 조준 표시 개선 (디버그 구체 -> 데칼 + 크로스헤어) (0) | 2026.07.13 |
|---|---|
| 소리 아이템 구현 (0) | 2026.07.13 |
| 투사체 조준 및 발사 (X키 조준 -> 좌클릭 발사) (0) | 2026.07.13 |
| 투사체 생성 (0) | 2026.07.10 |
| 투명화 아이템 사용 기능 구현 (Z키) (0) | 2026.07.10 |
