2026-05-15 충돌오류 진행중
This commit is contained in:
BIN
Assets/01_Scenes/GameScene.unity
LFS
BIN
Assets/01_Scenes/GameScene.unity
LFS
Binary file not shown.
42
Assets/02_Scripts/Combat/ActionData.cs
Normal file
42
Assets/02_Scripts/Combat/ActionData.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
|
[CreateAssetMenu(fileName = "ActionData", menuName = "Combat/ActionData")]
|
||||||
|
public class ActionData : ScriptableObject
|
||||||
|
{
|
||||||
|
[FormerlySerializedAs("AttackName")]
|
||||||
|
[FormerlySerializedAs("MotionName")]
|
||||||
|
public string ActionName;
|
||||||
|
public string AnimationState;
|
||||||
|
public float AnimationSpeed = 1f;
|
||||||
|
public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||||
|
public bool ReturnToIdleOnAnimationComplete;
|
||||||
|
public float Cooldown = 0.3f;
|
||||||
|
public float ComboWindow = 0.25f;
|
||||||
|
|
||||||
|
[Header("Motion")]
|
||||||
|
public bool HasMotion;
|
||||||
|
public Vector2 Velocity = Vector2.zero;
|
||||||
|
public AnimationCurve MotionSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||||
|
[FormerlySerializedAs("Duration")]
|
||||||
|
public float MotionDuration = 0.3f;
|
||||||
|
public bool CanMoveDuringAction;
|
||||||
|
public bool CanTurnDuringAction;
|
||||||
|
public bool UseInputDirection = true;
|
||||||
|
public bool PreserveYVelocity = true;
|
||||||
|
public bool StopHorizontalVelocityOnEnd = true;
|
||||||
|
public bool IgnoreCollisionDuringAction;
|
||||||
|
public LayerMask IgnoredCollisionLayers;
|
||||||
|
|
||||||
|
[Header("Hit")]
|
||||||
|
public bool HasHit = true;
|
||||||
|
public Vector2 Offset = new Vector2(0.5f, 0f);
|
||||||
|
public float Radius = 0.5f;
|
||||||
|
public int Damage = 10;
|
||||||
|
public float HitTiming = 0.15f;
|
||||||
|
public float HitDuration = 0f;
|
||||||
|
|
||||||
|
[Header("Hit Reaction")]
|
||||||
|
public Vector2 HitVelocity = Vector2.zero;
|
||||||
|
public string HitReactionAnimationState;
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Combat/ActionData.cs.meta
Normal file
2
Assets/02_Scripts/Combat/ActionData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 526ebe3dc7ff32e4faa2cfcdc1670638
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
[CreateAssetMenu(fileName = "AttackData", menuName = "Combat/AttackData")]
|
|
||||||
public class AttackData : ScriptableObject
|
|
||||||
{
|
|
||||||
public string AttackName;
|
|
||||||
public string AnimationState;
|
|
||||||
public Vector2 Offset = new Vector2(0.5f, 0f);
|
|
||||||
public float Radius = 0.5f;
|
|
||||||
public int Damage = 10;
|
|
||||||
public float Cooldown = 0.3f;
|
|
||||||
|
|
||||||
[Header("Timing")]
|
|
||||||
public float HitTiming = 0.15f;
|
|
||||||
public float HitDuration = 0f;
|
|
||||||
public float MotionDuration = 0.3f;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 526ebe3dc7ff32e4faa2cfcdc1670638
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
public enum ComboInputType
|
public enum ComboInputType
|
||||||
{
|
{
|
||||||
Punch,
|
Punch,
|
||||||
Kick
|
Kick,
|
||||||
|
Motion
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -20,7 +22,8 @@ public class ComboTransition
|
|||||||
public class ComboNode : ScriptableObject
|
public class ComboNode : ScriptableObject
|
||||||
{
|
{
|
||||||
public string NodeName;
|
public string NodeName;
|
||||||
public AttackData Attack;
|
[FormerlySerializedAs("Attack")]
|
||||||
|
public ActionData Action;
|
||||||
public float ComboWindow = 0.8f;
|
public float ComboWindow = 0.8f;
|
||||||
public ComboTransition[] Transitions;
|
public ComboTransition[] Transitions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
public interface IDamageable
|
public interface IDamageable
|
||||||
{
|
{
|
||||||
void TakeDamage(int amount);
|
void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
[RequireComponent(typeof(Collider2D))]
|
[RequireComponent(typeof(Collider2D))]
|
||||||
|
[RequireComponent(typeof(Rigidbody2D))]
|
||||||
public class Enemy : MonoBehaviour, IDamageable
|
public class Enemy : MonoBehaviour, IDamageable
|
||||||
{
|
{
|
||||||
[Header("Stats")]
|
[Header("Stats")]
|
||||||
@@ -10,14 +11,28 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
[SerializeField] private float _hitFlashDuration = 0.1f;
|
[SerializeField] private float _hitFlashDuration = 0.1f;
|
||||||
[SerializeField] private Color _hitFlashColor = Color.red;
|
[SerializeField] private Color _hitFlashColor = Color.red;
|
||||||
|
|
||||||
|
[Header("Hit Bounce")]
|
||||||
|
[SerializeField] private float _hitReactionDuration = 0.5f;
|
||||||
|
[SerializeField] private float _airborneHitYVelocity = 3f;
|
||||||
|
[SerializeField] private float _wallBounceVelocityMultiplier = 0.8f;
|
||||||
|
[SerializeField] private float _wallBounceMinXVelocity = 1f;
|
||||||
|
[SerializeField] private float _wallBounceUpwardVelocity = 1.5f;
|
||||||
|
|
||||||
private int _currentHealth;
|
private int _currentHealth;
|
||||||
|
private Rigidbody2D _rb;
|
||||||
|
private Animator _anim;
|
||||||
private SpriteRenderer _spriteRenderer;
|
private SpriteRenderer _spriteRenderer;
|
||||||
private Color _originalColor;
|
private Color _originalColor;
|
||||||
private float _flashTimer;
|
private float _flashTimer;
|
||||||
|
private float _hitReactionTimer;
|
||||||
|
private bool _isGrounded;
|
||||||
|
private Vector2 _lastVelocity;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_currentHealth = _maxHealth;
|
_currentHealth = _maxHealth;
|
||||||
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
|
_anim = GetComponentInChildren<Animator>();
|
||||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
if (_spriteRenderer != null)
|
if (_spriteRenderer != null)
|
||||||
_originalColor = _spriteRenderer.color;
|
_originalColor = _spriteRenderer.color;
|
||||||
@@ -31,9 +46,18 @@ private void Update()
|
|||||||
if (_flashTimer <= 0f && _spriteRenderer != null)
|
if (_flashTimer <= 0f && _spriteRenderer != null)
|
||||||
_spriteRenderer.color = _originalColor;
|
_spriteRenderer.color = _originalColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_hitReactionTimer > 0f)
|
||||||
|
_hitReactionTimer -= Time.deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TakeDamage(int amount)
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (_rb != null)
|
||||||
|
_lastVelocity = _rb.linearVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null)
|
||||||
{
|
{
|
||||||
if (_currentHealth <= 0) return;
|
if (_currentHealth <= 0) return;
|
||||||
|
|
||||||
@@ -46,10 +70,89 @@ public void TakeDamage(int amount)
|
|||||||
_flashTimer = _hitFlashDuration;
|
_flashTimer = _hitFlashDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_anim != null && !string.IsNullOrEmpty(hitReactionAnimationState))
|
||||||
|
_anim.Play(hitReactionAnimationState);
|
||||||
|
|
||||||
|
if (_rb != null)
|
||||||
|
{
|
||||||
|
Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity);
|
||||||
|
if (nextVelocity != Vector2.zero)
|
||||||
|
{
|
||||||
|
_rb.linearVelocity = nextVelocity;
|
||||||
|
_hitReactionTimer = _hitReactionDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_currentHealth <= 0)
|
if (_currentHealth <= 0)
|
||||||
Die();
|
Die();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnCollisionEnter2D(Collision2D collision)
|
||||||
|
{
|
||||||
|
UpdateGroundedState(collision);
|
||||||
|
|
||||||
|
if (_hitReactionTimer <= 0f || _rb == null) return;
|
||||||
|
if (collision.collider.GetComponentInParent<PlayerController>() != null) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < collision.contactCount; i++)
|
||||||
|
{
|
||||||
|
Vector2 normal = collision.GetContact(i).normal;
|
||||||
|
if (Mathf.Abs(normal.x) < 0.5f) continue;
|
||||||
|
|
||||||
|
BounceOffWall(normal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCollisionStay2D(Collision2D collision)
|
||||||
|
{
|
||||||
|
UpdateGroundedState(collision);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCollisionExit2D(Collision2D collision)
|
||||||
|
{
|
||||||
|
_isGrounded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GetHitReactionVelocity(Vector2 hitVelocity)
|
||||||
|
{
|
||||||
|
if (_hitReactionTimer <= 0f || _isGrounded)
|
||||||
|
return hitVelocity;
|
||||||
|
|
||||||
|
Vector2 currentVelocity = _rb.linearVelocity;
|
||||||
|
Vector2 nextVelocity = hitVelocity == Vector2.zero ? currentVelocity : hitVelocity;
|
||||||
|
nextVelocity.y = _airborneHitYVelocity;
|
||||||
|
return nextVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGroundedState(Collision2D collision)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < collision.contactCount; i++)
|
||||||
|
{
|
||||||
|
if (collision.GetContact(i).normal.y > 0.5f)
|
||||||
|
{
|
||||||
|
_isGrounded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BounceOffWall(Vector2 wallNormal)
|
||||||
|
{
|
||||||
|
Vector2 incomingVelocity = _lastVelocity.sqrMagnitude > _rb.linearVelocity.sqrMagnitude
|
||||||
|
? _lastVelocity
|
||||||
|
: _rb.linearVelocity;
|
||||||
|
|
||||||
|
if (Mathf.Abs(incomingVelocity.x) < _wallBounceMinXVelocity) return;
|
||||||
|
|
||||||
|
Vector2 bouncedVelocity = Vector2.Reflect(incomingVelocity, wallNormal) * _wallBounceVelocityMultiplier;
|
||||||
|
if (bouncedVelocity.y < _wallBounceUpwardVelocity)
|
||||||
|
bouncedVelocity.y = _wallBounceUpwardVelocity;
|
||||||
|
|
||||||
|
_rb.linearVelocity = bouncedVelocity;
|
||||||
|
_hitReactionTimer = _hitReactionDuration;
|
||||||
|
}
|
||||||
|
|
||||||
private void Die()
|
private void Die()
|
||||||
{
|
{
|
||||||
Debug.Log($"{name} 사망");
|
Debug.Log($"{name} 사망");
|
||||||
|
|||||||
@@ -127,6 +127,24 @@ public @GameInput()
|
|||||||
""processors"": """",
|
""processors"": """",
|
||||||
""interactions"": """",
|
""interactions"": """",
|
||||||
""initialStateCheck"": false
|
""initialStateCheck"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""Dash"",
|
||||||
|
""type"": ""Button"",
|
||||||
|
""id"": ""4245d8e3-7e61-4548-84af-75512958eb2f"",
|
||||||
|
""expectedControlType"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""interactions"": """",
|
||||||
|
""initialStateCheck"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""Roll"",
|
||||||
|
""type"": ""Button"",
|
||||||
|
""id"": ""7e00ae7c-ad0c-460d-be3d-0072054ceb9c"",
|
||||||
|
""expectedControlType"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""interactions"": """",
|
||||||
|
""initialStateCheck"": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
""bindings"": [
|
""bindings"": [
|
||||||
@@ -188,7 +206,7 @@ public @GameInput()
|
|||||||
{
|
{
|
||||||
""name"": """",
|
""name"": """",
|
||||||
""id"": ""b9d8e2f3-4a5c-4b6d-8e7f-901234567890"",
|
""id"": ""b9d8e2f3-4a5c-4b6d-8e7f-901234567890"",
|
||||||
""path"": ""<Keyboard>/space"",
|
""path"": ""<Keyboard>/c"",
|
||||||
""interactions"": """",
|
""interactions"": """",
|
||||||
""processors"": """",
|
""processors"": """",
|
||||||
""groups"": """",
|
""groups"": """",
|
||||||
@@ -217,6 +235,28 @@ public @GameInput()
|
|||||||
""action"": ""Kick"",
|
""action"": ""Kick"",
|
||||||
""isComposite"": false,
|
""isComposite"": false,
|
||||||
""isPartOfComposite"": false
|
""isPartOfComposite"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": """",
|
||||||
|
""id"": ""8bca2399-c8c8-41e8-a3b2-08267c8bc571"",
|
||||||
|
""path"": ""<Keyboard>/leftShift"",
|
||||||
|
""interactions"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""groups"": """",
|
||||||
|
""action"": ""Dash"",
|
||||||
|
""isComposite"": false,
|
||||||
|
""isPartOfComposite"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": """",
|
||||||
|
""id"": ""df4d72ec-012c-413a-862d-1a36d2c5b69a"",
|
||||||
|
""path"": ""<Keyboard>/space"",
|
||||||
|
""interactions"": """",
|
||||||
|
""processors"": """",
|
||||||
|
""groups"": """",
|
||||||
|
""action"": ""Roll"",
|
||||||
|
""isComposite"": false,
|
||||||
|
""isPartOfComposite"": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -229,6 +269,8 @@ public @GameInput()
|
|||||||
m_Player_Jump = m_Player.FindAction("Jump", throwIfNotFound: true);
|
m_Player_Jump = m_Player.FindAction("Jump", throwIfNotFound: true);
|
||||||
m_Player_Punch = m_Player.FindAction("Punch", throwIfNotFound: true);
|
m_Player_Punch = m_Player.FindAction("Punch", throwIfNotFound: true);
|
||||||
m_Player_Kick = m_Player.FindAction("Kick", throwIfNotFound: true);
|
m_Player_Kick = m_Player.FindAction("Kick", throwIfNotFound: true);
|
||||||
|
m_Player_Dash = m_Player.FindAction("Dash", throwIfNotFound: true);
|
||||||
|
m_Player_Roll = m_Player.FindAction("Roll", throwIfNotFound: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
~@GameInput()
|
~@GameInput()
|
||||||
@@ -313,6 +355,8 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
|
|||||||
private readonly InputAction m_Player_Jump;
|
private readonly InputAction m_Player_Jump;
|
||||||
private readonly InputAction m_Player_Punch;
|
private readonly InputAction m_Player_Punch;
|
||||||
private readonly InputAction m_Player_Kick;
|
private readonly InputAction m_Player_Kick;
|
||||||
|
private readonly InputAction m_Player_Dash;
|
||||||
|
private readonly InputAction m_Player_Roll;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides access to input actions defined in input action map "Player".
|
/// Provides access to input actions defined in input action map "Player".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -341,6 +385,14 @@ public struct PlayerActions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public InputAction @Kick => m_Wrapper.m_Player_Kick;
|
public InputAction @Kick => m_Wrapper.m_Player_Kick;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Provides access to the underlying input action "Player/Dash".
|
||||||
|
/// </summary>
|
||||||
|
public InputAction @Dash => m_Wrapper.m_Player_Dash;
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the underlying input action "Player/Roll".
|
||||||
|
/// </summary>
|
||||||
|
public InputAction @Roll => m_Wrapper.m_Player_Roll;
|
||||||
|
/// <summary>
|
||||||
/// Provides access to the underlying input action map instance.
|
/// Provides access to the underlying input action map instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
||||||
@@ -378,6 +430,12 @@ public void AddCallbacks(IPlayerActions instance)
|
|||||||
@Kick.started += instance.OnKick;
|
@Kick.started += instance.OnKick;
|
||||||
@Kick.performed += instance.OnKick;
|
@Kick.performed += instance.OnKick;
|
||||||
@Kick.canceled += instance.OnKick;
|
@Kick.canceled += instance.OnKick;
|
||||||
|
@Dash.started += instance.OnDash;
|
||||||
|
@Dash.performed += instance.OnDash;
|
||||||
|
@Dash.canceled += instance.OnDash;
|
||||||
|
@Roll.started += instance.OnRoll;
|
||||||
|
@Roll.performed += instance.OnRoll;
|
||||||
|
@Roll.canceled += instance.OnRoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -401,6 +459,12 @@ private void UnregisterCallbacks(IPlayerActions instance)
|
|||||||
@Kick.started -= instance.OnKick;
|
@Kick.started -= instance.OnKick;
|
||||||
@Kick.performed -= instance.OnKick;
|
@Kick.performed -= instance.OnKick;
|
||||||
@Kick.canceled -= instance.OnKick;
|
@Kick.canceled -= instance.OnKick;
|
||||||
|
@Dash.started -= instance.OnDash;
|
||||||
|
@Dash.performed -= instance.OnDash;
|
||||||
|
@Dash.canceled -= instance.OnDash;
|
||||||
|
@Roll.started -= instance.OnRoll;
|
||||||
|
@Roll.performed -= instance.OnRoll;
|
||||||
|
@Roll.canceled -= instance.OnRoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -469,5 +533,19 @@ public interface IPlayerActions
|
|||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
void OnKick(InputAction.CallbackContext context);
|
void OnKick(InputAction.CallbackContext context);
|
||||||
|
/// <summary>
|
||||||
|
/// Method invoked when associated input action "Dash" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
|
void OnDash(InputAction.CallbackContext context);
|
||||||
|
/// <summary>
|
||||||
|
/// Method invoked when associated input action "Roll" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||||
|
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||||
|
void OnRoll(InputAction.CallbackContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
|
|||||||
public event Action OnJump_Event;
|
public event Action OnJump_Event;
|
||||||
public event Action OnPunch_Event;
|
public event Action OnPunch_Event;
|
||||||
public event Action OnKick_Event;
|
public event Action OnKick_Event;
|
||||||
|
public event Action OnDash_Event;
|
||||||
|
public event Action OnRoll_Event;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -27,7 +29,9 @@ private void Awake()
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable() => _input?.Player.Enable();
|
private void OnEnable() => _input?.Player.Enable();
|
||||||
|
|
||||||
private void OnDisable() => _input?.Player.Disable();
|
private void OnDisable() => _input?.Player.Disable();
|
||||||
|
|
||||||
private void OnDestroy() => _input?.Dispose();
|
private void OnDestroy() => _input?.Dispose();
|
||||||
|
|
||||||
public void OnMove(InputAction.CallbackContext ctx)
|
public void OnMove(InputAction.CallbackContext ctx)
|
||||||
@@ -54,4 +58,15 @@ public void OnKick(InputAction.CallbackContext ctx)
|
|||||||
OnKick_Event?.Invoke();
|
OnKick_Event?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnDash(InputAction.CallbackContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx.phase == InputActionPhase.Started)
|
||||||
|
OnDash_Event?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRoll(InputAction.CallbackContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx.phase == InputActionPhase.Started)
|
||||||
|
OnRoll_Event?.Invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,31 @@ public class PlayerController : MonoBehaviour
|
|||||||
private bool IsTouchingWall => _isTouchingLeftWall || _isTouchingRightWall;
|
private bool IsTouchingWall => _isTouchingLeftWall || _isTouchingRightWall;
|
||||||
private int _wallDirection;
|
private int _wallDirection;
|
||||||
private float _inputLockTimer;
|
private float _inputLockTimer;
|
||||||
|
private float _facingLockTimer;
|
||||||
|
|
||||||
|
[Header("Motion")]
|
||||||
|
[SerializeField] private ComboNode _dashRootNode;
|
||||||
|
[SerializeField] private ComboNode _rollRootNode;
|
||||||
|
private readonly Dictionary<ActionData, float> _motionCooldownTimers = new();
|
||||||
|
private readonly List<ActionData> _motionCooldownKeys = new();
|
||||||
|
private readonly List<IgnoredLayerCollision> _ignoredLayerCollisions = new();
|
||||||
|
private readonly List<Collider2D> _overlapResults = new();
|
||||||
|
private readonly List<RaycastHit2D> _castResults = new();
|
||||||
|
private int _activeCollisionRecoveryLayerMask;
|
||||||
|
private Collider2D[] _bodyColliders;
|
||||||
|
private CancellationTokenSource _restoreCollisionCts;
|
||||||
|
private CancellationTokenSource _motionCts;
|
||||||
|
|
||||||
|
[Header("Collision Recovery")]
|
||||||
|
[SerializeField] private float _overlapRecoverySpeed = 8f;
|
||||||
|
[SerializeField] private float _overlapRecoveryOtherBodyRatio = 0.5f;
|
||||||
|
[SerializeField] private float _bodyBlockCastDistance = 0.08f;
|
||||||
|
|
||||||
[Header("Attack")]
|
[Header("Attack")]
|
||||||
[SerializeField] private ComboNode _punchRootNode;
|
[SerializeField] private ComboNode _punchRootNode;
|
||||||
[SerializeField] private ComboNode _kickRootNode;
|
[SerializeField] private ComboNode _kickRootNode;
|
||||||
[SerializeField] private LayerMask _enemyLayer;
|
[SerializeField] private LayerMask _enemyLayer;
|
||||||
|
[SerializeField] private LayerMask _bodyCollisionIgnoredLayers;
|
||||||
[SerializeField] private string _idleAnimationState = "Idle";
|
[SerializeField] private string _idleAnimationState = "Idle";
|
||||||
[SerializeField] private float _bufferOpenTime = 0.1f;
|
[SerializeField] private float _bufferOpenTime = 0.1f;
|
||||||
[SerializeField] private float _bufferLifetime = 0.5f;
|
[SerializeField] private float _bufferLifetime = 0.5f;
|
||||||
@@ -41,10 +61,11 @@ public class PlayerController : MonoBehaviour
|
|||||||
private ComboNode _currentNode;
|
private ComboNode _currentNode;
|
||||||
private float _comboWindowTimer;
|
private float _comboWindowTimer;
|
||||||
private CancellationTokenSource _attackCts;
|
private CancellationTokenSource _attackCts;
|
||||||
private AttackData _lastAttackGizmoData;
|
private CancellationTokenSource _animationSpeedCts;
|
||||||
|
private ActionData _lastAttackGizmoData;
|
||||||
private float _lastAttackGizmoTime = -1f;
|
private float _lastAttackGizmoTime = -1f;
|
||||||
[SerializeField] private float _hitGizmoFadeDuration = 0.5f;
|
[SerializeField] private float _hitGizmoFadeDuration = 0.5f;
|
||||||
private AttackData _lastHitData;
|
private ActionData _lastHitData;
|
||||||
private Vector2 _lastHitCenter;
|
private Vector2 _lastHitCenter;
|
||||||
private float _lastHitTime = -1f;
|
private float _lastHitTime = -1f;
|
||||||
|
|
||||||
@@ -62,6 +83,8 @@ private void Awake()
|
|||||||
_rb = GetComponent<Rigidbody2D>();
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
_anim = GetComponent<Animator>();
|
_anim = GetComponent<Animator>();
|
||||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||||
|
IgnoreBodyCollisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
@@ -71,6 +94,8 @@ private void Start()
|
|||||||
|
|
||||||
InputManager.Instance.OnPunch_Event += OnPunchInput;
|
InputManager.Instance.OnPunch_Event += OnPunchInput;
|
||||||
InputManager.Instance.OnKick_Event += OnKickInput;
|
InputManager.Instance.OnKick_Event += OnKickInput;
|
||||||
|
InputManager.Instance.OnDash_Event += OnDashInput;
|
||||||
|
InputManager.Instance.OnRoll_Event += OnRollInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
@@ -81,10 +106,19 @@ private void OnDestroy()
|
|||||||
InputManager.Instance.OnJump_Event -= OnJumpInput;
|
InputManager.Instance.OnJump_Event -= OnJumpInput;
|
||||||
InputManager.Instance.OnPunch_Event -= OnPunchInput;
|
InputManager.Instance.OnPunch_Event -= OnPunchInput;
|
||||||
InputManager.Instance.OnKick_Event -= OnKickInput;
|
InputManager.Instance.OnKick_Event -= OnKickInput;
|
||||||
|
InputManager.Instance.OnDash_Event -= OnDashInput;
|
||||||
|
InputManager.Instance.OnRoll_Event -= OnRollInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
_attackCts?.Cancel();
|
_attackCts?.Cancel();
|
||||||
_attackCts?.Dispose();
|
_attackCts?.Dispose();
|
||||||
|
_animationSpeedCts?.Cancel();
|
||||||
|
_animationSpeedCts?.Dispose();
|
||||||
|
_motionCts?.Cancel();
|
||||||
|
_motionCts?.Dispose();
|
||||||
|
_restoreCollisionCts?.Cancel();
|
||||||
|
_restoreCollisionCts?.Dispose();
|
||||||
|
RestoreIgnoredLayerCollisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
@@ -97,6 +131,8 @@ private void FixedUpdate()
|
|||||||
if (_attackCooldownTimer > 0f)
|
if (_attackCooldownTimer > 0f)
|
||||||
_attackCooldownTimer -= Time.fixedDeltaTime;
|
_attackCooldownTimer -= Time.fixedDeltaTime;
|
||||||
|
|
||||||
|
UpdateMotionCooldowns();
|
||||||
|
|
||||||
if (_attackCooldownTimer <= 0f && _pendingInput.HasValue)
|
if (_attackCooldownTimer <= 0f && _pendingInput.HasValue)
|
||||||
{
|
{
|
||||||
ComboInputType buffered = _pendingInput.Value;
|
ComboInputType buffered = _pendingInput.Value;
|
||||||
@@ -115,7 +151,12 @@ private void FixedUpdate()
|
|||||||
if (_inputLockTimer > 0f)
|
if (_inputLockTimer > 0f)
|
||||||
_inputLockTimer -= Time.fixedDeltaTime;
|
_inputLockTimer -= Time.fixedDeltaTime;
|
||||||
else
|
else
|
||||||
_rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);
|
_rb.linearVelocity = new Vector2(GetBodyBlockedXVelocity(_moveInputX * _moveSpeed), _rb.linearVelocity.y);
|
||||||
|
|
||||||
|
if (_facingLockTimer > 0f)
|
||||||
|
_facingLockTimer -= Time.fixedDeltaTime;
|
||||||
|
else
|
||||||
|
UpdateFacingFromMoveInput();
|
||||||
|
|
||||||
if (IsTouchingWall && !_isGrounded && _rb.linearVelocity.y < -_wallSlideSpeed)
|
if (IsTouchingWall && !_isGrounded && _rb.linearVelocity.y < -_wallSlideSpeed)
|
||||||
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, -_wallSlideSpeed);
|
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, -_wallSlideSpeed);
|
||||||
@@ -125,6 +166,13 @@ private void OnMoveInput(Vector2 value)
|
|||||||
{
|
{
|
||||||
_moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
|
_moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
|
||||||
|
|
||||||
|
if (_facingLockTimer > 0f) return;
|
||||||
|
|
||||||
|
UpdateFacingFromMoveInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFacingFromMoveInput()
|
||||||
|
{
|
||||||
if (_moveInputX != 0f && _spriteRenderer != null)
|
if (_moveInputX != 0f && _spriteRenderer != null)
|
||||||
_spriteRenderer.flipX = _moveInputX < 0f;
|
_spriteRenderer.flipX = _moveInputX < 0f;
|
||||||
}
|
}
|
||||||
@@ -143,7 +191,9 @@ private void OnJumpInput()
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnPunchInput() => HandleComboInput(ComboInputType.Punch);
|
private void OnPunchInput() => HandleComboInput(ComboInputType.Punch);
|
||||||
private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
|
private void OnKickInput() => HandleComboInput(ComboInputType.Kick);
|
||||||
|
private void OnDashInput() => ExecuteMotionNode(_dashRootNode);
|
||||||
|
private void OnRollInput() => ExecuteMotionNode(_rollRootNode);
|
||||||
|
|
||||||
private void HandleComboInput(ComboInputType input)
|
private void HandleComboInput(ComboInputType input)
|
||||||
{
|
{
|
||||||
@@ -168,10 +218,10 @@ private void ExecuteComboInput(ComboInputType input)
|
|||||||
foreach (var transition in _currentNode.Transitions)
|
foreach (var transition in _currentNode.Transitions)
|
||||||
{
|
{
|
||||||
if (transition.Trigger != input) continue;
|
if (transition.Trigger != input) continue;
|
||||||
if (transition.Next == null || transition.Next.Attack == null) continue;
|
if (transition.Next == null || transition.Next.Action == null) continue;
|
||||||
|
|
||||||
ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration);
|
ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration);
|
||||||
PerformAttack(transition.Next.Attack);
|
PerformAttack(transition.Next.Action, transition.ForwardStep > 0f);
|
||||||
_currentNode = transition.Next;
|
_currentNode = transition.Next;
|
||||||
_comboWindowTimer = transition.Next.ComboWindow;
|
_comboWindowTimer = transition.Next.ComboWindow;
|
||||||
return;
|
return;
|
||||||
@@ -184,14 +234,262 @@ private void ExecuteComboInput(ComboInputType input)
|
|||||||
ComboInputType.Kick => _kickRootNode,
|
ComboInputType.Kick => _kickRootNode,
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
if (root == null || root.Attack == null) return;
|
if (root == null || root.Action == null) return;
|
||||||
|
|
||||||
PerformAttack(root.Attack);
|
PerformAttack(root.Action);
|
||||||
_currentNode = root;
|
_currentNode = root;
|
||||||
_comboWindowTimer = root.ComboWindow;
|
_comboWindowTimer = root.ComboWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PerformAttack(AttackData data)
|
private void ExecuteMotionNode(ComboNode root)
|
||||||
|
{
|
||||||
|
if (root == null || root.Action == null) return;
|
||||||
|
if (IsMotionOnCooldown(root.Action)) return;
|
||||||
|
|
||||||
|
PerformMotion(root.Action);
|
||||||
|
_currentNode = root;
|
||||||
|
_comboWindowTimer = root.ComboWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMotionCooldowns()
|
||||||
|
{
|
||||||
|
if (_motionCooldownTimers.Count == 0) return;
|
||||||
|
|
||||||
|
_motionCooldownKeys.Clear();
|
||||||
|
foreach (var pair in _motionCooldownTimers)
|
||||||
|
_motionCooldownKeys.Add(pair.Key);
|
||||||
|
|
||||||
|
foreach (var action in _motionCooldownKeys)
|
||||||
|
{
|
||||||
|
float remaining = _motionCooldownTimers[action] - Time.fixedDeltaTime;
|
||||||
|
if (remaining <= 0f)
|
||||||
|
_motionCooldownTimers.Remove(action);
|
||||||
|
else
|
||||||
|
_motionCooldownTimers[action] = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsMotionOnCooldown(ActionData data)
|
||||||
|
{
|
||||||
|
return data != null && _motionCooldownTimers.ContainsKey(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMotionCooldown(ActionData data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Cooldown <= 0f) return;
|
||||||
|
|
||||||
|
_motionCooldownTimers[data] = data.Cooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IgnoreBodyCollisions()
|
||||||
|
{
|
||||||
|
int ignoredLayers = _bodyCollisionIgnoredLayers.value;
|
||||||
|
if (ignoredLayers == 0) return;
|
||||||
|
|
||||||
|
int playerLayer = gameObject.layer;
|
||||||
|
for (int layer = 0; layer < 32; layer++)
|
||||||
|
{
|
||||||
|
if ((ignoredLayers & (1 << layer)) == 0) continue;
|
||||||
|
Physics2D.IgnoreLayerCollision(playerLayer, layer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetBodyBlockedXVelocity(float xVelocity, ActionData action = null)
|
||||||
|
{
|
||||||
|
if (Mathf.Approximately(xVelocity, 0f)) return 0f;
|
||||||
|
if (_bodyCollisionIgnoredLayers.value == 0) return xVelocity;
|
||||||
|
if (CanPassBodyCollision(action)) return xVelocity;
|
||||||
|
if (!IsBodyBlocked(Mathf.Sign(xVelocity))) return xVelocity;
|
||||||
|
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanPassBodyCollision(ActionData action)
|
||||||
|
{
|
||||||
|
return action != null
|
||||||
|
&& action.IgnoreCollisionDuringAction
|
||||||
|
&& (action.IgnoredCollisionLayers.value & _bodyCollisionIgnoredLayers.value) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsBodyBlocked(float direction)
|
||||||
|
{
|
||||||
|
if (_bodyColliders == null || _bodyColliders.Length == 0)
|
||||||
|
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||||
|
|
||||||
|
ContactFilter2D filter = new ContactFilter2D
|
||||||
|
{
|
||||||
|
useLayerMask = true,
|
||||||
|
layerMask = _bodyCollisionIgnoredLayers,
|
||||||
|
useTriggers = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector2 castDirection = new Vector2(direction, 0f);
|
||||||
|
for (int i = 0; i < _bodyColliders.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D bodyCollider = _bodyColliders[i];
|
||||||
|
if (bodyCollider == null || bodyCollider.isTrigger) continue;
|
||||||
|
|
||||||
|
_castResults.Clear();
|
||||||
|
if (bodyCollider.Cast(castDirection, filter, _castResults, _bodyBlockCastDistance) > 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IgnoreCollisionsIfNeeded(ActionData data)
|
||||||
|
{
|
||||||
|
_restoreCollisionCts?.Cancel();
|
||||||
|
if (!data.IgnoreCollisionDuringAction)
|
||||||
|
{
|
||||||
|
RestoreIgnoredLayerCollisionsWhenClear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int playerLayer = gameObject.layer;
|
||||||
|
int ignoredLayers = data.IgnoredCollisionLayers.value;
|
||||||
|
_activeCollisionRecoveryLayerMask = ignoredLayers;
|
||||||
|
for (int layer = 0; layer < 32; layer++)
|
||||||
|
{
|
||||||
|
if ((ignoredLayers & (1 << layer)) == 0) continue;
|
||||||
|
if (Physics2D.GetIgnoreLayerCollision(playerLayer, layer)) continue;
|
||||||
|
|
||||||
|
Physics2D.IgnoreLayerCollision(playerLayer, layer, true);
|
||||||
|
_ignoredLayerCollisions.Add(new IgnoredLayerCollision(playerLayer, layer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreIgnoredLayerCollisions()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _ignoredLayerCollisions.Count; i++)
|
||||||
|
{
|
||||||
|
IgnoredLayerCollision ignored = _ignoredLayerCollisions[i];
|
||||||
|
Physics2D.IgnoreLayerCollision(ignored.LayerA, ignored.LayerB, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ignoredLayerCollisions.Clear();
|
||||||
|
_activeCollisionRecoveryLayerMask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreIgnoredLayerCollisionsWhenClear(LayerMask ignoredLayers = default)
|
||||||
|
{
|
||||||
|
int recoveryLayerMask = ignoredLayers.value != 0 ? ignoredLayers.value : _activeCollisionRecoveryLayerMask;
|
||||||
|
if (_ignoredLayerCollisions.Count == 0 && recoveryLayerMask == 0) return;
|
||||||
|
|
||||||
|
_restoreCollisionCts?.Cancel();
|
||||||
|
_restoreCollisionCts?.Dispose();
|
||||||
|
_restoreCollisionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
WaitUntilClearThenRestoreCollisions(recoveryLayerMask, _restoreCollisionCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void WaitUntilClearThenRestoreCollisions(int recoveryLayerMask, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (IsOverlappingIgnoredLayers(recoveryLayerMask))
|
||||||
|
{
|
||||||
|
ResolveIgnoredLayerOverlap(recoveryLayerMask);
|
||||||
|
await Awaitable.NextFrameAsync(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreIgnoredLayerCollisions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveIgnoredLayerOverlap(int layerMask)
|
||||||
|
{
|
||||||
|
if (layerMask == 0) return;
|
||||||
|
|
||||||
|
if (_bodyColliders == null || _bodyColliders.Length == 0)
|
||||||
|
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||||
|
|
||||||
|
ContactFilter2D filter = new ContactFilter2D
|
||||||
|
{
|
||||||
|
useLayerMask = true,
|
||||||
|
layerMask = layerMask,
|
||||||
|
useTriggers = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector2 selfCenter = _rb.position;
|
||||||
|
float distance = _overlapRecoverySpeed * Time.deltaTime;
|
||||||
|
|
||||||
|
for (int i = 0; i < _bodyColliders.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D bodyCollider = _bodyColliders[i];
|
||||||
|
if (bodyCollider == null || bodyCollider.isTrigger) continue;
|
||||||
|
|
||||||
|
_overlapResults.Clear();
|
||||||
|
bodyCollider.Overlap(filter, _overlapResults);
|
||||||
|
|
||||||
|
for (int j = 0; j < _overlapResults.Count; j++)
|
||||||
|
{
|
||||||
|
Collider2D other = _overlapResults[j];
|
||||||
|
if (other == null) continue;
|
||||||
|
|
||||||
|
Vector2 pushDirection = selfCenter - (Vector2)other.bounds.center;
|
||||||
|
if (pushDirection.sqrMagnitude < 0.0001f)
|
||||||
|
pushDirection = GetFacingDirection();
|
||||||
|
pushDirection.Normalize();
|
||||||
|
|
||||||
|
Rigidbody2D otherRb = other.attachedRigidbody;
|
||||||
|
bool canMoveOther = otherRb != null && otherRb != _rb && otherRb.bodyType != RigidbodyType2D.Static;
|
||||||
|
float otherDistance = canMoveOther ? distance * _overlapRecoveryOtherBodyRatio : 0f;
|
||||||
|
float selfDistance = distance - otherDistance;
|
||||||
|
|
||||||
|
_rb.position += pushDirection * selfDistance;
|
||||||
|
if (canMoveOther)
|
||||||
|
otherRb.position -= pushDirection * otherDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GetFacingDirection()
|
||||||
|
{
|
||||||
|
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||||
|
return new Vector2(facing, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOverlappingIgnoredLayers(int layerMask)
|
||||||
|
{
|
||||||
|
if (layerMask == 0) return false;
|
||||||
|
|
||||||
|
if (_bodyColliders == null || _bodyColliders.Length == 0)
|
||||||
|
_bodyColliders = GetComponentsInChildren<Collider2D>();
|
||||||
|
|
||||||
|
ContactFilter2D filter = new ContactFilter2D
|
||||||
|
{
|
||||||
|
useLayerMask = true,
|
||||||
|
layerMask = layerMask,
|
||||||
|
useTriggers = false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < _bodyColliders.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D bodyCollider = _bodyColliders[i];
|
||||||
|
if (bodyCollider == null || bodyCollider.isTrigger) continue;
|
||||||
|
|
||||||
|
_overlapResults.Clear();
|
||||||
|
if (bodyCollider.Overlap(filter, _overlapResults) > 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetCurrentIgnoredLayerMask()
|
||||||
|
{
|
||||||
|
int layerMask = 0;
|
||||||
|
for (int i = 0; i < _ignoredLayerCollisions.Count; i++)
|
||||||
|
layerMask |= 1 << _ignoredLayerCollisions[i].LayerB;
|
||||||
|
|
||||||
|
return layerMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void PerformAttack(ActionData data, bool preserveHorizontalVelocity = false)
|
||||||
{
|
{
|
||||||
_attackCts?.Cancel();
|
_attackCts?.Cancel();
|
||||||
_attackCts?.Dispose();
|
_attackCts?.Dispose();
|
||||||
@@ -203,20 +501,217 @@ private async void PerformAttack(AttackData data)
|
|||||||
_attackStartTime = Time.time;
|
_attackStartTime = Time.time;
|
||||||
_hitFired = false;
|
_hitFired = false;
|
||||||
|
|
||||||
if (_anim != null && !string.IsNullOrEmpty(data.AnimationState))
|
PlayActionAnimation(data);
|
||||||
_anim.Play(data.AnimationState);
|
|
||||||
|
if (data.HasMotion)
|
||||||
|
{
|
||||||
|
ApplyActionVelocity(data);
|
||||||
|
}
|
||||||
|
LockMovementIfNeeded(data, preserveHorizontalVelocity);
|
||||||
|
LockFacingIfNeeded(data);
|
||||||
|
IgnoreCollisionsIfNeeded(data);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await HitRoutine(data, token);
|
await HitRoutine(data, token);
|
||||||
}
|
}
|
||||||
catch (System.OperationCanceledException) { }
|
catch (System.OperationCanceledException) { }
|
||||||
|
RestoreIgnoredLayerCollisionsWhenClear(data.IgnoredCollisionLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Awaitable HitRoutine(AttackData data, CancellationToken token)
|
private async void PerformMotion(ActionData data)
|
||||||
|
{
|
||||||
|
if (data == null || IsMotionOnCooldown(data)) return;
|
||||||
|
|
||||||
|
CancelAttack();
|
||||||
|
_motionCts?.Cancel();
|
||||||
|
_motionCts?.Dispose();
|
||||||
|
_motionCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
CancellationToken token = _motionCts.Token;
|
||||||
|
|
||||||
|
SetMotionCooldown(data);
|
||||||
|
|
||||||
|
FaceMotionDirection(data);
|
||||||
|
PlayActionAnimation(data);
|
||||||
|
|
||||||
|
LockMovementIfNeeded(data);
|
||||||
|
LockFacingIfNeeded(data);
|
||||||
|
IgnoreCollisionsIfNeeded(data);
|
||||||
|
|
||||||
|
bool completed = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MotionRoutine(data, token);
|
||||||
|
completed = true;
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException) { }
|
||||||
|
|
||||||
|
if (completed)
|
||||||
|
{
|
||||||
|
StopActionVelocity(data);
|
||||||
|
PlayIdleAnimation();
|
||||||
|
}
|
||||||
|
RestoreIgnoredLayerCollisionsWhenClear(data.IgnoredCollisionLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
|
||||||
|
{
|
||||||
|
float elapsed = 0f;
|
||||||
|
float duration = Mathf.Max(data.MotionDuration, 0.01f);
|
||||||
|
|
||||||
|
while (elapsed < duration)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
float normalizedTime = Mathf.Clamp01(elapsed / duration);
|
||||||
|
ApplyActionVelocity(data, normalizedTime);
|
||||||
|
|
||||||
|
if (data.ReturnToIdleOnAnimationComplete && IsActionAnimationComplete(data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Awaitable.NextFrameAsync(token);
|
||||||
|
elapsed += Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelAttack()
|
||||||
|
{
|
||||||
|
_attackCts?.Cancel();
|
||||||
|
_attackCooldownTimer = 0f;
|
||||||
|
_pendingInput = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyActionVelocity(ActionData data, float normalizedTime = 0f)
|
||||||
|
{
|
||||||
|
float direction = GetMotionDirection(data);
|
||||||
|
float speedMultiplier = data.MotionSpeedCurve != null
|
||||||
|
? data.MotionSpeedCurve.Evaluate(normalizedTime)
|
||||||
|
: 1f;
|
||||||
|
Vector2 velocity = data.Velocity * speedMultiplier;
|
||||||
|
velocity.x *= direction;
|
||||||
|
velocity.x = GetBodyBlockedXVelocity(velocity.x, data);
|
||||||
|
|
||||||
|
if (data.PreserveYVelocity)
|
||||||
|
velocity.y = _rb.linearVelocity.y;
|
||||||
|
|
||||||
|
_rb.linearVelocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopActionVelocity(ActionData data)
|
||||||
|
{
|
||||||
|
if (!data.StopHorizontalVelocityOnEnd) return;
|
||||||
|
|
||||||
|
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsActionAnimationComplete(ActionData data)
|
||||||
|
{
|
||||||
|
if (_anim == null || string.IsNullOrEmpty(data.AnimationState)) return false;
|
||||||
|
|
||||||
|
AnimatorStateInfo stateInfo = _anim.GetCurrentAnimatorStateInfo(0);
|
||||||
|
return stateInfo.IsName(data.AnimationState) && stateInfo.normalizedTime >= 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVelocity = false)
|
||||||
|
{
|
||||||
|
if (data.CanMoveDuringAction) return;
|
||||||
|
|
||||||
|
if (!preserveHorizontalVelocity && !data.HasMotion)
|
||||||
|
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||||
|
|
||||||
|
_inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LockFacingIfNeeded(ActionData data)
|
||||||
|
{
|
||||||
|
if (data.CanTurnDuringAction) return;
|
||||||
|
|
||||||
|
_facingLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayIdleAnimation()
|
||||||
|
{
|
||||||
|
if (_anim == null) return;
|
||||||
|
|
||||||
|
_animationSpeedCts?.Cancel();
|
||||||
|
_anim.speed = 1f;
|
||||||
|
if (!string.IsNullOrEmpty(_idleAnimationState))
|
||||||
|
_anim.Play(_idleAnimationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayActionAnimation(ActionData data)
|
||||||
|
{
|
||||||
|
if (_anim == null) return;
|
||||||
|
|
||||||
|
_animationSpeedCts?.Cancel();
|
||||||
|
_animationSpeedCts?.Dispose();
|
||||||
|
_animationSpeedCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
|
||||||
|
_anim.speed = GetAnimationSpeed(data, 0f);
|
||||||
|
if (!string.IsNullOrEmpty(data.AnimationState))
|
||||||
|
_anim.Play(data.AnimationState);
|
||||||
|
|
||||||
|
ApplyAnimationSpeedCurve(data, _animationSpeedCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ApplyAnimationSpeedCurve(ActionData data, CancellationToken token)
|
||||||
|
{
|
||||||
|
float duration = Mathf.Max(data.MotionDuration, data.HitTiming + data.HitDuration, 0.01f);
|
||||||
|
float elapsed = 0f;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (elapsed < duration)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
float normalizedTime = Mathf.Clamp01(elapsed / duration);
|
||||||
|
_anim.speed = GetAnimationSpeed(data, normalizedTime);
|
||||||
|
|
||||||
|
await Awaitable.NextFrameAsync(token);
|
||||||
|
elapsed += Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetAnimationSpeed(ActionData data, float normalizedTime)
|
||||||
|
{
|
||||||
|
float curveMultiplier = data.AnimationSpeedCurve != null
|
||||||
|
? data.AnimationSpeedCurve.Evaluate(normalizedTime)
|
||||||
|
: 1f;
|
||||||
|
|
||||||
|
return Mathf.Max(data.AnimationSpeed * curveMultiplier, 0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetMotionDirection(ActionData data)
|
||||||
|
{
|
||||||
|
if (data.UseInputDirection && _moveInputX != 0f)
|
||||||
|
return _moveInputX;
|
||||||
|
|
||||||
|
return _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FaceMotionDirection(ActionData data)
|
||||||
|
{
|
||||||
|
if (_spriteRenderer == null) return;
|
||||||
|
if (!data.UseInputDirection || _moveInputX == 0f) return;
|
||||||
|
|
||||||
|
_spriteRenderer.flipX = _moveInputX < 0f;
|
||||||
|
_facingLockTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Awaitable HitRoutine(ActionData data, CancellationToken token)
|
||||||
{
|
{
|
||||||
float attackStartTime = Time.time;
|
float attackStartTime = Time.time;
|
||||||
|
|
||||||
|
if (!data.HasHit)
|
||||||
|
{
|
||||||
|
if (data.MotionDuration > 0f)
|
||||||
|
await Awaitable.WaitForSecondsAsync(data.MotionDuration, token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.HitTiming > 0f)
|
if (data.HitTiming > 0f)
|
||||||
await Awaitable.WaitForSecondsAsync(data.HitTiming, token);
|
await Awaitable.WaitForSecondsAsync(data.HitTiming, token);
|
||||||
|
|
||||||
@@ -245,11 +740,10 @@ private async Awaitable HitRoutine(AttackData data, CancellationToken token)
|
|||||||
if (remaining > 0f)
|
if (remaining > 0f)
|
||||||
await Awaitable.WaitForSecondsAsync(remaining, token);
|
await Awaitable.WaitForSecondsAsync(remaining, token);
|
||||||
|
|
||||||
if (_anim != null && !string.IsNullOrEmpty(_idleAnimationState))
|
PlayIdleAnimation();
|
||||||
_anim.Play(_idleAnimationState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyDamageInArea(AttackData data, HashSet<IDamageable> alreadyHit)
|
private void ApplyDamageInArea(ActionData data, HashSet<IDamageable> alreadyHit)
|
||||||
{
|
{
|
||||||
Vector2 center = GetAttackCenter(data.Offset);
|
Vector2 center = GetAttackCenter(data.Offset);
|
||||||
|
|
||||||
@@ -268,10 +762,23 @@ private void ApplyDamageInArea(AttackData data, HashSet<IDamageable> alreadyHit)
|
|||||||
alreadyHit.Add(target);
|
alreadyHit.Add(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
target.TakeDamage(data.Damage);
|
target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2 GetHitVelocity(Vector2 hitVelocity)
|
||||||
|
{
|
||||||
|
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||||
|
hitVelocity.x *= facing;
|
||||||
|
return hitVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetActionName(ActionData data)
|
||||||
|
{
|
||||||
|
if (data == null) return string.Empty;
|
||||||
|
return string.IsNullOrEmpty(data.ActionName) ? data.name : data.ActionName;
|
||||||
|
}
|
||||||
|
|
||||||
private Vector2 GetAttackCenter(Vector2 offset)
|
private Vector2 GetAttackCenter(Vector2 offset)
|
||||||
{
|
{
|
||||||
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||||
@@ -338,7 +845,7 @@ private void DrawLastAttackGizmo()
|
|||||||
float elapsed = Time.time - _lastAttackGizmoTime;
|
float elapsed = Time.time - _lastAttackGizmoTime;
|
||||||
if (elapsed < 0f) return;
|
if (elapsed < 0f) return;
|
||||||
|
|
||||||
AttackData data = _lastAttackGizmoData;
|
ActionData data = _lastAttackGizmoData;
|
||||||
float activeDuration = Mathf.Max(data.HitDuration, 0.05f);
|
float activeDuration = Mathf.Max(data.HitDuration, 0.05f);
|
||||||
float fadeDuration = 0.4f;
|
float fadeDuration = 0.4f;
|
||||||
float total = activeDuration + fadeDuration;
|
float total = activeDuration + fadeDuration;
|
||||||
@@ -360,7 +867,7 @@ private void OnGUI()
|
|||||||
{
|
{
|
||||||
if (!_showAttackDebug || _lastAttackGizmoData == null) return;
|
if (!_showAttackDebug || _lastAttackGizmoData == null) return;
|
||||||
|
|
||||||
AttackData data = _lastAttackGizmoData;
|
ActionData data = _lastAttackGizmoData;
|
||||||
float elapsed = Time.time - _attackStartTime;
|
float elapsed = Time.time - _attackStartTime;
|
||||||
|
|
||||||
string status = _hitFired
|
string status = _hitFired
|
||||||
@@ -375,7 +882,7 @@ private void OnGUI()
|
|||||||
: (elapsed >= _bufferOpenTime && _attackCooldownTimer > 0f ? "ready to buffer" : "-");
|
: (elapsed >= _bufferOpenTime && _attackCooldownTimer > 0f ? "ready to buffer" : "-");
|
||||||
|
|
||||||
string info =
|
string info =
|
||||||
$"<b>{(string.IsNullOrEmpty(data.AttackName) ? data.name : data.AttackName)}</b> [{status}]\n" +
|
$"<b>{GetActionName(data)}</b> [{status}]\n" +
|
||||||
$"Elapsed : {elapsed:F3} s\n" +
|
$"Elapsed : {elapsed:F3} s\n" +
|
||||||
$"HitTiming : {data.HitTiming:F3} s\n" +
|
$"HitTiming : {data.HitTiming:F3} s\n" +
|
||||||
$"HitDuration : {data.HitDuration:F3} s\n" +
|
$"HitDuration : {data.HitDuration:F3} s\n" +
|
||||||
@@ -397,7 +904,7 @@ private void OnGUI()
|
|||||||
DrawTimelineBar(data, elapsed);
|
DrawTimelineBar(data, elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTimelineBar(AttackData data, float elapsed)
|
private void DrawTimelineBar(ActionData data, float elapsed)
|
||||||
{
|
{
|
||||||
float barX = 10f;
|
float barX = 10f;
|
||||||
float barY = 302f;
|
float barY = 302f;
|
||||||
@@ -431,4 +938,16 @@ private void DrawTimelineBar(AttackData data, float elapsed)
|
|||||||
|
|
||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly struct IgnoredLayerCollision
|
||||||
|
{
|
||||||
|
public readonly int LayerA;
|
||||||
|
public readonly int LayerB;
|
||||||
|
|
||||||
|
public IgnoredLayerCollision(int layerA, int layerB)
|
||||||
|
{
|
||||||
|
LayerA = layerA;
|
||||||
|
LayerB = layerB;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/05_Data/Combo/Combo_Dash.asset
LFS
Normal file
BIN
Assets/05_Data/Combo/Combo_Dash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Combo/Combo_Dash.asset.meta
Normal file
8
Assets/05_Data/Combo/Combo_Dash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c2c47ad8605b4f23a77d8f77bcebe312
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/05_Data/Combo/Combo_Roll.asset
LFS
Normal file
BIN
Assets/05_Data/Combo/Combo_Roll.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Combo/Combo_Roll.asset.meta
Normal file
8
Assets/05_Data/Combo/Combo_Roll.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 25cae4aa2eed34745ae44f422fe7db0b
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/05_Data/Motion.meta
Normal file
8
Assets/05_Data/Motion.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1c9e5f4a7b3649ce8f2a53d91b0c7e6a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/05_Data/Motion/Dash.asset
LFS
Normal file
BIN
Assets/05_Data/Motion/Dash.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Motion/Dash.asset.meta
Normal file
8
Assets/05_Data/Motion/Dash.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b31f0b5b733f4f1a8c9cb9bb96a5334e
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/05_Data/Motion/Roll.asset
LFS
Normal file
BIN
Assets/05_Data/Motion/Roll.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Motion/Roll.asset.meta
Normal file
8
Assets/05_Data/Motion/Roll.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8604a664935ecec4b82c162eca1ba4b6
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -41,6 +41,24 @@
|
|||||||
"processors": "",
|
"processors": "",
|
||||||
"interactions": "",
|
"interactions": "",
|
||||||
"initialStateCheck": false
|
"initialStateCheck": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dash",
|
||||||
|
"type": "Button",
|
||||||
|
"id": "4245d8e3-7e61-4548-84af-75512958eb2f",
|
||||||
|
"expectedControlType": "",
|
||||||
|
"processors": "",
|
||||||
|
"interactions": "",
|
||||||
|
"initialStateCheck": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roll",
|
||||||
|
"type": "Button",
|
||||||
|
"id": "7e00ae7c-ad0c-460d-be3d-0072054ceb9c",
|
||||||
|
"expectedControlType": "",
|
||||||
|
"processors": "",
|
||||||
|
"interactions": "",
|
||||||
|
"initialStateCheck": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bindings": [
|
"bindings": [
|
||||||
@@ -102,7 +120,7 @@
|
|||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"id": "b9d8e2f3-4a5c-4b6d-8e7f-901234567890",
|
"id": "b9d8e2f3-4a5c-4b6d-8e7f-901234567890",
|
||||||
"path": "<Keyboard>/space",
|
"path": "<Keyboard>/c",
|
||||||
"interactions": "",
|
"interactions": "",
|
||||||
"processors": "",
|
"processors": "",
|
||||||
"groups": "",
|
"groups": "",
|
||||||
@@ -131,6 +149,28 @@
|
|||||||
"action": "Kick",
|
"action": "Kick",
|
||||||
"isComposite": false,
|
"isComposite": false,
|
||||||
"isPartOfComposite": false
|
"isPartOfComposite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"id": "8bca2399-c8c8-41e8-a3b2-08267c8bc571",
|
||||||
|
"path": "<Keyboard>/leftShift",
|
||||||
|
"interactions": "",
|
||||||
|
"processors": "",
|
||||||
|
"groups": "",
|
||||||
|
"action": "Dash",
|
||||||
|
"isComposite": false,
|
||||||
|
"isPartOfComposite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"id": "df4d72ec-012c-413a-862d-1a36d2c5b69a",
|
||||||
|
"path": "<Keyboard>/space",
|
||||||
|
"interactions": "",
|
||||||
|
"processors": "",
|
||||||
|
"groups": "",
|
||||||
|
"action": "Roll",
|
||||||
|
"isComposite": false,
|
||||||
|
"isPartOfComposite": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user