Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

개발이 취미인 개발자

슈팅게임 알고리즘-스테이지 출격과 철수 본문

슈팅게임 알고리즘(pygame)

슈팅게임 알고리즘-스테이지 출격과 철수

도그풋79 2022. 9. 12. 13:45

슈팅게임에서 스테이지별 시작과 끝은 전통적으로 메인캐릭터의 출격하는 장면과 철수하는 장면이 나온다. 이번에는 그동안 개발한 소스에서 제트기류를 뒤로 뿜어내면서 출격하는 씬과 철수하는 씬을 추가하도록 하자.

 우선 출격하는 장면을 구현해 보자 제트기류 연출은 이전에 처리한 총알 구현 파트에서 레이저 발사를 응용하면 된다.

 제트기류가 뒤로 발사된 후 시간이 지나면서 사라지도록 처리하면 된다. 제트 기류의 발생 위치는 메인캐릭터의 꼬리 부분에 맞춰서 생성해준다. 시간이 모두 지나면 생성한 제트기류 객체를 삭제하면 끝이다. 제트기류 처리하는 class는 아래 소스코드를 참고하자.

class FlightJet(pygame.sprite.Sprite):
    def __init__(self, sx, sy):
        pygame.sprite.Sprite.__init__(self)
        self.images = \
            [pygame.image.load(img_dir + "/img/jet01.png")
           , pygame.image.load(img_dir + "/img/jet02.png")
           , pygame.image.load(img_dir + "/img/jet03.png")
           , pygame.image.load(img_dir + "/img/jet04.png")
           ]
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.rect.center = [sx, sy]
        self.name = "jet"      
        self.interval = 6
        self.frame = 0
        self.image_idx = 0

    def update(self, GameMain):
        self.frame += 1
        if self.frame >= self.interval:
            self.frame = 0
            self.image_idx += 1
            if self.image_idx > 3:
                self.kill()
            else:
                self.image = self.images[self.image_idx]

 스테이지 진행과 관련해서는 메인 캐릭터의 상태를 설정했다. 시작과 끝에서 출격과 철수하는 처리를 할 것이다.

class Globals:
    INIT    = 0
    READY   = 1
    PLAYING = 2
    END     = 3

 game_config.py 라는 새로운 파일을 생성하고 여기에 Globals 클래스를 생성한다. 위와 같이 전역변수를 통해 숫자값이 아니라 변수명으로 관리하도록 하겠다. 이렇게 하면 코드값 대신 변수명으로 된 값을 코드에 사용할 수 있다. 이는 개발이 종료된 이후 유지보수를 하기가 좋다. 기존에 개발된 Flight 클래스에 player_state 멤버변수를 선언하고 초기값은 Globals.INIT으로 설정한다.

class Flight(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        ... (중략) ...
        self.player_state = Globals.INIT
        self.frame = 0
        self.acc = 0
        self.max_acc = 4

Flight 클래스에는 아래와 같이 총 4개의 새로운 멤버함수를 추가할 것이다.

player_init() : 스테이지 시작 시 메인 캐릭터가 출격하는 모습을 연출하는 함수

player_ready() : 출격 연출이 종료되고 일정 시간 멈춰 있도록 처리하는 함수

player_end() : 해당 스테이지가 종료되면 철수하는 모습을 연출하는 함수

create_jet() : 메인캐릭터의 출격과 철수 시 제트기류를 연출하는 함수

    def player_init(self):        
        if self.rect.y >= 600:
            if self.acc < self.max_acc:
                self.acc += 0.5
            self.rect.y -= self.speed
        else:
            self.player_state = Globals.READY

    def player_ready(self):
        if self.frame <= 60:
            self.frame += 1            
        else:
            self.frame = 0
            self.player_state = Globals.PLAYING

    def player_end(self):
        if self.rect.y >= -100:
            if self.acc < self.max_acc:
                self.acc += 0.5
            self.rect.y -= self.speed + self.acc

    def create_jet(self, GameMain):
        flight_jet = FlightJet(self.rect.centerx, self.rect.y + 38)
        GameMain.flight_group.add(flight_jet)

init과 end 함수에서는 출격과 철수 시 좀 더 메인캐릭터의 동작을 역동적으로 처리하기 위해 가속도(acc)를 넣었다. 똑같은 속도로 출격과 철수하는 것보다 가속도로 이동하는 것이 메인 캐릭터의 움직임이 역동적으로 보인다. 혹시 차이가 궁금하다면 max_acc를 0으로 설정하고 재빌드한 후 직접 실행해 비교해보자.

 

기존의 update문은 상태값에 따라 아래와 같이 처리하도록 변경했다. 출격과 철수 시에는 사용자의 키입력을 막아 메인 캐릭터의 연출을 방해하지 않도록 하기 위해서이다. Globals.READY를 추가한 이유는 출격이 끝난 후로 생성된 jet기류가 완전히 사라질때까지 대기하도록 하기 위해서다. 만약 READY가 없다면 출격 이후 바로 이동처리가 되어 제트기류가 엉뚱한 곳에 그려지게 된다.

def update(self, GameMain):
        if self.player_state == Globals.INIT:
            self.player_init()
            self.create_jet(GameMain)

        elif self.player_state == Globals.READY:
            self.player_ready()

        elif self.player_state == Globals.PLAYING:
            ... (중략) ... //기존 update 소스코드
        
        elif self.player_state == Globals.END:
            self.player_end()
            self.create_jet(GameMain)

 

 지지난 강좌에서 적기 출현하는 코드가 매우 비효율적이라고 말을 했는데 그 이유는 코딩을 조금만 해 봤다면 금방 알 것이다. 적기가 출격한 이후로도 계속 for문이 돌면서 데이터 전체를 탐색하기 때문이다. 강좌에 사용된 예제는 단 12기의 적만이 등장하기 때문에 속도에 큰 문제가 없겠지만 적기가 그 이상으로 많이 등장하게 되면 속도가 굉장히 느려질 문제가 있다. 이미 출격한 적기 데이터는 굳이 다시 한번 탐색할 이유가 없다. 따라서 적기 한대가 출현한 후에는 다음 적기의 등장 시간을 확인한 후, 그 시간이 올때까지 대기를 하도록 처리하는 것이 가장 효율적일 것이다.

 

GameManager 클래스 초기 생성 시점에 data를 한번 정렬해 준다.

statge_data 는 [다음 등장 시간, 등장 x위치, 등장 y위치, 적기 타입] 배열을 원소로 가진 2차원 배열이다.

class GameManager():
    def __init__(self):
        self.data_idx  = 0
        self.time_line = 0
        self.data =  [[100, 100, -50, 0]
                    ...(중략)...
                    , [350, 600, -50, 1]]
        self.stage_data = sorted(self.data)
        self.creating   = True
        self.next_time  = self.stage_data[self.data_idx][0]
        self.enemy_total_cnt = len(self.stage_data)

 

 순서대로 출력하도록 하기 위해서는 정렬이 필요하기 때문에 sorted를 함수를 활용하고 다시 한번 stage_data를 만들어 준다. 그리고 배열의 모든 인덱스를 탐색한 후에는 더 이상 적기 등장 여부를 확인하는 것이 무의미 하기 때문에 creating이라는 멤버 변수로 이를 구분하도록 처리했다.

 그래서 기존 GameManager 클래스의 process 함수는 아래 코드처럼 변경했다.

    def process(self, GameMain):        
        if GameMain.flight.player_state == Globals.PLAYING:
            self.time_line += 1
            idx = self.data_idx
            if self.time_line == self.next_time:
                while self.next_time == self.stage_data[idx][0] \
                    and self.creating:                    
                    sx = self.stage_data[idx][1]
                    sy = self.stage_data[idx][2]
                    type = self.stage_data[idx][3]
                    enemy_flight = EnemyFlight(sx, sy, type, type)
                    GameMain.enemy_flight_group.add(enemy_flight)
                    if idx + 1 <= len(self.stage_data) - 1:
                        idx += 1
                    else:
                        self.creating = False
                        self.next_time = -1
            
                self.next_time = self.stage_data[idx][0]
                self.data_idx = idx

        elif GameMain.flight.player_state == Globals.INIT or \
            GameMain.flight.player_state == Globals.READY:
            font = pygame.font.Font(None, 40)
            text = font.render("STAGE 1", True, [255, 255, 255])
            text_rect = text.get_rect(center=(350, 200))
            GameMain.screen.blit(text, text_rect)

        elif GameMain.flight.player_state == Globals.END:
            font = pygame.font.Font(None, 40)
            text = font.render("STAGE CLEAR", True, [255, 255, 255])
            text_rect = text.get_rect(center=(350, 200))
            GameMain.screen.blit(text, text_rect)

 스테이지가 끝났다는 걸 판단하는 기준이 뭘까? 보통 슈팅게임에서는 보스를 클리어 했을때로 본다. 어떤 게임에서는 일정한 시간이 지나면 보스가 자동적으로 자폭을 해서 강제로 종료시키기도 한다. 지금 예제에서는 적기가 모두 파괴되는 시점으로 잡기로 했다. 적기는 메인 캐릭터에 의해 격추되어 파괴될 수도 있지만 화면 밖으로 일정 위치를 넘어서도 파괴되도록 처리되어 있다. 단지 파괴되는 여부를 기록하는 부분이 없었을 뿐이다. 그래서 GameManager 클래스에 self.enemy_total_cnt 라는 변수를 선언한 후, 적기가 파괴될때마다 1씩 차감하고 0이 될때 player_state를 Globals.END로 처리하도록 하자. 아래 destory_enemy 함수를 GameManager에 추가한 후, game_enemy.py를 조금 더 수정한다.

    def destroy_enemy(self, GameMain):
        self.enemy_total_cnt -= 1
        if self.enemy_total_cnt == 0:
            GameMain.flight.player_state = Globals.END

 

 game_enemy.py 기존 update 함수 코드는 화면을 넘어가거나 격추될때  단순히 self.kill()로 객체를 삭제만 하고 끝났다면 지금은 격추된 격기를 카운트 하기 위해 아래와 같이 코드를 수정해 줘야한다.

    def update(self, GameMain):
        ...(중략)...

        if self.rect.y < -300:            
            self.destory(GameMain)
        elif self.rect.y > 1210:
            self.destory(GameMain)
        elif self.rect.x < -300:
            self.destory(GameMain)
        elif self.rect.x > 1210:
            self.destory(GameMain)

    def destory(self, GameMain):
        GameMain.game_manager.destroy_enemy(GameMain)
        self.kill()

그리고 기존의 damaged 부분도 self.kill() 대신 이번에 작성한 self.destroy를 통해 GameManager의 격추된 카운트까지 처리한 후 처리하도록 하자.

    def damaged(self, GameMain, damage):
        self.energy -= damage
        if self.energy < 0:
            explosion_large = ExplosionLarge(self.rect.centerx, self.rect.centery)
            GameMain.explosion_group.add(explosion_large)
            self.destory(GameMain)

지난 번 충돌 강좌에서 틀린 부분을 발견했다. ExplosionLarge를 선언할때 위치를 self.rect.x와 self.rect.y로 만 선언하게 되면 실제로 폭발 위치가 적기의 정중앙이 아닌 rect 영역의 좌상단이 되어 폭발하는 위치가 어색하게 보이는 문제가 있었다.

이번 예제 코드에서는 centerx, centery로 수정해서 올렸다.

 

 모든 적기가 격추되면 아래 캡처한 영상처림 바로 메인 캐릭터는 철수를 하게 된다. 적기가 모두 터지자마자 철수하는 부분이 어색한데 이 부분은 나중에 점수를 매기는 부분을 추가해서 바꿔볼 계획이다.

 

전체 예제 소스를 첨부한다. game_main.py를 실행하면 예제가 동작하는 걸 볼 수가 있다.

11_enemy_formation_stage.zip
0.05MB