Skip to content

02_03.TheFirstProgram

jdeokkim edited this page May 2, 2022 · 3 revisions

첫 번째 프로그램

이번 글에서는 raylib로 게임을 만들 때 꼭 알아야 할 raylib 게임 프로그램의 구조에 대해 알아보도록 하자. 아래 코드는 raylib로 만들 수 있는 가장 기초적인 프로그램의 예시이다.

#include "raylib.h"

#define TARGET_FPS     60

#define SCREEN_WIDTH   800
#define SCREEN_HEIGHT  600

void UpdateGame(void);

int main(void) {
    InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "...");

    SetTargetFPS(TARGET_FPS);

    while (!WindowShouldClose()) 
        UpdateGame();
    
    CloseWindow();

    return 0;
}

void UpdateGame(void) {
    BeginDrawing();

    ClearBackground(BLACK);

    /* TODO: 게임을 업데이트하고 화면에 그리기 */

    EndDrawing();
}

와우! 소스 코드를 보면 알겠지만 지금까지 우리가 C 프로그래밍 과제를 하면서 한번도 본 적이 없었던 함수들만 있는 것을 확인할 수 있다. 하지만 쫄지 말자! 사실 저기 있는 함수들은 우리가 쉽고 편하게 게임을 개발할 수 있도록 raylib에서 제공하는 라이브러리 함수들이다. 예제 프로그램의 각 함수가 무슨 기능을 하는지는 잠시 후에 설명하도록 하겠다.


OpenGL의 이해

'라이브러리 소개' 페이지를 자세히 읽어봤으면 알겠지만, raylib은 OpenGL을 기반으로 한 게임 프로그래밍 라이브러리이다. 그렇기 때문에, raylib를 배우기 전에 최소한 OpenGL이 무엇이고 어떤 기능을 제공하는지를 알아야 raylib가 제공하는 함수에 대해 이해할 수 있다.

OpenGL 로고


여러분이 게임 프로그래밍이나 게임 엔진에 관심이 많다면 OpenGL이라는 단어를 한번쯤은 들어봤을 것이다. OpenGL은 크로노스 그룹 (Khronos Group)라는 비영리 산업체 협회에서 개발하고 관리하는, 그래픽과 이미지 출력을 위한 함수의 규격 (specification)이다. 이게 무슨 뜻이냐면... 크로노스 그룹이라는 협회에서 "자, 우리가 어떤 컴퓨터에서든 똑같이 그림을 그릴 수 있게 함수 표준을 만들었는데, 우리가 이게 무슨 기능을 하고 어떤 값을 반환하는 함수인지 다 알려줄 테니까 그래픽 카드 제조사 (NVIDIA, AMD, Intel, Apple 등) 너네들이 함수 구현은 알아서 해줘"라고 했다는 것과 같은 의미이다.

OpenGL은 그냥 함수의 이름, 매개 변수, 반환값, 그리고 함수가 수행하는 작업 등을 알려주기만 하는 함수 규격에 불과하기 때문에, 그래픽 카드 제조사마다 이 함수를 구현하는 코드는 달라질 수 있다. 이 함수 규격이 어떻게 생겼는지 궁금하다면 여기서 OpenGL v3.3의 모든 함수 규격을 확인해보자.

이제 OpenGL에 대해 어느 정도 이해했으니, raylib가 게임 창을 어떻게 생성하는지를 알아보자. 만약 OpenGL를 이용하여 게임 화면에 삼각형을 그리고 싶다고 하면, 우리는 OpenGL에게 "자, 우리가 이제 삼각형을 그릴 거니까 삼각형을 그릴 준비가 끝나면 현재 상태 (context)를 변경해줘"라고 요청을 해야 삼각형을 그릴 수 있다. 그렇기 때문에 우리가 게임 프로그램을 실행할 때 가장 먼저 해야할 일은 바로 OpenGL의 현재 상태를 초기화하고, 게임을 그리기 위한 게임 창을 만드는 것이다. 그런데, 이러한 작업은 운영 체제마다 모두 다르게 구현이 되어 있어서, 각 운영 체제마다 게임 창을 생성하고, OpenGL의 현재 상태를 초기화하고, 사용자로부터 마우스나 키보드 입력을 받는 것을 다 우리가 직접 해야 한다.

아니... 이거를 우리가 어떻게 다 하냐고 ㅋㅋㅋㅋㅋㅋㅋ


다행히도, 우리가 사용하는 운영 체제에 관계없이 저런 작업을 간편하게 해주는 GLUT, GLFW와 SDL 등의 라이브러리가 존재한다. 이 중에 raylib는 GLFW라는 라이브러리를 사용하여 우리의 게임 창을 만들어주고 마우스와 키보드 입력을 처리해준다. 이제 여러분은 예제 프로그램의 InitWindow()CloseWindow()를 이해할 준비가 되었다.

// 게임 창을 생성하고, OpenGL의 현재 상태를 초기화한다.
RLAPI void InitWindow(int width, int height, const char *title);

// 게임 창을 닫고, OpenGL과 관련된 메모리를 해제한다.
RLAPI void CloseWindow(void);

InitWindow()는 가로 길이가 width, 세로 길이 height이고 게임 창의 이름 title인 게임 창을 생성하는 함수이며, 우리가 raylib의 다른 함수를 호출하기 전에 제일 먼저 호출해야 하는 함수이다. CloseWindow()는 뭐... 딱 봐도 게임 창을 닫는 함수니까 우리가 main() 함수에서 return 0; 직전, 제일 마지막에 호출되는 함수임을 알 수 있다.


그 다음으로 살펴볼 함수는 SetTargetFPS()WindowShouldClose() 함수이다.

// 게임의 최대 FPS를 설정한다.
RLAPI void SetTargetFPS(int fps);

// 사용자가 지정한 게임 종료 키 또는 게임 창 닫기 버튼이 눌렸는지 확인한다.
RLAPI bool WindowShouldClose(void);

SetTargetFPS() 함수의 FPS (Frames Per Second)는 초당 프레임이라는 뜻으로, 게임 화면이 1초에 몇 번 그려지는지를 나타내는 값이다. 또한 WindowShouldClose()에서 '게임 종료 키'는 ESC 키가 기본값으로 설정이 되어 있어, 우리가 게임을 실행하고 ESC 키를 누르면 게임이 종료되는 것을 확인할 수 있다. 이것을 방지하기 위해서는 InitWindow() 함수 호출 이후에 아래 함수를 사용하여 게임 종료 키를 변경하면 된다.


// 게임 종료 키를 설정한다.
RLAPI void SetExitKey(int key);

SetExitKey() 함수의 매개 변수인 key는 사용자가 입력한 키보드의 키 코드를 나타내는 값이며, raylib.h 헤더 파일의 KeyboardKey라는 열거형에 정의되어 있다.

typedef enum {
    KEY_NULL            = 0,        // Key: NULL, used for no key pressed

    // Alphanumeric keys
    KEY_APOSTROPHE      = 39,       // Key: '
    KEY_COMMA           = 44,       // Key: ,
    KEY_MINUS           = 45,       // Key: -
    KEY_PERIOD          = 46,       // Key: .
    KEY_SLASH           = 47,       // Key: /
    KEY_ZERO            = 48,       // Key: 0
    KEY_ONE             = 49,       // Key: 1
    KEY_TWO             = 50,       // Key: 2
    KEY_THREE           = 51,       // Key: 3
    KEY_FOUR            = 52,       // Key: 4

    /* ... */

    // Android key buttons
    KEY_BACK            = 4,        // Key: Android back button
    KEY_MENU            = 82,       // Key: Android menu button
    KEY_VOLUME_UP       = 24,       // Key: Android volume up button
    KEY_VOLUME_DOWN     = 25        // Key: Android volume down button
} KeyboardKey;

프레임버퍼와 더블 버퍼링

그 다음으로 살펴볼 함수는 BeginDrawing()EndDrawing() 함수인데, 이 함수들은 게임 화면을 그릴 때 반드시 필요한 함수들이므로 꼭 기억해두자.

// 게임 화면을 그리기 위한 캔버스를 준비한다.
RLAPI void BeginDrawing(void);

// 준비가 완료된 캔버스를 게임 화면에 출력한다.
RLAPI void EndDrawing(void);

BeginDrawing()EndDrawing() 함수의 작동 방식을 이해하려면, 게임 화면이 어떻게 그려지는지를 알아야 한다. 우리가 게임 화면을 그리는 방법에는 두 가지가 있는데, 첫 번째 방법은 현재 출력되고 있는 게임 화면을 바로 수정해서 새로운 게임 화면을 그리는 방법으로, 단일 버퍼링 기법 (single-buffering)이라고 하며, 캔버스를 따로 만들고 거기에 다음으로 출력할 게임 화면을 그린 다음, 현재 게임 화면을 미리 만들어놓은 게임 화면으로 교체하는 방법이 있는데, 이 방법을 이중 버퍼링 기법 (double-buffering)이라고 한다.

단일 버퍼링 기법의 단점은 바로 현재 출력되고 있는 게임 화면을 수정하기 때문에 이 과정에서 게임 화면이 깜박이는 현상 (flickering)이 생길 수 있다는 것이다. 그렇기 때문에 거의 모든 게임에서는 더블 버퍼링 기법 등의 방법을 사용하여 게임 화면을 그린다.

따라서 BeginDrawing()은 게임의 각 프레임 사이의 경과 시간을 계산하고, 다음에 출력할 게임 화면을 미리 준비하는 함수이며, EndDrawing()은 현재 출력되고 있는 게임 화면을 우리가 미리 만들어놓은 게임 화면으로 교체하는 함수라고 생각하면 된다.


// 게임 화면을 주어진 색상으로 채운다.
RLAPI void ClearBackground(Color color);

ClearBackground() 함수는 뭐 그냥... 게임 화면의 캔버스를 주어진 색상으로 채우는 함수이다. ClearBackground() 함수의 매개 변수인 color에는 우리가 직접 선택한 색상이 들어갈 수도 있고, 아니면 raylib.h에서 정의된 매크로 변수를 사용할 수도 있다.

// Some Basic Colors
// NOTE: Custom raylib color palette for amazing visuals on WHITE background
#define LIGHTGRAY  CLITERAL(Color){ 200, 200, 200, 255 }   // Light Gray
#define GRAY       CLITERAL(Color){ 130, 130, 130, 255 }   // Gray
#define DARKGRAY   CLITERAL(Color){ 80, 80, 80, 255 }      // Dark Gray
#define YELLOW     CLITERAL(Color){ 253, 249, 0, 255 }     // Yellow
#define GOLD       CLITERAL(Color){ 255, 203, 0, 255 }     // Gold
#define ORANGE     CLITERAL(Color){ 255, 161, 0, 255 }     // Orange
#define PINK       CLITERAL(Color){ 255, 109, 194, 255 }   // Pink
#define RED        CLITERAL(Color){ 230, 41, 55, 255 }     // Red
#define MAROON     CLITERAL(Color){ 190, 33, 55, 255 }     // Maroon
#define GREEN      CLITERAL(Color){ 0, 228, 48, 255 }      // Green
#define LIME       CLITERAL(Color){ 0, 158, 47, 255 }      // Lime
#define DARKGREEN  CLITERAL(Color){ 0, 117, 44, 255 }      // Dark Green
#define SKYBLUE    CLITERAL(Color){ 102, 191, 255, 255 }   // Sky Blue
#define BLUE       CLITERAL(Color){ 0, 121, 241, 255 }     // Blue
#define DARKBLUE   CLITERAL(Color){ 0, 82, 172, 255 }      // Dark Blue
#define PURPLE     CLITERAL(Color){ 200, 122, 255, 255 }   // Purple
#define VIOLET     CLITERAL(Color){ 135, 60, 190, 255 }    // Violet
#define DARKPURPLE CLITERAL(Color){ 112, 31, 126, 255 }    // Dark Purple
#define BEIGE      CLITERAL(Color){ 211, 176, 131, 255 }   // Beige
#define BROWN      CLITERAL(Color){ 127, 106, 79, 255 }    // Brown
#define DARKBROWN  CLITERAL(Color){ 76, 63, 47, 255 }      // Dark Brown

#define WHITE      CLITERAL(Color){ 255, 255, 255, 255 }   // White
#define BLACK      CLITERAL(Color){ 0, 0, 0, 255 }         // Black
#define BLANK      CLITERAL(Color){ 0, 0, 0, 0 }           // Blank (Transparent)
#define MAGENTA    CLITERAL(Color){ 255, 0, 255, 255 }     // Magenta
#define RAYWHITE   CLITERAL(Color){ 245, 245, 245, 255 }   // My own White (raylib logo)
Clone this wiki locally