From 17b3cf67462e7f729e8aa74474ad4357075c0767 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Wed, 25 Feb 2026 15:15:29 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B2=B4=EB=A0=A5=EB=B0=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 플레이어는 상시 표시, 나머지는 체력 변경 시 표시 --- Assembly-CSharp-Editor.csproj | 8 +- Assembly-CSharp.csproj | 10 +- Assets/Data/Templates/CreepTemplate.prefab | 2 + Assets/Data/Templates/MonsterTemplate.prefab | 2 + Assets/Prefabs/Creep/Creep1.prefab | 2 + Assets/Prefabs/Monster/Monster101.prefab | 2 + Assets/Prefabs/Monster/Monster102.prefab | 2 + Assets/Prefabs/Monster/Monster103.prefab | 2 + Assets/Prefabs/Monster/Monster104.prefab | 2 + Assets/Prefabs/Monster/Monster105.prefab | 2 + Assets/Prefabs/Player/Player.prefab | 2 + Assets/Scenes/GameMain.unity | 12 +- Assets/Scripts/Building.cs | 6 +- Assets/Scripts/BuildingHealthBar.cs | 130 +---------- Assets/Scripts/EnemyUnit.cs | 57 ++++- Assets/Scripts/IHealthProvider.cs | 24 +++ Assets/Scripts/IHealthProvider.cs.meta | 2 + Assets/Scripts/NetworkPlayerController.cs | 42 +++- Assets/Scripts/UnitHealthBar.cs | 201 ++++++++++++++++++ Assets/Scripts/UnitHealthBar.cs.meta | 2 + ...ldingHealthBar.prefab => HealthBar.prefab} | 4 +- ...hBar.prefab.meta => HealthBar.prefab.meta} | 0 22 files changed, 369 insertions(+), 147 deletions(-) create mode 100644 Assets/Scripts/IHealthProvider.cs create mode 100644 Assets/Scripts/IHealthProvider.cs.meta create mode 100644 Assets/Scripts/UnitHealthBar.cs create mode 100644 Assets/Scripts/UnitHealthBar.cs.meta rename Assets/UI/{BuildingHealthBar.prefab => HealthBar.prefab} (99%) rename Assets/UI/{BuildingHealthBar.prefab.meta => HealthBar.prefab.meta} (100%) diff --git a/Assembly-CSharp-Editor.csproj b/Assembly-CSharp-Editor.csproj index 97aa760..6ae8295 100644 --- a/Assembly-CSharp-Editor.csproj +++ b/Assembly-CSharp-Editor.csproj @@ -1370,14 +1370,14 @@ Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll False - - Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll - False - Library\ScriptAssemblies\Unity.Profiling.Core.dll False + + Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll + False + Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll False diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index a11ef8e..0db9a79 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -78,6 +78,7 @@ + @@ -88,6 +89,7 @@ + @@ -1416,14 +1418,14 @@ Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll False - - Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll - False - Library\ScriptAssemblies\Unity.Profiling.Core.dll False + + Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll + False + Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll False diff --git a/Assets/Data/Templates/CreepTemplate.prefab b/Assets/Data/Templates/CreepTemplate.prefab index 6b5f33d..e53304d 100644 --- a/Assets/Data/Templates/CreepTemplate.prefab +++ b/Assets/Data/Templates/CreepTemplate.prefab @@ -106,6 +106,8 @@ MonoBehaviour: maxHealth: 100 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &4485945348237935463 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Data/Templates/MonsterTemplate.prefab b/Assets/Data/Templates/MonsterTemplate.prefab index 3d827c0..0b03d63 100644 --- a/Assets/Data/Templates/MonsterTemplate.prefab +++ b/Assets/Data/Templates/MonsterTemplate.prefab @@ -106,6 +106,8 @@ MonoBehaviour: maxHealth: 100 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &4485945348237935463 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Creep/Creep1.prefab b/Assets/Prefabs/Creep/Creep1.prefab index d2e5ce7..c617959 100644 --- a/Assets/Prefabs/Creep/Creep1.prefab +++ b/Assets/Prefabs/Creep/Creep1.prefab @@ -983,6 +983,8 @@ MonoBehaviour: maxHealth: 20 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &2894690479083926678 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Monster/Monster101.prefab b/Assets/Prefabs/Monster/Monster101.prefab index 9d977ee..7b9bb52 100644 --- a/Assets/Prefabs/Monster/Monster101.prefab +++ b/Assets/Prefabs/Monster/Monster101.prefab @@ -1317,6 +1317,8 @@ MonoBehaviour: maxHealth: 75 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &3634736894319727576 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Monster/Monster102.prefab b/Assets/Prefabs/Monster/Monster102.prefab index dd9339e..6b525eb 100644 --- a/Assets/Prefabs/Monster/Monster102.prefab +++ b/Assets/Prefabs/Monster/Monster102.prefab @@ -1741,6 +1741,8 @@ MonoBehaviour: maxHealth: 125 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &2580823509700602203 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Monster/Monster103.prefab b/Assets/Prefabs/Monster/Monster103.prefab index 2d6f4b1..78cd5ee 100644 --- a/Assets/Prefabs/Monster/Monster103.prefab +++ b/Assets/Prefabs/Monster/Monster103.prefab @@ -663,6 +663,8 @@ MonoBehaviour: maxHealth: 65 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &7475699343328683952 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Monster/Monster104.prefab b/Assets/Prefabs/Monster/Monster104.prefab index ab25260..e530410 100644 --- a/Assets/Prefabs/Monster/Monster104.prefab +++ b/Assets/Prefabs/Monster/Monster104.prefab @@ -1096,6 +1096,8 @@ MonoBehaviour: maxHealth: 45 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &5366269420367463183 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Monster/Monster105.prefab b/Assets/Prefabs/Monster/Monster105.prefab index 2e95c47..21c677f 100644 --- a/Assets/Prefabs/Monster/Monster105.prefab +++ b/Assets/Prefabs/Monster/Monster105.prefab @@ -1341,6 +1341,8 @@ MonoBehaviour: maxHealth: 100 damageEffectPrefab: {fileID: 4021103657954561961, guid: 5c755f9bc5253ea418e919994537dcc7, type: 3} destroyEffectPrefab: {fileID: 141433446842962269, guid: 9fe8f8b3288e45a44af36ff8aa04486e, type: 3} + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!195 &1255224548206942124 NavMeshAgent: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Player/Player.prefab b/Assets/Prefabs/Player/Player.prefab index fd5198c..9623284 100644 --- a/Assets/Prefabs/Player/Player.prefab +++ b/Assets/Prefabs/Player/Player.prefab @@ -93,6 +93,8 @@ MonoBehaviour: deathEffectPrefab: {fileID: 5642766282230003982, guid: b5c8ca7ed10b61e499cce8ec3b6e2e4c, type: 3} resourcePickupPrefab: {fileID: 1627676033990080135, guid: 8c45964a69bf8fa4ba461ed217bc052f, type: 3} respawnDelay: 10 + showHealthBar: 1 + healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!143 &3007098678582223509 CharacterController: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/GameMain.unity b/Assets/Scenes/GameMain.unity index ee95fdd..a86c41d 100644 --- a/Assets/Scenes/GameMain.unity +++ b/Assets/Scenes/GameMain.unity @@ -727,8 +727,8 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 519420028} serializedVersion: 2 - m_LocalRotation: {x: 0.38268346, y: -0.00000022436977, z: 0.000000092937015, w: 0.92387956} - m_LocalPosition: {x: -16.14601, y: 18.99998, z: 514.7231} + m_LocalRotation: {x: 0.60876137, y: 0, z: 0, w: 0.7933534} + m_LocalPosition: {x: -16.14601, y: 18.99998, z: 529.7231} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -2100,13 +2100,13 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1290143989} serializedVersion: 2 - m_LocalRotation: {x: 0.38268343, y: -0.00000022436976, z: 0.00000009293699, w: 0.92387956} - m_LocalPosition: {x: -16.14601, y: 18.99998, z: 514.7231} + m_LocalRotation: {x: 0.60876137, y: 0, z: 0, w: 0.7933534} + m_LocalPosition: {x: -16.14601, y: 18.99998, z: 529.7231} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 61373299} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_LocalEulerAnglesHint: {x: 75, y: 0, z: 0} --- !u!114 &1290143992 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2125,7 +2125,7 @@ MonoBehaviour: AngularDampingMode: 0 RotationDamping: {x: 1, y: 1, z: 1} QuaternionDamping: 1 - FollowOffset: {x: 0, y: 18, z: -18} + FollowOffset: {x: 0, y: 18, z: -3} --- !u!114 &1290143993 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Building.cs b/Assets/Scripts/Building.cs index 3efa480..eab42ac 100644 --- a/Assets/Scripts/Building.cs +++ b/Assets/Scripts/Building.cs @@ -5,7 +5,7 @@ using Northbound.Data; namespace Northbound { - public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember + public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember, IHealthProvider { [Header("References")] public TowerData buildingData; @@ -63,7 +63,7 @@ namespace Northbound public event Action OnDestroyed; public event Action OnTeamChanged; - private BuildingHealthBar _healthBar; + private UnitHealthBar _healthBar; private float _lastRegenTime; public override void OnNetworkSpawn() @@ -390,7 +390,7 @@ namespace Northbound } GameObject healthBarObj = Instantiate(healthBarPrefab, transform); - _healthBar = healthBarObj.GetComponent(); + _healthBar = healthBarObj.GetComponent(); if (_healthBar != null) { diff --git a/Assets/Scripts/BuildingHealthBar.cs b/Assets/Scripts/BuildingHealthBar.cs index 8a5ddd8..aac5efc 100644 --- a/Assets/Scripts/BuildingHealthBar.cs +++ b/Assets/Scripts/BuildingHealthBar.cs @@ -6,133 +6,11 @@ namespace Northbound { /// /// 건물 위에 표시되는 체력바 + /// UnitHealthBar의 별칭 (하위 호환성 유지) /// - public class BuildingHealthBar : MonoBehaviour + [System.Obsolete("Use UnitHealthBar instead. This class is kept for backward compatibility with existing prefabs.")] + public class BuildingHealthBar : UnitHealthBar { - [Header("UI References")] - public Image fillImage; - public TextMeshProUGUI healthText; - public GameObject barContainer; - - [Header("Settings")] - public float heightOffset = 3f; - public bool hideWhenFull = true; - public float hideDelay = 3f; - public float initialShowDuration = 2f; // 건설 완료 시 체력바 표시 시간 - - [Header("Colors")] - public Color fullHealthColor = Color.green; - public Color mediumHealthColor = Color.yellow; - public Color lowHealthColor = Color.red; - public float mediumHealthThreshold = 0.6f; - public float lowHealthThreshold = 0.3f; - - private Building _building; - private Camera _mainCamera; - private float _lastShowTime; - private Canvas _canvas; - private bool _initialized = false; - - private void Awake() - { - // Canvas 설정 - _canvas = GetComponent(); - if (_canvas == null) - { - _canvas = gameObject.AddComponent(); - } - _canvas.renderMode = RenderMode.WorldSpace; - - // Canvas Scaler 설정 - var scaler = GetComponent(); - if (scaler == null) - { - scaler = gameObject.AddComponent(); - } - scaler.dynamicPixelsPerUnit = 10f; - } - - private void Start() - { - _mainCamera = Camera.main; - } - - public void Initialize(Building building) - { - _building = building; - transform.localPosition = Vector3.up * heightOffset; - - // 건설 완료 시 체력바 표시 - ShowBar(initialShowDuration); - _initialized = true; - } - - /// - /// 체력바를 지정된 시간 동안 표시 - /// - public void ShowBar(float duration = 0f) - { - if (barContainer != null) - { - barContainer.SetActive(true); - _lastShowTime = Time.time; - - // duration이 0보다 크면 그 시간 동안만 표시 - if (duration > 0) - { - _lastShowTime = Time.time + duration - hideDelay; - } - } - } - - public void UpdateHealth(int currentHealth, int maxHealth) - { - if (fillImage != null) - { - float healthPercentage = maxHealth > 0 ? (float)currentHealth / maxHealth : 0f; - fillImage.fillAmount = healthPercentage; - - // 체력에 따른 색상 변경 - fillImage.color = GetHealthColor(healthPercentage); - } - - if (healthText != null) - { - healthText.text = $"{currentHealth}/{maxHealth}"; - } - - // 체력 변경 시 체력바 표시 - ShowBar(); - } - - private void Update() - { - // 카메라를 향하도록 회전 - if (_mainCamera != null) - { - transform.rotation = Quaternion.LookRotation(transform.position - _mainCamera.transform.position); - } - - // 체력이 가득 차면 숨김 - if (hideWhenFull && barContainer != null && _building != null && _initialized) - { - float healthPercentage = _building.GetHealthPercentage(); - - if (healthPercentage >= 1f && Time.time - _lastShowTime > hideDelay) - { - barContainer.SetActive(false); - } - } - } - - private Color GetHealthColor(float healthPercentage) - { - if (healthPercentage <= lowHealthThreshold) - return lowHealthColor; - else if (healthPercentage <= mediumHealthThreshold) - return mediumHealthColor; - else - return fullHealthColor; - } + // UnitHealthBar를 상속받아 기존 프리팹과의 호환성 유지 } } \ No newline at end of file diff --git a/Assets/Scripts/EnemyUnit.cs b/Assets/Scripts/EnemyUnit.cs index 5af8285..3b42bf4 100644 --- a/Assets/Scripts/EnemyUnit.cs +++ b/Assets/Scripts/EnemyUnit.cs @@ -7,7 +7,7 @@ namespace Northbound /// 적대 유닛 (적대세력 또는 몬스터) /// [RequireComponent(typeof(Collider))] - public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember + public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember, IHealthProvider { [Header("Team Settings")] [Tooltip("이 유닛의 팀 (Hostile = 적대세력, Monster = 몬스터)")] @@ -20,6 +20,10 @@ namespace Northbound public GameObject damageEffectPrefab; public GameObject destroyEffectPrefab; + [Header("Health Bar")] + public bool showHealthBar = true; + public GameObject healthBarPrefab; + private NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, @@ -37,6 +41,8 @@ namespace Northbound /// public event System.Action OnDeath; + private UnitHealthBar _healthBar; + public override void OnNetworkSpawn() { base.OnNetworkSpawn(); @@ -46,13 +52,48 @@ namespace Northbound _currentHealth.Value = maxHealth; _team.Value = enemyTeam; } + + // 체력 변경 이벤트 구독 + _currentHealth.OnValueChanged += OnHealthChanged; + + // 체력바 생성 + if (showHealthBar && healthBarPrefab != null) + { + CreateHealthBar(); + } } public override void OnNetworkDespawn() { + _currentHealth.OnValueChanged -= OnHealthChanged; base.OnNetworkDespawn(); } + private void OnHealthChanged(int previousValue, int newValue) + { + if (_healthBar != null) + { + _healthBar.UpdateHealth(); + } + } + + private void CreateHealthBar() + { + if (_healthBar != null) + return; + + if (healthBarPrefab == null) + return; + + GameObject healthBarObj = Instantiate(healthBarPrefab, transform); + _healthBar = healthBarObj.GetComponent(); + + if (_healthBar != null) + { + _healthBar.Initialize(this); + } + } + #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) @@ -144,6 +185,20 @@ namespace Northbound #endregion + #region IHealthProvider Implementation + + public int GetCurrentHealth() => _currentHealth.Value; + + public int GetMaxHealth() => maxHealth; + + public float GetHealthPercentage() + { + int max = GetMaxHealth(); + return max > 0 ? (float)_currentHealth.Value / max : 0f; + } + + #endregion + private void OnDrawGizmosSelected() { #if UNITY_EDITOR diff --git a/Assets/Scripts/IHealthProvider.cs b/Assets/Scripts/IHealthProvider.cs new file mode 100644 index 0000000..81f69a8 --- /dev/null +++ b/Assets/Scripts/IHealthProvider.cs @@ -0,0 +1,24 @@ +namespace Northbound +{ + /// + /// 체력 정보를 제공하는 인터페이스 + /// Building, Player, Enemy 등 체력바가 필요한 모든 유닛이 구현 + /// + public interface IHealthProvider + { + /// + /// 현재 체력 + /// + int GetCurrentHealth(); + + /// + /// 최대 체력 + /// + int GetMaxHealth(); + + /// + /// 체력 비율 (0.0 ~ 1.0) + /// + float GetHealthPercentage(); + } +} diff --git a/Assets/Scripts/IHealthProvider.cs.meta b/Assets/Scripts/IHealthProvider.cs.meta new file mode 100644 index 0000000..3dc3011 --- /dev/null +++ b/Assets/Scripts/IHealthProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 30fb4b69605e00a4eb6cac716420c414 \ No newline at end of file diff --git a/Assets/Scripts/NetworkPlayerController.cs b/Assets/Scripts/NetworkPlayerController.cs index c68e4c6..8bee75a 100644 --- a/Assets/Scripts/NetworkPlayerController.cs +++ b/Assets/Scripts/NetworkPlayerController.cs @@ -6,7 +6,7 @@ using UnityEngine.InputSystem; using Unity.Cinemachine; using Northbound; -public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable +public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable, IHealthProvider { [Header("Movement Settings")] public float rotationSpeed = 10f; @@ -22,6 +22,10 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl [SerializeField] private GameObject resourcePickupPrefab; // 자원 드랍 프리팹 [SerializeField] private float respawnDelay = 10f; // 부활 대기 시간 (초) + [Header("Health Bar")] + [SerializeField] private bool showHealthBar = true; + [SerializeField] private GameObject healthBarPrefab; + // 이 플레이어를 제어하는 클라이언트 ID (서버 소유권이지만 논리적 소유자) private NetworkVariable _ownerPlayerId = new NetworkVariable( ulong.MaxValue, @@ -47,6 +51,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl private Animator _animator; private NetworkAnimator _networkAnimator; private PlayerStats _playerStats; + private UnitHealthBar _healthBar; // 이 플레이어가 로컬 플레이어인지 확인 @@ -88,6 +93,12 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl _currentHealth.OnValueChanged += OnHealthChanged; _ownerPlayerId.OnValueChanged += OnOwnerPlayerIdChanged; + // 체력바 생성 + if (showHealthBar && healthBarPrefab != null) + { + CreateHealthBar(); + } + // 이미 로컬 플레이어로 설정되어 있으면 입력 초기화 TryInitializeLocalPlayer(); } @@ -443,6 +454,12 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl private void OnHealthChanged(int previousValue, int newValue) { + // 체력바 업데이트 + if (_healthBar != null) + { + _healthBar.UpdateHealth(); + } + if (IsLocalPlayer) { // UI 업데이트 등 @@ -451,6 +468,29 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl #endregion + #region Health Bar + + private void CreateHealthBar() + { + if (_healthBar != null) + return; + + if (healthBarPrefab == null) + return; + + GameObject healthBarObj = Instantiate(healthBarPrefab, transform); + _healthBar = healthBarObj.GetComponent(); + + if (_healthBar != null) + { + // 플레이어 체력바는 상시 표시 + _healthBar.hideWhenFull = false; + _healthBar.Initialize(this); + } + } + + #endregion + #region Gizmos private void OnDrawGizmosSelected() diff --git a/Assets/Scripts/UnitHealthBar.cs b/Assets/Scripts/UnitHealthBar.cs new file mode 100644 index 0000000..23866e4 --- /dev/null +++ b/Assets/Scripts/UnitHealthBar.cs @@ -0,0 +1,201 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace Northbound +{ + /// + /// 유닛(플레이어, 적, 건물 등) 위에 표시되는 체력바 + /// IHealthProvider 인터페이스를 구현하는 모든 유닛에서 사용 가능 + /// + public class UnitHealthBar : MonoBehaviour + { + [Header("UI References")] + [Tooltip("체력바 채우기 이미지 (Filled 타입 권장)")] + public Image fillImage; + [Tooltip("체력 텍스트 (선택사항)")] + public TextMeshProUGUI healthText; + [Tooltip("체력바 전체 컨테이너")] + public GameObject barContainer; + + [Header("Settings")] + [Tooltip("유닛 위쪽으로의 높이 오프셋")] + public float heightOffset = 2f; + [Tooltip("체력이 가득 찼을 때 체력바 숨김 여부")] + public bool hideWhenFull = true; + [Tooltip("체력이 가득 찬 후 숨기까지의 지연 시간")] + public float hideDelay = 3f; + [Tooltip("초기 표시 지속 시간")] + public float initialShowDuration = 2f; + + [Header("Colors")] + public Color fullHealthColor = Color.green; + public Color mediumHealthColor = Color.yellow; + public Color lowHealthColor = Color.red; + [Range(0f, 1f)] public float mediumHealthThreshold = 0.6f; + [Range(0f, 1f)] public float lowHealthThreshold = 0.3f; + + private IHealthProvider _healthProvider; + private Camera _mainCamera; + private float _lastShowTime; + private Canvas _canvas; + private bool _initialized = false; + private Transform _targetTransform; // 따라갈 타겟 + + private void Awake() + { + // Canvas 설정 + _canvas = GetComponent(); + if (_canvas == null) + { + _canvas = gameObject.AddComponent(); + } + _canvas.renderMode = RenderMode.WorldSpace; + + // Canvas Scaler 설정 + var scaler = GetComponent(); + if (scaler == null) + { + scaler = gameObject.AddComponent(); + } + scaler.dynamicPixelsPerUnit = 10f; + } + + private void Start() + { + _mainCamera = Camera.main; + } + + /// + /// 체력바 초기화 + /// + /// 체력 정보를 제공하는 유닛 + public void Initialize(IHealthProvider healthProvider) + { + _healthProvider = healthProvider; + _targetTransform = (healthProvider as Component)?.transform; + + // 부모와의 관계를 끊고 독립적으로 이동 + transform.SetParent(null); + + // 카메라 방향으로 한 번만 회전 설정 + if (_mainCamera == null) + { + _mainCamera = Camera.main; + } + if (_mainCamera != null) + { + // 카메라의 회전을 가져와서 체력바가 카메라를 향하도록 설정 + // Y축 회전만 사용하고, X축 90도로 Canvas가 수평이 되도록 함 + float cameraYRotation = _mainCamera.transform.eulerAngles.y; + transform.rotation = Quaternion.Euler(90, cameraYRotation, 0); + } + + // 초기 체력바 표시 + ShowBar(initialShowDuration); + _initialized = true; + + // 초기 체력 업데이트 + UpdateHealth(); + } + + /// + /// 체력바를 지정된 시간 동안 표시 + /// + public void ShowBar(float duration = 0f) + { + if (barContainer != null) + { + barContainer.SetActive(true); + _lastShowTime = Time.time; + + // duration이 0보다 크면 그 시간 동안만 표시 + if (duration > 0) + { + _lastShowTime = Time.time + duration - hideDelay; + } + } + } + + /// + /// 체력바 즉시 숨김 + /// + public void HideBar() + { + if (barContainer != null) + { + barContainer.SetActive(false); + } + } + + /// + /// 체력 정보 업데이트 (IHealthProvider 사용) + /// + public void UpdateHealth() + { + if (_healthProvider == null) return; + + int currentHealth = _healthProvider.GetCurrentHealth(); + int maxHealth = _healthProvider.GetMaxHealth(); + float healthPercentage = _healthProvider.GetHealthPercentage(); + + UpdateHealthDisplay(currentHealth, maxHealth, healthPercentage); + } + + /// + /// 체력 정보 업데이트 (직접 값 전달) + /// + public void UpdateHealth(int currentHealth, int maxHealth) + { + float healthPercentage = maxHealth > 0 ? (float)currentHealth / maxHealth : 0f; + UpdateHealthDisplay(currentHealth, maxHealth, healthPercentage); + } + + private void UpdateHealthDisplay(int currentHealth, int maxHealth, float healthPercentage) + { + if (fillImage != null) + { + fillImage.fillAmount = healthPercentage; + fillImage.color = GetHealthColor(healthPercentage); + } + + if (healthText != null) + { + healthText.text = $"{currentHealth}/{maxHealth}"; + } + + // 체력 변경 시 체력바 표시 + ShowBar(); + } + + private void LateUpdate() + { + // 타겟을 따라 이동 (회전은 하지 않음) + if (_targetTransform != null) + { + transform.position = _targetTransform.position + Vector3.up * heightOffset; + } + + // 체력이 가득 차면 숨김 + if (hideWhenFull && barContainer != null && _healthProvider != null && _initialized) + { + float healthPercentage = _healthProvider.GetHealthPercentage(); + + if (healthPercentage >= 1f && Time.time - _lastShowTime > hideDelay) + { + barContainer.SetActive(false); + } + } + } + + private Color GetHealthColor(float healthPercentage) + { + if (healthPercentage <= lowHealthThreshold) + return lowHealthColor; + else if (healthPercentage <= mediumHealthThreshold) + return mediumHealthColor; + else + return fullHealthColor; + } + } +} diff --git a/Assets/Scripts/UnitHealthBar.cs.meta b/Assets/Scripts/UnitHealthBar.cs.meta new file mode 100644 index 0000000..26c3878 --- /dev/null +++ b/Assets/Scripts/UnitHealthBar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 860b045e364e57547811662937cb3ac6 \ No newline at end of file diff --git a/Assets/UI/BuildingHealthBar.prefab b/Assets/UI/HealthBar.prefab similarity index 99% rename from Assets/UI/BuildingHealthBar.prefab rename to Assets/UI/HealthBar.prefab index 5085453..2669ac6 100644 --- a/Assets/UI/BuildingHealthBar.prefab +++ b/Assets/UI/HealthBar.prefab @@ -14,7 +14,7 @@ GameObject: - component: {fileID: 100004} - component: {fileID: 100005} m_Layer: 5 - m_Name: BuildingHealthBar + m_Name: HealthBar m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -85,7 +85,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 10 - m_PresetInfoIsWorld: 0 + m_PresetInfoIsWorld: 1 --- !u!114 &100004 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/UI/BuildingHealthBar.prefab.meta b/Assets/UI/HealthBar.prefab.meta similarity index 100% rename from Assets/UI/BuildingHealthBar.prefab.meta rename to Assets/UI/HealthBar.prefab.meta