# 第四章:碰撞 ## 函式介紹 1. ### `pygame.sprite.collide_rect()` * **簡介:** 判斷兩個<font color="#ff2020"> **矩形** </font>的 sprite 是否碰撞。 * **參數:** * `left`:第一個 sprite * `right`:第二個 sprite * **回傳:** <font color="#2020ff"> ***Boolean*** </font>。 :::spoiler 範例 ```python= import pygame FPS = 60 WIDTH, HEIGHT = 800, 700 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("打磚塊遊戲") clock = pygame.time.Clock() # 定義 Rectangle 類別 class Rectangle(pygame.sprite.Sprite): def __init__(self, center, size, color, speed): super().__init__() self.image = pygame.Surface((size, size)) self.image.fill(color) self.rect = self.image.get_rect() self.rect.center = center self.speed = speed def update(self): self.rect.x += self.speed def stop(self): self.speed = 0 all_sprites = pygame.sprite.Group() # 物件一 rectangle1 = Rectangle((300, 300), 100, RED, 1) all_sprites.add(rectangle1) # 物件二 rectangle2 = Rectangle((500, 380), 100, BLUE, -1) all_sprites.add(rectangle2) running = True while running: clock.tick(FPS) # 輸入處理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新遊戲 all_sprites.update() # =============== 當「矩形」碰撞時停止物件 =============== if (pygame.sprite.collide_rect(rectangle1, rectangle2)): rectangle1.stop() rectangle2.stop() # =============== 當「矩形」碰撞時停止物件 =============== # 畫面顯示 screen.fill(WHITE) all_sprites.draw(screen) pygame.display.update() pygame.quit() ``` ::: 1. ### `pygame.sprite.collide_circle()` * **簡介:** 判斷兩個<font color="#ff2020"> **圓形** </font>的 sprite 是否碰撞。 * **參數:** * `left`:第一個 sprite * `right`:第二個 sprite * **回傳:** <font color="#2020ff"> ***Boolean*** </font>。 * **註:** sprite 物件中須包含 `self.radius` 成員變數 :::spoiler 範例 ```python= import pygame FPS = 60 WIDTH, HEIGHT = 800, 700 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("打磚塊遊戲") clock = pygame.time.Clock() # 定義 Circle 類別 class Circle(pygame.sprite.Sprite): def __init__(self, center, size, color, speed): super().__init__() self.image = pygame.Surface((size, size), pygame.SRCALPHA) self.radius = size // 2 pygame.draw.circle(self.image, color, (self.radius, self.radius), self.radius) self.rect = self.image.get_rect() self.rect.center = center self.speed = speed def update(self): self.rect.x += self.speed def stop(self): self.speed = 0 all_sprites = pygame.sprite.Group() # 物件一 circle1 = Circle((300, 300), 100, RED, 1) all_sprites.add(circle1) # 物件二 circle2 = Circle((500, 380), 100, BLUE, -1) all_sprites.add(circle2) running = True while running: clock.tick(FPS) # 輸入處理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新遊戲 all_sprites.update() # =============== 當「圓形」碰撞時停止物件 =============== if (pygame.sprite.collide_circle(circle1, circle2)): circle1.stop() circle2.stop() # =============== 當「圓形」碰撞時停止物件 =============== # 畫面顯示 screen.fill(WHITE) all_sprites.draw(screen) pygame.display.update() pygame.quit() ``` ::: 1. ### `circle_rect_collision()` (自訂函式) * #### 步驟: 1. 找出矩形最近於圓心的點 <div style="width:330px; height:330px;"> <iframe src="https://www.youtube.com/embed/H6_9Kx_m9JU" width="100%" height="95%" frameborder="0" allowfullscreen ></iframe> </div> 3. 計算圓心到最近點的距離 4. 判斷是否碰撞 * #### 程式碼: ```python= def rect_circle_collision(rectangle, circle): cx, cy = circle.rect.center r = circle.radius rx, ry = rectangle.rect.topleft w, h = rectangle.rect.size closest_x = max(rx, min(cx, rx + w)) closest_y = max(ry, min(cy, ry + h)) distance = math.sqrt((cx - closest_x) ** 2 + (cy - closest_y) ** 2) return distance < r ``` :::spoiler 範例 ```python= import pygame import math FPS = 60 WIDTH, HEIGHT = 800, 700 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("打磚塊遊戲") clock = pygame.time.Clock() def rect_circle_collision(rectangle, circle): cx, cy = circle.rect.center r = circle.radius rx, ry = rectangle.rect.topleft w, h = rectangle.rect.size closest_x = max(rx, min(cx, rx + w)) closest_y = max(ry, min(cy, ry + h)) distance = math.hypot(cx - closest_x, cy - closest_y) return distance < r # 定義 Rectangle 類別 class Rectangle(pygame.sprite.Sprite): def __init__(self, center, size, color, speed): super().__init__() self.image = pygame.Surface((size, size)) self.image.fill(color) self.rect = self.image.get_rect() self.rect.center = center self.speed = speed def update(self): self.rect.x += self.speed def stop(self): self.speed = 0 # 定義 Circle 類別 class Circle(pygame.sprite.Sprite): def __init__(self, center, size, color, speed): super().__init__() self.image = pygame.Surface((size, size), pygame.SRCALPHA) self.radius = size // 2 pygame.draw.circle(self.image, color, (self.radius, self.radius), self.radius) self.rect = self.image.get_rect() self.rect.center = center self.speed = speed def update(self): self.rect.x += self.speed def stop(self): self.speed = 0 all_sprites = pygame.sprite.Group() # 物件一 circle = Circle((300, 300), 100, RED, 1) all_sprites.add(circle) # 物件二 rectangle = Rectangle((500, 380), 100, BLUE, -1) all_sprites.add(rectangle) running = True while running: clock.tick(FPS) # 輸入處理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新遊戲 all_sprites.update() # ============== 當「圓形與矩形」碰撞時停止物件 ============== if rect_circle_collision(rectangle, circle): rectangle.stop() circle.stop() # ============== 當「圓形與矩形」碰撞時停止物件 ============== # 畫面顯示 screen.fill(WHITE) all_sprites.draw(screen) pygame.display.update() pygame.quit() ``` ::: 1. ### `pygame.sprite.spritecollide()` * **簡介:** 判斷某個 sprite 跟一個 group 中哪些 sprites 有碰撞。 * **參數:** * `sprite`:一個 sprite * `group`:一個 group * `dokill`:group 中碰撞到的 sprite 是否要 `kill()` * `collided`(可不加):判斷兩個 sprite 之間是否碰撞的方式,若不加則預設為矩形碰撞。 * **回傳:** 一個<font color="#2020ff"> ***list*** </font>,裡面記錄 group 中所有被碰撞的 sprite。 1. ### `pygame.sprite.groupcollide()` * **簡介:** 判斷兩個 groups 中所有 sprites 的碰撞關係。 * **參數:** * `group1`:第一個 group * `group2`:第二個 group * `dokill1`:第一個 group 中被碰撞的 sprites 是否要 `kill()` * `dokill2`:第二個 group 中被碰撞的 sprites 是否要 `kill()` * `collided`(可不加):判斷兩個 sprite 之間是否碰撞的方式,若不加則預設為矩形碰撞。 * **回傳:** 一個<font color="#2020ff"> ***dictionary*** </font>,*key* 為第一個 group 被碰撞的 sprite,*value* 是一個 *list* ,表示第二個 group 中被此 sprite 碰撞到的所有 sprites。 ## 球碰板子 * ### 碰撞檢測 * #### 介紹: * 取得所有碰到板子的球(利用 `rect_circle_collision` 函式做碰撞判斷) * 呼叫此球的 `hit_board` 函式 * 傳入板子目前的位置資訊 * #### 程式碼: ```python= hits = pygame.sprite.spritecollide(board, balls, False, rect_circle_collision) for hit_ball in hits: hit_ball.hit_board(board.rect) ``` * ### `hit_board()` 成員函式 * #### 介紹: * 計算反射角: 1. 將反射角大約限制在 `-max_angle` ~ `max_angle` 1. 計算球對板子中心的偏移量 `offset`,範圍在 -1 ~ 1 1. 利用 `max_angle` 和 `offset` 計算反射角 * 利用三角函數計算 dx 和 dy (詳細方式見第三章的球類別) * #### 程式碼: ```python= class Ball(pygame.sprite.Sprite): ... def hit_board(self, board_rect): # 計算角度 max_angle = math.pi / 3 # 60 度 offset = (self.rect.centerx - board_rect.centerx) / (board_rect.width / 2) angle = max_angle * offset # 計算 dx, dy self.dx = Ball.speed * math.sin(angle) self.dy = -Ball.speed * math.cos(angle) ``` ## 球碰磚塊 * ### 碰撞檢測 * #### 介紹: * 取得所有被碰撞的磚塊(利用 `rect_circle_collision` 函式做碰撞判斷) * 讓碰撞它的「第一顆球」呼叫 `hit_brick` 函式 * 傳入磚塊的位置資訊 * #### 程式碼: ```python= hits = pygame.sprite.groupcollide(bricks, balls, True, False) for brick, hit_balls in hits.items(): hit_balls[0].hit_brick(brick.rect) ``` * ### `hit_brick()` 成員函式 * #### 介紹: 1. 加快球速 1. 反彈方向判斷: * 球在磚塊右側 且 球向左移動 $\Rightarrow$ 橫向反彈 * 球在磚塊左側 且 球向右移動 $\Rightarrow$ 橫向反彈 * 其他 $\Rightarrow$ 縱向反彈 1. 按照固定的機率隨機產生新球 * #### 程式碼: ```python= import random class Ball(pygame.sprite.Sprite): ... def hit_brick(self, brick_rect): # 加速 self.dx *= self.speed_factor self.dy *= self.speed_factor Ball.speed *= self.speed_factor # 反彈方向判斷 if self.rect.centerx - brick_rect.right > 0 and self.dx < 0: self.dx = -self.dx elif brick_rect.left - self.rect.centerx > 0 and self.dx > 0: self.dx = -self.dx else: self.dy = -self.dy # 隨機產生新球 if random.random() < 0.1: self.create_new_ball() ``` :::info 須先導入 `random` 模組 ::: * #### 語法說明: `random.random()`:隨機生成一個 0 到 1 的浮點數 * ### `Ball` 的 `create_new_ball()` 函式 * #### 介紹: 1. 隨機生成角度(-60 ~ 60 度) 2. 宣告一顆新球 3. 加入 group 中 * #### 程式碼: ```python= def create_new_ball(self): angle = random.uniform(-math.pi / 3, math.pi / 3) new_ball = Ball(angle, self.rect.center) all_sprites.add(new_ball) balls.add(new_ball) ``` * #### 語法說明: `random.uniform(min, max)`:隨機生成一個 `min` 到 `max` 的浮點數 --- :::spoiler 完整程式碼 ```python= import pygame import math import random FPS = 60 # 大小設定 WIDTH, HEIGHT = 800, 700 BRICK_ROWS = 5 BRICK_COLS = 8 BRICK_MARGIN = 70 BRICK_SPACING = 8 BRICK_WIDTH = (WIDTH - 2 * BRICK_MARGIN) // BRICK_COLS - BRICK_SPACING BRICK_HEIGHT = 40 BOARD_SIZE = (150, 30) BALL_RADIUS = 15 # 顏色設定 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("打磚塊遊戲") clock = pygame.time.Clock() # 矩形和圓形碰撞判斷 def rect_circle_collision(rectangle, circle): cx, cy = circle.rect.center r = circle.radius rx, ry = rectangle.rect.topleft w, h = rectangle.rect.size closest_x = max(rx, min(cx, rx + w)) closest_y = max(ry, min(cy, ry + h)) distance = math.sqrt((cx - closest_x) ** 2 + (cy - closest_y) ** 2) return distance < r # 定義磚塊類別 class Brick(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((BRICK_WIDTH, BRICK_HEIGHT)) self.image.fill(RED) self.rect = self.image.get_rect() self.rect.topleft = x, y # 定義板子類別 class Board(pygame.sprite.Sprite): def __init__(self, center): super().__init__() self.image = pygame.Surface(BOARD_SIZE) self.image.fill(BLUE) self.rect = self.image.get_rect() self.rect.center = center self.speed = 15 def update(self): keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: self.rect.x -= self.speed if self.rect.left < 0: self.rect.left = 0 if keys[pygame.K_RIGHT]: self.rect.x += self.speed if self.rect.right > WIDTH: self.rect.right = WIDTH class Ball(pygame.sprite.Sprite): speed = 7 def __init__(self, angle, center): super().__init__() self.radius = BALL_RADIUS self.image = pygame.Surface((2 * BALL_RADIUS, 2 * BALL_RADIUS), pygame.SRCALPHA) pygame.draw.circle(self.image, GREEN, (BALL_RADIUS, BALL_RADIUS), BALL_RADIUS) self.rect = self.image.get_rect() self.rect.center = center self.dx = Ball.speed * math.sin(angle) self.dy = -Ball.speed * math.cos(angle) self.speed_factor = 1.03 def update(self): self.rect.x += self.dx self.rect.y += self.dy if self.rect.left <= 0: self.rect.left = 0 self.dx = -self.dx elif self.rect.right >= WIDTH: self.rect.right = WIDTH self.dx = -self.dx if self.rect.top <= 0: self.rect.top = 0 self.dy = -self.dy elif self.rect.top >= HEIGHT: self.kill() def hit_board(self, board_rect): # 計算角度 max_angle = math.pi / 3 # 60 度 offset = (self.rect.centerx - board_rect.centerx) / (board_rect.width / 2) angle = offset * max_angle # 計算 dx, dy self.dx = Ball.speed * math.sin(angle) self.dy = -Ball.speed * math.cos(angle) def hit_brick(self, brick_rect): # 加速 self.dx *= self.speed_factor self.dy *= self.speed_factor Ball.speed *= self.speed_factor # 反彈方向判斷 if self.rect.centerx - brick_rect.right > 0 and self.dx < 0: self.dx = -self.dx elif brick_rect.left - self.rect.centerx > 0 and self.dx > 0: self.dx = -self.dx else: self.dy = -self.dy # 隨機產生新球 if random.random() < 0.1: self.create_new_ball() def create_new_ball(self): angle = random.uniform(-math.pi / 3, math.pi / 3) new_ball = Ball(angle, self.rect.center) all_sprites.add(new_ball) balls.add(new_ball) all_sprites = pygame.sprite.Group() # 磚塊 bricks = pygame.sprite.Group() for r in range(BRICK_ROWS): for c in range(BRICK_COLS): x = c * (BRICK_WIDTH + BRICK_SPACING) + BRICK_SPACING // 2 + BRICK_MARGIN y = r * (BRICK_HEIGHT + BRICK_SPACING) + BRICK_MARGIN brick = Brick(x, y) all_sprites.add(brick) bricks.add(brick) # 板子 board = Board((WIDTH // 2, HEIGHT - 60)) all_sprites.add(board) # 球 balls = pygame.sprite.Group() ball = Ball(math.pi / 4, (WIDTH // 2, HEIGHT - 150)) all_sprites.add(ball) balls.add(ball) running = True while running: clock.tick(FPS) # 輸入處理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新遊戲 all_sprites.update() # 球碰板子 hits = pygame.sprite.spritecollide(board, balls, False, rect_circle_collision) for hit_ball in hits: hit_ball.hit_board(board.rect) # 球碰磚塊 hits = pygame.sprite.groupcollide(bricks, balls, True, False, rect_circle_collision) for brick, hit_balls in hits.items(): hit_balls[0].hit_brick(brick.rect) # 畫面顯示 screen.fill(WHITE) all_sprites.draw(screen) pygame.display.update() pygame.quit() ``` :::