[pygame] 워미- 니블클론게임 - 1

서희찬·2021년 4월 17일
0

이제 4번째 게임 워미를 만들어보자 !
워미가 뭔지 아는가!
바로 니블게임의 클론으로 이름을 지은것이다!
그렇다면 니블게임은 아는가!

플레이어가 벌레를 조종하며 무작위로 나타나는 사과를 먹으면서 커가는 게임이다!
사과를 먹으면 커지고 벽을 만나면 게임오버!


이런 게임이당!
플레이어는 작은 벌레를 조종해 화면 전체를 돌아다니고, 벌레를 멈추고나 천천히 움직일 수 없다!
오직 방향만 바꿔가며 사과를 먹어야한다!
벌레가 자신의 몸이나 스크린의 벽에 부딪히면 게임은 종료한다 !

셋업 코드

import random, pygame, sys
from pygame.locals import *

FPS = 15
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 20 
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size."
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size."
CELLWIDTH = int(WINDOWWIDTH / CELLSIZE)
CELLHEIGHT = int(WINDOWHEIGHT / CELLSIZE)

aseert문으로 정수개의 셀이 윈도우 안에 들어가는지 확인한다.

#             R    G    B
WHITE     = (255, 255, 255)
BLACK     = (  0,   0,   0)
RED       = (255,   0,   0)
GREEN     = (  0, 255,   0)
DARKGREEN = (  0, 155,   0)
DARKGRAY  = ( 40,  40,  40)
BGCOLOR = BLACK

UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'

HEAD = 0 #신태틱 슈가 : 벌레의 머리부분의 인덱스 표시 

이제 기본적인 변수설정이 끝이 났으니 main함수를 시작해보자 !

main()함수

def main():
    global FPSCLOCK, DISPLAYSURF, BASICFONT

    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH,WINDOWHEIGHT))
    BASICFONT = pygame.font.Font("freesansbold.ttf",18)
    pygame.display.set_caption("Chans Wormy")

    showStartScreen()
    while True:
        runGame()
        showGameOverScreen()

이번게임의 main함수는 다른게임들과 다르게 간단한 편이다!

showStartScreen()반복하는 함수가 아니라 시작했을때 한번만 출력되는 화면이여야 하므로 반복문에서 뺐다!

"게임 시작" 화면을 보여준 다음 runGame()을 호출해서 진행되다가 게임이 종료되면 showGameOverScreen()을 호출해서 게임 종료 화면을 보여준다.
이 함수가 반환되면 순환문은 위로 돌아가서 runGame()을 호출한다!
게임이 종료될때까지 계속 수행한다.

분리한 runGame()함수

def runGame():
    # 무작위로 시작하는 위치 정하기 
    startx = random.randint(5,CELLWIDTH - 6)
    starty = random.randint(5,CELLHEIGHT - 6)
    wormCoords=[
        {
            "x":startx,
            "y":starty
        },
        {
            "x":startx - 1,
            "y":starty
        },
        {
            "x":startx -2,
            "y":starty
        }
    ]
    direction = RIGHT

    #사과를 무작위로 정한 위치에 놓기 
    apple = getRandomLocation()

플레이어가 조종할 벌레는 무작위 위치에서 시작되는데 벌레의 초기 길이는 3이다!
그리고 이 위치는 게임판의 가장자리와 너무 가까우면 안되므로 랜덤의 범위를 저렇게 한것이다!

벌레의 몸 부분은 딕셔너리 값의 리스트로 저장된다.
벌레의 몸 마디 하나하나마다 딕셔너리 값을 할당한다.
머리는 startx,starty이고 몸 마디 다른 부분은 머리의 왼쪽에 놓이게 된다.

벌레의 머리부분은 항상 wormCoords[0]에 위치하고 우리는 신태틱슈가를 썼으므로 wormCoords[head] 라고 쓰자 !

이벤트 루프처리

    while True: #메인 게임 루프
        for event in pygame.event.get(): #이벤트 핸들 
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if(event.key == K_LEFT or event.key == K_a) and direction != RIGHT:
                    direction = LEFT
                elif (event.key == K_RIGHT or event.key == K_d) and direction != LEFT :
                    direction = RIGHT
                elif (event.key = K_UP or event.key == K_w) and direction != DOWN:
                    direction = UP
                elif (evnet.key = K_DOWN or event.key = K_s) and direction != UP:
                    direction = DOWN
                elif event.key = K_ESCAPE:
                    terminate()

만약 KEYDOWN 이벤트가 발생했으면 이 키가 화살표 키이거나 WASD키 인지 확인한다.
하지만 여기에 벌레가 자기에게 충돌하는지 검사해 보는 과정이 필요하다!
벌레가 왼쪽으로 이동하고 있는데 오른쪽 화살표 키를 누르면 자기 자신과 충돌한다 !

따라서 direction 변수를 같이 검사해서 자기 자신과 충돌하는 키를 누르면 이 키는 무시한다.

충돌 감지

        #벌레가 자기 자신이나 벽과 충돌했는지 검사한다.
        if wormCoords[HEAD]["x"] == -1 or wormCoords[HEAD]['x'] == CELLWIDTH or wormCoords[HEAD]['y'] == -1 or wormCoords[HEAD]['y'] == CELLHEIGHT :
            return #end game
        for wormBody in wormCoords[1:]:
            if wormBody['x'] == wormCoords[HEAD]['x'] and wormBody['y'] == wormCoords[HEAD]['y']:
                return # end game 

벌레의 머리 부분이 게임판의 테두리에 닿거나 머리 부분이 몸의 다른 부분에 닿으면 충돌한것으로 되어 게임을 종료한다 !

충돌했으면 return 해서 runGame()->showGameOverSacreen() 함수가 호출된다.

머리외의 몸 부분과 충돌을 검사하기 때문에 wormCoords[1:] 을 사용한다.

사과와의 충돌 감지

     #벌레가 사과를 먹었는지 검사한다.
     if wormCoords[HEAD]['x'] == apple['x'] and wormCoords[HEAD]['y'] == apple['y']:
         #벌레의 꼬리 부분을 지우지 말 것 
         apple = getRandomLocation() # new APple
     else :
         del wormCoords[-1] # 벌레의 꼬리부분을 지운다?

[-1]은 리스트의 제일 마지막 아이템을 가리킨다.
벌레의 꼬리 부분을 지우고 머리 부분에 하나씩 마디를 추가 해야지 앞으로 나아가면서 벌레의 길이는 똑같이 유지된다.

벌레 움직이기

        # 움직이는 방향으로 마디를 더해서 벌레를 움직인다.
        if direction == UP:
            newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] - 1}
        elif direction == DOWN:
            newHead = {'x': wormCoords[HEAD]['x'], 'y': wormCoords[HEAD]['y'] + 1}
        elif direction == LEFT:
            newHead = {'x': wormCoords[HEAD]['x'] - 1, 'y': wormCoords[HEAD]['y']}
        elif direction == RIGHT:
            newHead = {'x': wormCoords[HEAD]['x'] + 1, 'y': wormCoords[HEAD]['y']}
        wormCoords.insert(0,newHead)

벌레를 움직이기 위해 wormCoords 리스트의 시작 부분에 새로운 마디를 하나씩 더해나가야한다.

새로운 머리의 좌표는 이전 머리의 바로 옆 좌표 이므로 x좌표에 1을 더하거나 뺄 것인지 혹은 y좌표이 1을 더하거나 뺄것인지 진행방향에 따라 달라진다.

insert() 리스트 메소드

append는 리스트의 맨 뒷부분에 아이템을 추가하는 반면 insert()는 어디에나 아이템을 더할 수 있다.
insert(추가할 위치 인덱스값, 추가할 아이템)으로 이뤄져 있다.

화면 그리기

        DISPLAYSURF.fill(BGCOLOR)
        drawGrid()
        drawWorm(wormCoords)
        drawApple(apple)
        drawScore(len(wormCoords)-3)
        pygame.display.update()
        FPSCLOCK.tick(FPS)

Surface를 그리고 격자,벌레,사과,점수를 그린다!
그리고 업데이트 하여 디스플레이 Surface를 실제 화면상에 반영한다.

화면에 "Press a Key"텍스트 그리기

def drawPressKeyMsg():
    pressKeySurf = BASICFONT.render("Press a Key to play",True,DARKGRAY)
    pressKeyRect = pressKeySurf.get_rect()
    pressKeyRect.topleft = (WINDOWWIDTH - 200,WINDOWHEIGHT - 30)
    DISPLAYSURF.blit(pressKeySurf,pressKeyRect)

매번 호출하는 대신 함수를짜주어서 게임이 시작/오버 할때 호출한다!

checkForKeyPress()함수

def checkForKeyPress():
    if len(pygame.event.get(QUIT)) > 0 :
        terminate()
    
    KeyUpEvents = pygame.event.get(KEYUP)
    if len(KeyUpEvents) == 0 :
        return None 
    if KeyUpEvents[0].key == K_ESCAPE:
        terminate()
    return KeyUpEvents[0].key

pygame.event.get(QUIT) 으로 QUIT 인자를 주면 QUIT 이벤트만 돌려준다.
만약 QUIT 이벤트가 발생하지 않았다면 빈리스트[]를 반환한다.

빈 리스틀 반환했다면 len() 값은 0이다.
이 값이 1 이상 수이면 QUIT 이벤트가 발생한 것이므로 종료!
발생한적이 없다면 KEYUP이벤트를 가져온다.
만약 esc키를 누뤘는지 확인하고 이 역시 없다면 pygame.event.get 에서 반환한 첫 번째 키 이벤트 객체를 반환한다.

시작화면

def showStartScreen():
    titleFont = pygame.font.Font("freesansbold.ttf",100)
    titleSurf1 = titleFont.render("Wormy!",True, WHITE, DARKGREEN)
    titleSurf2 = titleFont.render("Wormy!",True, GREEN)

    degrees1 = 0
    degress2 = 0
    while True : 
        DISPLAYSURF.fill(BGCOLOR)

시작화면을 보여주고 시작하지 않는다면 시작하자마자 벽에 박아 게임이 끝날것이다.

시작화면 텍스트 회전하기

    while True : 
        DISPLAYSURF.fill(BGCOLOR)
        rotatedSurf1 = pygame.transform.rotate(titleSurf1,degrees1)
        rotatedRect1 = rotatedSurf1.get_rect()
        rotatedRect1.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)
        DISPLAYSURF.blit(rotatedSurf1,rotatedRect1)

        rotatedSurf2 = pygame.transform.rotate(titleSurf2,degress2)
        rotatedRect2 = rotatedSurf2.get_rect()
        rotatedRect2.center =(WINDOWWIDTH/2,WINDOWHEIGHT/2)
        DISPLAYSURF.blit(rotatedSurf2,rotatedRect2)

        drawPressKeyMsg()

        if checkForKeyPress():
            pygame.event.get()
            return 
        pygame.display.update()
        FPSCLOCK.tick(FPS)

rotate 는 회전할 이미지의 복사본을 만들 Surface 객체이고 두 번째 파라미터는 Surface 객체를 얼마나 회전시킬지 정하는 각도이다.

이 새로운 surface는 당연 빠따로 돌리면 기존에 객체 사각형 보다 더 큰 사각형을 차지한다.
이 함수는 플레이어가 키보드를 눌러서 checkForKeyPress() 가 None이 아닌 값을 반환할 때까지 계속 보여준다.
반환전에 이벤트 큐에 들어있는 이벤트들을 모두 비운다.

회전만으로는 완전하지 않다.

왜 회전한 Surface 객체를 따로 변수로 할당하는지 궁금하지 않는가?

우선, 2D 이미지를 회전하는 동작은 완벽하게 작동하지 않는다.
회전한 이미지는 원래의 이미지에 상당히 많은 부분을 생략한 이미지이다.(복사기를 생각하면된다.)
예외적으로 90도씩 회전할때는 품질이 떨어지지않지만 그 외에는 왜곡현상이 생기기 마련이다.

두 번째로 2D이미지를 회전하면 회전한 이미지는 원래의 이미지보다 약간 커진다. 회전하면 또 커진다 ! 그렇기에
원래의 이미지를 계속 회전하면 error가 떠서 종료될것이다.

degrees1 += 3
        degress2 += 7

회전한당


def terminate():
    pygame.quit()
    sys.exit()

종료 함수당

이제 다음 함수들은 다음 포스트에서 소개하겠다.

profile
부족한 실력을 엉덩이 힘으로 채워나가는 개발자 서희찬입니다 :)

0개의 댓글