< 개요 >
- 캐릭터의 상태 머신도 구현했으니, 이어서 구르기 기능도 추가하도록 하자
< 회피 기능 추가 >
- 회피 기능 동작 과정
- Space 누르면 회피
- 애니메이션 재생
- 이동
- 무적 처리
- 일정 시간 후, Idle로 복귀
회피 기능 동작 과정 | 1. Space 누르면 회피 기능 발동 2. 애니메이션 재생 3. 캐릭터가 회피 방향으로 이동 4. 회피 중, 무적 처리 5. 일정 시간 후, 다시 Idle로 복귀하고 무적 해제 |
구현 계획 | 1. InputAction 생성 및 InputMappingContext에 등록 2. PlayerCharacter 클래스에 IA 등록 및 함수 바인드 3. PlayerCharacter::Evade() 작성 4. AnimNotify(EvadeStart, EvadeEnd) 생성 후, 회피 애니메이션 몽타주를 생성하고 노티파이 추가 5. 캐릭터 블루프린트에서 회피 애니메이션 몽타주 등록 6. BaseCharacter 클래스의 CharacterState와 BA_Common의 AnimState를 동기화해주는 컴포넌트 작성(StateSyncComponent) 7. BaseCharacter 클래스에 StateSyncComponent 부착 후, SetState 함수 내에서 동기화되도록 해줌 |
[ 1. InputAction 생성 및 InputMappingContext에 등록 ]
조작 키 변경 | |
이동 | WASD |
회전 | 마우스 XY축 |
점프 | 왼쪽 Ctrl |
전력질주 | 왼쪽 Shift |
회피 | Space |
락온 타겟 변경 |
락온 : Tab 타겟 변경 : T |
줌인 줌 리셋 |
줌 인 : 마우스 휠 줌 리셋 : End |
공격 | 마우스 좌클릭 |
[ 2. PlayerCharacter 클래스에 IA 등록 및 Evade 함수 바인드 ]
[ 3. PlayerCharacter::Evade() 작성 ]
Evade() 구조 | |
회피 가능 여부 체크 | - 회피 쿨타임이 도는 중이면 회피 불가 - Evade나 Dead 상태에서는 회피 불가 |
입력 방향 가져오기 | - 플레이어가 마지막으로 입력한 이동 방향을 가져옴(WASD) - 월드 좌표계 기준의 값이 들어있다 |
가져온 입력 방향을 로컬 기준으로 변환 |
- 캐릭터의 현재 Transform을 기준으로 하여, 입력 방향을 로컬 기준으로 변환 - 변환 후에는 로컬 기준의므로 이렇게 된다 - X축 : 내가 바라보는 앞(+)/뒤(-) 방향 - Y축 : 내가 바라보는 좌(-)/우(+) 방향 |
변환한 입력 방향을 평면 방향으로 압축 |
- 평면에서의 회피만 다루고 있으므로, XY만 필요하고 Z값은 무시 |
방향 판별 (Enum 결정) |
- 받아온 입력값의 X, Y축 절대값을 계산 - X축 입력값이 더 큼 : 캐릭터가 앞뒤로 움직임 - Y축 입력값이 더 큼 : 캐릭터가 좌우로 움직임 |
회피 몽타주 재생 및 방향 계산 |
- switch 문을 사용하여 방향에 따라 다른 회피 애니메이션 재생 - 캐릭터를 이동시킬 수 있게 EvadeDirection 변수를 선언 - 캐릭터가 현재 바라보는 방향 기준으로 EvadeDirection 설정 |
물리 이동 처리 | - LaunchCharacter()를 사용하여 EvadeDirection으로 물리 이동을 발생시킴 |
- FVector APawn::GetLastMovementInputVector() const
- 이 함수는 AddMovementInput() 으로 넣었던 입력 벡터들을 합산해서 기억해둔 다음, "월드 기준 방향"으로 반환함
- 그러므로 이를 로컬 기준으로 사용하기 위해서는 반드시 로컬 기준 좌표로 변환하는 작업이 필요함
- 예시)
- 플레이어가 앞을 누름 → "월드 Forward 방향" 기준의 값이 들어있음 (캐릭터가 보고 있는 방향이 아님)
- 플레이어가 앞을 누름 → "월드 Forward 방향" 기준의 값이 들어있음 (캐릭터가 보고 있는 방향이 아님)
- GetLastMovementInputVector() : 마지막 이동 입력(월드 기준) ~ 일반적인 이동 방향 판별에 사용됨
- GetPendingMovementInputVector() : 아직 처리되지 않은 이동 입력
- ConsumeMovementInputVector() : 입력 벡터를 꺼내고 초기화
- FVector FTransform::InverseTransformVectorNoScale(const FVector &V) const
- 월드 좌표계의 벡터를 내 로컬 좌표계 벡터로 변환해주는 함수
- 이 벡터가 내 위치/회전을 기준으로 봤을 때 어떤 방향인지를 알려줌
- 예시)
- 캐릭터가 북쪽(월드 상 +Z)을 보고 있음
- 플레이어가 "앞" 입력
- 월드 기준으로는 +Z / 로컬 기준으로는 +X
[ 4. 애님 노티파이 및 회피 애니메이션 몽타주 생성 후, 노티파이 추가 ]
- 굳이 Notify로 bCanEvade와 CharacterState를 관리하는 이유는 뭘까?
- SOJPlayerCharacter::Evade()에서 코드로 관리해도 되긴 하는데, 이렇게 하면 문제점이 몇 가지 존재한다
- 애니메이션 길이와 EvadeCooltime이 다르면 타이밍이 꼬임
- 회피 애니메이션이 길어지거나 짧아지더라도 쿨타임은 고정되어 있음
- 경직 처리나 Blend 시간을 고려할 수 없음
- AnimNotify 기반으로 관리하게 되면 갖는 장점
- 정확하게 Evade 애니메이션이 시작하는 시점에 무적 상태가 시작됨
- 정확하게 Evade 애니메이션이 종료되는 시점에 무적 상태가 종료되고 원상태(Idle)로 복귀 가능
- SOJPlayerCharacter::Evade()에서 코드로 관리해도 되긴 하는데, 이렇게 하면 문제점이 몇 가지 존재한다
- 그러면 bCanEvade와 CharacterState를 AnimNotify와 PlayerCharacter::Evade() 둘 다에서 관리해도 되는거 아닌가?
- 이렇게 되면 물론 몽타주가 재생되지 않거나 노티파이가 누락되더라도 최소한 상태 전이는 가능해져서 더 안전하긴 하다
- 그러나 이렇게 되면 상태를 누가 관리하는지 모호해지고 나중에 수정 시 실수할 위험이 있다
- ex) PlayerCharacter::Evade()에서 회피 도중에 다른 상태로 바꾸고 싶어서 코드를 수정했는데, 노티파이에서도 관리하고 있었던 것을 깜빡 잊어서 상태 변경이 정상적으로 안되는 실수
[ 5. 캐릭터 블루프린트에서 회피 애니메이션 몽타주 등록 ]
- 애니메이션 몽타주를 PlayerCharacter 클래스에 레퍼런스로 직접 등록하지 않고 굳이 블루프린트에서 등록하도록 한 이유
- 혹시나 적 캐릭터에서도 Evade 기능을 사용할 수도 있으므로
- 만약에 PlayerCharacter 클래스에 레퍼런스로 직접 등록하게 되면 캐릭터가 달라지는 경우(다른 플레이어 클래스 / 적 클래스가 사용하는 경우)에 동기화가 힘들어진다
- 블루프린트에서 애니메이션 몽타주를 등록하게 함으로써, 블루프린트는 단순한 데이터 주입기 역할만 하게 되고 전체 회피 로직은 C++ 코드로 작동하게 된다
[ 6. StateSyncComponent 생성 ]
- StateSyncComponent를 생성하는 이유
- 만약에 StateSyncComponent를 사용하여 CharacterState와 AnimState를 동기화시켜주지 않는다면, FSM 상태 전환만 되고 AnimState에서의 상태 전환은 되지 않아서 애니메이션 재생이 정상적으로 되지 않는다
- 이를 해결하기 위해 AnimNotify에서 CharacterState뿐만이 아니라 AnimState도 명시적으로 설정해줘야 함
- StateSyncComponent를 사용하여 이러한 과정을 줄여줄 수 있다
- 이전에는 Evade 상태로 바꾸고 싶으면 FSM과 애니메이션에서 각각 SetState를 1번씩 써야 했다
- 지금 구조에서는 FSM에서만 바꾸면 끝
- 즉, 책임은 그대로 분리되어 있는데 "전달 책임"만 컴포넌트가 맡은 것이다
둘 다 상태를 관리하는건데 왜 인터페이스나 상태 통합 클래스 같은 걸로 묶어서 한 곳에서 관리하지 않는 것인가? | |
CharacterState | - 게임 로직용 상태 - FSM 기반으로 전투, 회피, 피격, 죽음 등 "로직 흐름"을 제어 |
AnimState | - 애니메이션 상태 - BlendSpace, Blend Logic, AnimGraph에서의 전이를 제어 |
통합하지 않고 분리하여 사용하는 이유 |
1. 업데이트 주기가 다름 - CharacterState는 입력/이벤트 기반(즉시 반응) - AnimState는 Tick마다 애니메이션 상태 업데이트(Delay가 존재) 2. 책임이 다름 - FSM은 게임 로직, AnimState는 시각을 제어함 - 서로 변경 사유가 다름(SRP 위배) 3. 전이 조건 기준이 다름 - FSM은 키 입력/스킬이 조건이다 - AnimState는 속도, 가속도, 낙하 여부 등이 조건이다 4. 유연성 제한 방지 - ex) 피격 FSM 상태는 Hit이지만 AnimState는 Landing일 수 있다 - 그러므로 분리가 필요 5. 서로 다른 캐릭터에게 재활용 가능 - AnimState만 다른 캐릭터에게도 FSM은 공유가 가능함(반대의 경우도 마찬가지) |
그런데 상태 연동 컴포넌트를 만들면 결국 위의 내용들과 SRP를 위배하는 것 아닌가? | |
- StateSyncComponent는 "통합"이 아니라 "동기화(연결 고리)"임 - 그러므로 CharacterState와 AnimState를 하나로 합쳤다고 볼 수 없음 - 동기화를 했다는 것은 서로 독립적으로 존재하지만 의도된 시점에 상태만 맞춰준다는 것이다 - 그러므로 상호 침범이 아니라 중간 메신저 역할을 한다고 보면 됨 |
- BaseCharacter 클래스의 CharacterState와 BA_Common의 AnimState를 동기화해주는 컴포넌트 작성
[ 7. BaseCharacter 클래스에 StateSyncComponent 부착 및 상태 동기화 코드 추가 ]
그런데 StateSyncComponent를 왜 BaseCharacter 클래스에 부착하는건지? | |
- FSM 상태는 CharacterBase가 관리하고 있음 : SetCharacterState(), GetCharacterState()의 원본 위치임 - 애님 인스턴스는 로직을 알 필요가 없음 : 애니메이션은 데이터 소비자일 뿐이다 - CharacterBase는 FSM과 AnimInstance를 둘 다 참조 가능 : GetMesh()->GetAnimInstance() - Enemy, Player 모두 FSM을 사용하므로 재사용이 쉬움 : CharacterBase에 붙이면 모든 파생 클래스에 자동 적용 |
|
PlayerCharacter에서 사용한다면 | - FSM이 PlayerCharacter에만 있다고 가정하는 구조가 되어 재사용이 불가능해짐 - EnemyCharacter가 회피나 피격 상태를 가지게 되면 또 중복 구현을 해야됨 |
AnimInstance에서 사용한다면 | - 애님 인스턴스는 로직 계층을 참조하면 안됨 : SRP 위반 - CharacterState는 애니메이션이 직접 결정할 일이 아님 - 유지보수 시에 애니메이션 쪽 코드까지 뒤져야 함 |
- BaseCharacter 클래스에 StateSyncComponent 부착 후, SetState 함수 내에서 CharacterState를 설정할 때, 동일한 AnimState가 있다면 동기화시킴
'UE Projects > Sigh Of Jay 개발일지 (심화)' 카테고리의 다른 글
10 - 1. EnemyCharacter 클래스 구현 (0) | 2025.06.12 |
---|---|
9 - 3. FSM 기반으로 Attack, Evade, Jump 구조 변경 (0) | 2025.06.08 |
9 - 1. 논리 상태 머신 구성 및 캐릭터 피격 모션 추가 (1) | 2025.05.28 |
8. 플레이어 좌/우/뒤 걷기 애니메이션 추가 (0) | 2025.05.22 |
7. 콜리젼 프로필 수정 (0) | 2025.05.21 |