Skip to content

Commit

Permalink
구조 문서화 8
Browse files Browse the repository at this point in the history
- 중력 & 점프, 무적 시간, 충돌 감지, 스프라이트, 캐릭터, 버튼
  • Loading branch information
MineEric64 committed Aug 22, 2023
1 parent 7951a5f commit 636eb78
Show file tree
Hide file tree
Showing 54 changed files with 617 additions and 176 deletions.
137 changes: 96 additions & 41 deletions characters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,28 @@

class Character(ABC):
image: pygame.Surface | pygame.SurfaceType
"""단일 이미지"""

image_path: str
"""단일 이미지 경로"""

image_flipped: bool = False
"""단일 이미지가 좌우반전 되어있는가?"""

sprites: SpriteCollection = None
"""다중 스프라이트"""

"""
캐릭터 클래스는 단일 이미지 / 다중 스프라이트 중 하나를 지원하여 어느 것을 사용할지 선택할 수 있습니다.
1. 단일 이미지 => 기본 생성자 __init__()
2. 다중 스프라이트 => Player.get_from_sprite()
"""

x: int = 0
y: int = 0
width: int = 0
height: int = 0
"""좌표 및 크기"""

is_playable: bool = False
"""플레이어인가?"""
Expand All @@ -44,8 +57,8 @@ class Character(ABC):
velocity_y = 0.0
"""플레이어의 속도 (Y)"""

"""플레이어가 공중에 떠 있는가?"""
is_air = False
"""플레이어가 공중에 떠 있는가?"""

sign = None
"""말풍선"""
Expand All @@ -66,57 +79,72 @@ def __init__(
):
"""
캐릭터 클래스 생성
:param path: 캐릭터 이미지 경로
:param scale: 캐릭터 크기
:param position: 캐릭터가 위치한 절대좌표
:param scale: 캐릭터 상대크기
:param flipped: 이미지가 처음부터 좌우반전 되어 있었는가?
:param fit: 화면 크기에 맞춰 스케일링 할 것인가?
:param repeat_x: 이미지가 가로로 반복될 횟수
:param repeat_y: 이미지가 세로로 반복될 횟수
:param is_playable: 플레이어인가?
"""
if path == "":
if path == "": # 다중 스프라이트로 초기화하기 위해 단일 이미지를 선택하지 않은 경우
self.image_path = ""
return
return # 빈 클래스로 반환

# 단일 이미지로 설정
self.image_path = path
self.image = pygame.image.load(path)
self.image_flipped = flipped

if fit:
if fit: # 화면 크기에 맞춰 스케일링
self.image = pygame.transform.scale(self.image, CONST.SCREEN_SIZE)
else:
else: # 상대크기에 맞춰 스케일링
scale_x = float(self.image.get_width()) * scale
scale_y = float(self.image.get_height()) * scale

self.image = pygame.transform.scale(self.image, (scale_x, scale_y))

if repeat_x > 1 or repeat_y > 1:
width_per_image = self.image.get_width()
height_per_image = self.image.get_height()
image_repeated = pygame.Surface((width_per_image * repeat_x, height_per_image * repeat_y), pygame.SRCALPHA, 32)
if repeat_x > 1 or repeat_y > 1: # 이미지가 반복되는 경우
width_per_image = self.image.get_width() # 이미지당 너비
height_per_image = self.image.get_height() # 이미지당 높이
image_repeated = pygame.Surface((width_per_image * repeat_x, height_per_image * repeat_y), pygame.SRCALPHA, 32) # 배경이 투명한 이미지 생성

for y in range(0, repeat_y):
for x in range(0, repeat_x):
image_repeated.blit(self.image, (x * width_per_image, y * height_per_image))
image_repeated.blit(self.image, (x * width_per_image, y * height_per_image)) # 이미지 렌더링 반복

self.image = image_repeated
self.image = image_repeated # 반복된 이미지로 저장

# 좌표 & 크기 변수 초기화
self.x = position[0]
self.y = position[1]
self.width = self.image.get_width()
self.height = self.image.get_height()

if is_playable:
if is_playable: # 플레이어인 경우 플레이어 관련 변수 업데이트
CONFIG.player_width = self.width
CONFIG.player_height = self.height

self.is_playable = is_playable

def get_pos(self) -> tuple:
"""
캐릭터의 현재 좌표를 가져옵니다.
:return: 현재 좌표
"""
return (self.x, self.y)

def set_pos(self, x: int, y: int):
"""
캐릭터의 현재 좌표를 설정합니다.
:param x: 설정할 X 좌표
:param y: 설정할 Y 좌표
"""
self.x = x
self.y = y

if self.is_playable:
if self.is_playable: # 플레이어인 경우 플레이어 관련 변수 업데이트
CONFIG.player_x = x
CONFIG.player_y = y

Expand Down Expand Up @@ -146,29 +174,34 @@ def is_bound(self, error_x = 0, error_y = 0, obj_x = -1, obj_y = -1, obj_width =
:param obj_width: 플레이어 대신 비교할 Width (기본: -1)
:param obj_height: 플레이어 대신 비교할 Height (기본: -1)
:param hitbox: 히트박스 여부 (디버깅)
:return: 충돌 감지 여부
"""
compared_x = CONFIG.player_x + CONFIG.player_width / 2 if obj_x == -1 else obj_x + obj_width / 2
compared_y = CONFIG.player_y + CONFIG.player_height / 2 if obj_y == -1 else obj_y + obj_height / 2

is_x = (
compared_x >= self.x - error_x and compared_x <= self.x + self.width + error_x
)
is_y = (
compared_y >= self.y - error_y and compared_y <= self.y + self.height + error_y
)

if hitbox:
pygame.draw.rect(CONFIG.surface, (0, 200, 0), (self.x - error_x, self.y - error_y, self.width + error_x * 2, self.height + error_x * 2), 3)
# 비교할 좌표 기준을 오브젝트의 중심으로 설정
compared_x = CONFIG.player_x + CONFIG.player_width / 2 if obj_x == -1 else obj_x + obj_width / 2 # 비교할 X 좌표 (obj_x가 -1인 경우 플레이어로 설정)
compared_y = CONFIG.player_y + CONFIG.player_height / 2 if obj_y == -1 else obj_y + obj_height / 2 # 비교할 Y 좌표 (obj_y가 -1인 경우 플레이어로 설정)

# 비교할 오브젝트의 좌표가 현재 오브젝트의 좌표 범위 안에 있는 경우
# 오차 보정을 해야하는 경우 보정값 추가
is_x = self.x - error_x <= compared_x <= self.x + self.width + error_x
is_y = self.y - error_y <= compared_y <= self.y + self.height + error_y

if hitbox: # 히트박스를 그려야 하는 경우
x = self.x - error_x
y = self.y - error_y
width = self.width + error_x * 2
height = self.height + error_y * 2
pygame.draw.rect(CONFIG.surface, (0, 200, 0), (x, y, width, height), 3) # 두께가 3이고 초록색으로 히트박스 범위를 그림

return is_x and is_y

def generate_hitbox(self):
"""히트박스를 생성합니다. (디버깅)"""
pygame.draw.rect(CONFIG.surface, (0, 200, 0), (self.x + self.width / 2, self.y + self.height / 2, 3, 3), 3)

def is_camera_bound(self) -> bool:
def is_camera_bound(self, error: float = 1.00) -> bool:
"""
해당 오브젝트가 카메라 안에 들어와있는지 확인합니다.
:param error: 오차 보정값
:return: 오브젝트가 카메라 안에 들어왔는지 여부
"""

Expand All @@ -178,9 +211,9 @@ def is_camera_bound(self) -> bool:
camera_width = camera[2]
camera_height = camera[3]

# 양쪽 범위를 끝까지 벗어나지 않았을 때도 렌더링 해야하므로 보정값 추가
is_x = camera_x <= self.x + self.width <= camera_x + camera_width + self.width
is_y = camera_y <= self.y + self.height <= camera_y + camera_height + self.height
# 양쪽 범위를 끝까지 벗어나지 않았을 때도 렌더링 해야하므로 보정값 추가, 오차 보정값이 있는 경우 오차 보정
is_x = camera_x <= self.x + self.width <= (camera_x + camera_width + self.width) / error
is_y = camera_y <= self.y + self.height <= (camera_y + camera_height + self.height) / error

return is_x and is_y

Expand Down Expand Up @@ -208,35 +241,57 @@ def render(self, other_surface: pygame.Surface = None, optimization = True):
other_surface.blit(self.sign.image, self.sign.get_pos())

def speech(self, sign):
if self.sign is None:
self.sign = sign
"""
캐릭터가 대화를 할 수 있도록 합니다.
:param sign: 말풍선
"""
if self.sign is None: # 말풍선이 초기화되어 있는 경우
self.sign = sign # 말풍선 설정

# 말풍선 좌표 설정
x = self.x - self.sign.width + 10
y = self.y - self.sign.height + 5

sign.set_pos(x, y)
TextEvent.dialog.set_position((10, 10))
TextEvent.dialog.set_position((10, 10)) # 말풍선 속 텍스트 상대 좌표 설정

def unspeech(self):
if self.sign is None:
"""
캐릭터가 대화가 끝났을 때 관련 변수를 재설정합니다.
"""
if self.sign is None: # 이미 말풍선이 초기화된 경우 반환
return

self.sign = None
self.sign = None # 말풍선 초기화

def refresh(self):
"""단일 이미지를 새로고침합니다."""
self.image = pygame.image.load(self.image_path)
self.image = pygame.transform.scale(self.image, (self.width, self.height))
self.image = pygame.transform.scale(self.image, (self.width, self.height)) # 상대크기에 맞춰 스케일링

def flip_image(self):
"""단일 이미지를 좌우 반전시킵니다."""
self.image = pygame.transform.flip(self.image, True, False)
self.image_flipped = not self.image_flipped
self.image_flipped = not self.image_flipped # 좌우 반전 변수 업데이트

def check_if_attacked(self, attacked: bool):
"""
공격받았는지 확인 후, 공격받았을 때 조건이 충족된 경우 공격받았다고 설정됩니다.
:param attacked: 공격받았는지 여부 (보통 함수로부터 반환됨)
"""

"""
공격받았을 때 조건
1. 공격을 받았는가?
2. 무적 시간에 위배되지 않았는가?
3. 플레이어가 상호작용하고 움직일 수 있었는가?
"""

if (not self.attacked
and not self.grace_period.is_grace_period()
and CONFIG.is_movable()):
and not self.grace_period.is_grace_period() # 무적 시간 계산
and CONFIG.is_movable()): # 플레이어가 움직일 수 있는 경우

self.attacked = attacked
self.attacked = attacked # 공격받았다고 설정

def get_surface_or_sprite(self) -> SpriteCollection | pygame.Surface:
"""단일 이미지인 경우 pygame.Surface를 반환하고, 다중 스프라이트인 경우 SpriteCollection을 반환합니다."""
Expand Down
23 changes: 12 additions & 11 deletions characters/enemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,24 @@
from components.sprites.sprite import Sprite

class Enemy(Player):
is_attacking = False
'''적이 플레이어를 공격하고 있는가?'''

def follow_player(self, obstacles: list[Texture]):
"""적이 플레이어를 따라가도록 합니다."""
if self.hp == 0:
"""
적이 플레이어를 따라가도록 합니다.
:param obstacles: 적이 지나다닐 수 없는 장애물 배열
"""
if self.hp <= 0:
return # 사망

if CONFIG.camera_x <= self.x <= (CONFIG.camera_x + CONST.SCREEN_SIZE[0] / 1.05): # 플레이어 화면 범위에 있는 경우
velocity_x = 0.7
if self.is_camera_bound(1.02): # 플레이어 화면 범위에 있는 경우 (보정값 추가)
velocity_x = 0.7 # 적의 이동 속도

if self.x > CONFIG.player_x:
if self.x > CONFIG.player_x: # 플레이어보다 뒤에 있는 경우 따라가기 위해 속도를 음수로 설정
velocity_x *= -1

if self.x != CONFIG.player_x and abs(velocity_x * 10) <= abs(CONFIG.player_x - self.x):
self.move_x(velocity_x)
if self.x != CONFIG.player_x and abs(velocity_x * 10) <= abs(CONFIG.player_x - self.x): # 따라가야할 위치가 플레이어에게 따라갈 수 있는 최소 위치보다 많은 경우
self.move_x(velocity_x) # 움직임

for obstacle in obstacles: # 장애물을 피해 다님
# 장애물을 인식하고 피해 다님
for obstacle in obstacles:
if obstacle.is_bound(5, 40, self.x, self.y, self.width, self.height): # 주변에 장애물이 있다면
self.move_y(13) # 점프
19 changes: 11 additions & 8 deletions characters/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def move_x(self, velocity: float):
(velocity > 0 and self.x <= 0)
or (velocity < 0 and self.x + self.width >= CONST.SURFACE_SIZE[0])
or (self.x >= 0 and self.x + self.width <= CONST.SURFACE_SIZE[0])
): # 윈도우 내부에 위치해 있는 경우 / 범위를 벗어났을 때 다시 범위 안으로 들어가려고 할 경우
): # 세계 내부에 위치해 있는 경우 / 범위를 벗어났을 때 다시 범위 안으로 들어가려고 할 경우
multiplied = 1 if not self.is_air else 0.7 # 공중에 떠 있으면 패널티 부여

self.velocity_x = velocity # 현재 속도 저장
Expand Down Expand Up @@ -61,29 +61,32 @@ def apply_movement_flipped(self, image: SpriteCollection | SpriteHandler | Sprit
if (self.velocity_x > 0 and sprite.flipped) or (
self.velocity_x < 0 and not sprite.flipped
): # 방향이 반대인 경우
sprite.flip()
sprite.flip() # 스프라이트 좌우 반전
elif surface is not None:
if (self.velocity_x > 0 and self.image_flipped) or (
self.velocity_x < 0 and not self.image_flipped
): # 방향이 반대인 경우
self.flip_image()
self.flip_image() # 단일 이미지 좌우 반전

@classmethod
def get_from_sprite(cls, sprites: SpriteCollection, is_playable=False) -> "Player":
def get_from_sprite(cls, sprites: SpriteCollection, is_playable = False) -> "Player":
"""
캐릭터 클래스 생성
다중 스프라이트를 사용하는 캐릭터 클래스를 생성합니다.
:param sprite: 캐릭터 스프라이트
:param sprites: 캐릭터 다중 스프라이트
:param is_playable: 플레이어인가?
"""
chr = Player("", (0, 0))

# 변수 초기화
chr = Player("", (0, 0)) # 기본 생성자는 다중 스프라이트를 지원하지 않으므로 빈 변수로 초기화 후 다중 스프라이트 추가
chr.sprites = sprites

chr.x = sprites.position[0]
chr.y = sprites.position[1]
chr.width = sprites.size[0]
chr.height = sprites.size[1]

if is_playable:
if is_playable: # 플레이어면 플레이어 관련 변수 업데이트
CONFIG.player_x = chr.x
CONFIG.player_y = chr.y
CONFIG.player_width = chr.width
Expand Down
Loading

0 comments on commit 636eb78

Please sign in to comment.