2026-05-19 hp바 추가

This commit is contained in:
2026-05-19 10:30:35 +09:00
parent 80cf41af2a
commit e01feec160
13 changed files with 399 additions and 19 deletions

Binary file not shown.

View 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);
}
}

View File

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

View File

@@ -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);

View File

@@ -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("플레이어 타격받음");
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b4a28598a934ef439e38f81568625cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3b4c4406a1fa1084dbce1fadbeb93104

8
Assets/06_Textures.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4e93b87e907f0ac43ab0af53acaf0091
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f3e9790e7db6ae043acd3184ec10eace
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,169 @@
fileFormatVersion: 2
guid: cf4b69be35d34f342b1c6fbdb443167e
TextureImporter:
internalIDToNameTable:
- first:
213: 2581929779970265829
second: LongSqure_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 0
alignment: 9
spritePivot: {x: 0, y: 0.5}
spritePixelsToUnits: 2500
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: LongSqure_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 2225
height: 139
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 5e2512a847bd4d320800000000000000
internalID: 2581929779970265829
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
LongSqure_0: 2581929779970265829
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant: