2026-05-20 구르기 연계
This commit is contained in:
@@ -21,6 +21,7 @@ public class PlayerController : MonoBehaviour,IDamageable
|
||||
[SerializeField] private float _moveSpeed = 5f; // 이동 속도 (units/sec)
|
||||
[SerializeField] private string _walkAnimationState = "Run"; // 걷기/달리기 애니메이션 State 이름
|
||||
private float _moveInputX = 0f; // 현재 X 입력값 (-1/0/1)
|
||||
private float _moveInputY = 0f; // 현재 Y 입력값 (-1/0/1) — 위/아래 방향 공격 판정용
|
||||
private string _activeBaseState; // 현재 재생 중인 locomotion State (중복 Play 방지용)
|
||||
private bool _isInActionAnimation; // 액션 애니메이션 재생 중인지 (locomotion 잠시 양보)
|
||||
|
||||
@@ -101,6 +102,8 @@ public class PlayerController : MonoBehaviour,IDamageable
|
||||
private CancellationTokenSource _actionVelocityCts; // 액션 속도 커브 토큰
|
||||
private bool _isAttackActive; // 공격 진행 중 (이동/페이싱 보조 잠금용)
|
||||
private bool _isMotionActive; // 모션 진행 중
|
||||
private bool _actionAllowsMovement; // 현재 액션이 진행 중 좌우 이동을 허용하는지 (CanMoveDuringAction)
|
||||
private bool _actionAllowsTurn; // 현재 액션이 진행 중 페이싱 변경을 허용하는지 (CanTurnDuringAction)
|
||||
private float _actionDirection = 1f; // 액션 시작 시 캐릭터 페이싱 (속도 X 부호)
|
||||
private ActionData _lastAttackGizmoData; // OnGUI 디버그 패널용 마지막 액션 참조
|
||||
[SerializeField] private float _hitGizmoFadeDuration = 0.5f; // hit 영역 Gizmo 페이드 시간
|
||||
@@ -247,11 +250,11 @@ private void FixedUpdate()
|
||||
ExecuteBufferedInputIfReady(); // 쿨다운 끝나면 저장된 입력 실행
|
||||
TickComboWindow(); // 콤보 윈도우 카운트다운
|
||||
|
||||
// 입력 잠금 + 액션 중이 아닐 때만 좌우 입력으로 velocity 갱신.
|
||||
if (!IsMovementLocked() && !IsActionActive())
|
||||
// 입력 잠금이 없고, (액션 중이 아니거나 액션이 이동을 허용할 때) 좌우 입력으로 velocity 갱신.
|
||||
if (!IsMovementLocked() && (!IsActionActive() || _actionAllowsMovement))
|
||||
_rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);
|
||||
|
||||
if (!IsFacingLocked() && !IsActionActive())
|
||||
if (!IsFacingLocked() && (!IsActionActive() || _actionAllowsTurn))
|
||||
UpdateFacingFromMoveInput();
|
||||
|
||||
//중력적용
|
||||
@@ -290,7 +293,8 @@ private void TickComboWindow()
|
||||
private void OnMoveInput(Vector2 value)
|
||||
{
|
||||
_moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
|
||||
if (_facingLockTimer <= 0f && !IsActionActive())
|
||||
_moveInputY = value.y == 0f ? 0f : Mathf.Sign(value.y);
|
||||
if (_facingLockTimer <= 0f && (!IsActionActive() || _actionAllowsTurn))
|
||||
UpdateFacingFromMoveInput();
|
||||
}
|
||||
|
||||
@@ -372,11 +376,15 @@ private void ExecuteComboInput(ComboInputType input)
|
||||
foreach (var transition in _currentNode.Transitions)
|
||||
{
|
||||
if (transition.Trigger != input) continue;
|
||||
if (transition.Next == null || transition.Next.Action == null) continue;
|
||||
if (!CanPerformAction(transition.Next.Action)) continue;
|
||||
if (transition.Next == null) continue;
|
||||
|
||||
// 방향키 입력에 맞는 변형 액션을 고른다 (없으면 노드의 기본 Action).
|
||||
ActionData nextAction = ResolveNodeAction(transition.Next);
|
||||
if (nextAction == null) continue;
|
||||
if (!CanPerformAction(nextAction)) continue;
|
||||
|
||||
ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration);
|
||||
PerformAttack(transition.Next.Action, transition.ForwardStep > 0f);
|
||||
PerformAttack(nextAction, transition.ForwardStep > 0f);
|
||||
_currentNode = transition.Next;
|
||||
_comboWindowTimer = transition.Next.ComboWindow;
|
||||
return;
|
||||
@@ -392,14 +400,49 @@ private void ExecuteComboInput(ComboInputType input)
|
||||
ComboInputType.Grab => _grabSmashRootNode,
|
||||
_ => null
|
||||
};
|
||||
if (root == null || root.Action == null) return;
|
||||
if (!CanPerformAction(root.Action)) return;
|
||||
if (root == null) return;
|
||||
|
||||
PerformAttack(root.Action);
|
||||
ActionData rootAction = ResolveNodeAction(root);
|
||||
if (rootAction == null) return;
|
||||
if (!CanPerformAction(rootAction)) return;
|
||||
|
||||
PerformAttack(rootAction);
|
||||
_currentNode = root;
|
||||
_comboWindowTimer = root.ComboWindow;
|
||||
}
|
||||
|
||||
// 현재 방향키 입력을 공격 방향(AttackDirection)으로 변환.
|
||||
// 대각선 입력은 위/아래를 우선한다 (점프 견제·다운 어택을 우선).
|
||||
// 좌우는 페이싱 기준으로 Forward(앞)/Back(뒤)로 구분.
|
||||
private AttackDirection GetAttackDirection()
|
||||
{
|
||||
if (_moveInputY > 0f) return AttackDirection.Up;
|
||||
if (_moveInputY < 0f) return AttackDirection.Down;
|
||||
if (_moveInputX == 0f) return AttackDirection.Neutral;
|
||||
|
||||
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||
return Mathf.Sign(_moveInputX) == facing ? AttackDirection.Forward : AttackDirection.Back;
|
||||
}
|
||||
|
||||
// 노드의 방향별 변형 중 현재 입력 방향과 일치하는 액션을 선택.
|
||||
// 일치하는 변형이 없거나 방향 입력이 없으면 노드의 기본 Action을 사용.
|
||||
private ActionData ResolveNodeAction(ComboNode node)
|
||||
{
|
||||
if (node == null) return null;
|
||||
|
||||
AttackDirection direction = GetAttackDirection();
|
||||
if (direction != AttackDirection.Neutral && node.DirectionalVariants != null)
|
||||
{
|
||||
foreach (var variant in node.DirectionalVariants)
|
||||
{
|
||||
if (variant != null && variant.Direction == direction && variant.Action != null)
|
||||
return variant.Action;
|
||||
}
|
||||
}
|
||||
|
||||
return node.Action;
|
||||
}
|
||||
|
||||
// 무장 시 무기 데이터의 AttackRootNode 사용 (비어있으면 기본 _punchRootNode 폴백).
|
||||
private ComboNode GetPunchRootNode()
|
||||
{
|
||||
@@ -412,11 +455,27 @@ private ComboNode GetPunchRootNode()
|
||||
private bool CanPerformAction(ActionData data)
|
||||
{
|
||||
if (data == null) return false;
|
||||
// 무기 조건 미충족이면 실행 불가 (콤보 트랜지션 무기별 분기 필터링).
|
||||
if (!IsWeaponRequirementMet(data)) return false;
|
||||
// 잡기 액션은 사정 거리 안에 적이 있어야만 실행. 없으면 입력 자체를 캔슬.
|
||||
if (data.IsGrab && FindGrabTarget(data) == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 현재 장착 무기가 액션의 RequiredWeapon 조건을 만족하는지.
|
||||
// 같은 입력의 콤보 트랜지션을 무기별로 깔아두면 이 검사로 맞는 것만 통과한다.
|
||||
private bool IsWeaponRequirementMet(ActionData data)
|
||||
{
|
||||
WeaponData equipped = _weaponInventory != null ? _weaponInventory.CurrentWeapon : null;
|
||||
return data.RequiredWeapon switch
|
||||
{
|
||||
WeaponRequirement.Unarmed => equipped == null,
|
||||
WeaponRequirement.Sword => equipped != null && equipped.Type == WeaponType.Sword,
|
||||
WeaponRequirement.Gun => equipped != null && equipped.Type == WeaponType.Gun,
|
||||
_ => true, // Any
|
||||
};
|
||||
}
|
||||
|
||||
private void ExecuteMotionNode(ComboNode root)
|
||||
{
|
||||
if (root == null || root.Action == null) return;
|
||||
@@ -466,6 +525,8 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
|
||||
_attackCooldownTimer = data.Cooldown;
|
||||
_isAttackActive = true;
|
||||
_actionAllowsMovement = data.CanMoveDuringAction;
|
||||
_actionAllowsTurn = data.CanTurnDuringAction;
|
||||
_lastAttackGizmoData = data;
|
||||
_attackStartTime = Time.time;
|
||||
_hitFired = false;
|
||||
@@ -476,7 +537,7 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
|
||||
PlayActionVelocity(data);
|
||||
|
||||
LockMovementIfNeeded(data, preserveHorizontalVelocity, true);
|
||||
LockMovementIfNeeded(data, preserveHorizontalVelocity);
|
||||
LockFacingIfNeeded(data);
|
||||
|
||||
try
|
||||
@@ -492,7 +553,11 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
}
|
||||
|
||||
if (_attackCts == currentAttackCts)
|
||||
{
|
||||
_isAttackActive = false;
|
||||
_actionAllowsMovement = false;
|
||||
_actionAllowsTurn = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void PerformMotion(ActionData data)
|
||||
@@ -509,10 +574,12 @@ private async void PerformMotion(ActionData data)
|
||||
ClearActionLocks();
|
||||
CaptureActionDirection(data);
|
||||
_isMotionActive = true;
|
||||
_actionAllowsMovement = data.CanMoveDuringAction;
|
||||
_actionAllowsTurn = data.CanTurnDuringAction;
|
||||
FaceMotionDirection(data);
|
||||
PlayActionAnimation(data);
|
||||
PlayActionVelocity(data);
|
||||
LockMovementIfNeeded(data, false, true);
|
||||
LockMovementIfNeeded(data, false);
|
||||
LockFacingIfNeeded(data);
|
||||
|
||||
bool completed = false;
|
||||
@@ -524,12 +591,16 @@ private async void PerformMotion(ActionData data)
|
||||
catch (System.OperationCanceledException)
|
||||
{
|
||||
_isMotionActive = false;
|
||||
_actionAllowsMovement = false;
|
||||
_actionAllowsTurn = false;
|
||||
}
|
||||
|
||||
if (completed)
|
||||
{
|
||||
StopActionVelocity(data);
|
||||
_isMotionActive = false;
|
||||
_actionAllowsMovement = false;
|
||||
_actionAllowsTurn = false;
|
||||
ClearActionLocks();
|
||||
PlayIdleAnimation();
|
||||
}
|
||||
@@ -646,6 +717,8 @@ private async void PerformGroundPound()
|
||||
private void CancelAttack()
|
||||
{
|
||||
_isAttackActive = false;
|
||||
_actionAllowsMovement = false;
|
||||
_actionAllowsTurn = false;
|
||||
CancelAndDispose(ref _attackCts);
|
||||
CancelAndDispose(ref _actionVelocityCts);
|
||||
_attackCooldownTimer = 0f;
|
||||
@@ -657,6 +730,8 @@ private void CancelAttack()
|
||||
private void CancelMotion()
|
||||
{
|
||||
_isMotionActive = false;
|
||||
_actionAllowsMovement = false;
|
||||
_actionAllowsTurn = false;
|
||||
CancelAndDispose(ref _motionCts);
|
||||
CancelAndDispose(ref _actionVelocityCts);
|
||||
}
|
||||
@@ -672,7 +747,11 @@ private bool CanTransitionFromCurrentNode(ComboInputType input)
|
||||
|
||||
foreach (var transition in _currentNode.Transitions)
|
||||
{
|
||||
if (transition.Trigger == input && transition.Next != null && transition.Next.Action != null)
|
||||
if (transition.Trigger != input || transition.Next == null) continue;
|
||||
|
||||
// 방향/무기 조건까지 반영해서 실제로 실행 가능한 트랜지션이 있는지 확인.
|
||||
ActionData nextAction = ResolveNodeAction(transition.Next);
|
||||
if (nextAction != null && CanPerformAction(nextAction))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -898,25 +977,24 @@ private void StopActionVelocity(ActionData data)
|
||||
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||
}
|
||||
|
||||
private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false, bool forceLock = false)
|
||||
private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false)
|
||||
{
|
||||
if (!forceLock && data.CanMoveDuringAction) return;
|
||||
// 액션 중 좌우 이동을 허용하는 액션(걸으면서 공격 등)은 입력 잠금을 걸지 않는다.
|
||||
// 실제 이동 허용은 FixedUpdate의 _actionAllowsMovement 게이트가 담당.
|
||||
if (data.CanMoveDuringAction)
|
||||
{
|
||||
_inputLockTimer = 0f;
|
||||
_movementLockAction = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 이동 없는 공격은 직전 프레임의 걷기 속도를 이어받지 않게 한다.
|
||||
if (!preserveHorizontalVelocity && data.Velocity == Vector2.zero)
|
||||
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||
|
||||
if (forceLock)
|
||||
{
|
||||
// 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금.
|
||||
_inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||
_movementLockAction = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputLockTimer = GetActionLockDuration(data);
|
||||
_movementLockAction = ShouldUseAnimationLength(data) ? data : null;
|
||||
}
|
||||
// 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금.
|
||||
_inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||
_movementLockAction = null;
|
||||
}
|
||||
|
||||
private void LockFacingIfNeeded(ActionData data)
|
||||
|
||||
Reference in New Issue
Block a user