이 Step에서 다루는 것

  • 2D 맵을 배열로 표현하고, 플레이어(좌표)를 이동시키는 최소 로그라이크 구조
  • 게임 루프를 Input → Logic → Output으로 나눠서 사고하는 습관
  • “콘솔에서 부드럽게 그리기”를 위해 커서를 이동해서 다시 그리는 방식
  • 가장 흔한 버그(좌표/범위/벽 충돌/입력 연속 처리)를 디버깅으로 잡는 위치

학습 목표

  • GMap2D[y][x]가 의미하는 바(좌표/배열 접근)를 설명할 수 있다.
  • 이동 로직을 “범위 체크 → 벽 체크 → 이전 위치 지우기 → 새 위치 찍기”로 재현할 수 있다.
  • 키 입력을 폴링(GetAsyncKeyState)으로 처리할 때 연속 입력이 생기는 이유와 해결법을 말할 수 있다.

게임 루프 구조

모든 게임은 무한 루프로 동작하며, 다음 3단계로 구성됨:

while (true) {
    // 1. 입력 (Input)  - 키보드, 마우스 등
    // 2. 로직 (Logic)  - 이동, AI, 충돌 등
    // 3. 출력 (Output) - 콘솔, 3D 그래픽 등
}
  • 입력(Input): 키보드 등으로부터 사용자 입력을 받음.
  • 로직(Logic): 현재 상태 계산 (이동 처리, 충돌 등).
  • 출력(Output): 콘솔에 결과 표시.
  • 온라인 게임: 로직의 일부가 서버에 이전. 기본 원칙은 동일.

텍스트(콘솔)에서도 “입력/로직/출력”을 분리해서 생각하면,
나중에 그래픽(렌더링)으로 옮겨갈 때 구조가 그대로 살아남습니다.


맵 데이터 구성

의미
0빈 공간 (□)
1벽 (■)
2플레이어 (♨)
  • 1D: GMap1D[MAP_SIZE * MAP_SIZE], 인덱스 = y * MAP_SIZE + x.
  • 2D: GMap2D[MAP_SIZE][MAP_SIZE], map[y][x]로 접근.

이번 실습은 2D 배열을 사용합니다.

GMap2D[y][x]
  y: 행(Row, 위→아래)
  x: 열(Col, 왼→오)

Helper (콘솔·입력 기능)

Helper.h

#pragma once

enum MoveDir { MD_NONE, MD_LEFT, MD_RIGHT, MD_UP, MD_DOWN };

void HandleKeyInput();
void SetCursorPosition(int x, int y);
void SetCursorOnOff(bool visible);

extern MoveDir GMoveDir;

Helper.cpp - 핵심 기능

  • SetCursorPosition(x, y): 커서 위치 이동 → 맵을 같은 위치에 다시 그려 움직임처럼 보임.
  • SetCursorOnOff(false): 커서 깜빡임 제거.
  • HandleKeyInput(): GetAsyncKeyState(VK_LEFT) 등으로 방향키 감지.
  • GetAsyncKeyState: 블로킹이 아닌 방식. 입력 대기 없이 "지금 눌리고 있나?"를 질의.
  • & 0x8000: 키가 눌린 상태인지 비트 플래그 검사.

왜 “블로킹 입력”이 아니라 폴링 입력을 쓰나?

  • cin 같은 입력은 “엔터를 칠 때까지” 멈추기 때문에, 게임 루프가 끊깁니다.
  • 폴링 입력은 “현재 눌림 상태”를 매 프레임 확인할 수 있어 게임 루프에 자연스럽습니다.

Map (맵 데이터 및 출력)

Map.h

#pragma once

inline constexpr int MAP_SIZE = 5;

void PrintMap2D();

extern int GMap2D[MAP_SIZE][MAP_SIZE];

Map.cpp

#include "Map.h"
#include "Helper.h"
#include <iostream>

int GMap2D[MAP_SIZE][MAP_SIZE] = {
    {1, 1, 1, 1, 1},
    {1, 0, 0, 0, 1},
    {1, 0, 2, 0, 1},
    {1, 0, 0, 0, 1},
    {1, 1, 1, 1, 1}
};

void PrintMap2D() {
    SetCursorPosition(0, 0);
    for (int y = 0; y < MAP_SIZE; y++) {
        for (int x = 0; x < MAP_SIZE; x++) {
            switch (GMap2D[y][x]) {
                case 0: std::cout << "□"; break;
                case 1: std::cout << "■"; break;
                case 2: std::cout << "♨"; break;
            }
        }
        std::cout << '\n';
    }
}

출력(렌더링) 포인트

  • 매 프레임 system("cls")로 화면을 지우면 깜빡임이 심해질 수 있습니다.
  • 그래서 SetCursorPosition(0,0)으로 커서를 맨 위로 옮긴 뒤, “같은 자리”에 다시 그립니다.

Player (MovePlayer, HandleMove)

Player.cpp

bool canMove = true;
int GPlayerX = 2;
int GPlayerY = 2;

void MovePlayer(int x, int y) {
    if (x < 0 || x >= MAP_SIZE || y < 0 || y >= MAP_SIZE)
        return;

    if (GMap2D[y][x] == 1)
        return;

    GMap2D[GPlayerY][GPlayerX] = 0;
    GPlayerX = x;
    GPlayerY = y;
    GMap2D[GPlayerY][GPlayerX] = 2;
}

void HandleMove() {
    if (GMoveDir == MD_NONE) {
        canMove = true;
        return;
    }

    if (!canMove)
        return;

    canMove = false;

    switch (GMoveDir) {
        case MD_LEFT:  MovePlayer(GPlayerX - 1, GPlayerY); break;
        case MD_RIGHT: MovePlayer(GPlayerX + 1, GPlayerY); break;
        case MD_UP:    MovePlayer(GPlayerX, GPlayerY - 1); break;
        case MD_DOWN:  MovePlayer(GPlayerX, GPlayerY + 1); break;
    }
}

MovePlayer 흐름(암기하면 좋은 순서)

  1. 범위 체크: 맵 밖이면 즉시 return
  2. 충돌 체크: 벽(1)이면 즉시 return
  3. 이전 위치 지우기: 기존 플레이어 위치를 0으로
  4. 좌표 갱신: ( (x, y) )로 이동
  5. 새 위치 찍기: 새 좌표를 2로

canMove 플래그가 필요한 이유

  • GetAsyncKeyState는 “키가 눌려있는 동안” 매 프레임 true가 될 수 있습니다.
  • 그래서 키를 누르고 있으면 플레이어가 한 번이 아니라 연속 이동합니다.
  • canMove는 “키를 뗐다가 다시 눌렀을 때만 1회 이동”되게 만드는 간단한 엣지 처리입니다.

더 정교하게 하려면 “이전 프레임 키 상태”를 저장해, 눌림(Down) 순간만 감지하는 방식으로 발전시킬 수 있습니다.


Main – 전체 게임 루프

#include "Helper.h"
#include "Map.h"
#include "Player.h"

int main() {
    SetCursorOnOff(false);

    while (true) {
        HandleKeyInput();   // 1. 입력 처리
        HandleMove();       // 2. 로직 처리
        PrintMap2D();       // 3. 출력 처리
    }

    return 0;
}

디버깅 포인트(추천)

  • “벽을 뚫고 간다” → MovePlayer()if (GMap2D[y][x] == 1) 줄에 브레이크포인트
  • “가끔 멈춘다/이동이 안 된다” → HandleMove()에서 GMoveDir, canMove를 Watch로 확인
  • “맵이 깨져 보인다” → PrintMap2D()에서 x/y 루프 범위를 먼저 확인 (< MAP_SIZE)

체크 질문 (스스로 답해보기)

  • GMap2D[y][x]에서 왜 y가 먼저일까?
  • MovePlayer()에서 “이전 위치를 0으로 지우는 코드”를 빼면 무슨 일이 생길까?
  • canMove 없이 구현하면 어떤 현상이 생기고, 그 원인은 무엇일까?

profile
李家네_공부방

0개의 댓글