治療面對pygame的mouse.get_pressed()和MOUSEBUTTONDOWN時的選擇障礙

2023/09/20閱讀時間約 9 分鐘

「小孩子才做選擇!」是句大家耳熟能詳的戲謔話,絕大多數的人在說這句話時,真正的實情是:全部都想要,但是沒能力全拿,有得選就偷笑了!

就是因為全部都想要,但是沒能力全拿,所以產生了選擇障礙這種症狀。其實,有個很簡單的辦法可以治療選擇障礙這個症狀:就丟銅板咩!反正自己下不了決心,那就讓老天爺幫忙決定不就好了?

雖然說一個銅板就能去除選擇障礙帶來的麻煩,但是寫程式時可不能這麼做。寫程式遇到有不同的東西都可以用來處理同一件事時,得先搞清楚,這些不同的東西之間到底有什麼差異?是不是在使用上有不同的限制?如果沒搞清楚就亂用一通,或許一時之間程式跑起來沒什麼不對勁,但那說不定是走了狗屎運,沒採到雷,等到處理的問題改變了,或者操作方式不一樣了,說不定就會出現完全想像不到的結果。最近在使用pygame偵測滑鼠動作的功能時,就充分體現了這種狀況。

原本是要寫一個模擬砲彈被大砲射出之後是如何運動的程式,因為覺得一執行程式就看到砲彈在空中飛,實在是有點太單調,所以就想著加入發砲的功能,也就是當按一下滑鼠左鍵時,就發射一顆砲彈。

這功能寫起來其實挺簡單的,只要能偵測到滑鼠左鍵有沒有被按下就可以了。查了一下,在pygame中,可以用mouse.get_pressed()或MOUSEBUTTONDOWN來偵測滑鼠是不是被按下,不過MOUSEBUTTONDOWN還牽涉到什麼event不event的,感覺用起來挺麻煩的。既然兩個都可以用來偵測滑鼠是不是被按下,那當然選用比較不麻煩的mouse.get_pressed()囉!

mouse.get_pressed()的用法很簡單,它會傳回一個tuple,裡頭放著三個boolean值,分別代表滑鼠左鍵、通常是滾輪的中間鍵、右鍵的狀態。如果按鍵是按下的,boolean值就是True,不然就是False。所以要偵測滑鼠左鍵是不是被按下,只要檢查mouse.get_press()[0]是不是True就可以了。

程式寫好之後,就開始測試看看效果如何。一開始還挺順的,一面點著滑鼠,一面看著砲彈滿天飛,還真是有點療癒。可是……怎麼覺得哪邊怪怪的。

咦?!為什麼同樣是按一下滑鼠,有時候會發射一顆砲彈,有時候卻會發射三顆砲彈?這可真是奇怪!明明程式是這樣寫的

while True:
    :
    if pygame.mouse.get_pressed()[0]:
        # 發射砲彈

看起來一點問題都沒有啊!那怎會這樣?如果這樣寫不行,那要怎樣寫才能按一下發射一顆砲彈?

爬文加上寫些小程式驗證自己的想法,總算搞清楚是怎麼一回事了。

寫小程式驗證想法,這個做法很重要,畢竟不管是網路上討論的文章或官網的文件,都不見得能面面俱到所有的小細節。有時候人家認為理所當然可以一句話帶過的觀念,卻是自己百思不得其解的大疑問。這時候,就只能靠自己寫程式,實際去驗證想法對不對了。

那為什麼會出現按一下發射三顆砲彈的情形呢?原來,這一切都因為mouse.get_pressed()傳回的是滑鼠按鈕的「狀態」,而不是「動作」。所以,只要呼叫mouse.get_pressed()時,滑鼠某個按鈕處於按下的狀態,對應於那個按鈕的傳回值,就會是True。因為從滑鼠按鈕按下到放開之間的時間雖然很短,但如果在這當中呼叫了不只一次mouse.get_pressed(),那就會被誤判為按了不只一次的滑鼠,導致出現明明只按了一次滑鼠,但卻發射了三顆砲彈的情況。

既然mouse.get_pressed()會有實際只按一次,但卻誤判為按了好幾次的情形,那豈不是沒什麼用處?畢竟那簡直就像是滑鼠用太久,已經快掛掉的症狀。其實,在玩射擊遊戲時,mouse.get_pressed()這樣的函數倒是挺好用的。滑鼠按著不放,子彈就砰砰砰砰砰連續發射,這時候巴不得發射越多越好,先前以為的缺點,這時候可不再是缺點,而是大大的優點啊!

mouse.get_pressed()在使用上,還有一個地方要注意的,那就是有可能滑鼠明明就按下又放開了,但卻沒偵測到。這也不難理解,這種情形會發生在滑鼠按鍵按下去再放開的過程中,mouse.get_pressed()根本就沒有被執行到。例如,當設定的fps不高或程式跑很慢時,有可能在滑鼠按鍵已經放開了之後,才執行到mouse.get_pressed(),這時候傳回來的滑鼠狀態,當然就是按鍵放開的狀態囉!

總之,mouse.get_pressed()就是個往事不需回首、過去了的就過去了,只活在當下,還有可能會恍神漏接的傢伙。

看來想要按一下滑鼠就只發射一顆砲彈,是不能用mouse.get_pressed()來處理了。那怎麼辦呢?沒有選擇之下,只好摸摸鼻子,用那個牽涉到什麼event不event的MOUSEBUTTONDOWN了。

什麼是event呢?翻譯成中文就是「事件」,就是某件發生的事情,例如滑鼠按鍵被按下或放開、滑鼠被移動、鍵盤被按下或放開等。在pygame中,定義了許多事件,如MOUSEBUTTONDOWN、MOUSEBUTTONUP、MOUSEMOTION、KEYDOWN、KEYUP、QUIT等,而使用者也可以自己定義需要的事件。

pygame利用事件佇列(event queue)來管理事件訊息的傳遞。每當有事件發生,例如滑鼠被按下時,就把這事件放到事件佇列裡頭去排隊等著處理,先排先處理。事件佇列有一定的容量,當放滿時,後續想要來排隊的事件,就會被默默地丟棄,彷彿從來不曾存在一般。

既然有事件發生時,就會被送入事件佇列中去等著執行,所以MOUSEBUTTONDOWN就不會像mouse.get_pressed()一樣,有漏接的情況出現。從這裡就可以看出來,MOUSEBUTTONDOWN的個性和mouse.get_pressed()是完全相反的。mouse.get_pressed()只活在當下,完全不管過去發生過什麼事;MOUSEBUTTONDOWN則會記得所有還沒處理完的事,只要事件佇列還沒被塞滿的話。

總之,跟mouse.get_pressed()的做法不同,MOUSEBUTTONDOWN偵測的是「動作」,也就是狀態的變化,而不是「狀態」。本來嘛,「事件發生」裡頭的「發生」這個詞,就是指「從沒有變有」,或「從有變沒有」,這種狀態的改變,不是嗎?

就因為MOUSEBUTTONDOWN偵測的是「動作」,而同一個滑鼠按鍵被按下這個動作,是不可能連續發生的,中間一定會有滑鼠按鍵被放開這個動作。所以,想要按一下滑鼠就只會發射一顆砲彈,MOUSEBUTTONDOWN就會是這種情況下的必然選擇。

治好了面對mouse.get_pressed()和MOUSEBUTTONDOWN時的選擇障礙後,接下來就是要搞清楚,使用MOUSEBUTTONDOWN時,要怎麼分辨被按下的,是滑鼠的哪個按鍵。

由pygame所定義的事件,都會帶有根據事件類型所設定的屬性,透過這些屬性,就可以知道事件更進一步的詳細資料。以MOUSEBUTTONDOWN來說,透過button這個屬性,就可以知道被按下的是哪個按鍵。當button裡頭放的值是1時,代表滑鼠左鍵被按下;是2時,代表按下的是中間鍵;是3時,則按下的是右鍵。

到這裡,總算搞清楚MOUSEBUTTONDOWN是怎麼運作的了。不過,要怎麼知道在事件佇列裡頭,有MOUSEBUTTONDOWN這個事件在排隊等著被處理?這個倒也不是太難辦到,只要利用event.get()把佇列裡頭的所有東西都拿出來,然後一個一個檢查,就可以知道了。

整合一下所有的東西,要想做到按一下滑鼠左鍵發射一顆砲彈的功能,程式可以這樣寫:

for event in pygame.event.get():
    if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                # 發射一顆砲彈

最後,關於event.get(),有個細節應該要知道:event.get()是把在事件佇列裡頭的東西拿出來,不是複製出來,而且拿出來之後,不會再放回去。所以呼叫完event.get()之後,事件佇列是被清空的。先前提到過,事件佇列滿了之後,新的事件會被默默地丟棄。現在既然event.get()會把事件佇列清空,那就不需要去擔心這個問題了,因為通常event.get()會不斷地被呼叫,這樣才能知道是不是有新發生的事件需要處理,這也是為什麼使用pygame的程式,通常會看到下面的程式片段的原因:

while True:
    for event in pygame.event.get():
         if event.type == pygame.QUIT:
             :

發射砲彈的完整程式,可以在
The Nature of Code閱讀心得筆記——使用Python實作》的第三章3.2節練習3.2找到。

13會員
86內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!