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

 

이전 포스팅에서는 집게가 발사되는 것을 구현했습니다.

 

 

그렇다면 이제 보석과 충돌했을 때의 상호작용도 처리해줘야겠죠?

 

 

바로 시작합니다!!

 

이번 포스팅은 나도코딩님의 강의 영상에서 1:24:52~1:55:19까지의 내용을 담고 있습니다.

 

목차

     


    0. 충돌 처리 기본 #1

    이제 집게와 보석이 만났을 때를 생각해봅시다.

    집게와 부딪힌 보석은 집게에 의해 위로 끌어올려져야하고, 집게도 충돌과 함께 다시 캐릭터쪽으로 당겨져야 합니다.

    어떤 보석을 잡아 당기냐에 따라 속도도 달라져야겠죠?

     

    이 작업을 처리해 주기 위해서 새롭게 9_collision.py 파일을 만들고, 8.claw_launch.py를 copy&paste 합니다.

     

    집게와 보석이 충돌하게 되면 충돌한 보석의 정보를 어딘가에 저장하고 있어야 합니다.

    그리고 충돌하고 나면 더이상 충돌을 확인할 필요가 없습니다.

    또 다른 보석을 동시에 끌고올 수 없으니까요!

     

    먼저 충돌한 보석의 정보를 저장하는 것부터 구현해보도록 하겠습니다.

    게임 관련 변수에 caught_gemstone 변수를 만들어줍니다.

    이 변수에는 집게를 뻗어서 잡은 보석의 정보가 들어가게 될 예정입니다! (111줄)

     

    이제 while문 내에서 충돌 여부를 확인해야겠죠?

    이미 가지고 있는 보석이 없을 때 충돌 처리를 확인해야합니다. (176줄)

    충돌 체크를 위해서는 claw 객체와 gemstone 그룹내의 sprite들을 하나씩 비교하면 됩니다! (177줄)

    rect의 colliderect 메소드를 이용해서 충돌 여부를 확인했을 때 충돌했다면 (178줄)

    caught_gemstone에 충돌한 보석의 정보를 담아줍니다. (179줄)

     

    이 때 우리는 잡힌 보석의 종류에 따라 무거운 보석이면 천천히 끌고오고, 가벼운 보석이면 빨리 끌고오는 처리를 해줄 수 있습니다.

    이걸 위해 Gemstone 클래스 내에 보석의 무게를 설정할 수 있도록 해 보겠습니다!

    이왕 Gemstone 클래스 만드는거 보석의 가치를 추가해서 나중에 점수화도 할 수 있도록 만들겠습니다.

     

    Gemstone 클래스의 생성자에서 price와 speed를 매개변수로 받을 수 있도록 바꿔주고, (78줄)

    price와 speed를 각각 매개변수에서 받아온 값으로 설정하도록 만들었습니다. (84~85줄)

     

    setup_gemstone()에서 우리가 만들었던 보석들에 대한 가격과 속도도 따로 정의합니다.

    작은 금, 큰 금, 돌, 다이아몬드의 가격과 속도는 나중에 재사용하기 쉽도록 변수처리해서 만들어주고, 그 변수를 각 보석들에 넣어줍니다.

     

    다시 while문으로 돌아와서 to_x값을 잡은 보석의 speed로 바꿔줍니다.

    이 때 원점으로 되돌아가야 하므로 speed에 -를 붙여줍니다. (188줄)

    그리고 모든 작업이 끝났다면 break를 걸어 for문을 종료해줍니다. (189줄)

     

    다 끌고 왔다면 그 보석은 더 이상 화면에 떠있을 필요가 없겠죠?

    끌고 왔을 때 가져온 보석이 있다면 (186줄)

    화면에 떠있는 보석 그룹(gemstone_group)에서 가져온 보석을 제거합니다. (187줄)

    그리고 잡고 있는 보석(caught_gemstone)을 None으로 초기화해줍니다. (188줄)

     

    가져온 보석의 점수를 추가해 줄 수 있겠죠?

    이 부분은 주석처리를 해 두었다가 나중에 처리하도록 해 보겠습니다! (187줄)

     

    여기까지 하고 실행해봅니다.

    보석을 잘 가져오고, 가져오고 나서는 그 보석이 사라집니다.

    하지만 보석을 가져올 때 보석의 이미지는 같이 딸려오지 않고 있습니다.

    # 9_collision.py
    # 충돌 처리
    import os # 경로를 위해 os 라이브러리 불러오기
    import pygame # 파이게임 라이브러리 불러오기
    
    
    # 집게 클래스 (보석 클래스와 거의 동일!)
    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 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와 집게가 충돌했다면
                    caught_gemstone = gemstone # caught_gemstone은 바로 그 보석의 정보를 담게 된다!
                    to_x = -gemstone.speed # 원점으로 되돌아가야 하므로 잡힌 보석의 속도에 -를 붙인 값을 이동 속도로 설정!
                    break # for문 탈출해서 게임이 진행되도록 설정!
                
    
        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문을 빠져나가면 게임이 끝나도록 설정

     


    1. 충돌 처리 기본 #2 : 호도법

    ※ 주의! 앞으로 내용들은 수학적인 내용이 조금 들어갑니다! 너무 어려운 내용은 아니니 걱정 마세요!

     

    집게에 맞춰서 보석 이미지도 같이 따라오게 하기 위해서는 집게가 움직이는 것 만큼 보석 이미지의 위치도 같이 변경시켜야 합니다.

     

    만약 현재 잡은 보석이 있다면 집게의 중심 좌표와 집게의 각도를 잡은 보석의 set_position 메소드에 전달합니다. (200~201줄)

     

    set_position 메소드를 Gemstone 클래스에도 만들어줘야겠죠?

    매개변수로는 position, angle을 받습니다.

     

    여기서부터는 수학적인 이해가 조금 필요합니다.

    우리는 각의 크기를 나타내는 방법으로 육십분법을 주로 사용합니다.

    하지만 우리는 이 육십분법으로 나타낸 각을 호도법 표현으로 바꾸어서 사용하도록 하겠습니다.

    고등학교때 삼각함수를 배우면서 호도법에 대해서 간략하게 배웠던 기억이 납니다. (교육과정에 따라 다를 수 있겠습니다만..)

    호도법으로 육십분법의 각을 나타내면 90도는 1/2파이, 180도는 파이, 270도는 3/2파이로 표현할 수 있습니다.

     

    육십분법 : 원 둘레를 360등분하여 생기는 각 하나를 1˚로 하고, 이것을 기본 단위로 하여 각의 크기를 나타내는 것

    호도법 : 호의 길이가 반지름인 부채꼴의 중심각을 1radian으로 하고, 이것을 ​기본 단위로 하여 각의 크기를 나타내는 것

     

     

    저는 수학자가 아니기 때문에 자세한 설명은 생략합니다.

    이걸 잘 써먹을 수만 있으면 되잖아요?!

    파이썬의 math 라이브러리의 radians() 메소드를 이용하면 육십분법의 각을 호도법의 각으로 바꿀 수 있습니다!

     

    간단한 예시로 practice.py 파일을 만들어서 변환을 해 보겠습니다.

     

    math 라이브러리를 불러온 후에 임의의 변수에다가 math.radians(각)을 입력한 후에 출력합니다.

    각 위치에 0, 90, 180, 270을 넣고 출력하면 주석처리된 값이 나오게 됩니다!!

     

    이걸 이용해서 Gemstone 클래스의 set_position 메소드에서 rad_angle을 정의해줍니다.

    angle로 받은 값을 radians 메소드를 이용해서 호도법으로 변환한 후에 그 값을 rad_angle에 할당합니다. (89줄)

    당연히 math 라이브러리를 불러와야 사용할 수 있겠죠? (5줄)

     

    # 9_collision.py
    # 충돌 처리
    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):
            rad_angle = math.radians(angle)
    
    
    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와 집게가 충돌했다면
                    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문을 빠져나가면 게임이 끝나도록 설정

     


    2. 충돌 처리 기본 #3

    집게가 보석을 집었을 때 그 위치 그대로 집게가 끌어오면 어색한 부분이 발생합니다.

     

    집게와 보석을 모두 원이라고 생각하고 그림을 그려서 설명하겠습니다.

     

    아래 그림과 같이 보석의 사이드를 잡고 그대로 끌고 오는 것보다 (왼쪽)

    보석의 중심이 집게를 연결한 선과 일직선이 될 수 있도록 보석을 이동시킨 후에 끌고오는 것이 자연스럽습니다. (오른쪽)

     

    이걸 위해서는 집게를 연결한 선과 직선이 되는 곳에 보석을 위치시켜야겠죠?

    집게의 반지름을 r, 보석의 반지름을 r'이라고 하겠습니다.

    두 빨간 선의 거리는 집게의 반지름과 보석의 반지름을 더한 것입니다.

    원점에서 (x,y)로 이동하기 위해서는 x와 y값을 구해야겠죠?

     

    우리는 r+r'과 세타를 이용해서 x와 y를 구할 수 있습니다.

    가로가 x, 세로가 y, 빗변이 r인 삼각함수를 다시 한번 떠올려보면,

    sinθ = y/r, cosθ = x/r, tanθ = y/x입니다. (우리는 sinθ, cosθ만 사용할 겁니다!)

    그림판 신공.. ㅋㅋㅋㅋㅋ

     

    이걸 조금 변형하면 y = rsinθ, x = rcosθ 가 됩니다. 

    빗변과 세타값만 알면 가로와 세로를 구할 수 있는 것이죠!

     

    여기서 조금 더 깔끔하게 이미지 처리를 하기 위해서는

    집게 이미지와 보석 이미지가 일정 부분 겹치게 만들어야 합니다.

    만약 단순히 서로가 맞닿았을 때를 기준(r+r')으로 x와 y를 구해서 보석을 이동시키면 집게와 보석이 조금 떨어져있는 상태로 끌고 오겠죠?

    r+r', θ를 이용해서 원을 이동

     

    그렇기 때문에 아예 r'만 기준으로 x와 y를 구해서 보석을 이동시키면 집게가 보석을 진짜 잡고 있는 것처럼 만들 수 있겠죠?

    r', θ를 이용해서 원을 이동 

     

    연산은 동일합니다.

    y' = r'sinθ, x' = r'cosθ가 되겠죠!

     

    이제 set_position 메소드를 아래와 같이 작성합니다.

    먼저 self.rect.size를 이용해서 Gemstone 객체의 한 변의 길이를 구하고

    매개변수로 받아온 angle을 이용해서 호도법으로 구한 각도를 rad_angle에 넣어줍니다.

    매개변수로 받은 각도는 while문에서 caught_gemstone.set_position() 의 마지막 매개변수로 claw.angle을 넣었기 때문에 집게의 각도가 됩니다.

    그리고 to_x, to_y 변수를 만들어서 보석의 위치를 결정하기 위해 현재 집게의 중심에서 추가해야 할 x와 y값을 구해서 넣어줍니다.

    r에다가 각각 cos(rad_angle), sin(rad_angle)을 곱해주면 되겠죠?

    그리고 마지막으로 보석의 rect.center(중앙값)을 원래 위치에서 (to_x, to_y)만큼 이동한 값으로 바꿔줍니다.

     

    여기까지 만들고 실행하면? 자연스럽게 보석을 가져오는 애니메이션이 연출됩니다! 😁😁

     


    이번 포스팅에서는 수학적인 내용이 조금 들어가서 이해하는데 시간이 꽤 오래 걸렸습니다.

    어떤 원리로 집게에 보석이 잡혔을 때 보석의 새로운 위치가 결정되는지만 이해하신다면, 코드 자체는 길지 않기 때문에 쉽게 응용하실 수 있을 것이라고 생각합니다!

     

    잘 이해가 안되신다면, 이번 파트는 이렇게 만드는구나 생각하시고 코드만 따라치고 넘어가셔도 됩니다 ^^:; 우리의 주된 목적은 구현이니까요!

    그래도 제가 시행착오를 겪었던 부분을 상세하게 적어놓았으니 꼭 이해하고 넘어가셨으면 좋겠습니다!

     

    다음 포스팅은 충돌 처리 고급입니다. 끝까지 무사히 따라오실 수 있었으면 좋겠습니다 :) 감사합니다!

     

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