8.2 절차지향형으로 개발하기 2

이번 절에서부터 우리의 3가지 사용자 함수의 구현체를 구체적으로 채워보는 시간이다. 먼저, move_player 함수이다.

이미 우리에겐 너무 익숙한 함수이다. 다만, 주인공 탱크의 사용자 키보드 조작이 있는 경우와 적 탱의 컴퓨터에 의한 자동조작의 경우를 파라미터값(player)으로 구분해 적용하고 있다. 탱크의 이동에 있어서 제약이 따르는데 화면 밖으로 나갈 수 없고(35~39번 라인), 벽을 뚫고 지나갈 수 없다(30~32번 라인). 그러한 시도의 경우에는 현재 제자리(origin_x, origin_y)에 그대로 머물로록 처리하였다.

def move_player(player):
    # 탱크의 최초 위치를 저장해 두기
    original_x = player.x
    original_y = player.y

    if player == tank:  # 주인공 탱크
        if keyboard.right:
            player.angle = 0
            player.x += 2
        elif keyboard.left:
            player.angle = 180
            player.x -= 2
        elif keyboard.up:
            player.angle = 90
            player.y -= 2
        elif keyboard.down:
            player.angle = 270
            player.y += 2
    else:  # 적 탱크
        if player.angle == 0:
            player.x += 2
        elif player.angle == 90:
            player.y -= 2
        elif player.angle == 180:
            player.x -= 2
        elif player.angle == 270:
            player.y += 2

    # 벽을 뚫고 나가지 못하고 자기 자리에 머물기
    if player.collidelist(walls) != -1:
        player.x = original_x
        player.y = original_y

    # 게임화면 안에 가두기
    if player.left < 0 or player.right > WIDTH \
        or player.top < 0 or player.bottom > HEIGHT:
        player.x = original_x
        player.y = original_y

다음으로 fire_bullets 함수이다. 역시나 워낙 간단한 구현체이고 이미 트윈비에서 만들어 본 것과 유사하므로 애해의 어려움은 없을 것이다. 다만, 차이는 함수에 파라미터값들이 존재한다는 것인데, 주인공 탱크와 적 탱크의 총알 이미지 종류를 구분해 총알을 생성할 수 있다.

def fire_bullets(player, bullets):
    if player == tank:
        bullet = Actor("bulletblue2")
    else:
        bullet = Actor("bulletred2")

    bullet.angle = player.angle
    bullet.pos = player.pos
    bullets.append(bullet)

마지막으로 collide_bullets 함수이다. 이름처럼 대포알이 날아가면서 다른 오브젝트(벽 또는 상대 탱크)들과의 충돌이 발생할 시의 처리를 담당한다. 하지만, 전혀 아무 것과 충돌없이 그냥 화면 밖을 빠져나갈 수도 있는데, 이 때는 단순히 그 대포알을 없에면 된다(21-23번 라인). 그밖에 주의할 점은 벽이과 상대탱크와 충돌시, 벽과 상대탱크는 당연히 없에지만 그와 부딪힌 대포알 자체도 또한 함께 없에야 한다는 것이다(18, 29번 라인).

우리가 최종 게임의 승패시점을 어디서 판단할까 했을 때, 충돌처리 함수 안에서 판별하는 것이 위치상 나쁘지 않다. 적 탱크가 하나도 남지 않은 시점(37-38번 라인), 주인공 탱크가 대포알에 맞은 시점(40-41번 라인)이 바로 그 시점이 될 수 있다.

def collide_bullets(bullets):
    global winner

    for bullet in bullets:
        if bullet.angle == 0:
            bullet.x += 5
        elif bullet.angle == 90:
            bullet.y -= 5
        elif bullet.angle == 180:
            bullet.x -= 5
        elif bullet.angle == 270:
            bullet.y += 5

        # 벽과 충돌처리
        wall_index = bullet.collidelist(walls)
        if wall_index != -1:
            del walls[wall_index]
            bullets.remove(bullet)
            
        # 화면이탈 처리
        if bullet.x < 0 or bullet.x > WIDTH \
            or bullet.y < 0 or bullet.y > HEIGHT:
            bullets.remove(bullet)
        
        # 탱크와의 충돌처리
        if bullets != enemy_bullets:  # 적 탱크
            enemy_index = bullet.collidelist(enemies)
            if enemy_index != -1:
                bullets.remove(bullet)
                explosion = Actor("explosion3")
                explosion.pos = enemies[enemy_index].pos
                explosion.images = ["explosion3", "explosion4"]
                explosion.fps = 8
                explosion.duration = 15
                explosions.append(explosion)
                del enemies[enemy_index]
                if len(enemies) == 0:
                    winner = "You"
        else:  # 주인공 탱크
            if bullet.colliderect(tank):
                winner = "Enemy"

    # 탱크폭발 애니메이션
    for explosion in explosions:
        explosion.animate()
        explosion.duration -= 1
        if explosion.duration == 0:
            explosions.remove(explosion)

3개의 사용자 함수에 대한 구현과 이해가 끝났다. 앞서도 언급했지만, 이 게임은 앞선 두 게임(블록격파, 트위비)의 특징들에 대한 사진 지식이 있었기 때문에 생각보다 이 게임의 구현이나 이해에 많은 시간이 걸리지 않았던 같다. 마지막으로 모든 전체코드를 공유하는 것으로 절차지향형으로 만들어본 배틀시티 게임을 마무리 짓고, 다음 절부터는 드디어 사용자 정의 객체를 만드는 방법부터 시작해 그 객체들을 활용해 동일한 게임을 객체지향으로 만들어 보기를 시작해보자.

from pgzhelper import *
import random

WIDTH = 800
HEIGHT = 600

bullets = []
bullet_delay_cnt = 0
BULLET_DELAY = 50
enemy_bullets = []
explosions = []
winner = ''

tank = Actor("tank_blue", (400, 575))
tank.angle = 90

ENEMY_MOVE_DELAY = 20
MAX_ENEMIES = 3
enemies = []
for i in range(MAX_ENEMIES):
    enemy = Actor("tank_red")
    enemy.angle = 270
    enemy.x = (i + 1) * WIDTH / (MAX_ENEMIES + 1)
    enemy.y = 25
    enemy.move_cnt = 0
    enemies.append(enemy)

# 50x50 크기의 벽의 더미 생성
walls = []
WALL_SIZE = 50
for x in range(int(WIDTH / WALL_SIZE)):  
    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)


def move_player(player):
    # 탱크의 최초 위치를 저장해 두기
    original_x = player.x
    original_y = player.y

    if player == tank:  # 주인공 탱크
        if keyboard.right:
            player.angle = 0
            player.x += 2
        elif keyboard.left:
            player.angle = 180
            player.x -= 2
        elif keyboard.up:
            player.angle = 90
            player.y -= 2
        elif keyboard.down:
            player.angle = 270
            player.y += 2
    else:  # 적 탱크
        if player.angle == 0:
            player.x += 2
        elif player.angle == 90:
            player.y -= 2
        elif player.angle == 180:
            player.x -= 2
        elif player.angle == 270:
            player.y += 2

    # 벽을 뚫고 나가지 못하고 자기 자리에 머물기
    if player.collidelist(walls) != -1:
        player.x = original_x
        player.y = original_y

    # 게임화면 안에 가두기
    if player.left < 0 or player.right > WIDTH \
        or player.top < 0 or player.bottom > HEIGHT:
        player.x = original_x
        player.y = original_y


def fire_bullets(player, bullets):
    if player == tank:
        bullet = Actor("bulletblue2")
    else:
        bullet = Actor("bulletred2")

    bullet.angle = player.angle
    bullet.pos = player.pos
    bullets.append(bullet)


def collide_bullets(bullets):
    global winner

    for bullet in bullets:
        if bullet.angle == 0:
            bullet.x += 5
        elif bullet.angle == 90:
            bullet.y -= 5
        elif bullet.angle == 180:
            bullet.x -= 5
        elif bullet.angle == 270:
            bullet.y += 5

        # 벽과 충돌처리
        wall_index = bullet.collidelist(walls)
        if wall_index != -1:
            del walls[wall_index]
            bullets.remove(bullet)
            
        # 화면이탈 처리
        if bullet.x < 0 or bullet.x > WIDTH \
            or bullet.y < 0 or bullet.y > HEIGHT:
            bullets.remove(bullet)
        
        # 탱크와의 충돌처리
        if bullets != enemy_bullets:  # 적 탱크
            enemy_index = bullet.collidelist(enemies)
            if enemy_index != -1:
                bullets.remove(bullet)
                explosion = Actor("explosion3")
                explosion.pos = enemies[enemy_index].pos
                explosion.images = ["explosion3", "explosion4"]
                explosion.fps = 8
                explosion.duration = 15
                explosions.append(explosion)
                del enemies[enemy_index]
                if len(enemies) == 0:
                    winner = "You"
        else:  # 주인공 탱크
            if bullet.colliderect(tank):
                winner = "Enemy"

    # 탱크폭발 애니메이션
    for explosion in explosions:
        explosion.animate()
        explosion.duration -= 1
        if explosion.duration == 0:
            explosions.remove(explosion)


def draw():
    screen.blit('grass', (0, 0))
    tank.draw()
    for enemy in enemies:
        enemy.draw()
    for wall in walls:
        wall.draw()
    for bullet in bullets:
        bullet.draw()
    for bullet in enemy_bullets:
        bullet.draw()
    for explosion in explosions:
        explosion.draw()

    if winner:
        screen.draw.text(winner + " Win!", \
            midbottom=(WIDTH / 2, HEIGHT / 2), fontsize=100)


def update():
    global bullet_delay_cnt, enemy_move_cnt

    # 주인공 탱크
    if winner == '':
        move_player(tank)
        if bullet_delay_cnt == 0:  # 지연시간 종료 후 재발사 가능
            if keyboard.space:
                sounds.sfx_exp_medium12.play()
                fire_bullets(tank, bullets)
                bullet_delay_cnt = BULLET_DELAY
        else:
            bullet_delay_cnt -= 1
        collide_bullets(bullets)

    # 적 탱크
    for enemy in enemies:
        choice = random.randint(0, 2)
        if enemy.move_cnt > 0:  # 탱크 움직이기
            enemy.move_cnt -= 1
            move_player(enemy)
        elif choice == 0:  # 움직임 지연시간 초기화
            enemy.move_cnt = ENEMY_MOVE_DELAY
        elif choice == 1:  # 탱크 방향전환
            enemy.angle = random.randint(0, 3) * 90
        else:  # 대포알 발사
            fire_bullets(enemy, enemy_bullets)
    collide_bullets(enemy_bullets)

Last updated