using System.Collections; using System.Collections.Generic; using UnityEngine; using Platformer.Gameplay; using static Platformer.Core.Simulation; using Platformer.Model; using Platformer.Core; using UnityEngine.InputSystem; namespace Platformer.Mechanics { /// /// This is the main class used to implement control of the player. /// It is a superset of the AnimationController class, but is inlined to allow for any kind of customisation. /// public class PlayerController : KinematicObject { public AudioClip jumpAudio; public AudioClip respawnAudio; public AudioClip ouchAudio; /// /// Attack sound effect (BT5-Dev 2단계 신설). 미지정 시 무음. /// public AudioClip attackAudio; /// /// Max horizontal speed of the player. /// public float maxSpeed = 7; /// /// Initial jump velocity at the start of a jump. /// public float jumpTakeOffSpeed = 7; /// /// Cooldown between attacks in seconds (기획 04 §5-1 Phase 3-B 튠 대상). /// public float attackCooldown = 0.35f; /// /// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크). /// public AttackHitbox attackHitbox; public JumpState jumpState = JumpState.Grounded; private bool stopJump; /*internal new*/ public Collider2D collider2d; /*internal new*/ public AudioSource audioSource; public Health health; public bool controlEnabled = true; bool jump; Vector2 move; SpriteRenderer spriteRenderer; internal Animator animator; readonly PlatformerModel model = Simulation.GetModel(); private InputAction m_MoveAction; private InputAction m_JumpAction; private InputAction m_AttackAction; // 마지막 공격 시각 — attackCooldown과 비교해 연타 제한 float nextAttackTime = 0f; // 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지) Vector2 facing = Vector2.right; public Bounds Bounds => collider2d.bounds; void Awake() { health = GetComponent(); audioSource = GetComponent(); collider2d = GetComponent(); spriteRenderer = GetComponent(); animator = GetComponent(); if (attackHitbox == null) attackHitbox = GetComponent(); m_MoveAction = InputSystem.actions.FindAction("Player/Move"); m_JumpAction = InputSystem.actions.FindAction("Player/Jump"); m_AttackAction = InputSystem.actions.FindAction("Player/Attack"); m_MoveAction.Enable(); m_JumpAction.Enable(); if (m_AttackAction != null) m_AttackAction.Enable(); } protected override void Update() { if (controlEnabled) { move.x = m_MoveAction.ReadValue().x; if (jumpState == JumpState.Grounded && m_JumpAction.WasPressedThisFrame()) jumpState = JumpState.PrepareToJump; else if (m_JumpAction.WasReleasedThisFrame()) { stopJump = true; Schedule().player = this; } // 공격 입력 처리 (마우스 좌클릭 / 게임패드 RT / 모바일 터치 — Phase 3-B UX) if (m_AttackAction != null && m_AttackAction.WasPressedThisFrame() && Time.time >= nextAttackTime) { nextAttackTime = Time.time + attackCooldown; var ev = Schedule(); ev.player = this; ev.direction = facing; } } else { move.x = 0; } UpdateJumpState(); base.Update(); } void UpdateJumpState() { jump = false; switch (jumpState) { case JumpState.PrepareToJump: jumpState = JumpState.Jumping; jump = true; stopJump = false; break; case JumpState.Jumping: if (!IsGrounded) { Schedule().player = this; jumpState = JumpState.InFlight; } break; case JumpState.InFlight: if (IsGrounded) { Schedule().player = this; jumpState = JumpState.Landed; } break; case JumpState.Landed: jumpState = JumpState.Grounded; break; } } protected override void ComputeVelocity() { if (jump && IsGrounded) { velocity.y = jumpTakeOffSpeed * model.jumpModifier; jump = false; } else if (stopJump) { stopJump = false; if (velocity.y > 0) { velocity.y = velocity.y * model.jumpDeceleration; } } if (move.x > 0.01f) { spriteRenderer.flipX = false; facing = Vector2.right; } else if (move.x < -0.01f) { spriteRenderer.flipX = true; facing = Vector2.left; } animator.SetBool("grounded", IsGrounded); animator.SetFloat("velocityX", Mathf.Abs(velocity.x) / maxSpeed); targetVelocity = move * maxSpeed; } public enum JumpState { Grounded, PrepareToJump, Jumping, InFlight, Landed } } }