발전하는 춘배

[원고, SDL3] 5. 윈도우 오목 게임 만들기 - 버그해결 본문

.원고

[원고, SDL3] 5. 윈도우 오목 게임 만들기 - 버그해결

춘배0 2025. 8. 25. 19:01

문제 3가지가 있었다. 해결해보자.

 

1. 바둑알 놓아지는 위치

void Game::HandleEvent(const SDL_Event& e) {
        if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
            int x = e.button.x;
            int y = e.button.y;

            int cellSize = 1200 / (BOARDSIZE + 1);
            int col = x / cellSize;
            int row = y / cellSize;

            if (row >= 0 && row < BOARDSIZE && col >= 0 && col < BOARDSIZE) {
                if (board[row][col] == 0) {
                    board[row][col] = isUser1Turn ? 1 : 2;
                    isUser1Turn = !isUser1Turn;
                }
            }
        }
}

 

지금코드다. 계산해보면 cellSizesms 75 나온다. 격자상에서 board[1][1]에 해당하는 위치는 (150,150)일 터이다. 그니깐 다시 말하면 대충 저기 이모티콘 있는 정도 위치를 클릭했을 때 board[1][1]의 값이 변화해야 된다.

그럼 합리적으로 생각했을때 (150-(75/2),150-(75/2)) ~ (150+(75/2),(150+(75/2))의 사각형 영역을 클릭했을 때 board[1][1]이 바뀌면 될거다. 그러면 col = x / cellSize로 구할 게 아니라 x-(75/2) / cellSize로 구해야 한다. 평행이동 했다고 생각하면 됨. row도 마찬가지.

            int col = (x - (cellSize/2)) / cellSize;
            int row = (y - (cellSize/2)) / cellSize;

해결완료.

 

3. 게임 승리 판정 타이밍

지금:

일단 위에 HandleEvent 안에서 바둑알 놓으면(클릭하면) 내부적으로 턴이 넘어간다. 근데 메인함수 보면

while (!quit) {
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_EVENT_QUIT) {
                quit = true;
            }
            game.HandleEvent(e);
        }

        // 바둑판 렌더링
        DrawGrid(renderer);

        // 바둑알 렌더링
        game.Update();
        game.Render(renderer);
        SDL_RenderPresent(renderer);
}

이렇게 되어 있는데, 승리 조건 판정은 한참 밑에 Update()에다가 짜 놓았다. 그리고 승리 조건도 아무나 승리했는가? 를 보는 게 아니라 '지금 턴인 유저가 승리했는가?' 만 판정하기 때문에 실제론 이미 a가 승리했지만 b까지 돌을 놓고, 내부적으로 a의 차례가 돌았을 때 Update()에서 승리조건을 감지하므로 승리 즉시 승리하지 못하는 상황인 것이다. 이건 애초에 SDL 로직을 적용하면서 게임 로직이 바뀌어야 했던 거라서 바꿔주면 된다. 서순의 문제일 뿐.

어떻게 바꿀까 잠깐 생각하다가, game.Update()에서 턴을 넘기는 게 맞겠다는 생각을 했다. 직관적이다. 게임.업데이트() 함수에서 턴을 업데이트한다. 핸들이벤트()에서는 딱 클릭 들어오면 board[][]에 돌을 등록한다.

 

라고 생각했지만 문제가 생긴다. 내가 약간 오해하고 있었던 건, SDL_PollEvent() while문이 다음 이벤트까지 break를 걸어주는 건 줄 알았는데 아니었고, 따라서 매 프레임마다 game.Update()가 실행되며 매 프레임마다 턴이 바뀐다. 그러면 안 되는 거니깐 과감하게 Update()를 포기한다. 핸들이벤트()에서 제대로 착수가 됐을 때 승리 여부도 확인하고, 턴도 바꾼다.

3번문제도 해결.

 

2. 바둑알 깜빡거림

바둑알이 무지하게 깜빡거린다. 이유가 뭘까?

1) 관찰을 해봤더니 마우스를 열심히 움직일 수록 더 깜빡거린다.

그렇다면 while(SDL_PollEvent())는 여태 있었던 이벤트들을 하나씩 스택에서 꺼내가며 처리하는 거니깐,,,

이벤트가 너무 많이 쌓이면 그동안엔 바둑알 렌더링이 안 되는 거니깐 이렇게 깜빡거리는 건가? 근데 그렇다기엔 바둑판은 안깜빡거린다. 그래도 밑져야 본전이니깐 저 while문 안에서도 바둑알 렌더링 함수를 불러봤지만 역시 변하는 건 없었다.

2) 일단 매 프레임 일어나는 건 바둑판 렌더링, 바둑알 렌더링말곤 없다. 바둑알 렌더링이 쉽지 않은 건가? 한번 보자.

void Game::Render(SDL_Renderer* renderer) {
    // 돌 그리기
    int cellSize = 1200 / (BOARDSIZE + 1);
    for (int i = 0; i < BOARDSIZE; i++) {
        for (int j = 0; j < BOARDSIZE; j++) {
            if (board[i][j] != 0) {
                int cx = (j + 1) * cellSize;
                int cy = (i + 1) * cellSize;
                DrawFilledCircle(renderer, cx, cy, cellSize / 2 - 2, board[i][j] == 1 ? 0 : 255); // 검정/흰 돌
            }
        }
    }
}

15*15 board에서 돌이 놓인 위치를 찾아 DrawFilledCircle 함수를 실행한다.

DrawFilledCircle은 모든 좌표에 점을 찍는, 오버헤드가 좀 있는 작업이다. 그래서 그런 건 아닐까?

테스트를 위해 DrawFilledCircle 대신 원의 테두리만 그리는 함수로 갈아끼워본다.

똑같다. 결국 함수 자체의 오버헤드가 문제는 아니다. 그럼 뭐가 문제일까?

3) 해결

gpt의 도움을 받아보았다. 해결되었다.

원인은 다음과 같았다

원래 코드에서 매 프레임 다음과 같은 일들을 한다.

바둑판 렌더링 -> SDL_RenderPresent(renderer) -> 바둑알 렌더링 -> SDL_RenderPresent(renderer)

이렇게 된 이유는 바둑판 렌더링 코드들을 함수로 뽑아내면서 SDL_RenderPresent(renderer)까지 합쳐서 뽑아버린 까닭임.

이럼 무슨 문제가 생기냐면 바둑알 없이 화면이 그려짐 -> 바둑알 포함된 화면이 그려짐 -> (다음프레임) -> 바둑알 없이 화면이 그려짐 -> 바둑알 포함된 화면이 그려짐. 이래서 바둑알이 생겼다없어졌다 한거다.

결론!

SDL_RenderPresent(renderer);는 프레임당 한번 호출하자.

 

근데 그건 그렇고 여기서 의문이 생겼다.

그럼 바둑판은 왜 안 깜빡거린 것일까?

gpt형님한테 물어봤다.

이해는 했어. 근데 나 그럼 궁금한 거. 바둑판은 왜 안 깜빡거려?
보드 그리기 -> SDL_RenderPresent(renderer); -> 바둑알 그리기 -> SDL_RenderPresent(renderer);
가 한 프레임에 모두 있다면, 첫 SDL_RenderPresent(renderer);때 렌더러 버퍼는 비워지지 않아?
그럼 바둑알 그린 이후 SDL_RenderPresent(renderer);때는 바둑판이 없어지고 바둑알만 그려져야 하는 게 맞는 거 아니야?

답변 요약:

SDL_RenderPresent()는 버퍼를 “비우는” 게 아니라 “스왑(swap)”하는 거다.

SDL은 내부적으로 더블 버퍼링(double buffering) 을 쓴다.
즉, 두 개의 버퍼(front, back)를 번갈아 가면서 사용한다.

  • front buffer → 지금 모니터에 보이는 화면
  • back buffer → SDL_RenderCopy, SDL_RenderDraw 같은 함수로 그리고 있는 임시 공간

SDL_RenderPresent(renderer)를 호출하면 back buffer의 내용을 front buffer와 교체 (→ 화면에 표시)

버퍼 초기화는 SDL_RenderClear();를 호출해야함.

아하 이해가 됐다.

 

 

결과적으로 오목 게임의 핵심은 완성이 됐다.

이제 게임답게 시작화면이랑 게임승리문구정도 띄우고 다시하기 기능까지만 추가하고 끝내려 한다.

 

 

반응형