Created
December 15, 2021 10:06
-
-
Save RyuaNerin/09ffe28412081ddd9fd4c848e8c19205 to your computer and use it in GitHub Desktop.
tkinter 를 이용한 간단한 게임.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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