프로젝트 개요
- pygame을 사용하여 Pirate Mario제작
- Youtube Clear Code의 Platformer in Pygame을 따라하며 진행
- 주요 과제: 레벨을 디자인하고 csv 파일로 저장해 pygame에서 구현
프로젝트 내용
level design by 'Tiled'
Tiled
main.py
import pygame
from sys import exit
from settings import *
from PirateMario.code.level import Level
from game_data import level_0
pygame.init()
screen = pygame.display.set_mode((screen_width, screen_height))
clock = pygame.time.Clock()
level = Level(level_0, screen)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
screen.fill('grey')
level.run()
pygame.display.update()
clock.tick(60)
game_data.py
level_0 = {
'terrain': '../levels/0/level_0._terrain.csv',
'coins': '../levels/0/level_0._coins.csv',
'fg palms': '../levels/0/level_0._fg_palms.csv',
'bg palms': '../levels/0/level_0._bg_palms.csv',
'crates': '../levels/0/level_0._crates.csv',
'enemies': '../levels/0/level_0._enemies.csv',
'constraints': '../levels/0/level_0._constraints.csv',
'player': '../levels/0/level_0._player.csv',
'grass': '../levels/0/level_0._grass.csv'
}
settings.py
vertical_tile_number = 11
tile_size = 64
screen_height = vertical_tile_number * tile_size
screen_width = 1200
support.py
from csv import reader
from settings import tile_size
from os import walk
import pygame
def import_folder(path):
surface_list = []
for _, __, image_files in walk(path):
for image in image_files:
full_path = path + '/' + image
image_surf = pygame.image.load(full_path).convert_alpha()
surface_list.append(image_surf)
return surface_list
def import_csv_layout(path):
terrain_map = []
with open(path) as map:
level = reader(map, delimiter = ',')
for row in level:
terrain_map.append(list(row))
return terrain_map
def import_cut_graphics(path):
surface = pygame.image.load(path).convert_alpha()
tile_num_x = int(surface.get_size()[0] / tile_size)
tile_num_y = int(surface.get_size()[1] / tile_size)
cut_tiles = []
for row in range(tile_num_y):
for col in range(tile_num_x):
x = col * tile_size
y = row * tile_size
new_surf = pygame.Surface((tile_size, tile_size), flags = pygame.SRCALPHA)
new_surf.blit(surface, (0, 0), pygame.Rect(x, y, tile_size, tile_size))
cut_tiles.append(new_surf)
return cut_tiles
tiles.py
import pygame
from support import import_folder
class Tile(pygame.sprite.Sprite):
def __init__(self, size, x, y):
super().__init__()
self.image = pygame.Surface((size, size))
self.rect = self.image.get_rect(topleft = (x, y))
def update(self, shift):
self.rect.x += shift
class StaticTile(Tile):
def __init__(self, size, x, y, surface):
super().__init__(size, x, y)
self.image = surface
class Crate(StaticTile):
def __init__(self, size, x, y):
super().__init__(size, x, y, pygame.image.load('../graphics/terrain/crate.png').convert_alpha())
offset_y = y + size
self.rect = self.image.get_rect(bottomleft = (x, offset_y))
class AnimatedTile(Tile):
def __init__(self, size, x, y, path):
super().__init__(size, x, y)
self.frames = import_folder(path)
self.frame_index = 0
self.image = self.frames[self.frame_index]
def animate(self):
self.frame_index += 0.15
if self.frame_index >= len(self.frames): self.frame_index = 0
self.image = self.frames[int(self.frame_index)]
def update(self, shift):
self.animate()
self.rect.x += shift
class Coin(AnimatedTile):
def __init__(self, size, x, y, path):
super().__init__(size, x, y, path)
center_x = x + int(size / 2)
center_y = y + int(size / 2)
self.rect = self.image.get_rect(center = (center_x, center_y))
class Palm(AnimatedTile):
def __init__(self, size, x, y, path, offset):
super().__init__(size, x, y, path)
offset_y = y - offset
self.rect.topleft = (x, offset_y)
particle.py
import pygame
from support import import_folder
class ParticleEffect(pygame.sprite.Sprite):
def __init__(self, pos, type):
super().__init__()
self.frame_index = 0
self.animation_speed = 0.5
if type == 'jump':
self.frames = import_folder('../graphics/character/dust_particles/jump')
if type == 'land':
self.frames = import_folder('../graphics/character/dust_particles/land')
self.image = self.frames[self.frame_index]
self.rect = self.image.get_rect(center = pos)
def animate(self):
self.frame_index += self.animation_speed
if self.frame_index >= len(self.frames):
self.kill()
else:
self.image = self.frames[int(self.frame_index)]
def update(self, x_shift):
self.animate()
self.rect.x += x_shift
decoration.py
import pygame
from settings import vertical_tile_number, tile_size, screen_width
from tiles import AnimatedTile, StaticTile
from support import import_folder
from random import choice, randint
class Sky:
def __init__(self, horizon):
self.top = pygame.image.load('../graphics/decoration/sky/sky_top.png').convert()
self.bottom = pygame.image.load('../graphics/decoration/sky/sky_bottom.png').convert()
self.middle = pygame.image.load('../graphics/decoration/sky/sky_middle.png').convert()
self.horizon = horizon
self.top = pygame.transform.scale(self.top, (screen_width, tile_size))
self.bottom = pygame.transform.scale(self.bottom, (screen_width, tile_size))
self.middle = pygame.transform.scale(self.middle, (screen_width, tile_size))
def draw(self, surface):
for row in range(vertical_tile_number):
y = row * tile_size
if row < self.horizon:
surface.blit(self.top, (0, y))
elif row == self.horizon:
surface.blit(self.middle, (0, y))
else:
surface.blit(self.bottom, (0, y))
class Water:
def __init__(self, top, level_width):
water_start = -screen_width
water_tile_width = 192
tile_x_amount = int((level_width + screen_width) / water_tile_width)
self.water_sprites = pygame.sprite.Group()
for tile in range(tile_x_amount):
x = tile * water_tile_width + water_start
y = top
sprite = AnimatedTile(192, x, y, '../graphics/decoration/water')
self.water_sprites.add(sprite)
def draw(self, surface, shift):
self.water_sprites.update(shift)
self.water_sprites.draw(surface)
class Clouds:
def __init__(self, horizon, level_width, cloud_number):
cloud_surf_list = import_folder('../graphics/decoration/clouds')
min_x = -screen_width
max_x = level_width + screen_width
min_y = 0
max_y = horizon
self.cloud_sprites = pygame.sprite.Group()
for cloud in range(cloud_number):
cloud = choice(cloud_surf_list)
x = randint(min_x, max_x)
y = randint(min_y, max_y)
sprite = StaticTile(0, x, y, cloud)
self.cloud_sprites.add(sprite)
def draw(self, surface, shift):
self.cloud_sprites.update(shift)
self.cloud_sprites.draw(surface)
enemy.py
import pygame
from tiles import AnimatedTile
from random import randint
class Enemy(AnimatedTile):
def __init__(self,size,x,y):
super().__init__(size,x,y,'../graphics/enemy/run')
self.rect.y += size - self.image.get_size()[1]
self.speed = randint(3,5)
def move(self):
self.rect.x += self.speed
def reverse_image(self):
if self.speed > 0:
self.image = pygame.transform.flip(self.image,True,False)
def reverse(self):
self.speed *= -1
def update(self,shift):
self.rect.x += shift
self.animate()
self.move()
self.reverse_image()
player.py
import pygame
from support import import_folder
class Player(pygame.sprite.Sprite):
def __init__(self, pos, surface, create_jump_particles):
super().__init__()
self.import_character_assets()
self.frame_index = 0
self.animation_speed = 0.15
self.image = self.animations['idle'][self.frame_index]
self.rect = self.image.get_rect(topleft = pos)
self.import_dust_run_particles()
self.dust_frame_index = 0
self.dust_animation_speed = 0.15
self.display_surface = surface
self.create_jump_particles = create_jump_particles
self.direction = pygame.math.Vector2(0, 0)
self.player_speed = 8
self.gravity = 0.8
self.jump_speed = -16
self.status = 'idle'
self.facing_right = True
self.on_ground = False
self.on_ceiling = False
self.on_left = False
self.on_right = False
def import_character_assets(self):
character_path = '../graphics/character/'
self.animations = {'idle': [], 'run': [], 'jump': [], 'fall': []}
for animation in self.animations.keys():
full_path = character_path + animation
self.animations[animation] = import_folder(full_path)
def import_dust_run_particles(self):
self.dust_run_particles = import_folder('../graphics/character/dust_particles/run')
def animate(self):
animation = self.animations[self.status]
self.frame_index += self.animation_speed
if self.frame_index >= len(animation):
self.frame_index = 0
image = animation[int(self.frame_index)]
if self.facing_right:
self.image = image
else:
flipped_image = pygame.transform.flip(image, True, False)
self.image = flipped_image
if self.on_ground and self.on_right:
self.rect = self.image.get_rect(bottomright = self.rect.bottomright)
elif self.on_ground and self.on_left:
self.rect = self.image.get_rect(bottomleft = self.rect.bottomleft)
elif self.on_ground:
self.rect = self.image.get_rect(midbottom = self.rect.midbottom)
elif self.on_ceiling and self.on_right:
self.rect = self.image.get_rect(topright = self.rect.topright)
elif self.on_ceiling and self.on_left:
self.rect = self.image.get_rect(topleft = self.rect.topleft)
elif self.on_ceiling:
self.rect = self.image.get_rect(midtop = self.rect.midtop)
def run_dust_animation(self):
if self.status == 'run' and self.on_ground:
self.dust_frame_index += self.dust_animation_speed
if self.dust_frame_index >= len(self.dust_run_particles):
self.dust_frame_index = 0
dust_particle = self.dust_run_particles[int(self.dust_frame_index)]
if self.facing_right:
pos = self.rect.bottomleft - pygame.math.Vector2(6, 10)
self.display_surface.blit(dust_particle, pos)
else:
pos = self.rect.bottomright - pygame.math.Vector2(6, 10)
flipped_dust_particle = pygame.transform.flip(dust_particle, True, False)
self.display_surface.blit(flipped_dust_particle, pos)
def get_input(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_RIGHT]:
self.direction.x = 1
self.facing_right = True
elif keys[pygame.K_LEFT]:
self.direction.x = -1
self.facing_right = False
else: self.direction.x = 0
if keys[pygame.K_SPACE] and self.on_ground:
self.jump()
self.create_jump_particles(self.rect.midbottom)
def get_status(self):
if self.direction.y < 0:
self.status = 'jump'
elif self.direction.y > 1:
self.status = 'fall'
else:
if self.direction.x != 0:
self.status = 'run'
else:
self.status = 'idle'
def apply_gravity(self):
self.direction.y += self.gravity
self.rect.y += self.direction.y
def jump(self):
self.direction.y = self.jump_speed
def update(self):
self.get_input()
self.get_status()
self.animate()
self.run_dust_animation()
level.py
import pygame
from support import import_csv_layout, import_cut_graphics
from settings import tile_size, screen_height, screen_width
from tiles import Tile, StaticTile, Crate, Coin, Palm
from enemy import Enemy
from decoration import Sky, Water, Clouds
from player import Player
from particle import ParticleEffect
class Level:
def __init__(self, level_data, surface):
self.display_surface = surface
self.world_shift = 0
self.current_x = None
player_layout = import_csv_layout(level_data['player'])
self.player = pygame.sprite.GroupSingle()
self.goal = pygame.sprite.GroupSingle()
self.player_setup(player_layout)
self.dust_sprite = pygame.sprite.GroupSingle()
self.player_on_ground = False
terrain_layout = import_csv_layout(level_data['terrain'])
self.terrain_sprites = self.create_tile_group(terrain_layout, 'terrain')
grass_layout = import_csv_layout(level_data['grass'])
self.grass_sprites = self.create_tile_group(grass_layout, 'grass')
crate_layout = import_csv_layout(level_data['crates'])
self.crate_sprites = self.create_tile_group(crate_layout, 'crates')
coin_layout = import_csv_layout(level_data['coins'])
self.coin_sprites = self.create_tile_group(coin_layout, 'coins')
fg_palm_layout = import_csv_layout(level_data['fg palms'])
self.fg_palm_sprites = self.create_tile_group(fg_palm_layout, 'fg palms')
bg_palm_layout = import_csv_layout(level_data['bg palms'])
self.bg_palms_sprites = self.create_tile_group(bg_palm_layout, 'bg palms')
enemy_layout = import_csv_layout(level_data['enemies'])
self.enemy_sprites = self.create_tile_group(enemy_layout, 'enemies')
constraint_layout = import_csv_layout(level_data['constraints'])
self.constraint_sprites = self.create_tile_group(constraint_layout, 'constraints')
self.sky = Sky(7)
level_width = len(terrain_layout[0]) * tile_size
self.water = Water(screen_height - 40, level_width)
self.clouds = Clouds(400, level_width, 30)
def create_tile_group(self, layout, type):
sprite_group = pygame.sprite.Group()
for row_index, row in enumerate(layout):
for col_index, val in enumerate(row):
if val != '-1':
x = col_index * tile_size
y = row_index * tile_size
if type == 'terrain':
terrain_tile_list = import_cut_graphics('../graphics/terrain/terrain_tiles.png')
tile_surface = terrain_tile_list[int(val)]
sprite = StaticTile(tile_size, x, y, tile_surface)
if type == 'grass':
grass_tile_list = import_cut_graphics('../graphics/decoration/grass/grass.png')
grass_surface = grass_tile_list[int(val)]
sprite = StaticTile(tile_size, x, y, grass_surface)
if type == 'crates':
sprite = Crate(tile_size, x, y)
if type == 'coins':
if val == '0': sprite = Coin(tile_size, x, y, '../graphics/coins/gold')
if val == '1': sprite = Coin(tile_size, x, y, '../graphics/coins/silver')
if type == 'fg palms':
if val == '0': sprite = Palm(tile_size, x, y, '../graphics/terrain/palm_small', 38)
if val == '1': sprite = Palm(tile_size, x, y, '../graphics/terrain/palm_large', 64)
if type == 'bg palms':
sprite = Palm(tile_size, x, y, '../graphics/terrain/palm_bg', 64)
if type == 'enemies':
sprite = Enemy(tile_size, x, y)
if type == 'constraints':
sprite = Tile(tile_size, x, y)
sprite_group.add(sprite)
return sprite_group
def player_setup(self, layout):
for row_index, row in enumerate(layout):
for col_index, val in enumerate(row):
x = col_index * tile_size
y = row_index * tile_size
if val == '0':
sprite = Player((x, y), self.display_surface, self.create_jump_particles)
self.player.add(sprite)
if val == '1':
hat_surface = pygame.image.load('../graphics/character/hat.png').convert_alpha()
sprite = StaticTile(tile_size, x, y, hat_surface)
self.goal.add(sprite)
def enemy_collision_reverse(self):
for enemy in self.enemy_sprites.sprites():
if pygame.sprite.spritecollide(enemy, self.constraint_sprites, False):
enemy.reverse()
def create_jump_particles(self, pos):
if self.player.sprite.facing_right:
pos -= pygame.math.Vector2(10, 5)
else:
pos += pygame.math.Vector2(10, -5)
jump_particle_sprite = ParticleEffect(pos, 'jump')
self.dust_sprite.add(jump_particle_sprite)
def horizontal_movement_collision(self):
player = self.player.sprite
player.rect.x += player.direction.x * player.player_speed
collidable_sprites = self.terrain_sprites.sprites() + self.crate_sprites.sprites() + self.fg_palm_sprites.sprites()
for sprite in collidable_sprites:
if sprite.rect.colliderect(player.rect):
if player.direction.x < 0:
player.rect.left = sprite.rect.right
player.on_left = True
self.current_x = player.rect.left
elif player.direction.x > 0:
player.rect.right = sprite.rect.left
player.on_right = True
self.current_x = player.rect.right
if player.on_left and (player.rect.left < self.current_x or player.direction.x >= 0):
player.on_left = False
if player.on_right and (player.rect.right > self.current_x or player.direction.x <= 0):
player.on_right = False
def vertical_movement_collision(self):
player = self.player.sprite
player.apply_gravity()
collidable_sprites = self.terrain_sprites.sprites() + self.crate_sprites.sprites() + self.fg_palm_sprites.sprites()
for sprite in collidable_sprites:
if sprite.rect.colliderect(player.rect):
if player.direction.y > 0:
player.rect.bottom = sprite.rect.top
player.direction.y = 0
player.on_ground = True
elif player.direction.y < 0:
player.rect.top = sprite.rect.bottom
player.direction.y = 0
player.on_ceiling = True
if player.on_ground and player.direction.y < 0 or player.direction.y > 1:
player.on_ground = False
if player.on_ceiling and player.direction.y > 0:
player.on_ceiling = False
def scroll_x(self):
player = self.player.sprite
player_x = player.rect.centerx
direction_x = player.direction.x
if player_x < screen_width / 4 and direction_x < 0:
self.world_shift = 8
player.player_speed = 0
elif player_x > screen_width - (screen_width / 4) and direction_x > 0:
self.world_shift = -8
player.player_speed = 0
else:
self.world_shift = 0
player.player_speed = 8
def get_player_on_ground(self):
if self.player.sprite.on_ground:
self.player_on_ground = True
else:
self.player_on_ground = False
def create_landing_dust(self):
if not self.player_on_ground and self.player.sprite.on_ground and not self.dust_sprite.sprites():
if self.player.sprite.facing_right:
offset = pygame.math.Vector2(10, 15)
else:
offset = pygame.math.Vector2(-10, 15)
fall_dust_particle = ParticleEffect(self.player.sprite.rect.midbottom - offset, 'land')
self.dust_sprite.add(fall_dust_particle)
def run(self):
self.sky.draw(self.display_surface)
self.clouds.draw(self.display_surface, self.world_shift)
self.bg_palms_sprites.update(self.world_shift)
self.bg_palms_sprites.draw(self.display_surface)
self.terrain_sprites.update(self.world_shift)
self.terrain_sprites.draw(self.display_surface)
self.constraint_sprites.update(self.world_shift)
self.enemy_collision_reverse()
self.enemy_sprites.update(self.world_shift)
self.enemy_sprites.draw(self.display_surface)
self.crate_sprites.update(self.world_shift)
self.crate_sprites.draw(self.display_surface)
self.grass_sprites.update(self.world_shift)
self.grass_sprites.draw(self.display_surface)
self.coin_sprites.update(self.world_shift)
self.coin_sprites.draw(self.display_surface)
self.fg_palm_sprites.update(self.world_shift)
self.fg_palm_sprites.draw(self.display_surface)
self.dust_sprite.update(self.world_shift)
self.dust_sprite.draw(self.display_surface)
self.player.update()
self.horizontal_movement_collision()
self.get_player_on_ground()
self.vertical_movement_collision()
self.create_landing_dust()
self.scroll_x()
self.player.draw(self.display_surface)
self.goal.update(self.world_shift)
self.goal.draw(self.display_surface)
self.water.draw(self.display_surface, self.world_shift)
KEEP
PROBLEM
- 간헐적으로 발생하는 적의 움직임 버그
- 맵의 일부 지형의 이미지가 올바르지 않음
- 일부 맵의 적을 회피해야하는 난이도가 높음
TRY
- 코드 분석을 통해 버그가 생기는 부분을 최대한 파악
- 올바르지 않은 지형의 이미지를 다시 대체