TIL
06.23 TIL
2026. 6. 23. 17:44

일정표

시간 할 일 비고
08:00~10:00 코드카타, 디자인패턴  
10:00~13:00 팀프로젝트, 디자인패턴  
14:00~15:00 디자인 패턴  
15:00~17:00 언리얼 강의 수강  
17:00~18:00 팀프로젝트  
19:00~21:00 TIL, 팀프로젝트  
21:00~22:00 운동  
22:00~23:30 개인 공부(언리얼 게임 만들기)  

*오늘의 코드카타*

 

문제. 시저 암호

더보기

어떤 문장의 각 알파벳을 일정한 거리만큼 밀어서 다른 알파벳으로 바꾸는 암호화 방식을 시저 암호라고 합니다. 예를 들어 "AB"는 1만큼 밀면 "BC"가 되고, 3만큼 밀면 "DE"가 됩니다. "z"는 1만큼 밀면 "a"가 됩니다. 문자열 s와 거리 n을 입력받아 s를 n만큼 민 암호문을 만드는 함수, solution을 완성해 보세요.

#include <string>

using namespace std;

string solution(string s, int n) {
    
    for (char& c : s)
    {
        if (c == ' ')
        {
            continue;
        }
        
        if (islower(c))
        {
            c = (c - 'a' + n) % 26 + 'a';
        }
        else if (isupper(c))
        {
            c = (c - 'A' + n) % 26 + 'A';
        }
    }
    return s;
}

(1) 아스키코드를 활용할 때, z에 1을 단순히 더하면 알파벳을 벗어난 기호가 출력된다 (실제로 { 가 출력됨). 그러면 z를 초과했을 때 다시 a로 돌아가게 하는 방법이 무엇일까. 바로  ( c - 'a' + n ) % 26 + 'a' 를 이용하는 것이다.

1) 일단 기준점을 0으로 맞춘다. c - 'a'를 하면 수학적으로 계산하기 편한 0~25의 인덱스로 바꿀 수 있다.

2) 그 이후에 이동거리(n)을 더한다.

3) 초과했을 때 처음으로 돌아간다? % 연산자를 사용하는 것을 떠올리면 된다. 따라서 % 26을 한다.

4) + 'a' 를 해서 원래 아스키코드로 복원한다. 소문자와 대문자를 분리해서 똑같이 진행하면 된다.


*Unreal C++*

주제 1 : 컨트롤러 및 총기 구현

 

컨트롤러 구현 : PlayerInputConfigDataAsset

 

총기 구현 : EquippedWeaponMesh 컴포넌트 + UWeaponDataAsset C++ 클래스

 

무기 교체 구현 : 무기 3종 전환 — 키 1/2/3로 저격/유탄/머신건 메시 교체

 

저격 모드 구현 : 라인 트레이스 — 마우스 좌클릭 → 디버그 라인 + 충돌점 표시

 

 

주제 2 : 입력과 상호작용의 구조화

 

1. 전통 Input System의 문제와 패러다임 전환 🫤

기존 입력 시스템의 문제점

void ACharacter::SetupPlayerInputComponent(UInputComponent* Input)
{
    Input->BindAction("Interact", IE_Pressed, this, &ACharacter::OnInteract);
}

void ACharacter::OnInteract()
{
    if (bIsInCleaningMode)
    {
        if (bNearCarpet)
            CleanCarpet();
        else if (bNearWindow)
            CleanWindow();
    }
    else if (bIsInCombatMode)
    {
        if (bHasWeapon)
            AttackWithWeapon();
        else
            Punch();
    }
    else if (bNearVehicle)
    {
        if (bInVehicle)
            ExitVehicle();
        else
            EnterVehicle();
    }
    // ... 계속 이어짐
}

문제점

상황에 따른 다른 동작 불가능!

확장성 부족!

코드 재사용성 낮음!

 

Enhanced Input System의 해결책

void ACharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    
    // "상호작용하고 싶다"는 의도만 바인딩
    Input->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ACharacter::OnInteract);
}

void ACharacter::OnInteract()
{
    // if문이 없음ㅇㅋ
    // 현재 상황(Context)이 자동으로 의미를 결정
}

구조의 차이

전통 방식
[E키] → [OnInteract 함수] → [내부에서 if문으로 분기] → [행동]

Enhanced 방식
[E키] → [InteractAction (의도)] → [Context가 해석] → [행동]

2. Enhanced Input 아키텍처

 

2-1. InputAction의 정체 - 데이터와 타입

 

🔹 InputAction은 단순한 데이터 에셋

 

클래스: UInputAction → UDataAsset 상속

역할: “입력의 타입과 의미”를 정의하는 설정 파일

enum class EInputActionValueType : uint8
{
    Boolean,   // 참/거짓
    Axis1D,    // 1차원 축
    Axis2D,    // 2차원 벡터
    Axis3D     // 3차원 벡터
};

타입 데이터 예시 사용 예

Boolean true / false 점프, 상호작용, 공격
Axis1D -1.0 ~ +1.0 앞뒤 이동(W/S), 좌우 회전
Axis2D (X, Y) 평면 이동 (WASD)
Axis3D (X, Y, Z) 3D 회전, 비행, 자유 이동

 Axis2D 예시 (Move Action)

void AMyCharacter::Move(const FInputActionValue& Value)
{
    FVector2D Movement = Value.Get<FVector2D>();
    AddMovementInput(GetActorForwardVector(), Movement.Y);
    AddMovementInput(GetActorRightVector(), Movement.X);
}

W, D 동시 입력 → (0,1)+(1,0)=(1,1) → 대각선 이동

길이 √2 → 정규화 필요 (엔진 옵션으로 자동 처리 가능)

 

🔹 Axis1D로 분리하는 이유

카메라 회전은 X/Y 축 민감도를 다르게 조정해야 함

따라서 LookX, LookY를 각각 Axis1D로 정의

IA_LookX → Axis1D  (마우스 X축)
IA_LookY → Axis1D  (마우스 Y축)

 

2-2. 입력 처리 파이프라인 개요

입력은 매 프레임 아래 과정을 거침.

1단계: [키 입력] 
   물리적으로 E키를 누름
   Raw Input Value = 1.0

2단계: [Modifier 적용]
   입력값을 변환
   예: Scale ×2.0 → 2.0

3단계: [Modified Value]
   변환된 최종 입력값 = 2.0

4단계: [Trigger 평가]
   이 입력이 조건을 만족하는가?
   예: 0.5초 이상 눌렀나?

5단계: [조건 만족 시]
   → InputAction 이벤트 발생
   → 바인딩된 함수 호출

 

2-3. Modifier - 입력값 변환

이름 기능 예시

Negate 부호 반전 W(+1), S(+1) → Negate → -1
Scale 민감도 조절 1.0 × 0.5 = 0.5
Dead Zone 잡음 제거 스틱의 미세한 흔들림 무시
Swizzle 축 재배열 (X,Y,Z) → (Y,X,Z)

🔹 예시 – Scale Modifier

// 마우스 민감도 조절
// UserSetting.Sensitivity = 0.5
Mapping.Modifiers.Add(NewObject<UInputModifier_Scale>());
Modifier->Scale = FVector(0.5f);

 

2-4. Trigger - 조건 평가

 

입력 이벤트가 “언제” 발동되는지를 정의

 

Trigger 조건 예시

Down 키가 눌렸는가 기본 클릭
Released 키를 뗐는가 활쏘기
Hold N초 유지 차지 공격
HoldAndRelease N초 유지 후 뗐는가 완충 후 발사
Tap N초 이내 눌렀다 뗐는가 더블탭 회피
Pulse 주기적으로 트리거 자동 연사

 

예시 – Hold Trigger (차지 공격)

BindAction(AttackAction, ETriggerEvent::Started, this, &AChar::LightAttack);
BindAction(AttackAction, ETriggerEvent::Triggered, this, &AChar::HeavyAttack);

Started: 키 눌린 즉시 (약공격)

Triggered: 0.5초 이상 눌렀을 때 (강공격)

 

2-5. InputAction의 생명 주기 - 엔진 내부 처리

// UEnhancedPlayerInput.cpp (단순화 버전)
void UEnhancedPlayerInput::ProcessInputStack(float DeltaTime)
{
    // 1️⃣ 활성 IMC 수집
    for (auto& IMC : ActiveContexts)
    {
        for (auto& Mapping : IMC->Mappings)
        {
            if (IsKeyPressed(Mapping.Key))
            {
                // 2️⃣ Modifier 적용
                FInputActionValue Value = Mapping.Key.GetRawValue();
                for (auto& Mod : Mapping.Modifiers)
                    Value = Mod->ModifyRaw(Value);

                // 3️⃣ 같은 Action 값 누적
                ActionValues[Mapping.Action].Accumulate(Value);
            }
        }
    }

    // 4️⃣ Trigger 평가 및 5️⃣ 이벤트 호출
    for (auto& Pair : ActionValues)
    {
        auto Action = Pair.Key;
        auto Value  = Pair.Value;
        auto Event  = EvaluateTriggers(Action, Value, DeltaTime);

        if (Event != ETriggerEvent::None)
            BroadcastInputAction(Action, Event, Value);
    }
}
1 ActiveContexts(IMC) 순회, 매핑 수집
2 Modifier 적용 (값 변환)
3 같은 Action 값 누적 (합산)
4 Trigger 조건 평가
5 이벤트 브로드캐스트 (바인딩 함수 호출)

 

3. Context 시스템의 실체 - Subsystem과 Stack 

 

3-1. Subsystem이란?

 

 :: 언리얼의 Subsystem 계층 ::

 

Subsystem은 “특정 범위의 생명주기를 가지는 관리 객체”.

Subsystem 종류 생명 범위 예시

UGameInstanceSubsystem 게임 전체 세이브 데이터 관리
UWorldSubsystem 월드 단위 AI 네비게이션, 날씨
ULocalPlayerSubsystem 플레이어 단위 Enhanced Input 관리

 

Enhanced Input Subsystem 구조

UGameInstance
 └─ ULocalPlayer (플레이어 1)
      └─ UEnhancedInputLocalPlayerSubsystem
           └─ UEnhancedPlayerInput
 └─ ULocalPlayer (플레이어 2)
      └─ UEnhancedInputLocalPlayerSubsystem
           └─ UEnhancedPlayerInput

 

플레이어마다 독립적인 입력 관리 시스템

LocalPlayer 기준이라, Possess가 바뀌어도 유지됨

→ (Spectator로 전환해도 입력 컨텍스트 유지 가능)

 

Subsystem 접근 경로

void UContextManagerComponent::InitializeSubsystem()
{
    AActor* Owner = GetOwner();  // 보통 캐릭터
    APlayerController* PC = Cast<APlayerController>(Owner->GetInstigatorController());
    ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();

    CachedSubsystem =
        ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer);
}

경로 요약:

Actor → PlayerController → LocalPlayer → Subsystem

 

게임 시작 직후나 AI 캐릭터는 PlayerController가 nullptr일 수 있음.

항상 null 체크 필수.

 

 

3-2. Priority System - 우선순위의 실제

 

 내부 구조 (단순화)

class UEnhancedInputLocalPlayerSubsystem : public ULocalPlayerSubsystem
{
    TArray<FInputMappingContextAndPriority> AppliedInputContexts;

    void AddMappingContext(UInputMappingContext* IMC, int32 Priority);
    void RemoveMappingContext(UInputMappingContext* IMC);
    void RequestRebuildControlMappings();
};

struct FInputMappingContextAndPriority
{
    UInputMappingContext* MappingContext;
    int32 Priority;

    bool operator<(const FInputMappingContextAndPriority& Other) const
    {
        return Priority > Other.Priority; // 높은 Priority가 먼저
    }
};

 AddMappingContext 동작 흐름

void AddMappingContext(UInputMappingContext* IMC, int32 Priority)
{
    // 이미 등록되어 있으면 Priority 업데이트
    for (auto& Ctx : AppliedInputContexts)
        if (Ctx.MappingContext == IMC)
        {
            Ctx.Priority = Priority;
            RequestRebuildControlMappings();
            return;
        }

    // 새로 추가
    AppliedInputContexts.Add({IMC, Priority});
    RequestRebuildControlMappings();
}

IMC들은 항상 Priority 순서로 정렬됨.

높은 Priority일수록 먼저 처리.

 

 

 처리 순서 예시

IMC_UI 100 UI 입력만 처리, 게임 입력 차단
IMC_Combat 50 UI에서 처리 안한 입력만 통과
IMC_Default 0 기본 입력 처리
→ 위에서 아래로 순회
   → ConsumeInput = true면 중단

 

 ConsumeInput 예시

UI 메뉴 UI만 반응, 게임 무시
인게임 HUD HUD도 반응, 게임도 반응
// 실제 동작
if (Mapping.bConsumeInput)
    break; // 아래 IMC로 전파 중단

 

 실전 예시 – UI 열기/닫기

void OpenMenu()
{
    Subsystem->AddMappingContext(IMC_Menu, 100);
}

void CloseMenu()
{
    Subsystem->RemoveMappingContext(IMC_Menu);
}

 

기본 IMC_Default(0)
메뉴 열림 IMC_Menu(100), IMC_Default(0)
메뉴 닫힘 IMC_Default(0)

E키 → “문 열기” → “UI 클릭” 으로 동적 전환.

 

3-3. ContextManager의 역할 - 의미론적 레이어

 Subsystem의 한계

상황 개념 없음 IMC 객체만 알고 “청소 모드” 의미를 모름
Stack 구조 없음 Priority 기반, “최근 추가” 개념 없음
중복 방지 없음 같은 IMC 여러 번 추가 가능
타입 안정성 부족 문자열/객체 기반, enum 미지원
상황 추적 불가 “이전 컨텍스트” 정보 없음

 

 우리가 추가하는 구조

USTRUCT()
struct FActiveContext
{
    EGameplayContext Context;              // enum 기반 이름
    UInputMappingContext* MappingContext;  // 실제 IMC
    float ActivationTime;                  // 활성화 시점
};

UCLASS()
class UContextManagerComponent : public UActorComponent
{
    TArray<FActiveContext> ContextStack;
    UEnhancedInputLocalPlayerSubsystem* CachedSubsystem;
    TMap<EGameplayContext, UInputMappingContext*> ContextMappings;
};

 

 PushContext() 구현

void UContextManagerComponent::PushContext(EGameplayContext NewContext)
{
    if (!CachedSubsystem) return;

    // 중복 방지
    for (auto& Ctx : ContextStack)
        if (Ctx.Context == NewContext) return;

    UInputMappingContext* IMC = *ContextMappings.Find(NewContext);
    int32 Priority = ContextStack.Num(); // Stack 크기 = Priority
    CachedSubsystem->AddMappingContext(IMC, Priority);

    ContextStack.Add({NewContext, IMC, GetWorld()->GetTimeSeconds()});
}

 

 PopContext() 구현

void UContextManagerComponent::PopContext()
{
    if (ContextStack.Num() == 0) return;

    const FActiveContext& Top = ContextStack.Last();
    CachedSubsystem->RemoveMappingContext(Top.MappingContext);
    ContextStack.Pop();
}

 

LIFO 구조 – 마지막으로 들어간 Context가 먼저 제거됨

 

 

 GetCurrentContext()

EGameplayContext UContextManagerComponent::GetCurrentContext() const
{
    return ContextStack.Num() == 0 ?
        EGameplayContext::Default :
        ContextStack.Last().Context;
}

 의미론적 레이어의 가치

[우리 레이어]
  EGameplayContext::WindowCleaning
  EGameplayContext::CombatMode
    ↓ (ContextMappings)
[엔진 레이어]
  IMC_WindowCleaning
  IMC_Combat
    ↓ (Subsystem)
[입력 처리]
  E키 → InteractAction → "유리 닦기"

 

3-4. Priority 전략 - 우리의 선택

 

:: 문제: Priority 0 고정의 위험 ::

CachedSubsystem->AddMappingContext(IMC, 0);

모든 IMC가 같은 우선순위면 순서 예측 불가

엔진 버전에 따라 동작이 달라질 수 있음

 

 :: 해결: Stack 크기를 Priority로 사용 ::

int32 Priority = ContextStack.Num();
CachedSubsystem->AddMappingContext(IMC, Priority);

 

Default 0 기본
FloorCleaning 1 위에 쌓임
WindowCleaning 2 최우선

 

숫자가 클수록 최근에 추가된 Context.

항상 “가장 최신 상황이 최우선”.

 

 

 Pop 후 자동 복귀

PopContext(); // 최상위 제거 → 아래 Context가 우선순위 승계

 실제 동작 예시

1 [Default(0)] IMC_Default(0) 문 열기
2 [Default, Floor(1)] IMC_Floor(1), IMC_Default(0) 카펫 청소
3 [Default, Window(1)] IMC_Window(1), IMC_Default(0) 유리 닦기

 

3-5. 전체 구조 요약

[플레이어 입력]
     ↓
[InputAction]  ← 값/타입
     ↓
[ContextManager]  ← 의미/상황 (우리 시스템)
     ↓
[Subsystem]  ← IMC 관리 (엔진 시스템)
     ↓
[Input Mapping Context]  ← 실제 키 해석
     ↓
[함수/행동 실행]

 

Subsystem IMC를 Priority 순서로 관리
ConsumeInput 입력 전파 제어
ContextManager enum 기반 의미 추가
Stack 구조 LIFO, 동적 전환 지원
Priority 전략 Stack 크기 = Priority
GetCurrentContext 현재 상황 조회 API

 

4. State Machine - 행동 허가증 

 

4-1. Context vs. State - 외적 상황과 내적 상태

의미 외적 상황 (환경/모드) 내적 상태 (현재 동작)
질문 “어떤 환경인가?” “지금 무엇을 하고 있는가?”
변경 주체 플레이어의 선택 시스템의 전환
예시 전투 모드, 청소 모드, 차량 탑승 Idle, Cleaning, Attacking, Dead
지속성 명시적 전환까지 유지 조건 충족 시 자동 변경

 

같은 장소(Context)라도, **상태(State)**에 따라 가능한 행동이 다르다.

 

 

 코드 비교

 Context만 있을 때 (문제 발생)

void OnInteract()
{
    if (CurrentContext == FloorCleaning)
        CleanFloor();  // 계속 호출됨 → 애니메이션 중복
}

 State까지 고려한 경우

void OnInteract()
{
    if (CurrentContext == FloorCleaning && CurrentState == Idle)
    {
        CleanFloor();
        CurrentState = Cleaning;
    }
}

 

State가 “허가증” 역할을 함.

Idle → Cleaning 전환은 허용, Cleaning 중이면 거부.

 

 

4-2. Finite State Machine (FSM) 패턴

 

 구성요소

 State – 가능한 상태 집합

 Transition – 상태 간 이동 규칙

 Guard – 전환 허용 조건

 

🔹 1️⃣ State 정의

enum class ECleaningState : uint8
{
    Idle,
    Cleaning,
};

 

Enum 사용으로 의미 명확, 타입 안정성 확보.

 

 

🔹 2️⃣ Transition 규칙 (다이어그램)

   ┌──────────┐
   │   Idle   │◄──────────────┐
   └────┬─────┘               │
        │                     │
        │ StartCleaning()     │ InterruptCleaning()
        │ (E키 입력)          │ (이동/점프/시간 종료)
        ↓                     │
   ┌──────────┐               │
   │ Cleaning │───────────────┘
   └──────────┘

 

🔹 3️⃣ Guard (조건 함수)

bool CanStartCleaning() const { return CurrentState == Idle; }
bool CanMove() const          { return CurrentState != Cleaning; }
bool CanJump() const          { return CurrentState != Cleaning; }

 

class UCleaningStateMachine : public UActorComponent
{
    GENERATED_BODY()

private:
    ECleaningState CurrentState = ECleaningState::Idle;
    float StateStartTime = 0.f;
    float AnimationDuration = 0.f;

    UPROPERTY()
    class AEnhancedChallengeCharacter* OwnerCharacter;

public:
    // 상태 전환
    void StartCleaning(float Duration);
    void InterruptCleaning();

    // 조건 검사
    bool CanStartCleaning() const;
    bool CanMove() const;
    bool CanJump() const;

    // 조회
    ECleaningState GetCurrentState() const { return CurrentState; }

protected:
    virtual void BeginPlay() override;
    virtual void TickComponent(float DeltaTime, ...) override;

private:
    void SetState(ECleaningState NewState);
    void StopCurrentMontage();
};
void UCleaningStateMachine::BeginPlay()
{
    Super::BeginPlay();
    
    // 캐릭터 참조 캐싱
    OwnerCharacter = Cast<AEnhancedChallengeCharacter>(GetOwner());
    
    if (!OwnerCharacter)
    {
        UE_LOG(LogTemp, Error, TEXT("StateMachine: Owner is not EnhancedChallengeCharacter!"));
    }
}
void UCleaningStateMachine::StartCleaning(float Duration)
{
    // 1단계: Guard 체크
    if (!CanStartCleaning())
    {
        UE_LOG(LogTemp, Warning, TEXT("Cannot start cleaning: wrong state (%d)"), 
            (int32)CurrentState);
        return;
    }
    
    // 2단계: Exit Current State
    // Idle 상태에서는 특별히 할 게 없음
    // 다른 상태였다면 여기서 정리 작업
    
    // 3단계: Transition (상태 변경)
    ECleaningState OldState = CurrentState;
    CurrentState = ECleaningState::Cleaning;
    
    // 4단계: Enter New State (새 상태 초기화)
    StateStartTime = GetWorld()->GetTimeSeconds();
    AnimationDuration = Duration;
    
    // 5단계: Event (상태 변경 알림)
    UE_LOG(LogTemp, Log, TEXT("State changed: %d -> %d"), 
        (int32)OldState, (int32)CurrentState);
    
    // 델리게이트로 외부에 알림 (나중에 설명)
    OnStateChanged.Broadcast(OldState, CurrentState);
}

// Enter-Update-Exit 패턴
void UCleaningStateMachine::TickComponent(
    float DeltaTime, 
    ELevelTick TickType, 
    FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    
    // Update State: Cleaning 상태일 때만 체크
    if (CurrentState == ECleaningState::Cleaning)
    {
        // 경과 시간 계산
        float CurrentTime = GetWorld()->GetTimeSeconds();
        float ElapsedTime = CurrentTime - StateStartTime;
        
        // 애니메이션 시간이 다 됐나?
        if (ElapsedTime >= AnimationDuration)
        {
            // 자동 전환: Cleaning → Idle
            UE_LOG(LogTemp, Log, TEXT("Cleaning finished (%.2fs), returning to Idle"), 
                ElapsedTime);
            
            SetState(ECleaningState::Idle);
        }
    }
}
void UCleaningStateMachine::InterruptCleaning()
{
    // 현재 Cleaning 상태일 때만
    if (CurrentState == ECleaningState::Cleaning)
    {
        UE_LOG(LogTemp, Log, TEXT("Cleaning interrupted!"));
        
        // 애니메이션 정지
        StopCurrentMontage();
        
        // 상태 전환
        SetState(ECleaningState::Idle);
    }
}
void UCleaningStateMachine::SetState(ECleaningState NewState)
{
    // 같은 상태로의 중복 전환 방지
    if (CurrentState == NewState)
    {
        UE_LOG(LogTemp, Warning, TEXT("Already in state: %d"), (int32)NewState);
        return;
    }
    
    // 이전 상태 저장
    ECleaningState OldState = CurrentState;
    
    // Exit Old State
    switch (OldState)
    {
    case ECleaningState::Cleaning:
        // 청소 상태를 빠져나갈 때 정리
        StopCurrentMontage();
        break;
        
    case ECleaningState::Idle:
        // Idle은 정리할 게 없음
        break;
    }
    
    // Transition
    CurrentState = NewState;
    
    // Enter New State
    switch (NewState)
    {
    case ECleaningState::Idle:
        // Idle은 초기화할 게 없음
        break;
        
    case ECleaningState::Cleaning:
        // Cleaning은 StartCleaning에서 초기화함
        break;
    }
    
    // Event
    UE_LOG(LogTemp, Log, TEXT("State changed: %d -> %d"), 
        (int32)OldState, (int32)NewState);
    
    // 델리게이트 브로드캐스트
    OnStateChanged.Broadcast(OldState, NewState);
}

 

4-3. Animation과 State의 분리

 

잘못된 방식

void OnInteract()
{
	PlayAnimMontage(CleaningMontage); // 애니메이션만 실행
}

중단되거나 스킵 가능 → 신뢰도 낮음

논리적 상태 불일치 발생

 

올바른 방식

void OnInteract()
{
    // 1. State 확인
    if (!StateMachine->CanStartCleaning())
    {
        return;  // 불가능하면 아무것도 안 함
    }
    
    // 2. 애니메이션 재생
    float Duration = PlayAnimMontage(CleaningMontage);
    
    // 3. State Machine에 알림 (여기가 핵심!)
    StateMachine->StartCleaning(Duration);
}

 

State가 논리의 중심,

Animation은 표현만 담당.

 

 

StopCurrentMontage()

void UCleaningStateMachine::StopCurrentMontage()
{
    // 캐릭터가 유효한지 확인
    if (!OwnerCharacter)
    {
        UE_LOG(LogTemp, Warning, TEXT("OwnerCharacter is null!"));
        return;
    }
    
    // AnimInstance 가져오기
    USkeletalMeshComponent* Mesh = OwnerCharacter->GetMesh();
    if (!Mesh)
    {
        return;
    }
    
    UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
    if (!AnimInstance)
    {
        return;
    }
    
    // 몽타주가 재생 중인지 확인
    if (AnimInstance->IsAnyMontagePlaying())
    {
        // 0.25초 블렌드 타임으로 부드럽게 정지
        AnimInstance->Montage_Stop(0.25f);
        
        UE_LOG(LogTemp, Log, TEXT("Stopped montage with 0.25s blend"));
    }
}

 

0.25초 블렌드 타임으로 부드럽게 전환.

 

 

4-4. Single Source of Truth

 

모든 시스템은 StateMachine만 참조해야 한다.

// UI 시스템
void UpdateHUD()
{
    switch (StateMachine->GetCurrentState())
    {
    case Idle:
        HUD->Hide();
        break;
    case Cleaning:
        HUD->ShowMessage("Cleaning...");
        break;
    }
}

// 전투 시스템
void TakeDamage(float Damage)
{
    // 청소 중엔 피해 안 받음
    if (StateMachine->GetCurrentState() == Cleaning)
    {
        return;
    }
    
    Health -= Damage;
}

// 다른 입력
void OnAttack()
{
    // 청소 중엔 공격 불가
    if (StateMachine->GetCurrentState() != Idle)
    {
        return;
    }
    
    Attack();
}

 

StateMachine이 논리적 진실의 단일 원천(Single Source of Truth).

 

 

5. Enhanced Input System이 추구한 설계 원칙

 

5-1. 단일 책임 원칙 (SRP)

 

 

Single Responsibility Principle

→ 하나의 클래스는 하나의 책임만 가져야 한다.

 

 

우리 시스템의 구조

UContextManagerComponent 입력 의미 해석 (IMC 관리)
UCleaningStateMachine 상태 전이와 행동 허가
AEnhancedChallengeCharacter 입력 위임 및 통합
ContextManagerComponent  → 입력 해석만 담당
CleaningStateMachine     → 상태 관리만 담당
EnhancedChallengeCharacter → 위임만 담당

 

나쁜 예 – 책임 혼합

void ACharacter::OnInteract()
{
    // 입력 처리
    if (CurrentMode == CleaningMode) { ... }

    // 상태 확인
    if (bIsBusy) { ... }

    // 애니메이션 / UI / 사운드 전부 여기서
    PlayMontage(...);
    UpdateHUD(...);
    PlaySound(...);
}

→ 유지보수 지옥. 테스트 불가능. 의존 관계 꼬임.

 

좋은 구조 – 분리된 책임

ContextManager: 현재 모드(외적 상황)만 판단

StateMachine: 지금 가능한가(내적 상태)만 판단

Character: 호출만 담당

 

각 컴포넌트를 독립적으로 테스트 가능

수정해도 다른 부분 영향 없음

재사용성 극대화

 

 

5-2. 개방-폐쇄 원칙 (OCP)

 

 

Open–Closed Principle

→ “확장에는 열려 있고, 수정에는 닫혀 있어야 한다.”

 

 

🔹 새 Context 추가 예시

enum class EGameplayContext : uint8
{
    Default,
    FloorCleaning,
    WindowCleaning,
    Fishing,      // ← 새 기능 추가
};

 IMC_Fishing 에셋 생성

ContextMappings에 등록

끝. 기존 코드는 그대로.

 

확장만 했지, 기존 코드는 수정하지 않았다.

 

 

🔹 새 State 추가 예시

enum class ECleaningState : uint8
{
    Idle,
    Cleaning,
    Resting,     // ← 추가
    Exhausted    // ← 추가
};

void StartResting()
{
    if (CurrentState == Idle || CurrentState == Exhausted)
        SetState(Resting);
}

Idle ↔ Cleaning 로직은 그대로

새로운 전이(Transition)만 추가

 

5-3. 의존성 역전 원칙 (DIP)

 

 

Dependency Inversion Principle

→ 구체적 입력(E키, 게임패드 버튼)에 의존하지 않고,

추상적 의도(InputAction) 에 의존해야 한다.

 

 

나쁜 예

if (IsKeyPressed(EKeys::E))  // 키보드에 고정 의존
    CleanFloor();

키보드에서만 작동

게임패드나 VR 컨트롤러 추가 시 코드 수정 필요

 

좋은 예

void OnInteract(const FInputActionValue& Value)
{
    // 어떤 키인지 모름
    // 어떤 장치인지도 모름
    ExecuteInteraction();  // 추상화된 의도만 실행
}

 

InputAction = “상호작용하고 싶다”라는 추상 신호

→ 장치(IMC)만 바꾸면 그대로 동작

 

 

5-4. 확장 시나리오

 

시나리오 1: 멀티 도구 시스템

enum class EGameplayContext : uint8
{
    // 청소
    FloorCleaning,
    WindowCleaning,
    CarCleaning,

    // 전투
    Sword,
    Bow,
    Magic,

    // 이동 수단
    OnFoot,
    InCar,
    InBoat,
};

각각 IMC를 따로 등록

PushContext()로 모드 전환 시 완전히 다른 조작 체계

 

시나리오 2: 복합 State Machine

class ACharacter
{
    UCleaningStateMachine* CleaningState;
    UCombatStateMachine* CombatState;
    UMovementStateMachine* MovementState;
};

void OnAttack()
{
    if (CombatState->CanAttack() &&
        CleaningState->IsIdle() &&
        MovementState->CanAct())
    {
        Attack();
    }
}

 

여러 FSM을 병렬로 운용해도 간섭 없이 동작.

 

 

시나리오 3: 데이터 주도 설계

USTRUCT()
struct FContextDefinition
{
    EGameplayContext ContextType;
    UInputMappingContext* IMC;
    TArray<UAnimMontage*> AvailableActions;
    float StaminaCost;
};

Context 정의를 Data Asset으로 관리

디자이너가 밸런스 조정 가능

코드 수정 없이 행동 세부 조정 가능

 


 

'TIL' 카테고리의 다른 글

06.25 TIL  (0) 2026.06.25
06.24 TIL  (0) 2026.06.24
06.22 TIL  (0) 2026.06.22
06.19 TIL  (0) 2026.06.19
06.18 TIL  (0) 2026.06.18