# 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)