플레이어 HPBar

This commit is contained in:
2026-05-20 13:05:49 +09:00
parent a0d04cf934
commit 57333a853e
6 changed files with 198 additions and 5 deletions

Binary file not shown.

View File

@@ -35,13 +35,38 @@ public class ComboTransition
public float ForwardStepDuration = 0.1f;// 전진 동작 시간 public float ForwardStepDuration = 0.1f;// 전진 동작 시간
} }
// 공격 방향. 방향키 입력에 따라 같은 노드라도 다른 액션을 낼 수 있다.
// Neutral = 방향키 없음(제자리) / Forward = 바라보는 방향 / Back = 반대 방향
// Up = 위 입력 / Down = 아래 입력
// 대각선 입력은 위/아래를 우선한다 (PlayerController.GetAttackDirection 참고).
public enum AttackDirection
{
Neutral,
Forward,
Back,
Up,
Down,
}
// 한 노드의 방향별 액션 변형. ComboNode.DirectionalVariants 배열의 항목.
// 예: 같은 펀치 노드라도 Forward면 전진 펀치, Up이면 어퍼컷.
[Serializable]
public class DirectionalAction
{
public AttackDirection Direction; // 이 변형이 발동할 입력 방향
public ActionData Action; // 해당 방향일 때 수행할 액션
}
// 콤보 트리의 노드. .asset 파일로 관리. // 콤보 트리의 노드. .asset 파일로 관리.
[CreateAssetMenu(fileName = "ComboNode", menuName = "Combat/ComboNode")] [CreateAssetMenu(fileName = "ComboNode", menuName = "Combat/ComboNode")]
public class ComboNode : ScriptableObject public class ComboNode : ScriptableObject
{ {
public string NodeName; // Inspector 식별용 public string NodeName; // Inspector 식별용
[FormerlySerializedAs("Attack")] [FormerlySerializedAs("Attack")]
public ActionData Action; // 이 노드에 진입했을 때 수행할 액션 public ActionData Action; // 기본(Neutral) 액션 — 방향키 입력이 없을 때 수행
// 방향키를 누른 채 공격하면 이 목록에서 일치하는 방향의 액션으로 대체된다.
// 비어 있거나 일치하는 방향이 없으면 Action(기본)으로 폴백.
public DirectionalAction[] DirectionalVariants;
public float ComboWindow = 0.8f; // 이 노드에서 다음 입력 받을 수 있는 시간 public float ComboWindow = 0.8f; // 이 노드에서 다음 입력 받을 수 있는 시간
public ComboTransition[] Transitions; // 다음 노드들 (입력별로 분기) public ComboTransition[] Transitions; // 다음 노드들 (입력별로 분기)
} }

View File

@@ -119,6 +119,7 @@ public class PlayerController : MonoBehaviour,IDamageable
private Rigidbody2D _rb; private Rigidbody2D _rb;
private Animator _anim; private Animator _anim;
private SpriteRenderer _spriteRenderer; // 좌우 반전 + flipX 페이싱용 private SpriteRenderer _spriteRenderer; // 좌우 반전 + flipX 페이싱용
private Health _health;
// ─── 무기 시스템 ──────────────────────────────────────────────────── // ─── 무기 시스템 ────────────────────────────────────────────────────
// PlayerWeaponInventory가 현재 장착 무기 관리. 무기 교체 시 OnWeaponChanged 이벤트 발화. // PlayerWeaponInventory가 현재 장착 무기 관리. 무기 교체 시 OnWeaponChanged 이벤트 발화.
@@ -131,6 +132,7 @@ private void Awake()
_rb = GetComponent<Rigidbody2D>(); _rb = GetComponent<Rigidbody2D>();
_anim = GetComponent<Animator>(); _anim = GetComponent<Animator>();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>(); _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_health = GetComponent<Health>();
ResolveBodyColliderReference(); ResolveBodyColliderReference();
EnsureAttackHitbox(); EnsureAttackHitbox();
// 공격이 enemy를 hit하면 _lastHitEnemy를 기억 → 다음 잡기에서 그 enemy를 우선 타겟팅. // 공격이 enemy를 hit하면 _lastHitEnemy를 기억 → 다음 잡기에서 그 enemy를 우선 타겟팅.
@@ -1578,5 +1580,9 @@ private void DrawTimelineBar(ActionData data, float elapsed)
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0) public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0)
{ {
if (_health == null || _health.IsDead) return;
_health.TakeDamage(amount);
Debug.Log($"{name} 피격: -{amount} (HP: {_health.CurrentHealth}/{_health.MaxHealth})");
} }
} }

View File

@@ -0,0 +1,160 @@
using UnityEngine;
using UnityEngine.UI;
// Screen-space HP bar for the player HUD.
// Enemy HP bars can keep using the SpriteRenderer-based HpBar.
public class PlayerHpBarUI : MonoBehaviour
{
[SerializeField] private Health _health;
[SerializeField] private Image _fillImage;
[SerializeField] private bool _autoFindPlayerHealth = true;
[SerializeField] private bool _autoFindFillImageInChildren = true;
[SerializeField] private bool _configureAsFilledImage = true;
[SerializeField] private float _smoothSpeed = 0f;
[Header("Color Thresholds")]
[SerializeField] private Color _highColor = new Color(0.2f, 0.9f, 0.3f, 1f);
[SerializeField] private Color _midColor = new Color(1f, 0.85f, 0.2f, 1f);
[SerializeField] private Color _lowColor = new Color(0.95f, 0.25f, 0.25f, 1f);
[SerializeField, Range(0f, 1f)] private float _midThreshold = 0.5f;
[SerializeField, Range(0f, 1f)] private float _lowThreshold = 0.2f;
private float _currentRatio = 1f;
private float _targetRatio = 1f;
private void Awake()
{
ResolveFillImage();
ResolveHealth();
ConfigureFillImage();
}
private void OnEnable()
{
ResolveFillImage();
ResolveHealth();
ConfigureFillImage();
if (_health == null) return;
_health.OnHealthChanged += HandleHealthChanged;
HandleHealthChanged(_health.CurrentHealth, _health.MaxHealth);
}
private void OnDisable()
{
if (_health != null)
_health.OnHealthChanged -= HandleHealthChanged;
}
private void Start()
{
ResolveFillImage();
ResolveHealth();
ConfigureFillImage();
if (_health != null)
HandleHealthChanged(_health.CurrentHealth, _health.MaxHealth);
}
private void Update()
{
if (_fillImage == null || _smoothSpeed <= 0f) return;
if (Mathf.Approximately(_currentRatio, _targetRatio)) return;
_currentRatio = Mathf.MoveTowards(_currentRatio, _targetRatio, _smoothSpeed * Time.deltaTime);
ApplyFill();
}
public void Bind(Health health)
{
if (_health == health) return;
if (isActiveAndEnabled && _health != null)
_health.OnHealthChanged -= HandleHealthChanged;
_health = health;
if (isActiveAndEnabled && _health != null)
{
_health.OnHealthChanged += HandleHealthChanged;
HandleHealthChanged(_health.CurrentHealth, _health.MaxHealth);
}
}
private void HandleHealthChanged(int current, int max)
{
_targetRatio = max > 0 ? Mathf.Clamp01((float)current / max) : 0f;
if (_smoothSpeed <= 0f)
{
_currentRatio = _targetRatio;
ApplyFill();
}
ApplyColor(_targetRatio);
}
private void ApplyFill()
{
if (_fillImage == null) return;
_fillImage.fillAmount = Mathf.Clamp01(_currentRatio);
}
private void ApplyColor(float ratio)
{
if (_fillImage == null) return;
if (ratio <= _lowThreshold)
_fillImage.color = _lowColor;
else if (ratio <= _midThreshold)
_fillImage.color = _midColor;
else
_fillImage.color = _highColor;
}
private void ResolveHealth()
{
if (_health != null || !_autoFindPlayerHealth) return;
PlayerController player = FindFirstObjectByType<PlayerController>();
if (player != null)
_health = player.GetComponent<Health>();
}
private void ResolveFillImage()
{
if (_fillImage != null || !_autoFindFillImageInChildren) return;
Transform fill = FindChildByName(transform, "Fill");
if (fill != null)
_fillImage = fill.GetComponent<Image>();
}
private Transform FindChildByName(Transform parent, string childName)
{
if (parent == null) return null;
for (int i = 0; i < parent.childCount; i++)
{
Transform child = parent.GetChild(i);
if (child.name == childName)
return child;
Transform nested = FindChildByName(child, childName);
if (nested != null)
return nested;
}
return null;
}
private void ConfigureFillImage()
{
if (_fillImage == null || !_configureAsFilledImage) return;
_fillImage.type = Image.Type.Filled;
_fillImage.fillMethod = Image.FillMethod.Horizontal;
_fillImage.fillOrigin = (int)Image.OriginHorizontal.Left;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0ce8dd9c4b8941e2a69bbe4864891425

Binary file not shown.