소개
이 프로젝트에서는 Pygame 라이브러리를 사용하여 간단한 Flappy Bird 게임을 만드는 코드를 관리 가능한 단계로 나눌 것입니다. 이 단계를 따르면 게임을 점진적으로 구축하는 방법을 배울 수 있습니다. 각 단계에는 간략한 설명, 코드 블록 및 주석이 포함되어 게임을 이해하고 구현하는 데 도움이 됩니다. 시작해 봅시다!
👀 미리보기

🎯 작업
이 프로젝트에서 다음을 배우게 됩니다.
- Flappy Bird 게임을 위한 프로젝트 파일을 설정하는 방법
- 게임의 시작 애니메이션을 표시하는 방법
- Flappy Bird 의 주요 게임 로직을 구현하는 방법
- 플레이어가 졌을 때 게임 오버 화면을 표시하는 방법
- 게임을 위한 헬퍼 함수를 정의하는 방법
🏆 성과
이 프로젝트를 완료하면 다음을 수행할 수 있습니다.
- Pygame 라이브러리를 사용하여 게임을 만들 수 있습니다.
- 게임 루프, 충돌 및 애니메이션과 같은 게임 개발 개념을 이해할 수 있습니다.
프로젝트 파일 생성
먼저, Flappy Bird 게임을 위한 프로젝트 파일을 생성합니다.
cd ~/project
touch flappy.py
sudo pip install pygame
이 단계에서는 기본적인 프로젝트 구조를 설정하고 필요한 라이브러리를 가져옵니다. 또한 몇 가지 상수 (constants) 를 정의하고 초기 게임 에셋 (assets) 을 로드합니다.
from itertools import cycle
import random
import sys
import pygame
from pygame.locals import *
FPS = 30
SCREENWIDTH = 288
SCREENHEIGHT = 512
PIPEGAPSIZE = 100 ## 파이프 상하 간격
BASEY = SCREENHEIGHT * 0.79
## 이미지 및 hitmask 딕셔너리
IMAGES, HITMASKS = {}, {}
## 가능한 모든 플레이어 목록 (플랩의 3 가지 위치 튜플)
PLAYERS_LIST = (
## 빨간 새
(
"data/sprites/redbird-upflap.png",
"data/sprites/redbird-midflap.png",
"data/sprites/redbird-downflap.png",
),
## 파란 새
(
"data/sprites/bluebird-upflap.png",
"data/sprites/bluebird-midflap.png",
"data/sprites/bluebird-downflap.png",
),
## 노란 새
(
"data/sprites/yellowbird-upflap.png",
"data/sprites/yellowbird-midflap.png",
"data/sprites/yellowbird-downflap.png",
),
)
## 배경 목록
BACKGROUNDS_LIST = (
"data/sprites/background-day.png",
"data/sprites/background-night.png",
)
## 파이프 목록
PIPES_LIST = (
"data/sprites/pipe-green.png",
"data/sprites/pipe-red.png",
)
- 게임을 만들기 위한
pygame, 무작위 요소를 생성하기 위한random, 시스템 관련 함수를 위한sys, 주요 상수를 위한pygame.locals를 포함하여 게임에 필요한 라이브러리를 가져옵니다. pygame.init()로 Pygame 을 초기화합니다.FPS,SCREENWIDTH,SCREENHEIGHT,PIPEGAPSIZE,BASEY와 같은 상수를 정의하여 게임의 차원과 속도를 설정합니다.- 게임 에셋을 저장하기 위해 빈 딕셔너리 (
IMAGES및HITMASKS) 를 생성합니다. - 플레이어, 배경 및 파이프 에셋의 목록은 파일 경로를 사용하여 정의됩니다.
시작 화면 애니메이션 표시
이 단계에서는 Flappy Bird 게임의 시작 화면 애니메이션을 생성합니다.
def showWelcomeAnimation():
"""Flappy Bird 의 시작 화면 애니메이션을 표시합니다."""
## 화면에 blit 할 플레이어의 인덱스
playerIndex = 0
playerIndexGen = cycle([0, 1, 2, 1])
## 5 번째 반복마다 playerIndex 를 변경하는 데 사용되는 반복자
loopIter = 0
playerx = int(SCREENWIDTH * 0.2)
playery = int((SCREENHEIGHT - IMAGES["player"][0].get_height()) / 2)
messagex = int((SCREENWIDTH - IMAGES["message"].get_width()) / 2)
messagey = int(SCREENHEIGHT * 0.12)
basex = 0
## base 가 왼쪽으로 최대 이동할 수 있는 양
baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()
## 시작 화면에서 위아래 움직임을 위한 플레이어 shm
playerShmVals = {"val": 0, "dir": 1}
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
## 첫 번째 플랩 소리를 내고 mainGame 에 대한 값을 반환합니다.
return {
"playery": playery + playerShmVals["val"],
"basex": basex,
"playerIndexGen": playerIndexGen,
}
## playery, playerIndex, basex 조정
if (loopIter + 1) % 5 == 0:
playerIndex = next(playerIndexGen)
loopIter = (loopIter + 1) % 30
basex = -((-basex + 4) % baseShift)
playerShm(playerShmVals)
## 스프라이트 그리기
SCREEN.blit(IMAGES["background"], (0, 0))
SCREEN.blit(
IMAGES["player"][playerIndex], (playerx, playery + playerShmVals["val"])
)
SCREEN.blit(IMAGES["message"], (messagex, messagey))
SCREEN.blit(IMAGES["base"], (basex, BASEY))
pygame.display.update()
FPSCLOCK.tick(FPS)
- 시작 화면 애니메이션을 표시하는 역할을 하는
showWelcomeAnimation함수를 정의합니다. - 이 함수는 애니메이션에 대한 변수를 설정하고 게임을 시작하기 위한 사용자 입력을 처리합니다.
- 루프를 사용하여 애니메이션 프레임을 업데이트하고 게임을 시작하기 위한 사용자 입력을 확인합니다.
- 애니메이션에는 새가 날갯짓하는 모습과 화면에 표시되는 메시지가 포함됩니다.
pygame.display.update()함수는 디스플레이를 업데이트하는 데 사용되며,FPSCLOCK.tick(FPS)는 프레임 속도를 제어합니다.
메인 게임 로직 구현
이 단계에서는 Flappy Bird 게임의 메인 게임 로직을 구현합니다.
def mainGame(movementInfo):
score = playerIndex = loopIter = 0
playerIndexGen = movementInfo["playerIndexGen"]
playerx, playery = int(SCREENWIDTH * 0.2), movementInfo["playery"]
basex = movementInfo["basex"]
baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()
## 상단 및 하단 파이프에 추가할 새 파이프 2 개 가져오기
newPipe1 = getRandomPipe()
newPipe2 = getRandomPipe()
## 상단 파이프 목록
upperPipes = [
{"x": SCREENWIDTH + 200, "y": newPipe1[0]["y"]},
{"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[0]["y"]},
]
## 하단 파이프 목록
lowerPipes = [
{"x": SCREENWIDTH + 200, "y": newPipe1[1]["y"]},
{"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[1]["y"]},
]
dt = FPSCLOCK.tick(FPS) / 1000
pipeVelX = -128 * dt
## 플레이어 속도, 최대 속도, 하강 가속도, 플랩 시 가속도
playerVelY = -9 ## Y 축을 따라가는 플레이어의 속도, 기본값은 playerFlapped 와 동일
playerMaxVelY = 10 ## Y 축 최대 속도, 최대 하강 속도
playerMinVelY = -8 ## Y 축 최소 속도, 최대 상승 속도
playerAccY = 1 ## 플레이어의 하강 가속도
playerRot = 45 ## 플레이어의 회전
playerVelRot = 3 ## 각속도
playerRotThr = 20 ## 회전 임계값
playerFlapAcc = -9 ## 플랩 시 플레이어 속도
playerFlapped = False ## 플레이어가 플랩할 때 True
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
if playery > -2 * IMAGES["player"][0].get_height():
playerVelY = playerFlapAcc
playerFlapped = True
## 여기에서 충돌 확인
crashTest = checkCrash(
{"x": playerx, "y": playery, "index": playerIndex}, upperPipes, lowerPipes
)
if crashTest[0]:
return {
"y": playery,
"groundCrash": crashTest[1],
"basex": basex,
"upperPipes": upperPipes,
"lowerPipes": lowerPipes,
"score": score,
"playerVelY": playerVelY,
"playerRot": playerRot,
}
## 점수 확인
playerMidPos = playerx + IMAGES["player"][0].get_width() / 2
for pipe in upperPipes:
pipeMidPos = pipe["x"] + IMAGES["pipe"][0].get_width() / 2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
score += 1
## playerIndex basex 변경
if (loopIter + 1) % 3 == 0:
playerIndex = next(playerIndexGen)
loopIter = (loopIter + 1) % 30
basex = -((-basex + 100) % baseShift)
## 플레이어 회전
if playerRot > -90:
playerRot -= playerVelRot
## 플레이어의 움직임
if playerVelY < playerMaxVelY and not playerFlapped:
playerVelY += playerAccY
if playerFlapped:
playerFlapped = False
## 임계값을 커버하기 위한 더 많은 회전 (가시적 회전으로 계산됨)
playerRot = 45
playerHeight = IMAGES["player"][playerIndex].get_height()
playery += min(playerVelY, BASEY - playery - playerHeight)
## 파이프를 왼쪽으로 이동
for uPipe, lPipe in zip(upperPipes, lowerPipes):
uPipe["x"] += pipeVelX
lPipe["x"] += pipeVelX
## 첫 번째 파이프가 화면 왼쪽 가장자리에 닿으려고 할 때 새 파이프 추가
if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]["x"] < 5:
newPipe = getRandomPipe()
upperPipes.append(newPipe[0])
lowerPipes.append(newPipe[1])
## 화면 밖으로 나간 경우 첫 번째 파이프 제거
if len(upperPipes) > 0 and upperPipes[0]["x"] < -IMAGES["pipe"][0].get_width():
upperPipes.pop(0)
lowerPipes.pop(0)
## 스프라이트 그리기
SCREEN.blit(IMAGES["background"], (0, 0))
for uPipe, lPipe in zip(upperPipes, lowerPipes):
SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))
SCREEN.blit(IMAGES["base"], (basex, BASEY))
## 플레이어가 점수를 겹치도록 점수를 출력합니다.
showScore(score)
## 플레이어 회전에는 임계값이 있습니다.
visibleRot = playerRotThr
if playerRot <= playerRotThr:
visibleRot = playerRot
playerSurface = pygame.transform.rotate(
IMAGES["player"][playerIndex], visibleRot
)
SCREEN.blit(playerSurface, (playerx, playery))
pygame.display.update()
FPSCLOCK.tick(FPS)
mainGame함수를 정의하며, 이 함수는 Flappy Bird 게임의 핵심 로직을 포함합니다.- 이 함수는 사용자 입력을 처리하고, 게임 상태를 업데이트하며, 충돌을 확인하고, 점수를 추적합니다.
- 게임 루프는 지속적으로 실행되어 게임의 디스플레이와 로직을 업데이트합니다.
- 플레이어 컨트롤은 키 이벤트 (스페이스 또는 위쪽 화살표) 를 통해 처리됩니다.
- 이 함수는 또한 파이프 및 지면과의 충돌을 확인하고, 점수를 업데이트하며, 새의 애니메이션을 관리합니다.
- 게임 루프는 플레이어가 충돌하거나 게임을 종료할 때까지 계속됩니다.
게임 오버 화면 표시
이 단계에서는 플레이어가 졌을 때 나타나는 게임 오버 화면을 생성합니다.
def showGameOverScreen(crashInfo):
"""플레이어를 추락시키고 게임 오버 이미지를 표시합니다."""
score = crashInfo["score"]
playerx = SCREENWIDTH * 0.2
playery = crashInfo["y"]
playerHeight = IMAGES["player"][0].get_height()
playerVelY = crashInfo["playerVelY"]
playerAccY = 2
playerRot = crashInfo["playerRot"]
playerVelRot = 7
basex = crashInfo["basex"]
upperPipes, lowerPipes = crashInfo["upperPipes"], crashInfo["lowerPipes"]
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
if playery + playerHeight >= BASEY - 1:
return
## 플레이어 y 이동
if playery + playerHeight < BASEY - 1:
playery += min(playerVelY, BASEY - playery - playerHeight)
## 플레이어 속도 변경
if playerVelY < 15:
playerVelY += playerAccY
## 파이프 충돌일 때만 회전
if not crashInfo["groundCrash"]:
if playerRot > -90:
playerRot -= playerVelRot
## 스프라이트 그리기
SCREEN.blit(IMAGES["background"], (0, 0))
for uPipe, lPipe in zip(upperPipes, lowerPipes):
SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))
SCREEN.blit(IMAGES["base"], (basex, BASEY))
showScore(score)
playerSurface = pygame.transform.rotate(IMAGES["player"][1], playerRot)
SCREEN.blit(playerSurface, (playerx, playery))
SCREEN.blit(IMAGES["gameover"], (50, 180))
FPSCLOCK.tick(FPS)
pygame.display.update()
showGameOverScreen함수는 플레이어가 졌을 때 게임 오버 화면을 표시합니다.- 플레이어의 최종 점수를 표시하고, 사운드 효과를 재생하며, 플레이어가 스페이스 또는 위쪽 화살표를 눌러 게임을 다시 시작할 때까지 기다립니다.
- 애니메이션에는 새가 땅에 떨어지는 모습과 화면에 표시되는 게임 오버 메시지가 포함됩니다.
헬퍼 함수 정의
이 단계에서는 게임에서 사용되는 헬퍼 함수를 정의합니다.
def playerShm(playerShm):
"""playerShm['val'] 의 값을 8 과 -8 사이에서 진동시킵니다."""
if abs(playerShm["val"]) == 8:
playerShm["dir"] *= -1
if playerShm["dir"] == 1:
playerShm["val"] += 1
else:
playerShm["val"] -= 1
def getRandomPipe():
"""무작위로 생성된 파이프를 반환합니다."""
## 상단 및 하단 파이프 사이의 간격 y
gapY = random.randrange(0, int(BASEY * 0.6 - PIPEGAPSIZE))
gapY += int(BASEY * 0.2)
pipeHeight = IMAGES["pipe"][0].get_height()
pipeX = SCREENWIDTH + 10
return [
{"x": pipeX, "y": gapY - pipeHeight}, ## 상단 파이프
{"x": pipeX, "y": gapY + PIPEGAPSIZE}, ## 하단 파이프
]
def showScore(score):
"""화면 중앙에 점수를 표시합니다."""
scoreDigits = [int(x) for x in list(str(score))]
totalWidth = 0 ## 인쇄할 모든 숫자의 총 너비
for digit in scoreDigits:
totalWidth += IMAGES["numbers"][digit].get_width()
Xoffset = (SCREENWIDTH - totalWidth) / 2
for digit in scoreDigits:
SCREEN.blit(IMAGES["numbers"][digit], (Xoffset, SCREENHEIGHT * 0.1))
Xoffset += IMAGES["numbers"][digit].get_width()
def checkCrash(player, upperPipes, lowerPipes):
"""플레이어가 지면 또는 파이프와 충돌하는 경우 True 를 반환합니다."""
pi = player["index"]
player["w"] = IMAGES["player"][0].get_width()
player["h"] = IMAGES["player"][0].get_height()
## 플레이어가 지면에 충돌하는 경우
if player["y"] + player["h"] >= BASEY - 1:
return [True, True]
else:
playerRect = pygame.Rect(player["x"], player["y"], player["w"], player["h"])
pipeW = IMAGES["pipe"][0].get_width()
pipeH = IMAGES["pipe"][0].get_height()
for uPipe, lPipe in zip(upperPipes, lowerPipes):
## 상단 및 하단 파이프 사각형
uPipeRect = pygame.Rect(uPipe["x"], uPipe["y"], pipeW, pipeH)
lPipeRect = pygame.Rect(lPipe["x"], lPipe["y"], pipeW, pipeH)
## 플레이어 및 상단/하단 파이프 히트마스크
pHitMask = HITMASKS["player"][pi]
uHitmask = HITMASKS["pipe"][0]
lHitmask = HITMASKS["pipe"][1]
## 새가 상단 파이프 또는 하단 파이프와 충돌한 경우
uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask)
lCollide = pixelCollision(playerRect, lPipeRect, pHitMask, lHitmask)
if uCollide or lCollide:
return [True, False]
return [False, False]
def pixelCollision(rect1, rect2, hitmask1, hitmask2):
"""두 객체가 단순히 사각형이 아닌 충돌하는지 확인합니다."""
rect = rect1.clip(rect2)
if rect.width == 0 or rect.height == 0:
return False
x1, y1 = rect.x - rect1.x, rect.y - rect1.y
x2, y2 = rect.x - rect2.x, rect.y - rect2.y
for x in range(rect.width):
for y in range(rect.height):
if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]:
return True
return False
def getHitmask(image):
"""이미지의 알파를 사용하여 히트마스크를 반환합니다."""
mask = []
for x in range(image.get_width()):
mask.append([])
for y in range(image.get_height()):
mask[x].append(bool(image.get_at((x, y))[3]))
return mask
playerShm함수는playerShm["val"]의 값을 8 과 -8 사이에서 진동시킵니다. 이는 시작 화면에서 새를 위아래로 움직이는 데 사용됩니다.getRandomPipe함수는 무작위로 생성된 파이프를 반환합니다. 상단 및 하단 파이프 사이에 무작위 간격을 생성합니다.showScore함수는 화면 중앙에 점수를 표시합니다.IMAGES["numbers"]목록을 사용하여 점수를 표시합니다.checkCrash함수는 플레이어가 지면 또는 파이프와 충돌하는 경우True를 반환합니다.pixelCollision함수를 사용하여 충돌을 확인합니다.pixelCollision함수는 두 객체가 단순히 사각형이 아닌 충돌하는지 확인합니다.getHitmask함수를 사용하여 플레이어 및 파이프에 대한 히트마스크를 가져옵니다.getHitmask함수는 이미지의 알파를 사용하여 히트마스크를 반환합니다.image.get_at함수를 사용하여 이미지의 각 픽셀의 알파 값을 가져옵니다.
메인 함수
이 단계에서는 게임을 초기화하고 게임 루프를 시작하는 메인 함수를 정의합니다.
def main():
global SCREEN, FPSCLOCK
pygame.init()
FPSCLOCK = pygame.time.Clock()
SCREEN = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("Flappy Bird")
## 점수 표시를 위한 숫자 스프라이트
IMAGES["numbers"] = (
pygame.image.load("data/sprites/0.png").convert_alpha(),
pygame.image.load("data/sprites/1.png").convert_alpha(),
pygame.image.load("data/sprites/2.png").convert_alpha(),
pygame.image.load("data/sprites/3.png").convert_alpha(),
pygame.image.load("data/sprites/4.png").convert_alpha(),
pygame.image.load("data/sprites/5.png").convert_alpha(),
pygame.image.load("data/sprites/6.png").convert_alpha(),
pygame.image.load("data/sprites/7.png").convert_alpha(),
pygame.image.load("data/sprites/8.png").convert_alpha(),
pygame.image.load("data/sprites/9.png").convert_alpha(),
)
## 게임 오버 스프라이트
IMAGES["gameover"] = pygame.image.load("data/sprites/gameover.png").convert_alpha()
## 시작 화면용 메시지 스프라이트
IMAGES["message"] = pygame.image.load("data/sprites/message.png").convert_alpha()
## 기본 (지면) 스프라이트
IMAGES["base"] = pygame.image.load("data/sprites/base.png").convert_alpha()
while True:
## 무작위 배경 스프라이트 선택
randBg = random.randint(0, len(BACKGROUNDS_LIST) - 1)
IMAGES["background"] = pygame.image.load(BACKGROUNDS_LIST[randBg]).convert()
## 무작위 플레이어 스프라이트 선택
randPlayer = random.randint(0, len(PLAYERS_LIST) - 1)
IMAGES["player"] = (
pygame.image.load(PLAYERS_LIST[randPlayer][0]).convert_alpha(),
pygame.image.load(PLAYERS_LIST[randPlayer][1]).convert_alpha(),
pygame.image.load(PLAYERS_LIST[randPlayer][2]).convert_alpha(),
)
## 무작위 파이프 스프라이트 선택
pipeindex = random.randint(0, len(PIPES_LIST) - 1)
IMAGES["pipe"] = (
pygame.transform.flip(
pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), False, True
),
pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(),
)
## 파이프용 히트마스크
HITMASKS["pipe"] = (
getHitmask(IMAGES["pipe"][0]),
getHitmask(IMAGES["pipe"][1]),
)
## 플레이어용 히트마스크
HITMASKS["player"] = (
getHitmask(IMAGES["player"][0]),
getHitmask(IMAGES["player"][1]),
getHitmask(IMAGES["player"][2]),
)
movementInfo = showWelcomeAnimation()
crashInfo = mainGame(movementInfo)
showGameOverScreen(crashInfo)
main함수는 게임을 초기화하고, 디스플레이를 설정하며, 게임 루프를 시작합니다.- 이미지와 같은 게임 에셋을 로드하고, 배경, 플레이어 및 파이프 스프라이트를 무작위로 선택합니다.
- 게임 루프는 시작 애니메이션, 메인 게임 및 게임 오버 화면을 처리합니다.
- 플레이어가 게임을 종료하거나 창을 닫을 때까지 게임은 계속 루프됩니다.
게임 실행
이 단계에서는 Flappy Bird 게임을 실행합니다.
if __name__ == "__main__":
main()
__name__ == "__main__"조건은 현재 모듈이 자체적으로 실행되는지 또는 다른 모듈에 의해 가져와지는지 확인합니다.- 현재 모듈이 자체적으로 실행되는 경우,
main함수가 호출되어 게임을 시작합니다.
모든 단계를 완료했으면 다음 명령을 사용하여 Flappy Bird 게임을 실행할 수 있습니다.
cd ~/project
python flappy.py

요약
이 프로젝트에서는 Flappy Bird 게임 코드를 여러 단계로 나누고 각 단계에 대한 설명을 제공했습니다. 기본적인 게임 구조를 만들고, 플레이어 입력을 처리하고, 게임 상태를 업데이트하고, 충돌을 확인하고, 게임 화면을 표시하는 방법을 배웠습니다. 이제 제공된 코드를 사용하여 Flappy Bird 게임을 실행하고 플레이할 수 있습니다. 게임 개발 여정을 즐기세요!



