일상/ssafy

스네이크 게임, Win32

래울 2024. 6. 28. 01:45

내용

기본적인 스네이크 게임 기능

게임시간에 비례해서 난이도 올라감

 

조작키

S: Start

P: Pause

방향키: 조작

 

실행결과

 

코드

#include <stdio.h>  
#include <iostream>
#include <vector>
#include <deque>
#include <Windows.h>  
#include "resource.h"

// 공간 가로 세로
#define WIDTH 20
#define HEIGHT 20

// 블록 크기 픽셀 크기
#define TILESIZE 24
#define RESOURCESIZE 4
#define LINF 999999999999999999

// debug flag
int DEBUGFLAG = 0;

// Window는 한번의 실행으로 끝나는 것이 아니라 윈도우가 종료될 때까지의 무한 루프의 메시지가 필요하기 때문에 callback 함수를 사용한다. 메시지 처리 함수.  
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;

// 포인트 구조체
struct Point {
    int x, y;
};
int dy[4] = { -1, 0, 1, 0 };
int dx[4] = { 0, 1, 0, -1 };
int snakeDir;
int turnFlag = 1;   // 회전이 한번에 여러번 일어나는 것을 배제
int score = 0;
time_t startTime = LINF;
// 보드판 크기
int board[WIDTH + 2][HEIGHT + 2];
std::deque<Point> snake;

enum tag_Status { GAMEOVER, RUNNING, PAUSE };
tag_Status GameStatus;

int Interval;
HBITMAP hBit[10];

void InitGame();
void TurnSnake(int _d);
int MoveSnake();
void AppearStar();
void DrawScreen(HDC hdc);
void PrintTile(HDC hdc, int x, int y, int c);
void DrawBitmap(HDC hdc, int x, int y, HBITMAP hBit);
int GetRunSenconds();

// 실행 함수  
int main() {
    // 프로그램 인스턴스 핸들 취득.  
    // Window 프로그램은 하나의 인스턴스 윈도우를 OS에 등록을 해야 한다.  
    HINSTANCE hInstance = GetModuleHandle(NULL);
    g_hInst = hInstance;

    // 윈도우 핸들
    HWND hWnd;
    // 윈도우 메시지
    MSG Message;
    // 윈도우를 정의하는 구조체
    WNDCLASS WndClass;
    // 윈도우의 초기 설정하는 값
    WndClass.cbClsExtra = 0;
    WndClass.cbWndExtra = 0;
    // 윈도우의 메인 바탕색  
    WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    // 윈도우의 기본 마우스 커서 형태  
    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    // 윈도우의 아이콘  
    WndClass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
    // 윈도우의 인스턴스  
    WndClass.hInstance = hInstance;
    // 윈도우의 메시지 callbak 함수  
    WndClass.lpfnWndProc = WndProc;
    // 윈도우를 인식하기 위한 클래스 명  
    WndClass.lpszClassName = L"Snake";
    // 윈도우 메뉴 이름  
    WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
    // 윈도우 스타일  
    WndClass.style = 0;
    // OS에 윈도우를 등록한다.  
    // 윈도우라는 것은 결국 Window OS에서 실행하는 것인데 다른 프로그램들과 동시에 관리하기 위해서는 OS가 Window의 정보를 알고 있어야 한다.  
    // 그를 위해 등록한다.  
    RegisterClass(&WndClass);
    // 윈도우 생성
    hWnd = CreateWindow(WndClass.lpszClassName, WndClass.lpszClassName, WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, NULL, (HMENU)NULL, hInstance, NULL);
    // 이제 윈도우를 띄운다.  
    ShowWindow(hWnd, SW_SHOWDEFAULT);

    HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
    // 메시지 루프 생성
    while (GetMessage(&Message, NULL, 0, 0)) {
        if (!TranslateAccelerator(hWnd, hAccel, &Message)) {
            // 키보드 메시지를 받고 프로그램에서 사용할 수 있게 변환한다.  
            TranslateMessage(&Message);
            // 메시지를 WndProc로 보내는 함수.  
            DispatchMessage(&Message);
        }
    }
    return 0;
}


// 메시지 처리 함수, 메시지를 처리하는 함수로 메시지란 유저의 입력 또는 OS에서 요구하는 메시지 등을 처리하는 함수  
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) {
    int i;
    RECT crt;
    int ret;
    HDC hdc;
    PAINTSTRUCT ps;

    // 윈도우를 생성할 때의 구조, main에서 CreateWindowEx로 넣었던 파라미터 값이 있다.  
    // 다른 윈도우를 생성하려면 인스턴스가 필요한데 여기서 취득한다.  
    CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
    switch (iMessage) {
        // 최초 윈도우가 생성되면 호출된다.  
    case WM_CREATE:
        hWndMain = hWnd;
        // 윈도우 크기 설정
        SetRect(&crt, 0, 0, (WIDTH + 10) * TILESIZE, (HEIGHT + 2) * TILESIZE);
        AdjustWindowRect(&crt, WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, TRUE);
        SetWindowPos(hWndMain, NULL, 0, 0, crt.right - crt.left, crt.bottom - crt.top, SWP_NOMOVE | SWP_NOZORDER);

        // 최초 상태
        GameStatus = GAMEOVER;
        // 랜덤 틱 설정
        srand(GetTickCount());
        // 리소스 로드
        for (i = 0; i < 4; i++) {
            // 104 105 106
            hBit[i] = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1 + i));
        }
        InitGame();
        return 0;

        // 윈도우에서 명령이 발생하면 여기로 호출   
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case ID_GAME_START:
            // 게임 시작
            if (GameStatus != GAMEOVER) {
                break;
            }

            // 게임 상태
            GameStatus = RUNNING;
            startTime = time(NULL);

            // 시간이 지남에 따라 점점 속도가 빨라짐
            // 최대 0.1s
            Interval = max(100, 300 - GetRunSenconds());

            SetTimer(hWnd, 1, Interval, NULL);
            break;

        case ID_GAME_PAUSE:
            // 일시 정지
            if (GameStatus == RUNNING) {
                GameStatus = PAUSE;
                KillTimer(hWnd, 1);
            }
            else if (GameStatus == PAUSE) {
                GameStatus = RUNNING;
                SetTimer(hWnd, 1, Interval, NULL);
            }
            break;

        case ID_GAME_EXIT:
            // 종료
            DestroyWindow(hWnd);
            break;
        }
        return 0;
    case WM_TIMER:
        ret = MoveSnake();
        if (ret == 0)
        {
            //common
            InvalidateRect(hWndMain, NULL, FALSE);
        }
        else if (ret == 1)
        {
            //eat star
            InvalidateRect(hWndMain, NULL, FALSE);
        }
        else if (ret == 2)
        {
            //game over
            KillTimer(hWndMain, 1);
            GameStatus = GAMEOVER;
            MessageBox(hWndMain, TEXT("Game Over !!!"), TEXT("MSG"), MB_OK);
        }
        return 0;

    case WM_KEYDOWN:
        if (GameStatus != RUNNING) {
            return 0;
        }

        switch (wParam) {
        case VK_LEFT:
            TurnSnake(3);
            InvalidateRect(hWnd, NULL, FALSE);
            break;
        case VK_RIGHT:
            TurnSnake(1);
            InvalidateRect(hWnd, NULL, FALSE);
            break;

        case VK_UP:
            TurnSnake(0);
            InvalidateRect(hWnd, NULL, FALSE);
            break;
        case VK_DOWN:
            TurnSnake(2);
            InvalidateRect(hWnd, NULL, FALSE);
            break;
        }
        return 0;

    case WM_PAINT:
        // 윈도우 draw 이벤트
        hdc = BeginPaint(hWnd, &ps);
        // 윈도우에 그리기
        DrawScreen(hdc);
        EndPaint(hWnd, &ps);
        return 0;

    case WM_DESTROY:
        KillTimer(hWndMain, 1);
        for (i = 0; i < RESOURCESIZE; i++) {
            DeleteObject(hBit[i]);
        }
        PostQuitMessage(0);
        return 0;
    }

    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


void InitGame()
{
    // 기본 보드판 설정.
    for (int x = 0; x < WIDTH + 2; x++) {
        for (int y = 0; y < HEIGHT + 2; y++) {
            // wall : 1, empty : 0
            board[x][y] = (y == 0 || y == HEIGHT + 1 || x == 0 || x == WIDTH + 1) ? 1 : 0;
        }
    }

    snake.clear();

    int range = 5;
    int x = ((1 + WIDTH) / 2 - range) + rand() % (range * 2); //1~WIDHT
    int y = ((1 + HEIGHT) / 2 - range) + rand() % (range * 2); //1~HEIGHT

    snake.push_back({ x, y });
    for (int i = 0; i < snake.size(); ++i)
    {
        Point curr = snake[i];
        board[curr.x][curr.y] = 2;
    }

    snakeDir = rand() % 4;
    turnFlag = 0;

    AppearStar();
    return;
}

void TurnSnake(int _d)
{
    // 진행 방향으로는 ㄴㄴ
    if (_d == 0 && snakeDir == 2) return;
    if (_d == 1 && snakeDir == 3) return;
    if (_d == 2 && snakeDir == 0) return;
    if (_d == 3 && snakeDir == 1) return;
    if (turnFlag) return;
    snakeDir = _d;
    turnFlag = 1;
    return;
}

int MoveSnake()
{
    int ret = 0;
    Point snakeHead = snake[0];
    int updateX = snakeHead.x + dx[snakeDir];
    int updateY = snakeHead.y + dy[snakeDir];
    int meetStar = 0;
    if (updateX < 1 || updateY < 1 || updateX > WIDTH || updateY > HEIGHT)
    {
        // out of bound
        return 2;
    }
    if (board[updateX][updateY] == 1 || board[updateX][updateY] == 2)
    {
        //1 : 벽
        //2 : snake
        return 2;
    }

    //can move
    turnFlag = 0;

    if (board[updateX][updateY] == 3)
    {
        meetStar = 1;
    }
    
    if (meetStar == 0)
    {
        if (DEBUGFLAG) std::cout << "get star !!!!\n";
        board[snake.back().x][snake.back().y] = 0;
        snake.pop_back();
    }
    else
    {
        score++;
        AppearStar();
    }

    board[updateX][updateY] = 2;
    snake.push_front({ updateX, updateY });

    if(DEBUGFLAG) std::cout << "xy: " << updateX << ", " << updateY << "\n";

    return ret;
}


void AppearStar()
{
    int x = WIDTH / 2;
    int y = HEIGHT / 2;
    while (true)
    {
        x = 1 + rand() % WIDTH;
        y = 1 + rand() % HEIGHT;
        if (board[x][y] == 0) break;
    }

    board[x][y] = 3;
    return;
}

void DrawScreen(HDC hdc) {
    /*
    PrintTile : x, y, i번째 타일로 그리기
    */
    for (int i = 0; i < HEIGHT + 2; ++i)
    {
        for (int j = 0; j < WIDTH + 2; ++j)
        {
            PrintTile(hdc, j, i, board[j][i]);
        }
    }
    
    // 점수판 그리기
    TCHAR str[128];
    int currTime = GetRunSenconds();
    int currScore = currTime + score * 50;
    lstrcpy(str, TEXT("Snake Game"));
    TextOut(hdc, (WIDTH + 4) * TILESIZE, 30, str, lstrlen(str));
    wsprintf(str, TEXT("Score : %d   "), currScore);
    TextOut(hdc, (WIDTH + 4) * TILESIZE, 60, str, lstrlen(str));
    wsprintf(str, TEXT("Time : %d   "), currTime);
    TextOut(hdc, (WIDTH + 4) * TILESIZE, 80, str, lstrlen(str));

}


// 타일을 bitmap으로 그리기
void DrawBitmap(HDC hdc, int x, int y, HBITMAP hBit) {
    HDC MemDC;
    HBITMAP OldBitmap;
    int bx, by;
    BITMAP bit;

    MemDC = CreateCompatibleDC(hdc);
    OldBitmap = (HBITMAP)SelectObject(MemDC, hBit);

    GetObject(hBit, sizeof(BITMAP), &bit);
    bx = bit.bmWidth;
    by = bit.bmHeight;

    BitBlt(hdc, x, y, bx, by, MemDC, 0, 0, SRCCOPY);

    SelectObject(MemDC, OldBitmap);
    DeleteDC(MemDC);
}

void PrintTile(HDC hdc, int _x, int _y, int index) {
    DrawBitmap(hdc, _x * TILESIZE, _y * TILESIZE, hBit[index]);
    return;
}

int GetRunSenconds()
{
    int ret = (time(NULL) - startTime);
    return max(ret, 0);
}