2026-05-21 보스추가
This commit is contained in:
BIN
Assets/01_Scenes/BossScene.unity
LFS
Normal file
BIN
Assets/01_Scenes/BossScene.unity
LFS
Normal file
Binary file not shown.
7
Assets/01_Scenes/BossScene.unity.meta
Normal file
7
Assets/01_Scenes/BossScene.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d3ad68e4d8a6f544eb79d5c180d28868
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/01_Scenes/GameScene.unity
LFS
BIN
Assets/01_Scenes/GameScene.unity
LFS
Binary file not shown.
@@ -1,19 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
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
|
public class Health : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private int _maxHealth = 30;
|
[SerializeField] private int _maxHealth = 30;
|
||||||
@@ -26,8 +13,8 @@ public class Health : MonoBehaviour
|
|||||||
public bool IsDead => _currentHealth <= 0;
|
public bool IsDead => _currentHealth <= 0;
|
||||||
|
|
||||||
// ─── 외부 구독용 이벤트 ──────────────────────────────────────────────
|
// ─── 외부 구독용 이벤트 ──────────────────────────────────────────────
|
||||||
// OnHealthChanged: (current, max). HP바, 데미지 숫자 표시 등에 사용.
|
// OnHealthChanged: (current, max). 데미지 숫자 표시 등에 사용.
|
||||||
// OnDied: 사망 순간 1회만 발화. Enemy.HandleDeath에서 구독.
|
// OnDied: 사망 순간 1회만 발화.
|
||||||
public event Action<int, int> OnHealthChanged;
|
public event Action<int, int> OnHealthChanged;
|
||||||
public event Action OnDied;
|
public event Action OnDied;
|
||||||
|
|
||||||
@@ -50,7 +37,7 @@ public void TakeDamage(int amount)
|
|||||||
OnDied?.Invoke();
|
OnDied?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회복. 죽은 상태에서는 회복 안 됨 (부활 로직은 별도로 만들어야 함).
|
// 회복
|
||||||
public void Heal(int amount)
|
public void Heal(int amount)
|
||||||
{
|
{
|
||||||
if (amount <= 0 || IsDead) return;
|
if (amount <= 0 || IsDead) return;
|
||||||
|
|||||||
75
Assets/02_Scripts/Enemy/Boss.cs
Normal file
75
Assets/02_Scripts/Enemy/Boss.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Boss
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 보스의 페이즈를 관리하는 컴포넌트. 같은 GameObject의 Enemy(몸)·BossAI(두뇌)와
|
||||||
|
// 함께 동작한다.
|
||||||
|
// - Health 비율이 임계값 아래로 떨어지면 페이즈 전환
|
||||||
|
// - 전환 중엔 Enemy.SetInvulnerable(true)로 무적 + 전환 애니메이션 재생
|
||||||
|
// - CurrentPhase를 BossAI가 읽어 공격 패턴을 바꾼다 (BossAttack.MinPhase)
|
||||||
|
// ============================================================================
|
||||||
|
[RequireComponent(typeof(Enemy))]
|
||||||
|
[RequireComponent(typeof(Health))]
|
||||||
|
public class Boss : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Phase")]
|
||||||
|
// 페이즈 업이 일어나는 HP 비율 (내림차순). 예: {0.66, 0.33} → 페이즈 0→1→2.
|
||||||
|
[SerializeField] private float[] _phaseThresholds = { 0.66f, 0.33f };
|
||||||
|
[SerializeField] private string _phaseTransitionAnimation = "BossPhaseChange";
|
||||||
|
[SerializeField] private float _phaseTransitionDuration = 1.2f; // 전환(무적) 지속 시간
|
||||||
|
|
||||||
|
private Enemy _enemy;
|
||||||
|
private Health _health;
|
||||||
|
private Animator _anim;
|
||||||
|
|
||||||
|
private int _currentPhase; // 0부터 시작
|
||||||
|
private bool _isTransitioning; // 페이즈 전환 진행 중
|
||||||
|
|
||||||
|
public int CurrentPhase => _currentPhase;
|
||||||
|
public bool IsTransitioning => _isTransitioning;
|
||||||
|
public event Action<int> OnPhaseChanged; // 전환 완료 시 발화 (인자 = 새 페이즈)
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_enemy = GetComponent<Enemy>();
|
||||||
|
_health = GetComponent<Health>();
|
||||||
|
_anim = GetComponentInChildren<Animator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_isTransitioning || _health.IsDead) return;
|
||||||
|
if (_currentPhase >= _phaseThresholds.Length) return; // 이미 마지막 페이즈
|
||||||
|
|
||||||
|
// 현재 페이즈의 임계값 아래로 HP가 떨어지면 다음 페이즈로 전환.
|
||||||
|
if (_health.Ratio <= _phaseThresholds[_currentPhase])
|
||||||
|
RunPhaseTransition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이즈 전환: 무적 ON → 전환 애니메이션 → 일정 시간 후 무적 OFF + 페이즈 증가.
|
||||||
|
// 코루틴 대신 Awaitable 사용. destroyCancellationToken으로 파괴 시 자동 취소.
|
||||||
|
private async void RunPhaseTransition()
|
||||||
|
{
|
||||||
|
_isTransitioning = true;
|
||||||
|
_enemy.SetInvulnerable(true);
|
||||||
|
|
||||||
|
if (_anim != null && !string.IsNullOrEmpty(_phaseTransitionAnimation))
|
||||||
|
_anim.Play(_phaseTransitionAnimation);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Awaitable.WaitForSecondsAsync(_phaseTransitionDuration, destroyCancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return; // 보스가 파괴됨 → 그대로 종료
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPhase++;
|
||||||
|
_enemy.SetInvulnerable(false);
|
||||||
|
_isTransitioning = false;
|
||||||
|
OnPhaseChanged?.Invoke(_currentPhase);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Enemy/Boss.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/Boss.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 574f5c8f5a8ab4e438e2f8f8f1997bbf
|
||||||
333
Assets/02_Scripts/Enemy/BossAI.cs
Normal file
333
Assets/02_Scripts/Enemy/BossAI.cs
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// BossAttack
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 보스 공격 하나의 설정. 별도 .asset 없이 BossAI 인스펙터에서 배열로 편집.
|
||||||
|
// ============================================================================
|
||||||
|
[Serializable]
|
||||||
|
public class BossAttack
|
||||||
|
{
|
||||||
|
public string Name = "Attack"; // 디버그/식별용 이름
|
||||||
|
public int MinPhase; // 이 페이즈(0부터) 이상에서만 사용 가능
|
||||||
|
public float Range = 1.5f; // 타겟이 이 X거리 안일 때만 사용
|
||||||
|
public float Windup = 0.4f; // 공격 시작 ~ 데미지 발생 (선딜)
|
||||||
|
public float Duration = 0.9f; // 공격 동작 전체 길이 (이 동안 이동 정지)
|
||||||
|
public float Cooldown = 2f; // 이 공격 자체의 재사용 대기
|
||||||
|
public int Damage = 15;
|
||||||
|
public Vector2 HitVelocity = new Vector2(5f, 2f); // 피격자 넉백 (X는 보스 facing 방향으로 부호 적용)
|
||||||
|
public string AnimationState = "BossAttack";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// BossAI
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 보스의 두뇌. EnemyAI와 골격(감지→추격→공격)은 같지만:
|
||||||
|
// - 여러 공격(BossAttack 배열)을 보유하고, 사거리·쿨다운·페이즈 조건을
|
||||||
|
// 만족하는 것 중 무작위로 골라 사용
|
||||||
|
// - 각 공격의 MinPhase로 페이즈 잠금 → Boss.CurrentPhase가 오르면 신규 공격 해금
|
||||||
|
// 같은 GameObject의 Enemy(몸)·Boss(페이즈)와 함께 동작.
|
||||||
|
// ============================================================================
|
||||||
|
[RequireComponent(typeof(Enemy))]
|
||||||
|
[RequireComponent(typeof(Rigidbody2D))]
|
||||||
|
public class BossAI : MonoBehaviour
|
||||||
|
{
|
||||||
|
private enum AIState { Idle, Chase, Attack }
|
||||||
|
|
||||||
|
[Header("Target")]
|
||||||
|
[SerializeField] private Transform _target; // 비우면 자동으로 플레이어 검색
|
||||||
|
[SerializeField] private bool _autoFindPlayer = true;
|
||||||
|
[SerializeField] private float _detectRange = 12f; // 이 X거리 안에 들어오면 인지
|
||||||
|
[SerializeField] private float _detectVerticalRange = 4f;
|
||||||
|
|
||||||
|
[Header("Movement")]
|
||||||
|
[SerializeField] private float _moveSpeed = 2.5f;
|
||||||
|
[SerializeField] private float _stopDistance = 1f; // 타겟과 이 거리 안이면 이동 정지
|
||||||
|
|
||||||
|
[Header("Attacks")]
|
||||||
|
[SerializeField] private BossAttack[] _attacks;
|
||||||
|
[SerializeField] private float _attackVerticalTolerance = 1.5f; // 공격 적중 인정 세로 오차
|
||||||
|
[SerializeField] private float _globalAttackCooldown = 0.6f; // 공격 종료 후 다음 공격까지 최소 간격
|
||||||
|
|
||||||
|
[Header("Animation")]
|
||||||
|
[SerializeField] private string _idleAnimationState = "Idle";
|
||||||
|
[SerializeField] private string _runAnimationState = "Run";
|
||||||
|
|
||||||
|
private Enemy _enemy;
|
||||||
|
private Boss _boss;
|
||||||
|
private Health _health;
|
||||||
|
private Rigidbody2D _rb;
|
||||||
|
private Animator _anim;
|
||||||
|
private SpriteRenderer _spriteRenderer;
|
||||||
|
private IDamageable _targetDamageable;
|
||||||
|
|
||||||
|
private AIState _state = AIState.Idle;
|
||||||
|
private bool _hasAggro;
|
||||||
|
|
||||||
|
private float[] _attackCooldownTimers; // _attacks와 1:1 — 각 공격의 남은 쿨다운
|
||||||
|
private int[] _attackPickBuffer; // PickAttackIndex 후보 수집용 (GC 회피)
|
||||||
|
private float _globalCooldownTimer; // 공격 사이 전역 대기
|
||||||
|
|
||||||
|
private BossAttack _currentAttack; // 진행 중인 공격
|
||||||
|
private bool _isAttacking;
|
||||||
|
private bool _attackDamageApplied; // 이번 공격에서 데미지를 이미 줬는지
|
||||||
|
private float _attackTimer;
|
||||||
|
|
||||||
|
private float _facingDirection = 1f;
|
||||||
|
private string _activeAnimationState;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_enemy = GetComponent<Enemy>();
|
||||||
|
_boss = GetComponent<Boss>();
|
||||||
|
_health = GetComponent<Health>();
|
||||||
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
|
_anim = GetComponentInChildren<Animator>();
|
||||||
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
|
||||||
|
int count = _attacks != null ? _attacks.Length : 0;
|
||||||
|
_attackCooldownTimers = new float[count];
|
||||||
|
_attackPickBuffer = new int[count];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
TickCooldowns();
|
||||||
|
ResolveTarget();
|
||||||
|
|
||||||
|
// 사망 / 행동 불가(피격 경직·잡힘) / 페이즈 전환 중 / 타겟 없음 → 정지.
|
||||||
|
if (_health.IsDead || !_enemy.CanUseAI
|
||||||
|
|| (_boss != null && _boss.IsTransitioning) || _target == null)
|
||||||
|
{
|
||||||
|
if (_isAttacking) CancelAttack();
|
||||||
|
StopMoving();
|
||||||
|
SetState(AIState.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshAggro();
|
||||||
|
|
||||||
|
// 공격 중이면 facing을 고정한 채 공격 진행만 처리.
|
||||||
|
if (_isAttacking)
|
||||||
|
{
|
||||||
|
TickAttack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFacing();
|
||||||
|
|
||||||
|
if (!_hasAggro)
|
||||||
|
{
|
||||||
|
StopMoving();
|
||||||
|
SetState(AIState.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용 가능한 공격이 있으면 공격 시작, 없으면 추격.
|
||||||
|
int attackIndex = _globalCooldownTimer <= 0f ? PickAttackIndex() : -1;
|
||||||
|
if (attackIndex >= 0)
|
||||||
|
BeginAttack(attackIndex);
|
||||||
|
else
|
||||||
|
SetState(AIState.Chase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (_rb == null) return;
|
||||||
|
|
||||||
|
// 추격 상태에서만 X축 이동. 그 외 상태에선 손대지 않음
|
||||||
|
// (정지는 Update에서 상태 전환 시 StopMoving으로 처리).
|
||||||
|
if (_state != AIState.Chase || _target == null) return;
|
||||||
|
|
||||||
|
float dx = _target.position.x - transform.position.x;
|
||||||
|
Vector2 v = _rb.linearVelocity;
|
||||||
|
v.x = Mathf.Abs(dx) <= _stopDistance ? 0f : Mathf.Sign(dx) * _moveSpeed;
|
||||||
|
_rb.linearVelocity = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 공격 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 현재 사용 가능한 공격 후보 중 무작위 1개의 인덱스 반환. 없으면 -1.
|
||||||
|
// 조건: 페이즈 해금 + 쿨다운 종료 + 사거리 안.
|
||||||
|
private int PickAttackIndex()
|
||||||
|
{
|
||||||
|
if (_attacks == null || _attacks.Length == 0 || _target == null) return -1;
|
||||||
|
|
||||||
|
Vector2 delta = _target.position - transform.position;
|
||||||
|
if (Mathf.Abs(delta.y) > _attackVerticalTolerance) return -1;
|
||||||
|
float distanceX = Mathf.Abs(delta.x);
|
||||||
|
int phase = _boss != null ? _boss.CurrentPhase : 0;
|
||||||
|
|
||||||
|
int candidateCount = 0;
|
||||||
|
for (int i = 0; i < _attacks.Length; i++)
|
||||||
|
{
|
||||||
|
BossAttack a = _attacks[i];
|
||||||
|
if (a == null) continue;
|
||||||
|
if (phase < a.MinPhase) continue; // 페이즈 미해금
|
||||||
|
if (_attackCooldownTimers[i] > 0f) continue; // 쿨다운 중
|
||||||
|
if (distanceX > a.Range) continue; // 사거리 밖
|
||||||
|
_attackPickBuffer[candidateCount++] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateCount == 0) return -1;
|
||||||
|
return _attackPickBuffer[UnityEngine.Random.Range(0, candidateCount)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginAttack(int index)
|
||||||
|
{
|
||||||
|
_currentAttack = _attacks[index];
|
||||||
|
_isAttacking = true;
|
||||||
|
_attackDamageApplied = false;
|
||||||
|
_attackTimer = 0f;
|
||||||
|
_attackCooldownTimers[index] = _currentAttack.Cooldown;
|
||||||
|
// 공격 동작 길이 + 후딜만큼 다음 공격을 막는다.
|
||||||
|
_globalCooldownTimer = _currentAttack.Duration + _globalAttackCooldown;
|
||||||
|
|
||||||
|
_state = AIState.Attack;
|
||||||
|
StopMoving();
|
||||||
|
UpdateFacing(); // 공격 시작 순간 타겟을 바라보고, 이후 공격 끝까지 고정
|
||||||
|
PlayAnim(_currentAttack.AnimationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickAttack()
|
||||||
|
{
|
||||||
|
_attackTimer += Time.deltaTime;
|
||||||
|
StopMoving();
|
||||||
|
|
||||||
|
// 선딜이 끝나는 순간 데미지 1회 적용.
|
||||||
|
if (!_attackDamageApplied && _attackTimer >= _currentAttack.Windup)
|
||||||
|
{
|
||||||
|
_attackDamageApplied = true;
|
||||||
|
ApplyAttackDamage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공격 동작이 끝나면 종료 — 상태는 다음 Update에서 재판단.
|
||||||
|
if (_attackTimer >= _currentAttack.Duration)
|
||||||
|
{
|
||||||
|
_isAttacking = false;
|
||||||
|
_currentAttack = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelAttack()
|
||||||
|
{
|
||||||
|
_isAttacking = false;
|
||||||
|
_currentAttack = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선딜 종료 시점에 호출. 그 사이 플레이어가 사거리를 벗어났으면 빗나감.
|
||||||
|
private void ApplyAttackDamage()
|
||||||
|
{
|
||||||
|
if (_target == null || _currentAttack == null) return;
|
||||||
|
|
||||||
|
Vector2 delta = _target.position - transform.position;
|
||||||
|
if (Mathf.Abs(delta.x) > _currentAttack.Range) return;
|
||||||
|
if (Mathf.Abs(delta.y) > _attackVerticalTolerance) return;
|
||||||
|
|
||||||
|
if (_targetDamageable == null) CacheTargetComponents();
|
||||||
|
if (_targetDamageable == null) return;
|
||||||
|
|
||||||
|
Vector2 knockback = new Vector2(
|
||||||
|
_currentAttack.HitVelocity.x * _facingDirection,
|
||||||
|
_currentAttack.HitVelocity.y);
|
||||||
|
_targetDamageable.TakeDamage(_currentAttack.Damage, knockback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickCooldowns()
|
||||||
|
{
|
||||||
|
if (_globalCooldownTimer > 0f)
|
||||||
|
_globalCooldownTimer -= Time.deltaTime;
|
||||||
|
|
||||||
|
for (int i = 0; i < _attackCooldownTimers.Length; i++)
|
||||||
|
{
|
||||||
|
if (_attackCooldownTimers[i] > 0f)
|
||||||
|
_attackCooldownTimers[i] -= Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 타겟 / 인지 / 방향 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void ResolveTarget()
|
||||||
|
{
|
||||||
|
if (_target != null)
|
||||||
|
{
|
||||||
|
if (_targetDamageable == null) CacheTargetComponents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_autoFindPlayer) return;
|
||||||
|
|
||||||
|
PlayerController player = FindFirstObjectByType<PlayerController>();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
_target = player.transform;
|
||||||
|
CacheTargetComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheTargetComponents()
|
||||||
|
{
|
||||||
|
_targetDamageable = _target != null ? _target.GetComponent<IDamageable>() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보스는 한 번 플레이어를 인지하면 계속 추격한다 (인지 해제 없음).
|
||||||
|
private void RefreshAggro()
|
||||||
|
{
|
||||||
|
if (_hasAggro || _target == null) return;
|
||||||
|
|
||||||
|
Vector2 delta = _target.position - transform.position;
|
||||||
|
if (Mathf.Abs(delta.x) <= _detectRange && Mathf.Abs(delta.y) <= _detectVerticalRange)
|
||||||
|
_hasAggro = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFacing()
|
||||||
|
{
|
||||||
|
if (_target == null) return;
|
||||||
|
|
||||||
|
float dx = _target.position.x - transform.position.x;
|
||||||
|
if (Mathf.Abs(dx) <= 0.05f) return;
|
||||||
|
|
||||||
|
_facingDirection = Mathf.Sign(dx);
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
_spriteRenderer.flipX = _facingDirection < 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopMoving()
|
||||||
|
{
|
||||||
|
if (_rb == null) return;
|
||||||
|
Vector2 v = _rb.linearVelocity;
|
||||||
|
v.x = 0f;
|
||||||
|
_rb.linearVelocity = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 상태 / 애니메이션 ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void SetState(AIState state)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
if (state == AIState.Idle) PlayAnim(_idleAnimationState);
|
||||||
|
else if (state == AIState.Chase) PlayAnim(_runAnimationState);
|
||||||
|
// Attack 상태의 애니메이션은 BeginAttack에서 직접 재생.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 같은 State 중복 재생 방지 (Play를 매 프레임 호출하면 애니가 처음으로 리셋됨).
|
||||||
|
private void PlayAnim(string state)
|
||||||
|
{
|
||||||
|
if (_anim == null || string.IsNullOrEmpty(state)) return;
|
||||||
|
if (_activeAnimationState == state) return;
|
||||||
|
|
||||||
|
_anim.Play(state);
|
||||||
|
_activeAnimationState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = Color.yellow;
|
||||||
|
Gizmos.DrawWireSphere(transform.position, _detectRange);
|
||||||
|
|
||||||
|
if (_attacks == null) return;
|
||||||
|
Gizmos.color = Color.red;
|
||||||
|
for (int i = 0; i < _attacks.Length; i++)
|
||||||
|
if (_attacks[i] != null)
|
||||||
|
Gizmos.DrawWireSphere(transform.position, _attacks[i].Range);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Enemy/BossAI.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/BossAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 15a49fe6ad5fb2646a39dc8a830433e3
|
||||||
@@ -50,6 +50,10 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
[SerializeField] private WeaponData _dropWeapon; // null이면 드랍 안 함
|
[SerializeField] private WeaponData _dropWeapon; // null이면 드랍 안 함
|
||||||
[SerializeField] private WeaponPickup _weaponPickupPrefab; // 픽업 오브젝트 프리팹 (한 종류 공유 가능)
|
[SerializeField] private WeaponPickup _weaponPickupPrefab; // 픽업 오브젝트 프리팹 (한 종류 공유 가능)
|
||||||
|
|
||||||
|
// ─── 잡기 설정 ───────────────────────────────────────────────────────
|
||||||
|
[Header("Grab")]
|
||||||
|
[SerializeField] private bool _isGrabbable = true; // false면 플레이어가 잡을 수 없음 (보스 등)
|
||||||
|
|
||||||
private Health _health; // HP 컴포넌트 (별도 분리 → 플레이어도 같은 컴포넌트 재사용 가능)
|
private Health _health; // HP 컴포넌트 (별도 분리 → 플레이어도 같은 컴포넌트 재사용 가능)
|
||||||
private Rigidbody2D _rb;
|
private Rigidbody2D _rb;
|
||||||
private Animator _anim;
|
private Animator _anim;
|
||||||
@@ -79,8 +83,14 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
private Vector2 _grabTargetPosition;// 잡힌 동안 강제로 위치할 좌표
|
private Vector2 _grabTargetPosition;// 잡힌 동안 강제로 위치할 좌표
|
||||||
private int _grabSolidMask; // 잡혀서 이동할 때 벽에 끼이지 않게 검사할 솔리드 레이어
|
private int _grabSolidMask; // 잡혀서 이동할 때 벽에 끼이지 않게 검사할 솔리드 레이어
|
||||||
|
|
||||||
|
// ─── 무적 ────────────────────────────────────────────────────────────
|
||||||
|
// true면 모든 피격을 무시한다 (보스 페이즈 전환 i-frame 등). SetInvulnerable로 토글.
|
||||||
|
private bool _isInvulnerable;
|
||||||
|
|
||||||
public bool IsDead => _health != null && _health.IsDead;
|
public bool IsDead => _health != null && _health.IsDead;
|
||||||
public bool IsGrabbed => _isGrabbed;
|
public bool IsGrabbed => _isGrabbed;
|
||||||
|
public bool IsGrabbable => _isGrabbable;
|
||||||
|
public bool IsInvulnerable => _isInvulnerable;
|
||||||
public bool IsInHitReaction => _hitStunTimer > 0f || _hitReactionTimer > 0f || _isHitPositionCorrecting || _flashTimer > 0f;
|
public bool IsInHitReaction => _hitStunTimer > 0f || _hitReactionTimer > 0f || _isHitPositionCorrecting || _flashTimer > 0f;
|
||||||
public bool CanUseAI => !IsDead && !_isGrabbed && !IsInHitReaction;
|
public bool CanUseAI => !IsDead && !_isGrabbed && !IsInHitReaction;
|
||||||
|
|
||||||
@@ -274,6 +284,8 @@ 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, float hitStunDuration = -1f)
|
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f, float hitStunDuration = -1f)
|
||||||
{
|
{
|
||||||
if (_health == null || _health.IsDead) return;
|
if (_health == null || _health.IsDead) return;
|
||||||
|
// 무적이면 데미지/플래시/넉백 전부 무시.
|
||||||
|
if (_isInvulnerable) return;
|
||||||
|
|
||||||
float appliedHitStunDuration = hitStunDuration >= 0f ? hitStunDuration : _hitStunDuration;
|
float appliedHitStunDuration = hitStunDuration >= 0f ? hitStunDuration : _hitStunDuration;
|
||||||
_hitStunTimer = Mathf.Max(_hitStunTimer, appliedHitStunDuration);
|
_hitStunTimer = Mathf.Max(_hitStunTimer, appliedHitStunDuration);
|
||||||
@@ -306,10 +318,17 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
|||||||
Debug.Log($"{name} 피격: -{amount} (HP: {_health.CurrentHealth}/{_health.MaxHealth})");
|
Debug.Log($"{name} 피격: -{amount} (HP: {_health.CurrentHealth}/{_health.MaxHealth})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 무적 토글. 보스 페이즈 전환 중 무적 부여 등에 사용 (Boss 컴포넌트가 호출).
|
||||||
|
public void SetInvulnerable(bool value)
|
||||||
|
{
|
||||||
|
_isInvulnerable = value;
|
||||||
|
}
|
||||||
|
|
||||||
// 플레이어가 잡기 액션 시작 시 호출. 이 적이 잡힘 상태로 진입.
|
// 플레이어가 잡기 액션 시작 시 호출. 이 적이 잡힘 상태로 진입.
|
||||||
// 이후 매 FixedUpdate에서 _grabTargetPosition으로 강제 이동됨.
|
// 이후 매 FixedUpdate에서 _grabTargetPosition으로 강제 이동됨.
|
||||||
public void BeginGrab(string grabbedAnimationState, int solidMask)
|
public void BeginGrab(string grabbedAnimationState, int solidMask)
|
||||||
{
|
{
|
||||||
|
if (!_isGrabbable) return;
|
||||||
if (_health == null || _health.IsDead) return;
|
if (_health == null || _health.IsDead) return;
|
||||||
|
|
||||||
_isGrabbed = true;
|
_isGrabbed = true;
|
||||||
|
|||||||
6
Assets/02_Scripts/ISceneInitializable.cs
Normal file
6
Assets/02_Scripts/ISceneInitializable.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public interface ISceneInitializable
|
||||||
|
{
|
||||||
|
public void OnSceneLoaded();
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/ISceneInitializable.cs.meta
Normal file
2
Assets/02_Scripts/ISceneInitializable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f914cb8158f1b04db838af77516820b
|
||||||
116
Assets/02_Scripts/Managers/SceneLoadManager.cs
Normal file
116
Assets/02_Scripts/Managers/SceneLoadManager.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
public class SceneLoadManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static SceneLoadManager Instance;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance == null)
|
||||||
|
{
|
||||||
|
Instance = this; //만들어진 자신을 인스턴스로 설정
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||||
|
OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//씬이 로드되었을때 호출
|
||||||
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||||
|
{
|
||||||
|
if(scene.name == "GameScene")
|
||||||
|
{
|
||||||
|
MonoBehaviour[] allObjs = UnityEngine.Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
|
||||||
|
|
||||||
|
foreach (var obj in allObjs)
|
||||||
|
{
|
||||||
|
if (obj is ISceneInitializable initializable)
|
||||||
|
{
|
||||||
|
//씬에서 ISceneInitializable 인터페이스를 가진 오브젝트의 초기화 로직을 실행
|
||||||
|
initializable.OnSceneLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSceneLoadingProgressValue(float value)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public void SetSceneLoadingProgressValue(float value,string loadingText)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestSceneChange(string sceneName)
|
||||||
|
{
|
||||||
|
_ = SceneChange(sceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Awaitable SceneChange(string sceneName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//로딩바 수치 0으로 설정
|
||||||
|
SetSceneLoadingProgressValue(0f);
|
||||||
|
|
||||||
|
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
|
||||||
|
|
||||||
|
//자동 전환을 하고 싶지 않을 경우 해당값을 false로 두었다가 true로 바꾸면 그 때 전환됨
|
||||||
|
op.allowSceneActivation = false;
|
||||||
|
|
||||||
|
//화면에 보여줄 로딩 수치
|
||||||
|
float displayProgress = 0f;
|
||||||
|
|
||||||
|
//op.progress 0.9가 데이터 로딩이 끝난 기준 allowSceneActivation이 트루면 다음으로 넘어가면서 op.isDone이 true가 된다.
|
||||||
|
while (op.progress < 0.9f)
|
||||||
|
{
|
||||||
|
//실제 로딩 수치
|
||||||
|
float realProgress = Mathf.Clamp01(op.progress / 0.9f);
|
||||||
|
|
||||||
|
//보여줄 값을 실제값을 향해 부드럽게 이동
|
||||||
|
displayProgress = Mathf.MoveTowards(displayProgress, realProgress, Time.deltaTime * 0.5f);
|
||||||
|
|
||||||
|
// 로딩바 UI에 값 적용
|
||||||
|
SetSceneLoadingProgressValue(displayProgress);
|
||||||
|
|
||||||
|
//자기자신이 파괴될때 토큰에 취소요청을 보냄
|
||||||
|
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
//로딩바 수치 1(100%)로 설정 (데이터 로딩은 이미 끝이기 때문에)
|
||||||
|
SetSceneLoadingProgressValue(1);
|
||||||
|
|
||||||
|
// 잠시 대기했다가 전환
|
||||||
|
await Awaitable.WaitForSecondsAsync(1.0f, this.destroyCancellationToken);
|
||||||
|
|
||||||
|
// 다음씬으로 넘어가도 됨을 알림
|
||||||
|
op.allowSceneActivation = true;
|
||||||
|
|
||||||
|
// 씬 활성화가 완전히 끝날 때까지 대기
|
||||||
|
// allowSceneActivation가 true가 되고 완전히 전환되기까지는 몇프레임 걸림. op.isDone 은 이 과정이 끝난 뒤에 true가 됨.
|
||||||
|
while(!op.isDone)
|
||||||
|
{
|
||||||
|
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.Log("씬 전환 작업이 취소됨");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Managers/SceneLoadManager.cs.meta
Normal file
2
Assets/02_Scripts/Managers/SceneLoadManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97560af4542644140871240158003267
|
||||||
@@ -860,7 +860,7 @@ private Enemy FindGrabTarget(ActionData data)
|
|||||||
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
|
Vector2 playerPosition = _rb != null ? _rb.position : (Vector2)transform.position;
|
||||||
float grabRangeSqr = data.GrabRange * data.GrabRange;
|
float grabRangeSqr = data.GrabRange * data.GrabRange;
|
||||||
|
|
||||||
if (_lastHitEnemy != null && _lastHitEnemy.isActiveAndEnabled &&
|
if (_lastHitEnemy != null && _lastHitEnemy.isActiveAndEnabled && _lastHitEnemy.IsGrabbable &&
|
||||||
GetEnemySqrDistance(_lastHitEnemy, playerPosition) <= grabRangeSqr)
|
GetEnemySqrDistance(_lastHitEnemy, playerPosition) <= grabRangeSqr)
|
||||||
{
|
{
|
||||||
return _lastHitEnemy;
|
return _lastHitEnemy;
|
||||||
@@ -874,6 +874,7 @@ private Enemy FindGrabTarget(ActionData data)
|
|||||||
{
|
{
|
||||||
Enemy enemy = hit.GetComponentInParent<Enemy>();
|
Enemy enemy = hit.GetComponentInParent<Enemy>();
|
||||||
if (enemy == null) continue;
|
if (enemy == null) continue;
|
||||||
|
if (!enemy.IsGrabbable) continue; // 잡기 불가 적(보스 등)은 후보에서 제외
|
||||||
|
|
||||||
float sqrDistance = GetEnemySqrDistance(enemy, playerPosition);
|
float sqrDistance = GetEnemySqrDistance(enemy, playerPosition);
|
||||||
if (sqrDistance > grabRangeSqr) continue;
|
if (sqrDistance > grabRangeSqr) continue;
|
||||||
|
|||||||
@@ -4,21 +4,25 @@
|
|||||||
// HpBar
|
// HpBar
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// SpriteRenderer 기반 HP바. Canvas보다 가벼움 (Canvas Rebuild 비용 없음).
|
// SpriteRenderer 기반 HP바. Canvas보다 가벼움 (Canvas Rebuild 비용 없음).
|
||||||
// 부모(또는 Inspector 할당)의 Health 컴포넌트 이벤트 OnHealthChanged 구독.
|
|
||||||
//
|
//
|
||||||
// 동작:
|
// 폴링 방식:
|
||||||
// - Background와 Fill 두 SpriteRenderer로 구성
|
// 이벤트 구독 대신 매 프레임 Update에서 Health.Ratio를 직접 읽어 반영한다.
|
||||||
// - Fill의 스프라이트 피벗은 LEFT (0, 0.5) 여야 X 스케일 변경 시 왼쪽부터 줄어듦
|
// - 구독/해제 생명주기가 없어 OnEnable 타이밍 버그가 원천적으로 없음
|
||||||
// - HP 비율에 따라 _fill.localScale.x 조정
|
// - Update는 항상 모든 Awake 뒤에 실행되므로 Health는 늘 초기화된 상태
|
||||||
// - 임계값 기반으로 색상도 자동 변경 (높음/중간/낮음)
|
// - 비용: 매 프레임 float 비교 1회 + "비율이 바뀐 프레임"에만 시각 갱신
|
||||||
|
//
|
||||||
|
// 구성:
|
||||||
|
// - Background와 Fill 두 SpriteRenderer
|
||||||
|
// - Fill 스프라이트 피벗은 LEFT (0, 0.5) 여야 X 스케일 변경 시 왼쪽부터 줄어듦
|
||||||
|
// - HP 비율에 따라 _fill.localScale.x 조정 + 임계값별 색상 변경
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
public class HpBar : MonoBehaviour
|
public class HpBar : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private Health _health; // 비워두면 _autoFindHealthInParent로 자동 검색
|
[SerializeField] private Health _health; // 비워두면 _autoFindHealthInParent로 자동 검색
|
||||||
[SerializeField] private Transform _fill; // 채움 sprite transform (스케일 조정 대상)
|
[SerializeField] private Transform _fill; // 채움 sprite transform (스케일 조정 대상)
|
||||||
[SerializeField] private bool _autoFindHealthInParent = true; // _health가 null이면 부모에서 Health 자동 검색
|
[SerializeField] private bool _autoFindHealthInParent = true; // _health가 null이면 부모에서 Health 자동 검색
|
||||||
[SerializeField] private bool _hideWhenFull = true; // HP 풀이거나 0일 때 HpBar 자동 숨김
|
[SerializeField] private bool _hideWhenFull = true; // HP 풀이거나 0일 때 HP바 숨김 (렌더러만 끔)
|
||||||
[SerializeField] private float _smoothSpeed = 0f; // 0이면 즉시 반영, 0보다 크면 보간 (units/sec)
|
[SerializeField] private float _smoothSpeed = 0f; // 0이면 즉시 반영, 0보다 크면 보간 (ratio/sec)
|
||||||
|
|
||||||
// ─── 임계값별 색상 ──────────────────────────────────────────────────
|
// ─── 임계값별 색상 ──────────────────────────────────────────────────
|
||||||
// ratio > _midThreshold → _highColor (정상)
|
// ratio > _midThreshold → _highColor (정상)
|
||||||
@@ -32,9 +36,10 @@ public class HpBar : MonoBehaviour
|
|||||||
[SerializeField, Range(0f, 1f)] private float _lowThreshold = 0.2f;
|
[SerializeField, Range(0f, 1f)] private float _lowThreshold = 0.2f;
|
||||||
|
|
||||||
private Vector3 _baseFillScale; // 풀체력 시 Fill 스케일 (Awake에 캐싱, 이후 ratio 곱해서 사용)
|
private Vector3 _baseFillScale; // 풀체력 시 Fill 스케일 (Awake에 캐싱, 이후 ratio 곱해서 사용)
|
||||||
private SpriteRenderer _fillRenderer; // Fill의 색상 변경용 SpriteRenderer
|
private SpriteRenderer _fillRenderer; // Fill 색상 변경용
|
||||||
private float _currentRatio = 1f; // 현재 표시된 HP 비율 (보간 진행 시 _targetRatio로 수렴)
|
private SpriteRenderer[] _renderers; // 숨김 처리용 — Background + Fill 전부
|
||||||
private float _targetRatio = 1f; // 도달해야 할 HP 비율
|
private float _currentRatio = 1f; // 현재 표시 중인 HP 비율 (보간 시 점진적으로 수렴)
|
||||||
|
private float _appliedRatio = -1f; // 마지막으로 시각에 반영한 비율 (중복 갱신 방지, -1이면 첫 프레임 강제 갱신)
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -46,55 +51,30 @@ private void Awake()
|
|||||||
_baseFillScale = _fill.localScale;
|
_baseFillScale = _fill.localScale;
|
||||||
_fillRenderer = _fill.GetComponent<SpriteRenderer>();
|
_fillRenderer = _fill.GetComponent<SpriteRenderer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 숨김 토글 대상: 이 HP바 계층의 모든 SpriteRenderer (Background + Fill).
|
||||||
|
_renderers = GetComponentsInChildren<SpriteRenderer>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 활성/비활성 토글 시 자동 구독·해제 (메모리 누수 방지).
|
// 매 프레임 Health 상태를 폴링해서 반영.
|
||||||
// 활성 시 즉시 한 번 갱신해서 현재 HP 상태 반영.
|
// 비율이 실제로 바뀐 프레임에만 transform/color를 건드린다 (정지 시 비용 거의 0).
|
||||||
private void OnEnable()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (_health == null) return;
|
if (_health == null) return;
|
||||||
|
|
||||||
_health.OnHealthChanged += HandleHealthChanged;
|
float target = _health.Ratio;
|
||||||
HandleHealthChanged(_health.CurrentHealth, _health.MaxHealth);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
if (_smoothSpeed > 0f)
|
||||||
{
|
_currentRatio = Mathf.MoveTowards(_currentRatio, target, _smoothSpeed * Time.deltaTime);
|
||||||
if (_health != null)
|
else
|
||||||
_health.OnHealthChanged -= HandleHealthChanged;
|
_currentRatio = target;
|
||||||
}
|
|
||||||
|
|
||||||
// 보간 모드일 때만 매 프레임 스케일 갱신.
|
if (Mathf.Approximately(_currentRatio, _appliedRatio)) return;
|
||||||
private void Update()
|
_appliedRatio = _currentRatio;
|
||||||
{
|
|
||||||
if (_smoothSpeed <= 0f) return;
|
|
||||||
if (Mathf.Approximately(_currentRatio, _targetRatio)) return;
|
|
||||||
|
|
||||||
_currentRatio = Mathf.MoveTowards(_currentRatio, _targetRatio, _smoothSpeed * Time.deltaTime);
|
|
||||||
ApplyScale();
|
ApplyScale();
|
||||||
}
|
ApplyColor(_currentRatio);
|
||||||
|
ApplyVisibility();
|
||||||
// Health.OnHealthChanged 이벤트 콜백. ratio 계산 → 스케일/색상 갱신 → 숨김 처리.
|
|
||||||
private void HandleHealthChanged(int current, int max)
|
|
||||||
{
|
|
||||||
_targetRatio = max > 0 ? (float)current / max : 0f;
|
|
||||||
|
|
||||||
// 즉시 모드면 바로 스케일 반영. 보간 모드면 Update에서 점진적으로.
|
|
||||||
if (_smoothSpeed <= 0f)
|
|
||||||
{
|
|
||||||
_currentRatio = _targetRatio;
|
|
||||||
ApplyScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyColor(_targetRatio);
|
|
||||||
|
|
||||||
// 풀체력(1)이거나 사망(0)이면 HpBar 자체를 숨김 (UI 정리 + GameObject 부하 감소).
|
|
||||||
if (_hideWhenFull && _fill != null)
|
|
||||||
{
|
|
||||||
bool shouldShow = _targetRatio < 1f && _targetRatio > 0f;
|
|
||||||
if (gameObject.activeSelf != shouldShow)
|
|
||||||
gameObject.SetActive(shouldShow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill의 X 스케일 = baseScale.x × ratio. 피벗이 LEFT여야 왼쪽부터 채워짐.
|
// Fill의 X 스케일 = baseScale.x × ratio. 피벗이 LEFT여야 왼쪽부터 채워짐.
|
||||||
@@ -112,14 +92,26 @@ private void ApplyColor(float ratio)
|
|||||||
{
|
{
|
||||||
if (_fillRenderer == null) return;
|
if (_fillRenderer == null) return;
|
||||||
|
|
||||||
Color color;
|
|
||||||
if (ratio <= _lowThreshold)
|
if (ratio <= _lowThreshold)
|
||||||
color = _lowColor;
|
_fillRenderer.color = _lowColor;
|
||||||
else if (ratio <= _midThreshold)
|
else if (ratio <= _midThreshold)
|
||||||
color = _midColor;
|
_fillRenderer.color = _midColor;
|
||||||
else
|
else
|
||||||
color = _highColor;
|
_fillRenderer.color = _highColor;
|
||||||
|
}
|
||||||
|
|
||||||
_fillRenderer.color = color;
|
// HP가 풀(1)이거나 사망(0)이면 HP바를 숨김.
|
||||||
|
// GameObject가 아니라 SpriteRenderer의 enabled만 끈다 — GameObject를 끄면
|
||||||
|
// Update가 멈춰 폴링이 중단되고 다시 켤 수 없게 되기 때문.
|
||||||
|
private void ApplyVisibility()
|
||||||
|
{
|
||||||
|
if (!_hideWhenFull || _renderers == null) return;
|
||||||
|
|
||||||
|
bool visible = _currentRatio > 0f && _currentRatio < 1f;
|
||||||
|
for (int i = 0; i < _renderers.Length; i++)
|
||||||
|
{
|
||||||
|
if (_renderers[i] != null)
|
||||||
|
_renderers[i].enabled = visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Assets/03_Character/ColorMan/Prefabs/BossMan.prefab
LFS
Normal file
BIN
Assets/03_Character/ColorMan/Prefabs/BossMan.prefab
LFS
Normal file
Binary file not shown.
7
Assets/03_Character/ColorMan/Prefabs/BossMan.prefab.meta
Normal file
7
Assets/03_Character/ColorMan/Prefabs/BossMan.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 22b71e2307558b84093af390934cdac6
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
BIN
Assets/05_Data/Wave/WaveData_Boss.asset
LFS
Normal file
BIN
Assets/05_Data/Wave/WaveData_Boss.asset
LFS
Normal file
Binary file not shown.
8
Assets/05_Data/Wave/WaveData_Boss.asset.meta
Normal file
8
Assets/05_Data/Wave/WaveData_Boss.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3bfeb438ec14d3e4e9c4621d0a47ec31
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/06_Textures/BossZoneButton.png
LFS
Normal file
BIN
Assets/06_Textures/BossZoneButton.png
LFS
Normal file
Binary file not shown.
169
Assets/06_Textures/BossZoneButton.png.meta
Normal file
169
Assets/06_Textures/BossZoneButton.png.meta
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 77250519c4adab74e9f2f68094dc2c86
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable:
|
||||||
|
- first:
|
||||||
|
213: 6839007031424135520
|
||||||
|
second: BossZoneButton_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: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
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: BossZoneButton_0
|
||||||
|
rect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 882
|
||||||
|
height: 239
|
||||||
|
alignment: 0
|
||||||
|
pivot: {x: 0, y: 0}
|
||||||
|
border: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
customData:
|
||||||
|
outline: []
|
||||||
|
physicsShape: []
|
||||||
|
tessellationDetail: -1
|
||||||
|
bones: []
|
||||||
|
spriteID: 0613f6419a809ee50800000000000000
|
||||||
|
internalID: 6839007031424135520
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable:
|
||||||
|
BossZoneButton_0: 6839007031424135520
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user