Загружаем каталог…
Загружаем каталог…
📝 플레이어 애니메이션 리팩토링 이번 작업에서는 플레이어 애니메이션 구현을 마무리한 뒤 지금까지 작성한 플레이어 코드를 전체적으로 다시 점검했다. 기능 자체는 대부분 정상적으로 동작하고 있었다. 하지만 기능을 하나씩 추가하면서 애니메이션 길이를 코드에서 직접 관리하는 부분이 생겼다. PlayerAnimation 이 Combat 상태와 종료 시간까지 관리하는 등 각 클래스의 책임도 조금씩 섞이기 시작했다. 몬스터와 실제 전투 로직을 연결하기 전에 플레이어 구조를 한 번 정리하는 편이 좋다고 판단했다. 이번 리팩토링에서는 다음 세 가지를 기준으로 잡았다. 리팩토링 기준 Gameplay 상태와 행동 규칙은 C# 코드에서 관리한다. Animator는 Gameplay 결과를 화면에 표현하는 역할에 집중한다. 애니메이션 Clip 길이가 변경돼도 Gameplay 코드를 함께 수정하지 않는 구조를 만든다. 이번 작업에서는 플레이어가 사용하는 10개의 State와 주요 플레이어 컴포넌트를 다시 확인했다. 애니메이션 Clip 길이를 코드에 직접 입력해서 사용하던 6개의 의존성도 제거했다. 1. 플레이어 애니메이션 구현 마무리 1-1. 플레이어 행동별 애니메이션 연결 현재 플레이어는 다음 행동에 대한 애니메이션 연결을 완료했다. 이동 전투 이동 질주 질주 정지 일반 점프 질주 점프 정지 착지 이동 착지 방향 회피 기본 공격 방향 피격 Stun KnockDown Die Rebirth Interact Gameplay State는 총 10개를 사용한다. Move Jump Dodge Sprint Attack Stun KnockDown Die Rebirth Interact 모든 기능을 별도의 State로 만든 것은 아니다. 처음에는 피격도 하나의 기능이라고 생각해서 PlayerHitState 를 만들었다. 하지만 일반 피격과 Stun과 KnockDown의 실제 행동 규칙을 비교하면서 같은 피격이라도 State 사용 여부가 달라져야 한다고 판단했다. 일반 피격은 HP가 감소해도 현재 행동을 계속할 수 있다. 그래서 별도의 State로 전환하지 않는다. 현재 State가 MoveState 일 때 피격 애니메이션만 재생하도록 구성했다. 반면 Stun과 KnockDown은 현재 행동을 중단시킨다. 일정 시간 동안 행동도 제한한다. 이 경우에는 플레이어의 행동 규칙 자체가 달라지기 때문에 각각 별도의 State로 유지했다. State Pattern을 기능의 개수에 맞춰 만드는 것이 아니라 실제 행동 규칙이 달라지는 시점을 기준으로 사용하도록 정리했다. 1-2. 점프 애니메이션 시작 책임을 JumpState로 이동 기존에는 점프로 전환하기 전 State에서 점프 애니메이션까지 실행하는 부분이 있었다. 이 구조에서는 Move에서 점프하는 경우와 Sprint에서 점프하는 경우마다 이전 State가 Jump의 애니메이션 구현까지 알아야 했다. 점프 상태가 어떤 애니메이션으로 시작될지는 PlayerJumpState 가 결정하는 편이 자연스럽다고 판단했다. 현재는 JumpState로 변경할 때 일반 점프인지 질주 점프인지만 전달한다. stateMachine.ChangeJumpState(false); stateMachine.ChangeJumpState(true); 실제 애니메이션 선택은 PlayerJumpState.Enter() 에서 처리한다. public override void Enter() { if (isSprintJump) { stateMachine.PlayerAnimation.SetSprintJump(); } else { stateMachine.PlayerAnimation.SetJump(); } stateMachine.PlayerMovement.Jump(); } 착지 역시 애니메이션 시간을 기준으로 판단하지 않는다. 실제 CharacterController 의 Ground 상태를 기준으로 착지를 판단한다. if (stateMachine.PlayerMovement.IsGrounded) { bool isMove = stateMachine.PlayerInput.MoveAction.sqrMagnitude > 0.001f; if (isMove) { stateMachine.PlayerAnimation.SetJumpLandRun(); } else { stateMachine.PlayerAnimation.SetJumpLand(); } } 이를 통해 점프의 실제 시작과 종료 기준은 Gameplay 코드가 관리하도록 했다. Animator는 그 결과에 맞는 애니메이션을 표현한다. 1-3. 방향 회피와 Root Motion 회피는 입력 방향에 따라 8방향 애니메이션을 사용한다. 현재 이동 입력은 카메라 기준 월드 방향으로 계산된다. Animator에서는 캐릭터 기준 방향이 필요하기 때문에 회피를 시작할 때 Local 방향으로 변환한다. Vector3 localDodgeDir = stateMachine.transform.InverseTransformDirection(dodgeDir); 이 값을 이용해서 전방과 후방과 좌우와 대각선 방향에 맞는 회피 애니메이션을 선택한다. 회피 이동에는 Root Motion을 사용한다. 하지만 Animator의 Root Motion을 Transform에 바로 적용하지 않는다. PlayerAnimation 이 Animator.deltaPosition 을 받아서 PlayerMovement 로 전달한다. private void OnAnimatorMove() { if (!useRootMotion) return; playerMovement.MoveRootMotion(animator.deltaPosition); } 최종 이동은 기존 이동과 동일하게 CharacterController.Move() 를 사용한다. 일반 이동과 Root Motion 이동 모두 최종적인 이동 처리를 CharacterController 를 통해 수행하도록 유지했다. 2. 애니메이션 길이에 의존하던 코드 제거 2-1. 문제 - 애니메이션 길이를 State에서 직접 관리 기존에는 일부 State가 애니메이션 종료 시점을 초 단위 값으로 직접 관리하고 있었다. 대표적으로 다음 값들이 존재했다. Stun 2.0초 Rebirth 2.458초 Interact End 0.958초 KnockDown End 1.375초 Attack 2.333초 Dodge 회피 애니메이션 길이 처음 구현할 때는 간단한 방법이었다. 애니메이션 길이를 확인한 뒤 같은 시간을 State에 입력하면 해당 시간이 지난 후 다음 State로 이동시킬 수 있었다. 하지만 플레이어 애니메이션 구조를 다시 확인하면서 문제가 보였다. 이 숫자들은 Gameplay 규칙이 아니었다. 현재 사용 중인 애니메이션 Clip이 가지고 있는 재생 시간이었다. 애니메이션을 다른 Clip으로 변경하거나 재생 속도를 수정하면 State 코드의 시간도 함께 수정해야 했다. Animator가 이미 알고 있는 애니메이션 종료 시점을 C#에서도 다시 관리하고 있던 셈이었다. 2-2. Gameplay 시간과 Animation 시간을 구분 이번 리팩토링에서는 모든 시간값을 제거하지 않았다. 숫자가 있다는 이유만으로 제거하는 것이 아니라 해당 값이 무엇을 의미하는지 먼저 구분했다. 현재 유지한 대표적인 값은 다음과 같다. Interact 3.0초 → 실제 상호작용 유지시간 KnockDown 2.0초 → 실제 KnockDown 유지시간 DodgeMoveCancelTime 0.35초 → 회피 후 이동으로 취소할 수 있는 시점 ComboInterval 0.5초 → 다음 콤보로 넘어가는 전투 템포 이 값들은 실제 Gameplay 규칙이나 플레이 조작감을 조절하기 위한 값이다. 반대로 애니메이션 자체가 몇 초인지를 나타내는 값은 제거했다. 같은 시간값이라도 숫자라는 이유만으로 동일하게 취급하지 않았다. 실제 플레이 규칙을 의미하는지부터 확인한 뒤 코드에 남길지 결정했다. 2-3. 공통 AnimationEnd 구조 추가 애니메이션 종료를 확인하기 위해 각 State가 Animator의 현재 State를 직접 조회하는 방법도 사용할 수 있었다. 하지만 그렇게 하면 Gameplay 코드가 Animator Controller 내부 State 이름과 구조를 알아야 한다. 이번 리팩토링에서 정한 방향과 맞지 않았다. 대신 Animation Event를 이용해서 애니메이션이 끝났다는 사실만 코드로 전달하도록 했다. PlayerAnimation 에 공통 이벤트를 추가했다. public event Action OnAnimationEnd; //애니메이션 종료 public void AnimationEnd() { OnAnimationEnd?.Invoke(); } PlayerStateMachine 은 해당 이벤트를 받아 현재 State로 전달한다. //애니메이션 종료 private void AnimationEnd() { currentState?.OnAnimationEnd(); } 모든 State가 필요한 경우 사용할 수 있도록 PlayerState 에도 메서드를 추가했다. public virtual void OnAnimationEnd() { } 애니메이션이 끝난 뒤 실제로 어떤 행동을 해야 하는지는 현재 State가 판단한다. 예를 들어 Rebirth는 애니메이션 종료 후 MoveState로 복귀한다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } StateMachine은 Rebirth 애니메이션이 몇 초인지 알 필요가 없다. Animator도 MoveState가 무엇인지 알 필요가 없다. Animation Event는 애니메이션이 종료됐다는 사실만 전달한다. 2-4. AnimationEnd 적용 결과 이번 작업에서 다음 애니메이션 길이 의존성을 제거했다. Stun → 2.0초 타이머 제거 Rebirth → 2.458초 타이머 제거 Interact End → 0.958초 타이머 제거 KnockDown End → 1.375초 타이머 제거 Attack → 2.333초 AttackDuration 제거 Dodge → DodgeDuration 제거 총 6개의 애니메이션 길이 기반 로직을 제거했다. 애니메이션 Clip이 변경돼도 해당 State의 종료 시간을 다시 수정할 필요가 없도록 정리했다. 3. State별 종료 규칙 정리 3-1. Stun Stun은 기존에 애니메이션 길이만큼 시간을 계산한 뒤 MoveState로 복귀했다. 현재는 Stun 애니메이션의 마지막에 AnimationEnd Event를 배치했다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } Stun 상태에서는 회피를 통한 탈출도 가능하다. if (stateMachine.PlayerInput.IsSprint && stateMachine.PlayerMovement.IsGrounded) { stateMachine.ChangeState(PlayerStateType.Dodge); return; } Stun 중에는 무기를 강제로 표시한다. Stun 애니메이션에 맞는 전용 위치와 회전값도 적용한다. 어떤 경로로 State를 빠져나가더라도 원래 무기 상태로 돌아가야 한다. 그래서 원복 처리는 Exit() 에 배치했다. public override void Exit() { stateMachine.PlayerAnimation.ResetStunWeapon(); } 정상적으로 Stun 애니메이션이 끝난 경우에도 실행된다. 회피로 중간에 State를 벗어난 경우에도 동일하게 실행된다. 3-2. KnockDown KnockDown에서는 2.0f 를 제거하지 않았다. 이 값은 KnockDown 애니메이션 길이가 아니다. 실제로 플레이어를 쓰러진 상태로 유지할 Gameplay 시간이다. if (!isKnockDownEnd && knockDownTimer >= 2.0f) { isKnockDownEnd = true; stateMachine.PlayerAnimation.SetKnockDownEnd(); } 반면 일어나는 End 애니메이션 길이를 재던 1.375f 는 제거했다. End 애니메이션이 끝나면 Animation Event가 발생한다. public override void OnAnimationEnd() { if (!isKnockDownEnd) return; stateMachine.ChangeState(PlayerStateType.Move); } isKnockDownEnd 를 함께 확인하는 이유도 있다. KnockDown의 시작 단계나 유지 단계에서 Animation Event가 발생하더라도 바로 MoveState로 빠져나가는 상황을 방지하기 위해서다. 3-3. Interact Interact 역시 실제 상호작용 시간과 애니메이션 시간을 분리했다. 3.0f 는 플레이어가 실제로 상호작용 상태를 유지하는 시간이다. 따라서 그대로 유지했다. if (!isInteractEnd && interactTimer >= 3.0f) { isInteractEnd = true; stateMachine.PlayerAnimation.SetInteractEnd(); } 기존에는 End 애니메이션 길이인 0.958f 까지 State에서 다시 계산했다. 현재는 해당 타이머를 제거했다. End 애니메이션의 AnimationEnd Event를 사용한다. public override void OnAnimationEnd() { if (!isInteractEnd) return; stateMachine.ChangeState(PlayerStateType.Move); } Interact 애니메이션은 Start와 Loop와 End로 나뉘어 있다. 실제 상호작용 전체가 종료되는 시점은 End이다. 그래서 AnimationEnd Event는 End Clip에만 적용했다. 3-4. Dodge Dodge에는 두 종류의 종료 조건이 존재한다. 첫 번째는 회피 중 이동 입력을 통해 조기에 다음 행동으로 넘어가는 경우다. 두 번째는 이동 입력 없이 회피 애니메이션 전체를 재생하는 경우다. 기존에는 두 번째 경우를 위해 DodgeDuration 을 사용했다. 이 값은 회피 애니메이션의 실제 길이를 확인해서 입력한 값이었다. 따라서 DodgeDuration 은 제거했다. 이동 입력을 통한 Cancel은 기존 DodgeMoveCancelTime 을 계속 사용한다. if (moveMagnitude > 0.1f && dodgeTimer >= stateMachine.PlayerMovement.DodgeMoveCancelTime) { if (stateMachine.IsCombat) { stateMachine.ChangeState(PlayerStateType.Move); } else { stateMachine.ChangeState(PlayerStateType.Sprint); } return; } 아무 입력이 없는 경우에는 8방향 Dodge Clip에 추가한 AnimationEnd Event가 MoveState 복귀를 담당한다. public override void OnAnimationEnd() { stateMachine.ChangeState(PlayerStateType.Move); } DodgeMoveCancelTime 은 실제 조작 규칙이라 유지했다. DodgeDuration 은 애니메이션 길이였기 때문에 제거했다. 같은 Dodge 관련 시간값이라도 의미에 따라 다르게 처리했다. 4. 기본 공격 콤보 구조 개선 4-1. 한 번 클릭과 Hold 공격을 동시에 지원 처음에는 공격 입력에 WasPressedThisFrame() 만 사용했다. public bool IsAttack => attackAction.WasPressedThisFrame(); 이 방식은 클릭할 때마다 한 번씩 공격하는 구조에는 적합했다. 하지만 마우스를 계속 누르고 있을 때도 기본 공격 콤보가 이어지도록 만들고 싶었다. 처음에는 IsAttack 자체를 IsPressed() 로 변경했다. 하지만 테스트해보니 한 번 짧게 클릭했을 때도 B 공격까지 이어지는 문제가 발생했다. 첫 공격을 시작하기 위해 누른 입력이 AttackState에 진입한 뒤에도 유지되고 있었다. 그 입력이 다음 콤보 입력으로 다시 사용되고 있었다. 그래서 클릭 입력과 Hold 입력을 분리했다. public bool IsAttack => attackAction.WasPressedThisFrame(); public bool IsAttackHeld => attackAction.IsPressed(); 현재 동작은 다음과 같다. 한 번 클릭 → A 공격만 실행 다시 클릭 → 다음 콤보 예약 계속 누르기 → A → B → C → A → B → C 반복 같은 마우스 버튼을 사용하더라도 클릭 순간과 입력 유지 상태는 다른 의미를 가진다. 그래서 코드에서도 두 입력을 따로 제공하도록 정리했다. 4-2. 트러블슈팅 - 마지막 콤보 이후 Idle이 끼는 문제 처음에는 기본 공격을 A부터 D까지 4단계로 구성했다. Hold 공격을 구현한 뒤 마지막 공격에서 첫 번째 공격으로 다시 돌아갈 때 잠깐 서 있는 자세가 보였다. 처음에는 Animator Transition을 의심했다. 마지막 공격에서 첫 공격으로 직접 Transition을 추가했다. Has Exit Time 도 확인했다. 마지막 공격에서 Idle로 빠지는 Transition의 Exit Time 도 확인했다. 하지만 테스트를 계속하면서 Animator 설정만의 문제는 아니라는 것을 확인했다. 당시 콤보 전환 조건은 현재 comboIndex 가 마지막 Index보다 작을 때만 다음 공격을 실행하는 구조였다. 이 조건에서는 마지막 공격이 시작된 뒤 Update() 에서 첫 공격으로 다시 넘어갈 수 없었다. 결국 마지막 애니메이션의 AnimationEnd 까지 기다린 뒤 첫 공격을 다시 시작하고 있었다. 그래서 앞선 공격들과 연결 타이밍이 다르게 느껴졌다. 해결 - 마지막 콤보에서도 같은 ComboInterval 사용 마지막 공격에서도 동일한 ComboInterval 을 기준으로 다음 공격을 실행할 수 있도록 변경했다. 마지막 인덱스에서는 다시 0으로 돌아간다. if ((nextAttack || stateMachine.PlayerInput.IsAttackHeld) && attackTimer >= stateMachine.PlayerAttack.ComboInterval) { if (comboIndex < 2) { comboIndex++; } else { comboIndex = 0; } StartAttack(); return; } 최종적으로는 현재 전투 템포에 더 잘 맞는 A부터 C까지의 3타 구조를 사용하기로 했다. A → B → C → A comboInterval = 0.5f 는 현재 플레이 테스트에서 콤보 연결 템포를 조절하는 값으로 사용하고 있다. 애니메이션 전체 길이를 의미하는 값은 아니다. 그래서 Gameplay 튜닝값으로 유지했다. 5. PlayerAnimation 책임 정리 5-1. Combat 상태 관리 위치 변경 기존에는 PlayerAnimation 내부에서 Combat 상태와 Combat 종료 타이머까지 관리하고 있었다. 하지만 Combat 상태는 단순한 시각 효과가 아니다. 플레이어 이동 애니메이션의 기준에도 사용된다. 무기 표시 여부에도 영향을 준다. 실제 Gameplay 상태에 가까운 정보라고 판단했다. 그래서 Combat 여부와 Combat 종료 대기 타이머를 PlayerStateMachine 으로 이동했다. 현재 PlayerStateMachine 이 다음 값을 관리한다. private float cIdleTimer; private bool isCIdleTimer; private bool isCombat; public bool IsCombat => isCombat; Combat 진입과 종료 역시 StateMachine이 결정한다. public void EnterCombat() { isCombat = true; cIdleTimer = 0.0f; isCIdleTimer = false; playerAnimation.SetCombat(true); } public void ExitCombat() { isCombat = false; cIdleTimer = 0.0f; is
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[Unity/개인프로젝트] #8 플레이어 애니메이션 리팩토링. 📝 플레이어 애니메이션 리팩토링 이번 작업에서는 플레이어 애니메이션 구현을 마무리한 뒤 지금까지 작성한 플레이어 코드를 전체적으로 다시 점검했다. 기능 자체는 대부분 정상적으로 동작하고 있었다. 하지만 기능을 하나씩 추가하면서 애니메이션 길이를 코드에서 직접 관리하는 부분이 생겼다. PlayerAnimation 이 Combat 상태와 종료 시간까지 관리하는 등 각 클래스의 책임도 조금씩 섞이기 시작했다. 몬스터와 실제 전투 로직을 연결하기 전에 플레이어 구조를 한 번 정리하는 편이 좋다고 판단했다. 이번 리팩토링에서는 다음 세 가지를 기준으로 잡았다. 리팩토링 기준 Gameplay 상태와 행동 규칙은 C# 코드에서 관리한다. Animator는…
Открыть источник