개발/JavaScript

[JavaScript] 허접한 몬스터 때려잡기 게임 만들기

xuwon 2024. 9. 11. 19:14

현재 JavaScript의 기초 부분을 학습 중이다.
document.createElement() 메소드를 활용하여 몬스터 잡기 게임을 만들어 보았다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #container {
            width: 500px; /* 고정된 너비 */
            height: 500px; /* 고정된 높이 */
            position: relative; /* 자식 요소들의 위치 기준 */
            border: 2px solid rgb(255, 158, 158); /* 경계 확인용 */
            overflow: hidden; /* 화면 밖으로 넘어가는 요소 숨기기 */
        }
    </style>
</head>
<body>
    <div id = "container"></div>

    <script src="monster1.js"></script>
</body>
</html>

우선 HTML 코드는 이렇게 작성해 보았다. containerstyle은 추후에 작성하겠지만 monster의 움직임을 구현하기 위해 작성하였다.

const container = document.getElementById("container");

// 몬스터 모양 만들기
const monster = document.createElement("div");
monster.textContent = "*_*";
monster.style.width = "50px";
monster.style.height = "50px";
monster.style.backgroundColor = "rgb(255, 158, 158)";
monster.style.display = "flex";
monster.style.justifyContent = "center";
monster.style.alignItems = "center";

container.appendChild(monster);

우선 containergetElementById()를 통해 container 변수에 할당해 주었다.

그 다음으로는 몬스터 모양을 만들었다. (그냥 사진을 쓸 걸 그랬나... 대충 만드느라 그냥 네모로 만들었다.)
document.createElement() 를 사용해서 div 태그를 생성해 주었고, textContent 로 표정을 만들어주었다.
그리고 display: flex; 를 이용해서 표정을 가운데로 위치시켰다.

monster

나름.. 귀여운 몬스터 생성

다음으로는 변수를 선언 및 초기화하고, 입력을 받아주는 부분을 작성했다.
attackDamage 는 사용자에게 prompt 로 입력을 받는다. 한 번 때릴 때마다 몬스터에게 들어가는 데미지.
parseInt() 로 입력값을 정수로 변환하여 할당하였다.

지금 생각해보니 attackCount 는 화면에 넣질 않았는데... 빼는 걸 깜빡했다.

// 입력 받기
let monsterHP = 100;
let attackDamage = parseInt(prompt("1회 공격시 데미지는? (양의 정수)"));
let attackCount = 0;

 

그리고 이벤트 리스너 함수를 만들었다.
몬스터를 클릭하면 공격할 수 있도록!

// 이벤트 리스너 함수 설정
const attackFunction = () => {
    attackCount++;
    monsterHP -= attackDamage;

    const attackText = document.createElement("p");
    attackText.textContent = `Attack! : ${-attackDamage}`;
    container.appendChild(attackText);
    attackText.style.color = "red";

    if (monsterHP <= 0) {
        container.removeChild(attackText);
        container.removeChild(monster);

        const h2 = document.createElement("h2");
        h2.textContent = `몬스터 잡기 완료! 수고하셨습니다.`;
        h2.style.color = "green";
        container.appendChild(h2)
    } else {
        setTimeout(() => {
            container.removeChild(attackText);
        }, 300); // 0.3초 뒤 삭제
    }
}

몬스터를 한 대씩 때릴 때마다 attackDamage만큼 monsterHP가 깎인다.

그리고 document.createElement()p 태그를 하나 생성해서 container.textContent() 로 어택 메시지를 지정하였고, appendChild()container 에 넣어주었다.

해당 p 태그는 때릴 때마다 생겼다가 0.3초 뒤 삭제되도록 setTimeout() 을 사용하여 container.removeChild(attackText) 를 작성하였다.

monsterHP <= 0 인 경우 attackTextmonster 를 화면에서 제거해 주고,
몬스터 잡기 완료 텍스트를 보여주도록 똑같이 createElement(), textContent, appenChild()를 사용하여 작성하였다.

monsterHP > 0 인 경우엔 attackText 0.3초 뒤 삭제!
조건문에 이걸 넣은 이유는, 몬스터 잡기가 완료 됐을 때도 0.3초 뒤 attackText 가 없어지니까 몬스터 잡기 완료 텍스트랑 겹쳐서 안 예뻤다.

함수 작성 끝!

if (attackDamage > 0) {
    monster.addEventListener('click', attackFunction);
} else {
    alert("잘못 입력하셨습니다. 게임을 취소합니다.")
    container.removeChild(monster);
    container.style.border = "none";
}

사용자에게 입력 받은 attackDamage 의 유효성 검사도 진행하였다.
유효하지 않은 값을 받은 경우엔 게임을 취소하도록.

.
.
.
.
.

그리고 마지막으로 몬스터가 이리저리 움직이도록 만들었는데, 이게 진짜 헷갈리고 짜증났다.

#container {
    width: 500px; /* 고정된 너비 */
    height: 500px; /* 고정된 높이 */
    position: relative; /* 자식 요소들의 위치 기준 */
    border: 2px solid rgb(255, 158, 158); /* 경계 확인용 */
    overflow: hidden; /* 화면 밖으로 넘어가는 요소 숨기기 */
}

우선 container 에 스타일을 적용해 주었다.

그냥 화면 넓이만큼 하고 싶었는데 잘 설정이 안되고 몬스터가 화면을 자꾸 벗어나서... 너비랑 높이는 지정해 주었다.

그리고 position: relative; 로 설정하였는데,
container 의 자식 요소인 monsterpositionabsolute를 지정하여 좌표대로 자유롭게 위치를 제어하기 위해서이다. 
(부모 요소에 position 을 설정하지 않을 경우 뷰포트 기준으로 자식 요소 위치가 지정됨!)

border 은 그냥 경계를 확인하기 위해 만들었다. overflow 역시 화면 밖으로 나가는지 아닌지 확인하려고 설정하였다.

// 화면 크기
const screenWidth = container.offsetWidth;
const screenHeight = container.offsetHeight;

// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

function moveMonster() {
    xPosition += xSpeed;
    yPosition += ySpeed;

    // X축 경계에 도달했을 때 반대 방향으로 전환
    if (xPosition <= 0 || xPosition >= screenWidth - 50) {
        xSpeed = -xSpeed; // 속도 반전
    }

    // Y축 경계에 도달했을 때 반대 방향으로 전환
    if (yPosition <= 0 || yPosition >= screenHeight - 50) {
        ySpeed = -ySpeed; // 속도 반전
    }

    // 몬스터의 위치 업데이트
    monster.style.left = xPosition + "px";
    monster.style.top = yPosition + "px";
}

// 40ms마다 moveMonster 함수 실행
// 40ms마다 몬스터 이동
setInterval(moveMonster, 40);

 

먼저 몬스터가 지정된 화면 밖으로 나가면 안되므로, 화면 크기를 지정해 주었다. (containerwidthheight 를 그대로 가져왔다.)

그리고 초기 x, y 위치와 속도를 지정해 주었는데,

// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

처음에는 xSpeedySpeed 에 5 라는 같은 값을 주었는데, 같은 방향으로만 움직여서 어색했다.

그래서 랜덤값을 주었다.
Math.random() 은 0부터 1까지의 값을 랜덤으로 출력한다. 따라서 xSpeedySpeed는 1에서 7까지의 랜덤 값을 갖게 된다!

 

xPositionyPosition 은 말 그대로 초기 위치이다. 몬스터가 처음에 어디에 있을지 설정해 준 것이고, 나는 맨 왼쪽 꼭대기에 위치시켰다.


이 코드를 그림으로 그려보겠다. (오로지 나의 이해를 위해...)

xSpeed = 3, ySpeed = 4 인 상태라고 가정하자.

// moveMonster()
xPosition += xSpeed;
yPosition += ySpeed;

그렇다면 moveMonster() 의 해당 코드에 의해, xPosition = 3, yPosition = 4 가 될 것이다.

그리고 경계에 닿지 않았으니까 바로

// 몬스터의 위치 업데이트
monster.style.left = xPosition + "px";
monster.style.top = yPosition + "px";

해당 코드로 넘어간다.


그러면 monster.left = 3px;, monster.top = 4px; 이 된다! (position: absolute;)
left 에서 3px 만큼 떨어지고, top 에서 4px 만큼 떨어진 곳으로 이동하게 된다.

따라서 몬스터는 이렇게 대각선 선을 따라서 이동하게 된다.
그렇기에 저 대각선의 각도는 당연히, xSpeedySpeed에 따라서 달라지겠지?

그리고 40ms 후 다시 함수를 실행하여 또 몬스터를 이동시킨다.
그러다가 벽에 붙으면 어찌 될까?

// X축 경계에 도달했을 때 반대 방향으로 전환
if (xPosition <= 0 || xPosition >= screenWidth - 50) {
    xSpeed = -xSpeed; // 속도 반전
}

// Y축 경계에 도달했을 때 반대 방향으로 전환
if (yPosition <= 0 || yPosition >= screenHeight - 50) {
    ySpeed = -ySpeed; // 속도 반전
}

벽에 붙게 되면 속도가 반전되어 반대로 가게 된다!!

xPosition 이 450 (screenWidth(500) - 50 (monster의 width)) 보다 커지면... 오른쪽 경계를 벗어나게 된다. 
yPosition도 역시 마찬가지.

이 정도 하면 몬스터가 경계 내에서 요리조리 움직이는 이유를 설명할 수 있을 것 같다.

 

그리고 마지막으로 monsterstyle 을 추가 지정해 주었다.

monster.style.position = "absolute";
monster.style.pointerEvents = "all"; // div 전체가 클릭 가능하게 만듦

자유롭게 제어하기 위해 position: absolute; 를, 그리고 div 태그 전체를 클릭할 수 있도록 pointer-Events: all;을 지정했다!

 

결과는 조금 허접하지만 아무튼 몬스터 때려잡기 완료~!!...

 

해보다 실패한 것은...
몬스터의 숫자를 지정해서 여러 마리를 보여주고 싶었는데, clone를 복제해도 원래 monster 밖에 함수 적용이 안됐다. (왜 그러지...??)

그래서 두 번째로 시도한 건
몬스터 숫자를 지정해서 한 마리 없앨 때마다 한 마리씩 다시 나타나게 하고 싶었다.

그래서 첫 번째 몬스터가 죽고 container.removeChild(monster) 를 한 뒤에
다시 container.appendChild(monster) 를 줬는데, 함수 적용은 되는데 누르자마자 또 바로 죽어버리더라...;; (왜지 이건)


나중에 시간이 되면 해당 기능들도 구현해 보고 싶다. 

끄읕~~!!