探索 SDL
SDL 是一個跨平臺應用程式程式設計介面 (API),它允許您在多個平臺上編寫圖形程式碼。它的許多工作都在幕後完成,為您(程式設計師)提供了一個更容易的介面來訪問這些內部機制。
跨平臺程式設計是透過動態檢查使用者正在執行的作業系統來實現的。這是透過使用條件宏來完成的
#ifdef _WIN32
/*Windows specific code here*/
#endif
#ifdef _APPLE_
/*Macintosh OS code here*/
#endif
#ifdef _linux_
/*Linux code here*/
#endif
這些宏檢查作業系統編譯器庫中儲存的預定義變數的存在。根據定義的變數,將執行對應於該特定系統的程式碼。此方法還防止作業系統特定程式碼相互衝突。
這樣做的原因是作業系統具有不同的顯示圖形方式。儘管程式碼在每個作業系統中都不同,但大多數操作都執行類似的任務,例如建立視窗、渲染到視窗、獲取使用者輸入等。SDL 將這些任務整合到統一的介面上,讓您基本上可以在多個平臺上編寫、編譯和執行程式。
影片表面是包含畫素資料的影片記憶體塊。每個畫素都由表面儲存的記憶體塊內的記憶體位置表示。此記憶體塊通常是一個簡單的陣列,有時稱為線性記憶體。表面的主要屬性是其以畫素為單位的寬度和高度,以及其畫素格式,即為每個畫素分配的資料量。此畫素大小決定了可以顯示到表面的顏色數量。
| 畫素格式 | 大小(顏色數量) |
|---|---|
| 索引(調色盤) | 1 位元組 – 256 種顏色 |
| 高色 | 2 位元組 – 65536 種顏色 |
| 真彩色 | 3-4 位元組 – 16777216 種顏色 (額外位元組用於 alpha 值,或未用) |
主影片表面指的是使用者螢幕上的影像。與 SDL 影片表面類似,此表面的主要屬性是其尺寸(寬度和高度)及其畫素格式。

#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Rect sDim;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 8 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 8, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
/*wait 5 seconds (5000ms) before exiting*/
SDL_Delay(5000);
SDL_Quit();
exit(0);
}
截至 2014 年 10 月,SDL2 的當前穩定版本為 2.0.3。設定此庫類似於 SDL 1.2 設定,但有一個小錯誤,該錯誤應該在下個版本 (2.0.4) 中解決。然而,在釋出之前,有一個變通方法。錯誤位於 SDL2 標頭檔案包含資料夾中,特別是 SDL_platform.h 檔案。此錯誤(及其解決方案)在 這裡 有更好的解釋。以下是替換檔案的連結:SDL_platform.h。
SDL2 還導致上述函式發生變化。不是使用 SDL_SetVideoMode 建立表面,而是可以建立視窗,並使用該視窗獲取表面
SDL_Window* window = SDL_CreateWindow(
"window title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (window == NULL) {
SDL_Quit();
exit(-1)
}
SDL_Surface* display = SDL_GetWindowSurface(window);
同樣,在 SDL2 中,不是呼叫 `SDL_UpdateRect(display, 0, 0, 0, 0);`,而是可以呼叫
SDL_UpdateWindowSurface(window);將 SDL 標頭檔案從開發包複製到編譯器的包含目錄中。為了更好地組織,在包含目錄中建立一個名為“SDL”的新資料夾,並將所有標頭檔案複製到該資料夾中。
要從 gcc 編譯器連結 SDL 庫,請使用以下語法
gcc sdl_prog.c -o sdl_out -lSDL
在 Windows 中,您還需要連結 SDLmain 庫
gcc sdl_prog.c -o sdl_out -lSDL -lSDLmain
在 SDL 程式的頂部,包含標準的 C 標頭檔案以允許輸入/輸出以及記憶體分配
#include <stdio.h>
#include <stdlib.h>
假設您將 SDL 標頭檔案複製到包含目錄中的 SDL 資料夾中,請在 C 標頭檔案之後新增此標頭檔案包含
#include “SDL/SDL.h”
建立主入口點函式幷包含其命令列引數。SDL 在內部需要這些引數進行初始化。
int main(int argc, char **argv)
{
SDL 的初始化使用 SDL_Init 函式完成,該函式接受一個引數,即要初始化的系統。
int SDL_Init(Uint32 flags);
如果函式成功,則返回值為 0。l 如果由於任何原因初始化失敗,則返回值為 -1。可用的系統包括:影片、音訊、CD-ROM、計時器和操縱桿。您可以使用這些標誌值指定要初始化的系統,這些標誌值可以透過 OR(|)運算進行組合以允許初始化多個系統
SDL_INIT_VIDEO
SDL_INIT_AUDIO
SDL_INIT_CDROM
SDL_INIT_TIMER
SDL_INIT_JOYSTICK
現在,我們只需要初始化影片系統,因此我們只包含該標誌作為 SDL_Init 函式的引數。此外,我們檢查返回值是否為 -1,以確保它已成功初始化。如果初始化失敗,我們立即使用 C 函式 exit 退出程式
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error initializing*/
exit(0)
}
請注意,無論提供哪些標誌,呼叫 SDL_Init 函式都會自動初始化事件處理、檔案輸入/輸出和執行緒系統,我們將在後面討論。
現在我們開始初始化主顯示錶面。主顯示器表示為 SDL Surface。讓我們快速瀏覽一下 SDL_Surface 結構
SDL Surface Structure
typedef struct SDL_Surface {
Uint32 flags; /* Read-only */
SDL_PixelFormat *format; /* Read-only */
int w, h; /* Read-only */
Uint16 pitch; /* Read-only */
void *pixels; /* Read-write */
/* clipping information */
SDL_Rect clip_rect; /* Read-only */
/* Reference count -- used when freeing surface */
int refcount; /* Read-mostly */
/* This structure also contains private fields not shown here */
} SDL_Surface;
- flags – 設定表面支援的某些功能。這將在我們設定顯示錶面時討論
- format – 畫素資料的描述
- w,h – 表面的尺寸(寬度和高度)
- pitch – 表面每行的長度(掃描線)。以位元組為單位。請注意,pitch 並不總是與表面寬度匹配。
- pixels – 用於渲染的實際表面資料。
- clip_rect – 用於將表面的繪製限制為設定尺寸的剪下矩形。
要使用給定的位深度和尺寸檢索主影片顯示,可以使用 SDL_SetVideoMode 函式
SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags);
寬度和高度指的是您希望設定的螢幕的畫素尺寸。每畫素位數可以設定為標準的 8、16、24、32,也可以設定為 0,這將與使用者當前的顯示位深度匹配。flags 引數指的是您希望表面支援的額外功能。在這裡,我將介紹一些主要的功能
| SDL_HWSURFACE | SDL_HWSURFACE 允許將表面資料(即螢幕、點陣圖、字型)直接儲存到影片記憶體中。SDL_SWSURFACE 將表面資料儲存在主系統記憶體中,這往往速度較慢。 |
| SDL_DOUBLEBUF | 這允許雙緩衝支援以實現更流暢的動畫。請注意,此標誌僅在與 SDL_HWSURFACE 標誌進行 OR 運算時才有效。也可以在不指定此標誌的情況下實現此效果,這將在後面介紹。 |
| SDL_FULLSCREEN | 允許 SDL 控制視窗系統並以給定的解析度在整個螢幕上顯示影像。退出時,SDL 會將控制權返回給作業系統及其預設解析度。 |
| SDL_HWPALETTE | 提供對調色盤的訪問許可權。僅在 8 位索引模式下有效。 |
在這個例子中,我們唯一設定的標誌是 SDL_HWSURFACE 標誌,它允許將 SDL_Surface 資料儲存到影片記憶體而不是主系統記憶體,這可以提供更好的效能。如果使用者的機器無法支援影片記憶體儲存,SDL 將回退到系統記憶體儲存。
此函式在成功後將返回指向所請求的 SDL_Surface 的指標。如果由於任何原因失敗,它將返回 NULL。請務必始終檢查返回值以確保影片模式已設定。
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
函式 SDL_Quit 關閉了您使用 SDL_Init 函式初始化的所有 SDL 子系統。由於我們只初始化了影片,SDL_Quit 將只關閉該系統。
接下來的程式碼片段設定了用於確定繪製位置的 SDL Rect 結構。
typedef struct{
Sint16 x, y;
Uint16 w, h;
} SDL_Rect;
* x, y – Position of upper left corner of rectangle in relation to the display surface coordinate system. * w, h – Width and height of rectangle
顯示錶面被設定為一個網格,原點 (x=0, y=0) 是顯示屏的左上角。此網格的每個條目都是該位置畫素的顏色值。在這個例子中,我們定義的矩形將它的左上角 (原點) 定位在右側 320 畫素、下方 240 畫素處,寬度為 200 畫素,高度為 200 畫素。
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
設定矩形尺寸後,我們可以使用 SDL_FillRect 函式用純色填充它。
int SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color);
* dst – SDL_Surface pointer to draw to. Specifying the main display surface will cause drawing to the screen. * dstrect – Rectangle structure pointer containing upper-left position and dimensions of rectangle to fill with color * color – Color to fill rectangle structure with
顏色使用 SDL 函式 SDL_MapRGB 指定。
Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);
* fmt – Pixel structure defining pixel color, depending on the depth. Specifying the display surface's structure member format, matches the color to that display's pixel format. * r,g,b – Individual color components to specify final color.
此函式的返回值是與指定表面畫素格式匹配的顏色值。此值可用作 SDL 繪圖函式中的顏色引數。請注意,如果顯示深度低於 24 位,此函式將返回與指定顏色最接近的匹配顏色,因為在如此小的位深度 (8 位或 16 位) 上畫素格式的空間有限。
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
在向主顯示錶面繪製內容後,我們需要指示 SDL 重新整理表面的內容,以確保我們獲得表面的當前狀態。函式 SDL_UpdateRect 更新給定表面的一部分矩形區域。如果我們想要更新整個表面,我們將位置和尺寸設定為 0,這將指示 SDL 更新整個表面。
void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Sint32 w, Sint32 h);
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
在最終在螢幕上繪製內容後,我們需要防止 SDL 在我們執行它時立即關閉,方法是引入基於時間的延遲。現在,我們將指示 SDL 在繪製到顯示錶面並更新顯示錶面後,等待 5 秒鐘再退出,方法是使用 SDL 時間函式 SDL_Delay,該函式接受一個引數,即毫秒數 (在 5 的情況下為 5000 毫秒),等待該時間後,程式將繼續執行下一行程式碼。
void SDL_Delay(Uint32 ms);
/*wait 5 seconds (5000ms) before exiting*/
SDL_Delay(5000);
延遲應用程式後,我們使用 SDL_Quit 函式關閉所有 SDL 系統 (對於此示例,只是影片子系統),並使用成功程式碼 (0) 退出程式。
void SDL_Quit(void);
SDL_Quit();
exit(0);
迴圈和基本輸入
[edit | edit source]在前面的示例中,程式執行的持續時間由我們想要延遲的毫秒數決定。這種方法對於讓我們檢視單個幀的圖形是有效的,但如果我們想要檢視多幀,例如動畫的繪製,則不是有效的。理想的幀迴圈將允許我們以設定的時間間隔重複繪製和更新螢幕。讓我們看一下通用 C 迴圈實現。請注意,其中沒有計時機制,這會導致迴圈以由螢幕繪製和更新速度決定的間隔重複。
int loop = 1;
while(loop){
if(quit_event()){
loop = 0;
}
draw_and_update_screen();
}
這個迴圈會導致螢幕更新儘可能快地進行。當程式接收到使用者想要退出的事件時,迴圈最終退出,並且迴圈變數設定為 0,以便在迴圈重新啟動時評估為 false。請注意,即使在收到退出事件後,在迴圈退出之前也會進行一次螢幕更新。
在實現此迴圈之前,我們必須瞭解 SDL 如何處理事件,例如鍵盤或滑鼠輸入。事件使用 SDL_Event 聯合體進行處理。
typedef union{
Uint8 type;
SDL_ActiveEvent active;
SDL_KeyboardEvent key;
SDL_MouseMotionEvent motion;
SDL_MouseButtonEvent button;
SDL_JoyAxisEvent jaxis;
SDL_JoyBallEvent jball;
SDL_JoyHatEvent jhat;
SDL_JoyButtonEvent jbutton;
SDL_ResizeEvent resize;
SDL_QuitEvent quit;
SDL_UserEvent user;
SDL_SywWMEvent syswm;
} SDL_Event;
要確定發生了什麼型別的事件 (例如,按鍵、滑鼠點選),我們檢查型別成員的值。處理的其他一些事件包括操縱桿輸入和顯示大小調整 (在程式執行時更改解析度和/或位深度)。現在,我們只介紹鍵盤和滑鼠事件型別。
| SDL_KEYDOWN | 當用戶按下某個鍵時 |
| SDL_KEYUP | 使用者釋放鍵 |
| SDL_MOUSEMOTION | 滑鼠移動 |
| SDL_MOUSEBUTTONDOWN | 滑鼠按鈕點選 |
| SDL_MOUSEBUTTONUP | 滑鼠按鈕釋放 |
如果型別成員的值為 SDL_KEYDOWN 或 SDL_KEYUP,則將設定 SDL_Event 聯合體的鍵成員,它是一個 KeyboardEvent 結構指標。
如果我們只檢查按鍵事件,甚至不知道按下了哪個鍵,我們就可以實現迴圈的第一個版本。如果您還記得前面的示例,迴圈的基本任務是
將布林變數 (int) 初始化為 true (1) 如果變數為 true,則進入迴圈 在迴圈中 如果出現退出事件 (鍵盤按下),則將變數設定為 false (0) 繪製和更新螢幕
重寫前面的 SDL 簡介程式,以實現此迴圈,您需要宣告一個 SDL_Event 例項以及一個用於儲存迴圈值的整數變數。
SDL_Event event;
int loop = 1;
在設定 SDL_Rect 結構的程式碼之後,我們使用一個 while 條件語句來檢查是否需要執行迴圈。
while(loop){
現在,我們檢查鍵盤事件。如果按下任何鍵,程式將把迴圈變數設定為 0,程式將繼續執行 while 迴圈後面的程式碼。
現在,我們填寫建立的 SDL_Event 聯合體例項。SDL_PollEvent() 函式將 SDL_Event 指標 (變數引用) 作為引數,如果發生事件 (例如,鍵盤、滑鼠),則填充事件聯合體的相應成員。如果正在處理事件,此函式返回 1,否則返回 0,表示沒有事件正在處理。
| int SDL_PollEvent(SDL_Event *event); event – SDL_Event 指標,用於填充相應的事件資料 返回值:如果正在處理事件,則為 1;如果未處理事件,則為 0 |
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
loop = 0;
}
}
檢查鍵盤事件後,我們可以使用前面示例中學習的函式繪製和更新螢幕。
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
完整的迴圈(包括初始化變數)如下所示
SDL_Event event;
int loop = 1;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
loop = 0;
}
}
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
此迴圈執行良好,除了它不允許任何鍵盤輸入,因為所有按鍵在按下時都會發送 SDL_KEYDOWN 事件型別。如果我們想要將退出程式與特定的鍵盤按鍵聯絡起來 (例如,按下 Esc 鍵退出),我們將按照前面列出的事件處理程式進行操作,但不是在按下某個鍵時將迴圈變數設定為 0,而是建立一個另一個 if 條件語句,檢查按下了哪個鍵並做出相應的響應。這是一個通用的虛擬碼概要,用於描述我們要實現的新迴圈。
- 宣告布林迴圈變數並設定為 true
- 進入迴圈 IF 迴圈變數為 TRUE
- 如果按下了鍵盤鍵 (SDL_Event.type == SDL_KEYDOWN)
- 如果按下的鍵盤鍵是
鍵 (SDL_Event.key.keysym.sym == SDLK_ESCAPE) - 將迴圈變數設定為 FALSE
- 繪製和更新螢幕
因此,使用前面相同的程式碼,在事件處理程式中,我們檢查是否按下了鍵盤鍵 (無論具體是哪個鍵)。在這個 if 條件語句中,我們沒有立即將迴圈變數設定為 0,而是檢查按下的具體鍵盤鍵的值。鍵盤值儲存在 SDL_Event 結構體的 SDL_KeyboardEvent 成員 key 中。
typedef union{
Uint8 type;
SDL_ActiveEvent active;
SDL_KeyboardEvent key;
SDL_MouseMotionEvent motion;
…
}
SDL_KeyboardEvent 結構比包含它的 SDL_Event 聯合體要簡單得多。
typedef struct{
Uint8 type;
Uint8 state;
SDL_keysym keysym;
} SDL_KeyboardEvent;
- type – 與我們在迴圈第一個版本中檢查的 SDL_Event 型別成員相同。由於這是一個鍵盤結構,因此此成員的唯一值為 SDL_KEYDOWN 或 SDL_KEYUP。
- state – 鍵盤的狀態。可以是 SDL_PRESSED 或 SDL_RELEASED。請注意,此成員與型別成員的功能相似。
- keysym – SDL_keysym 結構,包含按下的或釋放的實際鍵值。
typedef struct{
Uint8 scancode;
SDLKey sym;
SDLMod mod;
Uint16 unicode;
} SDL_keysym;
這裡唯一重要的欄位是 SDLKey 成員 sym,它包含鍵盤上每個鍵的預定義識別符號值。scancode 成員包含有關鍵盤鍵的資訊,但以硬體相關的方式 (例如,一個系統上的掃描碼與另一個系統上的掃描碼不同)。sym 成員包含跨平臺識別的系統無關鍵程式碼。以下是 SDLKey 成員的一些值。
| SDLK_ESCAPE | 按下了 |
| SDLK_RIGHT | SDLK_DOWN | SDLK_LEFT | 按下了方向鍵 |
因此,要檢查是否按下了
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
將此放在檢查 SDL_KEYDOWN 事件型別的條件語句中。
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
程式中的最新迴圈現在應該這樣實現
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
基於時間的延遲
[edit | edit source]在實現遊戲迴圈時,必須能夠以恆定的幀速率執行它。SDL 的計時機制精確到毫秒。考慮到一秒鐘有 1000 毫秒,透過能夠檢索程式執行以來的當前毫秒數 (或“滴答數”),人們可以實現一個以恆定速率設定的基於時間的迴圈。
如果您希望將程式設定為每秒更新 20 幀作為它的幀速率,則可以透過將一秒鐘中的毫秒數 (滴答數) (1000) 除以所需的幀速率來確定幀更新之前需要經過的滴答數。
tick_count = (1000 / frame_rate)
要獲得 20 幀/秒幀率所需的滴答計數
tick_count = (1000/ 20) = 50 ticks (ms)
使用 SDL_Delay 函式,該函式接受一個引數,即延遲的毫秒數,我們可以實現基於時間的迴圈的第一個實現。
/*set tick interval to 20 frames/ second (1000ms / 20 = 50 ticks)*/
#define TICK_INTERVAL 50
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
…}
draw_and_update_screen();
SDL_Delay(TICK_INTERVAL);
/*end game loop*/
}
此迴圈的主要問題是 [b]draw_and_update_screen()[/b] 函式可能需要幾毫秒才能完成,具體取決於繪製的圖形的複雜性。例如,如果每幀繪製和更新螢幕需要 5 毫秒,再加上迴圈結束時 [b]滴答間隔[/b] 的延遲,我們的幀率將從每 50 個滴答更新一次變為每 55 個滴答更新一次,使我們的恆定速率降低了 5 個滴答。
為了解決這個問題,我們必須能夠跟蹤程式開始時和迴圈中經過的時間,並考慮處理遊戲迴圈所需的時間。
SDL 包含一個函式,允許你檢索程式開始執行以來的滴答計數。
Uint32 SDL_GetTicks(void);
Get the number of milliseconds since the SDL library initialization.
因此,當程式第一次啟動時,[b]SDL_GetTicks[/b] 的返回值將為 0。如果程式執行其他需要 30 個滴答才能完成的過程,然後再次檢索此函式的值,它將返回 30。
為了實現一個始終如一的幀速率,它將考慮執行迴圈所需的時間,我們必須使用兩個變數來跟蹤時間。
- 一個使用 SDL_GetTicks 獲取當前滴答計數的變數。
- 一個靜態變數,它儲存幀將更新的下一個滴答計數,當前滴答計數需要達到該值才能更新到下一幀。此變數的值將是當前滴答計數加上滴答間隔(在本例中為 20 幀/秒速率的 50),以確定何時需要繪製下一幀。
static Uint32 next_tick = 0;
Uint32 cur_tick;
/*retrieve program tick count once per frame*/
cur_tick = SDL_GetTicks();
[i] 型別 [b]Uint32[/b] 表示一個 4 位元組(32 位)整數,SDL 在其庫中定義了該型別以實現跨平臺相容性。此型別類似於在 32 位機器上將變數宣告為 [b]unsigned int[/b]。[/i]
宣告並設定這些變數後,我們將進行比較,以檢視當前滴答計數是否已達到下一個滴答計數的值,以指示幀遞增(更新到下一幀)。
if(next_tick <= cur_tick){
噹噹前滴答計數等於或超過下一個滴答計數時,這將評估為真,允許幀更新。如果是這樣,我們需要更新下一個滴答計數,以便將其用於下一次幀更新,方法是向其新增滴答間隔值(在本例中為 50)。
next_tick = cur_tick + 50;
如果上述比較評估為假,則意味著該幀尚未準備好更新,並且幀中還有剩餘時間。要檢索剩餘時間,只需計算下一個滴答計數與當前滴答計數之間的差值即可。
int time_left = next_tick – cur_tick;
你會將此值傳遞給 [b]SDL_Delay[/b] 函式,以使程式以恆定速率執行。
以下是內聯實現的時間迴圈的下一個實現(注意,稍後我們將把所有這些邏輯放入單獨的函式中)。
#define TICK_INTERVAL 50
/*declare variables at beginning to keep track of current and next tick count*/
Uint32 next_tick = 0;
Uint32 cur_tick = SDL_GetTicks();
/*variable to hold number of ticks to delay after update of screen*/
Uint32 tick_delay = 0;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
…}
draw_and_update_screen();
/*retrieve tick count up to now (after time taken to draw and update screen)*/
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
tick_delay = 0;
}else{
tick_delay = (next_tick - cur_tick);
}
SDL_Delay(tick_delay);
/*end game loop*/
}
基於時間的動畫示例
[edit | edit source]此示例將採用我們在本章開頭做的程式,新增我們在本章中討論過的事件處理和時間延遲概念。
示例程式碼:動畫矩形
[edit | edit source] #include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Rect sDim;
/*direction of moving block (0 - left, 1, -right)*/
int dir = 0;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 8 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 8, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
dir = 1;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*update rectangle position*/
if(dir){
if((sDim.x + sDim.w) < 640){
sDim.x += 1;
}else{
dir = 0;
}
}
if(!dir){
if(sDim.x > 0){
sDim.x -= 1;
}else{
dir = 1;
}
}
/*clear display with black*/
SDL_FillRect(display, NULL, SDL_MapRGB(display->format, 0, 0, 0));
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
SDL_Delay(TimeLeft());
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
SDL_Quit();
exit(0);
}
程式的第一個新增部分是 dir 變數,如果它的值為正(1),則會導致我們在本章開頭定義的矩形向右移動一個畫素/幀。如果 dir 為 0,當矩形移出螢幕時會發生這種情況,則矩形向左移動 1 個畫素/幀。時間間隔設定為每 50 個滴答更新一次(20 幀/秒),因此矩形相對於螢幕座標的速度為 20 畫素/秒。
/*direction of moving block (0 - left, 1, -right)*/
int dir = 0;
在遊戲迴圈中,在我們向螢幕繪製矩形之前,我們有條件語句來確定矩形應該移動的方向(1-右,0-左),具體取決於它是否到達了螢幕上的邊界。
/*update rectangle position*/
if(dir){
if((sDim.x + sDim.w) < 640){
sDim.x += 1;
}else{
dir = 0;
}
}
if(!dir){
if(sDim.x > 0){
sDim.x -= 1;
}else{
dir = 1;
}
}
點陣圖和精靈
[edit | edit source]本章將討論使用點陣圖影像檔案以及在 SDL 程式中載入和顯示點陣圖影像檔案。我們將從討論 Windows 點陣圖格式(在所有平臺上都支援)開始。
點陣圖分解
[edit | edit source]點陣圖檔案,除了實際的影像資料外,還包含類似於在 SDL 表面中找到的資訊。這包括寬度和高度,以及影像資料的每畫素位數(畫素格式)。除了這些資訊外,還有點陣圖檔案的大小(以位元組為單位),以及幫助程式設計師讀取點陣圖檔案以在圖形程式中使用的資訊。
作為 SDL 程式設計師,我們不必太擔心點陣圖檔案的內部結構。SDL 包含函式來讀取點陣圖檔案並將其顯示為 SDL 表面。
SDL 點陣圖程式
[edit | edit source]使用以下影像(儲存在 .bmp 格式中),我將向你展示的程式碼清單將讀取檔案(假定名為 'clouds.bmp')並在螢幕上顯示它。請注意,由於此影像的畫素格式為 24 位(3 位元組),因此顯示錶面的位深度也將設定為該值以實現相容性。(作為一個有趣的練習,在執行以下程式碼後,嘗試將顯示錶面的位深度設定為不同的值,並檢視結果。它看起來正常嗎?)

#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Surface *image;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 24 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
SDL_BlitSurface(image, NULL, display, NULL);
SDL_UpdateRect(display, 0, 0, 0, 0);
}
SDL_FreeSurface(image);
SDL_Quit();
exit(0);
}
SDL 函式 SDL_LoadBMP 接受一個引數,即點陣圖檔案的名稱。如果檔案載入成功,該函式將返回指向 SDL 表面的指標(類似於顯示錶面),其中載入了點陣圖資料。如果由於任何原因(例如,檔名錯誤或資料不相容)函式失敗,它將返回 NULL。就像你檢查顯示錶面以檢視它是否為 NULL 一樣,對位圖表面也這樣做。
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
要檢視影像,我們必須將影像資料複製到顯示錶面。為此,我們使用 SDL_BlitSurface 函式。
int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
第一個引數是我們想要顯示的點陣圖影像表面。第二個引數是我們想要顯示的位圖表面的尺寸。將其設定為 NULL 允許顯示整個點陣圖。第三個引數是我們想要顯示點陣圖的 SDL 表面。我們在這裡傳遞顯示錶面以在螢幕上顯示點陣圖。第四個引數是我們想要顯示位圖表面的顯示錶面的尺寸。將其設定為 NULL 允許我們顯示整個顯示錶面的點陣圖。
在完成點陣圖處理後(例如,程式關閉),我們需要清除點陣圖資料所佔用的記憶體。這可以透過使用 SDL_FreeSurface 函式來完成,該函式接受一個指向要釋放的 SDL 表面的指標作為引數。
SDL_FreeSurface(image);
點陣圖定位和透明度
[edit | edit source]將點陣圖相對於螢幕進行定位與我們在上一章中對藍色矩形所做的操作基本相同。以以下點陣圖影像(來自 Ari Feldman 的 GPL 集合)為例。
要將此點陣圖影像的左上角定位在螢幕的中心,我們將建立一個 SDL_Rect 結構,其 x 和 y 欄位設定為螢幕的中心。
SDL_Surface *plane;
SDL_Rect plane_rect;
/*load plane image as previously shown*/
plane_rect.x = 640/2;
plane_rect.y = 480/2;
這裡唯一的問題是檢索影像的寬度和高度以完成 SDL_Rect 結構。要設定這些值,請使用 SDL_Surface 成員 'w' 和 'h',它們會在載入點陣圖後自動儲存其尺寸。
plane_rect.w = plane->w;
plane_rect.h = plane->h;
然後,在遊戲迴圈中,你將呼叫 SDL_BlitSurface 函式,並將你定義的 SDL_Rect 結構作為第四個(目標矩形)引數傳遞。
SDL_BlitSurface(image, NULL, display, NULL);
SDL_BlitSurface(plane, NULL, display, &plane_rect);
注意背景和飛機點陣圖的繪製順序。此順序允許飛機顯示在我們之前顯示的雲彩點陣圖之上。
飛機點陣圖看起來很粗糙,藍色背景在它後面。當處理精靈(動畫點陣圖)時,您需要能夠丟棄背景顏色以獲得更乾淨的外觀。SDL 允許透過使用顏色鍵來丟棄此顏色。顏色鍵是單個顏色(或顏色範圍),在繪製點陣圖時會被忽略。對於上面的圖形,飛機,背景是純藍色(RGB 0 0 255)。要設定顏色鍵以忽略該顏色,請使用 SDL 函式 SDL_SetColorKey
int SDL_SetColorKey(SDL_Surface *surface, Uint32 flag, Uint32 key);
此函式將 SDL 表面的指標作為第一個引數,以設定顏色鍵。第二個引數是確定顏色鍵型別的一個標誌。指定 SDL_SRCCOLORKEY 指示 SDL 包含的顏色鍵應為透明的。第三個引數是要設定為顏色鍵的顏色值。此值可以透過呼叫 SDL_MapRGB 來檢索。此函式在出錯時返回 -1,成功時返回 0。因此,在主迴圈之前以及載入飛機點陣圖之後,可以設定顏色鍵,如下所示
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY,
SDL_MapRGB(plane->format, 0, 0, 255)) < 0){
printf("\nUnable to set colorkey");
}
您也可以在變數中預定義顏色鍵,然後將該變數傳遞給 SDL_SetColorKey
Uint32 colorkey;
...
colorkey = SDL_MapRGB(plane->format, 0, 0, 255);
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set colorkey");
}
點陣圖動畫是透過將影像分割成相等的部分(尺寸方面)來實現的。然後,在設定的時間間隔(或速率)內切換影像的不同部分以模擬動畫。例如,以下旋轉飛機的點陣圖影像
鑑於此影像的尺寸為 256 畫素寬,32 畫素高,將其分成八部分,每個部分的尺寸為 32x32 畫素。在對該影像進行動畫處理時,我們將以一定的速率在這八個部分之間進行切換,類似於投影儀在幀之間切換的方式
為了將這種膠片卷軸的概念轉化為計算機圖形,可以將點陣圖精靈視為一個有序的幀陣列(0-7)
要執行動畫,您將切換幀 0 和 7。跟蹤表示它的矩形的定位和尺寸很容易。假設您正在跟蹤點陣圖的源矩形和目標矩形(分別是影像的源矩形和顯示器上的目標矩形)
/*frame (src) rectangle and destination rect*/
SDL_Rect frect;
SDL_Rect prect;
/*cur frame of animation*/
int curframe = 0;
在定義矩形後,您可以透過將當前幀的值乘以幀寬度來檢索源矩形的 x 值
frect.x = curframe * FRAME_WIDTH;
frect.y = 0;
frect.w = FRAME_WIDTH;
frect.h = FRAME_HEIGHT;
目標矩形類似於我們之前在討論點陣圖定位時在本章中定義的,只是我們不是使用整個影像的尺寸,而是使用幀尺寸
/*position bitmap at screen coordinates (50, 50)*/
prect.x = 50;
prect.y = 50;
prect.w = FRAME_WIDTH;
prect.h = FRAME_HEIGHT;
繪製圖像需要我們之前學到的有關顯示和定位點陣圖的知識。基本上,我們只需要提供之前定義的矩形
SDL_BlitSurface(plane_spr, &frect, display, &prect);
更新幀時,我們需要確保在遞增幀時,它不會超出與影像幀佈局相關的陣列邊界。以下顯示了根據最大幀數(8)更新當前幀的第一種方法
if(++curframe >= NUM_FRAMES){
curframe = 0;
}
這種方法的缺點是動畫精靈將在每次滴答間隔後更新,這使得動畫速度過快。為了降低速度,我們引入了速率的概念。速率將是在更新精靈幀之前經過的滴答間隔數。假設我們將速率定義為每 3 個滴答間隔更新一次
int rate = 3;
int cur_rate = 0;
可以透過以下方法更新當前速率並最終更新幀
if(++cur_rate > rate){
curframe += 1;
if(curframe >= NUM_FRAMES){
curframe = 0;
}
cur_rate = 0;
}
這將檢查當前速率是否達到最初設定的速率(在本例中為 3)。如果該條件為真,則當前幀將遞增,當前速率將重置。
#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
#define NUM_FRAMES 8
#define FRAME_WIDTH 32
#define FRAME_HEIGHT 32
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Surface *image;
SDL_Surface *plane;
SDL_Surface *plane_spr;
SDL_Rect frect;
SDL_Rect prect;
int curframe = 0;
int rate = 3;
int cur_rate = 0;
SDL_Rect plane_rect;
Uint32 colorkey;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 32 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
/*load plane bitmap*/
plane = SDL_LoadBMP("plane.bmp");
if(plane == NULL){
printf("\nUnable to load plane bitmap");
}
/*set rectangle dimensions to center of screen*/
plane_rect.x = 640/2;
plane_rect.y = 480/2;
plane_rect.w = plane->w;
plane_rect.h = plane->h;
/*set colorkey (transparent) to blue*/
colorkey = SDL_MapRGB(plane->format, 0, 0, 255);
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set colorkey");
}
/*load sprite bitmap*/
plane_spr = SDL_LoadBMP("plane_spr.bmp");
if(!plane_spr){
printf("\nUnable to open plane sprite");
}
/*update color key for sprite bitmap*/
colorkey = SDL_MapRGB(plane_spr->format, 0, 0, 255);
if(SDL_SetColorKey(plane_spr, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set sprite color key");
}
prect.x = 50;
prect.y = 50;
prect.w = FRAME_WIDTH;
prect.h = FRAME_HEIGHT;
frect.x = curframe * FRAME_WIDTH;
frect.y = 0;
frect.w = FRAME_WIDTH;
frect.h = FRAME_HEIGHT;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*update x position of frame*/
frect.x = curframe * FRAME_WIDTH;
SDL_BlitSurface(image, NULL, display, NULL);
SDL_BlitSurface(plane, NULL, display, &plane_rect);
/*Blit sprite based on frame dimensions (frect)*/
SDL_BlitSurface(plane_spr, &frect, display, &prect);
SDL_UpdateRect(display, 0, 0, 0, 0);
/*update frame based on rate*/
if(++cur_rate > rate){
curframe += 1;
if(curframe >= NUM_FRAMES){
curframe = 0;
}
cur_rate = 0;
}
SDL_Delay(TimeLeft());
}
SDL_FreeSurface(image);
SDL_Quit();
exit(0);
}
- 基本圖形庫
- 遊戲設計:空中戰爭
- 遊戲設計:圖形
- 遊戲設計:人工智慧
- 完整的遊戲程式碼和解釋



