시스템 개요 및 아키텍처
본 프로젝트는 STM32 마이크로컨트롤러 환경에서 SSD1306 OLED 디스플레이와 물리적 버튼을 사용하여 두 가지 클래식 아케이드 게임(티렉스 러너, 우주 운석 회피)을 구동하는 경량 게임 엔진을 구현하는 것을 목표로 합니다. 제한된 임베디드 리소스 내에서 효율적인 메모리 관리와 부드러운 프레임 렌더링을 위해 상태 머신(State Machine) 패턴과 더블 버퍼링 기법을 활용했습니다.
소프트웨어 레이어 구조
- 입력 계층 (Input Layer): GPIO 인터럽트 및 타이머 기반 폴링을 통한 버튼 스캔과 소프트웨어 디바운싱(Debouncing) 처리.
- 로직 계층 (Logic Layer): 게임 흐름을 제어하는 유한 상태 머신(FSM), 물리 법칙 시뮬레이션, 그리고 AABB(Axis-Aligned Bounding Box) 기반의 충돌 감지 알고리즘.
- 출력 계층 (Output Layer): I2C 통신을 이용한 OLED 프레임 버퍼 전송 및 하드웨어 타이머 PWM을 활용한 오디오 피드백 생성.
하드웨어 사양 및 핀 할당
시스템의 핵심 연산은 STM32F103C8T6가 담당하며, 시각적 출력은 128x64 해상도의 I2C OLED 모듈을 통해 이루어집니다.
| 주변 장치 | STM32 핀 | 기능 설명 |
|---|---|---|
| OLED SDA / SCL | PB7 / PB6 | I2C1 데이터 및 클록 라인 |
| Btn UP / JUMP | PA0 | 메뉴 상향 이동 및 캐릭터 점프 |
| Btn DOWN / SELECT | PA1 | 메뉴 하향 이동 및 게임 선택 |
| Btn LEFT / RIGHT | PA2 / PA3 | 우주선 좌우 이동 제어 |
| Piezo Buzzer | PA5 | 타이머 PWM을 이용한 효과음 출력 |
게임 1: 티렉스 러너 (T-Rex Runner) 로직
플레이러는 중력과 점프 임펄스를 기반으로 움직이는 캐릭터를 제어하여 무작위로 생성되는 장애물을 피해야 합니다. 게임 속도는 점수에 비례하여 점진적으로 증가합니다.
엔티티 구조체 및 물리 엔진
// trex_runner.h
#define GROUND_LEVEL 56
#define GRAVITY_ACCEL 1
#define JUMP_IMPULSE -11
typedef struct {
uint16_t posX, posY;
int16_t velocityY;
bool isAirborne;
uint8_t hitboxW, hitboxH;
} TRexEntity;
typedef struct {
uint16_t posX;
uint8_t hitboxW, hitboxH;
} ObstacleBlock;
// trex_runner.c
void InitializeTRex(TRexEntity* trex) {
trex->posX = 15;
trex->posY = GROUND_LEVEL - 10;
trex->velocityY = 0;
trex->isAirborne = false;
trex->hitboxW = 8;
trex->hitboxH = 10;
}
void UpdateTRexPhysics(TRexEntity* trex) {
trex->velocityY += GRAVITY_ACCEL;
trex->posY += trex->velocityY;
if (trex->posY >= GROUND_LEVEL - trex->hitboxH) {
trex->posY = GROUND_LEVEL - trex->hitboxH;
trex->velocityY = 0;
trex->isAirborne = false;
}
}
void TriggerJump(TRexEntity* trex) {
if (!trex->isAirborne) {
trex->velocityY = JUMP_IMPULSE;
trex->isAirborne = true;
}
}
bool CheckAABBCollision(TRexEntity* player, ObstacleBlock* block) {
return (player->posX < block->posX + block->hitboxW &&
player->posX + player->hitboxW > block->posX &&
player->posY < GROUND_LEVEL &&
player->posY + player->hitboxH > GROUND_LEVEL - block->hitboxH);
}
게임 2: 우주 운석 회피 (Space Dodge) 로직
화면 하단에서 우주선을 좌우로 조작하여 상단에서 낙하하는 운석들을 회피하는 게임입니다. 시간이 지남에 따라 운석의 생성 빈도와 낙하 속도가 증가합니다.
우주선 및 운석 관리
// space_dodge.h
#define SCREEN_W 128
#define SCREEN_H 64
#define MOVE_STEP 6
typedef struct {
uint16_t coordX, coordY;
uint8_t dimW, dimH;
} Spacecraft;
typedef struct {
uint16_t coordX, coordY;
uint8_t dimW, dimH;
uint8_t fallSpeed;
} Meteorite;
// space_dodge.c
void MoveSpacecraft(Spacecraft* craft, int8_t direction) {
int16_t nextX = craft->coordX + (direction * MOVE_STEP);
if (nextX >= 0 && nextX <= SCREEN_W - craft->dimW) {
craft->coordX = nextX;
}
}
void SpawnMeteorite(Meteorite* met) {
met->coordX = rand() % (SCREEN_W - 15);
met->coordY = 0;
met->dimW = (rand() % 8) + 5;
met->dimH = met->dimW;
met->fallSpeed = (rand() % 3) + 2;
}
void UpdateMeteorite(Meteorite* met) {
met->coordY += met->fallSpeed;
}
bool CheckSpaceCollision(Spacecraft* craft, Meteorite* met) {
return (craft->coordX < met->coordX + met->dimW &&
craft->coordX + craft->dimW > met->coordX &&
craft->coordY < met->coordY + met->dimH &&
craft->coordY + craft->dimH > met->coordY);
}
통합 게임 매니저 및 상태 머신
두 개의 독립적인 게임을 하나의 펌웨어에서 관리하기 위해 중앙 집중식 컨텍스트 구조체와 상태 머신을 구현했습니다.
// game_manager.h
typedef enum {
STATE_MAIN_MENU,
STATE_PLAY_TREX,
STATE_PLAY_SPACE,
STATE_GAMEOVER_SCREEN
} GameFlowState;
typedef struct {
GameFlowState currentState;
TRexEntity trex;
ObstacleBlock cactus;
Spacecraft ship;
Meteorite meteors[6];
uint8_t activeMeteors;
uint16_t score;
uint8_t scrollSpeed;
uint8_t selectedMenuIndex;
} GameContext;
// main.c
int main(void) {
HAL_Init();
SystemClock_Config();
SSD1306_Init();
GPIO_Input_Init();
GameContext ctx = {0};
ctx.currentState = STATE_MAIN_MENU;
while (1) {
HandleUserInput(&ctx);
ProcessGameLogic(&ctx);
DrawFrame(&ctx);
HAL_Delay(33); // 약 30 FPS 유지
}
}
void HandleUserInput(GameContext* ctx) {
if (ctx->currentState == STATE_MAIN_MENU) {
if (ReadButton(BTN_UP) || ReadButton(BTN_DOWN)) {
ctx->selectedMenuIndex = !ctx->selectedMenuIndex;
} else if (ReadButton(BTN_SELECT)) {
ctx->currentState = (ctx->selectedMenuIndex == 0) ? STATE_PLAY_TREX : STATE_PLAY_SPACE;
ResetGameVariables(ctx);
}
}
else if (ctx->currentState == STATE_PLAY_TREX) {
if (ReadButton(BTN_JUMP)) TriggerJump(&ctx->trex);
}
else if (ctx->currentState == STATE_PLAY_SPACE) {
if (ReadButton(BTN_LEFT)) MoveSpacecraft(&ctx->ship, -1);
if (ReadButton(BTN_RIGHT)) MoveSpacecraft(&ctx->ship, 1);
}
else if (ctx->currentState == STATE_GAMEOVER_SCREEN) {
if (ReadButton(BTN_SELECT)) ctx->currentState = STATE_MAIN_MENU;
}
}
OLED 렌더링 파이프라인
현재 상태에 따라 적절한 그래픽 요소를 프레임 버퍼에 드로잉한 후, 한 번에 I2C 버스로 전송하여 화면 찢김(Tearing) 현상을 방지합니다.
void DrawFrame(GameContext* ctx) {
SSD1306_ClearBuffer();
switch (ctx->currentState) {
case STATE_MAIN_MENU:
RenderMenuUI(ctx->selectedMenuIndex);
break;
case STATE_PLAY_TREX:
SSD1306_DrawLine(0, GROUND_LEVEL, 127, GROUND_LEVEL);
SSD1306_DrawRect(ctx->trex.posX, ctx->trex.posY, ctx->trex.hitboxW, ctx->trex.hitboxH);
SSD1306_DrawRect(ctx->cactus.posX, GROUND_LEVEL - ctx->cactus.hitboxH, ctx->cactus.hitboxW, ctx->cactus.hitboxH);
RenderScore(ctx->score);
break;
case STATE_PLAY_SPACE:
SSD1306_DrawTriangle(ctx->ship.coordX, ctx->ship.coordY + ctx->ship.dimH,
ctx->ship.coordX + ctx->ship.dimW / 2, ctx->ship.coordY,
ctx->ship.coordX + ctx->ship.dimW, ctx->ship.coordY + ctx->ship.dimH);
for (int i = 0; i < ctx->activeMeteors; i++) {
SSD1306_DrawCircle(ctx->meteors[i].coordX, ctx->meteors[i].coordY, ctx->meteors[i].dimW / 2);
}
RenderScore(ctx->score);
break;
case STATE_GAMEOVER_SCREEN:
RenderGameOverUI(ctx->score);
break;
}
SSD1306_UpdateScreen();
}
추가 시스템: 애니메이션, 오디오 및 난이도 조절
게임의 몰입감을 높이기 위해 프레임 기반의 단순 애니메이션과 PWM 주파수 조절을 통한 효과음, 그리고 점수 기반의 동적 난이도 조절 시스템을 통합했습니다.
// 난이도 동적 조절
void AdjustDifficulty(GameContext* ctx) {
if (ctx->currentState == STATE_PLAY_TREX) {
ctx->scrollSpeed = 3 + (ctx->score / 150);
} else if (ctx->currentState == STATE_PLAY_SPACE) {
// 점수가 오를수록 운석 생성 확률 증가 및 최대 동시 생성 수 확장
}
}
// PWM을 이용한 단순 효과음 재생
void PlayTone(uint16_t frequency, uint16_t durationMs) {
uint32_t periodUs = 1000000 / frequency;
uint32_t halfPeriod = periodUs / 2;
uint32_t cycles = (durationMs * 1000) / periodUs;
for (uint32_t i = 0; i < cycles; i++) {
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET);
DelayMicroseconds(halfPeriod);
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET);
DelayMicroseconds(halfPeriod);
}
}
개발 환경 및 프로젝트 구조
본 펌웨어는 ARM GCC 툴체인과 HAL(Hardware Abstraction Layer) 라이브러리를 기반으로 작성되었으며, 모듈화된 디렉토리 구조를 통해 유지보수성을 확보했습니다.
- IDE: STM32CubeIDE / VS Code (Cortex-Debug)
- 컴파일러: arm-none-eabi-gcc
- 주요 의존성: STM32F1xx HAL Driver, 커스텀 SSD1306 I2C 라이브러리
Project_Root/
├── Core/
│ ├── Inc/
│ │ ├── game_manager.h
│ │ ├── trex_runner.h
│ │ ├── space_dodge.h
│ │ └── ssd1306_oled.h
│ └── Src/
│ ├── main.c
│ ├── game_manager.c
│ ├── trex_runner.c
│ ├── space_dodge.c
│ └── ssd1306_oled.c
├── Drivers/
│ └── STM32F1xx_HAL_Driver/
└── Makefile