# 第四章:碰撞
## 函式介紹
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()
```
:::