SDL2: 控制

tags: SDL

前言

隨者我們掌握圖形的顯示後,遊戲還需要讀取玩家的操作,做出相對應的回應,才能算是完整的遊戲。因此在這個章節,我將介紹以鍵盤和滑鼠控制遊戲的方式。

一個很重要的觀念是,我們並不是真正”控制”程式,而是”讀取”我們對鍵盤和滑鼠的動作,稱作事件,並指定若發生某事件,則呼叫某函數,藉此實現控制的效果。幸運的是,鍵盤和滑鼠動作的偵測之底層邏輯已經被建立好了!使用者只需要呼叫正確的函數讀取事件,並學習判斷事件的種類即可。

SDL_Event: 事件載體

SDL利用這個struct來儲存事件,這個struct可以儲存所有你想的到的事件! 所以在看documents的時候其實很恐怖,有一堆參數根本看不懂@@,在這裡我只會介紹鍵盤、滑鼠的使用。此外,它跟我們之前學的物件都不太一樣,在於它只需要一個! 不論你想偵測幾個事件,都只需要一個。

這特性跟它的底層邏輯有關,所有事件都被儲存在一個佇列(queue)中,可以想像事件們按照發生的先後排隊,然後我們每次只能從隊伍最前面抽取事件出來,再決定要如何處理。SDL_Event並不是事件佇列本身,它擔任的角色只是那個抽取出來的物件的暫存區。 所以一次只需要一個就夠了。

宣告的方式很簡單,如下:

SDL_Event event;

簡單到我不會解釋。

SDL_FlushEvents: 清空佇列

在開始偵測之前,我們先學會清空佇列。這是因為各種事件會一直被加進佇列中,在遊戲開始前可能已經累積好幾百個你不希望偵測到的事件。例如: 很多人都會在等載入動畫的時候一直亂按鍵盤,沒清除掉事件的結果,就是你遊戲一進去角色就亂衝。因此在開始偵測想要的事件前,我們先呼叫它來清除之前的事件佇列。

語法如下:

void SDL_FlushEvents(Uint32 minType, Uint32 maxType);

這裡的minType和maxType需要到documents去查表。SDL所有的事件種類(EventType) 都被按照某個順序排好,這個函數可以讓你清掉指定區間內的所有事件種類。聽起來很複雜,其實我只想全部清掉啊,所以我們用以下的程式去清掉全部

SDL_FlushEvents(SDL_FIRSTEVENT,SDL_LASTEVENT);

這樣應該就很好理解first和last是什麼意思了吧!

對於有興趣深究清除事件種類的”順序”的人,請點我

SDL_PollEvent: 取出事件

int SDL_PollEvent(SDL_Event * event)

這是這個函數的定義。它接收的參數只有一個SDL_Event的指標,這個函數會取出佇列中第一個元素,然後放進指標所指的物件,並同步從佇列中移除

它的回傳值則是0或1,1代表佇列中還有事件尚未被取出、處理。 我們可以利用回傳值的特性,寫出一個常見的遊戲架構:

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的結構去處理事件種類。例如:

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。也就是說,想要存取一個鍵盤事件,你需要用以下的語法:

SDL_KeyboardEvent ke = event.key;
//event的一個member, key, 是一個型別為SDL_KeyboardEvent的物件,我們宣告一個變數ke去儲存。
//其實可以不要額外多這步會比較好寫,這只是讓你了解你在幹嘛。

針對這個部分的敘述,其實有很大的錯誤,有興趣的人可以看這裡。沒興趣的人就當成”上面講的是對的!”這樣理解就行。

取得按鍵種類

SDL有兩種取值的方式,分別叫”scancode”和”keycode”。兩種都用來決定到底是哪一個鍵被按下,比如說,希望按下鍵盤上的W鍵時,進行向前進的函數,則語法如下:

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

取得按鍵修飾

如果我們希望偵測Ctrl+C,用以上的寫法會是:

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)。如此一來就可以簡化程式碼。

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指定左右鍵。

取得按鍵狀態

if(event.key.state == KEY_PRESSED){
	//按下按鍵
}
else if(event.key.state == KEY_RELEASED){
	//放開按鍵
}

鍵盤事件中只有這兩種狀態。

時間戳

Uint32 t = event.key.timestamp;

回傳的時間是”自SDL library啟動之後的毫秒數”,在後續解釋時間的章節會再詳細解釋。

SDL_MouseButtonEvent: 滑鼠點按事件

存取滑鼠點按事件:

SDL_MouseButtonEvent me = event.button;
//event的一個member, button, 是一個型別為SDL_MouseButtonEvent的物件,我們宣告一個變數me去儲存。
//其實可以不要額外多這步會比較好寫,這只是讓你了解你在幹嘛

取得點按位置

Sint32 x = event.button.x;
Sint32 y = event.button.y;

單位同程式的其他部分,使用像素(px)作為單位,左上角為(0, 0),向左為+x,向下為+y。考慮到你可能點在視窗外,(x, y)可以帶有負值。

取得按鍵種類

if(event.button.button == SDL_BUTTON_LEFT){
		//按下左鍵
}
else if(event.button.button == SDL_BUTTON_RIGHT){
		//按下右鍵
}

使用這個參數可以辨別滑鼠被按下的是左鍵或右鍵。亦可以設定成偵測中鍵(MIDDLE)或輔助鍵(X1、X2)。

連點偵測

Uint8 ContinualClicks = event.button.clicks;

這個參數會回傳滑鼠是否屬於連點狀態。若是單擊,則回傳1,雙擊,回傳2,依此類推。


本章節介紹SDL2中的事件處理,包括事件種類、鍵盤事件、滑鼠點按事件等,並提供相關的程式碼範例。整個SDL2教學的基本盤就到此結束,你已經可以利用圖片和文字溝通,並透過監測使用者事件控制遊戲的進展了! 有了以上的技能,完成一個基本的,稱得上遊戲的程式絕對沒有問題。接下來,你可以深入學習對遊戲的控制,添加動畫、配樂、計時同步等等功能來增加遊戲性!

附錄大概講解了到此為止,我們把C裡面的各個東西稱為”物件”,所謂物件是如何透過純C實現的?


附錄: SDL_Event到底是個啥?

說到底,SDL的”物件”其實都不是C++意義上的物件。它們都是用C所有的功能組合而成的,只是行為模式相仿而已。

**SDL_Event其實是一個Union。**Union是一個很類似struct的東西,它們都用類似的語法。例如:

union num {
	int i;
	float f;
	double d;
	Uint32 u;
};
//union的定義,跟struct類似。

num x;
//宣告(instance creation)

x.f = 3.2;
//賦值

float y = x.f
//取值

然而,它們有根本上的不同。你可以試著編譯下面的範例:

#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都沒有被賦值就呼叫了,這樣還可以輸出答案? 而且你每次都會得到一個相同的輸出:

1078774989
3.2
5.32986e-315