Loading the catalog…
Loading the catalog…
📝 플레이어 애니메이션 기본 이동과 Sprint와 Dodge와 Jump까지 플레이어의 이동 계열 State를 정리한 뒤 전투 중 필요한 애니메이션과 상태를 연결했다. 처음에는 공격 애니메이션을 붙이는 것이 목표였다. 하지만 공격을 구현하면서 Combat Idle과 Combat Run이 필요해졌고 공격 중 Jump와 Dodge처럼 기존 State와의 연결도 다시 확인해야 했다. 이후에는 Hit과 Stun과 KnockDown과 Die와 Rebirth와 Interact까지 순서대로 구현했다. 기능의 종류는 서로 달랐지만 작업하면서 계속 확인한 기준은 같았다. 애니메이션이 하나 추가됐다는 이유만으로 새로운 State를 만들지 않는다. 해당 기능으로 인해 실제 플레이어의 행동 규칙이 달라지는지를 기준으로 State 사용 여부를 결정한다. 1. 공격 State와 기본 콤보 구현 1-1. 공격 로직을 PlayerAttackState에서 제어 실제 공격 판정은 PlayerAttack 이 담당한다. 하지만 공격 중 어떤 행동이 가능한지와 현재 몇 번째 콤보인지 같은 행동 규칙까지 PlayerAttack 에서 관리하고 싶지는 않았다. 현재 구조에서는 역할을 다음과 같이 나눴다. PlayerAttackState → 공격 상태 진입 / 유지 / 종료 → 콤보 순서와 입력 예약 → Jump / Dodge 등 다른 State로의 전환 판단 PlayerAttack → 실제 공격 범위 검사 → 적에게 데미지 전달 PlayerAnimation → 공격 Trigger와 Animator Parameter 전달 공격 State에 진입하면 첫 번째 공격부터 시작한다. public override void Enter() { comboIndex = 0; StartAttack(); } 실제 공격 시작 시에는 Timer를 초기화하고 전투 상태를 활성화한 뒤 공격 애니메이션과 공격 판정을 실행한다. private void StartAttack() { attackTimer = 0.0f; nextAttack = false; stateMachine.PlayerAnimation.SetCombat(true); stateMachine.PlayerAnimation.SetAttack(); stateMachine.PlayerAttack.Attack(); Debug.Log($"comboIndex : {comboIndex}"); } PlayerAttackState 가 공격 범위나 데미지 계산 방식까지 알 필요는 없다고 판단했다. State에서는 공격이 시작됐다는 사실만 전달하고 실제 공격 판정은 PlayerAttack.Attack() 에서 처리한다. 이렇게 역할을 나누면 공격 범위나 데미지 계산 방식이 변경돼도 State의 행동 전환 규칙과 직접 섞이지 않는다. 1-2. 공격 입력을 바로 실행하지 않고 다음 콤보로 예약 공격 애니메이션은 총 네 단계로 이어지는 구조였다. 공격 버튼을 누를 때마다 바로 다음 애니메이션을 실행하면 첫 공격이 충분히 재생되기 전에 다음 공격으로 넘어갈 수 있었다. 그래서 공격 중 다시 공격 입력이 들어오면 바로 다음 공격을 실행하지 않고 우선 입력을 예약한다. if (stateMachine.PlayerInput.IsAttack) { nextAttack = true; } 일정 시간이 지난 뒤 예약된 입력이 있다면 다음 콤보를 시작한다. if (nextAttack && comboIndex < 3 && attackTimer >= stateMachine.PlayerAttack.ComboInterval) { comboIndex++; StartAttack(); return; } 현재 ComboInterval 은 0.5초 를 사용한다. 공격 입력은 WasPressedThisFrame() 을 사용한다. 따라서 버튼을 계속 누르고 있는 것만으로 반복 공격이 발생하지 않는다. 공격 중 다시 버튼을 눌렀을 때 다음 공격이 예약되는 구조다. 추가 입력이 없다면 현재 공격 애니메이션의 재생 시간이 끝난 뒤 MoveState 로 복귀한다. if (attackTimer >= stateMachine.PlayerAttack.AttackDuration) { stateMachine.ChangeState(PlayerStateType.Move); } 현재 사용하는 공격 애니메이션의 길이가 모두 2.333초 였기 때문에 AttackDuration 도 같은 값을 사용했다. 현재 단계에서는 공격 State가 종료되는 시점을 맞추기 위한 값으로 사용했다. 2. 전투 상태와 Combat Idle / Run 2-1. 공격 후 바로 일반 Idle로 돌아가지 않도록 IsCombat 추가 공격 애니메이션만 연결했을 때 한 가지 어색한 점이 있었다. 공격이 끝나는 순간 바로 평상시 Idle과 Run으로 돌아가면 방금 전투를 수행했다는 느낌이 사라졌다. 그래서 Animator에 IsCombat 을 추가해 평상시 이동과 전투 중 이동을 구분했다. private static readonly int IsCombatHash = Animator.StringToHash("IsCombat"); public bool IsCombat => animator.GetBool(IsCombatHash); 공격을 시작하면 전투 상태를 활성화한다. public void SetCombat(bool isCombat) { animator.SetBool(IsCombatHash, isCombat); if (isCombat) { cIdleTimer = 0.0f; isCIdleTimer = false; } } Animator에서는 평상시 Normal Idle / Run 과 별도로 Combat Sub-State Machine을 구성했다. 일반 Idle / Run ↓ IsCombat = true ↓ Combat ├─ CIdle ├─ CRun └─ Attack 이 구조를 통해 공격이 끝난 뒤에도 일정 시간 동안 전투 자세를 유지할 수 있게 했다. 2-2. 전투 이동에서도 기존 이동 방향 값을 재사용 Combat 상태에서도 타겟을 바라보면서 전후좌우로 움직일 수 있어야 했다. 단순한 전진 Run만으로는 부족했다. 그래서 기존 이동에서 사용하던 Parameter를 Combat 이동에서도 그대로 사용했다. MoveMagnitude MoveX MoveY Combat의 CRun 은 2D Simple Directional Blend Tree로 구성했다. 이동 방향은 플레이어 Local 기준으로 변환한 뒤 Animator에 전달한다. Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); 평상시에는 타겟이 없고 전투 상태도 아니라면 전방 달리기 애니메이션을 사용한다. 타겟이 있거나 Combat 상태라면 실제 Local 이동 방향을 전달한다. if (currentTarget == null && !stateMachine.PlayerAnimation.IsCombat) { stateMachine.PlayerAnimation.SetMoveDir(0.0f, 1.0f); } else { Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); } 일반 이동과 전투 이동을 완전히 별개의 이동 시스템으로 만들지 않았다. 기존 이동 방향 계산 결과를 Animator 표현에 맞게 재사용했다. 2-3. 공격 종료와 전투 상태 종료를 분리 처음에는 공격 애니메이션이 끝나는 순간 IsCombat 을 바로 false 로 변경하는 방법도 생각했다. 하지만 이렇게 하면 공격 하나가 끝날 때마다 일반 Idle로 복귀한다. 이후 다시 공격하면 Combat으로 재진입해야 한다. 이 흐름은 원하는 전투 표현과 달랐다. 그래서 공격 종료와 전투 상태 종료를 서로 분리했다. PlayerAnimation 에서 Combat Idle 유지 시간을 관리한다. public void StartCIdleTimer() { cIdleTimer = 0.0f; isCIdleTimer = true; } Timer가 일정 시간을 넘으면 Combat 상태를 종료한다. if (!isCIdleTimer) return; cIdleTimer += Time.deltaTime; if (cIdleTimer >= cIdleDuration) { isCIdleTimer = false; SetCombat(false); } 현재 cIdleDuration 은 3초 를 사용한다. 이 Timer는 공격이 정상적으로 완료됐을 때만 필요한 것이 아니었다. 공격 중 Jump나 Dodge로 다른 State에 진입한 경우에도 공격 상태는 종료된다. 그래서 PlayerAttackState.Exit() 에서 Timer를 시작하도록 했다. public override void Exit() { //공격 상태 종료 후 전투 대기 시간 시작 stateMachine.PlayerAnimation.StartCIdleTimer(); } State가 어떤 이유로 종료되더라도 Exit() 은 실행된다. 따라서 공격이 종료된 경로와 관계없이 같은 위치에서 Combat 종료 규칙을 적용할 수 있었다. 3. 정지 공격과 이동 공격 분리 3-1. 공격 중 이동을 허용하면서 발생한 문제 현재 전투에서는 공격 중에도 이동할 수 있어야 했다. 그래서 PlayerAttackState.Update() 에서도 이동을 계속 처리한다. //공격하면서 움직일 수 있어야함 stateMachine.PlayerMovement.Move(false); 공격 방향은 유지하고 싶었기 때문에 일반 이동처럼 캐릭터를 이동 방향으로 회전시키지 않는다. 그래서 false 를 전달했다. 공격 중에도 이동 애니메이션을 갱신하기 위해 이동 Parameter 역시 계속 전달한다. stateMachine.PlayerAnimation.SetMoveMagnitude(stateMachine.PlayerInput.MoveAction.magnitude); Vector3 moveDir = stateMachine.PlayerMovement.GetMoveDir(); Vector3 localMoveDir = stateMachine.transform.InverseTransformDirection(moveDir); stateMachine.PlayerAnimation.SetMoveDir(localMoveDir.x, localMoveDir.z); 하지만 여기서 Animator 구조 문제가 발생했다. 정지 상태에서 사용하는 공격은 전신 공격 애니메이션이다. 이 상태에서 실제 캐릭터 위치만 이동시키면 다리는 공격 자세에 고정된 채 캐릭터가 미끄러지는 것처럼 보였다. 3-2. 이동 공격을 UpperBody Layer로 분리 정지 공격과 이동 공격은 필요한 표현이 달랐다. 정지 공격 → 전신 공격 모션 이동 공격 → 하체는 이동 → 상체만 공격 그래서 이동 공격은 UpperBody Layer에서 처리했다. Weight : 1 Blending : Override Avatar Mask : 상체 Generic Rig의 Transform Mask에서는 골반과 다리를 제외했다. Spine 위쪽과 팔과 목과 머리 계열을 포함시켰다. UpperBody Layer의 기본 State는 Motion이 없는 Empty State로 구성했다. 이동 중 공격 입력이 들어오면 공격 A부터 D까지 순서대로 재생된다. Base Layer → CRun 8방향 이동 UpperBody Layer → Attack A / B / C / D 이렇게 구성하면서 이동 중에는 Base Layer의 하체 이동을 유지할 수 있었다. 상체만 공격 애니메이션으로 덮어씌울 수 있었다. 반대로 MoveMagnitude < 0.1 인 정지 상태에서는 Combat Sub-State Machine 내부의 전신 공격을 사용한다. 3-3. 트러블슈팅 - 정지 공격 도중 이동하면 하체가 공격 자세로 남는 문제 정지 상태에서 공격을 시작한 뒤 공격 애니메이션 도중 이동 입력을 넣어봤다. 캐릭터의 실제 위치는 움직였지만 하체는 계속 전신 공격 애니메이션을 유지했다. 처음부터 이동 중에 공격을 시작하면 UpperBody Layer를 사용하기 때문에 문제가 없었다. 문제는 다음 순서에서 발생했다. 정지 상태에서 전신 공격 시작 → 공격 중 이동 입력 이미 Base Layer가 전신 공격 State에 진입한 상태였다. 그래서 공격이 끝나기 전까지 하체가 이동 애니메이션으로 전환되지 않았다. 이 문제를 해결하기 위해 하체만 별도로 제어하는 LowerBody Layer도 실험했다. 하지만 실제로 적용해보니 8방향 이동과 공격 상태가 겹치면서 애니메이션이 부자연스럽게 섞였다. 특히 대각선 이동에서 문제가 크게 보였다. LowerBody Layer를 유지하지 않은 이유 현재 문제 하나를 해결하기 위해 Animator Layer 구조를 더 복잡하게 확장하면 다른 이동 애니메이션까지 함께 조정해야 했다. 현재 작업 범위에서는 얻는 효과보다 구조 복잡도가 더 커질 수 있다고 판단했다. 그래서 LowerBody Layer는 Weight를 0 으로 두고 실험 상태로 남겨뒀다. 현재는 다음 구조까지만 유지했다. 정지 상태에서 공격 시작 → 전신 공격 이동 중 공격 시작 → UpperBody 공격 + 하체 이동 정지 공격 도중 이동을 시작하는 경우의 하체 전환은 이후 폴리싱 단계에서 다시 검토하기로 했다. 이번 작업에서는 문제가 발견됐다는 이유만으로 새로운 시스템을 끝까지 확장하지 않았다. 현재 플레이에 미치는 영향과 작업 우선순위를 기준으로 구현 범위를 결정했다. 4. 공격 중 Jump / Dodge / Sprint 연결 4-1. AttackState에서 Jump와 Dodge 허용 AttackState에 진입했다는 이유로 기존 Jump와 Dodge를 모두 막으면 전투 조작이 지나치게 답답해진다. 현재 전투 규칙에서는 공격 중 Jump와 Dodge를 허용하기로 했다. 그래서 PlayerAttackState.Update() 의 앞부분에서 먼저 입력을 확인한다. //공격 중 점프 if (stateMachine.PlayerInput.IsJump && stateMachine.PlayerMovement.IsGrounded) { stateMachine.PlayerAnimation.SetJump(); stateMachine.ChangeJumpState(false); return; } //공격 중 회피 if (stateMachine.PlayerInput.IsSprint && stateMachine.PlayerMovement.IsGrounded) { stateMachine.ChangeState(PlayerStateType.Dodge); return; } 여기서 return 을 사용하는 이유는 State가 변경된 프레임에 기존 AttackState의 나머지 로직이 계속 실행되지 않도록 하기 위해서다. Gameplay State만 변경한다고 UpperBody 공격 애니메이션이 자동으로 종료되는 것은 아니었다. 그래서 Animator에서도 공격 중 Jump와 Dodge가 들어오면 Empty State로 빠질 수 있도록 Transition을 추가했다. UpperBody Attack → Empty Condition : Jump 또는 Dodge Has Exit Time : OFF Gameplay State의 취소 경로와 Animator의 취소 경로를 함께 맞췄다. 4-2. Sprint 중 공격 후 Combat으로 연결 Sprint 중 공격 입력도 별도로 처리했다. PlayerSprintState 에서 공격 입력을 확인한 뒤 AttackState로 전환한다. if (stateMachine.PlayerInput.IsAttack) { stateMachine.ChangeState(PlayerStateType.Attack); return; } Sprint 중 공격을 시작한 뒤 공격이 끝났다고 다시 Sprint로 돌아가는 흐름은 원하는 전투 방식과 달랐다. 공격을 시작하는 순간 IsCombat 이 활성화된다. 따라서 공격 종료 후에는 Combat Idle 또는 Combat Run을 사용하는 것이 자연스럽다고 판단했다. Sprint → Attack → IsCombat = true → 공격 종료 → MoveState → Combat Idle / Run Gameplay State는 기존 MoveState 를 사용한다. Animator가 IsCombat 값을 확인해서 실제 화면에는 Combat 이동을 재생한다. 전투용 MoveState를 별도로 만들지 않고 기존 MoveState를 재사용했다. 4-3. Combat 중 Dodge와 Jump도 기존 State 재사용 Combat 상태라는 이유만으로 DodgeState 와 JumpState 를 새로 만들지 않았다. 일반 상태와 Combat 상태에서 실제 Gameplay 행동 규칙은 크게 달라지지 않았다. 달라지는 부분은 재생해야 하는 애니메이션이었다. 그래서 Gameplay State는 기존 State를 유지했다. Animator에서 IsCombat 을 기준으로 애니메이션 경로만 분리했다. Combat Jump는 다음과 같은 흐름으로 구성했다. Combat Idle / Run → Combat Jump Start → Loop → Land 또는 LandRun 공격 중 JumpState로 전환된 경우에도 Combat 상태가 유지되기 때문에 Combat용 Jump 애니메이션 경로를 사용할 수 있다. Gameplay 행동 규칙이 같다면 애니메이션 종류가 다르다는 이유만으로 State를 추가하지 않았다. State는 그대로 유지하고 Animator 표현을 분기하는 방식이 현재 구조에 더 적합하다고 판단했다. 5. 일반 Hit과 4방향 피격 5-1. 일반 Hit을 PlayerState로 만들지 않은 이유 피격 애니메이션을 추가하면서 PlayerHitState 같은 별도 State가 필요한지도 확인했다. 일반 Hit과 Stun과 KnockDown을 비교해보니 행동 규칙이 달랐다. 일반 Hit은 HP가 감소하고 피격 모션이 재생된다. 하지만 플레이어의 행동을 일정 시간 강제로 제한하지는 않는다. 반면 Stun과 KnockDown은 현재 행동을 중단시킨다. 일정 시간 동안 입력 규칙도 변경된다. 그래서 다음 기준으로 나눴다. 일반 Hit → Gameplay State 변경 없음 Stun / KnockDown → Gameplay State 변경 State Pattern을 피격 종류별로 하나씩 만드는 것이 아니라 행동 규칙을 변경할 필요가 있는 경우에만 사용하기로 했다. 5-2. OnDamaged와 직접 피격 이벤트 분리 기존 PlayerHealth 에는 이미 OnDamaged 이벤트가 있었다. 이 이벤트는 PlayerMovement 에서 데미지를 받은 뒤 일정 시간 이동 속도를 감소시키는 기능에 사용하고 있었다. private void OnEnable() { playerHealth.OnDamaged += DamageSlow; } 그런데 Hit 애니메이션까지 OnDamaged 에 연결하면 문제가 생긴다. 현재 화상 데미지는 Coroutine에서 일정 주기마다 TakeDamage() 를 호출한다. 따라서 OnDamaged 에 Hit 애니메이션을 연결하면 화상 데미지가 발생할 때마다 캐릭터가 피격 애니메이션을 재생하게 된다. 기존 이벤트의 의미는 유지하면서 직접 피격만 따로 구분하기 위해 OnHit 을 추가했다. public event Action OnDamaged; public event Action<Vector3> OnHit; 공격 위치가 존재하는 직접 피격용 TakeDamage() 도 별도로 만들었다. //공격 위치가 있는 직접 피격 public void TakeDamage(int damage, Vector3 hitPosition) { TakeDamage(damage
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
[Unity/개인프로젝트] #7 플레이어 애니메이션 (5). 📝 플레이어 애니메이션 기본 이동과 Sprint와 Dodge와 Jump까지 플레이어의 이동 계열 State를 정리한 뒤 전투 중 필요한 애니메이션과 상태를 연결했다. 처음에는 공격 애니메이션을 붙이는 것이 목표였다. 하지만 공격을 구현하면서 Combat Idle과 Combat Run이 필요해졌고 공격 중 Jump와 Dodge처럼 기존 State와의 연결도 다시 확인해야 했다. 이후에는 Hit과 Stun과 KnockDown과 Die와 Rebirth와 Interact까지 순서대로 구현했다. 기능의 종류는 서로 달랐지만 작업하면서 계속 확인한 기준은 같았다. 애니메이션이 하나 추가됐다는 이유만으로 새로운 State를 만들지 않는다. 해당 기능으로…
Open source