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. 자동 발동 시 PlayerAttack.Execute가 PlayOneShot으로 재생. /// BT7-Plan 2026-04-24 — VS 순수형 자동 발동 전환 후에도 audio hook 유지. /// 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; /// /// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크). /// 공격은 PlayerAttackTicker가 주기적으로 Schedule을 발화하여 실행. /// public AttackHitbox attackHitbox; public JumpState jumpState = JumpState.Grounded; private bool stopJump; // BT69 — Down + Jump Drop-Through (PD 명시 2026-05-08): Down 누른 상태에서 점프 시 발판 통과 + 점프 모션 private float dropThroughTimer = 0f; // 활성 시간 동안 Layer 16 mask 강제 OFF private const float DROP_THROUGH_DURATION = 0.3f; // Drop-Through 활성 지속 시간 (초) private bool dropThroughJump = false; // 본 frame Drop-Through 점프 발동 여부 (velocity.y 처리 분기) /*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; // 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지). // BT7-Plan 2026-04-24 — PlayerAttackTicker가 자동 발동 시 참조하므로 public 노출. Vector2 facing = Vector2.right; /// 현재 플레이어 facing 방향. PlayerAttackTicker가 Schedule 시점에 참조. public Vector2 Facing => facing; public Bounds Bounds => collider2d.bounds; /// /// 마지막 grounded 위치 — 낙사 시 안전 복귀 영역 (PD 지시 2026-05-07). /// public Vector3 LastGroundedPosition { get; private set; } void Awake() { health = GetComponent(); audioSource = GetComponent(); collider2d = GetComponent(); spriteRenderer = GetComponent(); if (spriteRenderer == null) spriteRenderer = GetComponentInChildren(); animator = GetComponent(); if (animator == null) animator = GetComponentInChildren(); // PD 지시 2026-05-07 — 동반 컴포넌트 자동 부착 (Inspector 부착 불요) if (GetComponent() == null) gameObject.AddComponent(); if (GetComponent() == null) gameObject.AddComponent(); // BT5-Dev #34 — PlatformDropThrough 폐기 (PlatformEffector2D 표준 패턴 활용) var oldDrop = GetComponent(); if (oldDrop != null) Destroy(oldDrop); // PD 지시 2026-05-07 — 사망 시 입력 차단 / 부활 시 입력 복원 if (health != null) { health.OnDeathEvent += OnHealthDeath; health.OnResurrectEvent += OnHealthResurrect; } if (attackHitbox == null) attackHitbox = GetComponent(); m_MoveAction = InputSystem.actions.FindAction("Player/Move"); m_JumpAction = InputSystem.actions.FindAction("Player/Jump"); m_MoveAction.Enable(); m_JumpAction.Enable(); LastGroundedPosition = transform.position; } void OnDestroy() { if (health != null) { health.OnDeathEvent -= OnHealthDeath; health.OnResurrectEvent -= OnHealthResurrect; } } // BT5-Dev #30 — 발판 GameObject 영역 식별 진단 (PD 영역 영역 영역 부딪힘 시 Console 출력) void OnCollisionEnter2D(Collision2D col) { if (col.gameObject == null) return; int layer = col.gameObject.layer; string name = col.gameObject.name; Debug.Log($"[BT30-Collide] name='{name}' layer={layer} (8=JumpThrough, 0=Default)"); } void OnHealthDeath() { controlEnabled = false; move = Vector2.zero; } void OnHealthResurrect() { controlEnabled = true; } protected override void Update() { if (controlEnabled) { Vector2 moveInput = m_MoveAction.ReadValue(); move.x = moveInput.x; if (jumpState == JumpState.Grounded && m_JumpAction.WasPressedThisFrame()) { // BT69 — Down + Jump = Drop-Through (PD 명시 2026-05-08) // BT70 — 발판(Layer 16) 위 검증 추가 (PD 보고 2026-05-08: 지면 위 Down + Jump = 점프 X 버그) // 발판 위만 Drop-Through 발동·지면 위 = 일반 점프 bool downHeld = moveInput.y < -0.5f; bool onJumpThroughPlatform = false; if (downHeld && collider2d != null) { Vector2 dropFootPos = new Vector2(collider2d.bounds.center.x, collider2d.bounds.min.y + 0.02f); int jumpThroughMask = 1 << JUMP_THROUGH_LAYER; RaycastHit2D dropFootHit = Physics2D.Raycast(dropFootPos, Vector2.down, 0.1f, jumpThroughMask); onJumpThroughPlatform = dropFootHit.collider != null; } if (downHeld && onJumpThroughPlatform) { dropThroughTimer = DROP_THROUGH_DURATION; dropThroughJump = true; } jumpState = JumpState.PrepareToJump; } else if (m_JumpAction.WasReleasedThisFrame()) { stopJump = true; Schedule().player = this; } // 공격은 PlayerAttackTicker가 자동 주기로 Schedule을 발화 (BT7-Plan VS 순수형). } else { move.x = 0; } // BT69 — Drop-Through Timer 감소 (Layer 16 mask 강제 OFF 지속 시간) if (dropThroughTimer > 0f) dropThroughTimer -= Time.deltaTime; UpdateJumpState(); base.Update(); // PD 지시 2026-05-07 — 낙사 시 복귀할 안전 위치 추적 if (IsGrounded) LastGroundedPosition = transform.position; } // BT5-Dev #40 — 개발팀장 진단: KinematicObject.Start() contactFilter 캐싱 우회 // Physics2D.IgnoreLayerCollision은 raycast contactFilter 영역 무관. SetLayerMask 직접 갱신 의무. const int JUMP_THROUGH_LAYER = 16; void UpdateJumpState() { jump = false; switch (jumpState) { case JumpState.PrepareToJump: jumpState = JumpState.Jumping; jump = true; stopJump = false; // BT41 — PrepareToJump frame 영역 즉시 contactFilter Layer 16 mask OFF (다음 ComputeVelocity 영역 velocity.y 적용 영역 영역 frame 영역 발판 영역 충돌 차단) { int baseMaskJump = Physics2D.GetLayerCollisionMask(gameObject.layer); contactFilter.SetLayerMask(baseMaskJump & ~(1 << JUMP_THROUGH_LAYER)); contactFilter.useLayerMask = true; } 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; } // BT40 — Drop-Through: velocity.y > 0(상승) 영역 Layer 16 mask 비활성, 그 외 활성 UpdateContactFilterForDropThrough(); } void UpdateContactFilterForDropThrough() { int baseMask = Physics2D.GetLayerCollisionMask(gameObject.layer); // BT5-Dev #43 — IsGrounded 영역 폐기 (frame 0 미설정 → 떨어짐). footHit 단독 + 점프 영역 OFF 강제 // BT69 — Drop-Through Timer 활성 시 Layer 16 mask 강제 OFF (Down + Jump 입력 발판 통과) bool isJumpingThrough = jumpState == JumpState.Jumping || (jumpState == JumpState.InFlight && velocity.y > 0.01f) || dropThroughTimer > 0f; bool standingOnPlatform = false; if (collider2d != null && !isJumpingThrough) { Vector2 footPos = new Vector2(collider2d.bounds.center.x, collider2d.bounds.min.y + 0.02f); int jumpThroughMask = 1 << JUMP_THROUGH_LAYER; RaycastHit2D footHit = Physics2D.Raycast(footPos, Vector2.down, 0.1f, jumpThroughMask); standingOnPlatform = footHit.collider != null; } // standingOnPlatform=true → Layer 16 ON (착지·서있는 영역 영역 X) / 그 외 → Layer 16 OFF (통과) int mask = standingOnPlatform ? baseMask : (baseMask & ~(1 << JUMP_THROUGH_LAYER)); contactFilter.SetLayerMask(mask); contactFilter.useLayerMask = true; } protected override void ComputeVelocity() { if (jump && IsGrounded) { // BT69 — Drop-Through 점프 분기: Down + Jump 입력 시 위로 점프 X (gravity로 떨어짐 + 점프 모션만 유지) // BT71 — velocity.y = 0 → 음수(-0.5) 정정 (PD 보고 2026-05-08: Drop-Through 후 점프키 X 버그) // 원인: velocity.y=0 시 IsGrounded=true 잔존 → jumpState=Jumping 영구 → 점프 입력 무시 // 정정: 즉시 떨어짐 → IsGrounded=false → Jumping→InFlight→착지→Grounded 정상 사이클 if (dropThroughJump) { velocity.y = -0.5f; // 즉시 낙하 시작 — 발판 통과 + IsGrounded=false 확보 dropThroughJump = false; } else { 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 = true; facing = Vector2.right; } else if (move.x < -0.01f) { spriteRenderer.flipX = false; 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 } } }