Files
Northbound/Assets/Scripts/GlobalTimer.cs
dal4segno 4bd46b2a0a 네트워크 동기화 문제 해결
몬스터와 크립에 네트워크 관련 컴포넌트가 없는 문제 수정
포탈/캠프와 몬스터/크립 간의 계층 구조 해제
 - 네트워크 오브젝트끼리 계층 구조로 둘 수 없음
2026-02-04 18:16:59 +09:00

284 lines
8.1 KiB
C#

using System;
using Unity.Netcode;
using UnityEngine;
namespace Northbound
{
/// <summary>
/// 전역 타이머 - 주기적으로 반복되며 모든 클라이언트에 동기화
/// </summary>
public class GlobalTimer : NetworkBehaviour
{
public static GlobalTimer Instance { get; private set; }
[Header("Timer Settings")]
public float cycleLength = 60f; // 한 주기 길이 (초)
public bool autoStart = true;
public bool pauseOnZero = false; // 0에 도달하면 일시정지
public float exceptionalFirstCycleLength = 0;
[Header("Debug")]
public bool showDebugLogs = false;
// 현재 타이머 값 (초)
private NetworkVariable<float> _currentTime = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner
);
// 타이머 실행 중 여부
private NetworkVariable<bool> _isRunning = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner
);
// 현재 사이클 번호
private NetworkVariable<int> _cycleCount = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner
);
// 이벤트
public event Action<float> OnTimerTick; // 매 프레임 업데이트
public event Action OnCycleComplete; // 사이클 완료
public event Action<int> OnCycleStart; // 새 사이클 시작
public event Action<float> OnHalfwayPoint; // 사이클 중간 지점
private bool _hasReachedHalfway;
private bool _isFirstTime = true;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
_isFirstTime = exceptionalFirstCycleLength > 0 ? true : false;
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsServer && autoStart)
{
StartTimer();
}
// 클라이언트도 이벤트 수신을 위해 변경 감지
_currentTime.OnValueChanged += OnCurrentTimeChanged;
}
public override void OnNetworkDespawn()
{
_currentTime.OnValueChanged -= OnCurrentTimeChanged;
}
private void Update()
{
if (!IsServer || !IsOwner || !_isRunning.Value)
return;
_currentTime.Value -= Time.deltaTime;
if (_isFirstTime)
{
if (_currentTime.Value + exceptionalFirstCycleLength < cycleLength)
{
_isFirstTime = false;
CompleteCycle();
}
}
else
{
// 중간 지점 체크
if (!_hasReachedHalfway && _currentTime.Value <= cycleLength / 2f)
{
_hasReachedHalfway = true;
OnHalfwayPoint?.Invoke(cycleLength / 2f);
NotifyHalfwayClientRpc();
if (showDebugLogs)
Debug.Log($"<color=yellow>[GlobalTimer] 사이클 중간 지점 도달</color>");
}
// 사이클 완료
if (_currentTime.Value <= 0f)
{
CompleteCycle();
}
}
OnTimerTick?.Invoke(_currentTime.Value);
}
private void CompleteCycle()
{
OnCycleComplete?.Invoke();
NotifyCycleCompleteClientRpc();
if (showDebugLogs)
Debug.Log($"<color=green>[GlobalTimer] 사이클 {_cycleCount.Value} 완료</color>");
if (pauseOnZero)
{
_isRunning.Value = false;
_currentTime.Value = 0f;
}
else
{
// 다음 사이클 시작
_currentTime.Value = cycleLength;
_cycleCount.Value++;
_hasReachedHalfway = false;
OnCycleStart?.Invoke(_cycleCount.Value);
NotifyCycleStartClientRpc(_cycleCount.Value);
if (showDebugLogs)
Debug.Log($"<color=cyan>[GlobalTimer] 사이클 {_cycleCount.Value} 시작</color>");
}
}
private void OnCurrentTimeChanged(float previousValue, float newValue)
{
// 클라이언트에서도 Tick 이벤트 발생
if (!IsServer)
{
OnTimerTick?.Invoke(newValue);
}
}
#region Public API
/// <summary>
/// 타이머 시작 (서버만)
/// </summary>
public void StartTimer()
{
if (!IsServer) return;
_currentTime.Value = cycleLength;
_isRunning.Value = true;
_cycleCount.Value = 1;
_hasReachedHalfway = false;
OnCycleStart?.Invoke(_cycleCount.Value);
NotifyCycleStartClientRpc(_cycleCount.Value);
if (showDebugLogs)
Debug.Log($"<color=green>[GlobalTimer] 타이머 시작: {cycleLength}초</color>");
}
/// <summary>
/// 타이머 일시정지 (서버만)
/// </summary>
public void PauseTimer()
{
if (!IsServer) return;
_isRunning.Value = false;
if (showDebugLogs)
Debug.Log($"<color=yellow>[GlobalTimer] 타이머 일시정지</color>");
}
/// <summary>
/// 타이머 재개 (서버만)
/// </summary>
public void ResumeTimer()
{
if (!IsServer) return;
_isRunning.Value = true;
if (showDebugLogs)
Debug.Log($"<color=green>[GlobalTimer] 타이머 재개</color>");
}
/// <summary>
/// 타이머 리셋 (서버만)
/// </summary>
public void ResetTimer()
{
if (!IsServer) return;
_currentTime.Value = cycleLength;
_cycleCount.Value = 0;
_isRunning.Value = false;
_hasReachedHalfway = false;
if (showDebugLogs)
Debug.Log($"<color=yellow>[GlobalTimer] 타이머 리셋</color>");
}
/// <summary>
/// 현재 남은 시간 (초)
/// </summary>
public float GetCurrentTime() => _currentTime.Value;
/// <summary>
/// 현재 남은 시간 (분:초 형식)
/// </summary>
public string GetFormattedTime()
{
int minutes = Mathf.FloorToInt(_currentTime.Value / 60f);
int seconds = Mathf.FloorToInt(_currentTime.Value % 60f);
return $"{minutes:00}:{seconds:00}";
}
/// <summary>
/// 진행률 (0.0 ~ 1.0)
/// </summary>
public float GetProgress() => 1f - (_currentTime.Value / cycleLength);
/// <summary>
/// 현재 사이클 번호
/// </summary>
public int GetCycleCount() => _cycleCount.Value;
/// <summary>
/// 타이머 실행 중 여부
/// </summary>
public bool IsRunning() => _isRunning.Value;
#endregion
#region Client RPCs
[ClientRpc]
private void NotifyCycleCompleteClientRpc()
{
if (!IsServer)
{
OnCycleComplete?.Invoke();
}
}
[ClientRpc]
private void NotifyCycleStartClientRpc(int cycleNumber)
{
if (!IsServer)
{
OnCycleStart?.Invoke(cycleNumber);
}
}
[ClientRpc]
private void NotifyHalfwayClientRpc()
{
if (!IsServer)
{
OnHalfwayPoint?.Invoke(cycleLength / 2f);
}
}
#endregion
}
}