안녕하세요! 게임 만드는 잉여 쏘코입니다.

 

이전 포스팅에서는 보석과의 충돌을 처리하는 부분을 구현했었습니다!

 

 

오늘은 이전 포스팅에서 했던 충돌 처리를 조금 더 업그레이드 해 보도록 하겠습니다!

 

 

이번 포스팅은 나도코딩님 영상의 1:55:20~2:08:44 부분을 담고 있습니다.

 

목차

     


    0. 충돌 처리 고급 #1 : 이미지 변환

    저번에 만들었던 충돌 처리도 충분히 만족스럽지만, 아직은 투박한 부분이 분명 존재합니다.

    이번에는 충돌처리를 조금 더 고급지게 할 수 있는 방법에 대해 알아보도록 하겠습니다.

     

    10_collision_mask.py 파일을 만들고, 9_collision.py를 copy&paste 해줍니다.

    어느새 부쩍 많아진 파일들..

     

    현재 우리가 구현한 충돌 처리는 rect끼리 부딪혔을 때 발생하게 됩니다.

    이것을 그림으로 그리면 아래와 같죠!

    나도코딩님 영상 속 예시 1

     

    하지만 실전에서는 집게도 보석도 네모낳지 않습니다.

    집게가 타원형, 보석이 원이라고 생각을 한다면 아래와 같이 rect가 그려집니다.

    이런 경우 rect끼리 충돌을 했을 때 조금 어색할 수 있습니다.

    "우리 눈으로 보이는 집게와 보석은 충돌하지 않았는데, 도대체 어떻게 보석을 가져온거지??" 라고 생각할 수 있겠죠?

     

    이 문제를 해결하는 가장 좋은 방법은 타원형 집게와 원형 보석이 실제로 마주쳤을 때 충돌이 발생하도록 구현하는 것입니다.

     

    이 작업을 위해서는 추가적인 이미지 편집이 필요합니다.

    현재 보석은 사각형으로 되어있고, 색이 꽉 차있기 때문에 동그란 모양으로 바꾸고 투명한 배경을 넣어보도록 하겠습니다.

     

    이미지의 배경을 투명하게 만드는 것은 그림판으로는 불가능합니다.

    그렇기 때문에 온라인 사진 에디터인 pixlr을 사용해보도록 하겠습니다. (컴퓨터에 포토샵이 깔려있다면 그걸 써도 무방합니다!)

     

     

    온라인 사진 에디터 - Pixlr.com

    무료 & 프리미엄 온라인 사진 에디터를 만나보세요. 효과, 필터, 오버레이, 간단한 기능부터 고급형 기능까지 모두 담았어요. 대부분의 파일 형식 (PSD, PXD, JPeg, PNG - 투명, webP, SVG 등)을 지원해요.

    pixlr.com

     

    위에 올려드린 링크로 들어가시면 PIXLR XPIXLR E가 있는데, 오른쪽에 있는 PIXLR E를 눌러줍니다.

     

    가운데 이미지 열기 버튼을 누르고 이미지를 불러옵니다.

    먼저 큰 금을 불러와서 수정해보도록 하겠습니다.

     

    파일을 불러오면 아래와 같은 창이 뜹니다.

     

    우리는 이 큰 금을 원형으로 만들기 위해서 왼쪽 메뉴 중에서 두 번째에 있는 기본 선택 버튼을 눌러줍니다.

    그러면 윗쪽에 타입이 뜨는데, 원을 눌러줍니다.

     

    쉬프트를 누른 상태로 왼쪽 위부터 오른쪽 아래까지 쭉 끌어서 원을 선택해줍니다.

     

    이제 바깥쪽 부분을 선택해서 투명하게 지워줘야겠죠?

    윗쪽의 선택 메뉴에서 반전 선택을 눌러줍니다. 혹은 Ctrl+I를 눌러줘도 되겠네요!

     

    이제 바깥쪽이 잘 선택됐죠? delete키를 눌러서 선택한 부분을 투명하게 만들어줍니다!

     

    배경이 잘 지워졌다면 Ctrl+S버튼을 눌러서 파일을 저장합니다.

    그대로 다운로드 받으시면 됩니다!

     

    이번엔 집게 이미지를 편집해 보겠습니다.

    같은 방식으로 집게 파일을 열어줍니다.

    이미지가 작기 때문에 Ctrl+휠 혹은 왼쪽 메뉴의 돋보기 버튼을 이용하여 크게 확대하셔서 편집하셔도 됩니다!

     

    이번에는 요술봉을 사용합니다.

    왼쪽 메뉴의 4번째에 요술봉 버튼을 눌러줍니다.

    요술봉은 클릭한 부분과 붙어있는 같은 색을 찾아서 자동으로 선택해줍니다.

    디자인 하시는 분들께서 흔히 '누끼를 딴다'라고 부르는 일이기도 하죠!

     

    집게 사진에서 흰 곳을 클릭하고 delete를 눌러서 지워줍니다.

    두 군데로 나뉘어져 있으니 두 번 작업을 해야겠군요!

    배경을 삭제하고 남은 집게 이미지

     

    작업이 끝났으니 저장해줍니다.

    저장된 파일을 작업 폴더에 그대로 덮어씌워줍니다!

    다운로드를 받아서
    그대로 덮어씌우기!

     

    이제 10_collision_mask.py를 실행시키면 집게 주변의 흰색 배경이 사라지고, 큰 금도 원형으로 바뀌게 됩니다!!

    훨씬 깔끔해졌네요!

     


    1. 충돌 처리 고급 #2

    하지만 이미지를 바꿨다고 해서 rect가 바뀌지는 않았습니다.

    이제 투명한 부분은 충돌처리가 되지 않도록 만들어야겠죠?

     

    우리는 while문 안에서 claw.rect.colliderect 메소드를 사용해서 gemstone과 claw의 충돌 처리 여부를 직사각형 기준으로 처리했습니다.

    하지만 우리는 투명한 부분은 충돌 처리가 되지 않게 하고 싶으므로, 두 sprite의 실제 이미지 부분이 충돌되는지 처리해줄 수 있는 pygame.sprite.collide_mask 메소드를 사용합니다.

    collide_mask 메소드의 매개변수는 충돌의 대상인 집게와 보석이 되겠죠?

     

    여기서 collide_mask를 효율적으로 사용하기 위해서는 알파이미지를 사용해야 합니다.

    pygame.image.load를 사용할 때 convert_alpha 메소드를 맨 뒤에 붙여주면 이미지를 불러올 때 그 이미지를 알파이미지로 변경합니다. (매개변수는 없습니다!)

     

    보석의 이미지를 불러오는 부분과 집게의 이미지를 불러오는 부분에 모두 convert_alpha 메소드를 넣어줍니다.

    (TMI : 사실 convert_alpha는 없어도 동작하는데는 문제가 없지만, 이미지를 최적화해서 불러올 수 있으므로 연산 시간이 감소하기 때문에 게임을 만들 때는 사용하는 것이 좋습니다! 아래 convert_alpha의 설명을 참고하세요!)

    출처 : https://www.pygame.org/docs/ref/surface.html#pygame.Surface.convert_alpha

     

    여기까지 만들고 실행하면 투명한 부분은 더이상 집게가 인식하지 않게 됩니다!

    이제는 집게가 좀 더 현실적으로 동작하는 것이죠!! 

    Before
    After

    # 10_collision_mask.py
    # 충돌 처리 (mask)
    import os # 경로를 위해 os 라이브러리 불러오기
    import pygame # 파이게임 라이브러리 불러오기
    import math # radians를 사용하기 위해서 math 라이브러리 불러오기
    
    
    # 집게 클래스 (보석 클래스와 거의 동일!)
    class Claw(pygame.sprite.Sprite):
        def __init__(self, image, position):
            super().__init__()
            self.image = image # 회전한 이미지를 가질 예정!
            self.original_image = image # 처음 이미지를 그대로 가지고 있는다!
            self.rect = image.get_rect(center = position)
    
            self.offset = pygame.math.Vector2(default_offset_x_claw, 0) # x위치는 100만큼 이동, y위치는 그대로
            self.position = position
    
            self.direction = LEFT # 집게의 이동 방향 (숫자값)
            self.angle_speed = 2.5 # 1프레임당 집게의 각도 변경폭 (좌우 이동 속도)
            self.angle = 10 # 최초 각도 정의 (오른쪽 끝)
            
    
        def update(self, to_x): # 보통 게임을 만들 때 캐릭터가 가만히 있을 때 숨쉬는 동작을 담당하는 부분!
            if self.direction == LEFT : # 왼쪽 방향으로 이동하고 있다면
                self.angle += self.angle_speed # 이동 속도만큼 각도를 증가
            elif self.direction == RIGHT : # 오른쪽 방향으로 이동하고 있다면
                self.angle -= self.angle_speed #이동 속도만큼 각도를 감소
            
            # 만약에 허용 각도 범위를 벗어나면?
            if self.angle > 170:
                self.angle = 170
                self.set_direction(RIGHT)
            elif self.angle < 10:
                self.angle = 10
                self.set_direction(LEFT)
    
            self.offset.x += to_x
    
            self.rotate()
            # print(self.angle, self.direction) 테스트용
            # rect_center = self.position + self.offset # 위에서 만든 두 변수를 더해줌!!
            # self.rect = self.image.get_rect(center = rect_center) # update 메소드를 실행하면 self.rect가 업데이트 되고, draw를 하면 바뀐 rect를 기준으로 화면에 출력!
    
    
        def rotate(self): # 원본 이미지가 아닌 새로운 이미지를 만드는 메소드!
            self.image = pygame.transform.rotozoom(self.original_image, -self.angle, 1)
            # 기본 사진을 주어진 각도만큼 회전 (실제로는 반시계방향으로 각도가 계산되지만, 우리는 시계방향으로 거꾸로 적용하기 위해 -를 붙였다!), 이미지의 크기 변동은 X(1)
            # self.image는 회전을 하고 나서 새롭게 만들어진 이미지를 넣기 위한 변수!! (rotozoom은 이미지 회전, 크기설정을 할 수 있는 메소드!)
            # rotozoom은 회전 대상 이미지, 회전 각도, 이미지 크기(scale)을 매개변수로 받는다!
    
            offset_rotated = self.offset.rotate(self.angle) # Vector2를 이용해 받아온 offset 데이터를 각도에 맞춰서 회전시킨 offset을 받아온다 (40으로 설정한 것을 기억하자!)
            # print(offset_rotated) # 확인용
    
            self.rect = self.image.get_rect(center = self.position + offset_rotated) # 집게의 중심점 기준으로 회전하게 만들어준다!
            # print(self.rect) # 확인용
            # pygame.draw.rect(screen, RED, self.rect, 1) # rect 확인용
    
    
        def set_direction(self, direction):
            self.direction = direction # 매개변수로 받은 direction을 self.direction으로 설정
    
        
        def draw(self, screen):
            screen.blit(self.image, self.rect) # 스크린의 blit을 이용해 Claw 클래스로 만든 객체의 이미지와 위치 정보를 이용해 그 내용을 화면에 출력한다! 
            # pygame.draw.circle(screen, RED, self.position, 3) # screen에 빨간 점을 적겠다, 위치는 self.position, 반지름은 3으로 하겠다! - 중심점 표시
            pygame.draw.line(screen, BLACK, self.position, self.rect.center, 5) # 직선 그리기
            # 스크린에 선을 그릴 것이고, 검은 색이고, self.position(중심점)부터 rect.center(집게의 중심점)까지 직선의 두께는 5로 연결하겠다!
    
        def set_init_state(self): # 각도와 이동방향을 초기화하는 메소드
            self.offset.x = default_offset_x_claw # 원위치
            self.angle = 10 # 오른쪽 끝에서 다시 실행하도록 설정
            self.direction = LEFT # 왼쪽으로 이동하도록 설정
    
    
    
    # 보석 클래스
    class Gemstone(pygame.sprite.Sprite): # pygame의 Sprite를 상속해와서 사용한다!!
        def __init__(self, image, position, price, speed): # 생성자 (사진인 image와 보석의 위치인 position을 매개변수로 받는다!)
            super().__init__() # 상속받은 Sprite의 생성자를 불러온다! (상속 받았으니 뭔지는 몰라도 초기화 해준다!)
            # 아래 2개의 변수는 Sprite 메소드를 사용하기 위해서 반드시 정의해야 함!!
            self.image = image # 캐릭터가 가진 이미지 정보 - 매개변수로 받아온다!
            self.rect = image.get_rect(center = position) # 캐릭터가 가지는 좌표, 크기 정보
            # 보석마다 위치가 달라져야 하기 때문에 받아온 이미지의 중앙이 매개변수로 받은 position에 맞춰서 rect를 가져오도록 설정한다!
            self.price = price
            self.speed = speed
    
        def set_position(self, position, angle): # 집게 이미지의 중심 좌표가 position
            # rect.size의 0번째 요소가 width, 1번째 요소가 height
            r = self.rect.size[0] // 2 # 동그라미 이미지를 기준으로 반지름, 정사각형이라면 한 변의 절반 (width의 절반)
            rad_angle = math.radians(angle) # 각도(세타값)
            to_x = r * math.cos(rad_angle) # 삼각형의 밑변
            to_y = r * math.sin(rad_angle) # 삼각형의 높이
            # position에서 (to_x, to_y)를 더한 값이 보석의 중심 위치가 되면 된다
    
            self.rect.center = (position[0] + to_x, position[1] + to_y)
    
    
    def setup_gemstone(): # 보석 클래스에서 설정한 보석의 사진과 위치 정보를 gemstone_group에 넣는 함수! 작은 금은 이해를 위해 나눠서 작성했고, 큰 금부터는 한번에 작성!
        # 보석 가격과 끌어오는 속도 설정
        small_gold_price, small_gold_speed = 100, 5
        big_gold_price, big_gold_speed = 300, 2
        stone_price, stone_speed = 10, 2
        diamond_price, diamond_speed = 600, 7
    
        # 작은 금
        small_gold = Gemstone(gemstone_images[0], (200, 380), small_gold_price, small_gold_speed) # 0번째 이미지를 (200, 300) 위치에 둬라
        gemstone_group.add(small_gold) # 그룹에 추가
        # 큰 금
        gemstone_group.add(Gemstone(gemstone_images[1], (300,500), big_gold_price, big_gold_speed))
        # 돌
        gemstone_group.add(Gemstone(gemstone_images[2], (300,380), stone_price, stone_speed))
        # 다이아몬드
        gemstone_group.add(Gemstone(gemstone_images[3], (900,420), diamond_price, diamond_speed))
    
    
    pygame.init() # 파이게임 초기화
    # 스크린 크기 지정
    screen_width = 1280
    screen_height = 720
    screen = pygame.display.set_mode((screen_width, screen_height)) # 이걸 통해 게임의 창 크기를 설정
    pygame.display.set_caption("Gold Miner") # 게임 제목 설정 "Gold Miner"
    
    clock = pygame.time.Clock() # 프레임값을 조정하기 위한 clock변수 설정
    
    
    # 게임 관련 변수
    default_offset_x_claw = 40 # 중심점으로부터 집게까지의 기본 x 간격
    to_x = 0 # x 좌표 기준으로 집게 이미지를 이동시킬 값을 저장하는 변수
    caught_gemstone = None # 집게를 뻗어서 잡은 보석 정보
    
    # 속도 변수
    move_speed = 12 # 발사할 때 이동 스피드 (x 좌표 기준으로 증가되는 값)
    return_speed = 20 # 아무것도 없이 돌아올 때의 이동 스피드
    
    # 방향 변수
    LEFT = -1 # 왼쪽 방향
    STOP = 0 # 이동 방향이 좌우가 아닌 고정인 상태 (집게를 뻗음)
    RIGHT = 1 # 오른쪽 방향
    
    
    
    # 색깔 변수
    RED = (255, 0, 0) # RGB값, 빨간색
    BLACK = (0, 0, 0) # 검은색
    
    
    # 배경 이미지 불러오기
    current_path = os.path.dirname(__file__) # os.path.dirname을 이용해서 현재 파일의 위치(2_background.py 위치) 반환 
    background = pygame.image.load(os.path.join(current_path, "background.png")) # 현재 위치의 파일이 있는 폴더의 background.png파일을 선택하게 됨!!
    
    
    # 4개 보석 이미지 불러오기 (작은 금, 큰 금, 돌, 다이아몬드) - 배경을 가져오는 과정과 동일!!
    gemstone_images = [
        pygame.image.load(os.path.join(current_path, "small_gold.png")), # 작은 금
        pygame.image.load(os.path.join(current_path, "big_gold.png")), # 큰 금
        pygame.image.load(os.path.join(current_path, "stone.png")), # 돌
        pygame.image.load(os.path.join(current_path, "diamond.png")) # 다이아몬드
    ]
    
    
    # 보석 그룹
    gemstone_group = pygame.sprite.Group() # 젬스톤 그룹 생성
    setup_gemstone() # 게임에 원하는 만큼의 보석을 정의
    
    
    # 집게 이미지 불러오기 (보석 이미지 불러오는 방식과 동일!)
    claw_image = pygame.image.load(os.path.join(current_path, "claw.png"))
    claw = Claw(claw_image, (screen_width // 2, 110)) # Claw 클래스를 이용해서 객체 생성!!
    # 가로 위치는 화면 가로 크기 기준으로 절반, 위에서 110px에 위치
    
    
    # 게임이 반복해서 수행될 수 있도록 게임 루프를 만든다!
    running = True
    while running: # 게임이 진행중이라면? while문을 계속해서 반복하게 된다!
        clock.tick(30) # FPS 값이 30으로 고정
    
        for event in pygame.event.get(): # 이벤트를 받아오고
            if event.type == pygame.QUIT: # 게임이 꺼지는 이벤트가 발생했다면
                running = False # running 변수를 False로 바꿔준다!
    
            if event.type == pygame.MOUSEBUTTONDOWN: # 마우스 버튼이 눌렸을 때 집게를 뻗음
                claw.set_direction(STOP) # 좌우 멈춤
                to_x = move_speed # move_speed 만큼 빠르게 쭉 뻗음
    
    
        # 화면의 양 옆과 아랫쪽 끝에 도달했을 때 다시 되돌아오도록 설정
        if claw.rect.left < 0 or claw.rect.right > screen_width or claw.rect.bottom > screen_height:
            to_x = -return_speed
    
    
        # 원위치에 오면 다시 원래대로 rotate하도록 설정
        if claw.offset.x < default_offset_x_claw:
            to_x = 0 # to_x 다시 초기화
            claw.set_init_state() # 각도와 이동 방향도 함께 초기화 하기 위해 set_init_state 메소드 실행
    
            if caught_gemstone: # 가져온 보석이 있다면
                # update_score(caught_gemstone.price) # 점수 업데이트 처리
                gemstone_group.remove(caught_gemstone) # 그룹에서 잡힌 보석 제외
                caught_gemstone = None # 잡은 보석이 없도록 다시 None으로 초기화
    
    
        if not caught_gemstone: # 잡힌 보석이 없다면 충돌 체크
            for gemstone in gemstone_group: # 우리가 만들어준 gemstone 그룹에 있는 보석들을 하나하나 대입해서
                if claw.rect.colliderect(gemstone.rect): # 그 보석의 rect와 집게가 충돌했다면 - 직사각형 기준으로 충돌 처리
                #if pygame.sprite.collide_mask(claw, gemstone): # 투명 영역은 제외하고 실제 이미지가 존재하는 부분에 대해 충돌 처리
                    caught_gemstone = gemstone # caught_gemstone은 바로 그 보석의 정보를 담게 된다!
                    to_x = -gemstone.speed # 원점으로 되돌아가야 하므로 잡힌 보석의 속도에 -를 붙인 값을 이동 속도로 설정!
                    break # for문 탈출해서 게임이 진행되도록 설정!
    
    
        if caught_gemstone: # 잡은 보석이 있다면
            caught_gemstone.set_position(claw.rect.center, claw.angle) # 집게의 중심 좌표 정보와 집게의 각도를 전달
    
    
        screen.blit(background, (0, 0)) # 맨 왼쪽 맨 위부터 ((0,0) 좌표부터)그림을 그려주도록 만들어준다!
    
        gemstone_group.draw(screen) # gemstone_group에 있는 모든 Sprite를 screen에다가 그려라!
        claw.update(to_x)
        claw.draw(screen)
    
        pygame.display.update() # 설정한 배경화면 이미지를 pygame에 반영! (display에 업데이트!!)
    
    pygame.quit() # while문을 빠져나가면 게임이 끝나도록 설정

     


    오늘 내용은 반드시 필수로 해야하는 부분은 아니지만, 보다 현실적으로 게임에 몰입할 수 있도록 도와주는 디자인적인 업그레이드에 필요한 내용이었습니다.

     

    이제 액션적인 부분은 거의 구현이 다 됐으니, 게임성을 만들기 위해 점수와 시간 처리가 필요하겠죠?

    다음 포스팅에서는 게임성을 업그레이드 하는 내용으로 돌아오겠습니다!

     

    오늘도 읽어주셔서 감사합니다!! 😄😄

     

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기