2026-05-19 hp바 추가
This commit is contained in:
BIN
Assets/01_Scenes/GameScene.unity
LFS
BIN
Assets/01_Scenes/GameScene.unity
LFS
Binary file not shown.
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
|
||||
Binary file not shown.
8
Assets/06_Textures.meta
Normal file
8
Assets/06_Textures.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e93b87e907f0ac43ab0af53acaf0091
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/06_Textures/Base.meta
Normal file
8
Assets/06_Textures/Base.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3e9790e7db6ae043acd3184ec10eace
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/06_Textures/Base/LongSquare.png
LFS
Normal file
BIN
Assets/06_Textures/Base/LongSquare.png
LFS
Normal file
Binary file not shown.
169
Assets/06_Textures/Base/LongSquare.png.meta
Normal file
169
Assets/06_Textures/Base/LongSquare.png.meta
Normal 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:
|
||||
Reference in New Issue
Block a user