2024-06-21

대부분의 싱글 플레이 게임은 일시 정지 기능을 제공한다.
ESC 키나 플레이 화면의 설정 버튼 등을 통해,
게임을 잠시 멈춰두고 사운드 볼륨을 조정하거나, 게임을 종료하거나, 다시 재개할 수 있다.
플레이 화면에 Button UI 를 통해 게임을 일시 정지하고,
다음과 같은 Paused 판넬을 띄워준다.
일시 정지 화면일 때, 배경 음악은 계속 흘러나오는데 ( 실행되고 있음 )
게임의 점수를 기록하는 로직, 맵이나 플레이어, 몬스터는 멈춰있어야 한다. ( 멈춰 있음 )
어떤 방식을 사용해야 특정 기능은 멈추게 하고, 특정 기능은 진행되게 할 수 있을까?
앞서 말했듯이 Button 을 클릭하면,
게임이 일시 정지 되고 ( Time.timeScale = 0f ),
Paused 판넬을 띄워야 (Paused.SetActive(true) ) 한다.
쉽게 말해서, 시간이 흐르는 정도이다.
Time.timeScale = 1f : 시간이 정상적으로 흐른다.
Time.timeScale = 0.5f : 시간이 원래 속도보다 0.5배 느리게 흐른다.
Time.timeScale = 0f : 시간이 원래 속도보다 0배 느리게 흐른다. (멈춘다)
Monobehaviour 의 라이프 함수인
Update 는 스크립트가 Enabled 일 때, 매 프레임마다 호출된다.
FixedUpdate 는 Fixed Timestep 이라는 일정한 간격으로 호출된다.
여기서, Update 는 Time.timeScale 의 영향을 받지 않지만,
FixedUpdate 는 Time.timeScale 의 영향을 받는다.
처음에 생각한 방법은
GameManager 에서 게임이 진행중인지 아닌지 판별하는 bool 변수 isPaused 를 도입한 것이다.
GameManager 를 싱글톤으로 선언하여 GameManager.instance.isPaused 을 확인한다.
// GameManager.cs
public class GameManager : Monobehaviour
{
public static GameManager instance;
public bool isPaused;
void Awake()
{
if(instance == null)
instance = this;
isPaused = false;
}
}
// Tile.cs
public class Tile : Monobehaviour
{
void Update()
{
if(GameManager.instance.isPaused == true)
return;
Func1(); // isPaused 가 true 라면, 실행 안됨
}
}
이 방법은 매 프레임마다 GameManager.instance.isPaused 를 참조해야하여
더 좋은 방법이 없을까 고민하다 튜터님을 찾아가보았다.
흔히 사용하는 방법인 Time.timeScale 를 조정하는 것이다.
원래 이 방법을 사용하고 싶었으나,
구현 초반에는 Update 와 Time.timeScale 이 서로 영향을 주지 않는 사실을 몰라서
방법 1을 사용하였다.
void Update()
{
if(Time.timeScale == 0f)
return;
Func1(); // 이 함수는 Time.timeScale = 0f 일 때 실행되지 않음
}
void FixedUpdate()
{
Func2(); // Time.timeScale = 0f 일 때 실행되지 않음
}
이동을 구현하는 Update 함수에서, Time.timeScale = 0f 일 때 멈추게하기 위한 방법이 존재한다.
Tile.cs 은 Update() 를 통해 맵 타일을 이동시켜준다.
매 프레임마다 호출되는 Update 에 Time.deltaTime 을 곱해주었다.
(Time.deltaTime : 전 프레임이 완료되기까지 걸린 시간)
// Time.cs
public class Tile : MonoBehaviour
{
private void Update()
{
transform.position -= Vector3.forward * TileManager.Instance.speed * Time.deltaTime;
}
}
Time.timeScale = 0f 로 설정되어,
현재 타이머가 진행되고 있지 않기 때문에 Time.deltaTime = 0f 이다.
따라서, Update() 안의 계산식은 0 을 곱해주고 있기 때문에
Time.timeScale 이 0 이 아닐때만 실행된다.
Button 을 클릭했을 때, Time.timeScale = 0f 로 만들거나, GameManager 의 isPaused = true 로 설정해주면 된다.
// UIManager.cs
public class UIManager : Monobehaviour
{
public GameObject pausedPanel;
void OnClickPaused()
{
// GameManager.instance.Pause();
Time.timeScale = 0f;
pausedPanel.SetActive(true);
}
void OnClickResume()
{
// GameManager.instance.Resume();
Time.timeScale = 1f;
pausedPanel.SetActive(false);
}
}
Update 와 FixedUpdate 처럼
코루틴도 함수에 따라 Time.timeScale 에 영향을 다르게 받는다.
yield return new WaitForSeconds(); 는 Time.timeScale 에 의존적이다.
즉, Time.timeScale = 0f 일 때, 진행중인 코루틴은 정지된다.
yield return new WaitForSecondsRealtime(); 는 Time.timeScale 에 독립적이다.
즉, Time.timeScale 의 값과 상관 없이, 코루틴은 진행된다.

여러 프로젝트를 진행하면서, 일시 정지 기능을 처음 담당하게 되어
쉽다고 생각한 기능에서 애를 먹었다.
흔하게 사용되어 쉽게 구현할 줄 알았던 일시 정지 기능을 구현하며
Time.timeScale 과 Update, FixedUpdate,
WaitForSeconds(), WaitForSecondsRealtime() 는 각각 어떻게 영향을 받는지 알게되었다.