KLingo Project Documentation 1.0.0
Unreal Engine 5.6 C++ Project Documentation
로딩중...
검색중...
일치하는것 없음
InteractableComponent.cpp
이 파일의 문서화 페이지로 가기
1// Copyright (c) 2025 Doppleddiggong. All rights reserved. Unauthorized copying, modification, or distribution of this file, via any medium is strictly prohibited. Proprietary and confidential.
2
3
5
6#include "APlayerActor.h"
7#include "GameLogging.h"
8#include "luggage.h"
9#include "TutorialComponent.h"
10#include "UInteractWidget.h"
11#include "Net/UnrealNetwork.h"
12#include "Components/BoxComponent.h"
13#include "Components/WidgetComponent.h"
14#include "UInteractionSystem.h"
15#include "GameFramework/Character.h"
16#include "Kismet/GameplayStatics.h"
17#include "Kismet/KismetMathLibrary.h"
18
19
20UInteractableComponent::UInteractableComponent()
21{
22 PrimaryComponentTick.bCanEverTick = true;
23
24 bIsPickedUp = false;
25 bOriginalSimulatePhysics = false;
26 OriginalCollisionType = ECollisionEnabled::NoCollision;
27
28 // 생성자에서 생성해서 자동 복제
29 DetectionRange = CreateDefaultSubobject<UBoxComponent>(TEXT("DetectionRange"));
30 if (DetectionRange)
31 {
32 DetectionRange->SetBoxExtent(FVector(250.f)); // DetectionDistance 기본값
33 DetectionRange->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
34 DetectionRange->SetCollisionResponseToAllChannels(ECR_Ignore);
35 DetectionRange->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
36 }
37
38 SetIsReplicatedByDefault(true);
39}
40
41void UInteractableComponent::BeginPlay()
42{
43 Super::BeginPlay();
44
45 AActor* Owner = GetOwner();
46 if (!Owner)
47 {
48 PRINTLOG( TEXT("UInteractableComponent::BeginPlay - GetOwner() returned nullptr!"));
49 return;
50 }
51
52 this->InitDetectionRange();
53}
54
55void UInteractableComponent::InitDetectionRange()
56{
57 // 1. 컴포넌트 등록
58 if (!DetectionRange->IsRegistered())
59 {
60 DetectionRange->RegisterComponent();
61 }
62
63 AActor* Owner = GetOwner();
64
65 // 2. Owner의 RootComponent에 부착
66 if (!DetectionRange->IsAttachedTo(Owner->GetRootComponent()))
67 {
68 DetectionRange->AttachToComponent(
69 Owner->GetRootComponent(),
70 FAttachmentTransformRules::KeepRelativeTransform
71 );
72 }
73
74 // BoxExtent 설정 업데이트
75 FVector BoxExtent(DetectionDistance, DetectionDistance, DetectionDistance);
76 DetectionRange->SetBoxExtent(BoxExtent);
77
78 // Overlap 콜백 바인딩
79 DetectionRange->OnComponentBeginOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionBeginOverlap);
80 DetectionRange->OnComponentEndOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionEndOverlap);
81}
82
83void UInteractableComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
84{
85 Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
86
87 // 상호작용 위젯 빌보드화
88 BillboardInteractWidget();
89
90 // 디버그 표시
91 if (bShowDetectionDebug && DetectionRange)
92 {
93 AActor* Owner = GetOwner();
94 if (Owner && IsValid(Owner))
95 {
96 FVector Center = Owner->GetActorLocation();
97 FVector BoxExtent = DetectionRange->GetScaledBoxExtent();
98 FQuat Rotation = Owner->GetActorQuat();
99
100 // DetectionRange 박스 그리기
101 DrawDebugBox(
102 GetWorld(),
103 Center,
104 BoxExtent,
105 Rotation,
106 bCanInteract ? FColor::Green : FColor::Yellow,
107 false,
108 0.0f,
109 0,
110 2.0f
111 );
112
113 // 거리 텍스트 표시
114 FString DebugText = FString::Printf(
115 TEXT("Detection: %.0f cm\n%s"),
116 DetectionDistance,
117 *InteractionPrompt
118 );
119
120 DrawDebugString(
121 GetWorld(),
122 Center + FVector(0, 0, BoxExtent.Z + 20.f),
123 DebugText,
124 nullptr,
125 FColor::White,
126 0.0f,
127 true,
128 1.0f
129 );
130 }
131 }
132}
133
134void UInteractableComponent::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
135{
136 Super::GetLifetimeReplicatedProps(OutLifetimeProps);
137
138 DOREPLIFETIME(UInteractableComponent, HoldingOwner);
139
144 DOREPLIFETIME(UInteractableComponent, bIsPickedUp);
145}
146
147void UInteractableComponent::OnRep_HoldingOwner()
148{
149 if (HoldingOwner)
150 {
151 APlayerActor* MyPlayer = Cast<APlayerActor>(HoldingOwner);
152 if (MyPlayer)
153 {
154 // 여기서 GetOwner() == 컴포넌트가 붙어있는 액터
155 GetOwner()->AttachToComponent(MyPlayer->HoldPosition, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
156
157 // PickUp 튜토리얼
158 if (GetOwner()->IsA(Aluggage::StaticClass()))
159 {
160 APlayerController* PC = nullptr;
161
162 // NewHoldingOwner가 Pawn인 경우 Controller를 가져옴
163 if (APawn* Pawn = Cast<APawn>(HoldingOwner))
164 {
165 PC = Cast<APlayerController>(Pawn->GetController());
166 }
167 // 혹시 직접 PlayerController인 경우
168 else
169 {
170 PC = Cast<APlayerController>(HoldingOwner);
171 }
172
173 if (PC)
174 {
175 if (UTutorialComponent* Tutorial = PC->FindComponentByClass<UTutorialComponent>())
176 {
177 Tutorial->OnObjectPickedUp(GetOwner());
178 }
179 }
180 }
181 }
182 }
183 else
184 {
185 GetOwner()->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
186 }
187}
188
195void UInteractableComponent::OnRep_IsPickedUp()
196{
197 if (bIsPickedUp)
198 {
199 // 픽업 상태 - Outline/Widget 끄기
200 SetOutlineState(false);
201 HideInteractWidget();
202
203 PRINTLOG(TEXT("OnRep_IsPickedUp: %s picked up (visual update)"), *GetOwner()->GetName());
204 }
205 else
206 {
207 // 드롭 상태 - Outline 켜기 (Widget은 거리에 따라 OnDetectionOverlap에서 처리)
208 SetOutlineState(true);
209
210 PRINTLOG(TEXT("OnRep_IsPickedUp: %s dropped (visual update)"), *GetOwner()->GetName());
211 }
212}
213
214void UInteractableComponent::PickUp(AActor* NewHoldingOwner)
215{
216 // 클라이언트 측 기본 검증 (빠른 피드백용)
217 if (!NewHoldingOwner)
218 {
219 PRINTLOG(TEXT("InteractableComponent::PickUp - NewHoldingOwner is null"));
220 return;
221 }
222
223 if (bIsPickedUp)
224 {
225 PRINTLOG(TEXT("InteractableComponent::PickUp - Already picked up"));
226 return;
227 }
228
229 // 서버 RPC 호출 (서버에서 재검증)
230 Server_PickUp(NewHoldingOwner);
231}
232
246bool UInteractableComponent::Server_PickUp_Validate(AActor* NewHoldingOwner)
247{
248 // 1. NewHoldingOwner가 유효한지
249 if (!NewHoldingOwner)
250 {
251 PRINTLOG(TEXT("Server_PickUp_Validate: NewHoldingOwner is null"));
252 return false;
253 }
254
255 // 2. NewHoldingOwner가 실제 PlayerController에 의해 제어되는지
256 APlayerActor* PlayerActor = Cast<APlayerActor>(NewHoldingOwner);
257 if (!PlayerActor || !PlayerActor->GetController())
258 {
259 PRINTLOG(TEXT("Server_PickUp_Validate: Not a valid PlayerActor or no Controller"));
260 return false;
261 }
262
263 // 3. 이미 픽업된 상태인지
264 if (bIsPickedUp)
265 {
266 PRINTLOG(TEXT("Server_PickUp_Validate: Already picked up"));
267 return false;
268 }
269
270 // 4. HoldingOwner가 이미 있는지 (중복 소유 방지)
271 if (HoldingOwner && HoldingOwner != NewHoldingOwner)
272 {
273 PRINTLOG(TEXT("Server_PickUp_Validate: Already held by %s"), *HoldingOwner->GetName());
274 return false;
275 }
276
277 // 5. 거리 검증 (서버 기준)
278 AActor* Owner = GetOwner();
279 if (!Owner)
280 {
281 PRINTLOG(TEXT("Server_PickUp_Validate: Owner is null"));
282 return false;
283 }
284
285 float Distance = FVector::Dist(Owner->GetActorLocation(), NewHoldingOwner->GetActorLocation());
286 float MaxDistance = DetectionDistance * 1.5f; // 네트워크 지연 고려 1.5배 여유
287
288 if (Distance > MaxDistance)
289 {
290 PRINTLOG(TEXT("Server_PickUp_Validate: Too far! Distance: %.2f, Max: %.2f"), Distance, MaxDistance);
291 return false;
292 }
293
294 return true;
295}
296
301void UInteractableComponent::Server_PickUp_Implementation(AActor* NewHoldingOwner)
302{
303 // Validation 통과 후 실행
304 HoldingOwner = NewHoldingOwner;
305
306 // Owner actor의 PrimitiveComponent 찾기
307 UPrimitiveComponent* PrimComp = GetOwnerPrimitiveComponent();
308 if (!PrimComp)
309 return;
310
311 // 원래 상태 저장 (드롭 시 복원용)
312 bOriginalSimulatePhysics = PrimComp->IsSimulatingPhysics();
313 OriginalCollisionType = PrimComp->GetCollisionEnabled();
314
315 // 물리 끄기
316 PrimComp->SetSimulatePhysics(false);
317
318 // Attach (OnRep_HoldingOwner에서 처리)
319 OnRep_HoldingOwner();
320
321 // 상태 변경 (복제됨 → OnRep_IsPickedUp 자동 호출)
322 bIsPickedUp = true;
323
324 PRINTLOG(TEXT("Server_PickUp: %s picked up by %s"), *GetOwner()->GetName(), *NewHoldingOwner->GetName());
325}
326
327void UInteractableComponent::Drop()
328{
329 // 클라이언트 측 기본 검증 (빠른 피드백용)
330 if (!bIsPickedUp)
331 {
332 PRINTLOG(TEXT("InteractableComponent::Drop - Not picked up"));
333 return;
334 }
335
336 // 서버 RPC 호출
337 Server_Drop();
338}
339
345bool UInteractableComponent::Server_Drop_Validate()
346{
347 // 1. 실제로 픽업된 상태인지
348 if (!bIsPickedUp)
349 {
350 PRINTLOG(TEXT("Server_Drop_Validate: Not picked up"));
351 return false;
352 }
353
354 // 2. HoldingOwner가 유효한지
355 if (!HoldingOwner)
356 {
357 PRINTLOG(TEXT("Server_Drop_Validate: HoldingOwner is null"));
358 return false;
359 }
360
361 return true;
362}
363
368void UInteractableComponent::Server_Drop_Implementation()
369{
370 // Detach (OnRep_HoldingOwner에서 처리됨)
371 GetOwner()->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
372
373 // 물리 복원
374 UPrimitiveComponent* PrimComp = GetOwnerPrimitiveComponent();
375 if (PrimComp)
376 {
377 PrimComp->SetSimulatePhysics(bOriginalSimulatePhysics);
378 PrimComp->SetCollisionEnabled(OriginalCollisionType);
379 }
380
381 // 상태 변경 (복제됨 → OnRep_IsPickedUp, OnRep_HoldingOwner 자동 호출)
382 bIsPickedUp = false;
383 HoldingOwner = nullptr;
384
385 PRINTLOG(TEXT("Server_Drop: %s dropped"), *GetOwner()->GetName());
386}
387
388
389UPrimitiveComponent* UInteractableComponent::GetOwnerPrimitiveComponent() const
390{
391 AActor* Owner = GetOwner();
392 if (!Owner)
393 return nullptr;
394
395 UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Owner->GetRootComponent());
396
397 // RootComponent가 PrimitiveComponent가 아니면, 자식 중에서 찾기
398 if (!PrimComp)
399 {
400 TArray<UActorComponent*> Components;
401 Owner->GetComponents(UPrimitiveComponent::StaticClass(), Components);
402
403 if (Components.Num() > 0)
404 {
405 // 첫 번쩨 primitive component 반환
406 PrimComp = Cast<UPrimitiveComponent>(Components[0]);
407 }
408 }
409
410 return PrimComp;
411}
412
413void UInteractableComponent::ShowDebugInfo(AActor* ViewerActor)
414{
415 if (!GetOwner() || !ViewerActor) return;
416
417 // 객체 위치 (약간 위쪽에 표시)
418 FVector DebugLocation = GetOwner()->GetActorLocation() + FVector(0.f, 0.f, 100.f);
419
420 // InteractionPrompt 사용
421 FString DebugMessage = InteractionPrompt;
422
423 // 3D 공간에 디버그 문자열 표시
424 DrawDebugString(
425 GetWorld(),
426 DebugLocation,
427 DebugMessage,
428 nullptr,
429 bIsPickedUp ? FColor::Yellow : FColor::Green,
430 0.0f,
431 true,
432 1.2f
433 );
434}
435
436void UInteractableComponent::TriggerInteraction(AActor* Interactor)
437{
438 if (!bCanInteract || !Interactor)
439 return;
440
441 // 델리게이트 브로드캐스트
442 OnInteractionTriggered.Broadcast(Interactor);
443
444 PRINTLOG( TEXT("InteractableComponent::TriggerInteraction - %s triggered by %s"), *GetOwner()->GetName(), *Interactor->GetName());
445}
446
447void UInteractableComponent::SetOutlineState(bool bEnabled)
448{
449 // 델리게이트 브로드캐스트 - Actor가 바인드하여 아웃라인 처리
450 OnOutlineStateChanged.Broadcast(bEnabled);
451}
452
453void UInteractableComponent::OnDetectionBeginOverlap(
454 UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
455 UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
456 bool bFromSweep, const FHitResult& SweepResult)
457{
458 if (!OtherActor)
459 return;
460
461 // Player 캐릭터인지 확인
462 if ( auto Character = Cast<ACharacter>(OtherActor) )
463 {
464 if ( Character->IsPlayerControlled())
465 {
466 // InteractionSystem 찾기 및 등록
467 if (auto InteractionSystem = Character->FindComponentByClass<UInteractionSystem>())
468 InteractionSystem->RegisterInteractable(this);
469
470 bool ShowState = IsWidgetShowEnable(Character);
471 if ( ShowState)
472 ShowInteractWidget();
473
474 // 픽업되지 않은 상태면 Outline 켜기
475 if (!bIsPickedUp)
476 SetOutlineState(true);
477
478 PRINTLOG( TEXT("InteractableComponent: Player entered detection range - %s"), *GetOwner()->GetName());
479 }
480 }
481}
482
483bool UInteractableComponent::IsWidgetShowEnable(const ACharacter* Character) const
484{
485 // 로컬 플레이어일 때만 위젯 표시 (멀티플레이어 버그 수정)
486 return Character && Character->IsLocallyControlled();
487}
488
489void UInteractableComponent::OnDetectionEndOverlap(
490 UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
491 UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
492{
493 if (!OtherActor)
494 return;
495
496 // Player 캐릭터인지 확인
497 ACharacter* Character = Cast<ACharacter>(OtherActor);
498 if (Character && Character->IsPlayerControlled())
499 {
500 // InteractionSystem에서 등록 해제
501 UInteractionSystem* InteractionSystem = Character->FindComponentByClass<UInteractionSystem>();
502 if (InteractionSystem)
503 {
504 InteractionSystem->UnregisterInteractable(this);
505 }
506
507 PRINTLOG( TEXT("InteractableComponent: Player left detection range - %s"), *GetOwner()->GetName());
508
509 // Widget 끄기
510 FTimerHandle TimerHandle;
511 GetWorld()->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([this]
512 {
513 HideInteractWidget();
514
515 // 픽업되지 않은 상태면 Outline 끄기
516 if (!bIsPickedUp)
517 SetOutlineState(false);
518 }), 0.1f, false);
519 }
520}
521
522#pragma region Widget
523void UInteractableComponent::InitWidget(UWidgetComponent* InWidgetComp)
524{
525 this->WidgetComp = InWidgetComp;
526
527 if (!WidgetComp)
528 return;
529
530 WidgetComp->SetVisibility(false); // 처음엔 숨겨두고
531
532 if (auto InteractWidget = Cast<UInteractWidget>(WidgetComp->GetWidget()))
533 InteractWidget->InitInfo(TEXT("E"), InteractionPrompt);
534}
535
536void UInteractableComponent::ShowInteractWidget()
537{
538 if (!WidgetComp)
539 return;
540
541 if (!WidgetComp->GetWidget())
542 return;
543
544 WidgetComp->SetVisibility(true);
545}
546
547void UInteractableComponent::HideInteractWidget()
548{
549 if (!WidgetComp)
550 return;
551
552 WidgetComp->SetVisibility(false);
553}
554
555void UInteractableComponent::BillboardInteractWidget()
556{
557 // 위젯이 없으면 빌보드화 안 함
558 if (!WidgetComp)
559 return;
560
561 // Visibility 체크 - 보이지 않으면 빌보드화 안 함
562 if (!WidgetComp->IsVisible())
563 return;
564
565 // 카메라 가져오기
566 AActor* Camera = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0);
567 if (!Camera)
568 return;
569
570 // 카메라를 향하도록 회전 계산
571 FRotator Rotation = UKismetMathLibrary::MakeRotFromXZ( -Camera->GetActorForwardVector(), Camera->GetActorUpVector() );
572 Rotation.Pitch = 0;
573
574 // 위젯 회전 설정
575 WidgetComp->SetWorldRotation(Rotation);
576}
577
578void UInteractableComponent::UpdateInteractPrompt(const FString& NewPrompt)
579{
580 // InteractionPrompt 업데이트
581 InteractionPrompt = NewPrompt;
582
583 // 위젯이 없으면 리턴
584 if (!WidgetComp)
585 return;
586
587 // UInteractWidget의 Txt_Desc 업데이트
588 if (auto InteractWidget = Cast<UInteractWidget>(WidgetComp->GetWidget()))
589 InteractWidget->UpdateDesc(NewPrompt);
590}
591
592void UInteractableComponent::SetWidgetVisibility(bool bVisible)
593{
594 if (!WidgetComp)
595 return;
596
597 if (bVisible)
598 ShowInteractWidget();
599 else
600 HideInteractWidget();
601}
602#pragma endregion
Declares the player-controlled character actor.
YiSan 전반에서 사용하는 공용 인터페이스를 선언합니다.
#define PRINTLOG(fmt,...)
Definition GameLogging.h:30
플레이어의 상호작용 감지 및 처리 시스템
Main character driven directly by the player.
TObjectPtr< class USceneComponent > HoldPosition