# SDL2: 控制 ###### tags: `SDL` ## 前言 隨者我們掌握圖形的顯示後,遊戲還需要讀取玩家的操作,做出相對應的回應,才能算是完整的遊戲。因此在這個章節,我將介紹以鍵盤和滑鼠控制遊戲的方式。 一個很重要的觀念是,我們並不是真正”控制”程式,而是”讀取”我們對鍵盤和滑鼠的動作,稱作**事件**,並指定若發生某事件,則呼叫某函數,藉此實現控制的效果。幸運的是,鍵盤和滑鼠動作的偵測之底層邏輯已經被建立好了!使用者只需要呼叫正確的函數讀取事件,並學習判斷事件的種類即可。 ## SDL_Event: 事件載體 SDL利用這個struct來儲存事件,這個struct可以儲存**所有你想的到的事件!** 所以在看documents的時候其實很恐怖,有一堆參數根本看不懂@@,在這裡我只會介紹鍵盤、滑鼠的使用。此外,它跟我們之前學的物件都不太一樣,**在於它只需要一個!** 不論你想偵測幾個事件,都只需要一個。 這特性跟它的底層邏輯有關,所有事件都被儲存在一個佇列(queue)中,可以想像事件們按照發生的先後排隊,然後我們每次只能從隊伍最前面抽取事件出來,再決定要如何處理。**SDL_Event並不是事件佇列本身,它擔任的角色只是那個抽取出來的物件的暫存區。** 所以一次只需要一個就夠了。 宣告的方式很簡單,如下: ```cpp SDL_Event event; ``` 簡單到我不會解釋。 ### SDL_FlushEvents: 清空佇列 在開始偵測之前,我們先學會清空佇列。這是因為各種事件會一直被加進佇列中,在遊戲開始前可能已經累積好幾百個你不希望偵測到的事件。例如: 很多人都會在等載入動畫的時候一直亂按鍵盤,沒清除掉事件的結果,就是你遊戲一進去角色就亂衝。因此在開始偵測想要的事件前,我們先呼叫它來清除之前的事件佇列。 語法如下: ```cpp void SDL_FlushEvents(Uint32 minType, Uint32 maxType); ``` 這裡的minType和maxType需要到documents去查表。SDL所有的**事件種類(EventType)** 都被按照某個順序排好,這個函數可以讓你清掉指定區間內的所有事件種類。聽起來很複雜,其實我只想全部清掉啊,所以我們用以下的程式去清掉**全部**。 ```cpp SDL_FlushEvents(SDL_FIRSTEVENT,SDL_LASTEVENT); ``` 這樣應該就很好理解first和last是什麼意思了吧! 對於有興趣深究清除事件種類的”順序”的人,[請點我](https://wiki.libsdl.org/SDL2/SDL_EventType)。 ### SDL_PollEvent: 取出事件 ```cpp int SDL_PollEvent(SDL_Event * event) ``` 這是這個函數的定義。它接收的參數只有一個SDL_Event的指標,這個函數會取出佇列中第一個元素,然後放進指標所指的物件,**並同步從佇列中移除**。 它的回傳值則是0或1,**1代表佇列中還有事件尚未被取出、處理。** 我們可以利用回傳值的特性,寫出一個常見的遊戲架構: ```cpp while (game_is_still_running) { SDL_Event event; while (SDL_PollEvent(&event)) { // poll 直到佇列清空! // 根據event種類決定response. } // 更新狀態、渲染畫面 } ``` 特別要注意在第二層while迴圈中,event 已經是有值的了,要使用時絕對不可以再呼叫一次SDL_PollEvent,否則會取到錯誤的事件! 如果需要同時接收多個event,則可以宣告更多SDL_Event物件去儲存,若一直使用同一個event做接收,則會有覆蓋的問題。**被覆蓋掉的event是無法回復的。** ### 事件種類 事件種類是SDL_Event裡面的一個元素(Uint32)。我們可以透過存取這個值來決定事件的種類。在SDL裡面有提供一個enum叫做SDL_EventType,常用的事件如下: | 名稱 | 事件內容 | | --- | --- | | SDL_QUIT | 退出。例如按下視窗上的”X”關閉按鈕,或是以指令結束程序。 | | SDL_KEYDOWN | 按下鍵盤。 | | SDL_KEYUP | 鬆開鍵盤。 | | SDL_MOUSEMOTION | 滑鼠移動。 | | SDL_MOUSEBUTTONDOWN | 按下滑鼠按鍵。 | | SDL_MOUSEBUTTONUP | 鬆開滑鼠按鍵。 | | SDL_MOUSEWHEEL | 滾動滑鼠滾輪。 | 實務上,我們常用一個switch的結構去處理事件種類。例如: ```cpp while (game_is_still_running) { SDL_Event event; while (SDL_PollEvent(&event)) { // poll 直到佇列清空! switch (event.type){ case SDL_QUIT: game_is_still_running = false; break; case SDL_KEYDOWN: //do something... default: continue; } } } ``` 事件種類必須在處理事件之前就決定好,否則會引發錯誤。例如: 程式要處裡Keyboard Event,卻被你餵一個Mouse Event給它,它就會出現取到NULL、甚至亂碼的情形。這種情況下很高機率會閃退。 --- 學會基礎的事件處理之後,我們接著深入探討兩種常用的控制機制: 鍵盤以及滑鼠。依照我個人的經驗,**鍵盤比較簡單。** 純鍵盤實踐的彈性也不會輸滑鼠太多,所以可以考慮用WASD或是上下左右鍵,去達成所有的使用者介面(UI)操作。 ## SDL_KeyboardEvent: 鍵盤事件 SDL_KeyboardEvent又是一個新的物件,它同時也是SDL_Event的一個member object。也就是說,想要存取一個鍵盤事件,你需要用以下的語法: ```cpp SDL_KeyboardEvent ke = event.key; //event的一個member, key, 是一個型別為SDL_KeyboardEvent的物件,我們宣告一個變數ke去儲存。 //其實可以不要額外多這步會比較好寫,這只是讓你了解你在幹嘛。 ``` 針對這個部分的敘述,其實有很大的錯誤,有興趣的人可以看這裡。沒興趣的人就當成”上面講的是對的!”這樣理解就行。 ### 取得按鍵種類 SDL有兩種取值的方式,分別叫”scancode”和”keycode”。兩種都用來決定到底是哪一個鍵被按下,比如說,希望按下鍵盤上的W鍵時,進行向前進的函數,則語法如下: ```cpp if(event.key.keysym.scancode == SDL_SCANCODE_Q){ MoveForward(); } //or, alternatively if(event.key.keysym.sym == SDLK_q){ MoveForward(); } ``` 關於按鍵對應的名稱,請查此表: [https://wiki.libsdl.org/SDL2/SDL_Keycode](https://wiki.libsdl.org/SDL2/SDL_Keycode) ### 取得按鍵修飾 如果我們希望偵測Ctrl+C,用以上的寫法會是: ```cpp if(event.key.keysym.sym == SDLK_LCTRL || event.key.keysym.sym == SDLK_RCTRL){ while(SDL_PollEvent(&event)){ if(event.key.keysym.sym == SDLK_c){ Copy(); } } } ``` 看起來程式碼就很複雜。此外,如果在按下Control後,都沒有按下C鍵,何時退出這個子While迴圈?(不要忘記主要程式結構就是兩個While了) 為了解決問題,SDL引入一種新的邏輯: **偵測C鍵的同時,檢查其他鍵有沒有被按下。**亦即將Ctrl當作一種鍵盤的修飾(modification)。如此一來就可以簡化程式碼。 ```cpp if(event.key.keysym.sym == SDLK_c && (event.key.keysym.mod == KMOD_CTRL)){ Copy(); } //event.key.keysym.mod: 按鍵模式 ``` 常用的KEYMOD如下: | Flags | 內容 | | --- | --- | | KMOD_NONE | 無特別Mod | | KMOD_SHIFT | 按下Shift鍵,可用KMOD_LSHIFT / KMOD_RSHIFT指定左右鍵。 | | KMOD_CTRL | 按下Ctrl鍵,可用KMOD_LCTRL/ KMOD_RCTRL指定左右鍵。 | | KMOD_ALT | 按下Alt鍵,可用KMOD_LALT/ KMOD_RALT指定左右鍵。 | | KMOD_GUI | 按下GUI鍵(Windows鍵/Command鍵),可用KMOD_LGUI/ KMOD_RGUI指定左右鍵。 | ### 取得按鍵狀態 ```cpp if(event.key.state == KEY_PRESSED){ //按下按鍵 } else if(event.key.state == KEY_RELEASED){ //放開按鍵 } ``` 鍵盤事件中只有這兩種狀態。 ### 時間戳 ```cpp Uint32 t = event.key.timestamp; ``` 回傳的時間是”自SDL library啟動之後的毫秒數”,在後續解釋時間的章節會再詳細解釋。 ## SDL_MouseButtonEvent: 滑鼠點按事件 存取滑鼠點按事件: ```cpp SDL_MouseButtonEvent me = event.button; //event的一個member, button, 是一個型別為SDL_MouseButtonEvent的物件,我們宣告一個變數me去儲存。 //其實可以不要額外多這步會比較好寫,這只是讓你了解你在幹嘛 ``` ### 取得點按位置 ```cpp Sint32 x = event.button.x; Sint32 y = event.button.y; ``` 單位同程式的其他部分,使用像素(px)作為單位,左上角為(0, 0),向左為+x,向下為+y。考慮到你可能點在視窗外,(x, y)可以帶有負值。 ### 取得按鍵種類 ```cpp if(event.button.button == SDL_BUTTON_LEFT){ //按下左鍵 } else if(event.button.button == SDL_BUTTON_RIGHT){ //按下右鍵 } ``` 使用這個參數可以辨別滑鼠被按下的是左鍵或右鍵。亦可以設定成偵測中鍵(MIDDLE)或輔助鍵(X1、X2)。 ### 連點偵測 ```cpp Uint8 ContinualClicks = event.button.clicks; ``` 這個參數會回傳滑鼠是否屬於連點狀態。若是單擊,則回傳1,雙擊,回傳2,依此類推。 --- 本章節介紹SDL2中的事件處理,包括事件種類、鍵盤事件、滑鼠點按事件等,並提供相關的程式碼範例。整個SDL2教學的基本盤就到此結束,你已經可以利用圖片和文字溝通,並透過監測使用者事件控制遊戲的進展了! 有了以上的技能,完成一個基本的,稱得上遊戲的程式絕對沒有問題。接下來,你可以深入學習對遊戲的控制,添加動畫、配樂、計時同步等等功能來增加遊戲性! 附錄大概講解了到此為止,我們把C裡面的各個東西稱為”物件”,所謂物件是如何透過純C實現的? --- ## 附錄: SDL_Event到底是個啥? 說到底,SDL的”物件”其實都不是C++意義上的物件。它們都是用C所有的功能組合而成的,只是行為模式相仿而已。 **SDL_Event其實是一個Union。**Union是一個很類似struct的東西,它們都用類似的語法。例如: ```cpp union num { int i; float f; double d; Uint32 u; }; //union的定義,跟struct類似。 num x; //宣告(instance creation) x.f = 3.2; //賦值 float y = x.f //取值 ``` 然而,它們有根本上的不同。你可以試著編譯下面的範例: ```cpp #include <iostream> using namespace std; union num{ int i; float f; double d; }; int main(){ num x; x.f = 3.2; cout << x.i << "\n" << x.f << "\n" << x.d; } ``` 你會先看到,x.i和x.d都沒有被賦值就呼叫了,這樣還可以輸出答案? 而且你每次都會得到一個相同的輸出: ```cpp 1078774989 3.2 5.32986e-315 ```
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up