[python library] pygame 1. 개요 및 최적화

Study·2021년 7월 31일
2
post-thumbnail

기본사항

pygame

Pygame 은 크로스 플랫폼 멀티미디어 라이브러리인 SDL의 래퍼로서 파이썬을 돌릴 수 있는 플랫폼이라면 어디서든 실행할 수 있다.

게임 개발 도구이지만 이미지 프로세스 또는 조이스틱 입력, 음악 재생 등의 기능을 떼어 쓸 수도 있다.

오픈 소스에다 무료 도구이다.

파이게임을 통해 파이썬 문법을 더 돈독히 익히며 추후에 딥러닝에 사용할 게임을 미리 만들면서 익혀보자.

최소한의 코드

프로그램을 실행하기 위한 최소한의 파이게임은 다음과 같다.

import pygame


# 메인 함수 정의
def main():
    # 파이게임 모듈 초기화
    pygame.init()
    # 로고 로드하고 세팅
    logo = pygame.image.load("logo32x32.png")
    pygame.display.set_icon(logo)
    pygame.display.set_caption("minimal program")

    # 240 x 180 사이즈의 스크린 표면을 만듦
    screen = pygame.display.set_mode((240, 180))

    # 메인 루프를 제어할 변수 정의
    running = True

    # 메인 루프
    while running:
        # 이벤트 핸들러, 이벤트 큐로부터 모든 이벤트를 얻는다.
        for event in pygame.event.get():
            # QUIT 타입의 이벤트라면 다음 코딩을 실행
            if event.type == pygame.QUIT:
                # 메인 루프를 탈출하기 위해 변수를 False 로 바꾼다.
                running = False


# 현재 모듈이 메인 스크립트라면 메인 함수 실행
# (이 모듈을 임포트하면 아무것도 실행하지 않는다)
if __name__ == "__main__":
    # call the main function
    main()
함수기능
pygame.init()init() -> (numpass, numfail)
임포트된 모든 파이게임 모듈들을 초기화
각 모듈을 개별적으로 초기화할 수 있지만 pygame.init() 이 가장 쉬운 방법
pygame.display.set_mode()set_mode(size=(0, 0), flags=0, depth=0, display=0, vsync=0) -> Surface
스크린 표면을 생성
매개변수 사이드를 통해 높이와 너비를 지정할 수 있다.
보통 매개변수 depth 는 전달하지 않는 것이 최적이다.
풀 스크린 모드를 요청할 때 정확히 전체 화면으로 되지 않을 수 있다. 이런 상황에선 가장 가까운 호환되는 일치 항목을 선택한다.
고해상도(4k, 1080p) 이나 작은 게임(640x480) 등에서 매우 작게 표시되어 재생할 수 없을 수 있는데, SCALED 가 창을 확장한다. 그래서 640x480 창은 실제로 더 클 수 있다. 이에 마우스 이벤트는 조정되므로 걱정할 필요가 없다.
pygame.event.get()get(eventtype=None) -> Eventlist
get(eventtype=None, pump=True) -> Eventlist
큐에서 이벤트를 얻는다.

이벤트 유형은 다음과 같다.

타입               리턴

QUIT              none
ACTIVEEVENT       gain, state
KEYDOWN           key, mod, unicode, scancode
KEYUP             key, mod
MOUSEMOTION       pos, rel, buttons
MOUSEBUTTONUP     pos, button
MOUSEBUTTONDOWN   pos, button
JOYAXISMOTION     joy (deprecated), instance_id, axis, value
JOYBALLMOTION     joy (deprecated), instance_id, ball, rel
JOYHATMOTION      joy (deprecated), instance_id, hat, value
JOYBUTTONUP       joy (deprecated), instance_id, button
JOYBUTTONDOWN     joy (deprecated), instance_id, button
VIDEORESIZE       size, w, h
VIDEOEXPOSE       none
USEREVENT         code

위 예제를 실행하면 다음과 같이 아무 것도 할 수 없는 창이 뜬다. 유일한 것은 닫는 것이다.

닫기 버튼을 클릭하면 QUIT 이벤트가 생성되어 메인 루프에서 QUIT 이벤트를 처리하므로 실제로 닫힌다.

파이게임에서의 표면

파이게임에는 이미지를 나타내는 표면이란 객체가 있다. 이는 화면에 필요한 정보를 저장하는 데이터 구조이며, 다른 형식을 가질 수도 있다.(참조)

블리팅(표면을 그리는 용어) 속도를 향상시크는 몇 가지 방법이 있는데, convert()convert_alpha() (이미지에 픽셀 당 알파) 의 두 함수가 있다.
표면을 블리팅하는 것이 더 빠르기 때문에 표면을 화면 표면의 형식으로 변환하는 것이다.

블리트

첫 블리트

화면에 이미지를 넣어보자. blit 함수는 한 표면에서 다른 표면으로 픽셀을 복사한다. 그 전에 미리 이미지를 로드해야 한다.
이미지 모듈인 load()에서 살펴볼 수 있다.

image = pygame.image.load("01_image.png")

이미지를 로드했다면 블리팅해보자.

screen.blit(image, (50, 50))

블리팅을 통해 이미지 표면 픽셀을 스크린 표면에 복사하는 것이다. 위치(50, 50) 에 나타나는 것을 볼 수 있다.
아직은 화면이 검은색일텐데, 화면을 업데이트해주어야 한다.
화면 업데이트는 pygame.display.flip()을 사용하여 수행한다.

pygame.display.flip()

이제 화면에 표시되는 것을 볼 수 있다.

배경

배경은 다른 이미지거나 채워진 표면일 수도 있다. 이번엔 이런 배경 위에 이미지를 블리팅하는 것이다.
가장 쉽게 배경을 채우는 것은 fill(color, rect=None) 함수이다.

screen.fill((r,g,b))

위는 RGB 를 빨강으로 처리하고 난 실행 결과다.

배경 이미지를 사용하는 것도 비슷하지만 채우는 대신 배경 이미지를 먼저 블리팅 처리한다.

screen.blit(bgd_image, (0, 0))

화면 왼쪽 상단인 모서리 (0, 0)에서 블리팅한다.
배경 이미지 bgd_image 의 크기가 화면과 같아야한다. (또는 더 크면 잘린 부분은 표시되지 않음)

이미지를 블리팅 처리하는 순서를 유의하자. 먼저 배경, 그 다음 이미지이다.

투명도

파이게임에서 오브젝트를 투명하게 만드는 3가지 방법이 잇다.

  • Colorkey 로 정의된 색상의 픽셀은 그려지지 않는다. 즉, 100% 투명이다.
  • 픽셀당 알파 : 페인팅 프로그램이나 렌더러 등의 도구로 이미지를 만드는 경우 이미지에 반투명 픽셀이 있을 수 있다.
  • 이미지별 알파 : 모든 픽셀은 동일한 알파 값을 사용하여 그린다.

Colorkey

이전 예제들에서 스마일 그림에서 못생긴 분홍색 테두리가 있었다.
하나의 색상만 완전 투명하게 만드는 colorkey 라는 것이 있다.
함수는 매우 간단하며 set_colorkey(color)와 같다.

image.set_colorkey((255, 0, 255))

이미지에 알파값이 설정되어 있으면 colorkey 가 작동하지 않는다.
colorkey 가 작동하도록 하는 간단한 트릭은 image.set_alpha(None)를 사용하지 않도록 설정한 다음 set_colorkey(..) 를 사용할 수 있다.

알파

알파값을 사용하여 이미지를 투명하게 해보자.
사용하는 코드는 colorkey를 사용하는 것처럼 간단하다.

image.set_alpha(128)

위와 같이 이미지를 반투명하게 만들어보았다.
앞서 언급한 것과 같이 alpha 와 colorkey 를 사용한 몇 가지 문제를 피하기 위해서 SDL 문서에서는 조합들을 설명해주고 있다.

표면당 알파값 128은 특수한 경우로 간주되어 최적화되고 다른 표면값보다 훨씬 빠르다.

  • RGB : 픽셀당 알파가 없는 표면
  • RGBA : 픽셀당 알파가 있는 표면
  • SDL_SRCALPHA : 표면당 알파가 있는 표면
  • SDL_COLORKEY : 색상 키를 사용한 표면

움직이기

이번엔 화면에 객체를 움직이도록 해보자.
간단하게 스마일 얼굴의 위치를 움직이도록 하기 위해 몇 가지 변수가 필요하다.

# 스마일의 위치 값 정의
xpos = 50
ypos = 50
# 각 프레임 별로 얼마나 움직일 지 정함
step_x = 10
step_y = 10

그 다음으로 해야할 작업은 루프를 반복할 때마다 스텝 크기만큼 위치를 변경시키는 것이다.
그래서 메인 루프에 다음과 같이 작성한다.

# 스마일이 스크린에 있는지, 방향이 변하지 않는지 체크한다.
if xpos > screen_width - 64 or xpos < 0:
    step_x = -step_x
if ypos > screen_height - 64 or ypos < 0:
    step_y = -step_y
# 스마일의 위치 값을 업데이트한다.
xpos += step_x
ypos += step_y

화면에 스마일을 유지하기 위해 두 개의 if 문을 사용했다.
xpos > screen_width - 64 에서 - 64 에 대한 의문을 가질 수 있는데, 이는 blit 위치가 항상 이미지의 왼쪽 상단 모서리에 있기 때문이다.
xpos > screen_width 만으로 확인하면 스마일은 방향을 바꾸고 다시 돌아오기 전에 화면에서 사라질 것이다.

위 예제로 작성한 코드로는 스마일이 움직이지 않을 것이다.
기본 루프에 업데이트하는 코드를 넣자.

# 스마일을 스크린에 블리팅한다.
screen.blit(image, (xpos, ypos))
# 그리고 스크린을 업데이트 한다.
pygame.display.flip()
# 파이게임을 지정한 시간 만큼 멈춘다.
pygame.time.wait(1000)

하지만 위 화면을 보면 무엇인가 이상함을 알 수 있다. 그것은 스마일의 잔상이 남아있다는 것이다. 이는 이 전의 각 위치에 스마일 이미지가 남아있기 때문이다.

그래서 다음 방법을 통하여 스마일의 이전 이미지를 화면에서 지워야 한다. 방법은 화면에 있는 모든 것 위에 배경을 덮어 씌우는 것이다.

# 화면을 지운다. (화면을 배경화면을 덮음)
screen.blit(background, (0, 0))
# 그 다음 스마일을 블리팅한다.
screen.blit(image, (xpos, ypos))
# 그리고 스크린을 업데이트 한다.
pygame.display.flip()
# 파이게임을 지정한 시간 만큼 멈춘다.
pygame.time.wait(1000)

지금까지 진행한 메인 루프에 대한 수행을 요약하면 다음과 같다.

  1. 객체 업데이트(이동 및 변경 등)
  2. 배경을 사용하여 객체 지우기(덮어씌우기)
  3. 화면에 객체들 그리기
  4. flip() 또는 update() 함수로 화면을 업데이트

최적화

최적화를 설명하기 전에 먼저 pygame.Rect 를 소개해야 한다.
다음 그림을 잘 살펴보자.

왼쪽 위 모서리를 (100, 100) 에 놓고 싶다고 가정하자.
그럼 다음과 같이 작성할 수 있다. r.topleft = (100, 100)

일단 rect 객체를 사용하는 방법을 더 살펴보고 논의하자.

지저분한 사각형

일부 영역만 변경될 경우, 왜 변경된 부분만 업데이트하지 않고 전체 화면을 업데이트할까?
이런 영역은 다시 그릴 필요가 있는, 일반적으로 직사각형 모양이기 때문에 Dirty Rects 라고 한다.

이 영역은 blit()pygame.Rect 로 반환한다.
우리가 할 일은 rect 를 리스트에 저장하는 것 뿐이다. 그 후 flip() 대신 update() 를 사용하는 것이다.

update() 는 rect 리스트를 인수로 사용하여 앞서 설명한 영역에서 업데이트된다. 하지만 이리저리 움직이는 화면이 올바르게 업데이트 될까?

위 그림의 원래 있던 영역(파란색 영역)도 업데이트되어야 한다. 이를 old_rect 라 칭하자.

대부분의 스프라이트(sprite, 은 2차원 비트맵이나 애니메이션을 합성하는 기술)는 한 프레임에서 멀리 이동하지 않기 때문에 3 의 경우와 같이 교차점(분홍색 영역)이 있다. 그래서 두 영역을 독립적으로 업데이트하면 잘 작동하지만 3 의 분홍색 영역은 두 번 업데이트되므로 성능이 좋지 않다.

가장 간단한 방법은 4와 같이 두 개의 사각형을 결합하는 것이다.
노란색 영역은 지저분하지 않았지만 업데이트된다.
따라서 두 개의 직사각형(파란색, 녹색) 대신 이 둘 다 포함하는 하나의 큰 직사각형이 있다는 것이다.

이 방법은 pygame.sprites.RenderUpdates에서 확인할 수 있다. 어쨋든 두 rect 를 모두 업데이트해야하기 때문에 5의 경우는 흥미로운 부분은 아니다.

# pygame.sprites.RenderUpdates 로부터 그리는 함수
def draw(self, surface):
   spritedict = self.spritedict               # {sprite:old_rect}
   surface_blit = surface.blit
   dirty = self.lostsprites                   # dirty rects (제거된 스프라이트)
   self.lostsprites = []             
   dirty_append = dirty.append
   for s in self.sprites():
       r = spritedict[s]                      # old_rect 를 얻음
       newrect = surface_blit(s.image, s.rect)# 블리팅
       if r is 0:                             # 처음엔 old_rect 는 0
           dirty_append(newrect)              # 블리팅하여 사각형 추가
       else:
           if newrect.colliderect(r):         # old_rect 와 newrect가 겹치면, 3의 경우임
               dirty_append(newrect.union(r)) # 두 사각형을 결합하여 붙임
           else:
               dirty_append(newrect)          # old_rect 와 newrect 가 겹치지 않으면
               dirty_append(r)                # 5의 경우임
       spritedict[s] = newrect                # 오래된 걸 새 걸로 바꿈
   return dirty                               # dirty rect 리스트 반환

위의 방법은 결코 최선의 방법이 아니며 이동 중에 겹치는 움직이는 스프라이트가 많은 경우 이 방식은 여전히 많은 영역을 두 번 업데이트해야 하므고 더 큰 단점이 있다.

모든 스프라이트를 지우고 다시 그려야하기 때문에 다음 섹션을 더 살펴본다.

지저분한 영역 결합

위 이미지를 보면 알 수 있듯, 겹치는 부분이 있다.
녹색 사각형은 이동하는 스프라이트, 파란색은 녹색 스프라이트 이전의 위치다.
분홍과 빨간색 영역은 겹치는 부분을 나타내고 있다.
빨간색은 3개 이상이나 겹치는 지저분한 사각형을 나타내고 분홍색은 2개의 겹치는 사각형을 나타낸다.

이제 실제로 필요한 영역만 업데이트하는 방법의 알고리즘은 다음과 같다.

  1. dirty rect 리스트에 추가하려는 지저분한 사각형을 가져온다.
  2. 리스트에 이미 있는 사각형과 겹치는 부분이 있는지 확인한다.
  3. 리스트에 겹치는 사각형이 있으면 둘을 결합하고 리스트에서 하나를 지운다.
  4. 리스트의 나머지 사각형과 겹치는 부분에 대해 결합하고 다시 확인한다.

이를 수행할 최적화된 코드는 다음과 같다.

_update = []  # dirty rect 를 포함(또는 이미 추가된)하는 리스트

_union_rect = _rect(spr.rect)  # 수정될 것이기 때문에 사각형을 미리 복사한다.
_union_rect_collidelist = _union_rect.collidelist
_union_rect_union_ip = _union_rect.union_ip
i = _union_rect_collidelist(_update)  # 겹치는 영역 체크
while -1 < i:  # 겹치는 부분 찾을 때까지 반복
    _union_rect_union_ip(_update[i])  # 두 사각형 결합
    del _update[i]  # 리스트에서 하나 지움
    i = _union_rect_collidelist(_update)  # 겹치는 영역 다시 체크
_update.append(_union_rect.clip(_clip))  # 마지막에 새로 찾은 항목을 목록에 직접 추가
# 오래된 사각형 (스프라이트의 이전 위치)에 대한 동일한 작업 수행

이 알고리즘은 겹치는 영역이 많을 경우에 좋다.
결과는 다음과 같다.

지저분한 영역이 3개만 표시된다. 가장 큰 것은 실제로 매우 크지만, 사각형이 대부분의 화면을 덮지 않는 한, 성능에 병목 현상이 일어나지 않을 것이라는 것을 발견할 수 있다. (그럼 전체 화면을 다시 그리기가 더 빠를 수 있으므로).

최악의 경우는 지저분한 영역은 화면 영역일 뿐인 것으로, 직사각형들이 다른 사각형들과 겹치지 않는 것이다.

이 경우 O(n ** 2) 알고리즘으로, 이 코드는 실제로 DirtyLayered 그룹에서 사용된다. (FastRenderGroup 이라고도 함.)

지저분한 사각형을 사용하는 것과 같은 비슷한 문제가 더 있는데 이는 dirty flags를 참조하자.

지저분한 영역 분할

이부분은 성능이 얼마나 좋을 지는 모르지는 모른다.
이 아이디어는 겹치는 부분을 찾아 사각형이 겹치지지 않도록 분할하는 것이다.
시각화한다면 다음과 같다.

분할하여 많은 수의 직사각형을 얻을 수 있어 이것이 성능 저하를 일으킬 수 있는지는 아직 확인되지 않았다.

타일

화면을 여러 개의 작은 영역으로 분할하기 위해 타일링하는 아이디어다.
스프라이트를 그릴 때 스프라이트의 네 모서리가 있는 영역을 확인하여 해당 영역을 더티 설정을 해야 한다.

위 이미지처럼 단일 스프라이트(녹색)을 볼 수 있는데, 두 파란색 영역이을 업데이트한다.

지저분한 상태

더티 플래그 기술은 몇 가지의 의미가 있다.
먼저, 스프라이트에 새 속성을 추가하고 dirty 라 부르는 것이다.
두 개의 값을 가질 수 있다 가정하면, 0은 지저분하지 않은 것이고 1은 지저분 한것이다.

for spr in sprites:
    if spr.dirty:
        # 지저분한 스프라이트와 지저분한 상태에 대해서만 그린다.

따라서 dirty == 1 로 표시된 스프라이트만 그리고 플래스를 재설정한다. 하지만 스프라이트가 다른 스프라이트가 교차되거나, 그 스프라이트가 투명하고 지저분한 다른 스프라이트와 교차한다면 어떻게 될까?

이 교차하는 스프라이트도 다시 그려야한다. 이전에 언급한 내용으로 지저분한 영역에 있는 모든 스프라이트는 지저분한 영역을 찾는 방법에 관계없이 다시 그려야한다.

지저분해진 부분을 배경으로 채워 지운 다음 청소된 부분을 다시 그리게 되기 때문이다.

이 알고리즘으로 다음과 같이 변경한다.

# 첫 번째 지저분한 영역 찾음
dirty_areas = []
for spr in sprites:
    if spr.dirty:
        # 이 스프라이트 영역을 지저분한 영역 리스트로 추가
        dirty_areas.append(spr.rect)

# 스프라이트를 그림
for dirty_area in dirty_areas:
    # 교차하는 스프라이트를 그림
    for spr in sprites:
        if dirty_area.collide_rect(spr.rect):
            if spr.dirty:
            # 전체 스프라이트가 지저분한 영역 리스트에 있기때문에 그리기만 하면된다.
            # 상태값을 리셋한다.
            #
            else:
        # 교차하는 부분을 찾고 이 스프라이트의 이 부분만 그린다.

이 코드는 pygame.Rect 의 충돌 함수를 사용하여 최적화할 수 있다.
FastRenderGroup 에서 수행되는 방법에 대한 스니펫(재사용 가능한 코드)를 여기 추가한다.

for spr in _sprites:
    if 1 > spr.dirty:                           # 교차하는 부분만 블리팅한 지저분하지 않은 스프라이트만 실행
        _spr_rect = spr.rect
        _spr_rect_clip = _spr_rect.clip
        for idx in _spr_rect.collidelistall(_update):  # 교차하는 지저분한 모든 영역 찾음
            # clip
            clip = _spr_rect_clip(_update[idx])        # 교차하는 부분만 찾아 그린다.
            _surf_blit(spr.image, clip, \
                       (clip[0]-_spr_rect[0], \
                            clip[1]-_spr_rect[1], \
                            clip[2], \
                            clip[3]), spr.blendmode)
    else: # dirty sprite                               # 지저분하다면 전체 스프라이트를 그리고,
        _old_rect[spr] = _surf_blit(spr.image, spr.rect, \
                               None, spr.blendmode)
        if spr.dirty == 1:                             # 플래그를 리셋한다. (이부분이 중요한데,
            spr.dirty = 0                              # 더티 플래그 값이 1이면 리셋해주어야 한다.

이미 설명했듯, 겹치는 부분이 많고 없고에 따라 최적화가 좋을 수도, 나쁠 수도 있다.
더 빠른 속도가 필요할 경우엔 파이썬용 C 확장자를 고려할 수도 있다.

애니메이션

먼저 예제를 다운로드하자.

간단한 애니메이션

애니메이션이란 이미지가 빠르게 표시되는 이미지의 시퀀스라 볼 수 있다.
이 때문에 애니메이션이 부드럽게 보이는 것인데, 쉬워보일 수 있지만 애니메이션을 얻기 위해선 다음과 같은 할 일이 있다.

  1. 이미지를 리스트에 로드 (pygame.image.load() 사용)
  2. 업데이트 때마다 현재 이미지를 다음 이미지로 변경
  3. 카운터가 리스트 끝에 도달하면 처음으로 재설정
  4. 매끄럽게 보이도록 충분히 빠르게 진행

주요 코드는 다음과 같다.

import pygame
import pygame.sprite.Sprite as Sprite
 
class SimpleAnimation(Sprite):
 
    def __init__(self, frames):
        Sprite.__init__(self)
        self.frames = frames       # 여기에 이미지를 저장
        self.current = 0       # 애니메이션의 현재 이미지 idx
        self.image = frames[0]  # 몇 가지 에러를 방지
        self.rect = self.image.get_rect()    # 위와 동일
        self.playing = 0
         
    def update(self, *args):
        if self.playing:    # 플레이 중이면 애니메이션 업데이트
            self.current += 1
            if self.current == len(self.frames):
                self.current = 0
            self.image = self.frames[self.current]
            # 애니메이션 내의 사이즈가 변할때만 필요
            self.rect = self.image.get_rect(center=self.rect.center)

이렇게 몇 가지 정보들을 저장해야하며, 프레임도 리스트에 저장된다.
또한 어느 이미지가 현재 이미지인지 알아야하므로 current 에 인덱스를 저장한다.
당연히 처음으로 update() 를 호출하기 전에 프레임을 로드하고 리스트에 넣어야 한다.

self.playing 속성은 start(), stop() 또는 pause() 등의 함수로 변경할 수 있어야 한다.

위는 단순한 프레임 기반의 무한 반복 애니메이션인데, 게임의 fps 로 실행한다면 단점이 있다.

프레임 변경 방법

이번 섹션에선 애니메이션의 현재 프레임을 변경하는 몇 가지 방법을 설명한다.

이전 예제로 간단한 프레임 기반 애니메이션을 이미 보았다.

self.current += 1
if self.current = len(self.frames):
    self.current = 0

이번엔 약간 다르게 접근해보자.

self.current += 1
if self.current = len(self.frames):
    self.current -= len(self.frames)

또는 모듈을 사용할 수도 있다.

self.current += 1
self.current %= len(self.frames)

이렇게 애니메이션에 몇 개의 프레임이 있는지 알아야 하고 어느 정도 효율적으로 만들고 싶기 때문에 매번 len() 을 사용할 필요가 없도록 저장하는 것이 좋다.

시간 기반 접근 방식을 사용하는 경우 발생할 수 있는 프레임 건너뛰기를 경우 마지막 두 접근 방식이 좋다.

애니메이션 종류

여기선 다양한 시퀀스를 구현하는 방법을 제시한다.

  1. looping
  2. forward
  3. reverse
  4. pingpong

이미 루핑을 사용한 방법은 이미 배웠고, 루핑은 다른 세 가지 시퀀스와 결합될 수 있다.
따라서 서로 다른 시퀀스를 구현하는 두 가지의 어렵고, 간단한 방법이 있다.

어려운 방법은 애니메이션의 코드를 변경하는 것으로, 조작이 복잡하고 버그 발생 여지가 많아진다.

쉬운 방법은 프레임 순서를 변경하기만 하면 된다.
5프레임의 경우 [1,2,3,4,5] 를 [5,4,3,2,1]로 변경하면 된다.
이를 탁구공 애니메이션에 적용하면 [1,2,3,4,5,4,3,2,1] 과 같이 시퀀스를 만들 수 있다.

여기서 주의해야할 것은 이미지를 두 번 로드하지 않는 것이다.

frames = []
for i in range(5):
    frames.append(pygame.image.load("pic"+str(i)+".png"))
for i in range(4, -1, -1):
    frames.append(pygame.image.load("pic"+str(i)+".png")) # wrong!! 
# [0,1,2,3,4,4',3',2',1',0'] 와 같이 주어지는 것은 틀렸다.
# 프레임에 두번 로드 되기 때문
 
# 더 좋은 방법
frames = []
for i in range(5):
    frames.append(pygame.image.load("pic"+str(i)+".png"))
for i in range(5):
    frames.append(frames[4-i]) # 같은 오브젝트를 투면 사용하여
# [0,1,2,3,4,4,3,2,1,0] 와 같이 주어진다.

사용자 정의 코드 대신 함수로 래핑할 수 있다.
다른 종류의 시퀀스를 얻는데 필요한 것은 로드 함수 또는 더 나은 케이스의 다른 시퀀스 함수 뿐이다.

cache = {} # 글로벌 또는 클래스 변수를 가짐
def get_sequence(frames_names, sequence, optimize=True):
    frames = []
    global cache
    for name in frames_names:
        if not cache.has_key(name): # 이미 로드 되었는지 확인
            image = pygame.image.load(name) # 최적화되지 않음
            if optimize:
                if image.get_alpha() is not None:
                    image = image.convert_alpha()
                else:
                    image = image.convert()
            cache[name] = image
             
        # frames_names 와 동일한 프렝임의 시퀀스로 구성
        frames.append(cache[name]) 
    frames2 = []
    for idx in sequence:
        # 시퀀스에 따른 애니메이션 시퀀스로 구성
        frames2.append(frames[idx]) 
    return frames2
 
def get_names_list(basename, ext, num, num_digits=1, offset=0):
    names = []
    format = "%s%0"+str(num_digits)+"d.%s"
    for i in range(offset, num+1):
        names.append(format % (basename, i,ext)) 
    return names
     
image_names = get_names_list("pic", "png", 4)  # ["pic0.png","pic1.png","pic2.png","pic3.png"]
sequence = [0,1,2,3,2,1]
frames = get_sequence(image_names, sequence) # [0,1,2,3,2,1]

시간 기반

지금까지는 업데이트를 호출할 때마다 프레임을 변경했다.
이는 고성능 컴퓨터에선 빠르고 저성능에서는 느린 점이 단점이다.
이는 시간을 기반으로하여 조작하는 방법으로 해결할 수 있다.

녹색 표시는 애니메이션을 업데이트해야 할 때이다.(50ms, 20fps)

import os
from pygame.sprite import Sprite
 
 
class TimedAnimation(Sprite):
 
    def __init__(self, frames, pos, fps=20):
        Sprite.__init__(self)
        self.frames = frames     # 리스트에 프레임 저장
        self.image = frames[0]
        self.rect = self.image.get_rect(topleft=pos)
        self.current = 0         # 애니메이션의 최근 이미지
        self.playing = 0         # 플레이 중인지 상태값
        self._next_update = 0    # ms 업데이트에서 다음 시간
        self._inv_period = fps/1000. # ms 애니메이션의 1./period
        self._start_time = 0
        self._paused_time = 0
        self_pause_start = 0
        self._frames_len = len(self.frames)
         
    def update(self, dt, t):     
        # dt: time that has passed in last pass through main loop,  t: current time
        if self.playing:
            # period 는 프레임의 지속 시간이므로 애니메이션 실행 중인 시간을
            # 한 프레임으로 나누면 프레임 수가 표시
            self.current = int((t-self._start_time-self._paused_time)*self._inv_period)
            self.current %= self._frames_len
            # 이미지 업데이트
            self.image = self.frames[self.current]
            # 프레임들 사이에 사이즈 변경 때만 필요
            self.rect = self.image.get_rect(center=self.rect.center)

첫 번째 방법이 가장 빠르고 간단하며 다른 두 가지 방식은 훨씬 더 많은 계산이 필요하다. 그러나 차이가 작기 때문에(최대 5ms) 어떤 것이든 상관없다.

이보다 더 많은 애니메이션 기법이 있으며 이는 유용하지 않을 수 있는 부분이기 때문에 여기까지만 설명한다.

지금까지 파이게임을 사용하기 위해서 블리팅과 애니메이션을 기반으로 어떤 식으로 사용하는지에 관하여 살펴보았다.

profile
Study

1개의 댓글

comment-user-thumbnail
2023년 10월 17일

와 pygame 관련 한글문서가 없어서 정말 삽질 많이 했었습니다.. 정말 유용한 자료입니다.

답글 달기

관련 채용 정보