로컬이라면
시간이 흘러가면 게임 종료팝업을 띄운다고 해보자.
쉽게 예재 생각하면
public float gameTime = 20f;
void Update()
{
gameTime -= Time.deltaTime;
if (gameTime <= 0f) EndGamePopup();
}
이런식으로 작성하면된다.
하지만 네트워크 동기화시에는 모든플레이어의 타이머가 정확히 같은 순간 같은 속도로 흘러가야한다.
그렇지만
씬로드 타이밍차이,
프레임 레이트차이
재접속, 스펙테이터 관리등으로 틀어지는 시간때문에
로컬변수로만으로는 체크가 힘들다.
PhotonNetwork.Time은 Photon Cloud 서버에 부팅된 이후로 흐른 총시간을 초단위(double)로 나타낸값이다.
클라이언트는 주기적으로(0.1초~0.2초 간격)서버로부터 최신 타임스탬프를 받아와,
그 이후 로컬에서 흐른 Time.realtimeSinceStartup 차이를 더해서 내부적으로 계산한다.
그래서 모든 클라이언트가 거의 같은 시각을 읽을 수 있다.
우리는 게임이 시작되면 방을 만들때 세팅해준 시간을 기점으로 시간이 줄어들게 세팅하고싶고, 모두가 그 시간을
공유하며 0이되었을때 end popup이 뜨기를 원한다.
그렇다면 두가지정도 세팅이 필요한데
1. 방만들때 먼저 최대 시간을 int로 책정해준다. 예를들면 나는 2분 30초를 세팅했다. 150초다
public void CreateRoom(string roomName, byte maxPlayers, int matchTimeSeconds)
{
var options = new RoomOptions
{
MaxPlayers = maxPlayers,
CleanupCacheOnLeave = true,
CustomRoomProperties = new Hashtable { { CustomPropKey.MatchTimeSeconds, matchTimeSeconds } },
CustomRoomPropertiesForLobby = new[] { CustomPropKey.MatchTimeSeconds }
};
PhotonNetwork.CreateRoom(roomName, options);
}
2. 모두가 시작을 누른 타이밍에서 게임씬으로 진입하는 시점에 setCustomProperties로
딱 그때의 PhotonNetowork.Time을 저장해준다.
나는 방장이 start버튼을 누르면, 팀 배정을해주고 시작씬으로 넘어가게해놓았다. 그렇기에
코루틴으로 만들어놓은 함수가있는데 거기에 startTime을 저장해줫따.
private IEnumerator StartMatchWithDelay()
{
yield return new WaitForSeconds(3f);
// 게임 시작 플래그 + 서버 타임스탬프 기록
PhotonNetwork.CurrentRoom.SetCustomProperties(new Hashtable {
{ CustomPropKey.InGameFlag, true },
{ "StartTime", PhotonNetwork.Time }
});
PhotonNetwork.LoadLevel("GameScene");
}
그렇게되면 씬이 넘어가면서
호출되게끔한 GameHUD 에 start에서
void Start()
{
var props = PhotonNetwork.CurrentRoom.CustomProperties;
matchDuration = (int)props[CustomPropKey.MatchTimeSeconds];
startTime = (double)props["StartTime"];
resultPopup.SetActive(false);
returnButton.onClick.AddListener(OnReturnToRoom);
}
이런식으로 해당 저장한 프랍을 읽어서, 각 변수에 해당 프랍의 값들을 넣어준다.
void Update()
{
if (ended) return;
double elapsed = PhotonNetwork.Time - startTime;
int remain = Mathf.Max(0, matchDuration - (int)elapsed);
timeText.text = remain.ToString();
if (remain <= 0)
EndMatch();
}
업데이트단계에서 PhotonNetwork.Time -startTime;을 하는것은 경과시간을 체크하는것이다.
업데이트가 호출될 시점에서 PhotonNetwork.Time은 씬진입, start후에 라이프사이클 따라 가는거니
당연히 씬넘어가기전에 설정한 startTime보다 경과된 상태이다.
그 차이를 elapsed에 담는것이다. 그렇다면 Update에서는 당연히 그차이는 계속해서 커질것이다.
점점 씬 넘어오기전에 저장했던 startTime부터 흘러가고 있는것이다 시간이.
이커지는차이를
내가 설정했던 최대값인 matchDuration에서 매 업데이트때마다 빼주면 당연히 PhotonNetwork.Time이 흘러
elapsed 값이 커질때마다 machDuration에서 빼준 결과값은 점점 작아질것이다. 이것이 네트워크 동기화된
줄어드는 시간인셈.
remain에 담아서 출력해주면된다. 이때 형변환 해주면 딱 그만큼 흘렀을때 1.2 ... 1.1... 1... 까지 전부 1일테니까
1초 흘러가는 타이밍에서 1초가 깎이는것이 remain에 담길것이다.
-----
SceneManager.sceneLoaded 은 Unity의 씬로딩완료를 읽는 전역 이벤트다.
씬을 LoadScene 계열 Api로 불러와 로딩이끝나면(SceneManager.LoadScene)
Unity가 자동으로 내부에서 이 이벤트를 발생시킨다.
등록해둔 콜백이 호출되어 어떤씬이 어떤 모드로 로드되었는지 알려준다.
Awake()
OnEnable() ← 여기서 이벤트 등록
씬 로딩 후 sceneLoaded 이벤트 발생 → OnSceneLoaded 호출
Start()
…
이런 생명주기를 가지므로, OnEnable()에서 등록해주는게
첫씬로딩 -이후 재로딩 모두 놓치지않고 좋다.
OnDisable로 해제해주는게 오브젝트 껐다 켰을때 콜백이 중복등록되거나 존재하지않는
메서드 호출에러등을 방지한다.
포톤에는 그런식으로 로딩이 끝나고 씬로딩 완료를 알려주는 api는 없다
언제 로드가 끝났는지를 잡아내려면 Unity의 SceneManager.sceneLoaded를 써주는게좋다.
앞서 말했듯 씬이 완전히 로드된 다음 프레임에 호출되는데
Photon의 로딩과 순서를 맞추려면 반드시 AutomaticallySyncScene = true 상태에서 마스터가
PhotonNetwork.LoadLevel을 먼저 호출하고
각클라이언트가 해당 Unity 콜백을 통해 이제 씬이 준비됨을 인지할 수 있다.
SceneManager.sceneLoaded의 시그니처를 보면 Scene, LoadSceneMode를 타입으로 받고있다.
따라서 해당 전역이벤트에 메서드를 구독해주려면 시그니처를 맞춰야한다.
void OnEnable()
{
base.OnEnable();
SceneManager.sceneLoaded += HandleSceneLoaded;
}
만약 이렇다면
private void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.name != "GameScene") return;
StartCoroutine(SpawnLocalPlayerWhenReady());
}
이런식으로 되어야하는것이다.
파라메터가 같은 타입을 넣도록 되어있다.
SceneManager.LoadScene("GameScene"); 로 어딘가에서 호출하면 씬이
GameScene으로 넘어간다.
이때 이 GameScene에서 관리하는 매니저에서
위와 같이 OnEnable에 해당 HandleSceneLoaded를 시그니처를 통일시켜서 구독시켜놓으면
씬이 로드가 완료되는시점에서 자동으로 해당 씬이 메서드의 파라메터에 값으로 넘겨들어간다.
Scene은 빌드세팅에 포함된 씬 하나하나를 나타내는 구조체로
scene.name - 씬 에셋이름
scene.path -프로젝트 내 파일경로
scene.buildIndex -빌드세팅에서 할당된 인덱스
이며
LoadSceneMode(enum)의 경우
씬을 로드할때 어떤방식으로 로드했는지 나타내는 열거형으로
Single : 기존에 로드된 씬을 모두 언로드하고 새로 하나만 로드
Additive : 현재 씬위에 추가로 로드(여러 씬을 겹쳐보일때)
같은모드가 있다. 따로 설정하지않고 씬로드를 한다면 single이 기본값이다.
만약 여러씬을 겹쳐서 로드하고싶다면,
SceneManager.LoadScene("SubScene", LoadSceneMode.Additive);
이렇게 할수있따.
----- 위 방식으로 네트워크 지연문제를 통해 시간 오차를 해결하지 못했을시에는
Unity의 Time.time 을 이용해 해결해볼수있다.
photonnetwork.time이 서버동기화 이후부터 시작되는 지금 이순간을 서버기준으로 기록한다면,
Time.time은 Unity엔진이 그 씬이 시작된 뒤 해당 클라이언트 로컬에서 흘러온 누적초를 계산할 수 있다.
장점은 완전히 로컬이고, 코드를 직접 실행한 순간 부터 즉시 오차없이 읽어올 수 있다는 점이다.
네트워크 전달 타이밍과 무관하게 (Time.time - 시작지점)이라는 순수한 경과 시간 카운트다운에 편하다.
단점으로는 클라이언트마다 시계가 따로 굴러가기 때문에
A,B 두명의 플레이어가 정확히 동시에 타이머를 시작했다하더라도
둘의 Time.time오차 (프레임 드롭, GC, 렌더링 부하 등)에 의해 0.01~0.1초 정도 차이가 날 수 있다.
그러나 대부분 카운트다운 UI나 라운드 타이밍에선 이정도 오차는 눈에 띄지않는다고한다.
네트워크 불안정으로 인한 차이보다 아이러니하게도 씬진입 시점을 코드로 캐치해서 호출하기때문에
훨씬 안정적일 수도있다.
실제로 처음에 PhotonNetowrk.Time을 기반으로 StartTime을 찍었을때
클라이언트마다 언제 그 cp를 받고읽는지가 좀 달라 카운트다운이 잘못시작되기도했다.
Time.time기반으로 전환하면서 코루틴에서 한프레임 쉬고 호출하게되면
CustomProperties를 읽어오는 시간만 번두위 네트워크나 로딩지연과 완전히 분리되어 설정한 정확한 초에서
카운트 다운을 시작한다.
'Unity 개발 공부' 카테고리의 다른 글
| [내배캠] 본캠 63 쉐이더공부를 위한 기본이론공부 (6) | 2025.07.04 |
|---|---|
| [내배캠] 본캠 57 pun2 공부5 (2) | 2025.07.03 |
| [내배캠] 본캠 57 pun2 공부4 (1) | 2025.06.26 |
| [내배캠] 본캠 57 pun2 공부3 (0) | 2025.06.26 |
| [내배캠] 본캠 57 pun2 공부2 (3) | 2025.06.26 |