Skip to content

Latest commit

 

History

History
668 lines (501 loc) · 20.5 KB

File metadata and controls

668 lines (501 loc) · 20.5 KB

Tower Defense - 코드 구조 & 개념 가이드

전체 구조 한눈에 보기

game.js (약 2830줄)
│
├── [1~55]      캔버스 초기화 & 반응형 크기
├── [57~100]    게임 상태 변수들 (v2.0 신규 포함)
├── [102~265]   SoundManager (Web Audio API 프로시저럴 사운드)
├── [267~310]   맵 & 경로 시스템
├── [312~410]   앰비언트 시스템 (잔디/경로 디테일/파티클 생성)
├── [412~525]   타워 종류 정의 & 클래스
├── [527~620]   적/투사체/파티클/텍스트/충격파/지면마크 클래스
├── [622~700]   웨이브 & 난이도 시스템
├── [702~760]   타워 배치/업그레이드/판매 (사운드 포함)
├── [762~810]   적 사망 파티클 + 충격파 + 스크린쉐이크
├── [812~1100]  ★ update() - 핵심 게임 로직 (+ 새 이펙트 업데이트)
├── [1102~1115] 그리기 헬퍼 함수
├── [1117~1720] ★ draw() - 화면 렌더링 (스크린쉐이크, 앰비언트, 글로우, UI 그라디언트)
├── [1722~1870] 타워/적 그리기 함수
├── [1872~2530] 입력 처리 (마우스, 터치, 키보드, 음소거)
├── [2532~2575] 게임 리셋 (v2.0 변수 초기화 포함)
└── [2577~2830] 게임 루프 (스크린쉐이크/웨이브전환/게임오버 타이머)

1. 게임 루프 (심장)

위치: game.js:1572~1582

gameLoop() 호출 (60fps)
    ├── update(dt)   ← 모든 계산 (위치, 충돌, AI)
    ├── draw()        ← 화면에 그리기
    └── requestAnimationFrame(gameLoop)  ← 다음 프레임 예약

핵심 개념: dt (Delta Time)

const dt = Math.min((time - lastTime) / 1000, 0.05);
  • 이전 프레임과 현재 프레임 사이의 경과 시간 (초)
  • 60fps면 dt ≈ 0.016초
  • 모든 이동/타이머에 dt를 곱해서 프레임 속도에 독립적으로 동작
  • 느린 컴퓨터든 빠른 컴퓨터든 같은 속도로 게임 진행

2. 맵 & 경로 시스템

위치: game.js:59~120

격자(Grid)

20칸(가로) x 14칸(세로) 격자
각 칸의 값:
  0 = 잔디 (타워 배치 가능)
  1 = 길 (적이 지나가는 곳)
  2 = 입구
  3 = 출구

경로(Waypoints)

const waypoints = [
  { x: -1, y: 2 }, // 왼쪽 밖에서 시작
  { x: 4, y: 2 }, // →  오른쪽으로
  { x: 4, y: 5 }, // ↓  아래로
  { x: 10, y: 5 }, // →  오른쪽으로
  { x: 10, y: 2 }, // ↑  위로
  { x: 16, y: 2 }, // →  오른쪽으로
  { x: 16, y: 7 }, // ↓  아래로
  { x: 6, y: 7 }, // ←  왼쪽으로
  { x: 6, y: 10 }, // ↓  아래로
  { x: 14, y: 10 }, // →  오른쪽으로
  { x: 14, y: 12 }, // ↓  아래로
  { x: 20, y: 12 }, // →  오른쪽 밖으로 (출구)
];

S자 형태로 꺾이는 경로. 적은 이 좌표들을 순서대로 따라감.

경로를 격자에 새기는 원리 (carvePath)

두 waypoint 사이를 잇는 직선을 격자에 1로 표시:

  • 같은 y좌표면 → 가로로 칸을 채움
  • 같은 x좌표면 → 세로로 칸을 채움

업그레이드 팁

경로를 바꾸고 싶으면 waypoints 배열만 수정하면 됨! 더 복잡한 경로 = 타워 배치 전략이 다양해짐


3. 타워 시스템

위치: game.js:122~214

타워 종류 5가지

속성 화살 대포 얼음 번개
비용 50G 100G 75G 130G 90G
데미지 8 30 5 18 4
사거리 3.0칸 2.8칸 2.5칸 3.5칸 2.5칸
공격속도 0.4초 1.2초 0.6초 0.8초 0.7초
특수효과 없음 범위(스플래시) 감속 50% 없음 독 DOT 4초
총알속도 8 5 6 12 5
투사체 화살선 포탄+연기 얼음결정 전기줄기 독방울

타워 클래스 핵심 속성

class Tower {
    col, row          // 격자 위치
    x, y              // 픽셀 위치 (격자 중앙)
    level             // 현재 레벨 (1~5)
    cooldown          // 다음 발사까지 남은 시간
    angle             // 포탑이 바라보는 각도
    target            // 현재 조준 중인 적
    totalDamage       // 누적 데미지 (통계용)
}

업그레이드 공식

레벨업 할 때마다:
  데미지  = 기본 × (1 + (레벨-1) × 0.5)    → 레벨5면 3배
  사거리  = 기본 + (레벨-1) × 0.3칸          → 레벨5면 +1.2칸
  공격속도 = 기본 × 0.88^(레벨-1)            → 레벨5면 약 60%로 빨라짐
  업그레이드 비용 = 기본비용 × 0.6 × 현재레벨
  판매가 = 총 투자금 × 60%

타워 AI (타겟 선정)

위치: game.js:524~539

1. 사거리 안의 모든 적을 검색
2. "출구에 가장 가까운 적"을 우선 공격
   → pathIndex(경로 진행도)가 높은 적 선택
3. 타겟을 향해 포탑 각도를 회전
4. 쿨다운이 0이 되면 발사

이 전략을 "가장 진행된 적 우선(First/Furthest)" 이라고 함. 다른 전략: 가장 가까운 적, HP 가장 낮은 적, HP 가장 높은 적 등


4. 적(Enemy) 시스템

위치: game.js:216~236

적 클래스 핵심 속성

class Enemy {
    hp, maxHp         // 체력
    speed, baseSpeed  // 이동속도
    goldValue         // 처치 시 골드 보상
    type              // 'normal', 'fast', 'tank', 'boss'
    pathIndex         // 현재 따라가는 경로 인덱스
    x, y              // 현재 위치
    armor             // 방어력 (받는 데미지에서 차감)
    slowTimer         // 감속 남은 시간
    slowAmount        // 감속 비율 (0.5 = 50% 감속)
    poisonTimer       // 독 남은 시간
    poisonDmg         // 독 틱당 데미지
    poisonTick        // 다음 독 틱까지 남은 시간 (0.5초 간격)
    hitFlash          // 피격 시 흰색 반짝임
    size              // 그리기 크기
}

적 이동 원리

위치: game.js:486~501

매 프레임:
  1. 현재 목표 waypoint를 가져옴 (enemyPath[pathIndex])
  2. 목표까지의 거리(dx, dy)를 계산
  3. 방향을 정규화 (dx/d, dy/d) → 단위 벡터
  4. 이동속도만큼 그 방향으로 전진
  5. 목표에 도착하면 pathIndex++ → 다음 waypoint로
  6. 마지막 waypoint 도착 = 출구 도달 → 생명 감소

정규화(normalize)란?

거리 d = √(dx² + dy²)
방향 = (dx/d, dy/d)  ← 길이가 항상 1인 벡터
이동 = 방향 × 속도 × dt

이렇게 하면 어느 방향이든 동일한 속도로 이동.

적 종류별 특성

종류 등장 HP 배율 속도 배율 특수
normal 웨이브 1~ ×1.0 ×1.0 -
fast 웨이브 3~ ×0.5 ×1.6 크기 작음
tank 웨이브 5~ ×2.5 ×0.6 방어력 2
boss 5의 배수 ×6.0 ×0.45 방어력 3, 통과 시 생명 -5

5. 투사체(Projectile) 시스템

위치: game.js:238~253, 555~631

동작 원리

발사 → 매 프레임 적 추적 → 충돌 → 데미지 처리

1. 타워 위치에서 생성
2. 매 프레임: 타겟 적을 향해 이동 (유도 미사일처럼)
3. 적과의 거리 < 충돌 거리면 → 명중!
4. 명중 시:
   - 데미지 적용 (데미지 - 방어력, 최소 1)
   - 스플래시면 주변 적에게도 50% 데미지
   - 감속 효과가 있으면 적에게 감속 부여
   - 독 효과가 있으면 적에게 DOT(지속 피해) 부여
   - 적 HP ≤ 0이면 사망 처리 + 골드 획득

독(Poison) DOT 시스템

명중 시 → enemy.poisonTimer = 4초, poisonDmg = 5
    ↓
매 0.5초마다 독 데미지 적용 (방어력 감산)
    ↓
초록 기포 파티클 + 독 오라 이펙트
    ↓
4초 후 해제 (재감염 가능)
  • 스플래시로 주변 적도 독 감염 (지속시간 60%)
  • 독 데미지는 레벨업 시 기본 공식에 따라 증가

타워별 투사체 이펙트

타워 투사체 스타일
화살 얇은 직선 궤적 + 작은 화살촉
대포 큰 포탄 + 연기 잔상
얼음 회전하는 다이아몬드 결정 + 사각 파편
번개 지그재그 전기 줄기 (2중) + 전기 구체
물결치는 독방울 + 초록 드립

잔상(Trail) 효과

proj.trail.push({ x: proj.x, y: proj.y }); // 현재 위치 저장
if (proj.trail.length > 5) proj.trail.shift(); // 최대 5개만 유지

과거 5프레임의 위치를 저장해서 점점 투명하게 그리면 → 꼬리처럼 보임


6. 웨이브 & 난이도 시스템

위치: game.js:281~340

난이도 상승 공식

baseHp = 30 + wave × 15 + wave^1.5 × 5     // 지수적 증가
count  = 5 + wave × 1.5                      // 수도 증가
speed  = 1.0 + wave × 0.03                   // 미세하게 빨라짐
웨이브 HP 적 수 구성
1 ~50 6 일반 6
3 ~90 9 일반 9 + 빠른 1
5 ~140 12 일반 12 + 빠른 2 + 탱크 1 + 보스
10 ~310 20 일반 20 + 빠른 5 + 탱크 3 + 보스
20 ~747 35 일반 35 + 빠른 10 + 탱크 6 + 보스

웨이브 흐름

웨이브 사이 5초 대기 (스킵 가능)
    ↓
startWave() → 적 목록 생성 (getWaveEnemies)
    ↓
0.5~0.8초 간격으로 적을 하나씩 스폰
    ↓
모든 적 처치 또는 통과
    ↓
웨이브 클리어 보너스 (10 + wave × 5 골드)
    ↓
다음 웨이브 대기...

7. 충돌 감지

거리 계산 함수

function dist(a, b) {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}

두 점 사이의 거리 = 피타고라스 정리

사거리 판정

dist(타워, 적) ≤ 타워사거리 × 타일크기  →  사거리 안에 있음!

투사체 명중 판정

dist(투사체, 적) < 이동속도 + 적크기  →  명중!

스플래시 판정

명중 지점에서 dist(투사체, 다른적) ≤ 스플래시 범위  →  범위 피해!

8. 파티클 & 이펙트 시스템

위치: game.js:255~267, 398~418, 845~865

파티클 구조

class Particle {
    x, y       // 위치
    vx, vy     // 속도 (방향 + 크기)
    life       // 남은 수명 (0이 되면 제거)
    maxLife    // 최초 수명 (투명도 계산용)
    color      // 색상
    size       // 크기
}

파티클 업데이트 (매 프레임)

p.x += p.vx; // 이동
p.y += p.vy;
p.life -= dt; // 수명 감소
p.vx *= 0.95; // 마찰력 (점점 느려짐)
p.vy *= 0.95;

파티클 그리기

ctx.globalAlpha = p.life / p.maxLife; // 수명에 따라 투명해짐
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2); // 크기도 줄어듦

사용처

  • 피격: 작은 파티클 1개 (투사체 색상)
  • 스플래시 히트: 방사형 파티클 6개
  • 적 사망: 12~30개 폭발 (적 종류별 색상)
  • 떠오르는 텍스트: "+5G", "웨이브 클리어!" 등

9. 입력 처리

위치: game.js:1200~1540

마우스

mousemove → hoveredTile 업데이트 (어느 칸 위에 있는지)
click →
  1. UI 패널 영역? → 타워 선택 / 업그레이드 / 판매
  2. 기존 타워 위? → 업그레이드 패널 표시
  3. 빈 잔디 칸? → 타워 배치 (골드 충분하면)

터치

마우스와 동일한 로직, touchstart/touchmove/touchend 사용

키보드

1~5: 타워 선택 (5=독 타워)
Space/Enter: 웨이브 시작
U: 선택된 타워 업그레이드
S: 선택된 타워 판매
Q: 게임 속도 전환 (x1 → x2 → x3 → x1)
M: 사운드 음소거/해제
Escape: 선택 해제

10. 게임 속도 시스템

let gameSpeed = 1; // 1, 2, 3
const dt = rawDt * gameSpeed; // 모든 로직에 배속 적용
  • Q키 또는 UI 버튼 클릭으로 전환
  • x1: 기본 속도 / x2: 2배속 / x3: 3배속
  • rawDt(실제 시간)에 배속을 곱해서 모든 이동/타이머에 적용
  • 보스 경고 연출은 실제 시간(rawDt)으로 동작 (배속 영향 없음)

11. 보스 이벤트 시스템

5웨이브마다 보스 출현

웨이브 5, 10, 15, 20...
  ↓
빨간 경고 화면 2.5초 ("⚠ BOSS WAVE ⚠")
  ↓
보스 + 호위병(탱크) 함께 출현

보스 스케일링

보스 레벨 = 웨이브 / 5
HP 배율 = 6 + 보스레벨 × 2  (8배, 10배, 12배...)
호위병 수 = 보스레벨 × 2 (최대 8)
보상 = 기본 × 5 + 보스레벨 × 10

보스 비주얼

  • 붉은 맥동 오라 (1.6배 크기)
  • 양쪽 뿔 + 왕관
  • 빛나는 빨간 눈 (shadowBlur 효과)
  • 세로 슬릿 동공
  • 톱니 이빨

12. 렌더링 순서 (draw 함수)

그리는 순서가 곧 레이어 순서 (먼저 그린 것이 아래에 깔림):

1. 잔디 & 길 배경        ← 가장 아래
2. 경로 방향 화살표
3. 입구/출구 마커 (IN, OUT)
4. 타워 사거리 미리보기 (배치 시)
5. 타워들
6. 타워 레벨 별표 (★)
7. 선택된 타워 사거리 원
8. 적들 (독/감속 오라 + 체력바)
9. 투사체 (타워별 고유 이펙트)
10. 파티클 이펙트
11. 떠오르는 텍스트 (+5G 등)
12. 보스 경고 오버레이 (5웨이브마다)
13. UI 패널 (하단) + 속도 버튼
14. 업그레이드 팝업        ← 가장 위
15. 게임오버 화면

업그레이드 아이디어

코드 구조를 이해했으니, 추가할 수 있는 것들:

쉬운 난이도

  • 새 타워 종류TOWER_TYPES 배열에 추가하면 됨 (독 타워 추가 완료)
  • 새 적 종류getWaveEnemies()에 조건 추가
  • 경로 변경waypoints 배열 수정
  • 타워 최대 레벨 올리기 → 현재 5 → 원하는 만큼

중간 난이도

  • 타워 특수능력 (독 DOT 데미지 추가 완료, 연쇄 번개/치명타는 미구현)
  • 적 특수능력 (치유, 분열, 투명화, 보호막 등)
  • 맵 선택 (waypoints 배열을 여러 개 만들어 선택)
  • 스킬/마법 (화면 탭으로 범위 폭발 등)

도전적

  • 타워 조합 시스템 (두 타워를 합쳐서 새 타워)
  • 멀티 경로 (적이 두 갈래로 나뉘어 진입)
  • 보스 이벤트 (경고 연출, 레벨 스케일링, 호위병 동반 완료. 특수 공격/페이즈는 미구현)
  • 무한 모드 + 리더보드

13. 모바일 반응형 시스템

위치: game.js:14~48, index.html @media 쿼리

핵심 원리: 내부 해상도 vs CSS 표시 크기 분리

canvas.width / canvas.height   → 내부 그리기 해상도 (종이 크기)
canvas.style.width / height    → 화면에 보이는 크기 (액자 크기)

내부 해상도를 충분히 크게 유지(TILE ≥ 36)하면 텍스트와 그래픽이 선명하게 렌더링됨. CSS로 축소하면 화면에 맞게 보이되, 내부 좌표계는 변하지 않음.

resize() 동작 흐름

1. 사용 가능한 화면 크기 계산 (availW, availH)
2. TILE = min(availW/COLS, availH/TOTAL_ROWS)
3. TILE을 MIN_TILE(36) ~ MAX_TILE(56) 범위로 클램프
4. 내부 해상도 설정: canvas.width = COLS × TILE
5. CSS 축소 비율 계산: cssScale = min(availW/W, availH/H, 1.0)
6. CSS 크기 설정: canvas.style.width = W × cssScale

기기별 결과

기기 TILE 내부 해상도 CSS 표시 크기
iPhone (390×844) 36 720×612 386×328
iPad (1080×810) 44 880×748 880×748
데스크톱 (1920×1080) 52 1040×884 1040×884

터치 좌표 자동 변환

getCanvasPos() 함수가 canvas.getBoundingClientRect()로 CSS 크기를 얻고, canvas.width / rect.width 비율로 좌표를 변환하므로, CSS 축소 시에도 터치/클릭 좌표가 정확히 매핑됨.

isMobile 플래그

isMobile = ('ontouchstart' in window) && (innerWidth <= 768 || innerHeight <= 500);

이 플래그로 다음을 조건부 처리:

  • 키보드 단축키 힌트 [1], [Q] 숨김
  • "스페이스바 / 탭으로 시작" → "탭으로 시작"
  • "클릭하여 다시 시작" → "탭하여 다시 시작"
  • 잔디 블레이드 수 감소 (3개→1개/타일)

14. 사운드 시스템 (v2.0)

위치: game.js:102~265

핵심 원리: Web Audio API 프로시저럴 합성

외부 음원 파일(.mp3) 없이 OscillatorNode로 실시간 파형 생성.

AudioContext → OscillatorNode → GainNode → masterGain → destination
                (파형 생성)      (볼륨/엔벨로프)  (마스터 볼륨)   (스피커)

파형 종류

파형 음색 용도
sine 부드러운 순음 얼음, 독, 업그레이드
square 8bit 레트로 화살, 게임오버, UI
sawtooth 거친 톱날음 보스 경고, 라이프 감소
triangle 부드러운 중간음 타워 배치

주요 합성 기법

  • 주파수 스윕: frequency.exponentialRampToValueAtTime()으로 음높이 변화 → "pew" 효과
  • 화이트 노이즈: AudioBuffer에 랜덤값 채워서 "쉬~" 질감 → 폭발/전기
  • 맥놀이(Beat): 미세하게 다른 두 주파수 동시 재생 → 크리스탈/종소리 (얼음탑)
  • 아르페지오: 시간차를 두고 여러 음 순차 재생 → 웨이브 시작 팡파레
  • 엔벨로프: gain.exponentialRampToValueAtTime(0.001, endTime) → 자연스러운 소멸

사운드 목록 (15종)

사운드 트리거 합성 방식
Arrow fire 화살탑 발사 square 880→440Hz
Cannon fire 대포탑 발사 sine 150→40Hz + noise
Ice fire 냉기탑 발사 sine 1200 + 1500Hz 맥놀이
Lightning fire 번개탑 발사 noise + sawtooth 600→100Hz
Poison fire 독탑 발사 sine 주파수 워블
Enemy death 적 사망 noise + sine 400→100Hz
Boss death 보스 사망 noise×2 + sine×2
Wave start 웨이브 시작 C5→E5→G5 아르페지오
Boss warning 보스 웨이브 sawtooth 55Hz 럼블
Tower place 배치 triangle 200 + sine 100Hz
Tower upgrade 업그레이드 sine 600→800→1000→1300Hz
Tower sell 판매 square C6→E6
Life lost 라이프 감소 sawtooth 440→220Hz
Game over 게임오버 square C5→G4→C4→C3 하강
UI click UI 클릭 sine 800Hz, 0.03s

브라우저 정책

첫 클릭/터치 시 soundManager.init()을 호출하여 AudioContext를 생성. 브라우저는 사용자 상호작용 없이 오디오 재생을 차단하므로 이 패턴이 필수.


15. 앰비언트 시스템 (v2.0)

위치: game.js:312~410

정적 데코레이션 (1회 생성)

  • 잔디 블레이드: 잔디 타일마다 1~3개, Math.sin() 기반 바람 흔들림
  • 경로 디테일: 경로 타일마다 3~5개 조약돌/균열, 반투명 렌더링

동적 파티클 (매 프레임)

  • 먼지/반딧불이: 잔디 위에 랜덤 스폰, 최대 30개
  • 반딧불이는 shadowBlur 글로우 + 위로 떠오름
  • 먼지는 작고 반투명, 미세한 드리프트

16. 스크린쉐이크 & 충격파 (v2.0)

스크린쉐이크

ctx.save() → ctx.translate(randomX, randomY) → [게임 렌더링] → ctx.restore() → [UI 렌더링]

UI 패널은 흔들리지 않도록 restore() 후에 그림.

트리거 강도 지속
보스 사망 8px 0.4s
대포 스플래시 3px 0.15s
라이프 감소 4px 0.2s

충격파 링 (ShockwaveRing)

반지름 0→maxRadius 확장 + 투명도 감소. 적 사망 시 생성. 보스는 TILE×3 크기, 일반 적은 TILE×1.5. 최대 10개 동시.


17. UI 폴리시 (v2.0)

  • 패널 그라디언트: createLinearGradient()로 위→아래 밝음→어두움
  • 버튼 호버 글로우: mousePos 추적, 호버 시 밝은 배경 + 선택 시 shadowBlur
  • 골드/라이프 플래시: 값 변경 시 뱃지에 색상 오버레이 (0.4~0.5s)
  • 웨이브 전환: 텍스트 슬라이드인→정지→슬라이드아웃 (1.5s)
  • 게임오버 연출: 어두워지기(0.5s) → GAME OVER 스케일인 → 점수 카운트업 → 최고점수 → 재시작 안내 깜빡임
  • 볼륨 버튼: 🔊/🔇 토글, M키 단축키