Skip to content

Instantly share code, notes, and snippets.

@RyuaNerin
Created December 15, 2021 10:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RyuaNerin/09ffe28412081ddd9fd4c848e8c19205 to your computer and use it in GitHub Desktop.
Save RyuaNerin/09ffe28412081ddd9fd4c848e8c19205 to your computer and use it in GitHub Desktop.
tkinter 를 이용한 간단한 게임.
#!/bin/python3
# -*- coding: utf-8 -*-
import enum
import math
import random
import threading
import time
import tkinter as tk
import typing
from typing import Final as const
####################################################################################################
# 좌표, 크기, 바운딩 등을 직관화 하기 위한 부분
class Point(typing.NamedTuple):
x: float
y: float
def __add__(self, p):
return Point(self.x + p.x, self.y + p.y)
def __sub__(self, p):
return Point(self.x - p.x, self.y - p.y)
class Size(typing.NamedTuple):
w: float
h: float
class Bounds(typing.NamedTuple):
left: float
top: float
right: float
bottom: float
class Text:
def __init__(
self,
text: typing.Union[str, typing.Callable[[typing.Any], str]],
font: typing.Tuple[str, str, str],
point: Point,
anchor: str = "s",
color: typing.Optional[
typing.Union[str, typing.Callable[[typing.Any], str]]
] = None,
) -> None:
self.text = text
self.font = font
self.point = Point(*point)
self.color = color
self.anchor = anchor
def to_bounds(center: Point, size: Size) -> Bounds:
"""사각형 바운딩 크기를 계산하는 함수
Args:
center: 중앙 좌표
size : 크기
Returns:
Bounds: 바운딩 (left, top, right, bottom 순)
"""
center = Point(*center)
size = Size(*size)
return Bounds(
center.x - size.w / 2,
center.y - size.h / 2,
center.x + size.w / 2,
center.y + size.h / 2,
)
####################################################################################################
"""
상수 영역
- 기준점이 별도로 설정되어 있지 않으면 중앙 기준
"""
PI0: const = 0
PI1: const = math.pi / 2
PI2: const = math.pi
PI3: const = math.pi / 2 * 3
PI4: const = math.pi * 2
FPS: const = 60
GAME_TIME: const = 90 # 게임 진행 시간
BALL_WAIT_TIME: const = 2 # 공 출발 전 기다려주는 시간
BALL_MOVE_MIN: const = 1.0 # 이동 계산을 할 최소 길이
SLEEP_WHEN_SCORE: const = 2 # 점수 발생 시 잠시 기다릴 시간
# --------------------------------------------------------------------------------
WINDOW_SIZE: const = Size(600, 400)
WINDOW_BACKGROUND: const = "#FFFFFF"
# --------------------------------------------------------------------------------
WALL_COLOR: const = "#BDBDBD"
GOAL_COLOR: const = "#FFCC80"
WALL_THICKNESS: const = 32
GOAL_SIZE: const = Size(WALL_THICKNESS / 2, 150)
GOAL_BOUNDS_P1: const = to_bounds(
(WINDOW_SIZE.w - GOAL_SIZE.w / 2, WINDOW_SIZE.h / 2), GOAL_SIZE
)
GOAL_BOUNDS_P2: const = to_bounds((GOAL_SIZE.w / 2, WINDOW_SIZE.h / 2), GOAL_SIZE)
WALL_HORZ_SIZE: const = Size(WINDOW_SIZE.w - WALL_THICKNESS * 2, WALL_THICKNESS)
WALL_VERT_SIZE: const = Size(
WALL_THICKNESS, (WINDOW_SIZE.h - WALL_THICKNESS * 2 - GOAL_SIZE.h) / 2
)
WALL_TOP_BOUNDS: const = to_bounds(
(WINDOW_SIZE.w / 2, WALL_THICKNESS / 2), WALL_HORZ_SIZE
)
WALL_BOTTOM_BOUNDS: const = to_bounds(
(WINDOW_SIZE.w / 2, WINDOW_SIZE.h - WALL_THICKNESS / 2), WALL_HORZ_SIZE
)
WALL_LEFT_TOP_BOUNDS: const = to_bounds(
(WALL_THICKNESS / 2, WALL_THICKNESS + WALL_VERT_SIZE.h / 2), WALL_VERT_SIZE
)
WALL_LEFT_BOTTOM_BOUNDS: const = to_bounds(
(WALL_THICKNESS / 2, WINDOW_SIZE.h - WALL_THICKNESS - WALL_VERT_SIZE.h / 2),
WALL_VERT_SIZE,
)
WALL_RIGHT_TOP_BOUNDS: const = to_bounds(
(WINDOW_SIZE.w - WALL_THICKNESS / 2, WALL_THICKNESS + WALL_VERT_SIZE.h / 2),
WALL_VERT_SIZE,
)
WALL_RIGHT_BOTTOM_BOUNDS: const = to_bounds(
(
WINDOW_SIZE.w - WALL_THICKNESS / 2,
WINDOW_SIZE.h - WALL_THICKNESS - WALL_VERT_SIZE.h / 2,
),
WALL_VERT_SIZE,
)
# 중앙쪽에 벽을 배치
WALL_ADDITIONAL_LIST = [
to_bounds(Point(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2 - 100), (30, 30)),
to_bounds(Point(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2 + 100), (30, 30)),
]
# --------------------------------------------------------------------------------
BALL_COLOR: const = "#EF5350"
BALL_COLOR_BOUNCE: const = "#EF9A9A"
BALL_COLOR_HIGHLIGHT: const = ["#B71C1C", "#EF5350"]
BALL_BOUNCE_COLOR_TIME: const = 0.1 # 공이 부딛혔을때 색이 잠깐 변하는 시간
BALL_RADIUS: const = 10 # 공의 반지름
BALL_SIZE: const = Size(BALL_RADIUS * 2, BALL_RADIUS * 2)
BALL_POS_INIT: const = Point(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2) # 공의 초기 위치
BALL_SPEED_INIT: const = 250 # 공의 기본 이동 속도
BALL_SPEED_ADDITIONAL: const = 10 # 공이 벽에 부딛힐 때 마다 빨라지는 속도
BALL_ANGLE_INIT_RANGE: const = PI1 * 2 / 5 # 공이 처음 출발할 때 제한할 각도.
BALL_ANGLE_VERT_LIMIT: const = PI1 / 6 # 너무 세로로만 이동하면 각도의 보정을 수행
# --------------------------------------------------------------------------------
BAR_P1_COLOR: const = ["#26C6DA", "#00ACC1"]
BAR_P2_COLOR: const = ["#66BB6A", "#43A047"]
BAR_SIZE: const = Size(5, 60)
BAR_Y_INIT: const = WINDOW_SIZE.h / 2
BAR_Y_RANGE: const = (
WALL_THICKNESS + BAR_SIZE[1] / 2,
WINDOW_SIZE.h - BAR_SIZE[1] / 2 - WALL_THICKNESS,
)
BAR_P1_X: const = 50
BAR_P2_X: const = WINDOW_SIZE.w - BAR_P1_X
BAR_SPEED_PER_SEC: const = 400 # 1초당 움직이는 픽셀 수
BAR_BALL_REBOUND_ANGLE: const = PI1 # 바 끝에서 맞은 경우 반사되는 각도. 공이 바 중앙에 맞을때는 0을 기준(수평)으로 함.
BAR_BALL_REBOUND_ACCELERATION: const = 0.5 # 바 끝에서 맞은 경우 반사되는 추가 속도 배율. 공이 바 중앙에 맞을때는 x1
BAR_GLOW_TIME = 0.1
# --------------------------------------------------------------------------------
RENDER_MAIN_TITLE: const = Text(
text="Simple Game", font=("맑은 고딕", "40", "bold"), point=(WINDOW_SIZE.w / 2, 20), anchor="n"
)
RENDER_MAIN_NAME: const = Text(
text="By RyuaNerin",
font=("맑은 고딕", "10", "bold"),
point=(WINDOW_SIZE.w - 50, 100),
anchor="ne",
)
RENDER_MAIN_KEY: const = Text(
text="""- 단축키 -
Player 1 : q, a
Player 2 : UP, DOWN
일시정지 : ESC
공 이동경로 표시 : T
- 게임 규칙 -
공은 반사될 때 마다 조금씩 빨라집니다.
공이 각 플레이어의 막대기 중앙에 부딛힐 수록 직선으로, 빠르게 튕깁니다.""",
font=("맑은 고딕", "10", "bold"),
point=(10, WINDOW_SIZE.h / 2),
anchor="w",
)
RENDER_MAIN_HOWTOSTART: const = Text(
text="게임을 시작하려면 엔터키를 눌러주세요",
font=("맑은 고딕", "15", "bold"),
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2 + 150),
)
# --------------------------------------------------------------------------------
RENDER_GAME_SCORE_DESC_P1: const = Text(
text="Player 1",
font=("맑은 고딕", "10", "bold"),
point=(WINDOW_SIZE.w / 2 - 100, WALL_THICKNESS + 20),
)
RENDER_GAME_SCORE_DESC_P2: const = Text(
text="Player 2",
font=("맑은 고딕", "10", "bold"),
point=(WINDOW_SIZE.w / 2 + 100, WALL_THICKNESS + 20),
)
RENDER_GAME_SCORE_VALUE_P1: const = Text(
text="",
font=("맑은 고딕", "20", "bold"),
point=(WINDOW_SIZE.w / 2 - 100, WALL_THICKNESS + 60),
)
RENDER_GAME_SCORE_VALUE_P2: const = Text(
text="",
font=("맑은 고딕", "20", "bold"),
point=(WINDOW_SIZE.w / 2 + 100, WALL_THICKNESS + 60),
)
RENDER_GAME_SEC_DESC: const = Text(
text="남은 시간",
font=("맑은 고딕", "10", "bold"),
point=(WINDOW_SIZE.w / 2, WALL_THICKNESS + 20),
)
RENDER_GAME_SEC_VALUE: const = Text(
text=lambda x: f"{x}",
font=("맑은 고딕", "30", "bold"),
color=lambda x: ["#000000", "#F44336"][int(x * 5) % 2],
point=(WINDOW_SIZE.w / 2, WALL_THICKNESS + 60),
)
RENDER_BALL_MOVE_COUNT: const = Text(
text=lambda x: f"{int(x)}",
font=("맑은 고딕", "20", "bold"),
color="#3E2723",
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2),
)
# --------------------------------------------------------------------------------
RENDER_WIN_TITLE: const = Text(
text=lambda x: ["무승부", "Player1 승리", "Player2 승리"][x],
font=("맑은 고딕", "40", "bold"),
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2),
)
RENDER_WIN_SCORE: const = Text(
text=lambda x: f"{x[0]} VS {x[1]}",
font=("맑은 고딕", "20", "bold"),
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2 + 50),
)
RENDER_WIN_RESTART: const = Text(
text="처음으로 돌아가려면 ESC 를 눌러주세요",
font=("맑은 고딕", "15", "bold"),
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2 + 150),
)
# --------------------------------------------------------------------------------
RENDER_PAUSE: const = Text(
text="일시정지",
font=("맑은 고딕", "30", "bold"),
point=(WINDOW_SIZE.w / 2, WINDOW_SIZE.h / 2),
)
# --------------------------------------------------------------------------------
KEYCODE_P1_UP: const = ord("Q")
KEYCODE_P1_DOWN: const = ord("A")
KEYCODE_P2_UP: const = 38 # UP
KEYCODE_P2_DOWN: const = 40 # DOWN
KEYCODE_PAUSE: const = 27 # ESC
KEYCODE_START: const = 13 # ENTER
####################################################################################################
class KeyStatus(enum.Enum):
NORMAL = 0
UP = 1
DOWN = 2
class MainStatus(enum.Enum):
Main = 0
Game = 1
Pause = 2
Win = 3
class Game:
def __init__(self) -> None:
self._win = tk.Tk()
self._win.title("Simple Game")
self._win.geometry(f"{WINDOW_SIZE.w}x{WINDOW_SIZE.h}")
self._win.minsize(WINDOW_SIZE.w, WINDOW_SIZE.h)
self._win.maxsize(WINDOW_SIZE.w, WINDOW_SIZE.h)
self._canvas = tk.Canvas(
self._win,
width=WINDOW_SIZE.w,
height=WINDOW_SIZE.h,
background=WINDOW_BACKGROUND,
)
self._canvas.pack(fill="both", expand=True)
self._win.bind("<KeyPress>", self._win_key_press)
self._win.bind("<KeyRelease>", self._win_key_release)
self._canvas_id_ball_count = None # 공 이동 카운트다운
self._game_main()
threading.Thread(target=self._animation, daemon=True).start()
def run(self):
self._win.mainloop()
def _win_key_press(self, e: tk.Event):
if e.keycode == KEYCODE_P1_UP:
self._p1_status = KeyStatus.UP
elif e.keycode == KEYCODE_P1_DOWN:
self._p1_status = KeyStatus.DOWN
elif e.keycode == KEYCODE_P2_UP:
self._p2_status = KeyStatus.UP
elif e.keycode == KEYCODE_P2_DOWN:
self._p2_status = KeyStatus.DOWN
elif e.keycode == KEYCODE_START and self._game_state == MainStatus.Main:
self._game_start()
elif e.keycode == KEYCODE_PAUSE:
if self._game_state == MainStatus.Game:
self._game_pause()
elif self._game_state == MainStatus.Pause:
self._game_resume()
elif self._game_state == MainStatus.Win:
self._game_main()
def _win_key_release(self, e: tk.Event):
if e.keycode == KEYCODE_P1_UP and self._p1_status == KeyStatus.UP:
self._p1_status = KeyStatus.NORMAL
elif e.keycode == KEYCODE_P1_DOWN and self._p1_status == KeyStatus.DOWN:
self._p1_status = KeyStatus.NORMAL
elif e.keycode == KEYCODE_P2_UP and self._p2_status == KeyStatus.UP:
self._p2_status = KeyStatus.NORMAL
elif e.keycode == KEYCODE_P2_DOWN and self._p2_status == KeyStatus.DOWN:
self._p2_status = KeyStatus.NORMAL
####################################################################################################
def _game_main(self):
"""메인 함수를 띄우는 함수
"""
self._game_state = MainStatus.Main
self._canvas.delete("all")
self._create_text(RENDER_MAIN_KEY)
self._create_text(RENDER_MAIN_TITLE)
self._create_text(RENDER_MAIN_NAME)
self._create_text(RENDER_MAIN_HOWTOSTART)
def _game_pause(self):
"""일시정지 화면을 띄우는 함수
"""
self._game_state = MainStatus.Pause
self._time_paused_at = time.time()
self._create_text(RENDER_PAUSE, tag="pause")
def _game_resume(self):
"""일시정지 상태에서 복구시키는 함수
"""
self._canvas.delete("pause")
# 일시정지한 시간만큼 게임 끝나는 시간 연장
now = time.time()
paused_time = now - self._time_paused_at
self._time_rendered_at = now
self._ball_speed_base += paused_time
self._time_gameover_at += paused_time
self._game_state = MainStatus.Game
def _game_win(self):
"""승자와 점수를 표시하는 함수
"""
self._game_state = MainStatus.Win
self._canvas.delete("all")
self._create_text(
RENDER_WIN_TITLE,
x=0
if self._score_p1 == self._score_p2
else (1 if self._score_p1 > self._score_p2 else 2),
)
self._create_text(RENDER_WIN_SCORE, x=(self._score_p1, self._score_p2))
self._create_text(RENDER_WIN_RESTART)
def _game_start(self):
"""게임을 시작하기 위해 다양한 초기화를 진행하는 함수
"""
self._canvas.delete("all")
now = time.time()
self._time_gameover_at = now + GAME_TIME
self._time_rendered_at = now
self._time_scoring_before = None
self._time_p1_glow = None
self._time_p2_glow = None
self._time_ball_bounce = None
self._score_p1 = 0
self._score_p2 = 0
# --------------------------------------------------------------------------------
# 벽
self._create_rectangle(WALL_TOP_BOUNDS, fill=WALL_COLOR, width=0)
self._create_rectangle(WALL_BOTTOM_BOUNDS, fill=WALL_COLOR, width=0)
self._create_rectangle(WALL_LEFT_TOP_BOUNDS, fill=WALL_COLOR, width=0)
self._create_rectangle(WALL_LEFT_BOTTOM_BOUNDS, fill=WALL_COLOR, width=0)
self._create_rectangle(WALL_RIGHT_TOP_BOUNDS, fill=WALL_COLOR, width=0)
self._create_rectangle(WALL_RIGHT_BOTTOM_BOUNDS, fill=WALL_COLOR, width=0)
for bounds in WALL_ADDITIONAL_LIST:
self._create_rectangle(bounds, fill=WALL_COLOR, width=0)
# 골인 지점
self._create_rectangle(
GOAL_BOUNDS_P1, fill=GOAL_COLOR, width=0,
)
self._create_rectangle(
GOAL_BOUNDS_P2, fill=GOAL_COLOR, width=0,
)
# --------------------------------------------------------------------------------
# 남은 시간과 양 플레이어의 점수 표시
self._create_text(RENDER_GAME_SEC_DESC)
self._id_remain_seconds = self._create_text(RENDER_GAME_SEC_VALUE, x=GAME_TIME)
self._create_text(RENDER_GAME_SCORE_DESC_P1)
self._create_text(RENDER_GAME_SCORE_DESC_P2)
self._id_score_p1 = self._create_text(RENDER_GAME_SCORE_VALUE_P1, x=0)
self._id_score_p2 = self._create_text(RENDER_GAME_SCORE_VALUE_P2, x=0)
# --------------------------------------------------------------------------------
# 양측 플레이어 막대기
self._p1_bar_y = BAR_Y_INIT
self._p2_bar_y = BAR_Y_INIT
self._p1_bar_bounds = to_bounds((BAR_P1_X, self._p1_bar_y), BAR_SIZE)
self._p2_bar_bounds = to_bounds((BAR_P2_X, self._p2_bar_y), BAR_SIZE)
self._id_p1_bar = self._create_rectangle(
self._p1_bar_bounds, fill=BAR_P1_COLOR[0], width=0,
)
self._id_p2_bar = self._canvas.create_rectangle(
self._p2_bar_bounds, fill=BAR_P2_COLOR[0], width=0,
)
self._p1_status: KeyStatus = KeyStatus.NORMAL
self._p2_status: KeyStatus = KeyStatus.NORMAL
# --------------------------------------------------------------------------------
# 공 초기화 및 그리기
self._reset_ball()
self._id_ball = self._create_oval(
self._ball_center, BALL_RADIUS, fill=BALL_COLOR, width=0,
)
self._game_state = MainStatus.Game
####################################################################################################
def _reset_ball(self):
"""공 위치를 초기화 하는 함수
"""
now = time.time()
self._ball_center: Point = BALL_POS_INIT
self._ball_speed_base = BALL_SPEED_INIT
self._ball_speed = self._ball_speed_base
self._time_ball_move_after = now + BALL_WAIT_TIME
self._time_gameover_at += BALL_WAIT_TIME
d = random.randint(0, 3)
a = random.random() * BALL_ANGLE_INIT_RANGE
"""좌표평면에서는 x+ 시작이지만, 현재 코드에서는 y-축에서 시작"""
if d == 0: # ↗
self._ball_angle = PI3 + a
elif d == 1: # ↖
self._ball_angle = PI1 - a
elif d == 2: # ↙
self._ball_angle = PI1 + a
elif d == 3: # ↘
self._ball_angle = PI3 - a
print(
"init angle",
self._ball_angle,
math.degrees(self._ball_angle),
d,
math.degrees(a),
)
# 벽 종류
class _WallType(enum.Enum):
P1 = 0 # 플레이어 막대기
P2 = 1 # 플레이어 막대기
P1_GOAL = 2 # 플레이어 점수
P2_GOAL = 3 # 플레이어 점수
WALL = 4 # 벽
# 이동 방향
class _MoveDirection(enum.IntEnum):
UP = 0
LEFT = 1
RIGHT = 2
DOWN = 3
# 공의 충돌 정보
class _HitData(typing.NamedTuple):
point: Point # 충돌 지점
distance: float # 충돌 지점까지의 거리
direction: int # 공의 이동 방향
angle: float # 부딛힌 면의 각도
def _move_ball(self, distance: float):
"""공을 이동시킨다.
Args:
distance : 이동할 거리.
"""
now = time.time()
"""충돌 감지할 물체 목록
[0] 바운딩 정보
[1] Margin 보정을 할 것인지 여부
[2] 물체 타입
"""
WALL_LIST: typing.List[typing.Tuple[Bounds, bool, Game._WallType]] = [
(self._p1_bar_bounds, True, Game._WallType.P1),
(self._p2_bar_bounds, True, Game._WallType.P2),
(GOAL_BOUNDS_P1, False, Game._WallType.P1_GOAL),
(GOAL_BOUNDS_P2, False, Game._WallType.P2_GOAL),
(WALL_TOP_BOUNDS, True, Game._WallType.WALL),
(WALL_LEFT_TOP_BOUNDS, True, Game._WallType.WALL),
(WALL_LEFT_BOTTOM_BOUNDS, True, Game._WallType.WALL),
(WALL_RIGHT_TOP_BOUNDS, True, Game._WallType.WALL),
(WALL_RIGHT_BOTTOM_BOUNDS, True, Game._WallType.WALL),
(WALL_BOTTOM_BOUNDS, True, Game._WallType.WALL),
] + [(x, True, Game._WallType.WALL) for x in WALL_ADDITIONAL_LIST]
# ================================================================================
looped = 0
while BALL_MOVE_MIN < distance:
# 이동거리 계산
delta = Point(
distance * math.sin(self._ball_angle),
-distance * math.cos(self._ball_angle),
)
# 이동하는 횟수를 최대 N 회 까지만 계산한다. 넘어가면 강제 이동처리
looped += 1
if 20 < looped:
self._ball_center = self._ball_center + delta
break
# --------------------------------------------------------------------------------
# 공의 이동 경로가 각 물체에 부딛힌 위치를 계산. 부딛힌 자료를 저장한다.
hit_data_list: typing.List[typing.Tuple[Game._WallType, Game._HitData]] = []
for (bounds, with_margin, tag) in WALL_LIST:
hit_data = Game.___ball_hits_rect(
self._ball_center, delta, bounds, with_margin
)
if hit_data:
hit_data_list.append((tag, hit_data))
# --------------------------------------------------------------------------------
# 부딛히는 항목이 없다면 공을 이동시킴
if len(hit_data_list) == 0:
self._ball_center = self._ball_center + delta
break
hit_wall_type, hit_data = min(hit_data_list, key=lambda x: x[1])
# --------------------------------------------------------------------------------
# 점수내기의 경우 공 좌표가 리셋이된다...
if hit_wall_type in [Game._WallType.P1_GOAL, Game._WallType.P2_GOAL]:
if hit_wall_type == Game._WallType.P1_GOAL:
self._score_p1 += 1
else:
self._score_p2 += 1
self._ball_center = hit_data.point
self._time_scoring_before = now + SLEEP_WHEN_SCORE
self._time_gameover_at += SLEEP_WHEN_SCORE
break
# --------------------------------------------------------------------------------
"""
공통처리.
공 좌표를 충돌지점으로 이동,
기본 속도 증가
바운스 색상으로 변경...
남은 이동거리 계산,
"""
self._ball_center = hit_data.point
self._ball_speed_base += BALL_SPEED_ADDITIONAL
self._ball_speed = self._ball_speed_base
self._time_ball_bounce = now + BALL_BOUNCE_COLOR_TIME
# if not trace_mode:
distance = max(distance - hit_data.distance, 0)
"""
p1일때 왼 쪽으로 와서 오른쪽에 튕기거나
p2일때 오른쪽으로 와서 왼 쪽에 튕기거나 했을 땐
각도랑 속도를 조절한다.
"""
if (
hit_wall_type == Game._WallType.P1
and hit_data.direction == Game._MoveDirection.LEFT
) or (
hit_wall_type == Game._WallType.P2
and hit_data.direction == Game._MoveDirection.RIGHT
):
bar_y = (
self._p1_bar_y
if hit_wall_type == Game._WallType.P1
else self._p2_bar_y
)
k = (self._ball_center.y - bar_y) / BAR_SIZE.h
# 외각에서 맞으면 더 빠르게, 더 큰 각도로 이동.
if hit_wall_type == Game._WallType.P1:
self._ball_angle = (PI1 + BAR_BALL_REBOUND_ANGLE * k) % PI4
self._time_p1_glow = now + BAR_GLOW_TIME
else:
self._ball_angle = (PI3 - BAR_BALL_REBOUND_ANGLE * k) % PI4
self._time_p2_glow = now + BAR_GLOW_TIME
self._ball_speed = self._ball_speed_base * (
1 + BAR_BALL_REBOUND_ACCELERATION * k
)
else:
# 입사각 및 반사각 수정.
self._ball_angle = (hit_data.angle - self._ball_angle) % PI4
# 너무 직각으로 튀지 않도록 각도 추가 수정
if PI1 - BALL_ANGLE_VERT_LIMIT < self._ball_angle <= PI1: # 1사분면
self._ball_angle = PI1 - BALL_ANGLE_VERT_LIMIT
elif PI1 <= self._ball_angle <= PI1 + BALL_ANGLE_VERT_LIMIT: # 2사분면
self._ball_angle = PI1 + BALL_ANGLE_VERT_LIMIT
elif PI3 - BALL_ANGLE_VERT_LIMIT < self._ball_angle <= PI3: # 3사분면
self._ball_angle = PI3 - BALL_ANGLE_VERT_LIMIT
elif PI3 <= self._ball_angle < PI3 + BALL_ANGLE_VERT_LIMIT: # 4사분면
self._ball_angle = PI3 + BALL_ANGLE_VERT_LIMIT
# ================================================================================
# 공이 화면 밖으로 나간 경우 강제 재시작
if not (
0 <= self._ball_center.x <= WINDOW_SIZE.w
and 0 <= self._ball_center.y <= WINDOW_SIZE.h
):
self._reset_ball()
def ___ball_hits_rect(
center: Point, delta: Point, bounds: Bounds, with_margin: bool
) -> typing.Optional[_HitData]:
"""공이 이동 궤적이 해당 사각형 물체에 충돌할 수 있는지 판별하는 함수.
Args:
center : 공의 현재 중심
delta : 공이 이동할 좌표 변화값
bounds : 충돌을 감지할 물체
with_margin : 양 끝점 보정을 할 것인지
Results:
None : 출돌 위치가 없으면 반환.
_HitData : 충돌 위치와 거리, 충돌 위치를 반환.
"""
point: typing.Optional[Point] = None
dir: typing.Optional[Game._MoveDirection] = None # 충돌 위치
move_to = center + delta
margin = BALL_RADIUS if with_margin else 0
angle = PI2
# 양 옆 충돌범위 먼저 판별
if delta.x < 0: # 왼쪽으로 이동.
angle = PI0
dir = Game._MoveDirection.LEFT
point = Game.___get_intersect_point(
center,
move_to,
Point(bounds.right + BALL_RADIUS, bounds.top - margin),
Point(bounds.right + BALL_RADIUS, bounds.bottom + margin),
)
elif delta.x > 0: # 오른쪽으로 이동
angle = PI0
dir = Game._MoveDirection.RIGHT
point = Game.___get_intersect_point(
center,
move_to,
Point(bounds.left - BALL_RADIUS, bounds.top - margin),
Point(bounds.left - BALL_RADIUS, bounds.bottom + margin),
)
# 양 옆으로 충돌이 없었으면 옆으로
if point is None:
if delta.y < 0: # 위쪽으로 이동.
angle = PI2
dir = Game._MoveDirection.UP
point = Game.___get_intersect_point(
center,
move_to,
Point(bounds.left - margin, bounds.bottom + BALL_RADIUS),
Point(bounds.right + margin, bounds.bottom + BALL_RADIUS),
)
elif delta.y > 0: # 아래쪽으로 이동
angle = PI2
dir = Game._MoveDirection.DOWN
point = Game.___get_intersect_point(
center,
move_to,
Point(bounds.left - margin, bounds.top - BALL_RADIUS),
Point(bounds.right + margin, bounds.top - BALL_RADIUS),
)
# 충돌이 없었으면 반환.
if point is None:
return None
# 충돌이 있었으면 충돌 지점까지의 거리 계산
d = math.sqrt((center.x - point.x) ** 2 + (center.y - point.y) ** 2)
return Game._HitData(point, d, dir, angle)
def ___get_intersect_point(
p1: Point, p2: Point, p3: Point, p4: Point
) -> typing.Optional[Point]:
"""선분A(p1-p2) 와 선분B(p3-p4) 사이에 교점 있는지 확인하는 함수
Args:
p1, p2 : 선분 A의 양 끝점
p3, p4 : 선분 B의 양 끝점
Results:
접점. 접점이 없으면 None 을 반한한다.
.. 참고:
점 A, B를 잇는 선분 위에 있는 한 점 P(k)를 정의한다.
P(k) = A * (1 - k) + B * k
단, k in [0, 1]
선분A 위의 점 Pa(t)
Pa(t) = p_1 * (1 - t) + p_2 * t
= p_1 + (p_2 - p_1) * t ......................................... f1
단, t in [0, 1]
선분B 위의 점 Pb(s)
Pa(t) = p_1 * (1 - s) + p_2 * s
= p_1 + (p_2 - p_1) * s ......................................... f2
단, s in [0, 1]
두 선분의 교차점 P는 Pa(t)와 Pb(s)는 동일
P = p_1 + (p_2 - p_1) * t = p_3 + (p_4 - p_3) * s ..................... f3
점 P의 x, y 를 각각 계산하면
P_x = x_1 + (x_2 - x_1) * t = x_3 + (x_4 - x_3) * s ................... f4
P_y = y_1 + (y_2 - y_1) * t = y_3 + (y_4 - y_3) * s ................... f5
f4, f5를 t, s에 대해서 계산하면
k = (x_2 - x_1) * (y_4 - y_3) - (x_4 - x_3) * (y_2 - y_1) ............. f6
t = ((x_4 - x_3) * (y_1 - y_3) - (x_1 - x_3) * (y_4 - y_3)) / k ....... f7
s = ((x_2 - x_1) * (y_1 - y_3) - (x_1 - x_3) * (y_2 - y_1)) / k ....... f8
단, t in [0, 1]
s in [0, 1]
f6, f7, f8 에 의해
x = x_1 + t * (x_2 - x_1) ............................................. f9
y = y_1 + t * (y_2 - y_1) ............................................. f10
"""
k = (p2.x - p1.x) * (p4.y - p3.y) - (p4.x - p3.x) * (p2.y - p1.y)
if k == 0:
return None
t = ((p4.x - p3.x) * (p1.y - p3.y) - (p1.x - p3.x) * (p4.y - p3.y)) / k
if t < 0 or 1 < t:
return None
s = ((p2.x - p1.x) * (p1.y - p3.y) - (p1.x - p3.x) * (p2.y - p1.y)) / k
if s < 0 or 1 < s:
return None
return Point(p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y),)
####################################################################################################
def _animation(self):
"""아이템의 이동 등을 처리하는 함수
"""
while True:
time.sleep(1.0 / FPS)
if self._game_state != MainStatus.Game:
continue
# --------------------------------------------------------------------------------
now = time.time()
if self._time_gameover_at <= now:
self._game_win()
continue
dt = now - self._time_rendered_at
self._time_rendered_at = now
# --------------------------------------------------------------------------------
# 사용자가 버튼을 누르고 있는지 확인
if self._p1_status == KeyStatus.UP or self._p1_status == KeyStatus.DOWN:
if self._p1_status == KeyStatus.UP:
self._p1_bar_y = max(
self._p1_bar_y - BAR_SPEED_PER_SEC * dt, BAR_Y_RANGE[0]
)
else:
self._p1_bar_y = min(
self._p1_bar_y + BAR_SPEED_PER_SEC * dt, BAR_Y_RANGE[1]
)
self._p1_bar_bounds = to_bounds((BAR_P1_X, self._p1_bar_y), BAR_SIZE)
self._coords(self._id_p1_bar, self._p1_bar_bounds)
if self._p2_status == KeyStatus.UP or self._p2_status == KeyStatus.DOWN:
if self._p2_status == KeyStatus.UP:
self._p2_bar_y = max(
self._p2_bar_y - BAR_SPEED_PER_SEC * dt, BAR_Y_RANGE[0]
)
else:
self._p2_bar_y = min(
self._p2_bar_y + BAR_SPEED_PER_SEC * dt, BAR_Y_RANGE[1]
)
self._p2_bar_bounds = to_bounds((BAR_P2_X, self._p2_bar_y), BAR_SIZE)
self._coords(self._id_p2_bar, self._p2_bar_bounds)
# --------------------------------------------------------------------------------
# 바 부딛힘 색변화 처리
if self._time_p1_glow is not None:
if now < self._time_p1_glow:
self._canvas.itemconfigure(self._id_p1_bar, fill=BAR_P1_COLOR[1])
else:
self._time_p1_glow = None
self._canvas.itemconfigure(self._id_p1_bar, fill=BAR_P1_COLOR[0])
if self._time_p2_glow is not None:
if now < self._time_p2_glow:
self._canvas.itemconfigure(self._id_p2_bar, fill=BAR_P2_COLOR[1])
else:
self._time_p2_glow = None
self._canvas.itemconfigure(self._id_p2_bar, fill=BAR_P2_COLOR[0])
# --------------------------------------------------------------------------------
if self._time_scoring_before is None:
# 공의 이동 연산을 처리하는 부분
if self._time_ball_move_after < now:
self._move_ball(self._ball_speed * dt)
if self._canvas_id_ball_count is not None:
self._canvas.delete(self._canvas_id_ball_count)
self._canvas_id_ball_count = None
if self._time_ball_bounce is not None:
if now < self._time_ball_bounce:
self._canvas.itemconfigure(self._id_ball, fill=BALL_COLOR_BOUNCE)
else:
self._time_ball_bounce = None
self._canvas.itemconfigure(self._id_ball, fill=BALL_COLOR)
# 점수 발생 시 공을 깜빡깜빡하게 만드는 부분
if self._time_scoring_before is not None:
if now < self._time_scoring_before:
dt = int((self._time_scoring_before - now) * 4) % 2
self._canvas.itemconfigure(
self._id_ball, fill=BALL_COLOR_HIGHLIGHT[dt]
)
else:
self._time_scoring_before = None
self._canvas.itemconfigure(self._id_ball, fill=BALL_COLOR)
self._reset_ball()
self._coords(
self._id_ball, to_bounds(self._ball_center, BALL_SIZE),
)
# 공 이동까지 몇 초 남았나 보여주는거
if now < self._time_ball_move_after:
dt = self._time_ball_move_after - now
if self._canvas_id_ball_count is None:
self._canvas_id_ball_count = self._create_text(
RENDER_BALL_MOVE_COUNT, x=math.ceil(dt)
)
else:
self._canvas.itemconfigure(
self._canvas_id_ball_count, text=f"{int(math.ceil(dt))}"
)
# 점수 갱신
self._canvas.itemconfigure(self._id_score_p1, text=str(self._score_p1))
self._canvas.itemconfigure(self._id_score_p2, text=str(self._score_p2))
# --------------------------------------------------------------------------------
"""남은 시간을 멈춰야 하는 경우
1. 점수 땄을 때
2. 공이 이동 대기중일 때
"""
remain_seconds_f = self._time_gameover_at - now
remain_seconds = int(math.ceil(remain_seconds_f))
if (
self._time_scoring_before is None or self._time_scoring_before < now
) and (
self._time_ball_move_after is None or self._time_ball_move_after < now
):
self._canvas.itemconfigure(
self._id_remain_seconds, text=str(remain_seconds)
)
if remain_seconds <= 10:
# 10초 이하 남았으면 깜빡거림
self._canvas.itemconfigure(
self._id_remain_seconds,
fill=RENDER_GAME_SEC_VALUE.color(remain_seconds_f),
)
# 점수 갱신
self._canvas.itemconfigure(self._id_score_p1, text=str(self._score_p1))
self._canvas.itemconfigure(self._id_score_p2, text=str(self._score_p2))
####################################################################################################
# 유틸리티 함수들
def _coords(self, id, bounds: Bounds):
"""canvas.corrds 를 수행하는 함수
Args:
id : canvas 오브젝트 ID
bounds : 객체 바운딩
"""
self._canvas.coords(id, bounds.left, bounds.top, bounds.right, bounds.bottom)
def _create_oval(self, center: Point, radius: float, **kwargs) -> int:
"""create_oval 를 수행하는 함수
Args:
center: 원의 중심점
radius: 원의 반지름
kwargs
Returns:
int: canvas 오브젝트 ID
"""
return self._canvas.create_oval(
center.x - radius,
center.y - radius,
center.x + radius,
center.y + radius,
kwargs,
)
def _create_rectangle(self, bounds: Bounds, **kwargs) -> int:
"""create_rectangle 를 수행하는 함수
Args:
bounds: 객체 바운딩
kwargs
Returns:
int: canvas 오브젝트 ID
"""
return self._canvas.create_rectangle(
bounds.left, bounds.top, bounds.right, bounds.bottom, kwargs
)
def _create_text(
self, text: Text, x: typing.Optional[typing.Any] = None, **kwargs,
) -> int:
"""canvas.create_text 를 수행하고 추가적인 정렬을 하는 함수
Args:
text: 만들 텍스트 데이터
tag: 객체 생성할 떄 쓸 태그
x: args
Results:
int: canvas 오브젝트 ID
"""
args = kwargs if kwargs else {}
if isinstance(text.text, str):
args["text"] = text.text
elif x is not None:
args["text"] = text.text(x)
if text.color:
args["fill"] = text.color if isinstance(text.color, str) else text.color(x)
if text.font:
args["font"] = text.font
if text.anchor:
args["anchor"] = text.anchor
id = self._canvas.create_text(text.point.x, text.point.y, args)
return id
if __name__ == "__main__":
oop = Game()
oop.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment