TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > C / raylib > 창(Window) 없이 물리 루프를 고속 검증하는 C 언어 헤드리스(Headless) 시뮬레이터Headless C Physics Simulator for High-Speed Batch Verification
C / raylib Advanced C99 / raylib

창(Window) 없이 물리 루프를 고속 검증하는 C 언어 헤드리스(Headless) 시뮬레이터Headless C Physics Simulator for High-Speed Batch Verification

GPU 컨텍스트나 윈도우 창을 생성하지 않고 순수 C 언어 물리 업데이트 루프만 수만 프레임 실행하여, 레벨 클리어 가능 여부와 수치 적분(dt) 오차를 자동 검증하는 배치 러너 설계입니다.Designing a headless C batch runner that executes tens of thousands of physics frames without opening a window or GPU context to verify level solvability and numerical integration stability.

#C #Physics #Testing #Simulation #Deterministic
핵심 개념물리 기반 아케이드 게임에서 중력, 탄성 계수, 점프 초속도를 수정할 때마다 사람이 직접 창을 띄워 수십 개의 스테이지를 플레이하며 클리어 가능 여부를 확인하는 것은 비효율적입니다. 렌더링(BeginDrawing)과 상태 갱신(PhysicsStep)을 분리하면, InitWindow를 호출하지 않는 헤드리스(Headless) 배치 러너에서 10분 분량의 게임 플레이(36,000프레임)를 수 밀리초 만에 시뮬레이션하고 회귀 테스트(Regression Test)를 자동화할 수 있습니다.
Key ConceptManually launching a window and playing through dozens of stages every time gravity, restitution, or jump impulse values change is slow and error-prone. By decoupling rendering (BeginDrawing) from state updates (PhysicsStep), a headless batch runner without InitWindow can simulate 10 minutes of gameplay (36,000 frames) in a few milliseconds for automated regression testing.

1. 렌더링 의존성을 분리한 헤드리스 물리 검증기 C 구현1. Implementing a Render-Decoupled Headless Physics Verifier in C

게임 상태를 순수 구조체(SimState)로 격리하고 고정 시간 간격(FIXED_DT)과 입력 시퀀스 배열만을 인자로 받는 PhysicsStep 함수를 작성하면, CI/CD 파이프라인이나 터미널에서 즉시 물리 정합성을 검사할 수 있습니다.

Encapsulate the entire game state inside a plain struct (SimState) and write a PhysicsStep function that accepts only the fixed timestep (FIXED_DT) and input state. This allows physics verification to run directly in a terminal or CI/CD pipeline.

headless_verifier.c
#include <stdio.h>
#include <stdbool.h>
#include <math.h>

#define FIXED_DT (1.0f / 60.0f)
#define GRAVITY  980.0f

typedef struct {
    float x, y;
    float vx, vy;
    bool grounded;
    bool reached_goal;
} SimState;

typedef struct {
    int trigger_frame;
    float jump_impulse;
} InputEvent;

// 창(Window)이나 GPU 컨텍스트에 전혀 의존하지 않는 순수 물리 스텝
void PhysicsStep(SimState *s, float dt, bool jump_pressed, float goal_x) {
    if (jump_pressed && s->grounded) {
        s->vy = -420.0f;
        s->grounded = false;
    }
    // Semi-implicit Euler 적분 (속도 먼저 갱신 후 위치 갱신)
    s->vy += GRAVITY * dt;
    s->x  += s->vx * dt;
    s->y  += s->vy * dt;

    // 바닥 충돌 판정 (y = 0 기준)
    if (s->y >= 0.0f) {
        s->y = 0.0f;
        s->vy = 0.0f;
        s->grounded = true;
    }
    if (s->x >= goal_x) {
        s->reached_goal = true;
    }
}

int main(void) {
    SimState state = { .x = 0.0f, .y = 0.0f, .vx = 180.0f, .vy = 0.0f, .grounded = true, .reached_goal = false };
    const int max_frames = 3600; // 60초 분량을 대기 시간 없이 즉시 시뮬레이션

    for (int frame = 0; frame < max_frames && !state.reached_goal; ++frame) {
        bool jump = (frame == 30 || frame == 120);
        PhysicsStep(&state, FIXED_DT, jump, 500.0f);

        // NaN 또는 비정상 좌표 발산(터널링) 검증
        if (isnan(state.x) || isnan(state.y) || fabs(state.y) > 10000.0f) {
            fprintf(stderr, "[FAIL] Frame %d: 수치 발산 감지 (x=%.2f, y=%.2f)\n", frame, state.x, state.y);
            return 1;
        }
    }

    printf("[PASS] Goal Reached: %s (Final X=%.2f)\n", state.reached_goal ? "YES" : "NO", state.x);
    return state.reached_goal ? 0 : 1;
}
headless_verifier.c
#include <stdio.h>
#include <stdbool.h>
#include <math.h>

#define FIXED_DT (1.0f / 60.0f)
#define GRAVITY  980.0f

typedef struct {
    float x, y;
    float vx, vy;
    bool grounded;
    bool reached_goal;
} SimState;

typedef struct {
    int trigger_frame;
    float jump_impulse;
} InputEvent;

// Pure physics step with zero dependency on a Window or GPU context
void PhysicsStep(SimState *s, float dt, bool jump_pressed, float goal_x) {
    if (jump_pressed && s->grounded) {
        s->vy = -420.0f;
        s->grounded = false;
    }
    // Semi-implicit Euler integration (update velocity first, then position)
    s->vy += GRAVITY * dt;
    s->x  += s->vx * dt;
    s->y  += s->vy * dt;

    // Ground collision check (at y = 0)
    if (s->y >= 0.0f) {
        s->y = 0.0f;
        s->vy = 0.0f;
        s->grounded = true;
    }
    if (s->x >= goal_x) {
        s->reached_goal = true;
    }
}

int main(void) {
    SimState state = { .x = 0.0f, .y = 0.0f, .vx = 180.0f, .vy = 0.0f, .grounded = true, .reached_goal = false };
    const int max_frames = 3600; // Simulate 60 seconds of gameplay immediately without waiting

    for (int frame = 0; frame < max_frames && !state.reached_goal; ++frame) {
        bool jump = (frame == 30 || frame == 120);
        PhysicsStep(&state, FIXED_DT, jump, 500.0f);

        // Check for NaN or abnormal coordinate divergence (tunneling)
        if (isnan(state.x) || isnan(state.y) || fabs(state.y) > 10000.0f) {
            fprintf(stderr, "[FAIL] Frame %d: Numerical divergence detected (x=%.2f, y=%.2f)\n", frame, state.x, state.y);
            return 1;
        }
    }

    printf("[PASS] Goal Reached: %s (Final X=%.2f)\n", state.reached_goal ? "YES" : "NO", state.x);
    return state.reached_goal ? 0 : 1;
}

2. 수치 적분 방식 및 헤드리스 검증 체크리스트2. Numerical Integration Methods and Headless Verification Checklist

좌우로 밀어서 표 전체 내용을 확인하세요
검증 항목 / 기법수식 / 조건엔지니어링 목적
Semi-Implicit Euler 적분$v_{t+1} = v_t + a \cdot dt$, $x_{t+1} = x_t + v_{t+1} \cdot dt$Explicit Euler 대비 에너지 보존성이 높고 스프링·중력 시스템에서 발산을 억제
터널링(Tunneling) 감지$|v \cdot dt| > \text{WallThickness}$고속 이동체가 한 프레임 만에 얇은 벽 콜라이더를 뚫고 지나가는 현상 사전 검출
isnan() / isinf() 검사isnan(s->x) || isinf(s->vx)0으로 나누기(정규화 벡터 길이 0)로 인해 물리 상태 전체가 오염되는 버그 포착
결정론적 리플레이(Replay)동일 초기 상태 + 동일 프레임별 입력 배열물리 상수 변경 후에도 기존 스테이지의 클리어 궤적이 유지되는지 회귀 검증
Swipe horizontally to view the full table
Verification Item / TechniqueFormula / ConditionEngineering Purpose
Semi-Implicit Euler Integration$v_{t+1} = v_t + a \cdot dt$, $x_{t+1} = x_t + v_{t+1} \cdot dt$Conserves energy better than Explicit Euler and prevents divergence in gravity/spring systems
Tunneling Detection$|v \cdot dt| > \text{WallThickness}$Detects when high-velocity objects pass through thin wall colliders within a single frame
isnan() / isinf() Checksisnan(s->x) || isinf(s->vx)Catches division-by-zero bugs (e.g., normalizing a zero-length vector) before state corruption spreads
Deterministic ReplayIdentical initial state + identical per-frame input arrayVerifies that stage clear trajectories remain valid after tuning physics constants

3. 실무 적용 및 주의사항3. Practical Considerations and Caveats

  • 전역 함수 호출 격리: PhysicsStep 내부에서 GetFrameTime()이나 IsKeyPressed() 같은 엔진 입출력 함수를 직접 호출하면 헤드리스 분리가 불가능하므로, 반드시 dt와 입력 플래그를 매개변수로 주입받아야 합니다.
  • 부동소수점 결정론 유지: 컴파일러 최적화 플래그(-ffast-math)를 켜면 명령어 재배치로 인해 빌드 환경마다 미세한 부동소수점 오차가 누적될 수 있으므로, 엄격한 리플레이 일치가 필요할 때는 IEEE 754 표준 연산 순서를 유지합니다.
  • 난수 시드(Seed) 고정: 물리 시뮬레이션 중에 파편 방향 등 난수가 개입한다면 전역 rand() 대신 SimState 내부에 자체 LCG/XORShift 시드 상태를 포함시켜야 언제든 동일한 결과를 재현할 수 있습니다.
  • Isolate Global Engine Calls: Calling GetFrameTime() or IsKeyPressed() directly inside PhysicsStep couples physics to the windowing system; always pass dt and input flags as function parameters.
  • Maintain Floating-Point Determinism: Aggressive compiler flags like -ffast-math can reorder floating-point instructions across platforms; stick to standard IEEE 754 evaluation rules when deterministic replays are required.
  • Embed PRNG State in SimState: If physics logic uses random numbers, store a dedicated LCG or XORShift seed inside SimState rather than calling global rand() so runs remain reproducible.

물리 연산 계층을 그래픽 및 OS 창 관리 계층으로부터 완전히 분리하면, 수만 프레임의 경계 조건 테스트를 CI 환경에서 1초 미만에 수행하여 게임 플레이의 안정성을 확보할 수 있습니다.

Decoupling the physics simulation layer from graphics and OS window management allows tens of thousands of boundary-condition frames to be verified in under a second within automated CI pipelines.