__________

Designing the Future with Circuits

반도체 회로설계 취준기

자습시간/Verilog

Pmod OLED 공룡 게임 만들기(3) - 최종

semicon_designer 2025. 2. 20. 16:41
728x90

 

크롬 공룡 게임 구현

 

저번 게시글에 이어 마지막으로 Pmod KYPD와 Pmod OLED를 이용해 하드웨어 파일을 생성한 후, Vitis를 이용해 키패드로 동작하는 공룡 게임을 최종 완료했습니다.

 


Vivado Process


 

Vivado와 Vitis를 이용한 Pmod OLED 제어 [Pmod IP 이용]

이번 게시글에서는 Digilent의 게시물과 IP 라이브러리를 통해 Pmod OLED의 데모 파일 구현을 진행합니다. 추후 이를 응용하여 Pmod OLED를 통해 다양한 기능을 구현할 예정입니다. Vivado, Vitis 이용방법

semicon-circuit.tistory.com

 

이 게시글에서와 동일한 방법으로 Vivado Library에서 IP를 추가한 뒤, Pmod KYPD와 Pmod OLED를 추가하여 HDL Wrapper를 생성하고 하드웨어 파일을 추출하여 Vitis를 이용해 C언어로 게임을 제작했습니다.

전체 Block Design


함수 정의


초기화 및 메인 루프 관련 함수
  • main(): 게임을 실행하는 메인 함수로, 시스템 초기화 - 게임 실행 - 게임 종료 순으로 진행
  • DemoInitialize(): 캐시를 활성화하고 OLED 및 키패드 초기화
  • DemoCleanup(): 게임 종료 시 OLED를 정리하고 캐시를 비활성화
  • EnableCaches(), DisableCaches(): 캐시를 활성화하거나 비활성화하는 함수로, 성능 향상을 위해 사용됨.
게임 실행 및 장애물 관련 함수
  • DinoGAME_Run(): 게임의 메인 루프 실행
    • 무한루프 내에서 공룡의 움직임, 장애물 생성, 충돌 감지, 점수 증가 등 처리
    • initialize_obstacles()로 장애물 초기화 후 generate_obstacle()로 주기적으로 새로운 장애물 생성
    • 공룡의 점프 및 충돌을 처리하고 게임오버 시 화면 갱신
  • initialize_obstacles(): 장애물 배열을 초기화하여 게임 시작 시 모든 장애물 비활성화
  • generate_obstacle(int frame) 
    • 랜덤 장애물을 생헝하는 함수로, get_random_obstacle()을 호출하여 장애물 종류를 결정하고, 익룡인 경우 y좌표를 높게 설정
    • 최대 2개의 장애물만 한 화면에 존재할 수 있도록 제한
  • get_random_obstacle(int* width, int* height, int* is_petro)
    • 난수를 기반으로 장애물을 선택하는 함수
    • 장애물의 폭(width), 높이(height), 익룡 여부(is_petro)를 설정하여 반환
  • get_random_offset()
    • XORSHIFT 난수 생성 방식을 사용하여 장애물 간의 간격을 랜덤하게 설정
    • 반환값은 0~40 사이의 값으로, 장애물들이 일정하지 않은 간격으로 등장하도록 조정

작동 로직


 

Pmod_KYPD main.c

더보기
/******************************************************************************/
/*                                                                            */
/* PmodKYPD.c -- Demo for the use of the Pmod Keypad IP core                  */
/*                                                                            */
/******************************************************************************/
/* Author:   Mikel Skreen                                                     */
/* Copyright 2016, Digilent Inc.                                              */
/******************************************************************************/
/* File Description:                                                          */
/*                                                                            */
/* This demo continuously captures keypad data and prints a message to an     */
/* attached serial terminal whenever a positive edge is detected on any of    */
/* the sixteen keys. In order to receive messages, a serial terminal          */
/* application on your PC should be connected to the appropriate COM port for */
/* the micro-USB cable connection to your board's USBUART port. The terminal  */
/* should be configured with 8-bit data, no parity bit, 1 stop bit, and the   */
/* the appropriate Baud rate for your application. If you are using a Zynq    */
/* board, use a baud rate of 115200, if you are using a MicroBlaze system,    */
/* use the Baud rate specified in the AXI UARTLITE IP, typically 115200 or    */
/* 9600 Baud.                                                                 */
/*                                                                            */
/******************************************************************************/
/* Revision History:                                                          */
/*                                                                            */
/*    06/08/2016(MikelS):   Created                                           */
/*    08/17/2017(artvvb):   Validated for Vivado 2015.4                       */
/*    08/30/2017(artvvb):   Validated for Vivado 2016.4                       */
/*                          Added Multiple keypress error detection           */
/*    01/27/2018(atangzwj): Validated for Vivado 2017.4                       */
/*                                                                            */
/******************************************************************************/

#include "PmodKYPD.h"
#include "sleep.h"
#include "xil_cache.h"
#include "xparameters.h"

void DemoInitialize();
void DemoRun();
void DemoCleanup();
void DisableCaches();
void EnableCaches();
void DemoSleep(u32 millis);

PmodKYPD MYKYPD;

int main(void) {
   DemoInitialize();
   DemoRun();
   DemoCleanup();
   return 0;
}

// keytable is determined as follows (indices shown in Keypad position below)
// 12 13 14 15
// 8  9  10 11
// 4  5  6  7
// 0  1  2  3
#define DEFAULT_KEYTABLE "0FED789C456B123A"

void DemoInitialize() {
   EnableCaches();
   KYPD_begin(&MYKYPD, XPAR_PMODKYPD_0_AXI_LITE_GPIO_BASEADDR);
   KYPD_loadKeyTable(&MYKYPD, (u8*) DEFAULT_KEYTABLE);
}

void DemoRun() {
   u16 keystate;
   XStatus status, last_status = KYPD_NO_KEY;
   u8 key, last_key = 'x';
   // Initial value of last_key cannot be contained in loaded KEYTABLE string

   Xil_Out32(MYKYPD.GPIO_addr, 0xF);

   xil_printf("Pmod KYPD demo started. Press any key on the Keypad.\r\n");
   while (1) {
      // Capture state of each key
      keystate = KYPD_getKeyStates(&MYKYPD);

      // Determine which single key is pressed, if any
      status = KYPD_getKeyPressed(&MYKYPD, keystate, &key);

      // Print key detect if a new key is pressed or if status has changed
      if (status == KYPD_SINGLE_KEY
            && (status != last_status || key != last_key)) {
         xil_printf("Key Pressed: %c\r\n", (char) key);
         last_key = key;
      } else if (status == KYPD_MULTI_KEY && status != last_status)
         xil_printf("Error: Multiple keys pressed\r\n");

      last_status = status;

      usleep(1000);
   }
}

void DemoCleanup() {
   DisableCaches();
}

void EnableCaches() {
#ifdef __MICROBLAZE__
#ifdef XPAR_MICROBLAZE_USE_ICACHE
   Xil_ICacheEnable();
#endif
#ifdef XPAR_MICROBLAZE_USE_DCACHE
   Xil_DCacheEnable();
#endif
#endif
}

void DisableCaches() {
#ifdef __MICROBLAZE__
#ifdef XPAR_MICROBLAZE_USE_DCACHE
   Xil_DCacheDisable();
#endif
#ifdef XPAR_MICROBLAZE_USE_ICACHE
   Xil_ICacheDisable();
#endif
#endif
}

이 코드의 DemoRun함수 내에서 키가 한 번 눌렸을 때 last_status와 status가 달라진 순간을 비교하고, 키가 한 개만 눌렸을 때 눌린 키를 TeraTerm을 통해 출력하는 아래 코드를 이용하여 0이 눌렸을 때 점프하는 변수를 1로 변경하도록 설정했습니다.

// 점프 처리 (0번 키)
            if (!is_jumping && status == KYPD_SINGLE_KEY && score_str[0] == '0') {
                is_jumping = 1;
                jump_height = 0;
            }

 

 

게임 실행 과정

 

1. 게임 루프 시작프레임(frame)과 점수(score)를 0으로 초기화

  • 게임 속도(delay_time)는 10,000μs(10ms)에서 점점 감소
더보기
while (1) {  // 게임 전체를 감싸는 무한 루프 → 게임이 끝나면 처음부터 다시 시작
    int frame = 0;
    int dino_y = 32-DINO_WIDTH, dino_x = 15;
    int is_jumping = 0, jump_height = 0;
    int delay_time = 10000, min_delay = 1500, speed_increment_interval = 250;
    int anim_speed = 4, score = 0;
    char score_str[5];
    int dino_foot;
    int gameover;

    initialize_obstacles(); // 장애물 초기화

    while (1) {  // 게임 진행 루프
        gameover = 0;
        OLED_ClearBuffer(&MYOLED);
        dino_foot = dino_y + DINO_HEIGHT;

2. 공룡 점프 처리

  • is_jumping이 1일 때 jump_table[]을 사용해 공룡이 포물선을 따라 점프
  • 게임 속도가 증가하면 jump_table 길이를 늘려 점프 속도는 일정하게 유지
더보기
// 점프 처리 (0번 키)
if (!is_jumping && status == KYPD_SINGLE_KEY && score_str[0] == '0') {
    is_jumping = 1;
    jump_height = 0;
}

// 점프 로직 (속도에 따라 동적으로 조정)
if (is_jumping) {
    int base_jump_table[] = { -3, -3, -3, -3, -3, -3, -3,-3,-2, -2, -2, -2, -2, -2, -1, -1, -1, -1,
                               1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3};
    int base_jump_frames = sizeof(base_jump_table) / sizeof(base_jump_table[0]);

    int speed_factor = 10000 / delay_time;
    int jump_frames = base_jump_frames*(1 + (speed_factor / 4.0));

    int extended_jump_table[jump_frames];
    for (int i = 0; i < jump_frames; i++) {
        extended_jump_table[i] = base_jump_table[i * base_jump_frames / jump_frames];
    }

    if (jump_height < jump_frames) {
        dino_y += extended_jump_table[jump_height];
        jump_height++;
    } else {
        is_jumping = 0;
        jump_height = 0;
        dino_y = 32-DINO_WIDTH;
    }
}

 

3. 장애물 이동 및 랜덤 생성

  • generate_obstacle()을 통해 일정 확률로 장애물을 생성
  • OLED_PutBmp()에서 frame % 20 < 10을 이용해 petro1과 petro2를 번갈아 표시하는 방식이므로, 애니메이션 주기가 20프레임마다 바뀜
  • 장애물은 항상 1px씩 왼쪽으로 이동
더보기
// 장애물 이동 및 그리기
for (int i = 0; i < MAX_OBSTACLES; i++) {
    if (obstacles[i].active) {
        obstacles[i].x -= 1;

        if (obstacles[i].x >= 0) {
            OLED_MoveTo(&MYOLED, obstacles[i].x, obstacles[i].y);

            // 익룡이면 애니메이션 적용
            if (obstacles[i].is_petro) {
                const uint8_t* petro_image = (frame % 20 < 10) ? petro1 : petro2;
                OLED_PutBmp(&MYOLED, obstacles[i].width, obstacles[i].height, petro_image);
            } else {
                OLED_PutBmp(&MYOLED, obstacles[i].width, obstacles[i].height, obstacles[i].image);
            }
        }

 

4. 장애물, 익룡 추가

  • 장애물이 생성될 때 is_petro 값이 1이면 익룡 등장
  • 익룡은 지면이 아닌 화면 상단(0px) 에서 등장하며, 날개짓 애니메이션 적용(3번에서 적용)
더보기
const uint8_t* get_random_obstacle(int* width, int* height, int* is_petro) {
    static uint32_t seed = 0xBEEF;
    seed ^= seed << 11;
    seed ^= seed >> 7;
    seed ^= seed << 3;

    int obstacle_type = (seed % 4); // 0, 1, 2, 3 중 하나 선택

    *is_petro = (obstacle_type == 3) ? 1 : 0; // 익룡일 경우 1, 아니면 0

    if (obstacle_type == 0) {
        *width = CACTUS_WIDTH;
        *height = CACTUS_HEIGHT;
        return cactus;
    } else if (obstacle_type == 1) {
        *width = CACTUS2_WIDTH;
        *height = CACTUS2_HEIGHT;
        return cactus2;
    } else if (obstacle_type == 2) {
        *width = ROCK_WIDTH;
        *height = ROCK_HEIGHT;
        return rock;
    } else { // 익룡 (애니메이션은 그릴 때 적용)
        *width = PETRO_WIDTH;
        *height = PETRO_HEIGHT;
        return petro1; // 기본값으로 첫 번째 이미지 리턴
    }
}

 

5. 충돌 감지 및 게임오버 처리

  • 일반 장애물
    • 공룡이 점프하지 않았을 때 장애물과 부딪히면 게임오버
    • 점프 후 착지할 때 발이 장애물 위에 닿으면 게임오버
  • 익룡(PETRO)
    • 점프 중 공룡의 머리나 발이 익룡과 닿으면 게임오버
더보기
if ((obstacles[i].x >= dino_x && obstacles[i].x <= (dino_x + DINO_WIDTH - 2))) {
    if (obstacles[i].is_petro) {
        // 익룡 충돌 감지 (머리가 닿거나 앞으로 충돌하면 게임오버)
        if (is_jumping) {
            gameover = 1;
        }
    } else {
        // 기존 장애물(선인장, 바위 등) 충돌 감지
        if ((obstacles[i].x <= (dino_x + DINO_WIDTH) && !is_jumping) ||  // 앞으로 부딪힐 때
            (is_jumping &&
             (dino_foot >= (32 - obstacles[i].height)) &&  // 발이 장애물보다 아래 있을 때
             (obstacles[i].x - dino_x >= 0 && obstacles[i].x - dino_x <= DINO_WIDTH))) {
            gameover = 1;
        }
    }
}

 

6. 게임 속도 증가 및 점수 갱신

  • 250프레임마다 delay_time을 감소하여 점점 빨라짐
  • 30프레임마다 점수 증가 (score++)
더보기
// 속도 증가 로직
if (frame % speed_increment_interval == 0 && delay_time > min_delay) {
    delay_time = (int)(delay_time * 0.9);
}

// 점수 증가
if (frame % 30 == 0 && score < 9999) {
    score += 1;
}

// 점수 표시
sprintf(score_str, "%04d", score);
OLED_MoveTo(&MYOLED, 90, 0);
OLED_DrawString(&MYOLED, score_str);

 

7. 게임오버 후 재시작"GAME OVER" 3번 깜빡임

  • "PRESS KEY TO RETRY, C TO EXIT" 메시지를 좌우 스크롤
  • 키 입력을 받아 게임 재시작 또는 완전 종료
더보기
if (gameover) {
    OLED_ClearBuffer(&MYOLED);

    // GAME OVER 3번 깜빡임
    for (int j = 0; j < 3; j++) {
        OLED_MoveTo(&MYOLED, 35, 10);
        OLED_DrawString(&MYOLED, "GAME OVER");
        OLED_Update(&MYOLED);
        usleep(500000);
        OLED_ClearBuffer(&MYOLED);
        OLED_Update(&MYOLED);
        usleep(500000);
    }

    // 총 점수 표시
    sprintf(score_str, "%04d", score);
    OLED_MoveTo(&MYOLED, 20, 0);
    OLED_DrawString(&MYOLED, "Total Score:");
    OLED_MoveTo(&MYOLED, 50, 10);
    OLED_DrawString(&MYOLED, score_str);
    OLED_Update(&MYOLED);

    // "PRESS ANY KEY TO RETRY" 스크롤 효과 추가
    char retry_msg[] = "              PRESS KEY TO RETRY, C TO EXIT              ";
    int msg_length = strlen(retry_msg);
    int scroll_pos = 0;

    while (1) {
        OLED_MoveTo(&MYOLED, 5, 22);
        for (int i = 0; i < 16; i++) {
            if (i + scroll_pos < msg_length) {
                OLED_DrawChar(&MYOLED, retry_msg[i + scroll_pos]);
            } else {
                OLED_DrawChar(&MYOLED, ' ');
            }
        }
        OLED_Update(&MYOLED);
        scroll_pos = (scroll_pos + 1) % (msg_length - 15);
        usleep(200000);

        // 키 입력 감지
        keystate = KYPD_getKeyStates(&MYKYPD);
        status = KYPD_getKeyPressed(&MYKYPD, keystate, (u8*)score_str);
        if (status == KYPD_SINGLE_KEY && score_str[0] != 'C') {
            xil_printf("Restarting...\n");
            gameover = 1;
            break;
        } else if (status == KYPD_SINGLE_KEY && score_str[0] == 'C') {
            DemoCleanup();
            return;
        }
    }

    if (gameover) break; // 내부 while 종료 → 새로운 게임 시작
}

 

최종 파일


main.c

더보기
/* ------------------------------------------------------------ */
/*                  Include File Definitions                    */
/* ------------------------------------------------------------ */

#include <stdio.h>
#include "PmodOLED.h"
#include "PmodKYPD.h"
#include "sleep.h"
#include "xil_cache.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "obstacles.h"
#include "xuartps.h"

/* ------------------------------------------------------------ */
/*                  Global Variables                            */
/* ------------------------------------------------------------ */

PmodOLED MYOLED;
PmodKYPD MYKYPD;

/* ------------------------------------------------------------ */
/*                  Forward Declarations                        */
/* ------------------------------------------------------------ */

void DemoInitialize();
void DemoRun();
void DemoCleanup();
void EnableCaches();
void DisableCaches();
void DemoSleep(u32 millis);
void DinoGAME_Run();
int get_random_offset();
const uint8_t* get_random_obstacle(int* width, int* height, int* is_petro);

// To change between PmodOLED and OnBoardOLED is to change Orientation
const u8 orientation = 0x0; // Set up for Normal PmodOLED(false) vs normal
                            // Onboard OLED(true)
const u8 invert = 0x0; // true = whitebackground/black letters
                       // false = black background /white letters
#define DEFAULT_KEYTABLE "0FED789C456B123A"

/* ------------------------------------------------------------ */
/*                  Function Definitions                        */
/* ------------------------------------------------------------ */

int main() {
   DemoInitialize();
   DinoGAME_Run();
   DemoCleanup();

   // Microblaze applications can automatically reboot on exit. Prevent
   // this by looping indefinitely, giving the user time to power off the board.
   while (1);

   return 0;
}

void DemoInitialize() {
   EnableCaches();
   OLED_Begin(&MYOLED, XPAR_PMODOLED_0_AXI_LITE_GPIO_BASEADDR,
         XPAR_PMODOLED_0_AXI_LITE_SPI_BASEADDR, orientation, invert);
   KYPD_begin(&MYKYPD, XPAR_PMODKYPD_0_AXI_LITE_GPIO_BASEADDR);
   KYPD_loadKeyTable(&MYKYPD, (u8*) DEFAULT_KEYTABLE);
}

// 장애물 리스트 (최대 2개의 장애물 존재 가능)
#define MAX_OBSTACLES 2

typedef struct {
    int x;
    int y;  // 장애물의 y 좌표 추가
    int width;
    int height;
    const uint8_t* image;
    int active;
    int is_petro;  // 익룡인지 여부 추가 (0: 일반 장애물, 1: 익룡)
} Obstacle;

Obstacle obstacles[MAX_OBSTACLES];

// 🛠 장애물 초기화 함수
void initialize_obstacles() {
    for (int i = 0; i < MAX_OBSTACLES; i++) {
        obstacles[i].active = 0; // 비활성화 상태로 시작
    }
}

void generate_obstacle(int frame) {
    for (int i = 0; i < MAX_OBSTACLES; i++) {
        if (!obstacles[i].active) { // 비활성화된 장애물이 있으면 새로 생성
            int is_petro; // 익룡 여부 저장
            obstacles[i].x = 128 + get_random_offset(); // 랜덤 간격 설정
            obstacles[i].image = get_random_obstacle(&obstacles[i].width, &obstacles[i].height, &is_petro);
            obstacles[i].active = 1;
            obstacles[i].is_petro = is_petro;  // 익룡 여부 저장

            if (is_petro) {
                obstacles[i].y = 0;  // 익룡이면 상단에서 등장
            } else {
                obstacles[i].y = 32 - obstacles[i].height;  // 기존 장애물은 하단
            }
            break; // 한 번에 하나만 생성
        }
    }
}
void DinoGAME_Run() {
    xil_printf("DinoGAME RUNNING\n");

    while (1) {  // 게임 전체를 감싸는 무한 루프 → 게임이 끝나면 처음부터 다시 시작

        // 게임 변수 초기화 (게임이 처음 시작할 때 한 번만 초기화됨)
        int frame = 0;
        int dino_y = 32-DINO_WIDTH, dino_x = 15;
        int is_jumping = 0, jump_height = 0;
        int delay_time = 10000, min_delay = 1500, speed_increment_interval = 250;
        int anim_speed = 4, score = 0;
        char score_str[5];
        int dino_foot;
        int gameover;

        initialize_obstacles(); // 장애물 초기화

        while (1) {  // 게임 진행 루프 (게임오버 시 탈출 후 새 게임 시작)
        	gameover = 0;
            OLED_ClearBuffer(&MYOLED);
            dino_foot = dino_y + DINO_HEIGHT;

            // 현재 키패드 상태 가져오기
            u16 keystate = KYPD_getKeyStates(&MYKYPD);
            XStatus status = KYPD_getKeyPressed(&MYKYPD, keystate, (u8*)score_str);

            // 점프 처리 (0번 키)
            if (!is_jumping && status == KYPD_SINGLE_KEY && score_str[0] == '0') {
                is_jumping = 1;
                jump_height = 0;
            }

            // 점프 로직 (속도에 따라 동적으로 조정)
            if (is_jumping) {
                int base_jump_table[] = { -3, -3, -3, -3, -3, -3, -3,-3,-2, -2, -2, -2, -2, -2, -1, -1, -1, -1,
                                           1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3};
                int base_jump_frames = sizeof(base_jump_table) / sizeof(base_jump_table[0]);

                // 게임 속도가 빨라질수록 점프 프레임 수도 증가 (최대 1.5배 증가)
                int speed_factor = 10000 / delay_time;  // 게임 속도가 빨라질수록 증가
                int jump_frames = base_jump_frames*(1 + (speed_factor / 4.0));

                // 확장된 점프 테이블 생성
                int extended_jump_table[jump_frames];
                for (int i = 0; i < jump_frames; i++) {
                    extended_jump_table[i] = base_jump_table[i * base_jump_frames / jump_frames];
                }

                // 점프 동작
                if (jump_height < jump_frames) {
                    dino_y += extended_jump_table[jump_height];
                    jump_height++;
                } else {
                    is_jumping = 0;
                    jump_height = 0;
                    dino_y = 32-DINO_WIDTH;  // 착지
                }
            }


            // 공룡 그리기 (화면 밖으로 나가는 경우 처리)
            int draw_y = dino_y, visible_height = DINO_HEIGHT;
            dino_foot = dino_y+visible_height;
            if (draw_y < 0) {
                visible_height = DINO_HEIGHT + draw_y;
                draw_y = 0;
            }

            OLED_MoveTo(&MYOLED, dino_x, draw_y);
            OLED_PutBmp(&MYOLED, DINO_WIDTH, visible_height, (frame / anim_speed) % 2 == 0 ? dinorun1 : dinorun2);

            // 장애물 이동 및 그리기
            for (int i = 0; i < MAX_OBSTACLES; i++) {
                if (obstacles[i].active) {
                    obstacles[i].x -= 1;

                    if (obstacles[i].x >= 0) {
                        OLED_MoveTo(&MYOLED, obstacles[i].x, obstacles[i].y);

                        // 익룡이면 애니메이션 적용 (frame 값을 이용해 교체)
                        if (obstacles[i].is_petro) {
                            const uint8_t* petro_image = (frame % 20 < 10) ? petro1 : petro2;
                            OLED_PutBmp(&MYOLED, obstacles[i].width, obstacles[i].height, petro_image);
                        } else {
                            OLED_PutBmp(&MYOLED, obstacles[i].width, obstacles[i].height, obstacles[i].image);
                        }
                    }
                    //GAMEOVER 로직
                    if ((obstacles[i].x >= dino_x && obstacles[i].x <= (dino_x + DINO_WIDTH - 2))) {
                        if (obstacles[i].is_petro) {
                            // 🦅 익룡 충돌 감지 (머리가 닿거나 앞으로 충돌하면 게임오버)
                            if (is_jumping) {
                                gameover = 1;
                            }
                        } else {
                            // 🌵 기존 장애물(선인장, 바위 등) 충돌 감지
                        	if ((obstacles[i].x <= (dino_x + DINO_WIDTH) && !is_jumping) ||  // 앞으로 부딪힐 때
                        	    (is_jumping &&
                        	     (dino_foot >= (32 - obstacles[i].height)) &&  // 발이 장애물보다 아래 있을 때
                        	     (obstacles[i].x - dino_x >= 0 && obstacles[i].x - dino_x <= DINO_WIDTH)  // 장애물 위에 있을 때만 적용
                        	    )) {
                                gameover = 1;
                            }
                        }
                    }
                    if(gameover){
                        OLED_ClearBuffer(&MYOLED);

                        // GAME OVER 3번 깜빡임
                        for (int j = 0; j < 3; j++) {
                            OLED_MoveTo(&MYOLED, 35, 10);
                            OLED_DrawString(&MYOLED, "GAME OVER");
                            OLED_Update(&MYOLED);
                            usleep(500000);
                            OLED_ClearBuffer(&MYOLED);
                            OLED_Update(&MYOLED);
                            usleep(500000);
                        }

                        // 총 점수 표시
                        sprintf(score_str, "%04d", score);
                        OLED_MoveTo(&MYOLED, 20, 0);
                        OLED_DrawString(&MYOLED, "Total Score:");
                        OLED_MoveTo(&MYOLED, 50, 10);
                        OLED_DrawString(&MYOLED, score_str);
                        OLED_Update(&MYOLED);

                        // "PRESS ANY KEY TO RETRY" 스크롤 효과 추가
                        char retry_msg[] = "              PRESS KEY TO RETRY, C TO EXIT              ";
                        int msg_length = strlen(retry_msg);
                        int scroll_pos = 0;

                        while (1) {
                            OLED_MoveTo(&MYOLED, 5, 22);
                            for (int i = 0; i < 16; i++) {
                                if (i + scroll_pos < msg_length) {
                                    OLED_DrawChar(&MYOLED, retry_msg[i + scroll_pos]);
                                } else {
                                    OLED_DrawChar(&MYOLED, ' ');
                                }
                            }
                            OLED_Update(&MYOLED);
                            scroll_pos = (scroll_pos + 1) % (msg_length - 15);
                            usleep(200000);

                            // 키 입력 감지
                            keystate = KYPD_getKeyStates(&MYKYPD);
                            status = KYPD_getKeyPressed(&MYKYPD, keystate, (u8*)score_str);
                            if (status == KYPD_SINGLE_KEY && score_str[0] != 'C') {
                                xil_printf("Restarting...\n");
                                gameover = 1;
                                break; // 게임 재시작을 위해 break
                            } else if (status == KYPD_SINGLE_KEY && score_str[0] == 'C') {
                                DemoCleanup();
                                return; // 게임 완전 종료
                            }
                        }

                        if(gameover) break; // 내부 while 종료 → 새로운 게임 시작
                    }
                    if(gameover) break; // 내부 while 종료 → 새로운 게임 시작
                    // 장애물이 왼쪽 끝을 지나면 비활성화
                    if (obstacles[i].x < -obstacles[i].width) {
                        obstacles[i].active = 0;
                    }
                }
            }
            if(gameover) break; // 내부 while 종료 → 새로운 게임 시작

            // 랜덤 장애물 생성 (이전 장애물과 독립적으로)
            if ((frame % 100 == 0) && (rand() % 2 == 0)) { // 랜덤한 간격으로 생성
                generate_obstacle(frame);
            }

            // 속도 증가 로직
            if (frame % speed_increment_interval == 0 && delay_time > min_delay) {
                delay_time = (int)(delay_time * 0.9);
            }

            // 점수 증가
            if (frame % 30 == 0 && score < 9999) {
                score += 1;
            }

            // 점수 표시
            sprintf(score_str, "%04d", score);
            OLED_MoveTo(&MYOLED, 90, 0);
            OLED_DrawString(&MYOLED, score_str);

            OLED_Update(&MYOLED);
            usleep(delay_time);
            frame++;
        }
    }
}

// 난수를 직접 생성하는 함수 (XORSHIFT 사용)
int get_random_offset() {
    static uint32_t seed = 0xACE1;
    seed ^= seed << 13;
    seed ^= seed >> 17;
    seed ^= seed << 5;
    return (seed % 40); // 0~40 범위의 난수 반환 (장애물 간격 조절)
}

const uint8_t* get_random_obstacle(int* width, int* height, int* is_petro) {
    static uint32_t seed = 0xBEEF;
    seed ^= seed << 11;
    seed ^= seed >> 7;
    seed ^= seed << 3;

    int obstacle_type = (seed % 4); // 0, 1, 2, 3 중 하나 선택

    *is_petro = (obstacle_type == 3) ? 1 : 0; // 익룡일 경우 1, 아니면 0

    if (obstacle_type == 0) {
        *width = CACTUS_WIDTH;
        *height = CACTUS_HEIGHT;
        return cactus;
    } else if (obstacle_type == 1) {
        *width = CACTUS2_WIDTH;
        *height = CACTUS2_HEIGHT;
        return cactus2;
    } else if (obstacle_type == 2) {
        *width = ROCK_WIDTH;
        *height = ROCK_HEIGHT;
        return rock;
    } else { // 익룡 (애니메이션은 그릴 때 적용)
        *width = PETRO_WIDTH;
        *height = PETRO_HEIGHT;
        return petro1; // 기본값으로 첫 번째 이미지 리턴
    }
}

void DemoCleanup() {
   OLED_End(&MYOLED);
   DisableCaches();
}

void EnableCaches() {
#ifdef __MICROBLAZE__
#ifdef XPAR_MICROBLAZE_USE_ICACHE
   Xil_ICacheEnable();
#endif
#ifdef XPAR_MICROBLAZE_USE_DCACHE
   Xil_DCacheEnable();
#endif
#endif
}

void DisableCaches() {
#ifdef __MICROBLAZE__
#ifdef XPAR_MICROBLAZE_USE_DCACHE
   Xil_DCacheDisable();
#endif
#ifdef XPAR_MICROBLAZE_USE_ICACHE
   Xil_ICacheDisable();
#endif
#endif
}

obstacles.h

더보기
#define DINONCACTUS_HEIGHT 	32
#define DINONCACTUS_WIDTH 	128
#define DINO_HEIGHT 		20
#define DINO_WIDTH 			20
#define PETRO_HEIGHT 		12
#define PETRO_WIDTH 		22
#define CACTUS_HEIGHT 		16
#define CACTUS_WIDTH 		11
#define CACTUS2_HEIGHT 		14
#define CACTUS2_WIDTH 		16
#define ROCK_HEIGHT 		10
#define ROCK_WIDTH 			16

// 'cactus', 8x11px
const uint8_t cactus[] = {
		0xf0, 0xf8, 0x00, 0x00, 0xfe, 0xff, 0xfe, 0x00, 0x00, 0xe0, 0xf0, 0x00, 0x01, 0x03, 0x03, 0xff,
		0xff, 0xff, 0x0c, 0x0c, 0x0f, 0x07
};
// 'dinorun1', 20x20px
const uint8_t dinorun1[] = {
		0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xe0, 0xf0, 0xfe, 0xff, 0xdf,
		0x5f, 0x5f, 0x5f, 0x1e, 0x0f, 0x1f, 0x3f, 0x7f, 0x7e, 0xfe, 0xff, 0xff, 0x7f, 0x3f, 0x1f, 0xff,
		0xff, 0x1f, 0x0f, 0x06, 0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// 'dinorun2', 20x20px
const uint8_t dinorun2[] = {0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xe0, 0xf0, 0xfe, 0xff, 0xdf,
		0x5f, 0x5f, 0x5f, 0x1e, 0x0f, 0x1f, 0x3f, 0x7f, 0x7e, 0xfe, 0xff, 0x7f, 0x3f, 0x3f, 0x7f, 0xff,
		0xff, 0x7f, 0x0f, 0x06, 0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

const uint8_t petro1[] = {
		0x00, 0x00, 0x20, 0x30, 0x38, 0x3c, 0x70, 0xe3, 0xfe, 0xfe, 0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 0x40,
		0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
		0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
};

const uint8_t petro2[] = {
		0x00, 0x00, 0x20, 0x30, 0x38, 0x3c, 0x70, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xc0, 0x40,
		0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f,
		0x07, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
};

const uint8_t rock[] = {

		0x80, 0xe0, 0xf0, 0xf8, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xf8, 0xf8, 0xf0, 0xe0, 0xc0, 0x80, 0x00,
		0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00
};

const uint8_t cactus2[] = {
		0xf0, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xc0, 0xfc, 0x78, 0x00, 0xe0, 0xf0, 0x00, 0xff, 0xff,
		0x01, 0x01, 0x03, 0x03, 0x3f, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x06, 0x3f, 0x3f
};

 

728x90