Grobble
간단히 테트리스 구현한 후기 본문
https://github.com/smilu97/hamtris
최근 주변에 테트리스를 연습삼아 만드는 사람이 있어서 나도 한번 만들어보기로 했다.
테트리스는 원래 프로그래밍을 이제 막 시작하는 사람들이 입문단계에서 많이 도전하는 주제라고 생각하는데, 실제로 나도 어렸을 때 만들어보기도 했고, 도움이 많이 되었던 것 같다.
Super rotation rule
테트리스에는 T-spin이나 Z-spin, S-spin 같은 어려운 기술 같은 것들이 존재하고 이 기술들이 존재할 수 있는 이유는 테트리스의 rotation에 wall kick이라는 개념이 있어서라고 하는데... 생각보다 잘 정리되고 깔끔한 규칙이 있었다.
https://tetris.fandom.com/wiki/SRS
위키를 보면 각 테트리미노의 초기 생성시 회전방향과 회전 시 모양의 변화 등이 모두 정의되어있고, 제자리에서 회전할 수 없을 경우 어떻게 해야할 지가 나와있다.
Wallkick이 바로 이 제자리에서 회전할 수 없을 경우 행해지는 것을 지칭하는 단어인데, I-mino같은 길쭉한 테트리미노들이 벽에 붙었다가 회전할 경우 원래는 벽에서 떨어진 후에 회전해야 되는데 이 일련의 과정을 생략해서 자동으로 행해주는 것을 보고 벽을 차는 것 같아서 Wallkick이라고 명명한 게 아닐까 생각한다.
Wallkick은 그런데 단순히 벽을 차는 느낌의 것뿐만 아니라 원래 같으면 집어넣을 수 없을 것 같이 보이는 위치에 테트리미노를 껴넣는 것을 가능케 한다...
테트리미노를 회전할 때, 제자리에서 회전 하는 것이 불가능 할 경우 시스템은 테트리미노를 이리저리 움직인 후에도 회전할 수 없는지를 검사하는 데, O-mino를 제외한 나머지 테트리미노들 모두 5가지의 오프셋을 순서대로 시험해보게 되어있다.
이 5가지 오프셋은 I-mino와 JLSTZ-mino 두 가지 경우의 것들이 있는데 꽤 먼 오프셋들이 존재한다.
그러다 보니 T-spin으로 3줄을 지우거나, 이상한 곳에 S-spin으로 2줄을 지우는 등의 것들이 가능해진다... ㅋㅋ
https://www.youtube.com/watch?v=_Pu4wHzncnw
CLI 에서 테트리스 그리기
예전에는 별로 할 줄 아는 것도 없었고 자신도 없어서 예쁘게 그리는 것에 별로 관심이 없었는데, 이제는 CLI에서 트리키한 방법으로 예쁜 그림을 그리는 것들을 많이 봐와서 테트리스 정도는 적당히 예쁘게 출력할 수 있지 않을까 하는 생각에 연구해보았다.
원래는 이것저것 시험해보다가 @ 문자가 그나마 제일 꽉차고 예쁜 느낌이 들어서 @와 . 만으로 모든 출력을 커버하다가...
CLI에서도 색깔이 들어간 정사각형을 그릴 수 있다는 걸 발견했는데, 이 박스들은 사실 배경색이 칠해진 공백문자 2개로 이루어져있다.
https://en.wikipedia.org/wiki/ANSI_escape_code
ascii escape code를 이용하면 stdout에 출력되는 문자에 스타일을 지정하는 것이 가능한 걸로 보이는데, 배경색도 지정하는 것이 가능하다.
ESC[ 48;5;⟨n⟩ m <== 이런식으로 stdout에 출력하면 이 뒤로 모든 문자에 배경색이 붙게 되는데 (n은 색깔 코드)
예를 들어
void switchBackground(Color color) {
if (color == COLOR_RESET) {
printf("\e[0m");
return;
}
printf("\e[48;5;%dm", (int) color);
}
이런 함수를 만들어서 사용하는 식으로 쓸 수 있었다.
OpenGL 도전
사실 OpenGL을 써본적이 없어서 (그래픽스 수업을 들었어야 됬는데..) 좀 무섭기는 했는데 막상 써보니까 WinAPI랑 별로 다른 점이 없었다.
glut 라이브러리를 사용하니까 굉장히 편하게 원하는 그래픽을 출력할 수 있었다.
Callback을 인자로 받는 함수에 메소드를 어떻게 넘기나
glut에서는 여러 이벤트에서 어떻게 대처해야 할지 알려줄 수 있는 callback 등록함수를 다수 가지고 있는데, (glutDisplayFunc, glutTimerFunc, glutKeyboardFunc, glutSpecialFunc 등) 인자로 받는 함수 포인터 자리에 클래스 메소드를 넣고 싶은데 적절한 방법을 찾지 못했다. 결국
typedef void (*DisplayFunc)();
class TetrisGame {
void Init(..., DisplayFunc displayFunc) {
...
glutDisplayFunc(displayFunc);
}
};
TetrisGame g_game;
void displayFunc(void) {
g_game.Render();
}
int main() {
g_game.Init(..., displayFunc);
return 0;
}
이런 식의 이상한 구조로 작성할 수 밖에 없었는데... 메소드라는 것은 결국 첫번째 인자로 인스턴스 주소를 받는 것이기 때문에 애초에 불가능한 게 아닐까 생각이 들고, std::bind를 써서 혹시나 깔끔하게 처리할 수 있지 않을까 싶었지만 역시 불가능했고 람다를 사용하는 것도 역시 실패했다.
glEnable
OpenGL은 기본적으로 생각보다 많은 기능들이 Disable상태로 시작되는 것 같다...
Sprite를 읽어서 Texture로 변환한 뒤에 출력하는 간단한 것 조차 GL_TEXTURE_2D라는 기능이 꺼져 있으면 안된다는 것을 몰라서 몇 시간을 삽질했다.
여기저기 구글링 해봐도 Texture 생성 자체는 많은 예제가 있었지만 glEnable(GL_TEXTURE_2D); 코드를 삽입하라는 얘기는 거의 없었다.
또 Texture에 Alpha채널을 적용시키는 것도 당연히 기본적으로 작동할 줄 알았으나
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glClearColor(0.0, 0.0, 0.0, 0.0);
기능을 몇 가지 활성화시켜야만 했다.