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

 

패치버전 1.0.5, 1.0.6을 통해서 보석과 집게의 자연스러운 충돌을 구현했는데요!

 

이번 1.0.7 버전에서는 지금까지 만든 코드들에 게임성을 부여하기 위해서 점수와 제한시간을 만들어주도록 하겠습니다!

 

 

이번 포스팅은 나도코딩님 영상의 2:08:45~2:18:25까지의 내용을 담고 있습니다!

 

목차

     


    0. 게임 점수

    먼저 게임의 점수를 구현해보도록 하겠습니다.

    11_score.py 파일을 만들고, 10_collision_mask.py 파일을 copy&paste 해줍니다.

     

    가장 처음에 해야할 일은 게임에서 사용할 폰트 설정입니다.

    game_font라는 변수를 만들고, pygame.font.SysFont 메소드를 통해서 폰트를 설정해서 변수에 넣어줍니다.

    첫 번째는 글꼴명, 두 번째는 사이즈가 들어갑니다. (128줄)

     

    혹시 어떤 폰트를 사용할 수 있는지 잘 모르겠다면, pygame에서 사용할 수 있는 font들을 pygame.font.get_fonts 메소드를 출력하여 확인할 수 있습니다.

     

    practice2.py 파일을 하나 만들어서 직접 확인해 보았습니다.

    정말 많은 글꼴을 사용할 수 있다..!

     

    폰트를 설정했으니, 점수를 담을 변수들을 만들어봅시다. (131~133줄)

    goal_score는 목표 점수, curr_score는 현재 점수가 됩니다.

    가져온 보석의 가치가 curr_score에 계속해서 더해지겠죠?

     

    while문 안에 예전에 주석처리를 해 놨던 점수 업데이트 처리 부분이 있을 겁니다!

    이 부분의 주석을 풀어줍니다. (207줄)

    update_score 함수가 아직 정의되지 않았기 때문에 밑줄이 뜨죠?

     

    update_score 함수를 정의합니다. (119~121줄)

    매개변수로 보석의 가치를 받아서 그 값을 curr_score에 넣어주면 되겠죠?

    curr_score 변수가 전역 공간에 있기 때문에 이 변수를 사용하기 위해서 global 선언을 해 주는 것을 잊으시면 안됩니다!!

     

    이제 게임 화면 내에서 점수를 보여주는 부분을 만들어야 합니다.

    while문의 거의 끝부분에서 display_score 함수를 실행합니다.

    아직 display_score 함수를 만들지 않았기 때문에 밑줄이 뜨네요!

     

    display_score 함수를 만들어주겠습니다. (123~129줄)

    txt_curr_score, txt_goal_score 변수를 만들고, game_font.render 메소드를 이용해서 값을 넣어줍니다.

    이 때 파이썬 3.6 이상부터 사용할 수 있는 f함수를 이용하면 보다 편하게 문자열과 변수값을 함께 출력할 수 있습니다.

    첫 번째 매개변수는 출력할 값이고, 두 번째 매개변수는 안티앨리어싱 여부(글자를 부드럽게 만들어주는 효과입니다!), 세 번째 매개변수는 글자색, 마지막 매개변수는 배경색이 들어갑니다. (마지막 매개변수는 필수가 아닙니다.)

    (TMI : render 메소드의 사용법은 render(Text, antialias, color, background=None)

     

    출력할 내용을 모두 txt 변수들에 담았다면, screen.blit 메소드로 출력해줍니다.

    첫 번째 매개변수는 출력할 변수이고, 두 번째 매개변수는 출력하는 위치 좌표값입니다.

    아래 while문에서도 사용했었죠!

     

    이제 파이썬 파일을 실행하고 보석 하나를 끌어와봅니다.

    현재 점수와 목표 점수가 출력되고, 보석을 가져오면 현재 점수가 업데이트 됩니다!!

    # 11_score.py
    # 게임 스코어 처리
    # 목표 점수 (1500)
    # 현재 점수 (?)
    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))
    
    
    def update_score(score): # 보석의 price값을 받는다!
        global curr_score # 전역공간에 있는 curr_score를 사용한다는 것을 명시!!
        curr_score += score # 현재 점수를 더해준다!
    
    def display_score(): # 화면에 현재 점수와 목표 점수 출력
        # render 메소드와 f함수를 이용해 출력 (global을 선언하지 않아도 전역 변수를 값만 가져다 쓸 수는 있다!)
        txt_curr_score = game_font.render(f"Curr Score : {curr_score:,}", True, BLACK) # f함수에서 :,을 통해 세 자리마다 콤마 삽입 가능!
        screen.blit(txt_curr_score, (50, 20)) # 화면 왼쪽 위 기준으로 (50, 20)에 현재 점수 출력
    
        txt_goal_score = game_font.render(f"Goal Score : {goal_score:,}", True, BLACK)
        screen.blit(txt_goal_score, (50, 80)) # 현재 점수의 조금 아래에 목표 점수 출력
    
    
    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변수 설정
    game_font = pygame.font.SysFont("arialrounded", 30) # 게임 폰트 설정 (font.Font를 쓰면 파이인스톨러 사용 불가!)
    
    
    # 점수 관련 변수
    goal_score = 1500 # 목표 점수
    curr_score = 0 # 현재 점수
    
    
    # 게임 관련 변수
    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)
    
        # 점수 정보를 보여줌
        display_score()
    
        pygame.display.update() # 설정한 배경화면 이미지를 pygame에 반영! (display에 업데이트!!)
    
    pygame.quit() # while문을 빠져나가면 게임이 끝나도록 설정

     


    1. 게임 오버

    게임이 강제로 종료되는 조건도 있어야 스릴이 있겠죠?

    이번에는 게임 오버 조건을 만들어보도록 하겠습니다.

     

    12_game_over.py 파일을 만들고, 11_score.py 파일을 copy&paste 해 줍니다.

     

    60초라는 시간을 주고, 그 시간동안 목표 점수를 채우지 못하면 게임 오버가 되도록 만들어보겠습니다.

    게임 오버와 관련된 변수를 몇 가지 만들어줍니다.

    게임 결과를 담을 game_result 변수, 총 시간을 담을 total_time, 현재 시간을 받아올 start_ticks 변수를 만들어줍니다.

    start_ticks의 경우 pygame.time.get_ticks 메소드를 사용하여 할당해줍니다. (146~149줄)

    (tick은 쉽게 말해서 현재 시간과 같은 의미라고 보시면 됩니다!)

     

    다시 while문으로 내려와서 시간을 계산하는 부분을 만들어줍시다. (249~251줄)

    프로그램을 시작한 시간으로부터 얼마가 지났는 지를 담은 elapsed_time 변수를 만들어줍니다. (250줄)

    그리고 시간이 얼마나 남았는 지 실시간으로 보여주기 위한 display_time 함수를 실행합니다.

    display_time의 매개변수는 total_time에서 elapsed_time을 뺀 것이 들어갑니다.

    이제 display_time 함수에 밑줄이 그어진 것을 확인했다면 반사적으로 display_time 함수를 만들어야 한다는 느낌이 들죠?

    display_score와 동일한 방법으로 화면에 출력해줍시다.

     

    여기까지 만들고 실행해보면 시간이 millisecond(1/1000초)까지 출력되는 모습을 볼 수 있습니다.

     

    1초 단위로 출력을 하고 싶다면 아래 display_time 함수에서 사용한 elapsed_time에 int를 붙여주면 됩니다.

    int형태로 바꿔주면서 elapsed_time의 소숫점이 모두 버려지게 되서 깔끔하게 초만 나오게 됩니다!

     

    이제 시간이 다 줄어들면 게임이 종료될 수 있도록 만들어보겠습니다.

    while문의 시간 계산 부분 밑에 if문을 통해 시간이 0초가 되면 running을 False로 만들고, (259~260줄)

    현재 점수가 목표 점수와 같거나 더 높으면 Mission Complete, 그렇지 않으면 Game Over을 game_result 변수에 넣어줍니다. (261~264줄)

    그리고 게임 종료 메시지를 출력하도록 display_game_over 함수를 실행시킵니다. (266~267줄)

     

    게임 종료 메시지를 표시하는 display_game_over 함수를 만들어줍니다.

    display_game_over 함수 역시 display_score, display_time과 동일한 방식으로 출력합니다.

    하지만 우리는 스크린의 정 중앙에 맞춰서 글씨가 나올 수 있도록 만들어 주기 위해서 렌더링한 글자의 센터를 스크린의 정 중앙으로 하는 rect값을 구해서 그 값을 screen.blit에 사용합니다.

    이전에는 좌표값을 넣으면 그 부분을 기준으로 출력이 됐지만, 이번에는 중앙을 맞추고 난 후의 좌표값을 기준으로 출력합니다. (135~140줄)

    왼쪽 : 왼쪽 위 빨간점이 (50, 20) / 오른쪽 : 먼저 글자 중앙 빨간점이 스크린의 중앙에 가고, 그 상태의 왼쪽위 빨간점이 rect_game_over

     

     

    마지막으로 게임이 종료되고 바로 스크린이 꺼지지 않도록 pygame.time.delay 메소드를 사용해서 약간의 딜레이를 넣어줍니다. (280줄)

    매개변수로 ms시간을 받으므로, 1초 딜레이를 주고 싶다면 1000을 넣어주면 됩니다.

     

    이제 실행하면 게임 오버 메시지가 뜨고 2초 후에 게임이 종료되겠죠?

    정상적으로 작동하는지 확인하기 위해서 성공 케이스와 실패 케이스를 모두 테스트해보면 정상적으로 작동하는 것을 확인할 수 있습니다!!

    좌 : 성공 케이스 / 우 : 실패 케이스

    # 12_game_over.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): # 집게 이미지의 중심 좌표가 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))
    
    
    def update_score(score): # 보석의 price값을 받는다!
        global curr_score # 전역공간에 있는 curr_score를 사용한다는 것을 명시!!
        curr_score += score # 현재 점수를 더해준다!
    
    def display_score(): # 화면에 현재 점수와 목표 점수 출력
        # render 메소드와 f함수를 이용해 출력 (global을 선언하지 않아도 전역 변수를 값만 가져다 쓸 수는 있다!)
        txt_curr_score = game_font.render(f"Curr Score : {curr_score:,}", True, BLACK) # f함수에서 :,을 통해 세 자리마다 콤마 삽입 가능!
        screen.blit(txt_curr_score, (50, 20)) # 화면 왼쪽 위 기준으로 (50, 20)에 현재 점수 출력
    
        txt_goal_score = game_font.render(f"Goal Score : {goal_score:,}", True, BLACK)
        screen.blit(txt_goal_score, (50, 80)) # 현재 점수의 조금 아래에 목표 점수 출력
    
    
    def display_time(time): # 시간을 출력하는 함수
        txt_timer = game_font.render(f"Time : {time}", True, BLACK) # 매개변수로 받은 시간값을 이용해 만든 텍스트를 txt_timer 변수에 삽입
        screen.blit(txt_timer, (1100, 50)) # 오른쪽 위에 시간을 출력
    
    
    def display_game_over():
        game_font = pygame.font.SysFont("arialrounded", 60) # 폰트 사이즈를 조금 더 크게 사용하기 위해서 새로 설정!!
        txt_game_over = game_font.render(game_result, True, BLACK)
        rect_game_over = txt_game_over.get_rect(center = (int(screen_width / 2), int(screen_height / 2))) # 렌더링한 글자의 센터를 스크린의 중앙으로 설정한 좌표값 저장
        screen.blit(txt_game_over, rect_game_over) # 렌더링한 글자의 센터를 스크린의 중앙으로 하는 좌표값을 이용해서 txt_game_over 출력
        # 위에서 출력하기 위해 사용한 blit와는 다르게 글자의 센터를 맞추고 출력하기 위해 rect_game_over 변수를 생성함!!
        
    
    
    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변수 설정
    game_font = pygame.font.SysFont("arialrounded", 30) # 게임 폰트 설정 (font.Font를 쓰면 파이인스톨러 사용 불가!)
    
    
    # 점수 관련 변수
    goal_score = 1500 # 목표 점수
    curr_score = 0 # 현재 점수
    
    # 게임 오버 관련 변수
    game_result = None # 게임 결과
    total_time = 60 # 총 시간
    start_ticks = pygame.time.get_ticks() # 현재 tick을 받아옴 (현재 시간을 받아옴)
    
    # 게임 관련 변수
    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)
    
        # 점수 정보를 보여줌
        display_score()
    
        # 시간 계산
        elapsed_time = (pygame.time.get_ticks() - start_ticks) / 1000 # ticks의 기본인 ms(밀리초) 단위를 s(초) 단위로 바꿔줌!
        display_time(total_time - int(elapsed_time)) # 시간이 얼마나 남았는 지를 보여주는 함수
    
        # 만약에 시간이 0 이하이면 게임 종료
        if total_time - int(elapsed_time) <= 0:
            running = False
            if curr_score >= goal_score:
                game_result = "Mission Complete"
            else:
                game_result = "Game Over"
    
            # 게임 종료 메시지 표시
            display_game_over()
    
        pygame.display.update() # 설정한 배경화면 이미지를 pygame에 반영! (display에 업데이트!!)
    
    pygame.time.delay(2000) # 2초 딜레이 (ms기준)
    pygame.quit() # while문을 빠져나가면 게임이 끝나도록 설정

     


    오늘은 게임의 알파이자 오메가인 점수와 게임 오버 조건을 관리하는 방법에 대해서 알아보았습니다.

    여기까지 배운 내용만 가지고도 다른 게임도 무궁무진하게 만들 수 있을 것 같다는 생각이 마구마구 드네요 😊😊

     

    다음 포스팅에서는 게임 속에서 집을 수 있는 보석의 수를 추가해서 실제로 게임을 진행할 수 있도록 만들어보는 시간을 갖도록 하겠습니다!

    읽어주셔서 감사합니다 :)

     

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