# Python Pygame 事件處理(2)
在Pygame中,事件是`pygame`與使用者互動的關鍵機制,是遊戲循環中的重要部分,事件處理主要透過`pygame.event`來管理,例如鍵盤輸入、滑鼠點擊、視窗關閉等
## 事件佇(隊)列管理
Pygame使用**事件佇列**(**Event Queue**)來儲存用戶輸入與系統事件
事件佇列(Event Queue)是一種**先進先出**(FIFO,First In First Out)的資料結構,用來儲存程式運行時發生的事件,並依序處理這些事件
事件佇列的工作方式:
1. 事件產生:當某些操作發生時(例如鍵盤輸入、滑鼠點擊、網絡請求完成),系統會將這些事件放入事件佇列中
2. 事件儲存:事件被存放在事件佇列中,按照先進先出的順序等待處理
3. 事件處理:事件處理迴圈(Event Loop)負責檢查事件佇列,並將事件交給適當的事件處理器(Event Handler)
4. 重複步驟:程式不斷循環執行這個過程,確保所有事件都能依序處理
以下為相關的指令介紹
- `pygame.event.get()`:取得事件佇列中的所有事件,回傳`list`
- `pygame.event.poll()`:取出**當前仍存在於事件佇列中最早發生的事件**,若無事件則回傳`pygame.NOEVENT`
- `pygame.event.wait()`:阻塞程式直到事件佇列中出現任何事件,然後返回該事件並繼續執行程式
- `pygame.event.peek([eventtype])`:檢查是否有指定類型的事件,不會移除事件,回傳布林值
- `pygame.event.clear([eventtype])`:清除事件佇列中的所有事件或指定類型的事件
- `eventtype`:選填,若設定則函數功能變成清除指定類型事件
- `pygame.event.post(event)`:手動發送事件
- `event`:事件
- `pygame.event.set_blocked(type`:禁用指定類型的事件,不讓它進入佇列
- `pygame.event.set_allowed(type)`:允許指定類型的事件進入佇列
- `pygame.event.get_blocked(type)`:檢查某類事件是否被封鎖,回傳 `True/False
- `pygame.event.Event(type, attributes)`:創建一個自訂事件
- `type`:類型
- `attributes`:屬性
## 事件類型與事件屬性
前面講完了事件的管理方式,接下來我們要來介紹「**事件**」,但在那之前我們要先來說明一下「**類型(type)**」與「**屬性(attributes)**」的區別
在 Pygame 的事件系統中,**事件類型**(**type**)用來識別「**事件的種類**」,而**屬性**(**attributes**) 則是「**該事件攜帶的額外資訊**」
- 如何區分?
|項目|事件類型(type)|事件屬性(attributes)|
|---|--------------|--------------------|
|概念|標識事件的「種類」,類似於分類標籤|事件的具體內容,提供詳細資訊|
|是否必要|是(每個事件必定有`type`)|否(某些事件沒有額外屬性)
- 總結
1. **事件類型(type)**:用來判斷事件的種類
2. **事件屬性(attributes)**:用來存放事件的額外資訊
3. **判斷方法**:
- 若要知道發生了什麼事件,檢查`event.type`
- 若要獲取更詳細的事件資訊,查看`event`的屬性
### 視窗事件
當使用者進行視窗方面的操作時就會觸發「視窗事件」
視窗事件有以下三個類型
- `pygame.ACTIVEEVENT`:視窗獲得或失去焦點
當視窗**獲得或失去焦點**時,這個事件會觸發,這通常發生在使用者切換視窗時,例如:
- 切換到其他程式 → 觸發`pygame.ACTIVEEVENT`(視窗失去焦點)
- 切換回 Pygame 視窗 → 觸發`pygame.ACTIVEEVENT`(視窗獲得焦點)
事件屬性:
- `event.gain`:如果視窗獲得焦點,則`gain=1`,如果失去焦點,則 `gain=0`
- `event.state`:焦點變化編號
- `1` → 視窗的輸入焦點(鍵盤、滑鼠)變化
- `2` → 滑鼠焦點變化
- `4` → 視窗是否最小化變化
範例:
```python
for event in pygame.event.get():
if event.type == pygame.ACTIVEEVENT:
if event.gain == 0: # 失去焦點
print("視窗失去焦點")
else: # 獲得焦點
print("視窗獲得焦點")
```
- `pygame.VIDEORESIZE`:視窗大小改變
當使用者**調整視窗大小**時,這個事件會觸發
事件屬性:
- `event.w`:視窗新的寬度
- `event.h`:視窗新的高度
範例:
```python
for event in pygame.event.get():
if event.type == pygame.VIDEORESIZE:
print(f"視窗大小改變為 {event.w}x{event.h}")
```
>[!Important]注意
>需要將視窗設定為可調整大小,否則不會觸發此事件
>```python
>pygame.display.set_mode((500, 500), pygame.RESIZABLE)
>```
- `pygame.VIDEOEXPOSE`:視窗需要重新繪製
當視窗需要**重新繪製**時,這個事件會觸發,例如:
- 最小化後還原,視窗的內容可能需要重新繪製
- 被其他視窗遮擋後重新顯示,畫面可能會變黑或殘影,需要重新渲染
範例:
```python
for event in pygame.event.get():
if event.type == pygame.VIDEOEXPOSE:
print("視窗需要重新繪製!")
```
### 鍵盤事件
當使用者透過鍵盤按鍵來控制遊戲運作時,這類由鍵盤觸發的事件就稱為「**鍵盤事件**」
鍵盤事件分為兩種**類型**,一種是按下按鍵(`pygame.KEYDOWN`),另一種是放開按鍵(`pygame.KEYUP`)
再確定類型後,我們需要比對屬性來確認是哪個按鍵,常見的按鍵屬性如下
|按鍵|鍵盤常數|按鍵|鍵盤常數|按鍵|鍵盤常數|
|---|-------|---|-------|---|------|
|0~9|`K_0`~`K_9`|向上鍵|`K_UP`|Enter|`K_RETURN`|
|a~z|`K_a`~`K_z`|向下鍵|`K_DOWN`|Tab|`K_TAB`|
|F1~F12|`K_F1`~`K_F12`|向左鍵|`K_LEFT`|Esc|`K_ESCAPE`|
|Space|`K_SPACE`|向右鍵|`K_RIGHT`|Back Space|`K_BACKSPACE`|
|左Shift|`K_LSHIFT`|左Ctrl|`K_LCTRL`|左Alt|`K_LALT`|
當我們要**偵測單個按鍵**按下可以這樣做
```python
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
print(f"{event.key}被按下")
if event.key == pygame.K_w:
print("w")
```
而如果要**偵測多個按鍵**同時按下時,有兩種作法,第一種作法是在剛才偵測單個按鍵的情況下,利用邏輯運算子(`and`、`or`)來做操作
```python
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_w and event.key == pygame.K_SPACE:
print("向前跳")
```
另一種做法是利用`pygame.key.get_pressed()`,這個方法會回傳一個布林值的陣列,對應到所有按鍵是否處於按住狀態
```python
keys = pygame.key.get_pressed()
if keys[pygame.K_w] and keys[pygame.K_SPACE]:
print("向前跳")
```
但以上兩種方法都有一個共同問題,他們都無法**偵測長按**(按鍵重複`Key Repeat`)的情況,這時就得使用`pygame.key.set_repeat(start_time, repeat_time)`
- `pygame.key.set_repeat(500, 50)`:這表示
- **500毫秒**後開始重複觸發
- 之後每**50毫秒**觸發一次
```python
pygame.key.set_repeat(500, 50)
while running:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
print(f"{event.key}被按住")
```
>[!NOTE]補充
>如果覺得Pygame的鍵盤偵測不好用的話可以試試看**Keyboard套件**
>
### 滑鼠事件
當使用者透過滑鼠來控制遊戲運作時,這類由滑鼠觸發的事件就稱為「**滑鼠事件**」
主要有以下四個類型
- `pygame.MOUSEBUTTONDOWN`:當使用者**按下**滑鼠按鍵時觸發
事件屬性:
- `event.pos`:滑鼠點擊的座標`(x, y)`
- `event.button`:按下的按鍵編號
- `1` → 左鍵
- `2` → 中鍵
- `3` → 右鍵
- `4` → 滾輪上
- `5` → 滾輪下
>[!Important]注意
>滾輪部分只判斷有上下,不表示**滾動速度**
- `pygame.MOUSEBUTTONUP`:當使用者**放開**滑鼠按鍵時觸發
事件屬性:
- `event.pos`:滑鼠點擊的座標`(x, y)`
- `event.button`:按下的按鍵編號
- `1` → 左鍵
- `2` → 中鍵
- `3` → 右鍵
>[!Important]注意
>滾動滾輪時,只會觸發`pygame.MOUSEBUTTONDOWN`,不會觸發`pygame.MOUSEBUTTONUP`,因為滾輪的動作不會有按住和釋放的狀態,所以只將其合併到一個事件中
>[!Note]為什麼滾輪術性會用`pygame.MOUSEBUTTONDOWN`事件?
>因為在舊版Pygame是沒有`pygame.MOUSEWHEEL`的,所以將滾輪屬性合併到`pygame.MOUSEBUTTONDOWN`
- `pygame.MOUSEMOTION`:當滑鼠在視窗內**移動**時觸發
事件屬性:
- `event.pos`:滑鼠點擊的座標`(x, y)`
- `event.rel`:滑鼠移動的相對位置`(dx, dy)`,也就是向量
- `event.buttons`:目前哪些滑鼠按鍵仍被按住,回傳元組,例如`(1, 0, 0)`代表左鍵仍按住
- `pygame.MOUSEWHEEL`:當使用者**滾動滑鼠滾輪**時觸發
事件屬性:
- `event.y`:滾動方向
- `event.y > 0` → 向上,數值越大滾動速度越快
- `event.y < 0` → 向下,數值越小滾動速度越快
- `event.x`:左右滾動,部分滑鼠支援
- `event.x > 0` → 向右,數值越大滾動速度越快
- `event.x < 0` → 向左,數值越小滾動速度越快
### 特殊事件
有些事件同時具有或同時不具有多種事件種類的特性時,就被我歸類於特殊事件(不等於實際分類)
- `pygame.QUIT`:當使用者按下視窗右上的 **X** 時觸發
### 自訂義事件
自訂義事件是由程式設計者自己設計的事件,在 Pygame 中,自訂事件的類型值必須大於 pygame.USEREVENT,因為系統內建的事件都有固定的數值範圍
以下為流程介紹
1. 創建自訂義事件
```python
CUSTOM_EVENT = pygame.USEREVENT + 1
```
`pygame.USEREVENT`是Pygame提供的基礎數值,你可以加上數字(+1, +2, +3...)來區分不同的自訂義事件
2. 推送自訂義事件
使用`pygame.event.post()`將事件推送到 Pygame 的事件佇列,使其可以在事件迴圈中被捕捉到
```python
event = pygame.event.Event(CUSTOM_EVENT, message="Hello, World!")
pygame.event.post(event)
```
- `pygame.event.Event(type, **attributes)`:用來建立一個自訂事件物件,可以附加屬性
- `pygame.event.post(event)`:將事件放入事件佇列,等待處理
>[!Note]補充
>除了以上幾個常見的事件種類以外,還有以下幾種事件
>- 搖桿(手把)事件
>- 觸控螢幕事件
## 事件處理迴圈
在剛才我們提到Pygame使用事件佇列來儲存發生的事件,接下來,我們要介紹一下處理事件的部分,也就是**事件處理迴圈**(**Event Loop**)
事件處理迴圈是開發者寫的一段程式碼,負責從事件佇列中 取出事件並處理
事件處理迴圈的工作方式:
1. 從事件佇列讀取事件
2. 將事件分配給適當的事件處理器(Event Handler)
3. 回到第一步繼續執行
以下為Pygame的事件處理迴圈範例
```python
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
# 遊戲主迴圈
running = True
while running:
# 事件處理迴圈,從事件佇列中取出所有事件並處理
for event in pygame.event.get():
# 處理視窗關閉事件
if event.type == pygame.QUIT:
running = False
pygame.quit()
```
以上就是有關Pygame事件處理的相關整理
## 補充:控制迴圈執行速度
在 Pygame 裡,遊戲的運行是靠迴圈不斷執行來更新畫面和處理事件。但如果這個迴圈沒有速度限制,遊戲會跑得非常快,因為電腦會盡可能快地執行它,這可能導致畫面不流暢或 CPU 過熱
為了解決這個問題,Pygame 提供了`pygame.time.Clock()`,讓你可以控制遊戲的運行速度
>[!Note]畫面卡頓不是因為系統效能降低嗎?
>是的,畫面卡頓**通常**是因為系統效能不足,導致畫面無法順暢更新,例如 CPU 或 GPU 負擔過重。但如果沒有使用`pygame.time.Clock()`來控制 FPS,即使系統效能沒有問題,遊戲也可能會產生不穩定的畫面更新,出現類似「卡頓」的現象。這種卡頓的原因可能有兩種:
>1. FPS 過高導致不穩定
>當遊戲迴圈沒有限制 FPS 時,電腦會盡可能快地執行迴圈。這可能會導致:
> - 遊戲畫面更新速度過快,不同幀之間的時間差異變化大,產生不流暢的畫面跳動
> - CPU 長時間維持高運算負載,導致系統變慢,進而影響遊戲流暢度
>
> 這種情況不是真的「效能不足」,而是因為畫面更新速度不穩定,導致觀感上像是卡頓
>2. 幀與幀之間的時間差不一致
>即使 FPS 夠高,但如果遊戲更新的時間間隔不一致(有時一幀間隔 5ms,有時 30ms),畫面更新就會變得忽快忽慢,讓玩家感覺到不順暢。這可能發生在:
> - 遊戲沒有固定 FPS,導致每次畫面更新的時間間隔不同
> - FPS 過高,導致 CPU 被過度使用,造成其他背景程式影響遊戲的執行
>
> 使用 pygame.time.Clock() 來限制 FPS,可以讓幀與幀之間的時間間隔變得固定,確保畫面更新的平穩度,即使 FPS 變動,也不會有明顯的卡頓感
- `pygame.time.Clock()`:Pygame 中用來控制遊戲迴圈執行速度的物件
範例:
- 限制FPS
```python
clock = pygame.time.Clock()
clock.tick(60)
```
- 計算與前一幀的時間間隔
```python
dt = clock.tick(60) / 1000 # 轉換成秒
print(dt)
```
- 獲取FPS間隔
```python
fps = clock.get_fps()
print(fps)
```
## [下一篇](https://hackmd.io/@Huanyu763/PythonPygame動畫處理)
## [回到主頁](https://hackmd.io/@Huanyu763/home)