[CH4-12] 캐릭터 사망 시 관전자 모드 구현 트러블슈팅

김여울·2025년 9월 24일
1

내일배움캠프

목록 보기
84/111

하하하! 하루 아침에 사라졌던 관전자 모드 복구를 결국 해결했다!

📎 [CH4-09] 캐릭터 죽으면 관전자 모드로 변경하기

이건 주말에 만들어놨던 관전자 모드...

🎯 원하는 기능

1. 캐릭터가 죽으면:

  • 메쉬 꺼지고 캡슐 충돌 꺼짐
  • MovementMode = Flying
  • 스페이스바로 상승, WSAD로 자유롭게 날아다니는 구경 모드
  • 카메라는 블루프린트 → C++로 1인칭 전환

2. 살아있을 때:

  • 블루프린트 3인칭 카메라 유지
  • 죽으면 자동으로 1인칭 카메라 전환

🔧 1차 수정: 기본 관전자 모드 구현

Character.h 추가

protected:
    // 관전 모드 입력 핸들러
    void SpectatorMove(const struct FInputActionValue& Value);
    void SpectatorAscend(const struct FInputActionValue& Value);
    
    // 1인칭 카메라 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
    class UCameraComponent* FirstPersonCamera;

Character.cpp 수정

// 생성자에서 1인칭 카메라 생성
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, 64.f));
FirstPersonCamera->bUsePawnControlRotation = true;
FirstPersonCamera->SetActive(false); // 기본으론 꺼두기

// 입력 바인딩
EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ADCCharacter::SpectatorMove);
EIC->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ADCCharacter::SpectatorAscend);

// 관전자 모드 이동
void ADCCharacter::SpectatorMove(const FInputActionValue& Value) {
    if (!bIsDead) return;
    // ... 이동 로직
}

// 죽었을 때 처리
void ADCCharacter::OnRep_IsDead() {
    if (bIsDead) {
        GetMesh()->SetVisibility(false);
        GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
        GetCharacterMovement()->SetMovementMode(MOVE_Flying);
        
        // 1인칭 카메라 전환
        if (APlayerController* PC = Cast<APlayerController>(Controller)) {
            if (FirstPersonCamera) {
                FirstPersonCamera->SetActive(true);
                PC->SetViewTarget(this);
            }
        }
    }
}

⚠️ 문제점 1: 살아있을 때도 1인칭으로 바뀜

해결: else 블록 추가

void ADCCharacter::OnRep_IsDead() {
    if (bIsDead) {
        // 죽었을 때 처리
    } else {
        // 살아있을 때 처리
        GetMesh()->SetVisibility(true);
        GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
        GetCharacterMovement()->SetMovementMode(MOVE_Walking);
        
        // 1인칭 카메라 꺼두기
        if (FirstPersonCamera) {
            FirstPersonCamera->SetActive(false);
        }
    }
}

🔧 2차 수정: 관전자 모드가 사라지는 문제

문제점: 관전자 모드 상태 관리 부족

// Character.h에 상태 변수 추가
protected:
    UPROPERTY(BlueprintReadOnly, Category="Spectator")
    bool bIsSpectatorMode = false;
    
    UPROPERTY(BlueprintReadOnly, Category="Spectator")
    bool bSpectatorFreeMove = false;

관전자 이동 로직 개선

void ADCCharacter::SpectatorMove(const FInputActionValue& Value) {
    if (!bIsDead || !bIsSpectatorMode) return;
    
    const FVector2D Axis = Value.Get<FVector2D>();
    if (Controller && Axis.SizeSquared() > 0.0f) {
        const FRotator ControlRot = Controller->GetControlRotation();
        
        if (bSpectatorFreeMove) {
            // 자유 비행: 3D 이동
            const FVector Forward = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::X);
            const FVector Right = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::Y);
            AddMovementInput(Forward, Axis.Y);
            AddMovementInput(Right, Axis.X);
        } else {
            // 지상 관전: 수평면만
            const FRotator YawOnlyRot = FRotator(0, ControlRot.Yaw, 0);
            const FVector Forward = FRotationMatrix(YawOnlyRot).GetUnitAxis(EAxis::X);
            const FVector Right = FRotationMatrix(YawOnlyRot).GetUnitAxis(EAxis::Y);
            AddMovementInput(Forward, Axis.Y);
            AddMovementInput(Right, Axis.X);
        }
    }
}

⚠️ 문제점 2: 펭귄이 광란의 댄스 + 속도 문제

원인

  1. 속도 문제: 데미지 받으면 OnRep_IsSliding에서 속도가 줄어듦
  2. 캐릭터 위치 문제: 죽으면 땅에 묻히거나 이상한 위치에 나타남

해결책

// OnRep_IsSliding 수정: 죽었을 때는 슬라이딩 처리 안함
void ADCCharacter::OnRep_IsSliding() {
    if (GetCharacterMovement() == nullptr || bIsDead) return; // bIsDead 체크 추가
    
    if (bIsSliding) {
        GetCharacterMovement()->MaxWalkSpeed = SlideSpeed;
    } else {
        GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; // 명시적 복구
    }
}

// OnRep_IsDead에서 위치 조정
void ADCCharacter::OnRep_IsDead() {
    if (bIsDead) {
        // 현재 위치를 위로 올려서 땅에 묻히지 않게 함
        FVector CurrentLocation = GetActorLocation();
        SetActorLocation(CurrentLocation + FVector(0, 0, 100.f));
        
        // 중력 및 관성 제거
        GetCharacterMovement()->GravityScale = 0.0f;
        GetCharacterMovement()->BrakingDecelerationFlying = 2000.0f;
        
        // 회전 제한 해제
        GetCharacterMovement()->bOrientRotationToMovement = false;
    }
}

🔧 3차 수정: 입력 처리 통합

문제점: 중복 입력 바인딩으로 인한 충돌

해결: Move/Look 함수에서 상황별 분기 처리

// SetupPlayerInputComponent: 중복 바인딩 제거
void ADCCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {
    if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
        // 하나의 함수에서 모든 상황 처리
        EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ADCCharacter::Move);
        EIC->BindAction(LookAction, ETriggerEvent::Triggered, this, &ADCCharacter::Look);
        EIC->BindAction(JumpAction, ETriggerEvent::Started, this, &ADCCharacter::HandleJumpInput);
    }
}

// Move 함수에서 통합 처리
void ADCCharacter::Move(const FInputActionValue& Value) {
    const FVector2D Axis = Value.Get<FVector2D>();
    
    // 관전자 모드 처리
    if (bIsDead && bIsSpectatorMode) {
        if (Controller && Axis.SizeSquared() > 0.0f) {
            const FRotator ControlRot = Controller->GetControlRotation();
            
            if (bSpectatorFreeMove) {
                // 자유 비행 모드
                const FVector Forward = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::X);
                const FVector Right = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::Y);
                AddMovementInput(Forward, Axis.Y);
                AddMovementInput(Right, Axis.X);
            } else {
                // 지상 관전 모드
                const FRotator YawOnlyRot = FRotator(0, ControlRot.Yaw, 0);
                const FVector Forward = FRotationMatrix(YawOnlyRot).GetUnitAxis(EAxis::X);
                const FVector Right = FRotationMatrix(YawOnlyRot).GetUnitAxis(EAxis::Y);
                AddMovementInput(Forward, Axis.Y);
                AddMovementInput(Right, Axis.X);
            }
        }
        return;
    }

    // 일반 이동 처리
    if (bIsDead || bIsDancing) return;
    // ... 기존 이동 로직
}

⚠️ 문제점 3: 펭귄 정수리샷 문제 지속

원인: 카메라 위치가 (0, 0, 0)으로 설정되어 캐릭터 중심에서 머리 위를 바라보게 됨

해결책: 카메라 위치 조정

// OnRep_IsDead에서 카메라 위치 수정
void ADCCharacter::OnRep_IsDead() {
    if (bIsDead) {
        // ... 기존 코드
        
        // === 카메라 전환 ===
        if (APlayerController* PC = Cast<APlayerController>(Controller)) {
            if (FirstPersonCamera) {
                FirstPersonCamera->SetActive(true);
                PC->SetViewTarget(this);
                // 카메라를 캐릭터 중심이 아닌 눈 높이로
                FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, 64.f)); // 원래대로
            }
        }
    } else {
        // 부활 시에도 카메라 위치 복구
        if (FirstPersonCamera) {
            FirstPersonCamera->SetActive(false);
            FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, 64.f));
        }
    }
}

죽은 펭귄 시점 → 1인칭 제대로 작용
생존한 펭귄 시점 → 생존자 수 제대로 반영되고 죽은 펭귄 메시 안 보임


⚠️ 문제점 4: 스페이스바로 상승이 안됨

원인: ETriggerEvent::Started만 사용해서 지속적 입력 처리가 안됨

해결: Started/Triggered/Completed 이벤트 분리

// 헤더에 추가
void HandleJumpToggle(const FInputActionValue& Value);   // Started용
void HandleJumpTriggered(const FInputActionValue& Value); // Triggered용  
void HandleJumpStop(const FInputActionValue& Value);     // Completed용

// 입력 바인딩
void ADCCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {
    if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
        EIC->BindAction(JumpAction, ETriggerEvent::Started, this, &ADCCharacter::HandleJumpToggle);
        EIC->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ADCCharacter::HandleJumpTriggered);
        EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &ADCCharacter::HandleJumpStop);
    }
}

// 구현
void ADCCharacter::HandleJumpToggle(const FInputActionValue& Value) {
    if (bIsDead && bIsSpectatorMode && !bSpectatorFreeMove) {
        // 자유 비행 모드 토글 (한 번만)
        SpectatorToggleFreeMove(Value);
    }
}

void ADCCharacter::HandleJumpTriggered(const FInputActionValue& Value) {
    if (bIsDead && bIsSpectatorMode && bSpectatorFreeMove) {
        // 자유 비행 모드에서 지속적 상승
        SpectatorAscend(Value);
    } else if (!bIsDead && !GetCharacterMovement()->IsFalling()) {
        // 살아있을 때는 일반 점프
        Jump();
    }
}

void ADCCharacter::HandleJumpStop(const FInputActionValue& Value) {
    if (!bIsDead) {
        StopJumping();
    }
}

void ADCCharacter::SpectatorAscend(const FInputActionValue& Value) {
    if (!bIsDead || !bIsSpectatorMode || !bSpectatorFreeMove) return;
    AddMovementInput(FVector::UpVector, 1.0f);
}

짜잔~ 이제 스페이스 꾹 누르면 쭉~ 상승함!

✅ 최종 동작 방식

  1. 캐릭터 사망 → 관전자 모드 활성화 (bSpectatorFreeMove = false)
  2. 스페이스바 누름 (Started) → HandleJumpToggle → 자유 비행 모드 ON
  3. 스페이스바 홀드 (Triggered) → HandleJumpTriggered → 지속적 상승 ✅
    4.스페이스바 뗌 (Completed) → HandleJumpStop → 점프 정지

💡 기억해라!

  1. Enhanced Input 이벤트 타입의 중요성
  • Started: 키를 누르는 순간 (한 번만)
  • Triggered: 키를 누르고 있는 동안 (지속적)
  • Completed: 키를 떼는 순간 (한 번만)
  1. 중복 입력 바인딩 문제
  • 같은 Action에 여러 함수를 바인딩하면 모두 실행됨
  • 조건부 분기로 하나의 함수에서 처리하는 것이 안전
  1. 네트워크 리플리케이션 주의사항
  • OnRep 함수에서 상태 변경 시 다른 OnRep 함수에 영향을 줄 수 있음
  • 죽은 상태에서는 다른 상태 변경을 무시하도록 조건 체크 필요
  1. Movement Component 설정
  • 관전자 모드에서는 중력, 관성, 회전 제한 등을 적절히 조정해야 함
  • GravityScale = 0, BrakingDecelerationFlying 등의 세밀한 튜닝 필요

관전자 모드 필요한 기능 전부 구현 완료!

2개의 댓글

comment-user-thumbnail
2025년 9월 25일

너무 좋은글이네요 ^^

1개의 답글