2026-05-19 hp바 추가
This commit is contained in:
55
Assets/02_Scripts/Combat/Health.cs
Normal file
55
Assets/02_Scripts/Combat/Health.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
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;
|
||||
|
||||
public event Action<int, int> OnHealthChanged;
|
||||
public event Action OnDied;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_currentHealth = _maxHealth;
|
||||
}
|
||||
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (amount <= 0 || IsDead) return;
|
||||
|
||||
int previous = _currentHealth;
|
||||
_currentHealth = Mathf.Max(_currentHealth - amount, 0);
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
|
||||
if (_currentHealth <= 0 && previous > 0)
|
||||
OnDied?.Invoke();
|
||||
}
|
||||
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (amount <= 0 || IsDead) return;
|
||||
|
||||
_currentHealth = Mathf.Min(_currentHealth + amount, _maxHealth);
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
}
|
||||
|
||||
public void ResetHealth()
|
||||
{
|
||||
_currentHealth = _maxHealth;
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
}
|
||||
|
||||
public void SetMaxHealth(int newMax, bool fill = true)
|
||||
{
|
||||
_maxHealth = Mathf.Max(newMax, 1);
|
||||
if (fill) _currentHealth = _maxHealth;
|
||||
else _currentHealth = Mathf.Min(_currentHealth, _maxHealth);
|
||||
OnHealthChanged?.Invoke(_currentHealth, _maxHealth);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Combat/Health.cs.meta
Normal file
2
Assets/02_Scripts/Combat/Health.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcfe85aa11323ce47923dabc579cab00
|
||||
@@ -3,11 +3,9 @@
|
||||
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
[RequireComponent(typeof(Health))]
|
||||
public class Enemy : MonoBehaviour, IDamageable
|
||||
{
|
||||
[Header("Stats")]
|
||||
[SerializeField] private int _maxHealth = 30;
|
||||
|
||||
[Header("Hit Feedback")]
|
||||
[SerializeField] private float _hitFlashDuration = 0.1f;
|
||||
[SerializeField] private Color _hitFlashColor = Color.red;
|
||||
@@ -25,7 +23,7 @@ public class Enemy : MonoBehaviour, IDamageable
|
||||
[SerializeField] private LayerMask _separationLayer;
|
||||
private static readonly Collider2D[] _separationBuffer = new Collider2D[16];
|
||||
|
||||
private int _currentHealth;
|
||||
private Health _health;
|
||||
private Rigidbody2D _rb;
|
||||
private Animator _anim;
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
@@ -49,7 +47,8 @@ public class Enemy : MonoBehaviour, IDamageable
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_currentHealth = _maxHealth;
|
||||
_health = GetComponent<Health>();
|
||||
_health.OnDied += HandleDeath;
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
_anim = GetComponentInChildren<Animator>();
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
@@ -58,6 +57,12 @@ private void Awake()
|
||||
_originalColor = _spriteRenderer.color;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_health != null)
|
||||
_health.OnDied -= HandleDeath;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_flashTimer > 0f)
|
||||
@@ -143,13 +148,10 @@ private void ApplySeparation()
|
||||
|
||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f)
|
||||
{
|
||||
if (_currentHealth <= 0) return;
|
||||
if (_health == null || _health.IsDead) return;
|
||||
|
||||
_isGrabbed = false;
|
||||
|
||||
_currentHealth -= amount;
|
||||
Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})");
|
||||
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
Debug.Log($"[Flash START] t={Time.time:F3} duration={_hitFlashDuration:F3} (current color was {_spriteRenderer.color})");
|
||||
@@ -178,13 +180,14 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentHealth <= 0)
|
||||
Die();
|
||||
// 시각/반응 처리가 끝난 뒤 HP를 감산해서 OnDied 이벤트가 마지막에 발화되게 한다.
|
||||
_health.TakeDamage(amount);
|
||||
Debug.Log($"{name} 피격: -{amount} (HP: {_health.CurrentHealth}/{_health.MaxHealth})");
|
||||
}
|
||||
|
||||
public void BeginGrab(string grabbedAnimationState, int solidMask)
|
||||
{
|
||||
if (_currentHealth <= 0) return;
|
||||
if (_health == null || _health.IsDead) return;
|
||||
|
||||
_isGrabbed = true;
|
||||
_grabSolidMask = solidMask;
|
||||
@@ -373,7 +376,7 @@ private void BounceOffWall(Vector2 wallNormal)
|
||||
_hitReactionTimer = _hitReactionDuration;
|
||||
}
|
||||
|
||||
private void Die()
|
||||
private void HandleDeath()
|
||||
{
|
||||
Debug.Log($"{name} 사망");
|
||||
Destroy(gameObject);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerController : MonoBehaviour
|
||||
public class PlayerController : MonoBehaviour,IDamageable
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField] private float _moveSpeed = 5f;
|
||||
@@ -23,10 +23,12 @@ public class PlayerController : MonoBehaviour
|
||||
|
||||
[Header("Jump")]
|
||||
[SerializeField] private float _jumpForce = 8f;
|
||||
[SerializeField] private int _maxJumpCount = 2;
|
||||
[SerializeField] private Transform _groundCheck;
|
||||
[SerializeField] private float _groundCheckRadius = 0.1f;
|
||||
[SerializeField] private LayerMask _groundLayer;
|
||||
private bool _isGrounded;
|
||||
private int _jumpsUsed;
|
||||
|
||||
[Header("WallSlide")]
|
||||
[SerializeField] private Transform _wallCheckLeft;
|
||||
@@ -156,6 +158,10 @@ private void FixedUpdate()
|
||||
_isTouchingRightWall = Physics2D.OverlapCircle(_wallCheckRight.position, _wallCheckRadius, _groundLayer);
|
||||
_wallDirection = _isTouchingRightWall ? 1 : (_isTouchingLeftWall ? -1 : 0);
|
||||
|
||||
// 지면에 닿고 점프 중이 아니면(상승 중 아님) 점프 카운트 리셋.
|
||||
if (_isGrounded && _rb.linearVelocity.y <= 0f)
|
||||
_jumpsUsed = 0;
|
||||
|
||||
if (_attackCooldownTimer > 0f)
|
||||
_attackCooldownTimer -= Time.fixedDeltaTime;
|
||||
|
||||
@@ -217,13 +223,23 @@ private void OnJumpInput()
|
||||
{
|
||||
if (_isGrounded)
|
||||
{
|
||||
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, _jumpForce);
|
||||
PerformJump();
|
||||
}
|
||||
else if (IsTouchingWall)
|
||||
{
|
||||
_rb.linearVelocity = new Vector2(-_wallDirection * _wallJumpForce.x, _wallJumpForce.y);
|
||||
_inputLockTimer = _wallJumpInputLockDuration;
|
||||
}
|
||||
else if (_jumpsUsed < _maxJumpCount)
|
||||
{
|
||||
PerformJump();
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformJump()
|
||||
{
|
||||
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, _jumpForce);
|
||||
_jumpsUsed++;
|
||||
}
|
||||
|
||||
private void OnPunchInput() => HandleComboInput(ComboInputType.Punch);
|
||||
@@ -1343,4 +1359,9 @@ private void DrawTimelineBar(ActionData data, float elapsed)
|
||||
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0)
|
||||
{
|
||||
Debug.Log("플레이어 타격받음");
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/02_Scripts/UI.meta
Normal file
8
Assets/02_Scripts/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b4a28598a934ef439e38f81568625cb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
101
Assets/02_Scripts/UI/HpBar.cs
Normal file
101
Assets/02_Scripts/UI/HpBar.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class HpBar : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Health _health;
|
||||
[SerializeField] private Transform _fill;
|
||||
[SerializeField] private bool _autoFindHealthInParent = true;
|
||||
[SerializeField] private bool _hideWhenFull = 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 Vector3 _baseFillScale;
|
||||
private SpriteRenderer _fillRenderer;
|
||||
private float _currentRatio = 1f;
|
||||
private float _targetRatio = 1f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_health == null && _autoFindHealthInParent)
|
||||
_health = GetComponentInParent<Health>();
|
||||
|
||||
if (_fill != null)
|
||||
{
|
||||
_baseFillScale = _fill.localScale;
|
||||
_fillRenderer = _fill.GetComponent<SpriteRenderer>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_health == null) return;
|
||||
|
||||
_health.OnHealthChanged += HandleHealthChanged;
|
||||
HandleHealthChanged(_health.CurrentHealth, _health.MaxHealth);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_health != null)
|
||||
_health.OnHealthChanged -= HandleHealthChanged;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_smoothSpeed <= 0f) return;
|
||||
if (Mathf.Approximately(_currentRatio, _targetRatio)) return;
|
||||
|
||||
_currentRatio = Mathf.MoveTowards(_currentRatio, _targetRatio, _smoothSpeed * Time.deltaTime);
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private void HandleHealthChanged(int current, int max)
|
||||
{
|
||||
_targetRatio = max > 0 ? (float)current / max : 0f;
|
||||
|
||||
if (_smoothSpeed <= 0f)
|
||||
{
|
||||
_currentRatio = _targetRatio;
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
ApplyColor(_targetRatio);
|
||||
|
||||
if (_hideWhenFull && _fill != null)
|
||||
{
|
||||
bool shouldShow = _targetRatio < 1f && _targetRatio > 0f;
|
||||
if (gameObject.activeSelf != shouldShow)
|
||||
gameObject.SetActive(shouldShow);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyScale()
|
||||
{
|
||||
if (_fill == null) return;
|
||||
|
||||
Vector3 scale = _baseFillScale;
|
||||
scale.x = _baseFillScale.x * Mathf.Clamp01(_currentRatio);
|
||||
_fill.localScale = scale;
|
||||
}
|
||||
|
||||
private void ApplyColor(float ratio)
|
||||
{
|
||||
if (_fillRenderer == null) return;
|
||||
|
||||
Color color;
|
||||
if (ratio <= _lowThreshold)
|
||||
color = _lowColor;
|
||||
else if (ratio <= _midThreshold)
|
||||
color = _midColor;
|
||||
else
|
||||
color = _highColor;
|
||||
|
||||
_fillRenderer.color = color;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/UI/HpBar.cs.meta
Normal file
2
Assets/02_Scripts/UI/HpBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b4c4406a1fa1084dbce1fadbeb93104
|
||||
Reference in New Issue
Block a user