HTML과 자바스크립트로 직접 만드는 간단한 미니골프게임

외부 라이브러리 없이 순수 HTML, CSS, JavaScript만을 활용해 브라우저에서 바로 즐길 수 있는 미니골프 게임을 제작하는 방법을 살펴봅니다.

웹 브라우저에서 가볍게 즐길 수 있는 미니골프 게임을 직접 구현하면 프론트엔드 이벤트 처리와 기본 물리 연산 원리를 자연스럽게 익힐 수 있습니다. 별도의 게임 엔진이나 외부 라이브러리 설치 없이 HTML 캔버스와 자바스크립트 수학 함수만으로 완성도 높은 미니게임을 만들 수 있습니다.

개발 환경 및 사전 준비

이 프로젝트는 텍스트 에디터와 웹 브라우저만 있으면 누구나 바로 시작할 수 있습니다. 별도의 백엔드 서버나 빌드 도구가 필요하지 않으며, 세 개의 파일을 같은 디렉토리에 생성하여 서로 연결하는 방식으로 구성합니다.

  • 프로젝트 구성 파일 목록입니다. 각 파일은 독립된 역할을 담당하며 브라우저에서 통합 실행됩니다.
  • index.html: 게임 캔버스와 기본 레이아웃을 정의하는 마크업 파일
  • style.css: 게임 화면을 정렬하고 시각적 요소를 꾸미는 스타일시트 파일
  • app.js: 공의 움직임, 마우스 드래그 조작, 홀컵 충돌을 처리하는 자바스크립트 파일

HTML 마크업 구조 설계

관련 글: 순수 웹 기술 활용

게임 화면의 뼈대가 되는 마크업은 단순하게 유지합니다. HTML5의 canvas 요소가 게임 필드 역할을 수행하며, 현재 점수나 타수를 표시할 수 있는 정보 영역을 함께 배치합니다.

index.htmlHTML

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>미니골프 게임</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="game-container">
        <h1>간단한 미니골프 게임</h1>
        <div class="score-board">타수: <span id="stroke-count">0</span></div>
        <canvas id="golfCanvas" width="600" height="400"></canvas>
    </div>
    <script src="app.js"></script>
</body>
</html>

CSS 스타일 적용

캔버스가 화면 중앙에 깔끔하게 정렬되도록 레이아웃을 설정합니다. 초록색 잔디 느낌을 주는 배경색과 직관적인 버튼 배치를 통해 시각적인 몰입감을 높여줍니다.

style.cssCSS

body {
    margin: 0;
    padding: 0;
    background-color: #1a1a1a;
    color: #ffffff;
    font-family: 'Malgun Gothic', sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}
.game-container {
    text-align: center;
}
canvas {
    background-color: #2e8b57;
    border: 4px solid #ffffff;
    border-radius: 8px;
    box-shadow: 0 8px 16px rgba(0,0,0,0.4);
}

자바스크립트 물리 엔진 구현

마우스 드래그 거리와 방향을 계산해 골프공에 속도 벡터를 부여합니다. 매 프레임마다 마찰력에 의해 공의 속도가 줄어들며, 벽이나 장애물에 부딪히면 튕겨 나가는 간단한 반사 법칙을 적용합니다.

app.jsJAVASCRIPT

const canvas = document.getElementById('golfCanvas');
const ctx = canvas.getContext('2d');
const strokeSpan = document.getElementById('stroke-count');

let strokes = 0;
let isAiming = false;
let startX = 0;
let startY = 0;

let ball = {
    x: 100,
    y: 200,
    radius: 10,
    vx: 0,
    vy: 0,
    friction: 0.98,
    isMoving: false
};

const hole = {
    x: 500,
    y: 200,
    radius: 15
};

canvas.addEventListener('mousedown', (e) => {
    if (ball.isMoving) return;
    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;
    
    const dist = Math.hypot(mouseX - ball.x, mouseY - ball.y);
    if (dist < ball.radius * 2) {
        isAiming = true;
        startX = ball.x;
        startY = ball.y;
    }
});

canvas.addEventListener('mouseup', (e) => {
    if (!isAiming) return;
    isAiming = false;
    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;
    
    ball.vx = (startX - mouseX) * 0.15;
    ball.vy = (startY - mouseY) * 0.15;
    ball.isMoving = true;
    strokes++;
    strokeSpan.textContent = strokes;
});

function update() {
    if (ball.isMoving) {
        ball.x += ball.vx;
        ball.y += ball.vy;
        ball.vx *= ball.friction;
        ball.vy *= ball.friction;
        
        if (Math.hypot(ball.vx, ball.vy) < 0.2) {
            ball.vx = 0;
            ball.vy = 0;
            ball.isMoving = false;
        }
        
        if (ball.x - ball.radius < 0 || ball.x + ball.radius > canvas.width) {
            ball.vx *= -1;
        }
        if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) {
            ball.vy *= -1;
        }
        
        const distToHole = Math.hypot(ball.x - hole.x, ball.y - hole.y);
        if (distToHole < hole.radius) {
            alert('홀인원 성공! 총 타수: ' + strokes);
            resetGame();
        }
    }
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    ctx.beginPath();
    ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
    ctx.fillStyle = '#111111';
    ctx.fill();
    ctx.closePath();
    
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fillStyle = '#ffffff';
    ctx.fill();
    ctx.closePath();
}

function loop() {
    update();
    draw();
    requestAnimationFrame(loop);
}

function resetGame() {
    ball.x = 100;
    ball.y = 200;
    ball.vx = 0;
    ball.vy = 0;
    ball.isMoving = false;
    strokes = 0;
    strokeSpan.textContent = strokes;
}

loop();

자주 발생하는 오류 및 해결 방법

관련 글: 미니골프게임 구현 방법

캔버스 좌표 계산 시 브라우저 스크롤이나 CSS 크기 조정에 따라 마우스 위치가 어긋나는 현상이 종종 발생합니다. getBoundingClientRect () 메서드를 활용해 캔버스 내부 상대 좌표를 정확히 산출해야 오작동을 막을 수 있습니다.

공이 멈추지 않고 계속 움직여요.

공이 멈추지 않고 계속 미끄러지는 현상은 마찰력 계수가 너무 낮거나 0에 가까울 때 발생합니다. friction 값을 0.95에서 0.98 사이로 설정하면 자연스럽게 감속합니다.

마우스 드래그 방향과 반대로 공이 날아갑니다.

마우스 드래그 방향과 반대로 공이 발사되는 것은 새총 원리와 동일한 정상적인 동작입니다. 당기는 반대 방향으로 힘 벡터가 작용하도록 코드가 작성되었기 때문입니다.

완성 코드 예제

UtilLog Web Example
코드를 수정한 뒤 실행을 누르면 Result에 반영됩니다.

마무리

HTML, CSS, 자바스크립트의 기본 조합만으로도 충분히 재미있는 미니골프게임을 브라우저 상에서 구현할 수 있습니다.

기본적인 충돌 감지와 벡터 연산을 응용하여 장애물이나 경사로를 추가해 나만의 게임으로 확장해 보세요.

댓글 남기기

이메일 주소는 공개되지 않습니다.