자바스크립트로 만드는 테트리스 강의다. 코린이를 위해 쉽게 서술하였다. 지난 게시글에 이어서 개발을 진행한다. 기존의 클론코딩에서 있었던 버그들을 수정하는 데 중점을 두었다.
지난 게시글 링크!
2023.02.05 - [코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명
지난 글에서 기존 테트리스에서 개선해야 될 점 3가지를 정했다.
1. 다시 시작을 눌렀음에도 기존의 점수가 리셋되지 않는다.
2. 게임이 종료되었음에도 블록이 계속 내려온다.
3. 블럭이 벽에 닿았을 때 그쪽 벽으로 꾹 누르면 벽이 잠깐씩 뚫렸다가 돌아온다.
tetris.js 코드를 고치며 위의 3가지 사항을 고쳐보자!
1. 다시 시작 버튼을 눌렀음에도 기존점수가 리셋되지 않는다.
tetris.js
function init() {
gameover = false;
score=0;
scoreDisplay.innerText=score;
tempMovingItem={ ...movingItem };
//일명 three dots 문법으로 안에 있는 것을 전달받아 각각의 요소를 배열로 변환한다. 안의 값만 가져온다.
for (let i = 0; i < GAME_ROWS; i++) {
prependNewLine();
//시작하고 나면 새로운줄을 설정한 높이만큼 생성한다.
}
generateNewBlock();
//새로운 블럭 나와랏!
}
다시시작 버튼을 눌렀을 때 점수가 리셋되게 하기 위해 수정한 tetris.js의 init() 함수다.
기존에는 없었던 부분이 추가되었는데
function init() {
gameover = false;
score=0;
scoreDisplay.innerText=score;
}
function init() 안의 이 3가지 줄이다.
gameover= false; 는 나중에 게임 종료 후에도 블록이 계속 내려오는 버그를 해결할 때 쓸 부분이다.
score=0;과 scoreDisplay, innerText=score; 두 줄이 점수를 새로 표기하는 부분이다.
기존의 코드에는 게임이 리셋되었을 때 점수도 리셋해야 한다는 코드 자체가 없었다.
이제 이 코드를 추가하면
게임이 시작되었을 때(function init())
점수가 0으로 초기화되며, (score=0;)
그 0으로 초기화된 값이 보인다.(scoreDisplay, innerText=score;)
2. 게임이 종료되었음에도 블록이 내려온다.
기존의 코드에서는 테트리스 블럭이 천장에 닿아 게임이 종료되었음에도 방향키를 연타하면 블럭이 좌우로 이동하며 내려오는 걸 볼 수 있었다.
이는, 게임 종료 후에도 블록을 생성하고 렌더링 하는 함수들이 별도의 정지 없이 계속 순환하기 때문이다.
즉, 게임이 종료되면 더 이상 블록을 생성하지 않는 신호를 주는 변수가 필요하다.
이것을 flag(특정 동작을 수행할지 말지 결정하는 변수)라고 하며, 여기서는 gameover라는 flag변수를 코드에 추가했다.
//variables
let score = 0;
let duration = 500;
let downInterval;
let tempMovingItem;
let gameover = false;
기존의 변수들 모음에서 gameover라는 변수를 추가해 false를 시작할 때의 default 값으로 두었다. gameover라는 변수는 boolean으로 게임이 종료된 상태면 true, 게임이 종료되지 않고 계속되고 있는 상태면 false를 반환할 것이다. 처음에는 당연히 gameover 된 상태가 아니니까 일단 false로 둔다.
또한 이 변수는 함수나 블록 안에서 선언되지 않았기에 전역변수처럼 사용할 수 있다.
자료형이 명확하지 않은 javascript에서는 변수를 선언할 때 const, var, let로 선언할 수 있다.
그런데 이 중에서 왜 const나 var이 아닌 let로 gameover를 선언했을까?
그 이유를 알기 위해서는 우선 const, var, let의 차이를 알아야 한다.
const는 변수 재선 언 재할당 모두 불가능하다.
var은 변수 재선언 재할당 모두 가능하다.
let는 변수 재선언은 불가능하지만, 재할당은 가능하다.
변수 재선언이란 똑같은 이름의 변수를 다시 정하는 것이고,
변수 재할당이란 변수 안에 기존의 값과 다른 값을 새롭게 집어넣는 것이다.
우리가 gameover라는 flag변수를 어떻게 사용할지 생각해 보자
기존의 함수 중 gameover를 판단하는 부분에서 gameover를 true로 바꿀 것이며.
블록을 생성하고 렌더링 하는 다른 함수들은 gameover가 true라면 함수를 종료할 것이다.
그렇다면 변숫값 재할당이 불가능한 const는 일단 사용할 수 없다. gameover 변수의 값은 실행 중 바뀌어야 하기 때문이다.
gameover변수는 재선 언 할 필요가 없으므로 var과 let 모두를 쓸 수 있다.
하지만 굳이 재선언을 할 필요가 없는 변수에 var을 쓴다면, 나중에 코드가 길어졌을 때 실수로 재선언하 거나 에러가 날 확률이 높아진다. 그래서 여기서는 gameover변수를 let로 선언하였다.
function init() {
gameover = false;
}
위에서 init() 함수에 gameover=false;를 쓴 이유가 바로 여기서 나온다. 게임이 다시 시작 버튼을 눌러 재시작될 때에도 init() 함수를 다시 실행시킨다. 게임이 다시 시작될 때는 gameover가 true에서 false로 바뀌어야 할 것이다. 그래서 init함수에 gameover=false;라는 코드를 추가했다.
새롭게 바뀐 renderBlocks 함수다.
function renderBlocks(moveType="") {
//moveType이라는 파라미터가 있을때만 별도의 과정을 통해 처리한다.
const { type, direction, top, left } = tempMovingItem;
//여기서 각각의 값에 구조분해하여 할당한다.불러온 변수들을 바로바로 사용할 수 있기 위함. 하나하나 접근하기 귀찮음
const movingBlocks =document.querySelectorAll(".moving");
//아래에서 사용하기 위한 moving이라는 class가진것들 모두 movingblocks로 가져와 추가 관리
//여기서 moving은 원래 정의된 클래스가 아닌 밑의 classlist.add에서 만들어진건데, renderblock 할때 이동하면 겹쳐서 색칠되는 것을 막기 위해 moving 클래스를 부여해 구별하는 것이다
movingBlocks.forEach(moving => {
moving.classList.remove(type,"moving");
//movingblocks 즉, moving class를 가지는 모든 객체에서 type과 moving 클래스를 제거한다.
//화살표함수가 최초로 사용되었는데 a.foreach(b->c)면 a를 b라고 선언하고 그것에 대한 c를 실행한다는 의미이다.
//즉 움직이고 나서 그자리에 있었던 애들은 모양을 담는 값과 moving 클래스를 제거해준다. 그러면 빈값이 되어 색칠이 지워진다.
})
BLOCKS[type][direction].some(block => {
// tempmovingitem에서 불러와 변수값으로 선언한것들 , 블럭의 모양과 방향을 불러온다.
//some 함수 사용 한번이라도 true값이 리턴되면 foreach같은 순회 멈추기위해 foreach 대신 사용
//BLOCKS[type][direction]을 block으로 불러오고
const x = block[0] +left;
const y = block[1] +top;
//block[0]과 [1]값은 블럭한칸의 x좌표와 y좌표의미 이를 x와 y로 불러온다, 이동중이라면 이동시켜서 불러온다.
const target = playground.childNodes[y]? playground.childNodes[y].childNodes[0].childNodes[x] : null;
//배경의 위치가 지정된 블럭에 접근해 해당블럭이 위아래 배경 한계를 넘지 않는다면 해당 블럭 li들 그렇지 않다면target을 null값으로 정한다.
//타겟은 배경 하나하나를 채우는 블럭이다.
const isAvailable = checkEmpty(target);
//isavailable함수로 해당 블럭이 위의 target에서 범위를 벗어나 undefined 되어 있는 상태인지 정한다.
if(isAvailable){
//만약 움직일 수 있는 상태라면 해당 타겟에
target.classList.add(type,"moving")
//여기서 type명과 "moving"이라는 클래스가 추가된다!
}
else{
//해당 블럭이 벽을 넘어가 버려 움직일수 없는 상태라면
tempMovingItem={...movingItem}
//현재 움직이는 아이템의 모양, 위 아래 길이, 방향을 원복시킨다.
if(moveType ==='retry'){
clearInterval(downInterval)
gameover=true;
showGameoverText()
//===는 3항자 문법인데 타입형태까지 같은지 비교한다.
//아무튼 그렇게 해서 movetype이 retry라면 downinterval 함수를 중단하고, 게임오버 텍스트를 보여준다.
//renderblocks의 인수가 움직일 수 없는 상태라서 retry 받았는데 여전히 움직일 수 없는 상태로 또 retry받았다? 더이상 못움직이네! 게임끝났네
}
if(gameover){
return true;
}
setTimeout(()=>{
renderBlocks('retry')
if(moveType === "top"){
//여기서 top은 문자열로 movingitem의 숫자값 top이랑은 다르다. top이라는 상태는 더이상 블럭이 아래에 닿아 움직일 수 없는 상태인지 확인하는 것이다.
seizeBlock();
}
},0)
//settimeout은 실행시킬 함수, 몇초 뒤에 실행시키는 지를 인자로 받는다.
//그래서 인자 없는 renderblocks 부터 밑의 if문을 0초컷으로 작동시키는 것이다.
//0초 후에 실행시키는 게 무슨 소용인가요? 라고 생각하면 자바스크립트의 비동기 시스템을 생각해야 한다. 0초 후에 함수를 실행시키라는 뜻은 다른 함수 스케쥴링이 끝나고 나서 바로 실행해달라는 뜻이다.
//그러니까 다른 함수 실행이 끝나고 나면 renderblocks를 retry로 재귀호출하고 movetype이 top, 그러니까 ~면 블럭을 움직일 수 없게 굳힌다.
//이벤트 스택 계속 쌓이는 에러 막기 위함이다. 이게 없으면 계속 나온다.
return true;
}
})
movingItem.left=left;
movingItem.top=top;
movingItem.direction=direction;
//방향 변경 가능하다고 여겨지면 그제서야 현재 떨어지는 블럭 바꿔줌
}
여기서 변경된 부분은 함수 안에서 retry를 받는 부분 즉, 게임이 종료되었을 때 코드를 추가한 부분에 있다.
if(moveType ==='retry'){
clearInterval(downInterval)
gameover=true;
showGameoverText()
//===는 3항자 문법인데 타입형태까지 같은지 비교한다.
//아무튼 그렇게 해서 movetype이 retry라면 downinterval 함수를 중단하고, 게임오버 텍스트를 보여준다.
//renderblocks의 인수가 움직일 수 없는 상태라서 retry 받았는데 여전히 움직일 수 없는 상태로 또 retry받았다? 더이상 못움직이네! 게임끝났네
}
if(gameover){
return true;
}
게임이 종료되면 gameover는 true로 바꿔준다.
그리고 if문을 넣어 gameover면 true를 반환해 더 이상 renderBlocks()를 종료하며
renderBlocks()가 도중에 종료되므로 그 이후에 실행되는 seizeBlock()이 시작되지 않고,
seizeBlock()이 호출하는 checkMatch()도 실행되지 않으며, checkMatch()가 generateNewBlock()을 실행하지 않게 된다.
즉 , renderBlocks() -> seizeBlock() -> checkMatch() -> generateNewBlock()
-> renderBlocks()
로 실행되는 순환의 고리를 끊어 모든 함수를 정지시킨 것이다.
그 외에도 블록이 게임종료된 상태에서는 움직이면 안 되니. 블록을 조종하는 방향키에 gameover면 작동하지 않게 하는 flag를 넣었다.
//event handling
document.addEventListener("keyup", e => {
if(!gameover){
switch(e.keyCode){
case 38:
changeDirection();
break;
default:
break;
}
}
})
document.addEventListener("keydown", e => {
if(!gameover){
switch(e.keyCode){
case 39:
moveBlock("left", 1);
break;
case 37:
moveBlock("left", -1);
break;
case 40:
moveBlock("top",1);
break;
case 32:
dropBlock();
break;
default:
break;
}
}
})
3. 블록이 순간적으로 벽이 뚫리거나 지나치게 깜빡이는 현상
바로 위의 코드를 보면 무언가 방향키로 조정하는 부분이 지난번과는 달라졌음을 발견할 수 있다!
단지 gameover 변수 flag를 넣은 것뿐만 아니라 일부 키가 keydown에서 keyup 신호로 새로 받고 있다. 왜 이렇게 변경했을까?
블록이 순간적으로 깜빡이는 현상은 크게 2가지 원인이 있다.
1. 블럭이 가능한 가능한 영역에 있지만 키 입력을 지나치게 민감하게 받아서
2. 블럭이 순간적으로 불가능한 영역을 침범해서 렌더링을 되돌리느라
이 중에서 1번 원인을 수정한 것이다.
keydown은 키보드를 누르는 순간만 반응한다. 그리고 왜인지는 모르겠는데 꾹 누르고 있는 상태에서도 계속 입력을 받는다.
키보드 자판을 꾹 누르고 있으면 계속해서 반응하고, 블록이 움직이게 된다. 이것이 한번에 많이 움직여도 되는 좌,우, 아래, 스페이스 키의 경우에는 상관이 없다. 하지만, 블럭의 방향을 바꾸는 위 방향 화살표키는 그렇게 받을 필요도 없고, 받아서는 혼란만 준다.
그래서 블럭의 방향을 바꿔주는 위 화살표(아스키코드로 case 38)만 키보드를 눌렀다 떼는 순간만 반응하는 keyup으로 바꿔 주었다.
document.addEventListener("keyup", e => {
if(!gameover){
switch(e.keyCode){
case 38:
changeDirection();
break;
default:
break;
}
}
})
그럼 이제 마지막 버그인 블럭이 불가능한 영역에 있어 깜빡이는 버그를 수정해 보자!
블록이 어떤 경우에 움직여서, 새로 렌더링 해야 할까?
블록이 움직이는 경우는 자연적으로 떨어지는 경우, 키보드를 조작해서 이동 혹은 방향을 전환한 경우가 있다.
그리고 자연적으로 떨어지게 하는 generateNewBlock()의 downinterval은 downInterval = setInterval(() => { moveBlock('top', 1) }, duration)과 같이 moveBlock() 함수를 불러서 처리하고,
키보드를 통해 조작하는 경우 역시 마찬가지로 모두 moveBlock()을 불러서 처리한다,
그리고 그 moveBlock()은 모두 renderBlocks()를 호출한다.
즉 모든 블록의 새로운 렌더링은 블럭이동시키는 역할 하는 함수 -> moveBlock() -> renderBlocks() 로 처리된다.
그렇다면 renderBlocks()에서 블록의 한 칸한칸을 일단 이동 시키며 도중에 그것이 가능한지 판단하는 것이 아니라
현재 이동시키는 블럭의 한칸 한 칸이 모두 다 이동이 가능한지 판단하고 나서 모두 가능할 때, 그제야 이동시키면 깜빡거리는 버그를 없앨 수 있다.
그에 따라 바꾼 renderBlocks() 함수다.
function renderBlocks(moveType = "") {
let possible =true;
const { type, direction, top, left } = tempMovingItem;
const arr = [];
BLOCKS[type][direction].some(block => {
const x = block[0] + left;
const y = block[1] + top;
const target = playground.childNodes[y] ? playground.childNodes[y].childNodes[0].childNodes[x] : null;
const isAvailable = checkEmpty(target);
arr.push([x,y]);
if (!isAvailable) {
possible=false;
}
})
if(possible){
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove(type, "moving");
})
for(var i=0; i<4; i++){
playground.childNodes[arr[i][1]].childNodes[0].childNodes[arr[i][0]].classList.add(type,"moving");
}
//모든 이동한 블럭 클래스에 moving 부여하는 코드 필요
movingItem.left = left;
movingItem.top = top;
movingItem.direction = direction;
}
else{
tempMovingItem = { ...movingItem }
if (moveType === 'retry') {
clearInterval(downInterval)
gameover = true;
showGameoverText()
return true;
}
setTimeout(() => {
renderBlocks('retry')
if (moveType === "top") {
seizeBlock();
}
}, 0)
return true;
}
}
이전의 renderBlocks()와 달라진 점은 BLOCKS [type][direction]. some(block => {}) 반복문의 범위를 줄였으며
possible , arr이라는 변수가 생겼다.
그럼 왜 이렇게 달라졌는지 알아보자.
BLOCKS [type][direction]. some(block => {}) 반복문 밖으로 가능 불가능을 판단해 새롭게 블록을 렌더링 하는 구문들을 뺐다. 그 대신 반복문은 블록들의 이동이 모두 가능한지 그렇지 않은지만 판단하는 possible 값을 내놓는다. 이동시켜야 하는 블록이 모두 이동가능하면 그대로 true, 그중 한 칸이라도 불가능하면 possible은 false로 바뀐다.
그래서 만약 possible이 true라면(블록을 새롭게 이동시켜 렌더링 할 수 있다면) , 기존의 칸들의 색칠은 모두 moving 클래스를 없애 취소한다. 이전 반복문에서 바뀔 위치들을 2차원 배열 arr에 저장했는데 그 arr 값의 좌표들에 moving 클래스를 부여해 새롭게 렌더링 해준다.
만약 possible이 false라면 이전으로 원복 해준다.
버그들을 수정한 새로운 전체 코드들은 다음과 같다.
index.html
<!DOCTYPE html>
<html lang="en">
<!--lang 속성은 자동번역과 음성번형을 위한 국가를 지정하는 속성이다. 여기에서는 en으로 영어로 지정하였다. -->
<head>
<meta charset="UTF-8">
<!--라이브러리처럼 다양한 속성 표기를 위해 meta가 쓰인다. 그리고 charset은 문자를 어떻게 인코딩할지 그 방식을 정한다. 여기서는 일반적으로 자주 쓰는 UTF8 방식이 쓰였다.-->
<meta name="viewport" content="width=device-width,inital-scale=1.0">
<!--name에서 viewport를 태그하면 기기에 따른 크기조절을 나타내고 후에 content에서 이 창을 여는 장치넓이로 설정시킨다. -->
<title>테트리스</title>
<!--제목표시줄 및 페이지 탭의 제목으로 사용, 검색엔진에서 표시,2개이상 사용불가능한 title 태그-->
<link rel="stylesheet" href="css/style.css">
<!--link태그에서 rel에 속성값을 넣고 스타일시트로 활용할 외부 리소스 불러옴
href에서 외부리소스의 url 명시한다-->
</head>
<body>
<div class="wrapper">
<!--class태그의 wrapper를 통해 여러 요소를 grouping한다-->
<div class="game-text">
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있음-->
<span>게임종료</span>
<button>다시시작</button>
</div>
<div class="score">
0
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있다-->
</div>
<div class="playground">
<!--class태그를 통해 .으로 정의된 css 파일의 class를 불러올수 있다-->
<ul></ul>
<!--ul 사이에 빈공간 없어야지 사이에 텍스트값 있는걸로 인식이 안된다.-->
<!--고의적으로 안이 텅빈 ul태그를 테트리스의 한 칸으로 사용힌다.-->
</div>
</div>
<script src="javascript/tetris.js" type="module"></script>
<!--스크립트 태그를 통해 javascript 사용한다. src를 통해 위치시키고 모듈타입으로 지정해야 tetris.js에서 import 가능 -->
</body>
</html>
style.css
* {
margin: 0;
padding: 0;
}
/*전체선택자로 margin은 바깥 여백, padding은 내용과 테두리 사이 간격 의미한다. 여기서 간격은 없다.*/
ul{
list-style:none;
}
/*UL태그의 속성 지정, list-style을 none으로 지정해줌으로써 ul 앞에 나오는 점을 없앰*/
body{
height: 100%;
/*정해진 블럭에서 차지하는 높이다.*/
overflow: hidden;
/*overflow를 hidden으로 지정해줌으로써 화면 넘어갈 경우 걍 숨겨버린다.*/
}
.game-text{
display: none;
justify-content:center;
align-items: center;
flex-direction: column;
position: fixed;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
left: 0;
top: 0;
color: #fff;
font-size: 50px;
}
.game-text > button{
padding: 0.5rem 1rem;
color: #fff;
background: salmon;
border: none;
cursor: pointer;
/*재시작 버튼을 표시한다.*/
}
.score{
text-align: center;
font-size: 36px;
margin-bottom: 2rem;
}
.playground > ul {
border:1px solid #333;
width: 250px;
margin: 0 auto;
/*모든 블럭을 합친 전체 칸의 크기다.*/
}
.playground > ul > li {
width: 100%;
height: 25px;
/*세로 한 줄의 크기다*/
}
.playground > ul > li > ul {
display: flex;
}
.playground > ul > li > ul > li {
width: 25px;
height: 25px;
outline: 1px solid #ccc;
/*테트리스 블럭에서 최종적으로 한 칸이 되는 블럭 단위의 크기를 설정하였다.*/
}
.tree{
background: #67c23a;
}
.bar{
background: salmon;
}
.square{
background: #2c82c9;
}
.zee{
background: #e6a23c;
}
.elLeft{
background: #8e44ad;
}
.elRight{
background: #16a085;
}
/*각 클래스별 색깔을 숫자로 지정하거나 단어로 지정한다*/
tetris.js
import BLOCKS from "./blocks.js"
// DOM
const playground = document.querySelector(".playground > ul");
const gameText = document.querySelector(".game-text");
const scoreDisplay = document.querySelector(".score");
const restartButton = document.querySelector(".game-text > button");
// Setting
const GAME_ROWS = 20;
const GAME_COLS = 10;
//variables
let score = 0;
let duration = 500;
let downInterval;
let tempMovingItem;
let gameover = false;
const movingItem = {
type: "",
direction: 0,
top: 0,
left: 3,
};
init()
// fUNCTIONS
function init() {
gameover = false;
score = 0;
scoreDisplay.innerText = score;
tempMovingItem = { ...movingItem };
for (let i = 0; i < GAME_ROWS; i++) {
prependNewLine();
}
generateNewBlock();
}
function prependNewLine() {
const li = document.createElement("li");
const ul = document.createElement("ul");
for (let j = 0; j < GAME_COLS; j++) {
const matrix = document.createElement("li");
ul.prepend(matrix);
}
li.prepend(ul);
playground.prepend(li);
}
function generateNewBlock() {
clearInterval(downInterval);
downInterval = setInterval(() => { moveBlock('top', 1) }, duration)
const blockArray = Object.entries(BLOCKS);
const randomIndex = Math.floor(Math.random() * blockArray.length)
movingItem.type = blockArray[randomIndex][0]
movingItem.top = 0;
movingItem.left = 3;
movingItem.direction = 0;
tempMovingItem = { ...movingItem };
renderBlocks()
}
function renderBlocks(moveType = "") {
let possible =true;
const { type, direction, top, left } = tempMovingItem;
const arr = [];
BLOCKS[type][direction].some(block => {
const x = block[0] + left;
const y = block[1] + top;
const target = playground.childNodes[y] ? playground.childNodes[y].childNodes[0].childNodes[x] : null;
const isAvailable = checkEmpty(target);
arr.push([x,y]);
if (!isAvailable) {
possible=false;
}
})
if(possible){
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove(type, "moving");
})
for(var i=0; i<4; i++){
playground.childNodes[arr[i][1]].childNodes[0].childNodes[arr[i][0]].classList.add(type,"moving");
}
movingItem.left = left;
movingItem.top = top;
movingItem.direction = direction;
}
else{
tempMovingItem = { ...movingItem }
if (moveType === 'retry') {
clearInterval(downInterval)
gameover = true;
showGameoverText()
return true;
}
setTimeout(() => {
renderBlocks('retry')
if (moveType === "top") {
seizeBlock();
}
}, 0)
return true;
}
}
function checkEmpty(target) {
if (!target || target.classList.contains("seized")) {
return false;
}
return true;
}
function seizeBlock() {
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach(moving => {
moving.classList.remove("moving");
moving.classList.add("seized");
})
checkMatch()
}
function checkMatch() {
const childNodes = playground.childNodes;
childNodes.forEach(child => {
let matched = true;
child.children[0].childNodes.forEach(li => {
if (!li.classList.contains("seized")) {
matched = false;
}
})
if (matched) {
child.remove();
prependNewLine()
score++;
scoreDisplay.innerText = score;
}
})
generateNewBlock()
}
function moveBlock(moveType, amount) {
tempMovingItem[moveType] += amount;
renderBlocks(moveType);
}
function changeDirection() {
const direction = tempMovingItem.direction;
direction === 3 ? tempMovingItem.direction = 0 : tempMovingItem.direction += 1
renderBlocks()
}
function dropBlock() {
clearInterval(downInterval);
downInterval = setInterval(() => {
moveBlock("top", 1)
}, 10)
}
function showGameoverText() {
gameText.style.display = "flex"
}
//event handling
document.addEventListener("keyup", e => {
if (!gameover) {
switch (e.keyCode) {
case 38:
changeDirection();
break;
default:
break;
}
}
})
document.addEventListener("keydown", e => {
if (!gameover) {
switch (e.keyCode) {
case 39:
moveBlock("left", 1);
break;
case 37:
moveBlock("left", -1);
break;
case 40:
moveBlock("top", 1);
break;
case 32:
dropBlock();
break;
default:
break;
}
}
})
restartButton.addEventListener("click", () => {
playground.innerHTML = "";
gameText.style.display = "none"
init()
})
blocks.js
const BLOCKS = {
//BLOCKS 라는 블럭 상수를 선언하고, 그안에 KEY:VALUE로 객체 선언한다
tree: [
[[2, 1],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[1, 0],[1, 1]],
[[1, 2],[0, 1],[2, 1],[1, 1]],
[[2, 1],[1, 2],[1, 0],[1, 1]]
],
//각각 KEY로 이름을 정의하고 블럭들이 어떻게 생겼는지 각각 [x좌표, y좌표]안에 넣고 각각 돌렸을때 어떻게 되는지도 정의해준다.
square: [
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]],
[[0, 0],[0, 1],[1, 0],[1, 1]]
],
bar: [
[[1, 0],[2, 0],[3, 0],[4, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]],
[[1, 0],[2, 0],[3, 0],[4, 0]],
[[2, -1],[2, 0],[2, 1],[2, 2]]
],
zee: [
[[0, 0],[1, 0],[1, 1],[2, 1]],
[[0, 1],[1, 0],[1, 1],[0, 2]],
[[0, 1],[1, 1],[1, 2],[2, 2]],
[[2, 0],[2, 1],[1, 1],[1, 2]]
],
elLeft: [
[[0, 0],[0, 1],[1, 1],[2, 1]],
[[1, 0],[1, 1],[1, 2],[0, 2]],
[[0, 1],[1, 1],[2, 1],[2, 2]],
[[1, 0],[2, 0],[1, 1],[1, 2]]
],
elRight: [
[[0, 1],[1, 1],[2, 1],[2, 0]],
[[0, 0],[1, 0],[1, 1],[1, 2]],
[[0, 1],[1, 1],[2, 1],[0, 2]],
[[1, 0],[1, 1],[1, 2],[2, 2]]
]
}
export default BLOCKS;
//BLOCKS 하나를 EXPORT해서 나중에 다른 파일에서 IMPORT 할 수 있게 내보냄
버그들을 고쳤으니 다음번 강의들에서는 각종 요소들을 추가해 볼 것이다!
여담
기존의 코드에도 버그가 있었다. 위에서 고친 렌더링과 게임오버 시 점수 리셋, 블록 중지이다. 이것들이 원래 강의에서도 미쳐 신경 쓰지 못해 있는 버그인지 모르고 내가 잘못 옮겨 적은 줄 알고 한참을 헤맸다.
블록을 생성하고 렌더링하고 고정시키고 한줄이 맞춰졌는지 확인하고 다시 블럭을 생성하는 일련의 함수 무한 순환싸이클을 이해하였다. 그리고 그 고리를 끊는 flag를 넣어 게임오버를 더 명료하게 처리했다.
블럭 깜빡임 현상은 버그라고는 할 수 없을지 몰라도 이용자가 즐기는데에 시각적으로 큰 불편함을 준다. 따라서 미리 처리해 애초에 안될 이동이었으면 처음부터 이동시키지 않는 방식을 선택해 깜빡임을 막았는데 좋은 접근이었다고 생각한다.
내가 수정한 코드의 변수나 함수들은 캡스락을 누르지 않는 편의를 위해 전부 소문자로 작성했는데, 나중에 다른 사람이랑 협력하게 될 때는 대문자를 넣는 게 맞을 것 같다. 다른 사람들은 다 대문자 섞는 것 같더라. 그리고 변수나 함수 이름을 정할 때도 더 잘 알 수 있는 법을 찾아야겠다. 귀차니즘으로 인해 가능한 이름을 짧게 짓는 버릇이 있는데 이것도 차차 고쳐나가야겠다.
기존의 코드는 이랬는데 저런 식으로 새롭게 바꾸었다 고 서술하는 데 있어 어떻게 해야 더 가독성이 있을지는 모르겠다. 파일 내의 코드 일부를 보여주는 방식으로 했는데, 기존 코드에서 정확히 어떤 부분만 바뀌었는지 직관적으로 보여주는 데에는 한계가 있는 것 같다.
다음 게시글 링크!
2023.04.29 - [개발/클론코딩] - JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 3
'개발 > 클론코딩' 카테고리의 다른 글
[Nomad Coders] Flutter로 UI 만들기 (2) | 2024.01.10 |
---|---|
[Nomad Coders] Flutter 를 위한 DART 문법 요약 (1) | 2024.01.09 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 4 (完) (0) | 2023.05.08 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 3 (0) | 2023.04.29 |
JavaScript로 테트리스 만들기, 코린이가 코린이를 위한 A-Z 설명 (0) | 2023.02.05 |