비디오 게임 역사의 시조새이자 대중화의 일등 공신인 퐁(Pong)의 유래

💡 퐁(Pong)의 탄생과 유래
1. 앨런 알코어와 아타리(Atari)의 만남
퐁은 1972년, 전설적인 게임 회사 **아타리(Atari)**의 공동 창업자인 **놀런 부슈널(Nolan Bushnell)**의 아이디어에서 시작되었습니다. 부슈널은 갓 입사한 엔지니어 **앨런 알코어(Allan Alcorn)**의 실력을 테스트하기 위해 간단한 과제를 하나 주었습니다. 그것이 바로 ‘화면에 점 하나와 막대기 두 개가 있는 테니스 게임’을 만드는 것이었습니다.
2. 마그나복스 오디세이에서의 영감
사실 퐁은 완전한 무(無)에서 탄생한 것은 아닙니다. 부슈널은 세계 최초의 가정용 게임기인 **마그나복스 오디세이(Magnavox Odyssey)**의 탁구 게임 시연을 본 적이 있었고, 여기서 영감을 얻어 알코어에게 제작을 지시했습니다.
TMI: 이후 마그나복스는 아타리를 상대로 저작권 소송을 제기했고, 아타리가 로열티를 지불하며 합의에 이르게 됩니다. 이것이 비디오 게임 역사상 최초의 저작권 분쟁으로 기록되어 있습니다.
3. ‘퐁’이라는 이름의 유래
원래 게임의 이름은 단순히 ‘테니스(Tennis)’가 될 뻔했습니다. 하지만 이미 다른 게임들이 그 이름을 사용하고 있었기 때문에, 새로운 이름이 필요했습니다. 부슈널은 공이 패들에 부딪힐 때 나는 특유의 “퐁(Pong)” 하는 효과음에서 착안하여 이름을 지었습니다.
🚀 퐁이 세운 기록들
- 최초의 상업적 성공: 캘리포니아의 한 술집(Andy Capp’s Tavern)에 설치된 퐁 아케이드 기기는 며칠 만에 동전 통이 꽉 차서 고장 날 정도로 폭발적인 인기를 끌었습니다.
- 비디오 게임 산업의 시작: 퐁의 성공은 비디오 게임이 단순히 연구소의 실험물이 아니라, 거대한 상업적 가치가 있는 산업임을 증명했습니다.
- 가정용 게임기 열풍: 1975년 출시된 ‘홈 퐁(Home Pong)’ 버전은 크리스마스 시즌에 엄청난 판매고를 올리며 거실을 오락실로 바꾸어 놓았습니다.
🎨 퐁의 시각적 구성
퐁의 그래픽은 지금 보면 매우 단순하지만, 당시에는 혁명이었습니다.
- 패들(Paddle): 플레이어가 조종하는 수직 막대
- 볼(Ball): 화면을 오가는 사각형 점
- 점수판: 화면 상단에 표시되는 숫자

| 부품 | 기능 | ESP32-C3 핀 | 비고 |
| TFT LCD | SCK (Clock) | GPIO 4 | 하드웨어 SPI 고정 |
| MOSI (Data) | GPIO 6 | 하드웨어 SPI 고정 | |
| RES (Reset) | GPIO 10 | 일반 출력 | |
| DC (Data/Cmd) | GPIO 1 | 일반 출력 | |
| CS (Select) | GPIO 0 | 일반 출력 | |
| 입력 장치 | POT_L (가변저항) | GPIO 3 | ADC1_CH3 |
| POT_R (가변저항) | GPIO 2 | ADC1_CH2 | |
| BUTTON (시작/정지) | GPIO 9 | 내부 풀업 사용 | |
| 출력 장치 | BUZZER | GPIO 7 | PWM 출력 |
Python pong.py
from machine import Pin, SPI, ADC, PWM
import st7789py as st7789
import time
import random
import vga2_8x16 as font
# ==========================================================
# 1. HARDWARE PIN CONFIGURATION (핀 설정)
# ==========================================================
LCD_SCK = 4
LCD_MOSI = 6
LCD_RES = 10
LCD_DC = 1
LCD_CS = 0
PIN_POT_L = 3 # ADC
PIN_POT_R = 2 # ADC
PIN_BTN = 9 # Digital (Pull-up)
PIN_BUZZER = 7 # PWM
# ==========================================================
# 2. 초기화 (Hardware Initialization)
# ==========================================================
spi = SPI(1, baudrate=60000000, sck=Pin(LCD_SCK), mosi=Pin(LCD_MOSI))
tft = st7789.ST7789(spi, 240, 320, reset=Pin(LCD_RES, Pin.OUT),
cs=Pin(LCD_CS, Pin.OUT), dc=Pin(LCD_DC, Pin.OUT), rotation=1)
pot_l = ADC(Pin(PIN_POT_L))
pot_r = ADC(Pin(PIN_POT_R))
pot_l.atten(ADC.ATTN_11DB)
pot_r.atten(ADC.ATTN_11DB)
buzzer = PWM(Pin(PIN_BUZZER), duty_u16=0)
btn = Pin(PIN_BTN, Pin.IN, Pin.PULL_UP)
# ==========================================================
# 3. 게임 설정 및 상태 변수
# ==========================================================
WIDTH, HEIGHT = 320, 240
P_W, P_H = 8, 50
P1_X, P2_X = 8, 304
BALL_SIZE = 8
SENSITIVITY, FIXED_SPEED = 3.5, 3.2
SCORE_AREA_Y, RESTART_THRESHOLD = 25, 150
WINNING_SCORE = 5
CYAN, MAGENTA = st7789.CYAN, st7789.MAGENTA
WHITE, BLACK, YELLOW, GRAY = st7789.WHITE, st7789.BLACK, st7789.YELLOW, 0x4208
COLORS = [st7789.RED, st7789.GREEN, st7789.BLUE, st7789.YELLOW, st7789.MAGENTA, st7789.CYAN, st7789.WHITE]
smooth_l, smooth_r, ALPHA = 2048, 2048, 0.2
game_running, first_start = False, True
played_boot_melody = False
score_l, score_r = 0, 0
p1_y, p2_y = (HEIGHT-P_H)//2, (HEIGHT-P_H)//2
last_btn_state = 1
ready_l, ready_r = False, False
# ==========================================================
# 4. 핵심 기능 함수
# ==========================================================
def play_sound(freq, duration):
try: buzzer.freq(freq); buzzer.duty_u16(32768); time.sleep(duration); buzzer.duty_u16(0)
except: pass
def play_start_melody():
for f in [523, 659, 784]:
play_sound(f, 0.08); time.sleep(0.02)
def draw_court_static():
tft.fill(BLACK)
tft.fill_rect(0, SCORE_AREA_Y, WIDTH, 2, WHITE)
tft.fill_rect(0, HEIGHT-2, WIDTH, 2, WHITE)
for y in range(SCORE_AREA_Y + 5, HEIGHT, 20):
tft.fill_rect(WIDTH//2 - 1, y, 2, 10, GRAY)
def draw_score_only(sl, sr):
tft.fill_rect(40, 0, 240, SCORE_AREA_Y, BLACK)
tft.text(font, f"SCORE {sl}", 45, 5, CYAN)
tft.text(font, f"SCORE {sr}", WIDTH-115, 5, MAGENTA)
def show_winner(winner_text, color):
tft.fill(BLACK)
for _ in range(50):
x, y = random.randint(50, 270), random.randint(50, 190)
c = random.choice(COLORS)
size = random.randint(3, 8)
for i in range(10): tft.fill_rect(x + random.randint(-20, 20), y + random.randint(-20, 20), size, size, c)
play_sound(random.randint(400, 1200), 0.02)
tft.fill(BLACK)
tft.text(font, "VICTORY!", 120, 80, YELLOW)
tft.text(font, winner_text, 100, 110, color)
tft.text(font, "PRESS BUTTON TO RESTART", 65, 160, WHITE)
while btn.value() == 1: time.sleep(0.1)
global score_l, score_r, first_start, played_boot_melody
score_l, score_r = 0, 0
first_start, played_boot_melody = True, True # 재시작 시 멜로디 생략
draw_court_static(); draw_score_only(0, 0)
tft.text(font, "PONG CLASSIC", 110, 100, YELLOW)
tft.text(font, "PRESS BUTTON TO START", 75, 130, WHITE)
time.sleep(0.5)
def init_ball(direction=None):
if direction is None: # 첫 판 (가운데 출발)
return [WIDTH//2, (HEIGHT+SCORE_AREA_Y)//2, random.choice([-1, 1]) * FIXED_SPEED, (random.random() - 0.5) * 2]
elif direction == 1: # 왼쪽 승자 서브
return [P1_X + P_W + 5, p1_y + (P_H // 2) - (BALL_SIZE // 2), FIXED_SPEED, (random.random() - 0.5) * 2]
else: # 오른쪽 승자 서브
return [P2_X - BALL_SIZE - 5, p2_y + (P_H // 2) - (BALL_SIZE // 2), -FIXED_SPEED, (random.random() - 0.5) * 2]
# ==========================================================
# 5. 메인 실행 루프
# ==========================================================
draw_court_static(); draw_score_only(0, 0)
tft.text(font, "PONG CLASSIC", 110, 100, YELLOW)
tft.text(font, "PRESS BUTTON TO START", 75, 130, WHITE)
ball = init_ball()
last_raw_l, last_raw_r = pot_l.read(), pot_r.read()
while True:
btn_val = btn.value()
if first_start:
if not played_boot_melody:
play_start_melody()
played_boot_melody = True
if btn_val == 0:
first_start, game_running = False, True
play_sound(1000, 0.1); tft.fill_rect(70, 100, 200, 50, BLACK)
draw_score_only(score_l, score_r); time.sleep(0.2)
else:
if last_btn_state == 1 and btn_val == 0:
game_running = not game_running
if not game_running: tft.text(font, "- PAUSED -", 125, 120, YELLOW, BLACK)
else: tft.fill_rect(120, 120, 100, 20, BLACK)
time.sleep(0.2)
last_btn_state = btn_val
# 1. 패들 업데이트
smooth_l = (ALPHA * pot_l.read()) + ((1 - ALPHA) * smooth_l)
smooth_r = (ALPHA * pot_r.read()) + ((1 - ALPHA) * smooth_r)
target_p1 = max(SCORE_AREA_Y+2, min(HEIGHT-P_H-2, int((smooth_l * SENSITIVITY / 4095) * (HEIGHT-P_H-SCORE_AREA_Y) + SCORE_AREA_Y)))
target_p2 = max(SCORE_AREA_Y+2, min(HEIGHT-P_H-2, int((smooth_r * SENSITIVITY / 4095) * (HEIGHT-P_H-SCORE_AREA_Y) + SCORE_AREA_Y)))
p1_move_delta = (target_p1 - p1_y) * 0.3
p2_move_delta = (target_p2 - p2_y) * 0.3
for p_info in [(target_p1, p1_y, P1_X, CYAN), (target_p2, p2_y, P2_X, MAGENTA)]:
t_y, c_y, x, col = p_info
if t_y != c_y:
diff = t_y - c_y
if diff > 0:
tft.fill_rect(x, c_y, P_W, diff, BLACK)
tft.fill_rect(x, t_y + P_H - diff, P_W, diff, col)
else:
tft.fill_rect(x, c_y + P_H + diff, P_W, -diff, BLACK)
tft.fill_rect(x, t_y, P_W, -diff, col)
p1_y, p2_y = target_p1, target_p2
if not game_running:
if not first_start and (score_l > 0 or score_r > 0) and not (ready_l and ready_r):
if abs(smooth_l - last_raw_l) > RESTART_THRESHOLD: ready_l = True
if abs(smooth_r - last_raw_r) > RESTART_THRESHOLD: ready_r = True
tft.text(font, "READY" if ready_l else "WAIT ", 55, 110, CYAN if ready_l else GRAY, BLACK)
tft.text(font, "READY" if ready_r else "WAIT ", 225, 110, MAGENTA if ready_r else GRAY, BLACK)
if ready_l and ready_r:
game_running, ready_l, ready_r = True, False, False
play_sound(1200, 0.05); tft.fill_rect(50, 105, 220, 25, BLACK)
last_raw_l, last_raw_r = smooth_l, smooth_r
continue
# 2. 공 물리 엔진 (진동 해결 및 패들 반사)
last_ball = ball[:]
ball[0] += ball[2]; ball[1] += ball[3]
if ball[1] <= SCORE_AREA_Y + 2:
ball[3] = abs(ball[3]); ball[1] = SCORE_AREA_Y + 3; play_sound(800, 0.01)
elif ball[1] >= HEIGHT - BALL_SIZE - 2:
ball[3] = -abs(ball[3]); ball[1] = HEIGHT - BALL_SIZE - 3; play_sound(800, 0.01)
if ball[0] <= P1_X + P_W and p1_y <= ball[1] <= p1_y + P_H:
ball[2] = abs(ball[2]); ball[0] = P1_X + P_W + 1
hit_f = (ball[1] + BALL_SIZE/2 - (p1_y + P_H/2)) / (P_H/2)
ball[3] = (hit_f * FIXED_SPEED) + p1_move_delta; play_sound(1000, 0.01)
elif ball[0] >= P2_X - BALL_SIZE and p2_y <= ball[1] <= p2_y + P_H:
ball[2] = -abs(ball[2]); ball[0] = P2_X - BALL_SIZE - 1
hit_f = (ball[1] + BALL_SIZE/2 - (p2_y + P_H/2)) / (P_H/2)
ball[3] = (hit_f * FIXED_SPEED) + p2_move_delta; play_sound(1000, 0.01)
# 3. 득점 및 승자 판정
if ball[0] < -BALL_SIZE or ball[0] > WIDTH:
tft.fill_rect(int(last_ball[0]), int(last_ball[1]), BALL_SIZE, BALL_SIZE, BLACK)
tft.fill_rect(0, SCORE_AREA_Y + 2, P1_X, HEIGHT - SCORE_AREA_Y - 4, BLACK)
tft.fill_rect(P2_X + P_W, SCORE_AREA_Y + 2, WIDTH - (P2_X + P_W), HEIGHT - SCORE_AREA_Y - 4, BLACK)
if ball[0] < 0: score_r += 1; next_w = -1
else: score_l += 1; next_w = 1
draw_score_only(score_l, score_r)
if score_l >= WINNING_SCORE: show_winner("PLAYER 1 WINS", CYAN); ball = init_ball(); continue
elif score_r >= WINNING_SCORE: show_winner("PLAYER 2 WINS", MAGENTA); ball = init_ball(); continue
ball = init_ball(next_w); last_raw_l, last_raw_r = smooth_l, smooth_r; game_running = False; continue
# 4. 드로잉
tft.fill_rect(int(last_ball[0]), int(last_ball[1]), BALL_SIZE, BALL_SIZE, BLACK)
if last_ball[1] <= SCORE_AREA_Y + BALL_SIZE + 2: tft.fill_rect(int(last_ball[0]), SCORE_AREA_Y, BALL_SIZE, 2, WHITE)
if last_ball[1] >= HEIGHT - BALL_SIZE - 4: tft.fill_rect(int(last_ball[0]), HEIGHT-2, BALL_SIZE, 2, WHITE)
if abs(int(last_ball[0]) - WIDTH//2) < 5:
for y in range(SCORE_AREA_Y + 5, HEIGHT, 20):
if int(last_ball[1]) - 10 < y < int(last_ball[1]) + BALL_SIZE:
tft.fill_rect(WIDTH//2 - 1, y, 2, 10, GRAY)
tft.fill_rect(int(ball[0]), int(ball[1]), BALL_SIZE, BALL_SIZE, YELLOW)
time.sleep(0.005)


micro2iot에 답글 남기기 응답 취소