창(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.
BeginDrawing)과 상태 갱신(PhysicsStep)을 분리하면, InitWindow를 호출하지 않는 헤드리스(Headless) 배치 러너에서 10분 분량의 게임 플레이(36,000프레임)를 수 밀리초 만에 시뮬레이션하고 회귀 테스트(Regression Test)를 자동화할 수 있습니다.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.
#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;
}
#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) | 동일 초기 상태 + 동일 프레임별 입력 배열 | 물리 상수 변경 후에도 기존 스테이지의 클리어 궤적이 유지되는지 회귀 검증 |
| Verification Item / Technique | Formula / Condition | Engineering 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() Checks | isnan(s->x) || isinf(s->vx) | Catches division-by-zero bugs (e.g., normalizing a zero-length vector) before state corruption spreads |
| Deterministic Replay | Identical initial state + identical per-frame input array | Verifies 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()orIsKeyPressed()directly insidePhysicsStepcouples physics to the windowing system; always passdtand input flags as function parameters. - Maintain Floating-Point Determinism: Aggressive compiler flags like
-ffast-mathcan 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 insideSimStaterather than calling globalrand()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.