코드 리팩토링
재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
@@ -1,98 +1,131 @@
|
||||
using NUnit.Framework.Interfaces;
|
||||
using System.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public class MineableBlock : NetworkBehaviour
|
||||
/// <summary>
|
||||
/// A block that can be mined by players.
|
||||
/// Uses HealthComponent for health management and implements IDamageable.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(HealthComponent))]
|
||||
public class MineableBlock : NetworkBehaviour, IDamageable
|
||||
{
|
||||
[Header("Block Stats")]
|
||||
[SerializeField] private int maxHp = 100;
|
||||
// [동기화] 모든 플레이어가 동일한 블록 체력을 보게 함
|
||||
private NetworkVariable<int> _currentHp = new NetworkVariable<int>();
|
||||
|
||||
[Header("Drop Settings")]
|
||||
[SerializeField] private ItemData dropItemData;
|
||||
[SerializeField] private GameObject genericDropPrefab; // 여기에 위에서 만든 'GenericDroppedItem' 프리팹을 넣으세요.
|
||||
[SerializeField] private GameObject genericDropPrefab;
|
||||
|
||||
[Header("Visuals")]
|
||||
private Outline _outline;
|
||||
private Vector3 _originalPos;
|
||||
|
||||
[Header("Shake Settings")]
|
||||
[SerializeField] private float shakeDuration = 0.15f; // 흔들리는 시간
|
||||
[SerializeField] private float shakeMagnitude = 0.1f; // 흔들리는 강도
|
||||
[SerializeField] private float shakeDuration = 0.15f;
|
||||
[SerializeField] private float shakeMagnitude = 0.1f;
|
||||
private Coroutine _shakeCoroutine;
|
||||
|
||||
private Color _originalColor; // 본래의 색상을 저장할 변수
|
||||
private Color _originalColor;
|
||||
private float _lastVisibleTime;
|
||||
private const float VisibilityThreshold = 0.25f;
|
||||
|
||||
[Header("Fog Settings")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float darkIntensity = 0.2f; // 안개 속에서 얼마나 어두워질지 (0: 완전 검정, 1: 원본)
|
||||
[SerializeField] private float darkIntensity = 0.2f;
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
|
||||
private NetworkVariable<bool> isDiscovered = new NetworkVariable<bool>(false);
|
||||
private MeshRenderer _renderer;
|
||||
private HealthComponent _health;
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f;
|
||||
public float MaxHealth => _health != null ? _health.MaxHealth : 0f;
|
||||
public bool IsAlive => _health != null && _health.IsAlive;
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
TakeDamage(new DamageInfo(amount, DamageType.Mining));
|
||||
}
|
||||
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
if (_health != null)
|
||||
{
|
||||
_health.TakeDamage(damageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_health = GetComponent<HealthComponent>();
|
||||
_renderer = GetComponentInChildren<MeshRenderer>();
|
||||
_propBlock = new MaterialPropertyBlock();
|
||||
|
||||
// 시작 시에는 보이지 않게 설정
|
||||
// Start hidden
|
||||
if (_renderer != null) _renderer.enabled = false;
|
||||
|
||||
_originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor")
|
||||
if (_renderer != null && _renderer.sharedMaterial != null)
|
||||
{
|
||||
_originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor")
|
||||
? _renderer.sharedMaterial.GetColor("_BaseColor")
|
||||
: _renderer.sharedMaterial.GetColor("_Color");
|
||||
}
|
||||
|
||||
_renderer.enabled = false;
|
||||
|
||||
// 해당 오브젝트 혹은 자식에게서 Outline 컴포넌트를 찾습니다.
|
||||
// Find outline component
|
||||
_outline = GetComponentInChildren<Outline>();
|
||||
_originalPos = transform.localPosition; // 로컬 위치 저장
|
||||
_originalPos = transform.localPosition;
|
||||
|
||||
if (_outline != null)
|
||||
{
|
||||
// 게임 시작 시 하이라이트는 꺼둡니다.
|
||||
_outline.enabled = false;
|
||||
}
|
||||
else
|
||||
|
||||
// Subscribe to health events
|
||||
if (_health != null)
|
||||
{
|
||||
Debug.LogWarning($"{gameObject.name}: QuickOutline 에셋의 Outline 컴포넌트를 찾을 수 없습니다.");
|
||||
_health.OnDamaged += HandleDamaged;
|
||||
_health.OnDeath += HandleDeath;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_health != null)
|
||||
{
|
||||
_health.OnDamaged -= HandleDamaged;
|
||||
_health.OnDeath -= HandleDeath;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
_currentHp.Value = maxHp;
|
||||
}
|
||||
|
||||
// 데이터가 동기화될 때 비주얼 업데이트
|
||||
// Update visuals when discovered state syncs
|
||||
UpdateVisuals(isDiscovered.Value);
|
||||
|
||||
isDiscovered.OnValueChanged += (oldVal, newVal) => {
|
||||
isDiscovered.OnValueChanged += (oldVal, newVal) =>
|
||||
{
|
||||
if (newVal) UpdateState();
|
||||
};
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 1. 이미 발견된 블록인지는 서버 변수(isDiscovered)로 확인
|
||||
// 2. 현재 내 위치가 안개에서 벗어났는지 확인 (매우 단순화된 로직)
|
||||
if (!isDiscovered.Value)
|
||||
// Check if block should be discovered based on player distance
|
||||
if (!isDiscovered.Value && NetworkManager.Singleton != null &&
|
||||
NetworkManager.Singleton.LocalClient != null &&
|
||||
NetworkManager.Singleton.LocalClient.PlayerObject != null)
|
||||
{
|
||||
float dist = Vector3.Distance(transform.position, NetworkManager.Singleton.LocalClient.PlayerObject.transform.position);
|
||||
if (dist < FogOfWarManager.Instance.revealRadius)
|
||||
float dist = Vector3.Distance(transform.position,
|
||||
NetworkManager.Singleton.LocalClient.PlayerObject.transform.position);
|
||||
|
||||
if (FogOfWarManager.Instance != null && dist < FogOfWarManager.Instance.revealRadius)
|
||||
{
|
||||
// 서버에 "나 발견됐어!"라고 보고
|
||||
RequestRevealServerRpc();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 비주얼 업데이트: 발견된 적이 있을 때만 렌더러를 켬
|
||||
// Update renderer visibility
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.enabled = isDiscovered.Value;
|
||||
@@ -109,64 +142,64 @@ public class MineableBlock : NetworkBehaviour
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
if (_renderer == null) return;
|
||||
if (_renderer == null || !_renderer.enabled) return;
|
||||
|
||||
// 2. 현재 시야 안에 있는지 판단합니다.
|
||||
bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold;
|
||||
|
||||
// 3. 상태에 따라 색상과 렌더러 상태를 결정합니다.
|
||||
if (_renderer.enabled == false) return;
|
||||
|
||||
_renderer.GetPropertyBlock(_propBlock);
|
||||
// 2. 시야 내에 있으면 원본 색상(_originalColor), 멀어지면 어둡게 만든 색상을 적용합니다.
|
||||
Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity;
|
||||
_propBlock.SetColor("_BaseColor", targetColor);
|
||||
|
||||
_renderer.SetPropertyBlock(_propBlock);
|
||||
}
|
||||
|
||||
public void RevealBlock() // 서버에서 호출
|
||||
/// <summary>
|
||||
/// Reveal this block (called by server).
|
||||
/// </summary>
|
||||
public void RevealBlock()
|
||||
{
|
||||
if (IsServer && !isDiscovered.Value) isDiscovered.Value = true;
|
||||
if (IsServer && !isDiscovered.Value)
|
||||
{
|
||||
isDiscovered.Value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 플레이어가 주변을 훑을 때 호출해줄 함수
|
||||
/// <summary>
|
||||
/// Update local visibility for fog of war.
|
||||
/// </summary>
|
||||
public void UpdateLocalVisibility()
|
||||
{
|
||||
_lastVisibleTime = Time.time;
|
||||
}
|
||||
|
||||
// 서버에서만 대미지를 처리하도록 제한
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void TakeDamageRpc(int damageAmount)
|
||||
private void HandleDamaged(DamageInfo info)
|
||||
{
|
||||
if (_currentHp.Value <= 0) return;
|
||||
// Play hit effect on all clients
|
||||
PlayHitEffectClientRpc();
|
||||
}
|
||||
|
||||
_currentHp.Value -= damageAmount;
|
||||
|
||||
if (_currentHp.Value <= 0)
|
||||
private void HandleDeath()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
DestroyBlock();
|
||||
DropItem();
|
||||
GetComponent<NetworkObject>().Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyBlock()
|
||||
{
|
||||
DropItem();
|
||||
// 2. 서버에서 네트워크 오브젝트 제거 (모든 클라이언트에서 사라짐)
|
||||
GetComponent<NetworkObject>().Despawn();
|
||||
}
|
||||
|
||||
// 하이라이트 상태를 설정하는 공개 메서드
|
||||
/// <summary>
|
||||
/// Set highlight state for targeting feedback.
|
||||
/// </summary>
|
||||
public void SetHighlight(bool isOn)
|
||||
{
|
||||
if (_outline == null) return;
|
||||
|
||||
// 외곽선 컴포넌트 활성화/비활성화
|
||||
_outline.enabled = isOn;
|
||||
if (_outline != null)
|
||||
{
|
||||
_outline.enabled = isOn;
|
||||
}
|
||||
}
|
||||
|
||||
// 서버에서 호출하여 모든 클라이언트에게 흔들림 지시
|
||||
/// <summary>
|
||||
/// Play hit visual effect on all clients.
|
||||
/// </summary>
|
||||
[ClientRpc]
|
||||
public void PlayHitEffectClientRpc()
|
||||
{
|
||||
@@ -181,14 +214,9 @@ public class MineableBlock : NetworkBehaviour
|
||||
{
|
||||
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
|
||||
transform.localPosition = _originalPos + randomOffset;
|
||||
|
||||
// 좌표가 실제로 바뀌고 있는지 로그 출력
|
||||
// Debug.Log($"현재 좌표: {transform.localPosition}");
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
transform.localPosition = _originalPos;
|
||||
}
|
||||
|
||||
@@ -196,13 +224,11 @@ public class MineableBlock : NetworkBehaviour
|
||||
{
|
||||
if (!IsServer || dropItemData == null || genericDropPrefab == null) return;
|
||||
|
||||
// 원본 블록이 아니라 '범용 컨테이너'를 소환합니다.
|
||||
GameObject dropObj = Instantiate(genericDropPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity);
|
||||
|
||||
NetworkObject netObj = dropObj.GetComponent<NetworkObject>();
|
||||
netObj.Spawn();
|
||||
|
||||
// 소환된 컨테이너에 "너는 어떤 아이템의 모양을 따라해야 해"라고 알려줍니다.
|
||||
if (dropObj.TryGetComponent<DroppedItem>(out var droppedItem))
|
||||
{
|
||||
droppedItem.Initialize(dropItemData.itemID);
|
||||
@@ -212,7 +238,11 @@ public class MineableBlock : NetworkBehaviour
|
||||
private void UpdateVisuals(bool discovered)
|
||||
{
|
||||
if (_renderer != null) _renderer.enabled = discovered;
|
||||
// 발견되지 않은 블록은 아웃라인도 표시되지 않아야 함
|
||||
if (!discovered && _outline != null) _outline.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the health component for direct access if needed.
|
||||
/// </summary>
|
||||
public HealthComponent Health => _health;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user