기본 공격 구현

This commit is contained in:
2026-09-24 01:52:29 +09:00
parent 4a08a9d085
commit da4075e42e
36 changed files with 570 additions and 425 deletions

View File

@@ -230,14 +230,25 @@ private static AnimatorController BuildController(Dictionary<string, AnimationCl
{
name = "MoveY", type = AnimatorControllerParameterType.Float, defaultFloat = -1f
});
//공격은 대각선 4방향뿐이라 이동 방향과 파라미터를 나눈다.
//MoveX/MoveY를 그대로 쓰면 정면(상하좌우)을 볼 때 두 대각선 클립의 가중치가 같아져서
//어느 쪽이 나올지 알 수 없고, 공격이 끝난 뒤 바라보는 방향까지 대각선으로 틀어진다
controller.AddParameter(new AnimatorControllerParameter
{
name = "AttackX", type = AnimatorControllerParameterType.Float, defaultFloat = 0.7071f
});
controller.AddParameter(new AnimatorControllerParameter
{
name = "AttackY", type = AnimatorControllerParameterType.Float, defaultFloat = -0.7071f
});
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
controller.AddParameter("Attack", AnimatorControllerParameterType.Trigger);
controller.AddParameter("Roll", AnimatorControllerParameterType.Trigger);
AnimatorState idle = AddDirectionalState(controller, "Idle", Dirs8, d => clips["Idle_" + d]);
AnimatorState move = AddDirectionalState(controller, "Move", Dirs8, d => clips["Move_" + d]);
AnimatorState roll = AddDirectionalState(controller, "Roll", Dirs8, d => clips["Roll_" + d]);
AnimatorState slash = AddDirectionalState(controller, "Slash", Dirs4, d => clips["Slash_" + d]);
AnimatorState idle = AddDirectionalState(controller, "Idle", Dirs8, d => clips["Idle_" + d], "MoveX", "MoveY");
AnimatorState move = AddDirectionalState(controller, "Move", Dirs8, d => clips["Move_" + d], "MoveX", "MoveY");
AnimatorState roll = AddDirectionalState(controller, "Roll", Dirs8, d => clips["Roll_" + d], "MoveX", "MoveY");
AnimatorState slash = AddDirectionalState(controller, "Slash", Dirs4, d => clips["Slash_" + d], "AttackX", "AttackY");
AnimatorStateMachine stateMachine = controller.layers[0].stateMachine;
stateMachine.defaultState = idle;
@@ -316,14 +327,15 @@ private static void ClearController(AnimatorController controller)
/// <summary>8방향(혹은 4방향) 클립을 2D 블렌드 트리 하나로 묶은 스테이트를 만든다.</summary>
private static AnimatorState AddDirectionalState(AnimatorController controller, string name,
string[] dirs, Func<string, AnimationClip> pick)
string[] dirs, Func<string, AnimationClip> pick,
string paramX, string paramY)
{
AnimatorState state = controller.CreateBlendTreeInController(name, out BlendTree tree, 0);
tree.name = name;
tree.blendType = BlendTreeType.SimpleDirectional2D;
tree.blendParameter = "MoveX";
tree.blendParameterY = "MoveY";
tree.blendParameter = paramX;
tree.blendParameterY = paramY;
foreach (string dir in dirs)
{

View File

@@ -101,6 +101,16 @@ public @GameInput()
""interactions"": """",
""initialStateCheck"": true,
""priority"": 0
},
{
""name"": ""Attack"",
""type"": ""Button"",
""id"": ""bf2fbb7b-fd81-4ec4-824d-26ff5ce88e66"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false,
""priority"": 0
}
],
""bindings"": [
@@ -158,6 +168,17 @@ public @GameInput()
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": """",
""id"": ""7f4c479d-7d04-4598-9df4-cec2ca3408bf"",
""path"": ""<Keyboard>/space"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Attack"",
""isComposite"": false,
""isPartOfComposite"": false
}
]
}
@@ -167,6 +188,7 @@ public @GameInput()
// Character
m_Character = asset.FindActionMap("Character", throwIfNotFound: true);
m_Character_Move = m_Character.FindAction("Move", throwIfNotFound: true);
m_Character_Attack = m_Character.FindAction("Attack", throwIfNotFound: true);
}
~@GameInput()
@@ -248,6 +270,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
private readonly InputActionMap m_Character;
private List<ICharacterActions> m_CharacterActionsCallbackInterfaces = new List<ICharacterActions>();
private readonly InputAction m_Character_Move;
private readonly InputAction m_Character_Attack;
/// <summary>
/// Provides access to input actions defined in input action map "Character".
/// </summary>
@@ -264,6 +287,10 @@ public struct CharacterActions
/// </summary>
public InputAction @Move => m_Wrapper.m_Character_Move;
/// <summary>
/// Provides access to the underlying input action "Character/Attack".
/// </summary>
public InputAction @Attack => m_Wrapper.m_Character_Attack;
/// <summary>
/// Provides access to the underlying input action map instance.
/// </summary>
public InputActionMap Get() { return m_Wrapper.m_Character; }
@@ -292,6 +319,9 @@ public void AddCallbacks(ICharacterActions instance)
@Move.started += instance.OnMove;
@Move.performed += instance.OnMove;
@Move.canceled += instance.OnMove;
@Attack.started += instance.OnAttack;
@Attack.performed += instance.OnAttack;
@Attack.canceled += instance.OnAttack;
}
/// <summary>
@@ -306,6 +336,9 @@ private void UnregisterCallbacks(ICharacterActions instance)
@Move.started -= instance.OnMove;
@Move.performed -= instance.OnMove;
@Move.canceled -= instance.OnMove;
@Attack.started -= instance.OnAttack;
@Attack.performed -= instance.OnAttack;
@Attack.canceled -= instance.OnAttack;
}
/// <summary>
@@ -353,5 +386,12 @@ public interface ICharacterActions
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnMove(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "Attack" 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 OnAttack(InputAction.CallbackContext context);
}
}

View File

@@ -1,2 +1,2 @@
fileFormatVersion: 2
guid: b37768cc3bc80a445892592cfe1807a9
guid: dacd04d022fdbea4f8089c07c8a9ba2a

View File

@@ -10,6 +10,7 @@ public class InputManager : MonoBehaviour, GameInput.ICharacterActions
private GameInput _input;
public event Action<Vector2> OnMove_Event;
public event Action OnAttack_Event;
private void Awake()
{
@@ -39,4 +40,10 @@ public void OnMove(InputAction.CallbackContext ctx)
if (ctx.phase == InputActionPhase.Performed || ctx.phase == InputActionPhase.Canceled)
OnMove_Event?.Invoke(ctx.ReadValue<Vector2>());
}
public void OnAttack(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Started)
OnAttack_Event?.Invoke();
}
}

View File

@@ -8,12 +8,49 @@ public class PlayerController : MonoBehaviour,ISceneInitializable
private const string MoveXParam = "MoveX";
private const string MoveYParam = "MoveY";
private const string SpeedParam = "Speed";
private const string AttackParam = "Attack";
private const string AttackXParam = "AttackX";
private const string AttackYParam = "AttackY";
// 방향은 8개로 고정이다. 각도가 아니라 이 중 하나로 분류해서 들고 다닌다
private enum Direction
{
Right, UpRight, Up, UpLeft, Left, DownLeft, Down, DownRight
}
// 블렌드 트리에 박아둔 자식 좌표와 같은 값이어야 가중치가 그 클립에 온전히 실린다
private static readonly Vector2[] DirectionVectors =
{
new Vector2( 1f, 0f), // Right
new Vector2( 0.7071f, 0.7071f), // UpRight
new Vector2( 0f, 1f), // Up
new Vector2(-0.7071f, 0.7071f), // UpLeft
new Vector2(-1f, 0f), // Left
new Vector2(-0.7071f, -0.7071f), // DownLeft
new Vector2( 0f, -1f), // Down
new Vector2( 0.7071f, -0.7071f) // DownRight
};
// 오른손으로 휘두르므로 정면을 볼 땐 시계방향 45도 쪽을 벤다.
// 대각선을 볼 땐 그대로 (Slash 시트가 대각선 4장뿐이라 정면 그림 자체가 없다)
private static readonly Direction[] AttackDirections =
{
Direction.DownRight, // Right -> DownRight
Direction.UpRight, // UpRight -> UpRight
Direction.UpRight, // Up -> UpRight
Direction.UpLeft, // UpLeft -> UpLeft
Direction.UpLeft, // Left -> UpLeft
Direction.DownLeft, // DownLeft -> DownLeft
Direction.DownLeft, // Down -> DownLeft
Direction.DownRight // DownRight -> DownRight
};
[SerializeField] private float _moveSpeed = 4f;
private Rigidbody2D _rigidbody;
private Animator _animator;
private Vector2 _moveInput;
private Direction _facing = Direction.Down;
private void Awake()
{
@@ -26,6 +63,8 @@ public void OnSceneLoaded()
// 씬이 다시 로드돼도 중복 구독되지 않도록 먼저 떼고 붙인다
InputManager.Instance.OnMove_Event -= this.OnMove;
InputManager.Instance.OnMove_Event += this.OnMove;
InputManager.Instance.OnAttack_Event -= this.OnAttack;
InputManager.Instance.OnAttack_Event += this.OnAttack;
}
private void OnDestroy()
@@ -33,6 +72,7 @@ private void OnDestroy()
if (InputManager.Instance != null)
{
InputManager.Instance.OnMove_Event -= this.OnMove;
InputManager.Instance.OnAttack_Event -= this.OnAttack;
}
}
@@ -44,29 +84,42 @@ private void OnMove(Vector2 moveInput)
if (_moveInput.sqrMagnitude > 0.01f)
{
// 블렌드 트리는 스프라이트를 보간하지 못한다. 8방향 중 하나로 스냅해서 넣어야
// 한 클립에 가중치가 온전히 실려서 방향 경계에서 깜빡이지 않는다
Vector2 facing = SnapTo8(_moveInput);
_animator.SetFloat(MoveXParam, facing.x);
_animator.SetFloat(MoveYParam, facing.y);
_facing = ToDirection(_moveInput);
Vector2 facingVector = DirectionVectors[(int)_facing];
_animator.SetFloat(MoveXParam, facingVector.x);
_animator.SetFloat(MoveYParam, facingVector.y);
}
// 입력이 0이면 MoveX/MoveY를 건드리지 않는다 -> 멈춰도 마지막으로 보던 방향을 유지
// 입력이 0이면 방향을 갱신하지 않는다 -> 멈춰도 마지막으로 보던 을 유지
_animator.SetFloat(SpeedParam, _moveInput.magnitude);
}
private void OnAttack()
{
// Slash 블렌드 트리는 MoveX/MoveY가 아니라 AttackX/AttackY를 읽는다.
// 그래서 공격이 끝나도 바라보던 방향은 그대로 남는다
Vector2 attackVector = DirectionVectors[(int)AttackDirections[(int)_facing]];
_animator.SetFloat(AttackXParam, attackVector.x);
_animator.SetFloat(AttackYParam, attackVector.y);
_animator.SetTrigger(AttackParam);
}
private void FixedUpdate()
{
// Dynamic 바디지만 힘을 주지 않고 속도를 직접 대입하므로 관성 없이 딱딱 멈춘다
_rigidbody.linearVelocity = _moveInput * _moveSpeed;
}
// 입력 방향을 가장 가까운 45도 배수로 스냅한다 (대각선은 0.7071이 되어 블렌드 트리 좌표와 일치)
private static Vector2 SnapTo8(Vector2 direction)
/// <summary>
/// 입력 각도를 45도씩 8개 구간으로 나눠 분류한다.
/// 예를 들어 Up은 90도 한 점이 아니라 67.5 ~ 112.5도 구간 전체다.
/// </summary>
private static Direction ToDirection(Vector2 input)
{
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
float snapped = Mathf.Round(angle / 45f) * 45f * Mathf.Deg2Rad;
float angle = Mathf.Atan2(input.y, input.x) * Mathf.Rad2Deg;
int sector = Mathf.RoundToInt(angle / 45f);
return new Vector2(Mathf.Cos(snapped), Mathf.Sin(snapped));
return (Direction)((sector + 8) % 8); // -180도는 -4 -> 4(Left)로 접힌다
}
}