Node.js를 공부하기 전에 JS에 대한 지식을 쌓기 위하여 미니 프로젝트를 진행하려고 한다. 본 프로젝트는 유튜버 "데브리"님의 실전 강의를 참고 하였다. JS에 대한 지식이 얕았기 때문에 해당 프로젝트는 꽤 많이 도움이 되었다. 다양한 내장 함수를 사용하였고 Live Server를 사용하여 즉각적인 코드의 변화를 확인해가며 코드를 작성하니 오류 검출 또한 수월했다.
<테트리스 규칙>
테트리스의 규칙은 간단하다. 블록이 쌓이면서 한 줄이 꽉 채워지면 그 줄은 사라지고 한 줄로 된 블록을 없애면서 블록이 맨위로 쌓이지 않도록 버티는 방식이다.
프로젝트를 진행하기 앞서 필요한 내용들을 작성하여 보겠다.
- 블록들을 표현할 직사각형 모양의 툴이 필요하다.
- 블록들은 랜덤으로 표현되어야 한다.
- 아무런 행동을 하지 않아도 블록은 자동으로 밑으로 내려가야 한다.
- 블록이 한 줄을 만들면 해당 줄이 사라지며 점수를 획득한다.
- 원하는 방향으로 블록을 회전시킬 수 있어야 한다.
- 정해둔 직사각형 모양의 툴에서 범위 밖으로 이동할 수 없어야 한다. (범위 제한)
- 블록이 쌓이고 쌓여 맨위까지 쌓인다면 게임은 종료된다. (사용자 패배)
우선 해당 프로젝트의 hmtl 코드는 아래와 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>테트리스</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="wrapper">
<div class="game-text">
<span>게임종료!!</span>
<button>다시시작</button>
</div>
<div class="score">0</div>
<div class="playground">
<ul></ul>
</div>
</div>
<script src="blocks.js"></script>
<script src="tetris.js" ></script>
</body>
</html>
css 파일과 js 파일이 같은 폴더에 있기 때문에 경로 설정에 대해서는 생각하지 않아도 된다. 만약 다른 폴더에 있다면 해당 폴더까지의 경로를 통하여 html과 css , js 파일을 연결 시켜주어야 한다. html 코드에서 "데브리"님과 다른 점은 <script> 구문이다. 필자는 block.js 와 tetris.js 두 개의 파일을 연결해주었지만 "데브리"님은 모듈을 사용하셨다. 모듈을 사용한다면
두 줄의 코드가 아래와 같이 한 줄로 표현이 가능하다.
하지만 module을 사용한다면 html의 제약을 받게 되어 Open Live Server에서만 프로그램이 구동되었다. html에서는 파일 인젝션을 통하여 외부에서 실행 파일이 삽입되지 않도록 막아두기 때문에 타입을 모듈로 지정한 html 코드에 대해서는 Live Server가 아닌 외부에서 실행을 한다면 실행이 되지 않는다. 필자는 외부에서의 실행과 Live Server에서의 실행 모두 가능하게 하기 위해 모든 js 파일이 연결되도록 작성하였다.
해당 프로젝트의 핵심인 Tetris.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;
const movingItem = {
type : "",
direction : 0,
top : 0,
left : 3,
};
.css 에 있는 클래스들을 document.querySelector(" "); 구문을 통해서 삽입하고 각 필요 클래스를 연결한다.
해당 코드는 movingItem 이라는 Object를 선언한 것이다. C언어에서 구조체에 해당되며 자바에선 객체에 해당된다.
type에는 블록의 모양, direction, top, left는 블록의 위치를 나타낸다.
<초기화 및 틀 만들기>
function init(){
score = 0;
scoreDisplay.innerText = 0;
tempMovingItem = {...movingItem}; // spread를 통해 temp 값이 변경 되는 것을 방지
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);
}
init() 함수를 통해 초기 화면을 설정하고 prependNewLine() 함수를 통해 새로운 줄을 생성한다.
<랜더링 함수>
// 블록을 렌더링하는 함수
function renderBlocks(moveType = "") {
const { type, direction, top, left } = tempMovingItem;
const movingBlocks = document.querySelectorAll(".moving");
movingBlocks.forEach((moving) => {
moving.classList.remove(type, "moving");
});
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);
if (isAvailable) {
target.classList.add(type, "moving");
} else {
tempMovingItem = { ...movingItem };
if (moveType === "retry") {
clearInterval(downInterval);
showGameoverText();
}
setTimeout(() => {
// 무한 call stack 방지를 위해 비동기 처리
renderBlocks("retry");
if (moveType == "top") {
seizeBlock();
}
}, 0);
return true;
}
});
movingItem.left = left;
movingItem.top = top;
movingItem.direction = direction;
}
<블록 고정 함수>
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 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 checkEmpty(target){
if(!target || target.classList.contains("seized")){
return false;
}
return true;
}
<블록 이동 및 방향 전환 함수>
// 블록을 이동시키는 함수
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("keydown",e=>{
switch(e.keyCode){
case 39:
moveBlock("left",1);
break;
case 37:
moveBlock("left",-1);
break;
case 40:
moveBlock("top",1);
break;
case 38:
changeDirection();
break;
case 32:
dropBlock();
break;
default:
break;
}
})
restartButton.addEventListener("click",()=>{
playground.innerHTML = "";
gameText.style.display = "none";
init()
})
여기서 부턴 css 파일에 대한 내용들이다.
/* 전역 스타일 */
* {
margin: 0;
padding: 0;
}
/* 게임 오버 텍스트 스타일 */
.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-top: 2rem;
}
/* body 스타일 */
body {
height: 100%;
overflow: hidden;
}
/* 리스트 스타일 */
ul {
list-style: none;
}
/* playground 스타일 */
.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;
font-size: 12px;
}
/* 블록 스타일 */
.square {
background: #2c82c9;
}
.bar {
background: salmon;
}
.tree {
background: #67c23a;
}
.zee {
background: #e6a23c;
}
.elLeft {
background: #8e44ad;
}
.elRight {
background: #16a085;
}
해당 코드에 내용은 아래 git을 참고하길 바랍니다.
[github]
https://github.com/MinWook6457/Study_JavaScript/tree/master/Tetris
GitHub - MinWook6457/Study_JavaScript
Contribute to MinWook6457/Study_JavaScript development by creating an account on GitHub.
github.com
[Refor to DevLee]
https://www.youtube.com/watch?v=1lNy2mhvLFk
'자바 스크립트' 카테고리의 다른 글
[WEB] www.google.com 접속 시 발생하는 흐름 (0) | 2024.03.25 |
---|