🤔 초기 설계 의도
"Controller의 역할은 입력을 처리하는 것이 아닌가?"
PlayerController의 본연 기능이 입력 처리라고 생각했기에, 모든 입력을 Controller에서 받도록 설계했습니다:
// SoulPlayerControllerBase.cpp
void ASoulPlayerControllerBase::SetupInputComponent()
{
// 모든 입력을 Controller에 바인딩
EnhancedInputComp->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ThisClass::Move);
EnhancedInputComp->BindAction(AttackAction, ETriggerEvent::Started, this, &ThisClass::Attack);
EnhancedInputComp->BindAction(RollingAction, ETriggerEvent::Started, this, &ThisClass::Rolling);
// ... 30개 이상의 입력
}
책임 소재가 명확해 보였습니다:
- Controller: 입력 받기
- Character: 행동 실행
🚨 문제 인식
Controller는 단순 중계자일 뿐
그러나 실제 코드를 보면 Controller는 아무것도 하지 않습니다:
// SoulPlayerControllerBase.cpp - 반복되는 패턴
void ASoulPlayerControllerBase::Attack()
{
if (ASoulPlayerCharacter* SoulCharacter = Cast<ASoulPlayerCharacter>(GetCharacter()))
{
SoulCharacter->Attack(); // 그냥 전달만
}
}
void ASoulPlayerControllerBase::Rolling()
{
if (ASoulPlayerCharacter* SoulCharacter = Cast<ASoulPlayerCharacter>(GetCharacter()))
{
SoulCharacter->Rolling(); // 그냥 전달만
}
}
void ASoulPlayerControllerBase::HeavyAttack()
{
if (ASoulPlayerCharacter* SoulCharacter = Cast<ASoulPlayerCharacter>(GetCharacter()))
{
SoulCharacter->HeavyAttack(); // 그냥 전달만
}
}
// ... 30개 함수가 모두 동일한 패턴 ❌
실제 입력 흐름:
입력 → Controller::Attack()
↓ (캐스팅 + 전달)
Character::Attack()
↓ (다시 전달)
CombatComponent::Attack()
↓ (실제 로직)
DoAttack()
3단계를 거쳐야 실제 로직 도달 ❌
관찰한 문제점:
- Controller는 입력 토스하기 바쁨
- 이동 → CharacterMovementComponent
- 공격 → CombatComponent
- 인벤토리 → InventoryUI
- UI 모드 전환 → WidgetManager
- Controller가 모든 클래스를 알아야 함
// Controller가 알아야 하는 것들 ASoulPlayerCharacter* Character; UCombatComponent* CombatComp; UAttributeComponent* AttributeComp; UStateComponent* StateComp; UInventoryComponent* InventoryComp; UWidgetManagerComponent* WidgetManager; // ... 계속 증가- 결합도 폭발
- CombatComponent 수정 → Controller도 수정 필요
- 새 기능 추가 → Controller에 함수 추가
- "책임 소재 명확"하지만 실제론 God Class ❌
💭 고민의 흐름
고민 1: "Controller의 결합도를 낮추려면?"
Character로 위임했지만, Character도 같은 고민:
// SoulPlayerCharacter.cpp
void ASoulPlayerCharacter::Attack()
{
if (CombatComponent)
{
CombatComponent->Attack(); // 또 전달
}
}
결국 "입력 → Controller → Character → Component" 3단계 불필요한 중계
고민 2: "무기별로 다른 입력을 어떻게 처리할까?"
Q키를 눌렀을 때:
- 한손검: 즉발 특수 공격
- 대검: 차징 공격
- 폴암: 원거리 투척
시도했던 방법들:
A안: Character에서 Switch 문
void ASoulPlayerCharacter::SpecialAttack()
{
ECombatType CombatType = CombatComponent->GetWeapon()->GetCombatType();
switch (CombatType)
{
case ECombatType::Sword:
CombatComponent->SpecialAttack();
break;
case ECombatType::GreatSword:
CombatComponent->ChargeAttack();
break;
case ECombatType::Polearm:
CombatComponent->ThrowWeapon();
break;
}
}❌ Character가 모든 무기 타입을 알아야 함
❌ 무기 추가 시마다 Switch 문 수정
B안: Delegate로 태그만 전달?
// Controller
void ASoulPlayerControllerBase::SpecialAttack()
{
OnInputReceived.Broadcast(SoulGameplayTag::Input_SpecialAttack);
}
// Character
void ASoulPlayerCharacter::OnInputReceived(FGameplayTag InputTag)
{
switch (InputTag)
{
case Input_SpecialAttack:
// 무기 타입에 따라 분기
break;
}
}❌ 결국 Character의 Switch 문은 동일
❌ 추상화했지만 근본적 해결 아님
C안: 무기가 IMC를 가지고 장착 시 전달?
// 상상 속 코드
void ASoulWeapon::EquipItem()
{
Character->AddMappingContext(WeaponSpecificIMC);
// Q키 → 차징 공격으로 자동 매핑
}✅ 무기마다 다른 입력 가능
❌ 구현 복잡도 높음, Enhanced Input 이해 필요
❓ "이게 과연 올바른 방법일까?" → 막막함
💡 깨달음: "입력의 소비자에게 책임을"
왜 ACharacter::SetupPlayerInputComponent()가 존재하는가?
언리얼 공식 문서와 커뮤니티를 찾아보며 알게 된 사실:
"PlayerController는 '플레이어의 의지'를 대변하고,
Pawn은 '그 의지를 수행하는 육체'다."
빙의(Possess)의 의미:
- Controller가 Pawn에 빙의한다 = 의지(Controller)와 육체(Pawn) 분리
- 입력을 '받는' 곳과 '소비하는' 곳을 분리하는 설계 철학
핵심 인사이트:
❌ Controller: "공격 입력 → 공격 실행"
✅ Controller: "공격 입력 수신" / Pawn: "공격 실행"
</br>
</br>🔧 접근 방법
1. Enhanced Input + IMC
무기가 IMC를 소유하는 방식:
// SoulWeapon.h
UCLASS()
class ASoulWeapon
{
UPROPERTY(EditDefaultsOnly)
UInputMappingContext* WeaponInputContext; // 무기별 IMC
};
// SoulWeapon.cpp - 장착 시
void ASoulWeapon::EquipItem()
{
if (APlayerController* PC = Character->GetController<APlayerController>())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
// 무기 전용 입력 추가
Subsystem->AddMappingContext(WeaponInputContext, 1);
}
}
}
// IMC_Sword.asset
// Q키 → IA_ChargeAttack
// IMC_Axe.asset
// Q키 → IA_QuickAttack
흐름:
1. 검 장착 → IMC_Sword 추가
Q키 = IA_ChargeAttack 매핑
2. 도끼 장착 → IMC_Sword 제거 + IMC_Axe 추가
Q키 = IA_QuickAttack 매핑
3. Controller는 "Q가 눌렸다"만 알 뿐,
차징인지 즉발인지는 무기의 IMC가 결정
장점:
- ✅ Controller는 무기 타입을 몰라도 됨
- ✅ 무기 추가 시 코드 수정 0줄
- ✅ IMC Asset만 만들면 끝
- ✅ 결합도 완전 분리
2. GameplayTag + Delegate
입력을 '의도'로 추상화:
// PlayerController
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInputIntent, FGameplayTag, IntentTag);
void ASoulPlayerController::SpecialAttack()
{
// 무기/캐릭터를 모름, 태그만 방송
OnInputIntent.Broadcast(GameplayTag::Input_SpecialAttack);
}
// CombatComponent
void UCombatComponent::BeginPlay()
{
// 태그 구독
PlayerController->OnInputIntent.AddDynamic(this, &ThisClass::OnInputReceived);
}
void UCombatComponent::OnInputReceived(FGameplayTag IntentTag)
{
if (IntentTag == GameplayTag::Input_SpecialAttack)
{
// 현재 무기에 맞는 행동 실행
Weapon->ExecuteSpecialAttack();
}
}
장점:
- ✅ Controller는 의도만 전달
- ✅ 여러 시스템이 동시에 구독 가능
3. Character에서 직접 입력 처리 (가장 단순)
// SoulPlayerCharacter.cpp
void ASoulPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
// Controller 거치지 않고 직접 바인딩
EnhancedInput->BindAction(AttackAction, ETriggerEvent::Started, this, &ThisClass::Attack);
EnhancedInput->BindAction(RollingAction, ETriggerEvent::Started, this, &ThisClass::Rolling);
}
Controller는 시스템 입력만:
// SoulPlayerController.cpp
void ASoulPlayerController::SetupInputComponent()
{
// UI/시스템만
EnhancedInput->BindAction(PauseAction, ETriggerEvent::Started, this, &ThisClass::Pause);
EnhancedInput->BindAction(InventoryAction, ETriggerEvent::Started, this, &ThisClass::OpenInventory);
}
장점:
- ✅ 불필요한 중계 함수 제거
- ✅ 디버깅 용이 (Character만 확인)
- ✅ AI도 Character 함수 직접 호출
✅ 결론: PlayerController의 진짜 역할
현재 구조의 문제:
❌ PlayerController = "입력 처리자" (God Class)
→ 모든 시스템을 알아야 함
→ 결합도 폭발
→ 중계 함수 30개
올바른 구조:
✅ PlayerController = "입력 관리자" (Input Manager)
→ IMC 추가/제거 관리
→ 의도(Tag)로 변환하여 방송
→ UI/시스템 기능만 직접 처리
책임 분리:
- Controller: 누가 조종하는가 (Possess/Unpossess), 입력 컨텍스트 관리
- Character: 무엇을 할 수 있는가 (Move/Attack/Roll), 입력 직접 처리
- Component: 어떻게 하는가 (로직 구현)
"입력을 '처리'하는 것과 '관리'하는 것은 다르다"
Controller가 모든 입력을 처리하는 것은:
- ❌ 교과서적이지만 확장성 낮음
- ❌ 중계 함수만 늘어남
- ❌ 결합도 문제
Enhanced Input의 IMC나 GameplayTag를 활용하면:
- ✅ Controller는 "입력 컨텍스트 관리자"
- ✅ Character/Component는 "입력 소비자"
- ✅ 무기/스킬 추가 시 코드 수정 불필요
다음 프로젝트에서는:
- Character에서 직접 입력 처리 (기본)
- 무기별 IMC로 동적 매핑 (확장)
- Controller는 UI/시스템만 담당
ACharacter::SetupPlayerInputComponent()가 존재하는 이유는 "행동의 주체가 직접 입력을 받는 게 자연스럽다"
'프로젝트 회고' 카테고리의 다른 글
| [리팩토링] OCP(개방 폐쇄 원칙) 기반 설계: 상속에서 인터페이스로 전환 (0) | 2025.12.03 |
|---|---|
| [Dedicated Server] 네트워크 복제 대역폭 최적화: ```Fast Array Serializer```를 활용한 배열 요소 단위 델타 복제(Delta Replication) (0) | 2025.12.03 |
| [UE5 액션] [리팩토링] CPU 성능 최적화: Tick 의존성 해소, 이벤트 기반(Event-Driven Architecture)으로의 전환 (0) | 2025.12.02 |
| [DirectX 11] Unity 리소스 메타데이터 파싱을 통한 스프라이트 시트 관리 (0) | 2025.12.02 |
| [UE5 팀 프로젝트] Pull-Request 시행착오와 교훈 (0) | 2025.12.02 |
