게임 코드를 섹션별로 하나씩 설명할게. 1. 준비 (1~11줄) import pygame import random import math - pygame: 게임 화면, 키 입력, 도형 그리기 등을 담당하는 라이브러리 - random: 공의 생성 위치/방향을 무작위로 정할 때 사용 - math: 각도(atan2)와 거리(hypot) 계산에 사용 — 공이 여러 방향으로 날아가고, 원끼리 충돌했는지 판단할 때 필요 pygame.init() WIDTH, HEIGHT = 800, 600 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("공 피하기 게임") clock = pygame.time.Clock() - pygame.init(): pygame의 내부 모듈들을 초기화(필수 절차) - set_mode((800, 600)): 800×600 크기의 게임 창을 만들고 그 화면 객체를 screen에 저장 - set_caption: 창 제목 표시줄 텍스트 - clock: 나중에 clock.tick(60)으로 초당 프레임 수(FPS)를 60으로 고정하는 데 씀 2. 색상 & 변수 설정 (13~35줄) WHITE = (255, 255, 255) BLACK = (0, 0, 0) RED = (230, 30, 30) - pygame은 색을 (R, G, B) 튜플로 표현. 배경은 흰색, 공은 검정, 플레이어는 빨강. player_radius = 15 player_x = WIDTH // 2 player_y = HEIGHT // 2 player_speed = 6 - 플레이어(빨간 원)의 반지름, 시작 좌표(화면 정중앙), 한 프레임에 움직이는 픽셀 수. ball_radius = 6 balls = [] # 각 원소: [x, y, vx, vy] ball_speed = 3 spawn_timer = 0 spawn_interval = 25 - balls는 현재 화면에 떠 있는 모든 검은 공을 담는 리스트. 공 하나는 [x좌표, y좌표, x방향속도, y방향속도] 형태의 리스트로 관리. - spawn_timer/spawn_interval: "25프레임(≈0.4초)마다 공 하나 생성"을 세는 카운터. font = pygame.font.SysFont(None, 36) game_over = False start_ticks = pygame.time.get_ticks() score = 0 - font: 텍스트를 그릴 때 쓸 글꼴 객체 - start_ticks: 게임(또는 재시작) 시작 시점의 시각(ms) — 이후 "지금 시각 - 시작 시각"으로 생존 시간을 계산하는 기준점 3. 그리기용 함수 (37~45줄) def draw_player(x, y): pygame.draw.circle(screen, RED, (int(x), int(y)), player_radius) def draw_ball(ball): pygame.draw.circle(screen, BLACK, (int(ball[0]), int(ball[1])), ball_radius) def show_text(text, x, y, color=BLACK): img = font.render(text, True, color) screen.blit(img, (x, y)) - pygame.draw.circle(화면, 색, 중심좌표, 반지름)으로 원을 그림. 좌표는 정수여야 해서 int()로 변환. - show_text: 문자열을 이미지로 렌더링(font.render)한 뒤 screen.blit(도장 찍듯 붙여넣기)으로 화면에 표시. 4. 공 생성 로직 — 핵심 부분 (47~65줄) def spawn_ball(speed): side = random.choice(["top", "bottom", "left", "right"]) if side == "top": x, y = random.randint(0, WIDTH), -ball_radius elif side == "bottom": x, y = random.randint(0, WIDTH), HEIGHT + ball_radius elif side == "left": x, y = -ball_radius, random.randint(0, HEIGHT) else: x, y = WIDTH + ball_radius, random.randint(0, HEIGHT) - 상/하/좌/우 중 한 변을 무작위로 고르고, 그 변 위의 무작위 지점을 공의 시작 위치로 정함. 화면 경계선보다 살짝 바깥(-ball_radius, HEIGHT + ball_radius)에서 시작해서 등장하는 순간이 자연스럽게 보이도록 함. target_x = random.randint(0, WIDTH) target_y = random.randint(0, HEIGHT) angle = math.atan2(target_y - y, target_x - x) vx = math.cos(angle) * speed vy = math.sin(angle) * speed return [x, y, vx, vy] - 화면 안의 무작위 지점(target_x, target_y)을 목표로 잡고, 시작점에서 그 목표까지의 각도를 atan2로 구함. - 그 각도의 코사인/사인 값에 속도를 곱해서 **x방향 속도(vx), y방향 속도(vy)**를 만듦 → 이게 "여러 방향에서 날아오는" 효과의 핵심. 매번 목표 지점이 랜덤이라 공마다 날아오는 각도가 다 다름. 5. 재시작 함수 (67~76줄) def reset_game(): global balls, player_x, player_y, ... balls = [] player_x, player_y = WIDTH // 2, HEIGHT // 2 ... - 함수 안에서 바깥의 전역 변수를 수정하려면 global 선언이 필요. 게임오버 상태에서 R키를 누르면 모든 상태를 초깃값으로 되돌림. 6. 메인 루프 (78~145줄) 게임은 **"화면을 계속 새로 그리는 무한 반복문"**으로 동작해. running = True while running: clock.tick(60) screen.fill(WHITE) - clock.tick(60): 이 루프가 초당 60번만 돌도록 속도 제한 (프레임 고정) - screen.fill(WHITE): 매 프레임 화면을 흰색으로 지움 → 이전 프레임 잔상이 안 남게 함 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.KEYDOWN: if game_over and event.key == pygame.K_r: reset_game() - pygame이 감지한 이벤트(창 닫기, 키 입력 등)를 하나씩 확인 - 창의 X버튼을 누르면(QUIT) 루프 종료 → 프로그램 끝 - 게임오버 상태에서 R키가 눌리면 재시작 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT] and player_x - player_radius > 0: player_x -= player_speed ... - pygame.key.get_pressed()는 현재 눌려있는 모든 키 상태를 알려줌 (누르고 있는 동안 계속 반응 → 부드러운 이동) - 방향키 4개를 각각 체크해서 좌표를 더하거나 뺌 - player_x - player_radius > 0 같은 조건은 화면 경계를 벗어나지 않도록 막는 것 (원의 왼쪽 끝이 0보다 작아지면 못 움직이게) 6-2. 공 생성·이동·제거 (101~117줄) spawn_timer += 1 if spawn_timer >= spawn_interval: spawn_timer = 0 balls.append(spawn_ball(ball_speed)) - 매 프레임 카운터를 1씩 늘리다가, spawn_interval(예: 25)에 도달하면 새 공 하나를 만들어 balls 리스트에 추가하고 카운터 리셋 for ball in balls: ball[0] += ball[2] ball[1] += ball[3] - 모든 공에 대해 x좌표에 vx, y좌표에 vy를 더함 → 매 프레임 조금씩 이동 (직선 운동) margin = ball_radius * 2 balls = [b for b in balls if -margin <= b[0방지(메모리/성능 관리) 6-3. 충돌 판정 (119~125줄) for ball in balls: dx = ball[0] - player_x dy = ball[1] - player_y distance = math.hypot(dx, dy) if distance < player_radius + ball_radius: game_over = True - 원과 원의 충돌은 "두 중심 사이 거리가 두 반지름의 합보다 작으면 겹친 것"으로 판단 - math.hypot(dx, dy)는 √(dx² + dy²), 즉 두 점 사이 거리를 계산하는 함수 - 조건을 만족하는 공이 하나라도 있으면 game_over = True 6-4. 점수/난이도 (127~130줄) score = (pygame.time.get_ticks() - start_ticks) // 100 ball_speed = 3 + score // 50 spawn_interval = max(8, 25 - score // 20) - pygame.time.get_ticks()는 pygame 시작 후 지난 시간(ms). 시작 시각을 뺀 값을 100으로 나눠서 "0.1초 단위 점수"로 사용 - 점수가 오를수록 ball_speed는 증가, spawn_interval은 감소(더 자주 생성) → 시간이 지날수록 어려워짐 - max(8, ...)으로 생성 간격이 8프레임 밑으로는 안 내려가게 제한(너무 빨라져서 불가능해지는 걸 방지) 6-5. 그리기 & 텍스트 (132~143줄) for ball in balls: draw_ball(ball) draw_player(player_x, player_y) show_text(f"Score: {score}", 10, 10) if game_over: show_text("GAME OVER", ...) show_text("Retry", ...) pygame.display.flip() - 공들을 먼저 그리고 플레이어를 나중에 그림(순서상 플레이어가 위에 보이게) - pygame.display.flip(): 지금까지 그린 내용을 실제 화면에 반영(업데이트) — 이걸 안 하면 화면이 갱신되지 않음 7. 종료 (145줄) pygame.quit() - while running 루프를 빠져나오면(창 닫기) pygame 리소스를 정리하고 프로그램 종료. ---궁금한 부분(예: atan2가 왜 필요한지, 리스트 컴프리헨션 문법 등) 더 깊게 설명해줄까? ✻ Churned for 46s atan2가 왜 필요한지 리스트 컴프리헨션이 뭔지 cos,sin은 왜 쓰는건지 모르겠어. 좋아, 하나씩 풀어서 설명할게. 실제 코드에 쓰인 예시로 설명할게. 1. cos, sin이 왜 필요한가 컴퓨터 화면에서 공을 움직이려면 결국 **"매 프레임마다 x좌표를 얼마나 바꿀지(vx), y좌표를 얼마나 바꿀지(vy)"**가 있어야 해. 그런데 우리가 실제로 정하고 싶은 건 "이 공이 어느 각도로 날아갈지"거든. 각도(angle)를 x축 방향 이동량과 y축 방향 이동량으로 쪼개주는 게 바로 삼각함수야. y ↑ | ● (목표 지점) | ╱ | ╱ ← 이 대각선의 각도가 angle | ╱ ●────────→ x (시작점) 이 대각선(빗변)을 길이 speed인 화살표라고 하면: - cos(angle) * speed = 그 화살표를 x축에 눕혔을 때 길이 (가로로 얼마나 가는지) - sin(angle) * speed = 그 화살표를 y축에 눕혔을 때 길이 (세로로 얼마나 가는지) 즉 "각도 + 속력"을 "가로속도(vx) + 세로속도(vy)"로 바꿔주는 변환기가 cos/sin이야. vx = math.cos(angle) * speed vy = math.sin(angle) * speed 예를 들어 angle이 0도(오른쪽 정방향)면 cos(0)=1, sin(0)=0 → vx=speed, vy=0 → 오른쪽으로만 움직임. angle이 90도(아래쪽, pygame은 y가 아래로 갈수록 증가)면 cos=0, sin=1 → vx=0, vy=speed → 아래로만 움직임. 그 사이 각도면 vx, vy가 둘 다 적당히 섞여서 대각선으로 움직여. 이렇게 나온 [vx, vy]를 매 프레임 좌표에 더하는 게 이 부분이야: ball[0] += ball[2] # x += vx ball[1] += ball[3] # y += vy 매 프레임 같은 vx, vy를 계속 더하니까 결과적으로 일정한 각도의 직선으로 날아가. 2. atan2가 왜 필요한가 cos/sin이 "각도 → vx, vy"로 바꿔주는 거라면, atan2는 그 반대야: **"두 점의 좌표 차이 → 각도"**를 구해줘. 우리 코드에서는 "공의 시작 위치"와 "화면 안 무작위 목표 지점"이 있고, 그 사이 각도를 몰라. 그래서: target_x = random.randint(0, WIDTH) target_y = random.randint(0, HEIGHT) angle = math.atan2(target_y - y, target_x - x) target_y - y는 세로로 얼마나 떨어져 있는지(dy), target_x - x는 가로로 얼마나 떨어져 있는지(dx). 이 두 값으로 "시작점에서 목표점을 바라보는 방향의 각도"를 구하는 게 atan2(dy, dx)야. 왜 atan이 아니라 atan2인가? 수학 시간에 배우는 tan(각도) = dy/dx를 거꾸로 풀면 각도 = atan(dy/dx)처럼 보이는데, 여기엔 함정이 있어: - dx가 0이면 dy/dx가 나눗셈 오류(0으로 나누기)가 남 - atan은 dx, dy의 부호(+/-)를 무시하고 나눗셈 결과만 보기 때문에, 오른쪽으로 가는 방향인지 왼쪽으로 가는 방향인지 구분을 못해 (예: dx=1,dy=1과 dx=-1,dy=-1은 dy/dx가 똑같이 1이지만 방향은 정반대) atan2(dy, dx)는 dy, dx를 따로따로 받아서 이 부호까지 다 고려해 정확히 어느 방향인지(360도 전체 중 어디) 판단해줘. 그래서 두 점 사이 "정확한 방향"을 구할 때는 항상 atan2를 씀. 정리하면 이 코드의 흐름은: 1. atan2로 "시작점 → 목표점" **방향(각도)**을 구하고 2. cos, sin으로 그 각도를 **가로/세로 속도(vx, vy)**로 변환 이 두 단계 덕분에 공마다 매번 다른 무작위 목표 지점을 향해, 서로 다른 각도로 날아가는 게 가능해지는 거야. 3. 리스트 컴프리헨션이 뭔가 이 줄이 리스트 컴프리헨션이야: balls = [b for b in balls if -margin <= b[0] <= WIDTH + margin and -margin <= b[1] <= HEIGHT + margin] 이건 사실 아래 for문을 한 줄로 압축한 것과 완전히 같아: new_balls = [] for b in balls: if -margin <= b[0] <= WIDTH + margin and -margin <= b[1] <= HEIGHT + margin: new_balls.append(b) balls = new_balls 즉 **"기존 리스트를 하나씩 훑으면서, 조건을 만족하는 것만 골라 새 리스트를 만든다"**를 [표현식 for 변수 in 리스트 if 조건] 형태로 짧게 쓰는 문법이야. 읽는 순서: - for b in balls → balls의 원소를 b라는 이름으로 하나씩 꺼내면서 - if 조건 → 그 조건을 만족하는 것만 - [b for ...] → b(그 자체)를 모아서 새 리스트로 만든다 우리 코드에서 조건 -margin <= b[0] <= WIDTH + margin은 "이 공의 x좌표가 화면 범위(약간의 여유 margin 포함) 안에 있는가"를 뜻하고, y좌표도 마찬가지. 즉 **"아직 화면 근처에 있는 공만 살아남기고, 화면 밖으로 완전히 나간 공은 리스트에서 제거"**하는 거야. 매 프레임 이 줄이 실행되면서 balls 리스트가 계속 "지금 화면에 있는 공들만" 유지하도록 갱신돼. 리스트 컴프리헨션은 "for문 + if문 + append"를 짧게 쓰는 문법일 뿐, 동작 자체는 평범한 반복문과 똑같아. 처음엔 일반 for문으로 먼저 써보고 익숙해지면 이렇게 줄여 쓰는 게 이해하기 편할 거야. ✻ Baked for 35s