본문 바로가기

[Pygame] 🏰 2D 타워 디펜스 게임 만들기 8강 | 타워 만들기 & 클릭한 위치에 설치하기

@도마22026. 2. 8. 19:00
728x90


7강에서는 마우스 클릭을 감지하고 클릭 좌표를 얻는 구조를 만들었습니다.
이번 8강에서는 그 구조를 그대로 활용해 타워를 설치합니다.

이번 강의 목표는 다음 3가지입니다.

  • 타워(Tower) 클래스 만들기
  • 클릭한 위치에 타워 설치하기
  • 경로(Path) 위에는 설치하지 못하게 막기(기본 판정)

1. 타워 기본 클래스 만들기

이번 강의에서는 타워 공격 기능 없이 설치 및 출력만 구현합니다.

class Tower:
    def __init__(self, pos):
        self.x, self.y = pos
        self.radius = TOWER_RADIUS

    def draw(self, screen):
        pygame.draw.circle(screen, (80, 160, 255), (self.x, self.y), self.radius)
        pygame.draw.circle(screen, (30, 30, 30), (self.x, self.y), self.radius, 3)

2. 설치 비용(Gold) 추가

타워 설치 비용을 상수로 관리합니다.

TOWER_COST = 50

3. 경로 위 설치 금지(타워 크기 포함)

경로는 굵기 40px로 그려지고 있으므로,
경로 중심선에서 PATH_WIDTH/2 안쪽은 기본적으로 경로입니다.

타워는 반지름이 있으므로, 설치 판정은 다음처럼 처리합니다.

  • 경로 반폭 + 타워 반지름 안쪽이면 설치 불가
threshold = PATH_WIDTH / 2 + TOWER_RADIUS

4. 타워끼리 겹침 방지

새로 설치하려는 타워 중심점과, 기존 타워 중심점의 거리를 계산합니다.

  • 거리 < (반지름 + 반지름) 이면 겹침
  • 타워 크기가 동일하므로 기본 기준은 2 * TOWER_RADIUS
def is_overlapping_tower(pos, towers):
    px, py = pos
    min_dist = TOWER_RADIUS * 2
    for t in towers:
        dx = px - t.x
        dy = py - t.y
        if (dx * dx + dy * dy) ** 0.5 < min_dist:
            return True
    return False

5. 클릭 이벤트에서 설치 판정 적용

설치 조건은 아래 순서로 처리합니다.

  1. 경로 위인지 확인
  2. 기존 타워와 겹치는지 확인
  3. 골드가 충분한지 확인
  4. 설치 및 골드 차감

6. 실행 결과


전체 코드

더보기
import pygame
import sys

pygame.init()

# -----------------------------
# 화면 설정
# -----------------------------
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Tower Defense")

clock = pygame.time.Clock()
FPS = 60

# -----------------------------
# 경로 설정
# -----------------------------
path_points = [
    (0, 300),
    (100, 300),
    (100, 100),
    (300, 100),
    (300, 500),
    (650, 500),
    (650, 300),
    (450, 300),
    (450, 100),
    (700, 100),
    (700, 200),
    (800, 200)
]

PATH_WIDTH = 40        # 경로 두께
TOWER_RADIUS = 16      # 타워 반지름
TOWER_COST = 50        # 타워 설치 비용

# -----------------------------
# 유틸: 점-선분 거리
# -----------------------------
def point_to_segment_distance(px, py, ax, ay, bx, by):
    abx = bx - ax
    aby = by - ay
    apx = px - ax
    apy = py - ay

    ab_len_sq = abx * abx + aby * aby
    if ab_len_sq == 0:
        return ((px - ax) ** 2 + (py - ay) ** 2) ** 0.5

    t = (apx * abx + apy * aby) / ab_len_sq
    t = max(0.0, min(1.0, t))

    cx = ax + abx * t
    cy = ay + aby * t

    dx = px - cx
    dy = py - cy
    return (dx * dx + dy * dy) ** 0.5

# -----------------------------
# 유틸: 경로 위(또는 너무 가까움) 판정
# 타워 반지름까지 포함해 경로 겹침 방지
# -----------------------------
def is_on_path(pos, path_points):
    px, py = pos
    threshold = PATH_WIDTH / 2 + TOWER_RADIUS

    for i in range(len(path_points) - 1):
        ax, ay = path_points[i]
        bx, by = path_points[i + 1]
        if point_to_segment_distance(px, py, ax, ay, bx, by) <= threshold:
            return True
    return False

# -----------------------------
# 유틸: 기존 타워와 겹침 판정
# -----------------------------
def is_overlapping_tower(pos, towers):
    px, py = pos
    min_dist = TOWER_RADIUS * 2

    for t in towers:
        dx = px - t.x
        dy = py - t.y
        if (dx * dx + dy * dy) ** 0.5 < min_dist:
            return True
    return False

# -----------------------------
# Enemy 클래스(기존 흐름 유지)
# -----------------------------
class Enemy:
    def __init__(self, path_points, speed=2):
        self.path_points = path_points
        self.speed = speed
        self.x, self.y = path_points[0]
        self.target_index = 1

        self.max_hp = 50
        self.hp = self.max_hp
        self.reward = 10

        self.reached_end = False
        self.is_dead = False
        self.radius = 12

    def update(self):
        if self.reached_end or self.is_dead:
            return

        # 임시 데미지(추후 타워 공격으로 교체)
        self.hp -= 0.05
        if self.hp <= 0:
            self.is_dead = True
            return

        if self.target_index >= len(self.path_points):
            self.reached_end = True
            return

        tx, ty = self.path_points[self.target_index]
        dx = tx - self.x
        dy = ty - self.y

        dist = (dx * dx + dy * dy) ** 0.5
        if dist == 0:
            self.target_index += 1
            return

        self.x += (dx / dist) * self.speed
        self.y += (dy / dist) * self.speed

        if dist <= self.speed:
            self.x, self.y = tx, ty
            self.target_index += 1
            if self.target_index >= len(self.path_points):
                self.reached_end = True

    def draw(self, screen):
        pygame.draw.circle(screen, (220, 80, 80), (int(self.x), int(self.y)), self.radius)

# -----------------------------
# Tower 클래스
# -----------------------------
class Tower:
    def __init__(self, pos):
        self.x, self.y = pos
        self.radius = TOWER_RADIUS

    def draw(self, screen):
        pygame.draw.circle(screen, (80, 160, 255), (self.x, self.y), self.radius)
        pygame.draw.circle(screen, (30, 30, 30), (self.x, self.y), self.radius, 3)

# -----------------------------
# 게임 변수
# -----------------------------
enemies = []
towers = []

spawn_interval = 1000
last_spawn_time = 0

base_hp = 20
gold = 100

font = pygame.font.SysFont(None, 32)

# -----------------------------
# 메인 루프
# -----------------------------
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        # 타워 설치(좌클릭)
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            click_pos = pygame.mouse.get_pos()

            # 설치 판정: 경로 금지 + 타워 겹침 금지 + 골드 확인
            if not is_on_path(click_pos, path_points) and not is_overlapping_tower(click_pos, towers):
                if gold >= TOWER_COST:
                    towers.append(Tower(click_pos))
                    gold -= TOWER_COST

    game_over = base_hp <= 0

    if not game_over:
        # 적 스폰
        now = pygame.time.get_ticks()
        if now - last_spawn_time >= spawn_interval:
            enemies.append(Enemy(path_points))
            last_spawn_time = now

        # 적 업데이트
        for enemy in enemies:
            enemy.update()

        # 도착한 적 처리(베이스 HP 감소)
        arrived = [e for e in enemies if e.reached_end]
        base_hp -= len(arrived)

        # 죽은 적 처리(골드 획득)
        dead = [e for e in enemies if e.is_dead]
        gold += sum(e.reward for e in dead)

        # 적 정리
        enemies = [e for e in enemies if not e.reached_end and not e.is_dead]

    # -----------------------------
    # 화면 그리기
    # -----------------------------
    screen.fill((30, 30, 30))
    pygame.draw.lines(screen, (200, 180, 100), False, path_points, PATH_WIDTH)

    # 타워 출력
    for tower in towers:
        tower.draw(screen)

    # 적 출력
    for enemy in enemies:
        enemy.draw(screen)

    # UI
    screen.blit(font.render(f"HP: {base_hp}", True, (255, 255, 255)), (10, 10))
    screen.blit(font.render(f"Gold: {gold}", True, (255, 255, 0)), (10, 40))
    screen.blit(font.render(f"Tower Cost: {TOWER_COST}", True, (200, 200, 200)), (10, 70))

    if game_over:
        over_text = font.render("GAME OVER", True, (255, 255, 255))
        screen.blit(
            over_text,
            (SCREEN_WIDTH // 2 - over_text.get_width() // 2,
             SCREEN_HEIGHT // 2)
        )

    pygame.display.update()
    clock.tick(FPS)

728x90
도마2
@도마2 :: 도마의 코드노트

초보자를 위한 코딩 강의를 정리합니다. 파이썬부터 C#, Unity 게임 제작까지 차근차근 기록합니다. — 도마

목차