운영체제 부팅 이후 흐른 시간은 시스템 시계를 조작해도 변하지 않는다. 이 특성을 이용해 가상 시점을 기준으로 현재 시각을 계산하면, 플레이어가 OS 시계를 바꿔도 게임 내 시간이 흔들리지 않는다.
Windows 에서는 GetTickCount64(), Linux 에서는 clock_gettime(CLOCK_MONOTONIC)으로 부팅 후 경과 시간을 얻는다. C++11 부터는 std::chrono::steady_clock이 플랫폼 독립적으로 같은 값을 제공하므로 이를 사용한다.
필요한 기능은 다음과 같다.
- 임의 시점을 "게임 기준 시각"으로 설정
- 설정한 시각을 기준으로 현재 시각(밀리초/초) 반환
- 하루 00:00:00 기준 시각 계산
- 프레임 단위 Δt(delta time) 제공
- 가상 시계를 해제하면 다시 실제 시계를 따르게 함
전역 네임스페이스 gtime 아래 정적 함수만 제공하는 식으로 설계하면 싱글톤이나 인스턴스 없이 gtime::now() 형태로 호출할 수 있다.
// gtime.hpp
#pragma once
#include <cstdint>
namespace gtime
{
// 프레임 단위 경과 시간 (밀리초)
extern std::int64_t delta_ms;
// 현재 게임 시각 (밀리초/초)
std::int64_t now_ms();
std::int64_t now_sec();
// 실제 OS 시계 (밀리초/초)
std::int64_t sys_now_ms();
std::int64_t sys_now_sec();
// 오늘 00:00:00 시각 (초)
std::int32_t midnight_sec();
// 매 프레임 호출
void tick();
// 가상 시계 설정
void set(std::int64_t epoch_sec); // Unix epoch
void reset(); // 실제 시계로 복귀
}
// gtime.cpp
#include "gtime.hpp"
#include <chrono>
namespace gtime
{
std::int64_t delta_ms = 0;
namespace
{
// 부팅 후 흐른 시간(밀리초)
std::int64_t uptime_ms()
{
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// 실제 시계(밀리초)
std::int64_t wall_ms()
{
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
// 사용자가 추가한 오프셋(밀리초)
std::int64_t offset = wall_ms() - uptime_ms();
// 이전 tick 의 uptime
std::int64_t last_uptime = uptime_ms();
}
std::int64_t now_ms()
{
return uptime_ms() + offset;
}
std::int64_t now_sec()
{
return now_ms() / 1000;
}
std::int64_t sys_now_ms()
{
return wall_ms();
}
std::int64_t sys_now_sec()
{
return wall_ms() / 1000;
}
std::int32_t midnight_sec()
{
std::int64_t s = now_sec();
return static_cast<std::int32_t>(s - (s % 86400));
}
void tick()
{
std::int64_t current = uptime_ms();
delta_ms = current - last_uptime;
if (delta_ms < 0) delta_ms = 0; // 시계 뒤로 감기 방지
last_uptime = current;
}
void set(std::int64_t epoch_sec)
{
offset = epoch_sec * 1000 - uptime_ms();
}
void reset()
{
offset = wall_ms() - uptime_ms();
}
}
문자열 ↔ Unix epoch 변환 헬퍼는 C++20 std::format 대신 기존 C 라이브러리를 사용해 작성한다.
// datetime.hpp
#pragma once
#include <string>
#include <ctime>
inline std::string to_string(std::time_t t)
{
char buf[32]{};
std::strftime(buf, sizeof(buf), "%F %T", std::localtime(&t));
return buf;
}
inline std::time_t from_string(const std::string& s)
{
std::tm tm{};
sscanf(s.c_str(), "%d-%d-%d %d:%d:%d",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
tm.tm_year -= 1900;
tm.tm_mon -= 1;
tm.tm_isdst = -1;
return std::mktime(&tm);
}
사용 예시:
gtime::set(from_string("2023-08-30 15:00:00"));
// ...
std::cout << "게임 시각: " << to_string(gtime::now_sec()) << '\n';
gtime::tick();
std::cout << "이번 프레임 Δt: " << gtime::delta_ms << "ms\n";
gtime::reset(); // 실제 시계로 복귀