주석추가
This commit is contained in:
@@ -1,52 +1,71 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
// ============================================================================
|
||||
// ActionData
|
||||
// ----------------------------------------------------------------------------
|
||||
// 모든 액션(공격/모션/잡기/그라운드파운드)의 데이터를 표현하는 ScriptableObject.
|
||||
// .asset 파일로 만들어 Inspector에서 디자이너가 직접 편집 가능.
|
||||
// PlayerController는 코드 없이 데이터만 바꿔서 새 액션 추가 가능.
|
||||
//
|
||||
// 액션 종류 구분:
|
||||
// - HasMotion = true → 이동 액션 (Dash/Roll 등)
|
||||
// - HasHit = true → 데미지 액션 (펀치/킥 등)
|
||||
// - IsGrab = true → 잡기 (가까운 적을 끌어당김)
|
||||
// - 위 세 가지 조합 가능 → "전진하면서 때리는 콤보" 등
|
||||
// ============================================================================
|
||||
[CreateAssetMenu(fileName = "ActionData", menuName = "Combat/ActionData")]
|
||||
public class ActionData : ScriptableObject
|
||||
{
|
||||
// [FormerlySerializedAs]: 옛 이름의 직렬화 데이터도 자동 로드 (기존 에셋 호환).
|
||||
[FormerlySerializedAs("AttackName")]
|
||||
[FormerlySerializedAs("MotionName")]
|
||||
public string ActionName;
|
||||
public string AnimationState;
|
||||
public float AnimationSpeed = 1f;
|
||||
public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||
public float Cooldown = 0.3f;
|
||||
public float ComboWindow = 0.25f;
|
||||
public string ActionName; // 디버그/Inspector 표시용 이름
|
||||
|
||||
public string AnimationState; // Animator의 State 이름 (Play()로 직접 호출)
|
||||
public float AnimationSpeed = 1f; // 애니메이션 기본 재생 속도
|
||||
public AnimationCurve AnimationSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f); // 시간에 따른 속도 변화 (windup 빠르게/임팩트 느리게 등)
|
||||
public float Cooldown = 0.3f; // 이 액션 발동 후 다음 공격 입력 받기까지 시간
|
||||
public float ComboWindow = 0.25f; // 콤보 transition 받을 수 있는 시간 (ComboNode에서도 별도 설정 가능)
|
||||
|
||||
// ─── 이동(모션) 파라미터 (HasMotion=true일 때 적용) ──────────────────
|
||||
[Header("Motion")]
|
||||
public bool HasMotion;
|
||||
public Vector2 Velocity = Vector2.zero;
|
||||
public AnimationCurve MotionSpeedCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||
public bool HasMotion; // 이 액션이 위치 이동을 동반하는지
|
||||
public Vector2 Velocity = Vector2.zero; // 모션 속도 (X는 facing 방향으로 자동 부호 변환)
|
||||
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 float MotionDuration = 0.3f; // 모션 전체 길이 (애니메이션 길이 안 쓸 때)
|
||||
public bool CanMoveDuringAction; // 액션 중 좌우 입력 허용 여부 (보통 false)
|
||||
public bool CanTurnDuringAction; // 액션 중 페이싱 변경 허용 여부
|
||||
public bool UseInputDirection = true; // true면 현재 입력 방향, false면 현재 페이싱 방향으로 이동
|
||||
public bool PreserveYVelocity = true; // true면 점프/낙하 중인 vy 유지, false면 ActionData.Velocity.y로 덮어씀
|
||||
public bool StopHorizontalVelocityOnEnd = true; // 액션 종료 시 vx를 0으로 (관성으로 더 가지 않게)
|
||||
|
||||
// ─── 공격 판정 (HasHit=true일 때 적용) ─────────────────────────────
|
||||
[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;
|
||||
public bool HasHit = true; // 이 액션이 데미지를 주는지
|
||||
public Vector2 Offset = new Vector2(0.5f, 0f); // 캐릭터 기준 hit 영역 중심 (X는 facing 방향)
|
||||
public float Radius = 0.5f; // hit 영역 반경 (AttackHitbox.CircleCollider2D)
|
||||
public int Damage = 10; // 데미지 양
|
||||
public float HitTiming = 0.15f; // 액션 시작 후 hit 발동까지 시간 (선딜)
|
||||
public float HitDuration = 0f; // hit 영역이 활성 상태로 유지되는 시간 (0이면 단발)
|
||||
|
||||
// ─── 피격자 반응 (피격된 적의 동작) ─────────────────────────────────
|
||||
[Header("Hit Reaction")]
|
||||
public Vector2 HitVelocity = Vector2.zero;
|
||||
public bool UseHitPositionCorrection;
|
||||
public Vector2 HitTargetOffset = new Vector2(0.8f, 0f);
|
||||
public float HitPositionCorrectionDuration = 0.08f;
|
||||
public bool CorrectHitTargetY;
|
||||
public string HitReactionAnimationState;
|
||||
public Vector2 HitVelocity = Vector2.zero; // 적에게 가할 넉백 속도 (X는 공격자 facing 방향)
|
||||
public bool UseHitPositionCorrection; // 적의 위치를 강제로 보정할지 (잡기/연계 안정성)
|
||||
public Vector2 HitTargetOffset = new Vector2(0.8f, 0f); // 보정 시 공격자 기준 적의 목표 위치
|
||||
public float HitPositionCorrectionDuration = 0.08f; // 보정 보간 시간 (0이면 즉시 텔레포트)
|
||||
public bool CorrectHitTargetY; // 보정에서 Y도 이동시킬지 (false면 X만)
|
||||
public string HitReactionAnimationState; // 적이 재생할 피격 애니메이션 State
|
||||
|
||||
// ─── 잡기 전용 (IsGrab=true일 때 적용) ─────────────────────────────
|
||||
[Header("Grab")]
|
||||
public bool IsGrab;
|
||||
public Vector2 GrabOffset = new Vector2(0.6f, 0f);
|
||||
public AnimationCurve GrabOffsetXCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||
public AnimationCurve GrabOffsetYCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
|
||||
public string GrabbedAnimationState;
|
||||
public float GrabSearchRadius = 2f;
|
||||
public float GrabRange = 0.5f;
|
||||
public bool IsGrab; // 잡기 액션 (GrabRoutine에서 처리)
|
||||
public Vector2 GrabOffset = new Vector2(0.6f, 0f); // 잡힌 적의 위치 (공격자 기준)
|
||||
public AnimationCurve GrabOffsetXCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f); // 시간에 따른 X 위치 비율 (당기기 효과)
|
||||
public AnimationCurve GrabOffsetYCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f); // 시간에 따른 Y 위치 비율 (들어올리기 효과)
|
||||
public string GrabbedAnimationState; // 잡힌 적이 재생할 애니메이션
|
||||
public float GrabSearchRadius = 2f; // 잡기 타겟 검색 반경 (이 안에서 후보 찾기)
|
||||
public float GrabRange = 0.5f; // 실제 잡기 가능 거리 (후보 중 이 거리 안에 있어야 잡힘)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// ============================================================================
|
||||
// AttackHitbox
|
||||
// ----------------------------------------------------------------------------
|
||||
// 플레이어(또는 임의 캐릭터)의 공격 판정 콜라이더.
|
||||
// Player의 자식 GameObject로 두고, 액션마다 Activate/Deactivate로 hit window 표현.
|
||||
//
|
||||
// 핵심:
|
||||
// - CircleCollider2D Trigger (물리 충돌 아닌 트리거 감지만)
|
||||
// - 평소엔 disabled, 액션의 HitTiming~HitDuration 동안만 enabled
|
||||
// - 활성 순간 이미 범위 안에 있던 적도 ScanImmediateOverlap으로 즉시 검사
|
||||
// (OnTriggerEnter는 다음 frame에야 발화되므로 짧은 hit window엔 부족함)
|
||||
// - _alreadyHit HashSet으로 같은 적에 중복 데미지 방지
|
||||
// ============================================================================
|
||||
[RequireComponent(typeof(CircleCollider2D))]
|
||||
public class AttackHitbox : MonoBehaviour
|
||||
{
|
||||
// PlayerController가 구독해서 "방금 hit한 적" 추적용 (잡기 타겟 우선 등에 활용).
|
||||
public event System.Action<IDamageable> OnHit;
|
||||
|
||||
private CircleCollider2D _collider;
|
||||
|
||||
// ─── 현재 활성 액션의 데미지/효과 데이터 (Activate에서 세팅) ─────────
|
||||
private int _damage;
|
||||
private Vector2 _hitVelocity;
|
||||
private Vector2 _hitSourcePosition;
|
||||
private Vector2? _hitTargetPosition;
|
||||
private float _hitPositionMinDistance;
|
||||
private bool _correctHitTargetY;
|
||||
private int _hitPositionSolidMask;
|
||||
private float _hitPositionCorrectionDuration;
|
||||
private string _hitReactionState;
|
||||
private LayerMask _targetLayer;
|
||||
private Vector2 _hitVelocity; // 피해자에게 가할 넉백 속도
|
||||
private Vector2 _hitSourcePosition; // 공격자 위치 (피격자 위치 보정 거리 계산용)
|
||||
private Vector2? _hitTargetPosition; // 피격자 강제 이동 목표 위치 (null이면 보정 안 함)
|
||||
private float _hitPositionMinDistance; // 이 거리보다 가까우면 위치 보정 적용
|
||||
private bool _correctHitTargetY; // 위치 보정에서 Y도 보정할지
|
||||
private int _hitPositionSolidMask; // 보정 시 끼이지 않게 검사할 솔리드 레이어
|
||||
private float _hitPositionCorrectionDuration; // 보정 보간 시간 (0이면 즉시)
|
||||
private string _hitReactionState; // 피격자가 재생할 애니메이션 State 이름
|
||||
private LayerMask _targetLayer; // 데미지를 줄 레이어 (보통 Enemy)
|
||||
|
||||
// 한 번 hit한 IDamageable은 이 액션 활성 동안 다시 hit되지 않음.
|
||||
private readonly HashSet<IDamageable> _alreadyHit = new();
|
||||
|
||||
private void Awake()
|
||||
@@ -27,6 +45,8 @@ private void Awake()
|
||||
_collider.enabled = false;
|
||||
}
|
||||
|
||||
// 액션 시작 시 호출. 위치/반경/데미지 등 모든 파라미터 세팅 후 콜라이더 활성화.
|
||||
// _alreadyHit를 클리어해서 새 공격으로 다시 hit 가능하게 함.
|
||||
public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity, Vector2 sourcePosition, Vector2? hitTargetPosition, bool correctHitTargetY, int hitPositionSolidMask, LayerMask targetLayer)
|
||||
{
|
||||
transform.localPosition = localPosition;
|
||||
@@ -48,12 +68,15 @@ public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity
|
||||
ScanImmediateOverlap();
|
||||
}
|
||||
|
||||
// 액션의 HitDuration이 끝나면 호출. 콜라이더 비활성화 + hit 기록 초기화.
|
||||
public void Deactivate()
|
||||
{
|
||||
_collider.enabled = false;
|
||||
_alreadyHit.Clear();
|
||||
}
|
||||
|
||||
// 활성 순간 즉시 검사: Physics2D.OverlapCircleAll로 현재 겹친 콜라이더를 모두 가져와 TryDamage.
|
||||
// 이게 없으면 짧은 hit window (예: HitDuration=0.02)에 OnTriggerEnter가 못 따라옴.
|
||||
private void ScanImmediateOverlap()
|
||||
{
|
||||
Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, _collider.radius, _targetLayer);
|
||||
@@ -61,9 +84,17 @@ private void ScanImmediateOverlap()
|
||||
TryDamage(hit);
|
||||
}
|
||||
|
||||
// 트리거 이벤트로 들어온 적도 같은 함수로 처리.
|
||||
// Stay까지 받는 이유: 활성 중간에 새로 들어온 적도 잡으려고.
|
||||
private void OnTriggerEnter2D(Collider2D other) => TryDamage(other);
|
||||
private void OnTriggerStay2D(Collider2D other) => TryDamage(other);
|
||||
|
||||
// 데미지 적용 흐름:
|
||||
// 1) 레이어 마스크로 적 여부 확인
|
||||
// 2) Collider 자신 또는 부모에서 IDamageable 검색 (자식 콜라이더 대응)
|
||||
// 3) 이미 hit한 대상은 건너뜀 (HashSet)
|
||||
// 4) 데미지 + 넉백 + 위치 보정 정보를 전달하여 TakeDamage 호출
|
||||
// 5) OnHit 이벤트 발화 (PlayerController가 마지막 hit한 적 기억)
|
||||
private void TryDamage(Collider2D other)
|
||||
{
|
||||
if ((_targetLayer.value & (1 << other.gameObject.layer)) == 0) return;
|
||||
@@ -81,6 +112,10 @@ private void TryDamage(Collider2D other)
|
||||
OnHit?.Invoke(target);
|
||||
}
|
||||
|
||||
// 위치 보정 목표 결정.
|
||||
// 적이 공격자에게 너무 가까우면 (X 거리 < minDistance) hitTargetPosition을 반환,
|
||||
// 충분히 떨어져 있으면 null 반환해서 보정 안 함.
|
||||
// (가까운 적만 끌어당기는 "흡착" 효과 구현)
|
||||
private Vector2? GetCorrectionTargetPosition(Collider2D other)
|
||||
{
|
||||
if (!_hitTargetPosition.HasValue) return null;
|
||||
|
||||
@@ -2,29 +2,46 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
// ============================================================================
|
||||
// ComboNode + ComboTransition
|
||||
// ----------------------------------------------------------------------------
|
||||
// 콤보 트리 구조를 표현하는 ScriptableObject + Serializable 클래스.
|
||||
// 각 노드는 "어떤 액션을 수행할지" + "다음에 어떤 입력으로 어디로 갈지"를 정의.
|
||||
//
|
||||
// 예시 트리:
|
||||
// Punch_Root → [Punch] → Punch_Hit2 → [Punch] → Punch_Finisher
|
||||
// → [Kick] → Kick_Spin
|
||||
// → [Grab] → Grab_Smash
|
||||
//
|
||||
// 각 트랜지션마다 ForwardStep을 다르게 줘서 콤보 흐름에 맞춰 전진 거리 조절.
|
||||
// ============================================================================
|
||||
|
||||
// 콤보 입력 타입. PlayerController.HandleComboInput에 매핑됨.
|
||||
public enum ComboInputType
|
||||
{
|
||||
Punch,
|
||||
Kick,
|
||||
Grab,
|
||||
Motion
|
||||
Motion // 모션 액션 트리거 (현재는 사용 안 함, 확장 여지로 남겨둠)
|
||||
}
|
||||
|
||||
// 한 노드에서 다음 노드로 가는 "간선" 정보.
|
||||
[Serializable]
|
||||
public class ComboTransition
|
||||
{
|
||||
public ComboInputType Trigger;
|
||||
public ComboNode Next;
|
||||
public float ForwardStep = 0f;
|
||||
public float ForwardStepDuration = 0.1f;
|
||||
public ComboInputType Trigger; // 어떤 입력이 들어와야 이 transition을 탈지
|
||||
public ComboNode Next; // 이동할 다음 노드
|
||||
public float ForwardStep = 0f; // 이 transition을 탈 때 전진할 거리 (적과 거리 좁히기)
|
||||
public float ForwardStepDuration = 0.1f;// 전진 동작 시간
|
||||
}
|
||||
|
||||
// 콤보 트리의 노드. .asset 파일로 관리.
|
||||
[CreateAssetMenu(fileName = "ComboNode", menuName = "Combat/ComboNode")]
|
||||
public class ComboNode : ScriptableObject
|
||||
{
|
||||
public string NodeName;
|
||||
public string NodeName; // Inspector 식별용
|
||||
[FormerlySerializedAs("Attack")]
|
||||
public ActionData Action;
|
||||
public float ComboWindow = 0.8f;
|
||||
public ComboTransition[] Transitions;
|
||||
public ActionData Action; // 이 노드에 진입했을 때 수행할 액션
|
||||
public float ComboWindow = 0.8f; // 이 노드에서 다음 입력 받을 수 있는 시간
|
||||
public ComboTransition[] Transitions; // 다음 노드들 (입력별로 분기)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
// ============================================================================
|
||||
// Health
|
||||
// ----------------------------------------------------------------------------
|
||||
// 순수한 HP 데이터 컴포넌트. 시각 효과/넉백/사망 처리는 일절 안 함.
|
||||
// Enemy, Player 등 어떤 GameObject든 Health 컴포넌트만 추가하면 HP 관리 가능.
|
||||
//
|
||||
// 일반적 사용 흐름:
|
||||
// 1) Enemy/Player가 IDamageable.TakeDamage 받음
|
||||
// 2) 시각 효과(flash) + 넉백 처리
|
||||
// 3) 마지막에 _health.TakeDamage(amount) 호출
|
||||
// 4) Health가 HP 감소 + OnHealthChanged 이벤트 발화 → HP바 등 UI 자동 갱신
|
||||
// 5) HP가 0 되면 OnDied 이벤트 발화 → Enemy가 HandleDeath()로 Destroy 등 처리
|
||||
// ============================================================================
|
||||
public class Health : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int _maxHealth = 30;
|
||||
private int _currentHealth;
|
||||
|
||||
// ─── 읽기 전용 프로퍼티 ──────────────────────────────────────────────
|
||||
public int MaxHealth => _maxHealth;
|
||||
public int CurrentHealth => _currentHealth;
|
||||
public float Ratio => _maxHealth > 0 ? (float)_currentHealth / _maxHealth : 0f;
|
||||
public bool IsDead => _currentHealth <= 0;
|
||||
|
||||
// ─── 외부 구독용 이벤트 ──────────────────────────────────────────────
|
||||
// OnHealthChanged: (current, max). HP바, 데미지 숫자 표시 등에 사용.
|
||||
// OnDied: 사망 순간 1회만 발화. Enemy.HandleDeath에서 구독.
|
||||
public event Action<int, int> OnHealthChanged;
|
||||
public event Action OnDied;
|
||||
|
||||
@@ -19,6 +36,8 @@ private void Awake()
|
||||
_currentHealth = _maxHealth;
|
||||
}
|
||||
|
||||
// 데미지 적용. 양수 데미지만 받고, 이미 죽었으면 무시.
|
||||
// OnDied는 "방금 죽은 순간"에만 발화 (previous > 0 && current == 0).
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (amount <= 0 || IsDead) return;
|
||||
@@ -31,6 +50,7 @@ public void TakeDamage(int amount)
|
||||
OnDied?.Invoke();
|
||||
}
|
||||
|
||||
// 회복. 죽은 상태에서는 회복 안 됨 (부활 로직은 별도로 만들어야 함).
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (amount <= 0 || IsDead) return;
|
||||
@@ -39,12 +59,14 @@ public void Heal(int amount)
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
}
|
||||
|
||||
// 풀체력으로 리셋. 부활/재시작 시 사용.
|
||||
public void ResetHealth()
|
||||
{
|
||||
_currentHealth = _maxHealth;
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
}
|
||||
|
||||
// 최대 HP 변경. fill=true면 현재 HP도 풀로 채우고, false면 새 max로 클램프만.
|
||||
public void SetMaxHealth(int newMax, bool fill = true)
|
||||
{
|
||||
_maxHealth = Mathf.Max(newMax, 1);
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
using UnityEngine;
|
||||
|
||||
// ============================================================================
|
||||
// IDamageable
|
||||
// ----------------------------------------------------------------------------
|
||||
// "데미지를 받을 수 있는 무언가"를 표현하는 인터페이스.
|
||||
// AttackHitbox가 트리거에 들어온 콜라이더에서 이 인터페이스를 찾아 TakeDamage 호출.
|
||||
// Enemy, Player 등 어떤 GameObject든 이 인터페이스만 구현하면 같은 공격 시스템으로 피격 가능.
|
||||
// ----------------------------------------------------------------------------
|
||||
// 매개변수:
|
||||
// amount — 데미지 양
|
||||
// hitVelocity — 넉백 속도 (피해자에게 적용할 vx/vy)
|
||||
// hitReactionAnimationState — 피격 모션 애니메이션 State 이름 (null 가능)
|
||||
// hitTargetPosition — 피격 시 위치 보정 목표 (null이면 보정 안 함)
|
||||
// correctHitTargetY — 위 위치 보정에서 Y도 보정할지 (false면 X만)
|
||||
// hitPositionSolidMask — 위치 보정 시 끼이지 않게 검사할 솔리드 레이어
|
||||
// hitPositionCorrectionDuration — 위치 보정 보간 시간 (0이면 즉시 스냅)
|
||||
// ============================================================================
|
||||
public interface IDamageable
|
||||
{
|
||||
void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f);
|
||||
|
||||
Reference in New Issue
Block a user