궤적과 실제 발사가 같은 값을 써야 함 안 그러면 보이는 대로 안 날아감
발사 속도 나중에 DT로 뺄 수 있으면 빼기 (지금은 인벤토리/투사체 양쪽에 하드코딩)
1. 조준 상태 관리 (로컬 전용)
궤적은 나만 보는 보조선 -> 복제 안 함 매 프레임 갱신되는 걸 복제하면 네트워크 낭비
FDItemInventoryComponent.h
public:
// 조준 토글 (X키) - 로컬 전용
void ToggleAiming();
// 조준 중인지 (로컬 판단용)
bool IsAiming() const { return bIsAiming; }
// 조준 중 좌클릭 → 실제 발사
void FireThrowItem();
// 매 프레임 궤적 갱신용 (조준 중일 때만 Tick이 켜짐)
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
protected:
// 투사체 발사 속도 - AFDThrowItem의 InitialSpeed와 반드시 같아야 함
UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
float ThrowSpeed = 1200.f;
UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
float ThrowGravityScale = 1.f;
UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
float ThrowForwardOffset = 100.f;
UPROPERTY(EditDefaultsOnly, Category = "Item|Throw")
float ThrowUpOffset = 50.f;
private:
// 조준 상태 - 복제 안 함
bool bIsAiming = false;
void UpdateTrajectory();
void StartAiming();
void StopAiming();
// 발사 시작 위치/방향 계산 - 궤적 표시와 실제 발사가 같은 값을 쓰도록 공용화
void GetThrowStartAndVelocity(FVector& OutStart, FVector& OutVelocity) const;
FDItemInventoryComponent.cpp
UFDItemInventoryComponent::UFDItemInventoryComponent()
{
// Tick 자격은 열어두되, 실제로는 조준 중에만 켜짐
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
SetIsReplicatedByDefault(true);
}
void UFDItemInventoryComponent::ToggleAiming()
{
if (bIsAiming)
{
StopAiming();
return;
}
// 투사체가 없으면 조준 진입 불가
if (ThrowItemCount <= 0) return;
StartAiming();
}
void UFDItemInventoryComponent::StartAiming()
{
bIsAiming = true;
SetComponentTickEnabled(true); // 궤적 매 프레임 갱신 시작
}
void UFDItemInventoryComponent::StopAiming()
{
bIsAiming = false;
SetComponentTickEnabled(false); // 조준 안 할 땐 궤적 계산 필요 없음
}
void UFDItemInventoryComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bIsAiming) return;
UpdateTrajectory();
}
void UFDItemInventoryComponent::FireThrowItem()
{
if (!bIsAiming) return;
StopAiming(); // 조준 해제 먼저 (궤적 선 즉시 사라짐)
// 서버에 발사 요청 (개수 확인/소모/스폰은 서버가)
Server_UseItem(EFDItemEffect::TaggerStun);
}
2. 궤적 계산 + 표시
발사 시작점·속도를 한 함수로 공용화 -> 궤적과 실제 발사가 같은 값을 쓰도록 강제
void UFDItemInventoryComponent::GetThrowStartAndVelocity(FVector& OutStart, FVector& OutVelocity) const
{
OutStart = FVector::ZeroVector;
OutVelocity = FVector::ZeroVector;
const AFDHiderCharacter* Hider = Cast<AFDHiderCharacter>(GetOwner());
if (!Hider) return;
// 하이더가 보는 방향 (카메라 회전)
const FRotator ControlRot = Hider->GetControlRotation();
const FVector ThrowDir = ControlRot.Vector(); // 회전값 → 방향 벡터
// 발사 시작점 - 하이더 앞쪽 위 (자기 자신과 안 부딪히게)
OutStart = Hider->GetActorLocation()
+ ThrowDir * ThrowForwardOffset
+ FVector(0.f, 0.f, ThrowUpOffset);
// 발사 속도 벡터 = 방향 × 속력
OutVelocity = ThrowDir * ThrowSpeed;
}
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 = 0.f; // 0이면 월드 기본 중력
PathParams.bTraceWithCollision = true; // 벽에 부딪히면 거기서 궤적 종료
PathParams.TraceChannel = ECC_Visibility;
PathParams.MaxSimTime = 3.f;
PathParams.SimFrequency = 15.f; // 초당 15개 점
PathParams.DrawDebugType = EDrawDebugTrace::None;
PathParams.ActorsToIgnore.Add(GetOwner()); // 자기 자신 제외
FPredictProjectilePathResult PathResult;
const bool bHit = UGameplayStatics::PredictProjectilePath(this, PathParams, PathResult);
// 경로 점들을 디버그 선으로 이어서 그리기
const TArray<FPredictProjectilePathPointData>& Points = PathResult.PathData;
for (int32 i = 0; i < Points.Num() - 1; ++i)
{
DrawDebugLine(GetWorld(), Points[i].Location, Points[i + 1].Location,
FColor::Cyan, false, -1.f, 0, 3.f);
}
// 착지 지점 표시
if (bHit)
{
DrawDebugSphere(GetWorld(), PathResult.HitResult.Location, 20.f, 12, FColor::Red, false, -1.f);
}
}
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
3. 발사 (ExecuteEffect 수정)
기존 코드는 스폰 위치를 따로 계산했음 -> 궤적과 어긋나는 버그
- 궤적: GetControlRotation() (카메라 방향, 상하 각도 포함)
- 스폰: GetActorForwardVector() (캐릭터 수평 전방)
위를 보고 조준하면 궤적은 위로 향하는데 실제는 수평 앞에서 나감 -> GetThrowStartAndVelocity()로 통일
case EFDItemEffect::TaggerStun:
{
if (!Data.ThrowItemClass) break;
// 궤적 표시와 정확히 같은 계산 사용
FVector StartLoc;
FVector LaunchVelocity;
GetThrowStartAndVelocity(StartLoc, LaunchVelocity);
if (LaunchVelocity.IsNearlyZero()) break;
// 속도 벡터 → 회전값 변환
const FRotator SpawnRot = LaunchVelocity.Rotation();
FActorSpawnParameters Params;
Params.Instigator = Hider;
Params.Owner = Hider;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AFDThrowItem* Throw = GetWorld()->SpawnActor<AFDThrowItem>(
Data.ThrowItemClass, StartLoc, SpawnRot, Params);
if (Throw)
{
Throw->SetStunDuration(Data.Duration);
// 궤적 계산에 쓴 것과 동일한 속도 벡터를 실제 투사체에 적용
Throw->LaunchWith(LaunchVelocity);
}
break;
}
FDThrowItem에 속도 주입 함수 추가
// FDThrowItem.h
public:
void LaunchWith(const FVector& LaunchVelocity);
// FDThrowItem.cpp
void AFDThrowItem::LaunchWith(const FVector& LaunchVelocity)
{
if (!ProjectileMovement) return;
// 생성자의 InitialSpeed 대신, 인벤토리가 계산한 속도 벡터를 직접 적용
// 이렇게 해야 조준 궤적과 실제 경로가 정확히 일치함
ProjectileMovement->Velocity = LaunchVelocity;
}
4. 입력 변경
X키: 즉시 발사 -> 조준 토글
void AFDPlayerController::Input_UseThrowItem(const FInputActionValue& Value)
{
AFDHiderCharacter* Hider = Cast<AFDHiderCharacter>(GetPawn());
if (!Hider) return;
UFDItemInventoryComponent* Inventory =
Hider->FindComponentByClass<UFDItemInventoryComponent>();
if (!Inventory) return;
Inventory->ToggleAiming(); // Server_UseItem에서 변경
}
좌클릭: 술래는 포획, 하이더는 조준 중이면 발사
void AFDPlayerController::Input_Attack(const FInputActionValue& Value)
{
// 술래: 좌클릭 → 포획 시도
if (AFDTaggerCharacter* Tagger = Cast<AFDTaggerCharacter>(GetPawn()))
{
Tagger->Server_TryCapture();
return;
}
// 하이더: 조준 중이면 좌클릭 → 투사체 발사
if (AFDHiderCharacter* Hider = Cast<AFDHiderCharacter>(GetPawn()))
{
UFDItemInventoryComponent* Inventory =
Hider->FindComponentByClass<UFDItemInventoryComponent>();
if (!Inventory) return;
if (!Inventory->IsAiming()) return;
Inventory->FireThrowItem();
}
}
에디터 작업
BP_FDHiderCharacter의 ItemInventoryComp -> Details -> Item | Throw
- Throw Speed = 1200 : BP_FDThrowItem의 InitialSpeed와 같아야 함
- Throw Gravity Scale = 1.0 : ProjectileGravityScale과 같아야 함
두 값이 다르면 궤적이 어긋남
트러블슈팅
1) 좌클릭해도 발사 안 됨

중단점 걸어보면서 발견한 사실
하이더일 때 if (!Tagger) return; 에서 걸려서 함수 나가져버림
하이더는 Tagger가 nullptr이라 여기서 return -> 아래 하이더 코드는 실행조차 안 됨
early return으로 조용히 이탈되는 거였음
-> 코드 수정
2) X키 눌러도 로그 안 뜸
ToggleAiming은 로컬 전용 함수 -> 클라이언트 창 로그에만 찍힘 서버 로그만 보고 있어서 아무것도 안 뜨는 줄 알았음
서버 RPC와 로컬 함수가 섞여 있으면 양쪽 로그를 다 봐야 함
알게 된 것
UGameplayStatics::PredictProjectilePath (엔진 제공)
- 시작 위치, 속도 벡터, 중력을 받아 실제로 날리지 않고 예상 경로 점들을 반환
- bTraceWithCollision = true면 벽/바닥에 부딪히는 지점에서 궤적이 정확히 멈춤 -> 착지 지점까지 미리 보여줄 수 있음
bCanEverTick vs SetComponentTickEnabled
- bCanEverTick = Tick할 자격이 있는가 (생성자에서 한 번)
- SetComponentTickEnabled = 지금 실제로 Tick할 것인가 (런타임 토글)
- bCanEverTick = false면 SetComponentTickEnabled(true) 불러도 소용없음
FRotator::Vector() (엔진 제공)
- 회전값(Pitch/Yaw/Roll) -> 그 방향을 가리키는 단위 벡터
- "위 45도 앞" -> (0.7, 0, 0.7)
FVector::Rotation() (엔진 제공)
- 위의 반대. 방향 벡터 -> 회전값(FRotator)
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| 소리 아이템 구현 (0) | 2026.07.13 |
|---|---|
| 투사체 명중 시 술래 스턴 (0) | 2026.07.13 |
| 투사체 생성 (0) | 2026.07.10 |
| 투명화 아이템 사용 기능 구현 (Z키) (0) | 2026.07.10 |
| 인벤토리 개편 (0) | 2026.07.10 |
