게임 서버 가상 시계 구현 – C++11 기반

운영체제 부팅 이후 흐른 시간은 시스템 시계를 조작해도 변하지 않는다. 이 특성을 이용해 가상 시점을 기준으로 현재 시각을 계산하면, 플레이어가 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();   // 실제 시계로 복귀

태그: C++11 chrono steady_clock virtual_clock game_server

8월 23일 22:50에 게시됨