93 lines
3.9 KiB
C#
93 lines
3.9 KiB
C#
|
|
using System.Collections.Generic;
|
||
|
|
using Platformer.Gameplay;
|
||
|
|
using UnityEngine;
|
||
|
|
using static Platformer.Core.Simulation;
|
||
|
|
|
||
|
|
namespace Platformer.Mechanics
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 플레이어 근거리 공격 판정 박스.
|
||
|
|
/// PlayerAttack 이벤트에서 Fire(direction)를 호출하면 지정 활성 지속 시간 동안
|
||
|
|
/// OverlapBox 로 적을 감지하고, Health 보유 적에 Decrement 적용 → EnemyDeath 체인.
|
||
|
|
/// 기획 04 §5-1 근거리 공격 1종 — 쿨타임·대미지·판정 박스는 Phase 3-B 튠 대상.
|
||
|
|
/// </summary>
|
||
|
|
public class AttackHitbox : MonoBehaviour
|
||
|
|
{
|
||
|
|
[Header("판정 박스 크기 (플레이어 기준 로컬)")]
|
||
|
|
public Vector2 size = new Vector2(1.2f, 0.9f);
|
||
|
|
[Tooltip("플레이어 중심으로부터 공격 방향으로의 오프셋 거리")]
|
||
|
|
public float offsetDistance = 0.7f;
|
||
|
|
[Tooltip("판정 활성 지속 시간 (초). 정적 스프라이트 기반이면 짧게 유지")]
|
||
|
|
public float activeDuration = 0.12f;
|
||
|
|
[Tooltip("대미지 (Health.Decrement 호출 횟수)")]
|
||
|
|
public int damage = 1;
|
||
|
|
|
||
|
|
[Header("타격 대상 레이어 마스크")]
|
||
|
|
public LayerMask targetLayers = ~0; // 전 레이어 기본. 실전에서 Enemy 레이어로 제한 권장
|
||
|
|
|
||
|
|
float activeUntil = -1f;
|
||
|
|
Vector2 lastDirection = Vector2.right;
|
||
|
|
|
||
|
|
// 같은 스윙으로 동일 Health 중복 타격 방지
|
||
|
|
readonly HashSet<Health> alreadyHit = new HashSet<Health>();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// PlayerAttack.Execute 에서 호출. direction은 플레이어 facing (x축 ±1 or 0).
|
||
|
|
/// </summary>
|
||
|
|
public void Fire(Vector2 direction)
|
||
|
|
{
|
||
|
|
if (Mathf.Abs(direction.x) > 0.01f) lastDirection = new Vector2(Mathf.Sign(direction.x), 0);
|
||
|
|
activeUntil = Time.time + activeDuration;
|
||
|
|
alreadyHit.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
void Update()
|
||
|
|
{
|
||
|
|
if (Time.time > activeUntil) return;
|
||
|
|
|
||
|
|
// 로컬 오프셋: 플레이어 중심 + facing * offsetDistance
|
||
|
|
var center = (Vector2)transform.position + lastDirection * offsetDistance;
|
||
|
|
|
||
|
|
// OverlapBox로 적 검출
|
||
|
|
var hits = Physics2D.OverlapBoxAll(center, size, 0f, targetLayers);
|
||
|
|
foreach (var col in hits)
|
||
|
|
{
|
||
|
|
if (col == null) continue;
|
||
|
|
// 자기 자신 collider 제외 (PlayerController 부착 GameObject)
|
||
|
|
if (col.transform == transform || col.transform.IsChildOf(transform)) continue;
|
||
|
|
|
||
|
|
var health = col.GetComponent<Health>();
|
||
|
|
if (health == null || !health.IsAlive) continue;
|
||
|
|
if (alreadyHit.Contains(health)) continue;
|
||
|
|
|
||
|
|
alreadyHit.Add(health);
|
||
|
|
// BT7-Plan TODO (2026-04-24): 적 Health도 하트 분할 시스템 대상이지만
|
||
|
|
// i-frame 구조 탓에 Decrement() 반복 호출은 첫 호출 후 무효화된다.
|
||
|
|
// Health.Decrement(int damage) 단일 호출로 쿼터 단위 다중 피해 전달.
|
||
|
|
health.Decrement(damage);
|
||
|
|
|
||
|
|
// Enemy 즉사 시 EnemyController 기반 EnemyDeath 체인 발동 (선택)
|
||
|
|
if (!health.IsAlive)
|
||
|
|
{
|
||
|
|
var enemy = col.GetComponent<EnemyController>();
|
||
|
|
if (enemy != null)
|
||
|
|
{
|
||
|
|
Schedule<EnemyDeath>().enemy = enemy;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scene view 가시화 — 활성 상태에서 박스 표시
|
||
|
|
void OnDrawGizmos()
|
||
|
|
{
|
||
|
|
Gizmos.color = (Application.isPlaying && Time.time <= activeUntil)
|
||
|
|
? new Color(1f, 0.3f, 0.3f, 0.6f)
|
||
|
|
: new Color(1f, 1f, 1f, 0.2f);
|
||
|
|
var dir = Application.isPlaying ? lastDirection : Vector2.right;
|
||
|
|
var center = (Vector2)transform.position + dir * offsetDistance;
|
||
|
|
Gizmos.DrawWireCube(center, size);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|