궤적과 실제 발사가 같은 값을 써야 함 안 그러면 보이는 대로 안 날아감

발사 속도 나중에 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)

+ Recent posts