지난 시간에는 화면 출력에 관련하여 알아보았습니다.
오늘은 플레이어(블럭) 움직이는 처리에 대해서 알아보겠습니다.
플레이어(블럭) 클래스
먼저 플레이어(블럭)의 처리를 도와줄 클래스를 작성하여봅시다.
class CPlayer
{
public:
// Block 방향
enum eDirection
{
Dir0 = 0,
Dir90,
Dir180,
Dir270,
DirMax
};
private:
int m_nXPos; // 현재 X 위치
int m_nMinXPos; // 움직일 수 있는 최소 X 범위
int m_nMaxXPos; // 움직일 수 있는 최대 X 범위
int m_nYPos; // 현재 Y 위치
int m_nMinYPos; // 움직일 수 있는 최소 Y 범위
int m_nMaxYPos; // 움직일 수 있는 최대 Y 범위
int m_nCurBlock; // 현재 블록 (인덱스 값)
eDirection m_eDirection; // 현재 블록의 방향
int m_nGameScore; // 점수
bool m_bIsGameOver; // 게임 오버 유무
public:
/**
@brief 생성자
*/
CPlayer(int nXPos = 0, int nYPos = 0, int nCurBlock = 0, eDirection eDir = eDirection::Dir0)
: m_nXPos(nXPos), m_nYPos(nYPos), m_nCurBlock(nCurBlock), m_eDirection(eDir)
, m_nMinXPos(0), m_nMinYPos(0), m_nMaxXPos(20), m_nMaxYPos(20)
, m_nGameScore(0), m_bIsGameOver(false)
{}
/**
@brief X 위치 범위 설정
@param nMinXPos 최소 X 범위
@param nMaxXPos 최대 X 범위
@return
*/
inline void SetXPositionRange(int nMinXPos, int nMaxXPos)
{
m_nMinXPos = nMinXPos;
m_nMaxXPos = nMaxXPos;
}
/**
@brief Y 위치 범위 설정
@param nMinYPos 최소 Y 범위
@param nMaxYPos 최대 Y 범위
@return
*/
inline void SetYPositionRange(int nMinYPos, int nMaxYPos)
{
m_nMinYPos = nMinYPos;
m_nMaxYPos = nMaxYPos;
}
/**
@brief 현재 위치 설정
@param nXPos 현재 X 위치
@param nYPos 현재 Y 위치
@return
*/
inline void SetPosition(int nXPos, int nYPos)
{
m_nXPos = nXPos;
m_nYPos = nYPos;
}
/**
@brief 현재 X 위치 설정
@param nXPos 현재 X 위치
@return
*/
inline void SetXPosition(int nXPos) { m_nXPos = nXPos; }
/**
@brief 현재 Y 위치 설정
@param nYPos 현재 Y 위치
@return
*/
inline void SetYPosition(int nYPos) { m_nYPos = nYPos; }
/**
@brief 현재 블록의 방향 설정
@param eDir 현재 블록의 방향
@return
*/
inline void SetDirection(eDirection eDir) { m_eDirection = eDir; }
/**
@brief 현재 블록의 방향을 다음 방향으로 변경 (ex: 90도 -> 180도)
@param
@return
*/
inline void SetNextDirection()
{
m_eDirection = m_eDirection + 1 >= eDirection::DirMax ? eDirection::Dir0 : (eDirection)(m_eDirection + 1);
}
/**
@brief 점수 설정
@param nScore 점수
@return
*/
inline void SetGameScore(int nScore) { m_nGameScore = nScore; }
/**
@brief 현재 점수에서 더하는 기능
@param nAdder 더할 점수
@return
*/
inline void AddGameScore(int nAdder)
{
m_nGameScore = m_nGameScore + nAdder >= 0 ? m_nGameScore + nAdder : 0;
}
/**
@brief 게임 오버 설정
@param bIsGameOver 게임 오버 상태
@return
*/
inline void SetGameOver(bool bIsGameOver) { m_bIsGameOver = bIsGameOver; }
/**
@brief 현재 위치에서 더하는 기능
@param nXAdder X 위치 증가값
@param nYAdder Y 위치 증가값
@return
*/
inline void AddPosition(int nXAdder, int nYAdder)
{
m_nXPos = (m_nXPos + nXAdder >= m_nMinXPos) ? (m_nXPos + nXAdder <= m_nMaxXPos ? m_nXPos + nXAdder : m_nMaxXPos) : m_nMinXPos;
m_nYPos = (m_nYPos + nYAdder >= m_nMinYPos) ? (m_nYPos + nYAdder <= m_nMaxYPos ? m_nYPos + nYAdder : m_nMaxYPos) : m_nMinYPos;
}
/**
@brief 현재 블럭을 설정 (인덱스 값)
@param nBlock 현재 블럭 (인덱스 값)
@return
*/
inline void SetBlock(int nBlock) { m_nCurBlock = nBlock; }
/**
@brief 현재 X 위치 반환
@param
@return 현재 X 위치
*/
inline int GetXPosition() const { return m_nXPos; }
/**
@brief 현재 Y 위치 반환
@param
@return 현재 Y 위치
*/
inline int GetYPosition() const { return m_nYPos; }
/**
@brief 현재 블럭 반환 (인덱스 값)
@param
@return 현재 블럭 (인덱스 값)
*/
inline int GetBlock() const { return m_nCurBlock; }
/**
@brief 현재 블럭의 방향 반환
@param
@return 현재 블럭의 방향
*/
inline eDirection GetDirection() const { return m_eDirection; }
/**
@brief 현재 블럭의 다음 방향 반환
@param
@return 현재 블럭의 다음 방향
*/
inline eDirection GetNextDirection() const
{
return (m_eDirection + 1 >= eDirection::DirMax) ? eDirection::Dir0 : (eDirection)(m_eDirection + 1);
}
/**
@brief 현재 위치 반환 (COORD형)
@param
@return 현재 위치 (COORD형)
*/
inline COORD GetCursor() const
{
COORD cursor{ (SHORT)m_nXPos, (SHORT)m_nYPos };
return cursor;
}
/**
@brief 점수 반환
@param
@return 점수
*/
inline int GetGameScore() const { return m_nGameScore; }
/**
@brief 게임 오버 상태 반환
@param
@return 게임 오버 상태
*/
inline bool GetGameOver() const { return m_bIsGameOver; }
CPlayer& operator=(CPlayer& player)
{
m_nXPos = player.m_nXPos;
m_nYPos = player.m_nYPos;
m_nCurBlock = player.m_nCurBlock;
m_eDirection = player.m_eDirection;
m_nMinXPos = player.m_nMinXPos;
m_nMinYPos = player.m_nMinYPos;
m_nMaxXPos = player.m_nMaxXPos;
m_nMaxYPos = player.m_nMaxYPos;
return *this;
}
friend bool operator==(const CPlayer& player1, const CPlayer& player2)
{
return (player1.m_nXPos == player2.m_nXPos) &&
(player1.m_nYPos == player2.m_nYPos) &&
(player1.m_nCurBlock == player2.m_nCurBlock) &&
(player1.m_eDirection == player2.m_eDirection) &&
(player1.m_nMinXPos == player2.m_nMinXPos) &&
(player1.m_nMinYPos == player2.m_nMinYPos) &&
(player1.m_nMaxXPos == player2.m_nMaxXPos) &&
(player1.m_nMaxYPos == player2.m_nMaxYPos);
}
friend bool operator!=(const CPlayer& player1, const CPlayer& player2)
{
return !(player1 == player2);
}
};
자세한 설명들은 소스에 있으니 읽어보시면 이해가 될 겁니다.
이 플레이어(블럭)를 전역으로 선언합니다.
// Player Data
CPlayer g_player;
키보드 입력 처리
기본적인 키보드 입력에 관련한 처리는 2번 챕터에서 알아보았었습니다.
void InputKey()
{
int nKey = 0;
// 키입력이 감지되었을 때
if (_kbhit() > 0)
{
// 입력된 키를 받아온다.
nKey = _getch();
switch (nKey)
{
case eKeyCode::KEY_UP: // 방향키 위를 눌렀을 때
{
break;
}
case eKeyCode::KEY_DOWN: // 방향키 아래를 눌렀을 때
{
break;
}
case eKeyCode::KEY_LEFT: // 방향키 왼쪽을 눌렀을 때
{
break;
}
case eKeyCode::KEY_RIGHT: // 방향키 오른쪽을 눌렀을 때
{
break;
}
case eKeyCode::KEY_SPACE: // 스페이스바를 눌렀을 때
{
break;
}
case eKeyCode::KEY_R: // R키를 눌렀을 때
{
break;
}
}
}
}
이렇게 InputKey 함수
를 작성하였었습니다.
여기에 플레이어(블럭)의 좌표를 움직이도록 함수를 추가합니다.
void InputKey()
{
int nKey = 0;
if (_kbhit() > 0)
{
nKey = _getch();
switch (nKey)
{
case eKeyCode::KEY_UP:
{
// 나중에 블럭 바로 내리기에 사용
break;
}
case eKeyCode::KEY_DOWN:
{
// 아래로 1칸 이동 (Y좌표로 1증가)
g_player.AddPosition(0, 1);
break;
}
case eKeyCode::KEY_LEFT:
{
// 왼쪽으로 1칸 이동 (X좌표로 1감소)
g_player.AddPosition(-1, 0);
break;
}
case eKeyCode::KEY_RIGHT:
{
// 오른쪽으로 1칸 이동 (X좌표로 1증가)
g_player.AddPosition(1, 0);
break;
}
case eKeyCode::KEY_SPACE:
{
// 나중에 블럭 회전에 사용
break;
}
case eKeyCode::KEY_R:
{
// 나중에 초기화에 사용
break;
}
}
}
}
이렇게 플레이어(블럭)의 이동을 구현하였습니다.
이제 Render 함수
와 CalcPlayer
를 변경해야합니다.
이전에는 고정된 위치에 플레이어(블럭)를 그렸다면 지금은 플레이어(블럭)의 실시간 위치에 맞게 그리도록 변경해야합니다.
void CalcPlayer()
{
// 현재 플레이어(블럭) 위치를 받아온다.
COORD playerCursor = g_player.GetCursor();
// 현재 블럭의 방향에 따른 모양을 받아온다.
int* pBlock = GetRotateBlock(g_player.GetBlock(), g_player.GetDirection());
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
if (pBlock[(nY * BLOCK_HEIGHT) + nX])
g_nArrMap[playerCursor.Y + nY][playerCursor.X + nX] = pBlock[(nY * BLOCK_HEIGHT) + nX];
}
}
}
여기서 GetRotateBlock
라는 함수를 사용하였습니다.
현재 블럭의 방향에 따라서 블럭의 모양을 반환하는 함수로 반환 값은 일차원배열로 나옵니다.
// 블럭 데이터 (전역 변수)
int* g_pCurBlock = nullptr;
// nBlockIdx : 블럭의 인덱스
// eDir : 블럭의 방향
int* GetRotateBlock(int nBlockIdx, CPlayer::eDirection eDir)
{
// 이전 블럭의 데이터가 있다면 제거
if (g_pCurBlock != nullptr)
{
delete[] g_pCurBlock;
g_pCurBlock = nullptr;
}
// 새로운 블럭 할당
g_pCurBlock = new int[BLOCK_HEIGHT * BLOCK_WIDTH];
int nMemSize = sizeof(int) * BLOCK_HEIGHT * BLOCK_WIDTH;
memcpy_s(g_pCurBlock, nMemSize, BLOCKS[nBlockIdx], nMemSize);
// 블럭 회전
for (int nRot = 0; nRot < (int)eDir; ++nRot)
{
int nTemps[BLOCK_HEIGHT * BLOCK_WIDTH] = { 0, };
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
nTemps[(nX * BLOCK_WIDTH) + (BLOCK_HEIGHT - nY - 1)] = g_pCurBlock[(nY * BLOCK_HEIGHT) + nX];
}
}
memcpy_s(g_pCurBlock, nMemSize, nTemps, nMemSize);
}
return g_pCurBlock;
}
void DestroyGame()
{
// 블럭 데이터 제거
if (g_pCurBlock != nullptr)
{
delete[] g_pCurBlock;
g_pCurBlock = nullptr;
}
if (g_console.hBuffer[0] != nullptr)
{
CloseHandle(g_console.hBuffer[0]);
}
if (g_console.hBuffer[1] != nullptr)
{
CloseHandle(g_console.hBuffer[1]);
}
}
전역으로 블럭 데이터를 가지고 있을 포인터g_pCurBlock
를 선언 합니다.
해당 포인터는 DestroyGame 함수
에서 제거할 것입니다.
이제 InitGame 함수
에 플레이어(블럭)의 초기 값을 넣어줍시다.
void InitGame(bool bInitConsole = true)
{
// 플레이어(블럭) 데이터 초기화
{
// START_POS_X = 4
// START_POS_Y = 1
g_player.SetPosition(START_POS_X, START_POS_Y);
g_player.SetXPositionRange(-1, MAP_WIDTH);
g_player.SetYPositionRange(0, MAP_HEIGHT);
g_player.SetBlock(1); // J 블럭
g_player.SetDirection(CPlayer::eDirection::Dir0);
g_player.SetGameScore(0);
g_player.SetGameOver(false);
}
// 콘솔 데이터 초기화
if (bInitConsole)
{
// 생략 ...
}
// 맵 데이터 초기화
{
int nMapSize = sizeof(int) * MAP_WIDTH * MAP_HEIGHT;
memcpy_s(g_nArrMap, nMapSize, ORIGIN_MAP, nMapSize);
}
}
이제 메인 함수에 InputKey 함수
를 추가하고 실행하여봅시다.
int main(void)
{
InitGame();
while (true)
{
// 키 입력 함수
InputKey();
CalcPlayer();
Render(3, 1);
ClearScreen();
BufferFlip();
Sleep(1);
}
DestroyGame();
return 0;
}
하지만 실행을 하면
일단 문제점은 2가지 입니다.
- 잔상이 남음
- Frame을 넘어서 그림이 그려짐
먼저 첫번째 문제를 해결하여 봅시다.
잔상이 남는 문제 해결
현재 우리는 버퍼를 이용하여 새롭게 변경된 부분에만 그림을 그리고 있기 때문에 기존의 잔상이 남습니다.
해결 방법은 이전의 플레이어(블럭) 위치를 기억하여 위치를 변경할 때 마다 이전 위치를 Clear 시키고 현재 위치에 그림을 그리도록 하면 됩니다.
이전 위치를 기억하도록 하기 위한 플레이어(블럭) 객체를 하나 더 전역으로 선언합니다.
// 플레이어(블럭) 데이터 (전역 변수)
CPlayer g_player;
// 이전 위치를 기억할 플레이어(블럭) 데이터 (전역 변수)
CPlayer g_prevPlayerData;
이제 InitGame 함수
에 이전 위치 기록용 플레이어(블럭) 데이터를 초기화 해줍시다.
void InitGame(bool bInitConsole = true)
{
// 플레이어(블럭) 데이터 초기화
{
// START_POS_X = 4
// START_POS_Y = 1
g_player.SetPosition(START_POS_X, START_POS_Y);
g_player.SetXPositionRange(-1, MAP_WIDTH);
g_player.SetYPositionRange(0, MAP_HEIGHT);
g_player.SetBlock(1); // J 블럭
g_player.SetDirection(CPlayer::eDirection::Dir0);
g_player.SetGameScore(0);
g_player.SetGameOver(false);
// 플레이어(블럭) 데이터를 이전 위치 기록용 데이터에 초기 데이터로 설정
g_prevPlayerData = g_player;
}
// 생략 ...
}
이제 CalcPlayer 함수
를 변경하여줍니다.
void CalcPlayer()
{
COORD playerCursor = g_player.GetCursor();
// 이전 위치의 블럭 제거 코드
// (이전 위치과 현재 위치가 다른 경우)
if (g_prevPlayerData != g_player)
{
// 이전 위치의 블럭데이터를 가져옴
int* pBlock = GetRotateBlock(g_prevPlayerData.GetBlock(), g_prevPlayerData.GetDirection());
COORD sprevCursor = g_prevPlayerData.GetCursor();
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
// 이전 위치의 블럭이 위치한 좌표의 데이터를 지워줌
if (pBlock[(nY * BLOCK_HEIGHT) + nX] &&
pBlock[(nY * BLOCK_HEIGHT) + nX] == g_nArrMap[sprevCursor.Y + nY][sprevCursor.X + nX])
g_nArrMap[sprevCursor.Y + nY][sprevCursor.X + nX] = 0;
}
}
// 현재 플레이어(블럭)의 정보를 이전 정보에 기록
g_prevPlayerData = g_player;
}
int* pBlock = GetRotateBlock(g_player.GetBlock(), g_player.GetDirection());
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
if (pBlock[(nY * BLOCK_HEIGHT) + nX])
g_nArrMap[playerCursor.Y + nY][playerCursor.X + nX] = pBlock[(nY * BLOCK_HEIGHT) + nX];
}
}
}
그리고 실행하여 봅시다.
Frame을 넘어서 그림이 그려지는 문제 해결
이 문제는 우리가 그림을 그리는 방식때문에 일어나는 현상입니다.
이러한 상황인 것이죠.
이 부분을 해결하기 위해서는 Render 함수
를 변경하여야합니다.
void Render(int nXOffset = 0, int nYOffset = 0)
{
COORD coord{ 0, };
int nXAdd = 0;
DWORD dw = 0;
for (int nY = 0; nY < MAP_HEIGHT; ++nY)
{
// 한 라인에 그려지는 블록의 개수
int nBlockCount = 0;
nXAdd = 0;
for (int nX = 0; nX < MAP_WIDTH; ++nX)
{
coord.X = nXAdd + nXOffset;
coord.Y = nY + nYOffset;
SetConsoleCursorPosition(g_console.hBuffer[g_console.nCurBuffer], coord);
WriteFile(g_console.hBuffer[g_console.nCurBuffer], BLOCK_TYPES[g_nArrMap[nY][nX]], sizeof(BLOCK_TYPES[g_nArrMap[nY][nX]]), &dw, NULL);
// 1칸 증가
++nXAdd;
// 공백의 경우에는 2칸이 1칸을 이룸(공백 2칸이 문자 한 칸과 같은 넓이를 이룸)
if (g_nArrMap[nY][nX] == 0)
++nXAdd;
// 블럭의 개수를 셈
else
++nBlockCount;
}
// 벗어난 부분 덧칠하기
if (nY > 0 &&
nY < MAP_HEIGHT - 1)
{
// 프레임이 끝나는 지점을 계산
int nStart = nXOffset + nBlockCount + (MAP_WIDTH - nBlockCount) * 2;
for (int nX = 0; nX < nBlockCount; ++nX)
{
coord.X = nStart + nX;
SetConsoleCursorPosition(g_console.hBuffer[g_console.nCurBuffer], coord);
WriteFile(g_console.hBuffer[g_console.nCurBuffer], BLOCK_TYPES[0], sizeof(BLOCK_TYPES[0]), &dw, NULL);
}
}
}
}
이제 실행을 해봅시다.
이제 잔상도 제거되었고 Frame 외부에 그려지는 현상도 제거하였습니다.
다음 시간에는 플레이어(블럭)의 충돌 판정에 대해서 알아봅시다.
현재 까지의 진행 프로젝트 다운로드
'Study > C++' 카테고리의 다른 글
[C++/Console] 테트리스 만들어 보기 - 6 (바닥 충돌, 블럭 랜덤 생성) (0) | 2023.03.06 |
---|---|
[C++/Console] 테트리스 만들어 보기 - 5 (충돌 판정 - 벽, 블럭) (0) | 2023.02.20 |
[C++/Console] 테트리스 만들어 보기 - 3 (화면 출력에 대한 고찰) (0) | 2023.02.02 |
[C++/Console] 테트리스 만들어 보기 - 2 (키보드 입력 및 블럭) (0) | 2023.01.29 |
[C++/Console] 테트리스 만들어 보기 - 1 (간단 소개 및 더블 버퍼링) (1) | 2023.01.28 |