8.9 객체지향으로 개발하기 4

드디어 먼 길을 거처 구현의 마지막까지 도달한 여러분을 축하한다. 이번 절에서는 지난 절에서 만들었던 코드를 일부 수정하고, 미구현 된 부분을 마져 구현하는 것으로 전체 코딩을 마무리 하려고 한다.

먼저 미구현된 부분인 총알을 맞은 탱크가 폭발해 불이나는 것을 추가하려고 한다. 우리가 이미 절차지향 개발에서 살펴봐서 알고 있듯이 "폭발" 이란 것도 사실은 하나의 Actor 객체이고, 그 폭발객체를 에니메이션 하는 것으로 구현하였다. 그렇다면, 객체지향 버전에서는 이 폭발객체를 Actor에서 객체에서 상속한 독립객체(여기서는 Explosion라 명명함)로 만들고, 이를 생성하고 활용하는 쪽은 탱크객체 자신이 될 수 있다. 그래서, 이를 다음과 같이 코딩할 수 있다.

actors.py
class Explosion(Actor):
    def __init__(self, img_names, pos):
        super().__init__(img_names[0], pos)
        self.images = img_names
        self.fps = 8
        self.duration = 15

    def update(self):
        self.animate()
        self.duration -= 1

                
class Tank(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, walls, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self._original_pos = None
        self.angle = angle
        self.walls = walls
        self.bullets = []
        self.explosions = []

    ... 생략
    
    def draw(self):
        super().draw()
        for bullet in self.bullets:
            bullet.draw()
        for explosion in self.explosions:
            explosion.draw()
    
    def collidelist_bullets(self, tanks):
        tank_index = -1
        
        for bullet in self.bullets:
            bullet.move()

            # 화면경계 확인
            if bullet.is_out_of_screen():
                self.bullets.remove(bullet)

            # 벽 더미 확인
            wall_index = bullet.collidelist(self.walls)
            if wall_index != -1:
                del self.walls[wall_index]
                self.bullets.remove(bullet)

            # 상대 탱크 확인
            tank_index = bullet.collidelist(tanks)
            if tank_index != -1:
                self.bullets.remove(bullet)
                explosion = Explosion(["explosion3", "explosion4"], \
                    tanks[tank_index].pos)
                self.explosions.append(explosion)

        # 폭발 에니메이션       
        for explosion in self.explosions:
            explosion.update()
            if explosion.duration == 0:
                self.explosions.remove(explosion)

        return tank_index

🔢 폭발관련 처리를 위해 추가된 코드는 1~10, 21, 29~30, 52~59라인까지의 코드이다. 위의 내용을 실행시켜보면, 총알을 맞은 탱크는 우리가 목적한데로 폭발 에니메이션이 표현될 것이다.

이로써 우리가 만들고자 했던 배틀시티 게임의 객체지향 버전의 최종판을 완성했다. 어떤가 나만의 클래스를 정의해 나만의 객체를 만들고 이들 간의 협력을 통한 객체지향 패러다임으로 개발하는 것이 재미있는 경험이었을 것이라 믿는다. 다만, 아직 객체지향 복잡하기만 하고 큰 장점을 모르겠다라고 느낄 수 있을텐데 이번 과의 서두에서 아래와 같이 언급했지만,

예를들어 적으로서 탱크만이 아닌 다양한 형태의 다른 적이 등장한다던지, 내가 사용하는 무기도 대포알 뿐만 아니라 다른 형태의 공격무기도 갖게 할 것인지, 지금은 장매물이 벽 더미이지만, 그 외의 다른 형태의 장애물도 생각하는지 등등에 따라 당장의 상속의 깊이에서부터 객체지향 설계의 스케일이 달라지기 때문이다.

사실은 이 게임이 규모가 더 커지고 복잡해지면서 그 장점이 본격적으로 드러날 것이다. 물런 이런 고려까지 포함해 개발하려면 초기에 객체지향의 설계에서 더 난이도가 높아지고 추가적으로 더 알아야 할 지식들이 생기기는 하겠지만 말이다. 첫 술에 배부룰 순 없으니, 첫 객체지향 패러다임 개발은 이 정도 선에 만족하기로 하고, 다음 판 책 업그레이드 기회가 된다면 그 심화된 구현에 대해 부록으로 추가할 수 있는 기회를 갖으면 좋을 것 같다. 이제 마지막으로 지금까지 코딩내용의 전체 소스코들 첨부하는 것으로 이 과를 마무리 하겠다.

actors.py
from pgzhelper import *
from abc import ABC, abstractmethod


class CheckOutOfScreen(ABC):
    def __init__(self, screen):
        self.s_width, self.s_height = screen

    @abstractmethod
    def is_out_of_screen(self):
        pass


class Bullet(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, walls, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self.angle = angle
        self.walls = walls

    def is_out_of_screen(self):
        if self.x > self.s_width or self.x < 0 or \
            self.y > self.s_height or self.y < 0:
            return True
        else:
            return False

    def move(self):
        if self.angle == 0:
            self.x += 5
        elif self.angle == 90:
            self.y -= 5
        elif self.angle == 180:
            self.x -= 5
        elif self.angle == 270:
            self.y += 5

        
class Explosion(Actor):
    def __init__(self, img_names, pos):
        super().__init__(img_names[0], pos)
        self.images = img_names
        self.fps = 8
        self.duration = 15

    def update(self):
        self.animate()
        self.duration -= 1


class Tank(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, walls, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self._original_pos = None
        self.angle = angle
        self.walls = walls
        self.bullets = []
        self.explosions = []


    def is_out_of_screen(self):
        if self.right > self.s_width or self.left < 0 or \
            self.bottom > self.s_height or self.top < 0:
                return True
        else:
            return False

    def move(self):
        self._original_pos = self.pos
        if self.angle == 180:
            self.x -= 2
        elif self.angle == 0:
            self.x += 2
        elif self.angle == 90:
            self.y -= 2
        elif self.angle == 270:
            self.y += 2

        # 화면경계 확인
        if self.is_out_of_screen():
            self.pos = self._original_pos

        # 벽더미 확인
        if self.collidelist(self.walls) != -1:
            self.pos = self._original_pos

    def fire(self, image, sound_obj=None):
        bullet = Bullet(image, self.pos, self.angle, self.walls, (self.s_width, self.s_height))
        self.bullets.append(bullet)
        if sound_obj:
            sound_obj.play()

    def collidelist_bullets(self, tanks):
        tank_index = -1
        
        for bullet in self.bullets:
            bullet.move()

            # 화면경계 확인
            if bullet.is_out_of_screen():
                self.bullets.remove(bullet)

            # 벽더미 확인
            wall_index = bullet.collidelist(self.walls)
            if wall_index != -1:
                del self.walls[wall_index]
                self.bullets.remove(bullet)

            # 상대탱크 확인
            tank_index = bullet.collidelist(tanks)
            if tank_index != -1:
                self.bullets.remove(bullet)
                explosion = Explosion(["explosion3", "explosion4"], \
                    tanks[tank_index].pos)
                self.explosions.append(explosion)
        
        # 폭발 에니메이션
        for explosion in self.explosions:
            explosion.update()
            if explosion.duration == 0:
                self.explosions.remove(explosion)

        return tank_index

    def draw(self):
        super().draw()
        for bullet in self.bullets:
            bullet.draw()
        for explosion in self.explosions:
            explosion.draw()


class MyTank(Tank):
    def __init__(self, img_name, pos, angle, walls, screen):
        super().__init__(img_name, pos, angle, walls, screen)

class EnemyTank(Tank):
    def __init__(self, img_name, pos, angle, walls, screen):
        super().__init__(img_name, pos, angle, walls, screen)
battle_city_oop.py
import random
from actors import MyTank, EnemyTank

WIDTH = 800
HEIGHT = 600

winner = ""

# 적 탱크 이동 지연
enemy_move_cnt = 0
ENEMY_MOVE_DELAY = 20

# 주인공 탱크 총알 재장전 지연
bullet_delay_cnt = 0
BULLET_DELAY = 50

# 50x50 크기의 벽의 더미 생성
walls = []
WALL_SIZE = 50
for x in range(int(WIDTH / WALL_SIZE)):
    # 탱크가 위치 할 첫 행과 마지막 행 총 2행을 비워두기 위해 -2 하여 생성
    for y in range(int(HEIGHT / WALL_SIZE - 2)):
        if random.randint(0, 100) < 50:  # 적정 수의 벽을 생성
            wall = Actor("wall", anchor=("left", "top"))
            wall.x = x * WALL_SIZE
            wall.y = y * WALL_SIZE + WALL_SIZE  # 맨 첫 행 비우기 위해 전체적으로 아래로 밀기
            walls.append(wall)

# 주인공 탱크 생성
tank = MyTank("tank_blue", (400, 575), 90, walls, (WIDTH, HEIGHT))

# 적 탱크 생성
enemies = []
MAX_ENEMIES = 3
for i in range(MAX_ENEMIES):
    enemies.append(EnemyTank("tank_red", (400, 25), 270, walls, (WIDTH, HEIGHT)))


def draw():
    screen.blit("grass", (0, 0))  # 배경이미지 그리기
    tank.draw()
    for enemy in enemies:
        enemy.draw()
    for wall in walls:
        wall.draw()
    if winner:
        screen.draw.text(
            winner + " Win!", midbottom=(WIDTH / 2, HEIGHT / 2), fontsize=100
        )

def update():
    global enemy_move_cnt, bullet_delay_cnt, winner

    # 주인공 탱크
    if winner == "":
        if keyboard.left:
            tank.angle = 180
            tank.move()
        elif keyboard.right:
            tank.angle = 0
            tank.move()
        elif keyboard.up:
            tank.angle = 90
            tank.move()
        elif keyboard.down:
            tank.angle = 270
            tank.move()
            
        if bullet_delay_cnt == 0:  # 총알 재장전 가능
            if keyboard.space:
                tank.fire("bulletblue2", sounds.sfx_exp_medium12)
                bullet_delay_cnt = BULLET_DELAY
        else:
            bullet_delay_cnt -= 1

        enemy_idx = tank.collidelist_bullets(enemies)
        if enemy_idx != -1:
            del enemies[enemy_idx]
            if len(enemies) == 0:
                winner = "You"

    # 적 탱크
    for enemy in enemies:
        choice = random.randint(0, 3)
        if enemy_move_cnt > 0:
            enemy_move_cnt -= 1
            enemy.move()
        elif choice == 0:  # 움직임 지연 초기화
            enemy_move_cnt = ENEMY_MOVE_DELAY
        elif choice == 1:  # 랜덤방향 결정
            enemy.angle = random.randint(0, 3) * 90
        else:
            enemy.fire("bulletred2")

        tank_idx = enemy.collidelist_bullets([tank])
        if tank_idx != -1:
            winner = "Enemy"

Last updated