enemyRain 게임은 하늘에서 유령이 지속적으로 떨어지는 게임이다. 나는 유령을 피해야하고, 유령을 맞으면 생명력이 깎이게 되어, 모두 깎이면 게임이 종료되는 식으로 구현을 하였다.
🚀 HTML
<!DOCTYPE html> <html lang="ko"> <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>enemyRain</title> <link rel="stylesheet" href="css/style.css" /> <script src="https://kit.fontawesome.com/4e5b2f86bb.js" crossorigin="anonymous"></script> </head> <body> <div class="container"> <div class="topBox"> <div id="time"> 0 seconds </div> <div id="lastMsg"> </div> <div id="heart"> <i class="fa-solid fa-heart"></i> <i class="fa-solid fa-heart"></i> <i class="fa-solid fa-heart"></i> </div> </div> <div class="imgBox"> <div id="bg"> <img src="img/bg.png" alt="bg"> <img src="img/over.png" alt="over" id="over"> </div> <div id="hero"></div> </div> </div> <script src="js/main.js"></script> </body> </html>
🚀 CSS
*{ box-sizing: border-box; } body { margin: 0 auto; } .container{ display: flex; flex-direction: column; align-items: center; } .topBox{ margin: 20px; width: 100%; display: flex; justify-content: space-evenly; } .imgBox{ position: relative; }
🚀 CSS
#hero{ background-image: url(../img/hero.png); width:35px; height:55px; position: absolute; bottom:5px; left:50%; transform:translateX(-50%); } .heroRight{ background-position:35px; } .heroLeft{ background-position:70px; } .heroStop{ background-position:0; }
🚀 javascript
//키보드 감지 window.addEventListener("keydown", (e) => { if (e.key === 'ArrowLeft' && hero.offsetLeft>40) { leftMove() } else if (e.key === 'ArrowRight' && hero.offsetLeft<760) { rightMOve() } }); //좌 let leftMove = ()=>{ x = hero.offsetLeft - 20; hero.style.left = `${x}px`; hero.className='heroLeft' } //우 let rightMOve = ()=>{ x = hero.offsetLeft + 20; hero.style.left = `${x}px`; hero.className='heroRight' } //키보드 떼면 히어로 원위치모습 window.addEventListener("keyup", (e)=>{ hero.className='heroStop' });
javascript에서 keydown을 통해 무슨 키인지, 그때 히어로가 화면 범위 안에 있는지 확인한 후 그에 맞는 함수를 실행하도록 하였다.
히어로의 좌표를 offsetLeft로 가져와서, 좌표에 움직여질 위치값을 더해준 후 직접 스타일에 주었다. 또한 해당 방향의 클래스를 입혀 정상적인 해당 이미지 스프라이트가 보이도록 하였다.
키를 누르는 동안 이 과정은 반복 되며, 더이상 누르지 않는다면 keyup으로 감지해 herostop 클래스를 입혀 해당 이미지 스프라이트가 보이도록 하였다.
🚀 javascript
//600밀리초마다 유령 생성후 interval에 넘겨줌 let copyInterval = setInterval(()=>{ let copyEnermy = createEnemy(); moveInterval(copyEnermy) },600) //유령 생성 let createEnemy = ()=>{ let random = Math.floor(Math.random()*(bg.offsetWidth-66)+22); let copyEnermy = document.createElement('div'); copyEnermy.className='enemy'; copyEnermy.style.left = `${random}px`; imgBox.appendChild(copyEnermy); return copyEnermy } //20미리초마다 무브, 충돌감지 let moveInterval = function(enemy){ setInterval(()=>{ enemyMove(enemy) crash(enemy) },10) } //유령 무브 함수 function enemyMove(ene) { if(ene.offsetTop>540){ ene.classList.add('enemyDeath') setTimeout(() => ene.classList.remove('enemy'), 300); return clearInterval(moveInterval) } y = ene.offsetTop + 10; ene.style.top = `${y}px`; } //유령 충돌감지 함수 function crash(ene) { if(hero.offsetTop<=ene.offsetTop+15 && ene.offsetLeft-22<=hero.offsetLeft && hero.offsetLeft<=ene.offsetLeft+22){ ene.classList.remove('enemy') deleteHeart() return clearInterval(moveInterval) } } //충돌시 하트감소, 하트0은 게임오버 function deleteHeart() { heart[0].parentNode.removeChild(heart[0]); if(heart.length===0) { timeDiv.innerHTML=''; lastMsg.innerHTML=`${second}초 생존했습니다!`; gameOver.style.visibility = ''; clearInterval(time); clearInterval(copyInterval); } }
🚀 CSS
.enemy{ background-image: url(../img/enemy.png); width:44px; height:55px; position: absolute; top:2px; left:50%; transform:translateX(-50%); } .enemyDeath{ background-position:99%; top:30px; } .fa-heart{ color:red; font-size:20px; }
setInterval을 통해 600밀리초마다 creatEnemy를 통해 유령을 생성하고 moveInterval에 넘겨주었다.
createEnemy 함수에선 배경 범위안의 랜덤한 좌표를 생성해 유령을 만들고 좌표를 넣어줬고, 넘겨받은 유령을 moveInterval로 10밀리초마다 enemyMove와 crash 함수를 실행시켰다.
enemyMove에서 유령의 위치를 10px씩 하락시키며 만약 유령의 좌표가 바닥에 가까워진다면 enemyDeath 클래스를 주어 죽은 이미지로 만들고, 300밀리초 뒤에 enemy클래스를 제거시켜 화면 상에서 없애주었다.
crash함수에서는 유령과 히어로의 top,left좌표를 통해 상,좌,우 가 겹치는지 확인하였고 겹친다면 enemy함수를 제거해 유령을 화면에서 없앤 후, deleteHeart함수를 통해 생명력을 감소시켰다.
enemyMove나 crash에서 유령이 사라진다면, moveInterval을 통해 해당 유령에 대해 주기적으로 실행시켜주던 interval을 멈춰줬다.
deleteHeart함수에서는 미리 생명력 태그를 읽어와 저장한 heart들을 하나씩 감소시키고 만약 heart가 하나도 없다면, 생존시간을 출력하고 미리 지정한 gameOver 배경 이미지를 보여줬다. 그리고 주기적으로 실행시켜주던 time과 copyInterval을 끝내, 게임을 종료시켰다.
🚀 CSS
*{ box-sizing: border-box; } body { margin: 0 auto; } .container{ display: flex; flex-direction: column; align-items: center; } .topBox{ margin: 20px; width: 100%; display: flex; justify-content: space-evenly; } .imgBox{ position: relative; } #over{ position: absolute; left:0; width:100%; height: 100%; z-index: 1000; } #hero{ background-image: url(../img/hero.png); width:35px; height:55px; position: absolute; bottom:5px; left:50%; transform:translateX(-50%); } .heroRight{ background-position:35px; } .heroLeft{ background-position:70px; } .heroStop{ background-position:0; } .enemy{ background-image: url(../img/enemy.png); width:44px; height:55px; position: absolute; top:2px; left:50%; transform:translateX(-50%); } .enemyDeath{ background-position:99%; top:30px; } .fa-heart{ color:red; font-size:20px; } #time{ font-size:20px; } #lastMsg{ font-size:20px; font-weight: bold; }
🚀 javascript
const hero = document.querySelector('#hero') const enemy = document.querySelector('.enemy') const bg = document.querySelector('#bg') const imgBox = document.querySelector('.imgBox') const timeDiv = document.getElementById('time') const heart =document.getElementsByTagName('i') const gameOver = document.getElementById('over') const lastMsg = document.getElementById('lastMsg') gameOver.style.visibility = 'hidden'; //시간 출력 let second = 0; let time =setInterval(()=>{ second++; timeDiv.innerHTML=`${second} seconds` },1000) //키보드 감지 window.addEventListener("keydown", (e) => { if (e.key === 'ArrowLeft' && hero.offsetLeft>40) { leftMove() } else if (e.key === 'ArrowRight' && hero.offsetLeft<760) { rightMOve() } }); //좌 let leftMove = ()=>{ x = hero.offsetLeft - 20; hero.style.left = `${x}px`; hero.className='heroLeft' } //우 let rightMOve = ()=>{ x = hero.offsetLeft + 20; hero.style.left = `${x}px`; hero.className='heroRight' } //키보드 떼면 히어로 원위치모습 window.addEventListener("keyup", (e)=>{ hero.className='heroStop' }); //600밀리초마다 유령 생성후 interval에 넘겨줌 let copyInterval = setInterval(()=>{ let copyEnermy = createEnemy(); moveInterval(copyEnermy) },600) //유령 생성 let createEnemy = ()=>{ let random = Math.floor(Math.random()*(bg.offsetWidth-66)+22); let copyEnermy = document.createElement('div'); copyEnermy.className='enemy'; copyEnermy.style.left = `${random}px`; imgBox.appendChild(copyEnermy); return copyEnermy } //20미리초마다 무브, 충돌감지 let moveInterval = function(enemy){ setInterval(()=>{ enemyMove(enemy) crash(enemy) },10) } //유령 무브 함수 function enemyMove(ene) { if(ene.offsetTop>540){ ene.classList.add('enemyDeath') setTimeout(() => ene.classList.remove('enemy'), 300); return clearInterval(moveInterval) } y = ene.offsetTop + 10; ene.style.top = `${y}px`; } //유령 충돌감지 함수 function crash(ene) { if(hero.offsetTop<=ene.offsetTop+15 && ene.offsetLeft-22<=hero.offsetLeft && hero.offsetLeft<=ene.offsetLeft+22){ ene.classList.remove('enemy') deleteHeart() return clearInterval(moveInterval) } } //충돌시 하트감소, 하트0은 게임오버 function deleteHeart() { heart[0].parentNode.removeChild(heart[0]); if(heart.length===0) { timeDiv.innerHTML=''; lastMsg.innerHTML=`${second}초 생존했습니다!`; gameOver.style.visibility = ''; clearInterval(time); clearInterval(copyInterval); } }
실제로 이미지 스트라이프를 사용할 때 많이 생소하였으나, 출력 영역과 그 위치를 계속해서 잡다보니 어떻게 사용하는지 익히게 되었다. 이미지 스트라이프는 많은 이미지를 호출하지 않고, 하나의 이미지에서 위치를 움직이며 여러 이미지를 사용하므로 데이터와 시간 모두 절약되는 방법이므로 현업에서도 많이 사용한다고 한다.!
히어로와 유령이 움직이는데 나는 offset을 사용하여 해당 위치에서 스타일을 주었는데, tansform을 사용하여 움직이는 방법도 있었다. 해당 코드를 사용한 분의 게임화면을 보니 움직임이 더욱 부드럽고 좌우 전환이 쉽게 되었다. 같은 것을 구현하는데도 어떠한 함수를 사용한지에 따라 퀄리티가 달라지는것을 보고 무언가를 구현하는데 어떤 함수를 사용해야 하는지에 대한 선택의 중요성을 느꼈다.
또한 처음에 유령을 배치한 후, 그 유령을 내려오고 죽는 함수를 만들고, 또 그 유령과 히어로가 부딪히는것을 감지하고 동작하는 함수를 만들었다. 이 함수들을 구현한 후에야 copy함수를 통해 유령들을 지속적으로 만드는 함수를 구현 했었는데, 나중에 setInterval을 clear할 때 기존 하나의 유령과 후에 카피되는 유령과의 호출이 다르다보니 오류가 났다.
인자를 받도록 변경해야했고, 후에는 처음의 유령을 출력하고 카피를 하는것 보다, 처음부터 카피된 유령들로만 게임이 동작하는것이 맞기에 결국 여러 코드를 제거하고 수정해야 했다. 결론은, 처음부터 큰 틀을 만들고 제대로된 순서를 정해야 나중에 힘들지 않다는 것이었다. 계속 무언가를 구현할수록 서로의 연결성은 더욱 강해지는데 틀과 순서가 없이 구현하다보면 함수들을 결합할 때 오류가 생길수 있고, 수정하는 과정들이 추가되고 코드가 지저분해질수 있다는 것을 깨달았다.
마지막으로, 일관성과 직관성이 없이 코딩한 점이 아쉬웠다. 코드중 한 부분을 보면, 유령의 이미지는 44라서 저런식으로 계산을 했다. 여러 부분에서 저렇게 내가 이미지의 크기를 계산해서 그대로 숫자를 입력한 부분이 많다. 만약, 다른사람이 이러한 코드를 보았을때 '왜 66이야? 왜 22이야? 무슨숫잔데 이게?' 라는 생각을 갖게 될 것이다. 나는, 직접 계산이 아닌, 함수나 다른 방법을 통해 유령의 width를 가져와서 계산을 하는것이 이러한 코드에 더욱 직관성을 부여했을 것이란 생각이 들었다.
또한 배경화면의 경우에는 offsetWidth를 통해 너비를 가져왔는데, 어떠한 방법을 쓸 때, 일관되게 코딩하는 것이 맞다고 느꼈다.
이 게임을 구현하는데에 여러 생각도 하게 되고, 완성하여 코드를 돌아보았을 때 아쉬운 부분들도 많이 보였다. 하지만 이렇게 느끼고 깨달은 점들을 잊지 않고 다음 프로젝트 때는 더욱 완성도 높은 코딩을 해야겠다.