在上一篇文章中我們已經利用 SDL 的日誌介面實現了簡單的字串輸出,實際上是解決了開發環境搭建問題,接下來我們將在已有程式碼的基礎上繼續開發,實現第一個視窗的建立和背景色繪製。
初始化
首先設定日誌輸出級別:
SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE);
因為還是開發階段所以我們將輸出日誌級別設定為最低的 VERBOSE,這樣所有的日誌都會輸出,有助於我們觀察 SDL 的執行情況,出現錯誤時可以得到儘量詳細的出錯資訊,有助於我們快速定位問題。
接下來初始化 SDL 庫,引數 SDL_INIT_VIDEO
指定初始化的子系統為影片系統:
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return -1;
}
實際上這一步可以省略,因為在呼叫 SDL API 時其內部會自行檢查和初始化所需使用的子系統。比如接下來要使用的 SDL_CreateWindow
函式,內部有這樣的程式碼:
if (!_this) {
/* Initialize the video system if needed */
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
return NULL;
}
...
}
其中 _this
是影片子系統初始化完成後設定的全域性變數,宣告如下:
static SDL_VideoDevice *_this = NULL;
雖然不是必須的,但是我們仍然建議呼叫 SDL_Init
對主要用到的子系統進行顯式初始化,目的有兩個:
- 清晰完整的展示出執行過程,有助於理解程式碼;
- 如果執行失敗,可以在第一時間定位出錯位置,提高排障效率。
建立視窗和渲染器
建立一個 800x600 大小的視窗,然後移動到螢幕居中的位置:
SDL_Window* window = SDL_CreateWindow("Hello, SDL3!", 800, 600, SDL_WINDOW_RESIZABLE);
if (!window) {
SDL_Log("Could not create a window: %s", SDL_GetError());
return -1;
}
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
建立和視窗關聯的渲染器:
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer) {
SDL_Log("Create renderer failed: %s", SDL_GetError());
return -1;
}
所有圖形影像都是透過渲染器繪製到視窗,以背景色繪製為例,程式碼如下:
SDL_SetRenderDrawColor(renderer, 16, 0, 16, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
這裡使用的背景色是 RGB(16, 0, 16),像我這樣曾經用過 DirectDraw 渲染影片的老傢伙們對這個顏色值應該很熟悉😔,不過這裡用這個顏色只是為了和 SDL 視窗預設的黑色區分以便更好的觀察渲染結果,沒有其他特殊效果。
事件迴圈
SDL 有兩種方式獲取事件佇列中的事件:
SDL_PollEvent
類似 Win32 API 中的 PeekMessage,無論佇列中有無事件都會立即返回,區別只是返回值不同SDL_WaitEvent
類似 Win32 API 中的 WaitMessage,如果佇列中沒有事件會阻塞等待,直到收到第一個事件才返回
我們使用第二種方式實現事件迴圈:
SDL_Event event{};
bool keep_going = true;
while (keep_going) {
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_EVENT_QUIT: {
keep_going = false;
break;
}
case SDL_EVENT_KEY_DOWN: {
keep_going = keep_going && (event.key.keysym.sym != SDLK_ESCAPE);
break;
}
case SDL_EVENT_WINDOW_EXPOSED: {
SDL_SetRenderDrawColor(renderer, 16, 0, 16, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
break;
}
}
}
SDL_EVENT_QUIT
表示點選了關閉視窗按鈕所以收到該事件後跳出迴圈;
SDL_EVENT_KEY_DOWN
鍵盤按下事件,這裡我們實現了按 'Esc' 鍵退出的能力;
SDL_EVENT_WINDOW_EXPOSED
表示需要對視窗進行重繪,所以我們把繪製背景色的程式碼放在這個事件中執行。
可以在 while 迴圈中增加一行日誌來觀察收到了哪些事件:
SDL_Log("Event: %d", event.type);
注意上面這個事件迴圈的寫法和大多數 SDL 的示例不同,實際上在這裡我們是把 SDL 當作一個正經的視窗系統在使用,而不是當作一個遊戲引擎。兩者一個重要的區別是使用遊戲引擎時一般是按照固定幀率持續進行視窗重繪,而一般的 GUI 軟體使用視窗系統時只進行必要的重繪,以最大程度節省 CPU 和 GPU 的使用。影片雖然也有幀率的概念,但是採用的是第二種方式,只在影片幀重新整理時執行重繪。
完整程式碼
新增退出前清理資源的程式碼後,完整的程式碼如下:
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
int main(int argc, char* argv[])
{
SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE);
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return -1;
}
SDL_Window* window =
SDL_CreateWindow("Hello, SDL3!", 800, 600, SDL_WINDOW_RESIZABLE);
if (!window) {
SDL_Log("Could not create a window: %s", SDL_GetError());
return -1;
}
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer) {
SDL_Log("Create renderer failed: %s", SDL_GetError());
return -1;
}
SDL_Event event{};
bool keep_going = true;
while (keep_going) {
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_EVENT_QUIT: {
keep_going = false;
break;
}
case SDL_EVENT_KEY_DOWN: {
keep_going = keep_going && (event.key.keysym.sym != SDLK_ESCAPE);
break;
}
case SDL_EVENT_WINDOW_EXPOSED: {
SDL_SetRenderDrawColor(renderer, 16, 0, 16, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
break;
}
}
SDL_Log("Event: %d", event.type);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
輸出效果如圖: