코드 리팩토링
재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
@@ -1,24 +1,21 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
public class Core : MonoBehaviour, IDamageable
|
||||
/// <summary>
|
||||
/// Core structure that players must defend.
|
||||
/// Uses HealthComponent for health management.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(HealthComponent))]
|
||||
public class Core : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float maxHealth = 100f;
|
||||
[SerializeField] private float currentHealth = 100f;
|
||||
private float CurrentHealth;
|
||||
private HealthComponent _health;
|
||||
|
||||
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
|
||||
public static event Action<float> OnHealthChanged;
|
||||
public static event Action OnCoreDestroyed;
|
||||
|
||||
void Awake() => currentHealth = maxHealth;
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
void Awake()
|
||||
{
|
||||
currentHealth -= amount;
|
||||
OnHealthChanged?.Invoke(currentHealth / maxHealth);
|
||||
|
||||
if (currentHealth <= 0)
|
||||
OnCoreDestroyed?.Invoke();
|
||||
_health = GetComponent<HealthComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the health component for direct access.
|
||||
/// </summary>
|
||||
public HealthComponent Health => _health;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement; // 씬 재시작용
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Manages game state and responds to core destruction.
|
||||
/// </summary>
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
private bool _isGameOver = false;
|
||||
[SerializeField] private Core coreReference;
|
||||
|
||||
private void OnEnable()
|
||||
private bool _isGameOver = false;
|
||||
private HealthComponent _coreHealth;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Core의 파괴 이벤트를 구독
|
||||
Core.OnCoreDestroyed += GameOver;
|
||||
// Find Core if not assigned
|
||||
if (coreReference == null)
|
||||
{
|
||||
coreReference = FindFirstObjectByType<Core>();
|
||||
}
|
||||
|
||||
// Subscribe to core's health component
|
||||
if (coreReference != null)
|
||||
{
|
||||
_coreHealth = coreReference.Health;
|
||||
if (_coreHealth != null)
|
||||
{
|
||||
_coreHealth.OnDeath += GameOver;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
private void OnDestroy()
|
||||
{
|
||||
Core.OnCoreDestroyed -= GameOver;
|
||||
// Unsubscribe to prevent memory leaks
|
||||
if (_coreHealth != null)
|
||||
{
|
||||
_coreHealth.OnDeath -= GameOver;
|
||||
}
|
||||
}
|
||||
|
||||
private void GameOver()
|
||||
@@ -23,14 +46,13 @@ public class GameManager : MonoBehaviour
|
||||
_isGameOver = true;
|
||||
Debug.Log("Game Over! Core has been destroyed.");
|
||||
|
||||
// 여기에 패배 UI 표시 로직 등을 넣습니다.
|
||||
// 예: 3초 후 게임 재시작
|
||||
// Show defeat UI here
|
||||
// Example: restart game after 3 seconds
|
||||
Invoke(nameof(RestartGame), 3f);
|
||||
}
|
||||
|
||||
private void RestartGame()
|
||||
{
|
||||
// 현재 활성화된 씬을 다시 로드
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using UnityEngine.AI;
|
||||
|
||||
public class Gate : MonoBehaviour, IDamageable
|
||||
/// <summary>
|
||||
/// Gate structure that can be destroyed.
|
||||
/// Uses HealthComponent for health management.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(HealthComponent))]
|
||||
public class Gate : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float maxHealth = 50f;
|
||||
[SerializeField] private float currentHealth = 50f;
|
||||
private float CurrentHealth;
|
||||
private HealthComponent _health;
|
||||
private NavMeshObstacle _obstacle;
|
||||
|
||||
// 체력이 변경될 때 UI 등에 알리기 위한 이벤트 (Observer 패턴)
|
||||
public static event Action<float> OnHealthChanged;
|
||||
public static event Action OnGateDestroyed;
|
||||
|
||||
void Awake() => currentHealth = maxHealth;
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
void Awake()
|
||||
{
|
||||
currentHealth -= amount;
|
||||
OnHealthChanged?.Invoke(currentHealth / maxHealth);
|
||||
_health = GetComponent<HealthComponent>();
|
||||
_obstacle = GetComponent<NavMeshObstacle>();
|
||||
|
||||
if (currentHealth <= 0)
|
||||
// Subscribe to health component events
|
||||
_health.OnDeath += HandleDeath;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_health != null)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
var obstacle = GetComponent<UnityEngine.AI.NavMeshObstacle>();
|
||||
if(obstacle != null)
|
||||
{
|
||||
obstacle.carving = false;
|
||||
obstacle.enabled = false;
|
||||
}
|
||||
OnGateDestroyed?.Invoke();
|
||||
Destroy(gameObject, 0.1f);
|
||||
_health.OnDeath -= HandleDeath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDeath()
|
||||
{
|
||||
// Disable the gate visually
|
||||
gameObject.SetActive(false);
|
||||
|
||||
// Disable NavMesh carving so enemies can pass through
|
||||
if (_obstacle != null)
|
||||
{
|
||||
_obstacle.carving = false;
|
||||
_obstacle.enabled = false;
|
||||
}
|
||||
|
||||
// Destroy the object after a short delay
|
||||
Destroy(gameObject, 0.1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the health component for direct access.
|
||||
/// </summary>
|
||||
public HealthComponent Health => _health;
|
||||
}
|
||||
|
||||
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable health component that can be attached to any entity.
|
||||
/// Supports both networked (multiplayer) and local-only usage.
|
||||
/// Implements IDamageable for compatibility with existing damage systems.
|
||||
/// </summary>
|
||||
public class HealthComponent : NetworkBehaviour, IDamageable
|
||||
{
|
||||
[Header("Health Settings")]
|
||||
[SerializeField] private float maxHealth = 100f;
|
||||
[SerializeField] private bool destroyOnDeath = false;
|
||||
[SerializeField] private float destroyDelay = 0f;
|
||||
|
||||
// Network-synced health for multiplayer
|
||||
private NetworkVariable<float> _networkHealth = new NetworkVariable<float>(
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Local health for non-networked entities or single-player
|
||||
private float _localHealth;
|
||||
|
||||
// Track if we're in a networked context
|
||||
private bool _isNetworked;
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health changes. Parameters: (currentHealth, maxHealth)
|
||||
/// </summary>
|
||||
public event Action<float, float> OnHealthChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when damage is taken. Parameter: DamageInfo
|
||||
/// </summary>
|
||||
public event Action<DamageInfo> OnDamaged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when healed. Parameter: healAmount
|
||||
/// </summary>
|
||||
public event Action<float> OnHealed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health reaches zero.
|
||||
/// </summary>
|
||||
public event Action OnDeath;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health is restored from zero (revived).
|
||||
/// </summary>
|
||||
public event Action OnRevived;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public float CurrentHealth => _isNetworked ? _networkHealth.Value : _localHealth;
|
||||
public float MaxHealth => maxHealth;
|
||||
public bool IsAlive => CurrentHealth > 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_localHealth = maxHealth;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_isNetworked = true;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
_networkHealth.Value = maxHealth;
|
||||
}
|
||||
|
||||
_networkHealth.OnValueChanged += HandleNetworkHealthChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_networkHealth.OnValueChanged -= HandleNetworkHealthChanged;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Damage Methods
|
||||
|
||||
/// <summary>
|
||||
/// Simple damage method (backwards compatible).
|
||||
/// </summary>
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
TakeDamage(new DamageInfo(amount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced damage method with full context.
|
||||
/// </summary>
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
if (damageInfo.Amount <= 0) return;
|
||||
|
||||
if (_isNetworked)
|
||||
{
|
||||
// In networked mode, request damage through server
|
||||
ApplyDamageServerRpc(damageInfo.Amount, (int)damageInfo.Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Local mode - apply directly
|
||||
ApplyDamageLocal(damageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void ApplyDamageServerRpc(float amount, int damageType)
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
|
||||
float newHealth = Mathf.Max(0, _networkHealth.Value - amount);
|
||||
_networkHealth.Value = newHealth;
|
||||
|
||||
// Notify all clients about the damage
|
||||
NotifyDamageClientRpc(amount, damageType);
|
||||
|
||||
if (newHealth <= 0)
|
||||
{
|
||||
HandleDeathServer();
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyDamageClientRpc(float amount, int damageType)
|
||||
{
|
||||
OnDamaged?.Invoke(new DamageInfo(amount, (DamageType)damageType));
|
||||
}
|
||||
|
||||
private void ApplyDamageLocal(DamageInfo info)
|
||||
{
|
||||
float previousHealth = _localHealth;
|
||||
_localHealth = Mathf.Max(0, _localHealth - info.Amount);
|
||||
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
OnDamaged?.Invoke(info);
|
||||
|
||||
if (_localHealth <= 0 && previousHealth > 0)
|
||||
{
|
||||
HandleDeathLocal();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Healing Methods
|
||||
|
||||
/// <summary>
|
||||
/// Heal the entity by a specified amount.
|
||||
/// </summary>
|
||||
public void Heal(float amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
|
||||
if (_isNetworked)
|
||||
{
|
||||
HealServerRpc(amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
HealLocal(amount);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void HealServerRpc(float amount)
|
||||
{
|
||||
bool wasAlive = _networkHealth.Value > 0;
|
||||
float newHealth = Mathf.Min(maxHealth, _networkHealth.Value + amount);
|
||||
_networkHealth.Value = newHealth;
|
||||
|
||||
NotifyHealClientRpc(amount, !wasAlive && newHealth > 0);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyHealClientRpc(float amount, bool revived)
|
||||
{
|
||||
OnHealed?.Invoke(amount);
|
||||
if (revived)
|
||||
{
|
||||
OnRevived?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void HealLocal(float amount)
|
||||
{
|
||||
bool wasAlive = _localHealth > 0;
|
||||
_localHealth = Mathf.Min(maxHealth, _localHealth + amount);
|
||||
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
OnHealed?.Invoke(amount);
|
||||
|
||||
if (!wasAlive && _localHealth > 0)
|
||||
{
|
||||
OnRevived?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully restore health to maximum.
|
||||
/// </summary>
|
||||
public void HealToFull()
|
||||
{
|
||||
Heal(maxHealth - CurrentHealth);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health Modification
|
||||
|
||||
/// <summary>
|
||||
/// Set the maximum health value.
|
||||
/// </summary>
|
||||
/// <param name="newMax">New maximum health</param>
|
||||
/// <param name="healToMax">If true, also sets current health to the new max</param>
|
||||
public void SetMaxHealth(float newMax, bool healToMax = false)
|
||||
{
|
||||
maxHealth = Mathf.Max(1, newMax);
|
||||
|
||||
if (healToMax)
|
||||
{
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = maxHealth;
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = maxHealth;
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clamp current health to new max
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = Mathf.Min(_networkHealth.Value, maxHealth);
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = Mathf.Min(_localHealth, maxHealth);
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directly set the current health (use with caution).
|
||||
/// </summary>
|
||||
public void SetHealth(float health)
|
||||
{
|
||||
float newHealth = Mathf.Clamp(health, 0, maxHealth);
|
||||
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = newHealth;
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = newHealth;
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Death Handling
|
||||
|
||||
private void HandleDeathServer()
|
||||
{
|
||||
NotifyDeathClientRpc();
|
||||
|
||||
if (destroyOnDeath)
|
||||
{
|
||||
if (destroyDelay > 0)
|
||||
{
|
||||
// Schedule despawn
|
||||
Invoke(nameof(DespawnObject), destroyDelay);
|
||||
}
|
||||
else
|
||||
{
|
||||
DespawnObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DespawnObject()
|
||||
{
|
||||
if (TryGetComponent<NetworkObject>(out var netObj))
|
||||
{
|
||||
netObj.Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyDeathClientRpc()
|
||||
{
|
||||
OnDeath?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleDeathLocal()
|
||||
{
|
||||
OnDeath?.Invoke();
|
||||
|
||||
if (destroyOnDeath)
|
||||
{
|
||||
if (destroyDelay > 0)
|
||||
{
|
||||
Destroy(gameObject, destroyDelay);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Health Change Handler
|
||||
|
||||
private void HandleNetworkHealthChanged(float previousValue, float newValue)
|
||||
{
|
||||
OnHealthChanged?.Invoke(newValue, maxHealth);
|
||||
|
||||
// Check for death on clients (server handles it in ApplyDamageServerRpc)
|
||||
if (previousValue > 0 && newValue <= 0 && !IsServer)
|
||||
{
|
||||
// Death event already sent via ClientRpc, just update local state
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get health as a normalized value (0-1).
|
||||
/// </summary>
|
||||
public float GetHealthNormalized()
|
||||
{
|
||||
return maxHealth > 0 ? CurrentHealth / maxHealth : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if health is below a certain percentage.
|
||||
/// </summary>
|
||||
public bool IsHealthBelow(float percentage)
|
||||
{
|
||||
return GetHealthNormalized() < percentage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if at full health.
|
||||
/// </summary>
|
||||
public bool IsAtFullHealth()
|
||||
{
|
||||
return CurrentHealth >= maxHealth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill the entity instantly.
|
||||
/// </summary>
|
||||
public void Kill()
|
||||
{
|
||||
TakeDamage(new DamageInfo(CurrentHealth + 1, DamageType.True));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Scripts/GameBase/HealthComponent.cs.meta
Normal file
2
Assets/Scripts/GameBase/HealthComponent.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4b9c07450e6c9c4b8c741b633a2702e
|
||||
@@ -1,5 +1,141 @@
|
||||
// IDamageable.cs
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Types of damage that can be dealt.
|
||||
/// Used for damage resistance/vulnerability systems.
|
||||
/// </summary>
|
||||
public enum DamageType
|
||||
{
|
||||
Physical,
|
||||
Magical,
|
||||
Mining,
|
||||
Environmental,
|
||||
True // Ignores resistances
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains all information about a damage event.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct DamageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of damage to deal.
|
||||
/// </summary>
|
||||
public float Amount;
|
||||
|
||||
/// <summary>
|
||||
/// Type of damage being dealt.
|
||||
/// </summary>
|
||||
public DamageType Type;
|
||||
|
||||
/// <summary>
|
||||
/// The GameObject that caused the damage (can be null).
|
||||
/// </summary>
|
||||
public GameObject Source;
|
||||
|
||||
/// <summary>
|
||||
/// World position where the damage was applied.
|
||||
/// </summary>
|
||||
public Vector3 HitPoint;
|
||||
|
||||
/// <summary>
|
||||
/// Direction the damage came from (for knockback, effects, etc.).
|
||||
/// </summary>
|
||||
public Vector3 HitDirection;
|
||||
|
||||
/// <summary>
|
||||
/// Create a simple damage info with just an amount.
|
||||
/// </summary>
|
||||
public DamageInfo(float amount)
|
||||
{
|
||||
Amount = amount;
|
||||
Type = DamageType.Physical;
|
||||
Source = null;
|
||||
HitPoint = Vector3.zero;
|
||||
HitDirection = Vector3.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create damage info with amount and type.
|
||||
/// </summary>
|
||||
public DamageInfo(float amount, DamageType type)
|
||||
{
|
||||
Amount = amount;
|
||||
Type = type;
|
||||
Source = null;
|
||||
HitPoint = Vector3.zero;
|
||||
HitDirection = Vector3.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create full damage info with all parameters.
|
||||
/// </summary>
|
||||
public DamageInfo(float amount, DamageType type, GameObject source,
|
||||
Vector3 hitPoint = default, Vector3 hitDirection = default)
|
||||
{
|
||||
Amount = amount;
|
||||
Type = type;
|
||||
Source = source;
|
||||
HitPoint = hitPoint;
|
||||
HitDirection = hitDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create mining damage.
|
||||
/// </summary>
|
||||
public static DamageInfo Mining(float amount, GameObject source = null)
|
||||
{
|
||||
return new DamageInfo(amount, DamageType.Mining, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create physical damage with source and direction.
|
||||
/// </summary>
|
||||
public static DamageInfo Physical(float amount, GameObject source, Vector3 hitPoint, Vector3 direction)
|
||||
{
|
||||
return new DamageInfo(amount, DamageType.Physical, source, hitPoint, direction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for any object that can take damage.
|
||||
/// </summary>
|
||||
public interface IDamageable
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple damage method for backwards compatibility.
|
||||
/// </summary>
|
||||
/// <param name="amount">Amount of damage to deal</param>
|
||||
void TakeDamage(float amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced damage method with full context.
|
||||
/// Default implementation calls the simple TakeDamage for backwards compatibility.
|
||||
/// </summary>
|
||||
/// <param name="damageInfo">Full damage information</param>
|
||||
void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
TakeDamage(damageInfo.Amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current health value.
|
||||
/// </summary>
|
||||
float CurrentHealth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum health value.
|
||||
/// </summary>
|
||||
float MaxHealth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this object is still alive.
|
||||
/// </summary>
|
||||
bool IsAlive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Health as a percentage (0-1).
|
||||
/// </summary>
|
||||
float HealthPercent => MaxHealth > 0 ? CurrentHealth / MaxHealth : 0f;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,118 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// ScriptableObject defining item properties.
|
||||
/// Implements IUsableItem and IEquippableItem for extensibility.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
|
||||
public class ItemData : ScriptableObject
|
||||
public class ItemData : ScriptableObject, IUsableItem, IEquippableItem
|
||||
{
|
||||
[Header("Basic Info")]
|
||||
public int itemID;
|
||||
public string itemName;
|
||||
public Sprite icon;
|
||||
public float weight; // 아이템 개당 무게
|
||||
public int maxStack = 99; // 최대 중첩 개수
|
||||
public Sprite icon;
|
||||
[TextArea] public string description;
|
||||
|
||||
[Header("Stack & Weight")]
|
||||
public float weight = 1f;
|
||||
public int maxStack = 99;
|
||||
|
||||
[Header("Visual Source")]
|
||||
public GameObject originalBlockPrefab; // 이제 이것만 있으면 됩니다!
|
||||
[Tooltip("Original prefab for dropped item visuals")]
|
||||
public GameObject originalBlockPrefab;
|
||||
|
||||
[Header("Tool Settings")]
|
||||
public bool isTool; // 도구 여부
|
||||
public PlayerActionData toolAction; // 이 도구를 들었을 때 나갈 액션 (예: MiningActionData)
|
||||
[Header("Item Behavior")]
|
||||
[Tooltip("Defines what happens when the item is used")]
|
||||
public ItemBehavior behavior;
|
||||
|
||||
[Header("Visual Settings")]
|
||||
public GameObject toolPrefab; // 캐릭터 손에 스폰될 3D 프리팹
|
||||
public Vector3 equipPositionOffset; // 손 위치 미세 조정
|
||||
public Vector3 equipRotationOffset; // 손 회전 미세 조정
|
||||
}
|
||||
[Header("Equipment Settings")]
|
||||
[Tooltip("Whether this item can be equipped (shows in hand)")]
|
||||
public bool isEquippable;
|
||||
[Tooltip("Prefab spawned in player's hand when equipped")]
|
||||
public GameObject equipmentPrefab;
|
||||
public Vector3 equipPositionOffset;
|
||||
public Vector3 equipRotationOffset;
|
||||
[Tooltip("Name of the transform to attach equipment to")]
|
||||
public string attachmentPointName = "ToolAnchor";
|
||||
|
||||
#region IUsableItem Implementation
|
||||
|
||||
public bool IsConsumable => behavior != null && behavior.IsConsumable;
|
||||
|
||||
public bool CanUse(GameObject user, GameObject target)
|
||||
{
|
||||
return behavior != null && behavior.CanUse(user, target);
|
||||
}
|
||||
|
||||
public void Use(GameObject user, GameObject target)
|
||||
{
|
||||
if (behavior != null)
|
||||
{
|
||||
behavior.Use(user, target);
|
||||
}
|
||||
}
|
||||
|
||||
public ActionDescriptor GetUseAction()
|
||||
{
|
||||
return behavior?.GetActionDescriptor();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IEquippableItem Implementation
|
||||
|
||||
public GameObject GetEquipmentPrefab() => equipmentPrefab;
|
||||
public Vector3 GetPositionOffset() => equipPositionOffset;
|
||||
public Vector3 GetRotationOffset() => equipRotationOffset;
|
||||
public string GetAttachmentPointName() => attachmentPointName;
|
||||
|
||||
public Transform FindAttachmentPoint(GameObject user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(attachmentPointName))
|
||||
return user.transform;
|
||||
|
||||
var transforms = user.GetComponentsInChildren<Transform>();
|
||||
foreach (var t in transforms)
|
||||
{
|
||||
if (t.name == attachmentPointName)
|
||||
return t;
|
||||
}
|
||||
|
||||
return user.transform;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Properties
|
||||
|
||||
/// <summary>
|
||||
/// Check if this item has any usable behavior.
|
||||
/// </summary>
|
||||
public bool HasBehavior => behavior != null;
|
||||
|
||||
/// <summary>
|
||||
/// Check if this item can be equipped.
|
||||
/// </summary>
|
||||
public bool CanBeEquipped => isEquippable && equipmentPrefab != null;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get display name for UI.
|
||||
/// </summary>
|
||||
public string GetDisplayName() => string.IsNullOrEmpty(itemName) ? name : itemName;
|
||||
|
||||
/// <summary>
|
||||
/// Get description for UI.
|
||||
/// </summary>
|
||||
public string GetDescription() => description;
|
||||
|
||||
/// <summary>
|
||||
/// Calculate total weight for a stack.
|
||||
/// </summary>
|
||||
public float GetStackWeight(int count) => weight * count;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user