플레이 해보기 : https://wooseob.github.io/
- 조작키
- 좌/우 방향키 : 좌/우로 이동
- 위쪽 방향키 : 테트로미노 회전
- 아래쪽 방향키 : 한칸 더 빠르게 내려가기
- 스페이스 바 : 테트로미노 바닥까지 바로 내려보내기
Github Repository : https://github.com/WooSeob/wooseob.github.io/tree/kyd-feature/base
1. 기능 구현 설명
여러가지 기능의 나열을 하기보단 개발 과정이 뒤돌아보니 애자일이었다는 생각이 들었다. (테스트 코드를 통한 자동화된 테스트는 아니고 직접 로컬에서 확인했으나 시간은 많이 안걸렸다^^) 이런 관점에서 기능을 소개하고자 한다.
1) 블럭부터 찍어보기
먼저, 테트리스를 만들기로 했는데 테트리스를 즐겨하지 않아, 머릿속에 대략적인 이미지만 떠오를 뿐 엄밀한 룰은 잘 모르겠다. 일단 뭐라도 있어야 하니 레포를 생성하고 블럭부터 찍어보기로 한다.
이 커밋에서 Block 클래스를 추가할 때, ~Style 클래스를 추가해서 draw 작업의 일부를 위임했다. 이것이 후에 소개할 가이드 기능을 추가하는데 큰 도움이 되었다.
2) 기본적인 테트리스를 만들어보자
블럭은 찍었고, 이제 게임 화면을 그럴싸하게 만들어보자(+예시로 테트로미노 하나도 만들어서 찍어보자), 물론 블럭이 자동으로 내려가고 회전시킬 수 있고 한줄이 완성되면 사라지는 기능은 멀었다. Commit
아까는 Block을 메인 스크립트에서 바로 생성해서 뿌렸는데, 게임을 위한 인터페이스가 필요하니 GameManager 라는 클래스를 하나 만들고 여기 게임관련 로직을 다 때려박을 준비가 되되었으니, 연달아 회전, 바닥으로 내려가기, 충돌 감지, 회전 충돌 감지, 블록이 바닥에 닿으면 새로운 블록을 생성하기 등을 구현해 준다.
3) 그래픽 렌더링과 게임진행 사이클 분리하기
requestAnimationFrame을 사용하면, 화면 주사율에 맞게 함수를 호출해준다고 한다. 테트리스는 일정 시간마다 블럭이 내
려와야 하는데 렌더링을 담당하는 함수에서 여러가지 렌더링에 필요한 메서드를 모아서 호출해주는것 외에 시간을 체크하는 관심사 외의 로직이 생기니 분리하기로 하자.
이제 테트리스와 관련된(일정 시간마다 블럭이 내려옴) 시간을 스스로 관리할 수 있게 되었다. Commit
4) 옵저버 패턴으로 각종 이벤트 처리하기
게임 내의 많은 이벤트가 있고 외부 입력이 아닌 내부 이벤트 (라인 삭제, 게임 오버) 등이 있으니 이를 옵저버 패턴으로 관리해 보자.
EventEmitter 는 node js 스펙이라길래 비슷한걸 대충 만들었다. (아직 이벤트 수신이 1개밖에 안되는데 메서드만 구현하고 나중에 필요할때 수정했다.) Commit
5) 공통 기능끼리 분리하기
여러 로직을 한곳에 다 때려박고 나니, 다음 스텝을 위해서 관련 기능끼리 분리할 때가 되었음을 느낀다. 테트리스 보드를 2차원 배열로 표현해서 그냥 쓰지말고 class 를 하나 만들어서 이를 래핑해서 사용하도록 해보자.
이제, GameManager 쪽에서는 절대 board객체의 배열에 직접 접근하는일 없이 몇개의 메서드만 사용하기로 하자. 이는 내부 표현일 뿐 배열이 아닌 다른 스타일로 관리하더라도 isRotatable, isMoveable 등의 메서드는 동일하게 동작해야 한다. (그 외의 곳에서는 몇몇 사용하기는 한다 ㅎㅎ) Commit
6) 점수, 레벨 시스템을 만들어 보자
외부에서 GameManager를 바라볼 때, 점수 시스템을 기대할 수 있으나 이를 GameManager가 직접 관리할 필요는 없다. 점수 시스템 특성상 별의별 케이스로 점수를 매기는 스펙이 등장 할 수 있으니 이전에 만든 EventBus를 활용해서 Score 클래스를 하나 만들어 보자. 이제 GameManager는 점수 시스템을 가지지만, 어떻게 점수가 측정되는지는 몰라도 된다. (나중에 수정이 필요할 때 영향범위가 작다) 최초로 시간이 지나면 점수+, 라인을 지우면 점수+ 를 조건으로 우선 구현해 본다. Commit
점수도 만들었으니 게임을 시간이 지나면 더 어렵게 만드는 레벨 시스템을 도입해보자. 앞서 구현한 점수 시스템과 사실상 똑같다 Commit
레벨이 오르는 다양한 조건이 있을 수 있으니 EventBus로 이벤트를 받아서 처리하도록 하자. 라인이 삭제되면 레벨이 오르고 블럭이 빠르게 내려오도록 해보자.
7) 내려오는 테트로미노에 다양한 기능, 모양을 추가해 보자
랜덤한 테트로미노를 생성하고자 하니 메서드가 점점 두꺼워진다. 분리를 하기로 하고, 더 나아가 앞으로 "앞으로 나올 테트로미노" 를 모두 종합해서 Spawner 객체만 의존하도록 하자. Commit
테트로미노가 바닥에 어떻게 내려올지 보여주는 Guide 기능을 추가해보자.
앞서 Block이 자기 자신을 그리는 행위를 Style에 위임했으니 새로운 스타일을 생성한다. Commit
가이드 스타일을 만들었으니 가이드 기능이 있는 테트로미노 GuidedTetromino를 만들어 보자. 기본 테트로미노 클래스를 사용해도 기존 동작에 문제가 없도록 x, y, arr, style 속성에 접근할 때 실제로 위에서 내려오는 테트로미노 real의 값을 반환하도록 한다. Commit
그리고 Spawner.spawn 메서드에서 실제 Tetromino를 GuidedTetromino로 감싸서 반환시키고 실제로 가이드가 있는 테트로미노가 생성되는지 확인해보자.
가이드 기능이 있는 테트로미노가 생성되는것을 잘 확인했으니, 옵션에 따라 생성되도록 고쳐보자. 내려오고 있는 spawn 메서드가 반환하기 전에 여기에 기능을 붙일 수 있도록 래핑하는 함수를 만들고, 이를 static으로 외부에 제공해보자. 이 옵션은 선택적으로 지정할 수 있도록 기본은 Default로 설정한다. 이제 config에 대한 수정/읽기를 통제할 필요가 있으나 아직은 그냥 두었다.
그리고 ui에 체크박스를 달아서 인게임에서 반영되는지 확인해보자
8) 렌더링 로직을 분리하자
다시 GameManager로 돌아가서 render 메서드를 보면, 공통된 코드들이 조금 보인다. 다음에 나타날 테트로미노를 표시하기 위해 canvas를 하나 더 추가했는데 공통된 작업을 두번씩 하고 있는게 보인다. 게다가 조금 생각해보니 GameManager에 Canvas API에 대한 세부사항을 알고있는것이 조금 마음에 안든다. 이걸 모두 옮겨보자. Commit
공통된 작업을 하는 CanvasBoard를 두고, 세부사항을 구현하는 MainView, SpawnView를 만들자. 이로써 GameManager는 구체적인 그리기에서 손을 뗏다.
9) 다음에 나올 테트로미노를 3개 표시해보자
기존 Spawner 는 다음에 나올 테트로미노 한개만 관리했다. 이걸 배열로 만들고 뒤로 넣고 앞에서 하나씩 빼서 N개 표시할 수 있도록 해보자. 기존과의 호환을 위해 디폴트로 1을 지정하면 다른 코드는 수정하지 않아도 동작한다. Commit
이제 N개의 다음 테트로미노를 관리할 수 있게 되었으니, 3개로 지정하고 확인해보자.
확인해보니 블럭이 이상하게 보인다. 분명 블럭은 3개 잘 생성되었고, 그리는쪽에 문제가 생겼으니 아까 분리했던 SpawnView로 가보자. 지금 다음 블럭은 세로 레이아웃으로 보여주고 있다. 그러니 적절히 오프셋을 지정해서 다음 블럭들을 세로로 그려주도록 하자. Commit
10) Plus Alpha
+ 시간의 제약 상 생각했으나 못한것들이 있다. 라인 삭제 애니메이션인데, 블럭의 그리기를 Style로 분리해 두었고, 게임의 진행 루프와 주사율에 맞춰 렌더링하는 로직을 분리했으니 그리 어렵지 않게 기능을 추가할 수 있을거라 보는데 이제 시간이 없기도 하고 열린 결말로 놔두려 한다.
+ alert 등이 떠서 ui thread가 멈추거나, 크롬 탭을 이동하는등의 경우에 requestAnimationFrame이 프레임레이트만큼 호출되지 않는다. 코육대에 제시된 문제에 게임이 종료되면 버틴 시간을 출력하는 조건이 있어서 이를 게임오버시간 - 시작시간 으로 계산하면 정확하지 않을 수 있다. 이런 예외를 찾을 방법이 있는지 조금 찾아봤지만 못찾아서 그냥 적당한 시간(160ms = 6.25fps) 이상이면 멈춘것으로 가정하고 이를 최종 시간에서 빼서 출력하기로 했다.
2. 참여 소감
학교 다닐때는 만들고 싶은게 많아서 밤낮으로 코딩했는데, 요즘은 조금 무기력해진 감이 있었다.
이번 추석에는 넷플릭스나 보면서 보낼까.. 했는데, 우연치 않게 항해 플러스 제1회 코육대를 보게 되었고, 오랜만에 즐겁게 코딩한것 같다. (상품이 큰 동기가 되었다)
사실 테트리스는 너무 고전이라 인터넷에 널리고 널렸다. 코드도 널리고 널렸다. 실행파일이 아닌 코드의 가치는 유지보수/확장 가능성이라 생각한다. 이런 관점에서 코육대에 참여한 것 같고, 코딩 유희를 즐긴 것 같다.
테트리스를 만들다 보니 생각보다 심오한 분야인 것 같다. 매니아도 많고 이 글을 쓰는 몇시간 전에 알았는데 테트리스 위키도 있다. 테트리스의 엄격할 룰을 모르는데 정확한 구현이 될까? 아마 이부분은 실패했다고 본다. 하지만 앞서 말했듯 나는 유지보수성을 최우선으로 구현했고 나중에 정확한 룰을 아는날이 오면 반영할 수 있지 않을까?
+
Q. 왜 JS / Canvas로 만드셨나요?
A. 편리하고 / 재미로하기 좋을 거 같아서요. (게다가 github page를 사용하면 배포도 너무 쉽답니다.)
3. 코육대 이벤트 알아보기
항해99의 항해 플러스에서 주관하는 행사로 추석 연휴간 주어진 주제를 자유 개발스택으로 구현하고 주제별 1등은 좋은 상품들을 주는 행사라고 합니다.
https://hanghaeplus-coyukdae.oopy.io/
본 게시물은 항해99로 부터 아직(게시글 작성일) 금전적 보상이나 댓가를 받은게 없습니다. 만약 1등 해서 상품을 타게 되면 업데이트 하겠습니다.