2026-05-20 구르기 연계

This commit is contained in:
2026-05-20 15:34:12 +09:00
parent 08cdab421d
commit b7f35072bb
20 changed files with 181 additions and 38 deletions

View File

@@ -35,6 +35,12 @@ public class ActionData : ScriptableObject
public float Cooldown = 0.3f; // 이 액션 발동 후 다음 공격 입력 받기까지 시간
public float ComboWindow = 0.25f; // 콤보 transition 받을 수 있는 시간 (ComboNode에서도 별도 설정 가능)
// ─── 무기 조건 (콤보 트랜지션 무기별 분기 필터) ───────────────────────
// 같은 입력의 트랜지션을 무기별로 여러 개 깔아두면, 현재 장착 무기가
// 이 조건을 만족하는 액션만 실행된다 (Any면 무기와 무관하게 항상 가능).
[Header("Weapon")]
public WeaponRequirement RequiredWeapon = WeaponRequirement.Any;
// ─── 이동(모션) 파라미터 (HasMotion=true일 때 적용) ──────────────────
[Header("Motion")]
public bool HasMotion; // 이 액션이 위치 이동을 동반하는지

View File

@@ -11,6 +11,21 @@ public enum WeaponType
Gun
}
// ============================================================================
// WeaponRequirement
// ----------------------------------------------------------------------------
// 액션(ActionData)을 실행하기 위해 필요한 장착 무기 조건.
// 콤보 트랜지션을 무기별로 여러 개 깔아두고, 현재 장착 무기에 맞는 것만
// 실행되도록 필터링하는 데 사용 (예: 구르기 후 검=베기 / 총=사격 / 맨손=펀치).
// ============================================================================
public enum WeaponRequirement
{
Any, // 무기 무관 (제약 없음) — 기존 액션의 기본값
Unarmed, // 맨손일 때만 실행 가능
Sword, // 검 장착 시만 실행 가능
Gun // 총 장착 시만 실행 가능
}
// ============================================================================
// WeaponData
// ----------------------------------------------------------------------------

View File

@@ -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)