콘솔 테트리스 게임 만들기
헤더파일)
테트리스 기초 함수 헤더파일)
#ifndef __TETRIS_HELPER_H__
#define __TETRIS_HELPER_H__
#include <Windows.h>
#include <cstdio>
KEY_EVENT_RECORD krec;
void putStringOnPosition(int x, int y, const char* content) {
// Move Cursor
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
// Print string
printf("%s", content);
}
void showConsoleCursor(bool showFlag) {
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(out, &cursorInfo);
cursorInfo.bVisible = showFlag; // set the cursor visibility
SetConsoleCursorInfo(out, &cursorInfo);
}
void drawPosition(int x, int y, bool filled) {
putStringOnPosition(x * 2, y, filled ? "■" : "□");
}
bool keyState(char c) {
short ret;
switch (c) {
case 'a': ret = GetKeyState(0x41); break;
case 'd': ret = GetKeyState(0x44); break;
case 's': ret = GetKeyState(0x53); break;
case 'w': ret = GetKeyState(0x57); break;
default: return false;
}
return ret != 0 && ret != 1;
}
#endif // !__TETRIS_HELPER_H__
클래스 정의 헤더파일)
#define _CRT_SECURE_NO_WARNINGS
#include "Tetris.h"
#include <ctime>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <cstdio>
#define GRID_WIDTH 9
#define GRID_HEIGHT 12
#define USERBLOCK_SIZE 3
// 바깥족이 줄수 안쪽이 행수 = X,Y 좌표가 반대로 돼있따
int displayData[GRID_HEIGHT][GRID_WIDTH] = { 0, };
class Display {
public:
void draw() {
for (int i = 0; i < GRID_HEIGHT; i++) {
for (int k = 0; k < GRID_WIDTH; k++) {
//drawPositon은 x,y위치에 어떻게 출력을 할것인가 하는 함수
//한줄로 작성
//drawPosition(k, i, displayData[i][k] == 1);
//초보적인 방법
if (displayData[i][k] == 0) {
drawPosition(k, i, false);
}
else {
drawPosition(k, i, true);
}
}
}
}
};
class GameEngine {
public:
enum class GameState {
PLAYING, GAMEOVER
};
GameState state = GameState::PLAYING;
int gameGridData[GRID_HEIGHT][GRID_WIDTH] = { 0, };
int userBlock[USERBLOCK_SIZE][USERBLOCK_SIZE] = { 0, };
//랜덤한 유저블록을 만들기 위한 작업
int userBlockVarious[8][USERBLOCK_SIZE][USERBLOCK_SIZE] = {
{
{0, 1, 0},
{0, 1, 0},
{0, 1, 0},
},
{
{0, 0, 0},
{0, 1, 1},
{0, 1, 1},
},
{
{0, 0, 0},
{0, 1, 1},
{0, 0, 1},
},
{
{0, 0, 0},
{1, 1, 0},
{0, 1, 1},
},
{
{1, 1, 1},
{0, 0, 1},
{0, 0, 0},
},
{
{0, 0, 0},
{1, 0, 0},
{1, 1, 1},
},
{
{0, 0, 0},
{0, 1, 1},
{1, 1, 0},
},
{
{0, 0, 0},
{0, 1, 1},
{0, 1, 0},
}
};
int blockX = 0;
int blockY = 0;
float elapsed = 0.0f;
float controlCheck = 0.0f;
void init() {
// 최초에 게임 엔진을 초기화 하는 과정을 맡는다.
makeUserBlock();
}
// while 루프에서 매번 불려지는 함수
void next(float dt, char keyboardInput) {
if (state == GameState::GAMEOVER) return;
//elapsed = elapsed + dt; 아래 구문과 똑같다
elapsed += dt;
if (elapsed >= 0.5f) {
if (canGoDown()) {
blockY++;
}
else {
// userblock 을 gameGridData 에 전사한다. (나타낸다)
trans();
if (gameOverDecision()) state = GameState::GAMEOVER;
}
//elapsed = elapsed - 0.5f; 아래 구문과 같다
elapsed -= 0.5f;
}
// 속도가 너무 빠르기 때문에 속도가 0.1초 이상이면 움직이게 만든것
controlCheck = controlCheck + dt;
if (keyboardInput == 'a' && canGoLeft() && controlCheck > 0.1) {
blockX--;
controlCheck = 0.0f;
}
if (keyboardInput == 'd' && canGoRight() && controlCheck > 0.1) {
blockX++;
controlCheck = 0.0f;
}
if (keyboardInput == 's' && canGoDown() && controlCheck > 0.1) {
blockY++;
controlCheck = 0.0f;
}
if (keyboardInput == 'w' && canRotate() && controlCheck > 10.0) {
controlCheck = 2.0f;
}
}
// 블록이 아래로 내려갈 수 있냐
bool canGoDown() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
if (userBlock[i][k] == 1 && i + blockY + 1 >= GRID_HEIGHT) {
return false;
}
if (userBlock[i][k] == 1 && gameGridData[i + blockY + 1][k + blockX] == 1) {
return false;
}
}
}
return true;
}
//블록이 왼쪽으로 갈 수 있냐
bool canGoLeft() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
//한줄짜리 if 절은 {} 생략 후 바로 리턴을 사용해도된다.
if (userBlock[i][k] == 1 && k + blockX - 1 < 0)
return false;
if (userBlock[i][k] == 1 && gameGridData[i + blockY][k + blockX - 1] == 1)
return false;
}
}
return true;
}
//블록이 오른쪽으로 갈 수 잇냐
bool canGoRight() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
if (userBlock[i][k] == 1 && k + blockX + 1 > GRID_WIDTH - 1) {
return false;
}
if (userBlock[i][k] == 1 && gameGridData[i + blockY][k + blockX + 1] == 1)
return false;
}
}
return true;
}
//라인이 가득 찼는지 확인하는 변수
bool isLineFilled(int y) {
for (int i = 0; i < GRID_WIDTH; i++) {
if (gameGridData[y][i] == 0) return false;
}
return true;
}
// 라인이 가득 찼으면 y 좌표를 기준으로 한칸씩 아래로 내림
void eraseLine(int y) {
for (int i = 0; i < GRID_WIDTH; i++) {
gameGridData[y][i] = 0;
}
}
//라인이 지우진후 당겨오는 함수
void drop(int y) {
for (int i = y; i >= 0; i--) {
for (int k = 0; k < GRID_WIDTH; k++) {
gameGridData[i][k] = i - 1 < 0 ? 0 : gameGridData[i - 1][k];
}
}
}
// userBlock 을 gameGrid에 전사하는 함수
void trans() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
// makeDisplayData 처럼 최적화 가능
//gameGridData[i + blockY][k + blockX] = userBlock[i][k] == 1 ? userBlock[i][k] : gameGridData[i + blockY][k + blockX];
int X = k + blockX;
int Y = i + blockY;
gameGridData[Y][X] = userBlock[i][k] | gameGridData[Y][X];
}
}
// TODO: 한 줄이 가득차 있는지 확인
for (int i = 0; i < GRID_HEIGHT; i++) {
if (isLineFilled(i)) {
eraseLine(i);
drop(i);
}
}
// 새로운 블록 생성
makeUserBlock();
}
//게임 오버인지 판단하는 변수
bool gameOverDecision() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
if (userBlock[i][k] == 1 && gameGridData[i + blockY][k + blockX] == 1)
return true;
}
}
return false;
}
void makeUserBlock() {
blockX = GRID_WIDTH / 2 - USERBLOCK_SIZE / 2;
blockY = 0;
int various = rand() % 7;
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
userBlock[i][k] = userBlockVarious[various][i][k];
}
}
// TODO : 랜덤을 통해서 새로운 블록을 만든다.
}
//블록 회전 코드
void rotate() {
int tmp[USERBLOCK_SIZE][USERBLOCK_SIZE] = {0,};
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
tmp[i][k] = userBlock[USERBLOCK_SIZE - k - 1][i];
}
}
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
userBlock[i][k] = tmp[i][k];
}
}
}
bool canRotate() {
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
if (k < 0 || k >= GRID_HEIGHT) return false;
else if (USERBLOCK_SIZE - 1 - i || USERBLOCK_SIZE - 1 - i >= GRID_WIDTH) return false;
else if (gameGridData[k + blockY][USERBLOCK_SIZE - 1 - i + blockX] == 1) return false;
}
}
return true;
}
// 실제 게임 데이터를 화면에 출력할 수 있는 데이터로 바꿔준다.
void makeDisplayData() {
for (int i = 0; i < GRID_HEIGHT; i++) {
for (int k = 0; k < GRID_WIDTH; k++) {
displayData[i][k] = gameGridData[i][k];
}
}
for (int i = 0; i < USERBLOCK_SIZE; i++) {
for (int k = 0; k < USERBLOCK_SIZE; k++) {
if (i + blockY < 0 || i + blockY > GRID_HEIGHT) {
//DO NOTHING
}
else if (k + blockX < 0 || k + blockX > GRID_WIDTH) {
//DO NOTHING
}
else {
// TO DO:
// 아래코드를 줄이는 방법
// 줄이는 코드가 성능이 더 좋음
// 동일한 역할을 하는 함수 + 연산자를 함수화시켜서 줄 수를 줄임
//displayData[i + blockY][k + blockX] = userBlock[i][k] == 1 ? userBlock[i][k] : displayData[i + blockY][k + blockX];
int _x = k + blockX;
int _y = i + blockY;
displayData[_y][_x] = userBlock[i][k] | displayData[_y][_x];
}
}
}
}
};
소스파일)
#include "Header.h"
#define INTERVAL 1.0/60.0
int main() {
srand(time(0));
float prev = (float)clock() / CLOCKS_PER_SEC;
Display* display = new Display();
GameEngine* gameEngine = new GameEngine();
gameEngine->init();
//하단에 커서 지우는 함수 테트리스 헤더파일에 있음
showConsoleCursor(false);
while (true) {
float curr = (float)clock() / CLOCKS_PER_SEC;
float dt = curr - prev;
if (dt < INTERVAL) continue;
prev = curr;
bool left = keyState('a');
bool right = keyState('d');
bool down = keyState('s');
bool rotate = keyState('w');
if (left) {
// 왼쪽으로 블록 이동
gameEngine->next(dt, 'a');
}
else if (right) {
gameEngine->next(dt, 'd');
}
else if (down) {
gameEngine->next(dt, 's');
}
else if (rotate) {
gameEngine->rotate();
}
else {
// 그냥 블록 떨어짐
gameEngine->next(dt, 0);
}
// 화면출력
gameEngine->makeDisplayData();
display->draw();
// 게임 상태 판별
if (gameEngine->state == GameEngine::GameState::GAMEOVER) {
break;
}
}
return 0;
}