SMFL 教程&個人筆記
本文大部分來自官方教程的Google翻譯 但是加了一點點個人的理解和其他相關知識
轉載請註明 原文鏈接 ://www.cnblogs.com/Multya/p/16273753.html
本文有什麼
這是SFML官方教程的翻譯
涉及的模組有
- System module 系統模組
- Window module 窗口模組
- Graphics module 圖形模組
其實一共有五個模組 因為太長了所以就先做了三個 Typora都撐不住了。。
把垃圾Google翻譯調教了一下 引入了一點點其他知識(應該沒有私貨
還有隨機出現的自己手打和官方不一樣的程式 (大括弧不換行
不保證一定沒有bug(理直氣壯
接下來的內容請看後一篇吧 應該是有的
最好要先學一點點OpneGL的知識再來學SFML 畢竟SFML就是OpenGL良心包裝器 核心思想還是OpenGL的原理
這裡推薦cherno的教程 看完那個基本的點應該是沒有什麼問題的了 而且影片品質非常好 安利了
好 開始吧
配置cmake
其他配置的點這裡 本作者用冷門 的Clion敲程式碼 半天也弄不懂怎麼鏈接庫 原來只要改改cmake就行了(哭 人家弄好的你不要
這裡使用了預編譯的二進位庫文件 不嫌麻煩可以自行編譯
cmake文件需額外添加以下 其中Gm為項目名稱 SFML在根目錄下 dependence/SFML
源文件在根目錄下 src/
#提供查找包的路徑
set(SFML_DIR "dependence/SFML/lib/cmake/SFML")
#查找SFMLConfig.cmake文件並通過預置查找包工具查找
find_package(SFML REQUIRED COMPONENTS audio network graphics window system)
#頭文件目錄綁定
include_directories(${SFML_INCLUDE_DIR})
#添加編譯為可執行文件
add_executable(Gm src/main.cpp)
#鏈接二進位庫文件
target_link_libraries(Gm sfml-audio sfml-network sfml-graphics sfml-window sfml-system)
窗口
打開一個窗口
SFML 中的窗口由 sf::Window
類定義。可以在構建時直接創建和打開窗口:
#include <SFML/Window.hpp>
int main()
{
sf::Window window(sf::VideoMode(800, 600), "My window");
...
return 0;
}
第一個參數video mode定義了窗口的大小(內部大小,沒有標題欄和邊框)。在這裡,我們創建一個大小為 800×600 像素的窗口。
該類sf::VideoMode
有一些有趣的靜態函數來獲取桌面解析度,或全螢幕模式的有效影片模式列表。不要猶豫,看看它的文檔。
第二個參數只是窗口的標題。
此構造函數接受第三個可選參數:樣式,它允許您選擇所需的裝飾和功能。您可以使用以下樣式的任意組合:
sf::Style::None |
完全沒有裝飾(例如,對於啟動畫面很有用);這種風格不能與其他風格結合 |
---|---|
sf::Style::Titlebar |
窗口有一個標題欄 |
sf::Style::Resize |
窗口可以調整大小並有一個最大化按鈕 |
sf::Style::Close |
窗口有一個關閉按鈕 |
sf::Style::Fullscreen |
窗口以全螢幕模式顯示;此樣式不能與其他樣式組合,並且需要有效的影片模式 |
sf::Style::Default |
默認樣式,它是`Titlebar |
還有第四個可選參數,它定義了 OpenGL 特定的選項,這些選項在 專門的 OpenGL 教程中進行了解釋。
如果您想在實例構建後sf::Window
創建窗口,或者用不同的影片模式或標題重新創建它,您可以使用該create
函數來代替。它採用與構造函數完全相同的參數。
#include <SFML/Window.hpp>
int main()
{
sf::Window window;
window.create(sf::VideoMode(800, 600), "My window");
...
return 0;
}
讓窗口煥發生機
如果您嘗試執行上面的程式碼而不用任何東西代替「…」,您將幾乎看不到任何東西。首先,因為程式立即結束。其次,因為沒有事件處理——所以即使你在這段程式碼中添加了一個無限循環,你也會看到一個死窗口,無法移動、調整大小或關閉。
讓我們添加一些程式碼讓這個程式更有趣:
#include <SFML/Window.hpp>
#include <bits/stdc++.h>
int main() {
sf::Window window(sf::VideoMode(800, 600), "My window");
// run the program as long as the window is open
while (window.isOpen()) {
// check all the window's events that were triggered since the last iteration of the loop
sf::Event event;
while (window.pollEvent(event)) {
// "close requested" event: we close the window
if (event.type == sf::Event::Closed)
window.close();
}
}
return 0;
}
上面的程式碼將打開一個窗口,並在用戶關閉它時終止。讓我們詳細看看它是如何工作的。
首先,我們添加了一個循環,以確保應用程式將被刷新/更新,直到窗口關閉。大多數(如果不是全部)SFML 程式都會有這種循環,有時稱為主循環或遊戲循環。
然後,我們想要在遊戲循環中做的第一件事是檢查發生的任何事件。請注意,我們使用while
循環,以便在有多個事件的情況下處理所有未決事件。如果事件未決,則該pollEvent
函數返回 true,如果沒有,則返回 false。
每當我們得到一個事件時,我們必須檢查它的類型(窗口關閉?按鍵被按下?滑鼠移動?操縱桿連接?…),如果我們對它感興趣,就做出相應的反應。在這種情況下,我們只關心Event::Closed
當用戶想要關閉窗口時觸發的事件。此時,窗口仍處於打開狀態,我們必須使用close
函數顯式關閉它。這使您可以在窗口關閉之前執行某些操作,例如保存應用程式的當前狀態或顯示消息。
人們經常犯的一個錯誤是忘記事件循環,僅僅是因為他們還不關心處理事件(他們使用實時輸入代替)。如果沒有事件循環,窗口將變得無響應。值得注意的是,事件循環有兩個角色:除了向用戶提供事件之外,它還讓窗口有機會處理其內部事件,這是必需的,以便它可以對移動或調整用戶操作做出反應。
窗口關閉後,主循環退出,程式終止。
在這一點上,您可能注意到我們還沒有討論在窗口上繪製一些東西。如介紹中所述,這不是 sfml-window 模組的工作,如果您想繪製精靈、文本或形狀等內容,則必須跳轉到 sfml-graphics 教程。
要繪製東西,您也可以直接使用 OpenGL,完全忽略 sfml-graphics 模組。sf::Window
在內部創建一個 OpenGL 上下文並準備好接受您的 OpenGL 調用。您可以在相應的教程中了解更多相關資訊。
不要期望在此窗口中看到有趣的東西:您可能會看到統一的顏色(黑色或白色),或者之前使用 OpenGL 的應用程式的最後內容,或者……其他的東西。
玩窗口
當然,SFML 允許您稍微玩一下自己的窗口。支援更改大小、位置、標題或圖標等基本窗口操作,但與專用 GUI 庫(Qt、wxWidgets)不同,SFML 不提供高級功能。SFML 窗口僅用於為 OpenGL 或 SFML 繪圖提供環境。
// change the position of the window (relatively to the desktop)
window.setPosition(sf::Vector2i(10, 50));
// change the size of the window
window.setSize(sf::Vector2u(640, 480));
// change the title of the window
window.setTitle("SFML window");
// get the size of the window
sf::Vector2u size = window.getSize();
unsigned int width = size.x;
unsigned int height = size.y;
// check whether the window has the focus
bool focus = window.hasFocus();
...
您可以參考 API 文檔以獲取sf::Window
函數的完整列表。
如果您確實需要窗口的高級功能,您可以使用另一個庫創建一個(甚至是完整的 GUI),並將 SFML 嵌入其中。為此,您可以使用其他構造函數或create
函數,sf::Window
它採用現有窗口的特定於作業系統的句柄。在這種情況下,SFML 將在給定窗口內創建一個繪圖上下文,並在不干擾父窗口管理的情況下捕獲其所有事件。
sf::WindowHandle handle = /* specific to what you're doing and the library you're using */;
sf::Window window(handle);
如果您只想要一個額外的、非常具體的功能,您也可以反過來做:創建一個 SFML 窗口並獲取其特定於作業系統的句柄來實現 SFML 本身不支援的東西。
sf::Window window(sf::VideoMode(800, 600), "SFML window");
sf::WindowHandle handle = window.getSystemHandle();
// you can now use the handle with OS specific functions
將 SFML 與其他庫集成需要一些工作,此處不再贅述,但您可以參考專門的教程、示例或論壇帖子。
控制幀率
有時,當您的應用程式快速運行時,您可能會注意到諸如撕裂之類的視覺偽影。原因是您的應用程式的刷新率與顯示器的垂直頻率不同步,因此前一幀的底部與下一幀的頂部混合在一起。
解決這個問題的方法是激活垂直同步。由顯示卡自動處理,可通過以下setVerticalSyncEnabled
功能輕鬆開啟和關閉:
window.setVerticalSyncEnabled(true); // call it once, after creating the window
在此調用之後,您的應用程式將以與顯示器刷新率相同的頻率運行。
有時setVerticalSyncEnabled
沒有效果:這很可能是因為垂直同步在您的圖形驅動程式設置中被強制「關閉」。它應該設置為「由應用程式控制」。
在其他情況下,您可能還希望應用程式以給定的幀速率運行,而不是監視器的頻率。這可以通過調用來完成 setFramerateLimit
:
window.setFramerateLimit(60); // call it once, after creating the window
與 不同setVerticalSyncEnabled
的是,此功能由 SFML 本身實現,使用sf::Clock
和的組合sf::sleep
。一個重要的後果是它不是 100% 可靠的,尤其是對於高幀率:sf::sleep
的解析度取決於底層作業系統和硬體,可能高達 10 或 15 毫秒。不要依賴此功能來實現精確計時。
切勿同時使用setVerticalSyncEnabled
兩者setFramerateLimit
!他們會嚴重混合併使事情變得更糟。
關於窗口要知道的事
下面簡要列出了您可以使用 SFML 窗口做什麼和不能做什麼。
您可以創建多個窗口
SFML 允許您創建多個窗口,並在主執行緒中處理它們,或者在其自己的執行緒中處理它們(但…見下文)。在這種情況下,不要忘記為每個窗口設置一個事件循環。
尚未正確支援多台顯示器
SFML 沒有明確管理多個監視器。因此,您將無法選擇窗口出現在哪個監視器上,並且您將無法創建多個全螢幕窗口。這應該在未來的版本中得到改進。
必須在窗口的執行緒中輪詢事件
這是大多數作業系統的一個重要限制:事件循環(更準確地說是pollEvent
orwaitEvent
函數)必須在創建窗口的同一執行緒中調用。這意味著如果你想為事件處理創建一個專用執行緒,你必須確保窗口也是在這個執行緒中創建的。如果您真的想在執行緒之間拆分事物,將事件處理保留在主執行緒中並將其餘部分(渲染、物理、邏輯……)移到單獨的執行緒中會更方便。這種配置也將與下面描述的其他限制兼容。
在 macOS 上,窗口和事件必須在主執行緒中進行管理
是的,這是真的;如果您嘗試在主執行緒以外的執行緒中創建窗口或處理事件,macOS 將不會同意。
在 Windows 上,大於桌面的窗口將無法正常運行
出於某種原因,Windows 不喜歡比桌面更大的窗口。這包括使用 VideoMode::getDesktopMode()
: 添加的窗口裝飾(邊框和標題欄)創建的窗口,您最終會得到一個比桌面稍大的窗口。
獲取事件
sf::Event 類型
在處理事件之前,重要的是要了解sf::Event
類型是什麼,以及如何正確使用它。 sf::Event
是一個union,這意味著一次只有一個成員是有效的(記住你的 C++ 課程:一個 union 的所有成員共享相同的記憶體空間)。有效成員是與事件類型匹配的成員,例如event.key
事件 KeyPressed
。嘗試讀取任何其他union將導致未定義的行為(很可能:隨機或無效值)。永遠不要嘗試使用與其類型不匹配的事件成員,這一點很重要。
//Event 內部實現
union
{
SizeEvent size; ///< Size event parameters (Event::Resized)
KeyEvent key; ///< Key event parameters (Event::KeyPressed, Event::KeyReleased)
TextEvent text; ///< Text event parameters (Event::TextEntered)
MouseMoveEvent mouseMove; ///< Mouse move event parameters (Event::MouseMoved)
MouseButtonEvent mouseButton; ///< Mouse button event parameters (Event::MouseButtonPressed, Event::MouseButtonReleased)
MouseWheelEvent mouseWheel; ///< Mouse wheel event parameters (Event::MouseWheelMoved) (deprecated)
MouseWheelScrollEvent mouseWheelScroll; ///< Mouse wheel event parameters (Event::MouseWheelScrolled)
JoystickMoveEvent joystickMove; ///< Joystick move event parameters (Event::JoystickMoved)
JoystickButtonEvent joystickButton; ///< Joystick button event parameters (Event::JoystickButtonPressed, Event::JoystickButtonReleased)
JoystickConnectEvent joystickConnect; ///< Joystick (dis)connect event parameters (Event::JoystickConnected, Event::JoystickDisconnected)
TouchEvent touch; ///< Touch events parameters (Event::TouchBegan, Event::TouchMoved, Event::TouchEnded)
SensorEvent sensor; ///< Sensor event parameters (Event::SensorChanged)
};
sf::Event
實例由類的pollEvent
(或waitEvent
)函數填充sf::Window
。只有這兩個函數可以產生有效事件,任何嘗試使用sf::Event
未通過成功調用 pollEvent
(or waitEvent
) 返回的都會導致與上面提到的相同的未定義行為。
需要明確的是,典型的事件循環如下所示:
sf::Event event;
// while there are pending events...
while (window.pollEvent(event))
{
// check the type of the event...
switch (event.type)
{
// window closed
case sf::Event::Closed:
window.close();
break;
// key pressed
case sf::Event::KeyPressed:
...
break;
// we don't process other types of events
default:
break;
}
}
再次閱讀上面的段落並確保您完全理解它,sf::Event
union類型是導致沒有經驗的程式設計師許多bug的原因。
好的,現在我們可以看到 SFML 支援哪些事件,它們的含義以及如何正確使用它們。
關閉的事件
sf::Event::Closed
當用戶想要關閉窗口時,通過窗口管理器提供的任何可能的方法(「關閉」按鈕、鍵盤快捷鍵等)觸發 該事件。該事件僅代表關閉請求,收到事件時窗口尚未關閉。
典型的程式碼只會調用window.close()
這個事件來實際關閉窗口。但是,您可能還想先做其他事情,例如保存當前應用程式狀態或詢問用戶要做什麼。如果您不執行任何操作,窗口將保持打開狀態。
sf::Event
union 中沒有與此事件相關的成員。
if (event.type == sf::Event::Closed)
window.close();
調整大小的事件
sf::Event::Resized
當通過用戶操作或通過調用以編程方式調整窗口大小時觸發 該事件window.setSize
。
您可以使用此事件來調整渲染設置:如果您直接使用 OpenGL,則為viewport,如果您使用 sfml-graphics,則為current view。
與此事件關聯的成員是event.size
,它包含窗口的新大小。
if (event.type == sf::Event::Resized)
{
std::cout << "new width: " << event.size.width << std::endl;
std::cout << "new height: " << event.size.height << std::endl;
}
LostFocus 和 GainedFocus 事件
sf::Event::LostFocus
和事件 在sf::Event::GainedFocus
窗口失去/獲得焦點時觸發,這發生在用戶切換當前活動窗口時。當窗口失焦時,它不會接收鍵盤事件。
例如,如果您想在窗口不活動時暫停遊戲,可以使用此事件。
sf::Event
union中沒有與這些事件相關的成員。
if (event.type == sf::Event::LostFocus)
myGame.pause();
if (event.type == sf::Event::GainedFocus)
myGame.resume();
文本輸入事件
sf::Event::TextEntered
輸入字元時觸發 該事件。這不能與KeyPressed
事件混淆:TextEntered
解釋用戶輸入併產生適當的可列印字元。例如,在法語鍵盤上按「^」然後按「e」將產生兩個KeyPressed
事件,但一個TextEntered
事件包含「ê」字元。它適用於作業系統提供的所有輸入法,即使是最具體或最複雜的輸入法。
此事件通常用於捕獲文本欄位中的用戶輸入。
與此事件關聯的成員是event.text
,它包含輸入字元的 Unicode 值。您可以將其直接放在 a 中sf::String
,也可以char
在確保它在 ASCII 範圍內 (0 – 127) 後將其轉換為 a 。
if (event.type == sf::Event::TextEntered)
{
if (event.text.unicode < 128)
std::cout << "ASCII character typed: " << static_cast<char>(event.text.unicode) << std::endl;
}
請注意,由於它們是 Unicode 標準的一部分,因此此事件會生成 一些不可列印的字元,例如退格。在大多數情況下,您需要將它們過濾掉。
許多程式設計師使用該KeyPressed
事件來獲取用戶輸入,並開始實施瘋狂的演算法,試圖解釋所有可能的鍵組合以產生正確的字元。不要那樣做!
KeyPressed 和 KeyReleased 事件
sf::Event::KeyPressed
和事件 在sf::Event::KeyReleased
按下/釋放鍵盤鍵時觸發。
如果按住某個鍵,KeyPressed
則會在默認的作業系統延遲下生成多個事件(即,與您在文本編輯器中按住字母時應用的延遲相同)。要禁用重複KeyPressed
事件,您可以調用window.setKeyRepeatEnabled(false)
. 另一方面,很明顯,KeyReleased
事件永遠不會重演。
如果您想在按下或釋放某個鍵時僅觸發一次動作,例如使角色隨空格跳躍,或通過轉義退出某事,則可以使用此事件。
有時,人們試圖KeyPressed
直接對事件做出反應以實現平穩的運動。這樣做不會產生預期的效果,因為當你按住一個鍵時,你只會得到幾個事件(記住,重複延遲)。要實現事件的平滑移動,您必須使用設置為 onKeyPressed
和 clear on的布爾值KeyReleased
;只要設置了布爾值,您就可以移動(獨立於事件)。
產生平滑移動的另一個(更簡單)解決方案是使用實時鍵盤輸入sf::Keyboard
(參見 專用教程)。
與這些事件關聯的成員是event.key
,它包含按下/釋放鍵的程式碼,以及修飾鍵的當前狀態(alt、control、shift、system)。
if (event.type == sf::Event::KeyPressed)
{
if (event.key.code == sf::Keyboard::Escape)
{
std::cout << "the escape key was pressed" << std::endl;
std::cout << "control:" << event.key.control << std::endl;
std::cout << "alt:" << event.key.alt << std::endl;
std::cout << "shift:" << event.key.shift << std::endl;
std::cout << "system:" << event.key.system << std::endl;
}
}
請注意,某些鍵對作業系統具有特殊含義,會導致意外行為。例如,Windows 上的 F10 鍵「竊取」焦點,或者使用 Visual Studio 時啟動調試器的 F12 鍵。這可能會在 SFML 的未來版本中得到解決。
MouseWheelMoved 事件
sf::Event::MouseWheelMoved
事件自 SFML 2.3 起已棄用,請改用 MouseWheelScrolled 事件。
MouseWheelScrolled 事件
sf::Event::MouseWheelScrolled
當滑鼠滾輪向上或向下移動時觸發 該事件,如果滑鼠支援它,也會橫向觸發。
與此事件關聯的成員是event.mouseWheelScroll
,它包含滾輪移動的刻度數、滾輪的方向以及滑鼠游標的當前位置。
if (event.type == sf::Event::MouseWheelScrolled)
{
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
std::cout << "wheel type: vertical" << std::endl;
else if (event.mouseWheelScroll.wheel == sf::Mouse::HorizontalWheel)
std::cout << "wheel type: horizontal" << std::endl;
else
std::cout << "wheel type: unknown" << std::endl;
std::cout << "wheel movement: " << event.mouseWheelScroll.delta << std::endl;
std::cout << "mouse x: " << event.mouseWheelScroll.x << std::endl;
std::cout << "mouse y: " << event.mouseWheelScroll.y << std::endl;
}
MouseButtonPressed 和 MouseButtonReleased 事件
sf::Event::MouseButtonPressed
和事件 在sf::Event::MouseButtonReleased
按下/釋放滑鼠按鈕時觸發。
SFML 支援 5 個滑鼠按鈕:左鍵、右鍵、中鍵(滾輪)、額外的 #1 和額外的 #2(側鍵)。
與這些事件關聯的成員是event.mouseButton
,它包含按下/釋放按鈕的程式碼,以及滑鼠游標的當前位置。
if (event.type == sf::Event::MouseButtonPressed)
{
if (event.mouseButton.button == sf::Mouse::Right)
{
std::cout << "the right button was pressed" << std::endl;
std::cout << "mouse x: " << event.mouseButton.x << std::endl;
std::cout << "mouse y: " << event.mouseButton.y << std::endl;
}
}
滑鼠移動事件
sf::Event::MouseMoved
當滑鼠在窗口內移動時觸發 該事件。
即使窗口沒有聚焦,也會觸發此事件。但是,只有當滑鼠在窗口的內部區域內移動時才會觸發它,而不是在它移動到標題欄或邊框上時觸發。
與此事件關聯的成員是event.mouseMove
,它包含滑鼠游標相對於窗口的當前位置。
if (event.type == sf::Event::MouseMoved)
{
std::cout << "new mouse x: " << event.mouseMove.x << std::endl;
std::cout << "new mouse y: " << event.mouseMove.y << std::endl;
}
MouseEntered 和 MouseLeft 事件
sf::Event::MouseEntered
和事件 在sf::Event::MouseLeft
滑鼠游標進入/離開窗口時觸發。
sf::Event
union中沒有與這些事件相關的成員。
if (event.type == sf::Event::MouseEntered)
std::cout << "the mouse cursor has entered the window" << std::endl;
if (event.type == sf::Event::MouseLeft)
std::cout << "the mouse cursor has left the window" << std::endl;
鍵鼠
本部分解釋了如何訪問全局輸入設備:鍵盤、滑鼠和操縱桿。這不能與事件混淆。實時輸入讓您可以隨時查詢鍵盤、滑鼠和操縱桿的全局狀態(「當前是否按下此按鈕? 」、「當前滑鼠在哪裡? 」)而事件發生時通知您(「此按鈕被按下「,」滑鼠已移動「)。
鍵盤
提供對鍵盤狀態的訪問的類是sf::Keyboard
. 它只包含一個函數 ,isKeyPressed
它檢查一個鍵的當前狀態(按下或釋放)。它是一個靜態函數,因此您無需實例化sf::Keyboard
即可使用它。
此函數直接讀取鍵盤狀態,忽略窗口的焦點狀態。這意味著isKeyPressed
即使您的窗口處於非活動狀態,它也可能返回 true。(因此可能需要適當的判斷防止意外情況的發生)
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
{
// left key is pressed: move our character
character.move(1.f, 0.f);
}
key codes在sf::Keyboard::Key
枚舉中定義。
class SFML_WINDOW_API Keyboard
{
public:
////////////////////////////////////////////////////////////
/// Key codes
////////////////////////////////////////////////////////////
enum Key
{
Unknown = -1, ///< Unhandled key
A = 0, ///< The A key
B, ///< The B key
C, ///< The C key
D, ///< The D key
...
KeyCount, ///< Keep last -- the total number of keyboard keys
// Deprecated values:
Dash = Hyphen, ///< Use Hyphen instead
BackSpace = Backspace, ///< Use Backspace instead
BackSlash = Backslash, ///< Use Backslash instead
SemiColon = Semicolon, ///< Use Semicolon instead
Return = Enter ///< Use Enter instead
}
...
}
根據您的作業系統和鍵盤布局,某些鍵程式碼可能會丟失或解釋不正確。這將在 SFML 的未來版本中得到改進。
滑鼠
提供對滑鼠狀態的訪問的類是sf::Mouse
. 和它的朋友sf::Keyboard
一樣, sf::Mouse
只包含靜態函數,並不打算實例化(SFML 暫時只處理單個滑鼠)。
您可以檢查按鈕是否被按下:
if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
{
// left mouse button is pressed: shoot
gun.fire();
}
滑鼠按鈕程式碼在sf::Mouse::Button
枚舉中定義。SFML 最多支援 5 個按鈕:左、右、中間(滾輪)和兩個附加按鈕,無論它們是什麼。
您還可以獲取和設置滑鼠相對於桌面或窗口的當前位置:
// get the global mouse position (relative to the desktop)
sf::Vector2i globalPosition = sf::Mouse::getPosition();
// get the local mouse position (relative to a window)
sf::Vector2i localPosition = sf::Mouse::getPosition(window); // window is a sf::Window
// set the mouse position globally (relative to the desktop)
sf::Mouse::setPosition(sf::Vector2i(10, 50));
// set the mouse position locally (relative to a window)
sf::Mouse::setPosition(sf::Vector2i(10, 50), window); // window is a sf::Window
沒有讀取滑鼠滾輪當前狀態的功能。由於輪子只能相對移動,所以沒有可以查詢的絕對狀態。通過查看一個鍵,您可以判斷它是按下還是釋放。通過查看滑鼠游標,您可以知道它在螢幕上的位置。但是,查看滑鼠滾輪並不能告訴您它在哪個「刻度」上。所以只能在它移動(MouseWheelScrolled
事件)時收到通知。
小總結程式
#include <SFML/Graphics.hpp>
#include <bits/stdc++.h>
int main() {
sf::RenderWindow window(sf::VideoMode(200, 200), "SFML works!");
sf::CircleShape shape(100.f);
shape.setFillColor(sf::Color::Green);
sf::Mouse::setPosition(sf::Vector2i(10, 50), window);
while (window.isOpen()) {
sf::Event event{};
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
std::cout << "good byyyye" << std::endl;
window.close();
break;
case sf::Event::Resized:
std::cout << "height:" << event.size.height << std::endl;
std::cout << "weight:" << event.size.width << std::endl;
break;
case sf::Event::LostFocus:
std::cout << "hei! what are you doing!\n";
break;
case sf::Event::GainedFocus:
std::cout << "ok.." << std::endl;
break;
case sf::Event::KeyPressed:
std::cout << event.key.code << std::endl;
// std::boolalpha(std::cout);
// std::cout << "the escape key was pressed" << std::endl;
// std::cout << "control:" << event.key.control << std::endl;
// std::cout << "alt:" << event.key.alt << std::endl;
// std::cout << "shift:" << event.key.shift << std::endl;
// std::cout << "system:" << event.key.system << std::endl;
break;
case sf::Event::TextEntered:
if (event.text.unicode < 128)
std::cout << "ASCII character typed :" << static_cast<char>(event.text.unicode) << std::endl;
break;
case sf::Event::MouseWheelScrolled:
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
std::cout << "wheel type: vertical" << std::endl;
else if (event.mouseWheelScroll.wheel == sf::Mouse::HorizontalWheel)
std::cout << "wheel type: Horizontal" << std::endl;
else
std::cout << "while type: unknown" << std::endl;
std::cout << "wheel delta:" << event.mouseWheelScroll.delta << std::endl;
std::cout << "wheel x:" << event.mouseWheelScroll.x << std::endl;
std::cout << "wheel y:" << event.mouseWheelScroll.y << std::endl;
break;
case sf::Event::MouseButtonPressed:
if (event.mouseButton.button == sf::Mouse::Right)
std::cout << "right button pressed" << std::endl;
else if (event.mouseButton.button == sf::Mouse::Left)
std::cout << "left button pressed" << std::endl;
else if(event.mouseButton.button == sf::Mouse::Middle)
std::cout << "middle button pressed" << std::endl;
std::cout << "mouse x:" << event.mouseButton.x << std::endl;
std::cout << "mouse y:" << event.mouseButton.y << std::endl;
break;
case sf::Event::MouseButtonReleased:
if (event.mouseButton.button == sf::Mouse::Right)
std::cout << "right button Released" << std::endl;
else if (event.mouseButton.button == sf::Mouse::Left)
std::cout << "left button Released" << std::endl;
else if(event.mouseButton.button == sf::Mouse::Middle)
std::cout << "middle button Released" << std::endl;
std::cout << "mouse x:" << event.mouseButton.x << std::endl;
std::cout << "mouse y:" << event.mouseButton.y << std::endl;
break;
case sf::Event::MouseMoved:
// std::cout << "new mouse x " << event.mouseMove.x << std::endl;
// std::cout << "new mouse y " << event.mouseMove.y << std::endl;
default:
break;
}
}
window.clear();
window.draw(shape);
window.display();
}
return 0;
}
處理時間
SFML 中的時間
與許多其他時間是 uint32 毫秒數或浮點數秒數的庫不同,SFML 沒有為時間值強加任何特定的單位或類型。相反,它通過一個靈活的類將這個選擇留給用戶:sf::Time
. 所有處理時間值的 SFML 類和函數都使用這個類。
sf::Time
表示一個時間段(換句話說,兩個事件之間經過的時間)。它不是將當前年/月/日/小時/分鐘/秒表示為時間戳的日期時間類,它只是表示一定時間量的值,如何解釋它取決於上下文的使用。
轉換時間
sf::Time
可以從不同的單位構造一個值:秒、毫秒和微秒。有一個(非成員)函數可以將它們中的每一個變成sf::Time
:
sf::Time t1 = sf::microseconds(10000);
sf::Time t2 = sf::milliseconds(10);
sf::Time t3 = sf::seconds(0.01f);
請注意,這三個時間都是相等的。
同樣,sf::Time
可以轉換回秒、毫秒或微秒:
sf::Time time = ...;
sf::Int64 usec = time.asMicroseconds();
sf::Int32 msec = time.asMilliseconds();
float sec = time.asSeconds();
計算時間
sf::Time
只是一個時間量,所以它支援算術運算,如加法、減法、比較等。時間也可以是負數。
sf::Time t1 = ...;
sf::Time t2 = t1 * 2;
sf::Time t3 = t1 + t2;
sf::Time t4 = -t3;
bool b1 = (t1 == t2);
bool b2 = (t3 > t4);
測量時間
現在我們已經了解了如何使用 SFML 操作時間值,讓我們看看如何做幾乎每個程式都需要的事情:測量經過的時間。
SFML 有一個非常簡單的時間測量類:sf::Clock
. 它只有兩個功能:getElapsedTime
, 檢索自時鐘啟動以來經過的時間,以及restart
, 重新啟動時鐘。
sf::Clock clock; // starts the clock
...
sf::Time elapsed1 = clock.getElapsedTime();
std::cout << elapsed1.asSeconds() << std::endl;
clock.restart();
...
sf::Time elapsed2 = clock.getElapsedTime();
std::cout << elapsed2.asSeconds() << std::endl;
請注意,調用restart
還會返回經過的時間,這樣您就可以避免 getElapsedTime
之前必須顯式調用時存在的微小間隙restart
。
這是一個使用遊戲循環的每次迭代所經過的時間來更新遊戲邏輯的示例:
sf::Clock clock;
while (window.isOpen())
{
sf::Time elapsed = clock.restart();
updateGame(elapsed);
...
}
用戶數據流
介紹
SFML 有幾個資源類:影像、字體、聲音等。在大多數程式中,這些資源將藉助它們的 loadFromFile
功能從文件中載入。在其他一些情況下,資源將直接打包到可執行文件或大數據文件中,並使用loadFromMemory
. 這些功能幾乎涵蓋了所有可能的用例——但不是全部。
有時您想從不尋常的地方載入文件,例如壓縮/加密存檔或遠程網路位置。針對這些特殊情況,SFML 提供了第三種載入函數:loadFromStream
. 此函數使用抽象 sf::InputStream
介面讀取數據,它允許您提供自己的與 SFML 一起使用的流類的實現。
在本教程中,您將學習如何編寫和使用您自己的派生輸入流。
And標準流?
像許多其他語言一樣,C++ 已經有一個用於輸入數據流的類:std::istream
. 實際上它有兩個:std::istream
只是前端,自定義數據的抽象介面是std::streambuf
.
不幸的是,這些類對用戶不是很友好,如果你想實現一些重要的東西,它們會變得非常複雜。Boost.Iostreams庫試圖為標準流提供更簡單的介面,但 Boost 是一個很大的依賴項,SFML 不能依賴它。
這就是為什麼 SFML 提供了自己的流介面,希望它更加簡單和快速。
輸入流
該類sf::InputStream
聲明了四個虛函數:
class InputStream
{
public :
virtual ~InputStream() {}
virtual Int64 read(void* data, Int64 size) = 0;
virtual Int64 seek(Int64 position) = 0;
virtual Int64 tell() = 0;
virtual Int64 getSize() = 0;
};
read 必須從流中提取size個位元組的數據,並將它們複製到提供的數據地址。它返回讀取的位元組數,錯誤時返回 -1。
**seek **必須更改流中的當前讀取位置。它的位置參數是要跳轉到的絕對位元組偏移量(因此它是相對於數據的開頭,而不是相對於當前位置)。它返回新位置,或錯誤時返回 -1。
**tell **必須返迴流中的當前讀取位置(以位元組為單位),如果出錯則返回 -1。
**getSize **必須返回包含在流中的數據的總大小(以位元組為單位),如果出錯則返回 -1。
要創建自己的工作流,您必須根據他們的要求實現這四個功能中的每一個。
FileInputStream 和 MemoryInputStream
從 SFML 2.3 開始,創建了兩個新類來為新的內部音頻管理提供流。sf::FileInputStream
提供文件的只讀數據流,同時sf::MemoryInputStream
提供來自記憶體的只讀流。兩者都源自sf::InputStream
多態,因此可以使用多態。
使用輸入流
使用自定義流類很簡單:實例化它,並將其傳遞給要載入的對象 的loadFromStream
(或openFromStream
)函數。
sf::FileStream stream;
stream.open("image.png");
sf::Texture texture;
texture.loadFromStream(stream);
例子
如果您需要一個演示來幫助您專註於程式碼的工作原理,而不是迷失在實現細節上,您可以查看sf::FileInputStream
或sf::MemoryInputStream
的實現。
不要忘記查看論壇和維基。很有可能另一個用戶已經編寫了一個sf::InputStream
適合您需要的類。如果您寫了一篇新文章,並且覺得它對其他人也有用,請不要猶豫分享!
常見錯誤
某些資源類在loadFromStream
被調用後沒有完全載入。相反,只要它們被使用,它們就會繼續從它們的數據源中讀取。sf::Music
在播放音頻樣本時流式傳輸音頻樣本,而對於 sf::Font
,它根據顯示的文本動態載入字形。
因此,您用於載入音樂或字體的流實例及其數據源必須在資源使用它時保持活動狀態。如果它在仍在使用時被銷毀,則會導致未定義的行為(可能是崩潰、損壞的數據或不可見)。
另一個常見的錯誤是返回內部函數直接返回的任何內容,但有時它與 SFML 所期望的不匹配。例如,在sf::FileInputStream
程式碼中,可能會想將seek
函數編寫如下:
sf::Int64 FileInputStream::seek(sf::Int64 position)
{
return std::fseek(m_file, position, SEEK_SET);
}
此程式碼是錯誤的,因為std::fseek
成功時返回零,而 SFML 期望返回新位置。
繪製 2D
介紹
正如在前面的教程中所了解的,SFML 的窗口模組提供了一種簡單的方法來打開 OpenGL 窗口並處理其事件,但是在繪製某些東西時它並沒有幫助。留給您的唯一選擇是使用功能強大但複雜且低級的 OpenGL API。
幸運的是,SFML 提供了一個圖形模組,它可以幫助您以比 OpenGL 更簡單的方式繪製 2D 實體。
繪圖窗口
要繪製圖形模組提供的實體,您必須使用專門的窗口類:sf::RenderWindow
. 該類派生自sf::Window
,並繼承其所有功能。您所了解的所有內容sf::Window
(創建、事件處理、控制幀率、與 OpenGL 混合等)也適用sf::RenderWindow
。
最重要的是,sf::RenderWindow
添加高級功能以幫助您輕鬆繪製事物。在本教程中,我們將關注其中兩個函數:clear
和draw
. 它們就像它們的名字所暗示的那樣簡單:clear
用選定的顏色清除整個窗口,並draw
繪製你傳遞給它的任何對象。
這是帶有渲染窗口的典型主循環的樣子:
#include <SFML/Graphics.hpp>
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(800, 600), "My window");
// run the program as long as the window is open
while (window.isOpen())
{
// 檢查自上次循環迭代以來觸發的所有窗口事件
sf::Event event;
while (window.pollEvent(event))
{
// "close requested" event: we close the window
if (event.type == sf::Event::Closed)
window.close();
}
// clear the window with black color
window.clear(sf::Color::Black);
// draw everything here...
// window.draw(...);
// end the current frame
window.display();
}
return 0;
}
在繪製任何內容之前調用clear
是強制性的,否則之前幀的內容將出現在您繪製的任何內容後面。唯一的例外是當您用繪製的內容覆蓋整個窗口時,不會繪製任何像素。在這種情況下,您可以避免調用clear
(儘管它不會對性能產生明顯影響)。
調用display
也是強制性的,它獲取自上次調用以來繪製的內容display
並將其顯示在窗口上。確實,事物不是直接繪製到窗口,而是繪製到隱藏的緩衝區。然後在您調用時將此緩衝區複製到窗口display
– 這稱為雙緩衝。
這種清除/繪製/顯示循環是繪製事物的唯一好方法。不要嘗試其他策略,例如保留前一幀的像素,「擦除」像素,或繪製一次並多次調用 display。由於雙緩衝,你會得到奇怪的結果。
現代圖形硬體和 API確實是為重複的清除/繪製/顯示循環而設計的,在主循環的每次迭代中,所有內容都會完全刷新。不要害怕每秒繪製 1000 個精靈 60 次,你遠遠低於電腦可以處理的數百萬個三角形。
我現在可以畫什麼?
現在您已經準備好繪製一個主循環,讓我們看看您可以在那裡實際繪製什麼以及如何繪製。
SFML 提供了四種可繪製實體:其中三種可供使用(精靈、文本和形狀),最後一種是幫助您創建自己的可繪製實體(頂點數組)的構建塊。
儘管它們具有一些共同的屬性,但這些實體中的每一個都有自己的細微差別,因此在專門的教程(下文)中進行了解釋:
準備程式
#include <SFML/Graphics.hpp>
#include <bits/stdc++.h>
int main() {
sf::RenderWindow window(sf::VideoMode(700, 500), "title");
while (window.isOpen()) {
sf::Event event{};
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
std::cout << "success exit" << std::endl;
window.close();
break;
default:
break;
}
}
window.clear();
window.display();
}
return 0;
}
精靈圖和紋理
什麼是精靈圖
CSS Sprites通常被稱為css精靈圖, 在中國也被意譯為css圖片整合和css貼圖定位,也有人稱他為雪碧圖。 就是將導航的背景圖,按鈕的背景圖等有規則的合併成一張背景圖,即多張圖合併為一張整圖, 然後再利用background-position進行背景圖定位的一種技術
(下面都翻譯成精靈)
辭彙
大多數人(如果不是全部)已經熟悉這兩個非常常見的對象,所以讓我們非常簡要地定義它們。
紋理是影像。但我們稱其為「紋理」,因為它具有非常特殊的作用:被映射到 2D 實體。
精靈只不過是一個帶紋理的矩形。
好的,這很簡短,但如果您真的不了解精靈和紋理是什麼,那麼您會在 Wikipedia 上找到更好的描述。
載入紋理
在創建任何精靈之前,我們需要一個有效的紋理。令人驚訝的是,在 SFML 中封裝紋理的類是sf::Texture
. 由於紋理的唯一作用是載入和映射到圖形實體,因此幾乎所有的功能都是關於載入和更新它。
載入紋理的最常見方法是從磁碟上的影像文件,這是通過loadFromFile
函數完成的。
sf::Texture texture;
if (!texture.loadFromFile("image.png"))
{
// error...
}
該loadFromFile
功能有時會在沒有明顯原因的情況下失敗。首先,檢查 SFML 列印到標準輸出的錯誤消息(檢查控制台)。如果消息無法打開文件,請確保工作目錄(任何文件路徑都將被解釋為相對的目錄)是您認為的:當您從桌面環境運行應用程式時,工作目錄是可執行文件夾。但是,當您從 IDE(Visual Studio、Code::Blocks、…)啟動程式時,有時可能會將工作目錄設置為項目目錄。這通常可以在項目設置中很容易地更改。(在Clion中是從可執行文件為起點的)
您還可以從記憶體 ( loadFromMemory
)、自定義輸入流( loadFromStream
) 或已載入的影像( ) 載入影像文件loadFromImage
。後者從 載入紋理sf::Image
,這是一個實用程式類,可幫助存儲和操作影像數據(修改像素,創建透明度通道等)。留在系統記憶體中的像素sf::Image
,確保對它們的操作將儘可能快,與駐留在影片記憶體中的紋理像素形成對比,因此檢索或更新緩慢但繪製速度非常快。
SFML 支援最常見的影像文件格式。API 文檔中提供了完整列表。
所有這些載入函數都有一個可選參數,如果你想載入影像的一小部分,可以使用它。
// load a 32x32 rectangle that starts at (10, 10)
if (!texture.loadFromFile("image.png", sf::IntRect(10, 10, 32, 32)))
{
// error...
}
該類sf::IntRect
是一個表示矩形的簡單實用程式類型。它的構造函數獲取左上角的坐標和矩形的大小。
如果您不想從影像載入紋理,而是想直接從像素數組更新它,您可以將其創建為空並稍後更新:
// create an empty 200x200 texture
if (!texture.create(200, 200))
{
// error...
}
請注意,此時紋理的內容是未定義的。
要更新現有紋理的像素,您必須使用該update
函數。它具有多種數據源的重載:
// update a texture from an array of pixels
sf::Uint8* pixels = new sf::Uint8[width * height * 4]; // * 4 because pixels have 4 components (RGBA)
...
texture.update(pixels);
// update a texture from a sf::Image
sf::Image image;
...
texture.update(image);
// update the texture from the current contents of the window
sf::RenderWindow window;
...
texture.update(window);
這些示例都假設源與紋理大小相同。如果不是這種情況,即如果您只想更新紋理的一部分,您可以指定要更新的子矩形的坐標。您可以參考文檔以獲取更多詳細資訊。
此外,紋理有兩個屬性可以改變它的渲染方式。
第一個屬性允許平滑紋理。平滑紋理使像素邊界不那麼明顯(但影像更模糊),如果放大它可能是可取的。
texture.setSmooth(true);
由於對紋理中相鄰像素的取樣也進行了平滑處理,因此可能會導致不希望的副作用,即在選定紋理區域之外考慮像素。當您的精靈位於非整數坐標時,可能會發生這種情況。
第二個屬性允許紋理在單個精靈中重複平鋪。
texture.setRepeated(true);
這僅在您的精靈配置為顯示大於紋理的矩形時才有效,否則此屬性無效。
好的,我現在可以擁有我的精靈了嗎?
是的,你現在可以創建你的精靈了。
sf::Sprite sprite;
sprite.setTexture(texture);
……最後畫出來。
// inside the main loop, between window.clear() and window.display()
window.draw(sprite);
如果你不想讓你的精靈使用整個紋理,你可以設置它的紋理矩形。
sprite.setTextureRect(sf::IntRect(10, 10, 32, 32));
您還可以更改精靈的顏色。您設置的顏色會隨著精靈的紋理進行調製(相乘)。這也可以用來改變精靈的全局透明度(alpha)。
sprite.setColor(sf::Color(0, 255, 0)); // green
sprite.setColor(sf::Color(255, 255, 255, 128)); // half transparent
這些精靈都使用相同的紋理,但顏色不同:
精靈也可以變換:它們有一個位置、一個方向和一個比例。
// position
sprite.setPosition(sf::Vector2f(10.f, 50.f)); // absolute position
sprite.move(sf::Vector2f(5.f, 10.f)); // offset relative to the current position
// rotation
sprite.setRotation(90.f); // absolute angle
sprite.rotate(15.f); // offset relative to the current angle
// scale
sprite.setScale(sf::Vector2f(0.5f, 2.f)); // absolute scale factor
sprite.scale(sf::Vector2f(1.5f, 3.f)); // factor relative to the current scale
默認情況下,這三個變換的原點是精靈的左上角。如果您想將原點設置為不同的點(例如精靈的中心,或另一個角),您可以使用該setOrigin
功能。
sprite.setOrigin(sf::Vector2f(25.f, 25.f));
由於轉換函數對所有 SFML 實體都是通用的,因此在單獨的教程中對它們進行了說明: 轉換實體。
白方塊問題
您成功載入了紋理,正確構建了精靈,並且……您現在在螢幕上看到的只是一個白色方塊。發生了什麼?
這是一個常見的錯誤。當您設置精靈的紋理時,它在內部所做的只是存儲指向紋理實例的指針。因此,如果紋理被破壞或移動到記憶體中的其他位置,則精靈最終會得到一個無效的紋理指針。
編寫此類函數時會出現此問題:
sf::Sprite loadSprite(std::string filename)
{
sf::Texture texture;
texture.loadFromFile(filename);
return sf::Sprite(texture);
} // error: the texture is destroyed here
您必須正確管理紋理的生命周期,並確保它們在被任何精靈使用時一直存在。
使用儘可能少的紋理的重要性
使用儘可能少的紋理是一個好策略,原因很簡單:更改當前紋理對於顯示卡來說是一項昂貴的操作。繪製許多使用相同紋理的精靈將產生最佳性能。
此外,使用單個紋理允許您將靜態幾何體組合到單個實體中(每次調用只能使用一個紋理draw
),這將比一組許多實體更快地繪製。批處理靜態幾何體涉及其他類,因此超出了本教程的範圍,有關更多詳細資訊,請參閱頂點數組教程。
在創建動畫表或圖塊集時請記住這一點:儘可能少地使用紋理。
在 OpenGL 程式碼中使用 sf::Texture
如果您使用的是 OpenGL 而不是 SFML 的圖形實體,您仍然可以將sf::Texture
其用作 OpenGL 紋理對象的包裝器,並將其與其餘的 OpenGL 程式碼一起使用。
要綁定sf::Texture
繪圖(基本上glBindTexture
),您調用bind
靜態函數:
sf::Texture texture;
...
// bind the texture
sf::Texture::bind(&texture);
// draw your textured OpenGL entity here...
// bind no texture
sf::Texture::bind(NULL);
小總結程式
在根目錄下access/pi'c
中放了一張圖片loading,png
程式將不停的旋轉pic該圖片
#include <SFML/Graphics.hpp>
//#include <GLFW/glfw3.h>
#include <bits/stdc++.h>
int main() {
sf::RenderWindow window(sf::VideoMode(700, 500), "title");
window.setVerticalSyncEnabled(true);
sf::Texture texture;
if (!texture.loadFromFile("../access/pic/loading.png")) {
std::cerr << "load texture failed!" << std::endl;
}
sf::Sprite sprite;
sprite.setTexture(texture);
sprite.scale((float) window.getSize().x / (float) texture.getSize().x,
(float) window.getSize().y / (float) texture.getSize().y);
sprite.setOrigin((float) window.getSize().x / 2.f,
(float) window.getSize().y / 2.f);
sprite.setPosition((float) window.getSize().x / 2.f,
(float) window.getSize().y / 2.f);
while (window.isOpen()) {
sf::Event event{};
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
std::cout << "success exit" << std::endl;
window.close();
break;
default:
break;
}
}
window.clear();
sprite.rotate(1.f);
window.draw(sprite);
window.display();
}
return 0;
}
離屏繪圖
SFML 還提供了一種繪製到紋理而不是直接繪製到窗口的方法。為此,請使用 sf::RenderTexture
而不是 sf::RenderWindow
。它具有相同的繪圖功能,繼承自它們的共同基礎:sf::RenderTarget
.
// create a 500x500 render-texture
sf::RenderTexture renderTexture;
if (!renderTexture.create(500, 500))
{
// error...
}
// drawing uses the same functions
renderTexture.clear();
renderTexture.draw(sprite); // or any other drawable
renderTexture.display();
// get the target texture (where the stuff has been drawn)
const sf::Texture& texture = renderTexture.getTexture();
// draw it to the window
sf::Sprite sprite(texture);
window.draw(sprite);
該getTexture
函數返回一個只讀紋理,這意味著您只能使用它,不能修改它。如果您需要在使用前對其進行修改,您可以將其複製到您自己的sf::Texture
實例中並進行修改。
sf::RenderTexture
還具有與處理視圖和 OpenGL 相同的功能sf::RenderWindow
(有關詳細資訊,請參閱相應的教程)。如果您使用 OpenGL 繪製到渲染紋理,您可以使用函數的第三個可選參數請求創建深度緩衝區create
。
renderTexture.create(500, 500, true); // enable depth buffer
從執行緒中繪製*(此處用std::thread更好)
SFML 支援多執行緒繪圖,你甚至不需要做任何事情來讓它工作。唯一要記住的是在另一個執行緒中使用它之前停用一個窗口。這是因為一個窗口(更準確地說是它的 OpenGL 上下文)不能同時在多個執行緒中處於活動狀態。
void renderingThread(sf::RenderWindow* window)
{
// activate the window's context
window->setActive(true);
// the rendering loop
while (window->isOpen())
{
// draw...
// end the current frame
window->display();
}
}
int main()
{
// create the window (remember: it's safer to create it in the main thread due to OS limitations)
sf::RenderWindow window(sf::VideoMode(800, 600), "OpenGL");
// deactivate its OpenGL context
window.setActive(false);
// launch the rendering thread
sf::Thread thread(&renderingThread, &window);
thread.launch();
// the event/logic/whatever loop
while (window.isOpen())
{
...
}
return 0;
}
如您所見,您甚至不需要在渲染執行緒中激活窗口,SFML 會在需要時自動為您完成。
請記住始終在主執行緒中創建窗口並處理其事件,以獲得最大的可移植性。
文字和字體
載入字體
在繪製任何文本之前,您需要有一個可用的字體,就像任何其他列印文本的程式一樣。字體封裝在sf::Font
該類中,該類提供三個主要功能:載入字體、從中獲取字形(即視覺字元)以及讀取其屬性。在一個典型的程式中,你只需要使用第一個特性,載入字體,所以讓我們首先關注它。
載入字體最常見的方法是從磁碟上的文件中載入,這是通過loadFromFile
函數完成的。
sf::Font font;
if (!font.loadFromFile("arial.ttf"))
{
// error...
}
請注意,SFML 不會自動載入您的系統字體,即font.loadFromFile("Courier New")
不會工作。首先,因為 SFML 需要文件名,而不是字體名稱,其次,因為 SFML沒有對系統字體文件夾的神奇訪問許可權。如果要載入字體,則需要在應用程式中包含字體文件,就像所有其他資源(影像、聲音等)一樣。
在windows下 C:\Windows\Fonts
中就有字體文件 可以將其複製到 access/font
下
推薦自然是 consola
字體啦
sf::Font font;
if (!font.loadFromFile("../access/font/consola.ttf")) {
std::cerr << "load texture failed!" << std::endl;
}
該loadFromFile
功能有時會在沒有明顯原因的情況下失敗。首先,檢查 SFML 列印到標準輸出的錯誤消息(檢查控制台)。如果消息無法打開文件,請確保工作目錄(任何文件路徑都將被解釋為相對的目錄)是您認為的:當您從桌面環境運行應用程式時,工作目錄是可執行文件夾。但是,當您從IDE啟動程式時,有時可能會將工作目錄設置為項目目錄。這通常可以在項目設置中很容易地更改。
您還可以從記憶體 ( loadFromMemory
) 或自定義輸入流( loadFromStream
) 載入字體文件。
SFML 支援最常見的字體格式。API 文檔中提供了完整列表。
這就是你需要做的。載入字體後,您可以開始繪製文本。
繪圖文本
要繪製文本,您將使用sf::Text
該類。使用非常簡單:
sf::Text text;
// select the font
text.setFont(font); // font is a sf::Font
// set the string to display
text.setString("Hello world");
// set the character size
text.setCharacterSize(24); // in pixels, not points!
// set the color
text.setFillColor(sf::Color::Red);
// set the text style
text.setStyle(sf::Text::Bold | sf::Text::Underlined);
...
// inside the main loop, between window.clear() and window.display()
window.draw(text);
文本也可以轉換:它們具有位置、方向和比例。涉及的功能與 sf::Sprite
類和其他 SFML 實體的功能相同。它們在 轉換實體教程中進行了解釋。
小總結程式
在圖片的基礎上加了一行loading…
#include <SFML/Graphics.hpp>
//#include <GLFW/glfw3.h>
#include <bits/stdc++.h>
int main() {
sf::RenderWindow window(sf::VideoMode(700, 500), "title");
window.setVerticalSyncEnabled(true);
sf::Texture texture;
if (!texture.loadFromFile("../access/pic/loading.png")) {
std::cerr << "load texture failed!" << std::endl;
}
sf::Sprite sprite;
sprite.setTexture(texture);
sprite.scale((float) window.getSize().x / (float) texture.getSize().x,
(float) window.getSize().y / (float) texture.getSize().y);
sprite.setOrigin((float) window.getSize().x / 2.f,
(float) window.getSize().y / 2.f);
sprite.setPosition((float) window.getSize().x / 2.f,
(float) window.getSize().y / 2.f);
sf::Font font;
if (!font.loadFromFile("../access/font/consola.ttf")) {
std::cerr << "load texture failed!" << std::endl;
}
sf::Text text;
text.setFont(font);
text.setString("loading...");
text.setCharacterSize(50);
text.setFillColor(sf::Color::Blue);
text.setPosition((float) window.getSize().x / 2.f,
(float) window.getSize().y / 2.f);
while (window.isOpen()) {
sf::Event event{};
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
std::cout << "success exit" << std::endl;
window.close();
break;
default:
break;
}
}
window.clear();
sprite.rotate(1.f);
window.draw(sprite);
window.draw(text);
window.display();
}
return 0;
}
如何避免非 ASCII 字元的問題?
正確處理非 ASCII 字元(例如重音歐洲字元、阿拉伯字元或中文字元)可能很棘手。它需要對解釋和繪製文本過程中涉及的各種編碼有很好的理解。為了避免這些編碼的困擾,有一個簡單的解決方案:使用寬文本字元串。
text.setString(L"יטאח");
正是字元串前面的這個簡單的「L」前綴通過告訴編譯器生成一個寬字元串來使其工作。寬字元串在 C++ 中是一種奇怪的野獸:標準沒有說明它們的大小(16 位?32 位?),也沒有說明它們使用的編碼(UTF-16?UTF-32?)。但是我們知道,在大多數平台上(如果不是全部),它們都會生成 Unicode 字元串,並且 SFML 知道如何正確處理它們。
請注意,C++11 標準支援新的字元類型和前綴來構建 UTF-8、UTF-16 和 UTF-32 字元串文字,但 SFML 還不支援它們。
這似乎很明顯,但您還必須確保您使用的字體包含您要繪製的字元。實際上,字體並不包含所有可能字元的字形(Unicode 標準中有超過 100000 個字形!),例如,阿拉伯字體將無法顯示日文文本。
製作自己的文本類
如果sf::Text
太有限,或者如果你想用預渲染的字形做其他事情,sf::Font
提供你需要的一切。
您可以檢索包含特定大小的所有預渲染字形的紋理:
const sf::Texture& texture = font.getTexture(characterSize);
需要注意的是,字形會在請求時添加到紋理中。字元太多(記住,超過 100000 個),載入字體時無法全部生成。相反,它們會在您調用getGlyph
函數時即時呈現(見下文)。
要對字體紋理做一些有意義的事情,您必須獲取其中包含的字形的紋理坐標:
sf::Glyph glyph = font.getGlyph(character, characterSize, bold);
character
是要獲取其字形的字元的 UTF-32 程式碼。您還必須指定字元大小,以及是否需要粗體或常規版本的字形。
該sf::Glyph
結構包含三個成員:
textureRect
包含紋理內字形的紋理坐標bounds
包含字形的邊界矩形,這有助於相對於文本的基準線定位它advance
是用於獲取文本中下一個字形的起始位置的水平偏移量
您還可以獲得一些字體的其他指標,例如兩個字元之間的字距或行間距(總是針對特定字元大小):
int lineSpacing = font.getLineSpacing(characterSize);
int kerning = font.getKerning(character1, character2, characterSize);
形狀
介紹
SFML 提供了一組表示簡單形狀實體的類。每種類型的形狀都是一個單獨的類,但它們都派生自同一個基類,因此它們可以訪問相同的公共特徵子集。然後每個類添加自己的細節:圓形類的半徑屬性,矩形類的大小,多邊形類的點等。
常見的形狀屬性
變換(位置、旋轉、縮放)
這些屬性對所有 SFML 圖形類都是通用的,因此在單獨的教程中對它們進行了說明: 轉換實體。
顏色
形狀的基本屬性之一是它的顏色。您可以使用setFillColor
功能進行更改。
sf::CircleShape shape(50.f);
// set the shape color to green
shape.setFillColor(sf::Color(100, 250, 50));
輪廓
形狀可以有輪廓。您可以使用setOutlineThickness
和setOutlineColor
功能設置輪廓的粗細和顏色。
sf::CircleShape shape(50.f);
shape.setFillColor(sf::Color(150, 50, 250));
// set a 10-pixel wide orange outline
shape.setOutlineThickness(10.f);
shape.setOutlineColor(sf::Color(250, 150, 100));
默認情況下,輪廓從形狀向外突出(例如,如果您有一個半徑為 10 且輪廓厚度為 5 的圓,則該圓的總半徑將為 15)。您可以通過設置負厚度來使其向形狀中心拉伸。
要禁用輪廓,請將其粗細設置為 0。如果只需要輪廓,可以將填充顏色設置為透明sf::Color::Transparent
。
紋理
形狀也可以被紋理化,就像精靈一樣。要指定要映射到形狀的紋理的一部分,您必須使用該setTextureRect
函數。它需要紋理矩形映射到形狀的邊界矩形。這種方法沒有提供最大的靈活性,但它比單獨設置形狀每個點的紋理坐標要容易得多。
sf::CircleShape shape(50);
// map a 100x100 textured rectangle to the shape
shape.setTexture(&texture); // texture is a sf::Texture
shape.setTextureRect(sf::IntRect(10, 10, 100, 100));
請注意,輪廓沒有紋理。
重要的是要知道紋理是用形狀的填充顏色調製(相乘)的。如果其填充顏色為 sf::Color::White
,則紋理將顯示為未修改。
要禁用紋理,請調用setTexture(NULL)
.
繪製形狀
繪製形狀與繪製任何其他 SFML 實體一樣簡單:
window.draw(shape);
內置形狀類型
矩形
要繪製矩形,您可以使用sf::RectangleShape
該類。它有一個屬性:矩形的大小。
// define a 120x50 rectangle
sf::RectangleShape rectangle(sf::Vector2f(120.f, 50.f));
// change the size to 100x100
rectangle.setSize(sf::Vector2f(100.f, 100.f));
圓
圓圈由sf::CircleShape
類表示。它有兩個屬性:半徑和邊數。邊數是一個可選屬性,它可以讓你調整圓的「品質」:圓必須用多邊的多邊形來近似(顯示卡無法直接畫出完美的圓),這個屬性定義了你的圓近似有多少邊。如果你畫小圓圈,你可能只需要幾個邊。如果你畫大圓圈,或者放大常規圓圈,你很可能需要更多的邊。
// define a circle with radius = 200
sf::CircleShape circle(200.f);
// change the radius to 40
circle.setRadius(40.f);
// change the number of sides (points) to 100
circle.setPointCount(100);
正多邊形
規則多邊形沒有專門的類,實際上您可以使用sf::CircleShape
該類表示具有任意數量邊的規則多邊形:由於圓形由具有許多邊的多邊形近似,因此您只需使用邊數即可獲得所需的多邊形。有 3 個點是三角形,有 4 個點是正方形,依此類推。
// define a triangle
sf::CircleShape triangle(80.f, 3);
// define a square
sf::CircleShape square(80.f, 4);
// define an octagon
sf::CircleShape octagon(80.f, 8);
凸形狀
該類sf::ConvexShape
是最後的形狀類:它允許您定義任何凸形。SFML 無法繪製凹形。如果您需要繪製凹形,則必須將其拆分為多個凸多邊形。
要構造一個凸形,您必須首先設置它應該具有的點數,然後定義這些點。
// create an empty shape
sf::ConvexShape convex;
// resize it to 5 points
convex.setPointCount(5);
// define the points
convex.setPoint(0, sf::Vector2f(0.f, 0.f));
convex.setPoint(1, sf::Vector2f(150.f, 10.f));
convex.setPoint(2, sf::Vector2f(120.f, 90.f));
convex.setPoint(3, sf::Vector2f(30.f, 100.f));
convex.setPoint(4, sf::Vector2f(0.f, 50.f));
定義點的順序非常重要。它們都必須按順時針或逆時針順序定義。如果您以不一致的順序定義它們,形狀將被錯誤地構造。
雖然sf::ConvexShape
名稱意味著它應該只用於表示凸多邊形,但它的要求稍微寬鬆一些。事實上,您的形狀必須滿足的唯一要求是,如果繼續繪製從重心到所有點的線,這些線必須按相同的順序繪製。你不允許「跳到前一條線後面」。在內部,凸多邊形是使用三角形扇形自動構造的,因此,如果您的形狀可由三角形扇形表示,因此如果您的形狀可以用三角形扇形表示,則可以使用sf::ConvexShape
。通過這個輕鬆的定義,您可以使用sf::ConvexShape
例如繪製星星。(左圖為三角形扇形 TriangleFan
)
線條
線條沒有形狀類。原因很簡單:如果你的線有粗細,它就是一個矩形。如果沒有,可以用線基元繪製。
線與粗細:
sf::RectangleShape line(sf::Vector2f(150.f, 5.f));
line.rotate(45.f);
沒有粗細的線:
sf::Vertex line[] =
{
sf::Vertex(sf::Vector2f(10.f, 10.f)),
sf::Vertex(sf::Vector2f(150.f, 150.f))
};
window.draw(line, 2, sf::Lines);
要了解有關頂點和基元的更多資訊,您可以閱讀有關頂點數組的教程。(下文)
自定義形狀類型
您可以使用自己的形狀類型擴展形狀類集。為此,您必須派生sf::Shape
並覆蓋兩個函數:
getPointCount
:返回形狀中的點數getPoint
: 返回形狀的一個點
每當形狀中的任何點發生更改時,您還必須調用update()
受保護的函數,以便通知基類並可以更新其內部幾何圖形。
這是自定義形狀類的完整示例:EllipseShape橢圓類。
class EllipseShape : public sf::Shape
{
public :
explicit EllipseShape(const sf::Vector2f& radius = sf::Vector2f(0.f, 0.f)) :
m_radius(radius)
{
update();
}
void setRadius(const sf::Vector2f& radius)
{
m_radius = radius;
update();
}
const sf::Vector2f& getRadius() const
{
return m_radius;
}
virtual std::size_t getPointCount() const
{
return 30; // fixed, but could be an attribute of the class if needed
}
virtual sf::Vector2f getPoint(std::size_t index) const
{
static const float pi = 3.141592654f;
float angle = index * 2 * pi / getPointCount() - pi / 2;
float x = std::cos(angle) * m_radius.x;
float y = std::sin(angle) * m_radius.y;
return sf::Vector2f(m_radius.x + x, m_radius.y + y);
}
private :
sf::Vector2f m_radius;
};
抗鋸齒形狀
沒有對單個形狀進行抗鋸齒的選項。要獲得抗鋸齒形狀(即邊緣平滑的形狀),您必須在創建窗口時全局啟用抗鋸齒,並具有sf::ContextSettings
結構的相應屬性。
sf::ContextSettings settings;
settings.antialiasingLevel = 8; //抗鋸齒級別
sf::RenderWindow window(sf::VideoMode(800, 600), "SFML shapes", sf::Style::Default, settings);
請記住,抗鋸齒的可用性取決於顯示卡:它可能不支援它,或者在驅動程式設置中強制禁用它。
使用頂點數組(Vertex Array)設計自己的實體
介紹
SFML 為最常見的 2D 實體提供了簡單的類。雖然可以從這些構建塊輕鬆創建更複雜的實體,但它並不總是最有效的解決方案。例如,如果您繪製大量精靈,您將很快達到顯示卡的極限。原因是性能在很大程度上取決於對draw
函數的調用次數。實際上,每個調用都涉及設置一組 OpenGL 狀態、重置矩陣、更改紋理等。即使只是繪製兩個三角形(一個精靈),所有這些都是必需的。這對於您的顯示卡來說遠非最佳:今天的 GPU 旨在處理大批量的三角形,通常是幾千到幾百萬。
為了填補這個空白,SFML 提供了一種底層機制來繪製事物:頂點數組。事實上,所有其他 SFML 類都在內部使用頂點數組。它們允許更靈活地定義 2D 實體,包含所需數量的三角形。它們甚至允許繪製點或線。
什麼是頂點,為什麼它們總是在數組中?
頂點是您可以操作的最小圖形實體。簡而言之,它是一個圖形點:自然它有一個 2D 位置 (x, y),還有一個顏色,還有一對紋理坐標。稍後我們將介紹這些屬性的作用。
單獨的頂點(頂點的複數)並沒有多大作用。它們總是分組為基元:點(1 個頂點)、線(2 個頂點)、三角形(3 個頂點)或四邊形(4 個頂點)。然後,您可以將多個基元組合在一起以創建實體的最終幾何形狀。
現在你明白為什麼我們總是談論頂點數組,而不僅僅是頂點。
一個簡單的頂點數組
現在讓我們來看看sf::Vertex
類。它只是一個包含三個公共成員的容器,除了其構造函數之外沒有任何功能。這些構造函數允許您從您關心的一組屬性中構造頂點——您並不總是需要為您的實體著色或紋理。
確實SFML包裝的十分舒服(openGL你是什麼魔鬼
// create a new vertex
sf::Vertex vertex;
// set its position
vertex.position = sf::Vector2f(10.f, 50.f);
// set its color
vertex.color = sf::Color::Red;
// set its texture coordinates
vertex.texCoords = sf::Vector2f(100.f, 100.f);
…或者,使用正確的構造函數:
參數為: 位置坐標 顏色 紋理坐標
sf::Vertex vertex(sf::Vector2f(10.f, 50.f), sf::Color::Red, sf::Vector2f(100.f, 100.f));
現在,讓我們定義一個基元。請記住,基元由多個頂點組成,因此我們需要一個頂點數組。SFML 為此提供了一個簡單的包裝器: sf::VertexArray
. 它提供數組的語義(類似於std::vector
),並且還存儲其頂點定義的基元類型。
使用VertexArray
// create an array of 3 vertices that define a triangle primitive
sf::VertexArray triangle(sf::Triangles, 3);
// define the position of the triangle's points
triangle[0].position = sf::Vector2f(10.f, 10.f);
triangle[1].position = sf::Vector2f(100.f, 10.f);
triangle[2].position = sf::Vector2f(100.f, 100.f);
// define the color of the triangle's points
triangle[0].color = sf::Color::Red;
triangle[1].color = sf::Color::Blue;
triangle[2].color = sf::Color::Green;
// no texture coordinates here, we'll see that later
你的三角形已經準備好了,你現在可以畫它了。draw
通過使用以下函數 ,可以像繪製任何其他 SFML 實體一樣繪製頂點數組:
window.draw(triangle);
您可以看到頂點的顏色被插值以填充基元。這是創建漸變的好方法。
請注意,您不必使用sf::VertexArray
該類。它只是為了方便而定義的,它只不過是 std::vector<sf::Vertex>
和 sf::PrimitiveType
。如果您需要更大的靈活性或靜態數組,您可以使用自己的存儲。然後,您必須使用函數的重載,該draw
函數接受指向頂點、頂點計數和原始類型的指針。
std::vector<sf::Vertex> vertices;
vertices.push_back(sf::Vertex(...));
...
window.draw(&vertices[0], vertices.size(), sf::Triangles);
sf::Vertex vertices[2] =
{
sf::Vertex(...),
sf::Vertex(...)
};
window.draw(vertices, 2, sf::Lines);
原始類型
讓我們暫停一下,看看您可以創建什麼樣的基元。如上所述,您可以定義最基本的 2D 基元:點、線、三角形和四邊形(四邊形只是為了方便而存在,顯示卡在內部將其分成兩個三角形)。這些基元類型也有「鏈式」變體,允許在兩個連續基元之間共享頂點。這可能很有用,因為連續的基元通常以某種方式連接。
讓我們看一下完整列表:
原始類型 | 描述 | 例子 |
---|---|---|
sf::Points |
一組不相連的點。這些點沒有厚度:無論當前的變換和視圖如何,它們將始終佔據一個像素。 | ![]() |
sf::Lines |
一組不相連的線。這些線沒有粗細:無論當前的變換和視圖如何,它們總是一個像素寬。 | ![]() |
sf::LineStrip |
一組連接的線。一行的結束頂點用作下一行的開始頂點。 | ![]() |
sf::Triangles |
一組不連通的三角形。 | ![]() |
sf::TriangleStrip |
一組相連的三角形。每個三角形與下一個共享其最後兩個頂點。 | ![]() |
sf::TriangleFan |
一組連接到中心點的三角形。第一個頂點是中心,然後每個新頂點定義一個新三角形,使用中心和前一個頂點。 | ![]() |
sf::Quads |
一組不相連的四邊形。每個四邊形的 4 個點必須以順時針或逆時針順序定義一致。 | ![]() |
基元類型定義在PrimitiveType.hpp中 定義如下
enum PrimitiveType
{
Points, ///< List of individual points
Lines, ///< List of individual lines
LineStrip, ///< List of connected lines, a point uses the previous point to form a line
Triangles, ///< List of individual triangles
TriangleStrip, ///< List of connected triangles, a point uses the two previous points to form a triangle
TriangleFan, ///< List of connected triangles, a point uses the common center and the previous point to form a triangle
Quads, ///< List of individual quads (deprecated, don't work with OpenGL ES)
// Deprecated names
LinesStrip = LineStrip, ///< \deprecated Use LineStrip instead
TrianglesStrip = TriangleStrip, ///< \deprecated Use TriangleStrip instead
TrianglesFan = TriangleFan ///< \deprecated Use TriangleFan instead
};
紋理
像其他 SFML 實體一樣,頂點數組也可以被紋理化。為此,您需要操作texCoords
頂點的屬性。此屬性定義紋理的哪個像素映射到頂點。
// create a quad(四邊形)
sf::VertexArray quad(sf::Quads, 4);
// define it as a rectangle(長方形), located at (10, 10) and with size 100x100
quad[0].position = sf::Vector2f(10.f, 10.f);
quad[1].position = sf::Vector2f(110.f, 10.f);
quad[2].position = sf::Vector2f(110.f, 110.f);
quad[3].position = sf::Vector2f(10.f, 110.f);
// define its texture area to be a 25x50 rectangle starting at (0, 0)
quad[0].texCoords = sf::Vector2f(0.f, 0.f);
quad[1].texCoords = sf::Vector2f(25.f, 0.f);
quad[2].texCoords = sf::Vector2f(25.f, 50.f);
quad[3].texCoords = sf::Vector2f(0.f, 50.f);
紋理坐標以像素為單位定義(就像textureRect
精靈和形狀的一樣)。它們沒有像習慣於 OpenGL 編程的人所期望的那樣標準化(介於 0 和 1 之間)。
頂點數組是低級實體,它們只處理幾何圖形,不存儲紋理等附加屬性。要使用紋理繪製頂點數組,必須將其直接傳遞給draw
函數:
sf::VertexArray vertices;
sf::Texture texture;
...
window.draw(vertices, &texture);
這是簡短版本,如果您需要傳遞其他渲染狀態(如混合模式或變換),您可以使用帶有 sf::RenderStates
對象的顯式版本:
sf::VertexArray vertices;
sf::Texture texture;
...
sf::RenderStates states;
states.texture = &texture;
window.draw(vertices, states);
轉換頂點數組
變換類似於紋理。變換不存儲在頂點數組中,您必須將其傳遞給draw
函數。
sf::VertexArray vertices;
sf::Transform transform;
...
window.draw(vertices, transform);
或者,如果您需要傳遞其他渲染狀態:
sf::VertexArray vertices;
sf::Transform transform;
...
sf::RenderStates states;
states.transform = transform;
window.draw(vertices, states);
為甚麼draw傳什麼都可以呢 萬一我又想傳紋理又要變換怎麼辦? 來深入看看這是怎麼回事
此處調用的draw函數的聲明
////////////////////////////////////////////////////////////
/// \brief Draw a drawable object to the render target
///
/// \param drawable Object to draw
/// \param states Render states to use for drawing
///
////////////////////////////////////////////////////////////
void draw(const Drawable& drawable, const RenderStates& states = RenderStates::Default);
從draw的聲明中我們可以看出 此處應該調用的是RenderState的隱式構造函數
再往下翻到RenderState所有的構造函數
RenderStates();
RenderStates(const BlendMode& theBlendMode);
RenderStates(const Transform& theTransform);
RenderStates(const Texture* theTexture);
RenderStates(const Shader* theShader);
RenderStates(const BlendMode& theBlendMode, const Transform& theTransform,
const Texture* theTexture, const Shader* theShader);
不出所料 就是隱式構造 所以當我們需要複雜操作的時候要先對預先準備好的renderstate一通操作再傳進去
sf::Transform transform;
sf::RenderStates renderStates(&texture);//調用texture為參數的構造函數
...
//在窗口的循環中
renderStates.transform = transform;
...
window.draw(entity, renderStates);
創建類似 SFML 的實體
既然您知道了如何定義自己的紋理/著色/變換實體(textured/colored/transformed entity),那麼將其封裝在SFML樣式的類中不是很好嗎?
幸運的是,SFML通過提供sf::Drawable
和sf::Transformable
基類使這一點變得容易。這兩個類是內置SFML實體sf::Sprite
、sf::Text
和sf::Shape
的父類。
sf::Drawable
是一個介面:它聲明了一個純虛函數,沒有成員也沒有具體函數。繼承自sf::Drawable
允許您以與 SFML 類相同的方式繪製類的實例:
class MyEntity : public sf::Drawable
{
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
};
MyEntity entity;
window.draw(entity); // internally calls entity.draw
請注意,這樣做不是強制性的,您也可以在您的類中使用類似的draw
函數,然後簡單地使用entity.draw(window)
(這個會在window.draw(entity)
非vertexArray版本中自動調用)。但另一種方式,sf::Drawable
作為基類,更好,更一致。這也意味著,如果您計劃存儲可繪製對象的數組,則無需任何額外努力即可完成,因為所有可繪製對象(SFML 和您的)都派生自同一個類。
另一個基類sf::Transformable
沒有虛函數。從它繼承會自動將相同的轉換函數添加到您的類中,就像其他 SFML 類( setRotation
、move
、 scale
、setPosition
…)一樣。
使用這兩個基類和一個頂點數組(在本例中,我們還將添加一個紋理),這是典型的類似 SFML 的圖形類的樣子:
class MyEntity : public sf::Drawable, public sf::Transformable
{
public:
// add functions to play with the entity's geometry / colors / texturing...
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the entity's transform -- combine it with the one that was passed by the caller
states.transform *= getTransform(); // getTransform() is defined by sf::Transformable
//將變換矩陣相乘
// apply the texture
states.texture = &m_texture;
// you may also override states.shader or states.blendMode if you want
// draw the vertex array
target.draw(m_vertices, states);
}
sf::VertexArray m_vertices;
sf::Texture m_texture;
};
然後,您可以像使用內置 SFML 類一樣使用此類:
MyEntity entity;
// you can transform it
entity.setPosition(10.f, 50.f);
entity.setRotation(45.f);
// you can draw it
window.draw(entity);
示例:平鋪地圖
有了我們上面看到的內容,讓我們創建一個封裝瓦片地圖的類。整個地圖將包含在一個頂點數組中,因此繪製起來會非常快。請注意,只有當整個圖塊集可以適合單個紋理時,我們才能應用此策略。否則,我們將不得不為每個紋理使用至少一個頂點數組。
class TileMap : public sf::Drawable, public sf::Transformable
{
public:
bool load(const std::string& tileset, sf::Vector2u tileSize, const int* tiles, unsigned int width, unsigned int height)
{
// load the tileset texture
if (!m_tileset.loadFromFile(tileset))
return false;
// resize the vertex array to fit the level size
m_vertices.setPrimitiveType(sf::Quads);
m_vertices.resize(width * height * 4);
// populate the vertex array, with one quad per tile
for (unsigned int i = 0; i < width; ++i)
for (unsigned int j = 0; j < height; ++j)
{
// get the current tile number
int tileNumber = tiles[i + j * width];
// find its position in the tileset texture
//in this 1D tileset example tv == 0 always ture
int tu = tileNumber % (m_tileset.getSize().x / tileSize.x);// tile number % kinds of tiles
int tv = tileNumber / (m_tileset.getSize().x / tileSize.x);// tile number / kinds of tiles
// get a pointer to the current tile's quad
sf::Vertex* quad = &m_vertices[(i + j * width) * 4];
// define its 4 corners
quad[0].position = sf::Vector2f(i * tileSize.x, j * tileSize.y);
quad[1].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y);
quad[2].position = sf::Vector2f((i + 1) * tileSize.x, (j + 1) * tileSize.y);
quad[3].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y);
// define its 4 texture coordinates
quad[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y);
quad[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
quad[2].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y);
quad[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);
}
return true;
}
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the transform
states.transform *= getTransform();
// apply the tileset texture
states.texture = &m_tileset;
// draw the vertex array
target.draw(m_vertices, states);
}
sf::VertexArray m_vertices;
sf::Texture m_tileset;
};
現在,使用它的應用程式:
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(512, 256), "Tilemap");
// define the level with an array of tile indices
const int level[] =
{
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3,
0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0,
0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0,
0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0,
2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1,
0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1,
};
// create the tilemap from the level definition
TileMap map;
if (!map.load("tileset.png", sf::Vector2u(32, 32), level, 16, 8))
return -1;
// run the main loop
while (window.isOpen())
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
// draw the map
window.clear();
window.draw(map);
window.display();
}
return 0;
}
您可以在此處下載用於此圖塊地圖示例的圖塊集。
另 我自己敲的多文件版本 比示例語法規範化了一點
TileMap.h
//
// Created by Satar07 on 2022-05-22.
//
#ifndef GM_TILEMAP_H
#define GM_TILEMAP_H
#include <SFML/Graphics.hpp>
class TileMap : public sf::Drawable, public sf::Transform {
public:
bool load(const std::string &tile, sf::Vector2u tileSize,
const int *tiles, unsigned int width, unsigned int height);
private:
void draw(sf::RenderTarget &target, sf::RenderStates states) const override;
sf::VertexArray m_vertices;
sf::Texture m_tileset;
};
#endif //GM_TILEMAP_H
TileMap.cpp
//
// Created by Satar07 on 2022-05-22.
//
#include "TileMap.h"
bool TileMap::load(const std::string &tileset, sf::Vector2u tileSize, const int *tiles, unsigned int width,
unsigned int height) {
if (!m_tileset.loadFromFile(tileset))
return false;
m_vertices.setPrimitiveType(sf::Quads);
m_vertices.resize(width * height * 4);
// populate the vertex array, with one quad per tile
for (unsigned int i = 0; i < width; i++) {
for (unsigned int j = 0; j < height; j++) {
//get the current tile number
int tileNumber = tiles[j * width + i];
//find its position in the tileset texture
//in this 1D tileset example ty == 0 always ture
unsigned tx = tileNumber % (m_tileset.getSize().x / tileSize.x); // tile number % kinds of tiles
unsigned ty = tileNumber / (m_tileset.getSize().x / tileSize.x); // tile number / kinds of tiles
//get a pointer to the current tile's quad
sf::Vertex *quad = &m_vertices[(j * width + i) * 4];
//define its 4 corners
quad[0].position = sf::Vector2f(float(i * tileSize.x), float(j * tileSize.y));
quad[1].position = sf::Vector2f(float((i + 1) * tileSize.x), float(j * tileSize.y));
quad[2].position = sf::Vector2f(float((i + 1) * tileSize.x), float((j + 1) * tileSize.y));
quad[3].position = sf::Vector2f(float(i * tileSize.x), float((j + 1) * tileSize.y));
//define its 4 texture coordinates
quad[0].texCoords = sf::Vector2f(float(tx * tileSize.x), float(ty * tileSize.y));
quad[1].texCoords = sf::Vector2f(float((tx + 1) * tileSize.x), float(ty * tileSize.y));
quad[2].texCoords = sf::Vector2f(float((tx + 1) * tileSize.x), float((ty + 1) * tileSize.y));
quad[3].texCoords = sf::Vector2f(float(tx * tileSize.x), float((ty + 1) * tileSize.y));
}
}
return true;
}
void TileMap::draw(sf::RenderTarget &target, sf::RenderStates states) const {
//apply the transform
states.transform *= states.transform;
//apply the tileset texture
states.texture = &m_tileset;
//draw the vertex array
target.draw(m_vertices, states);
}
main.cpp
//
// Created by Satar07 on 2022-05-22.
//
#include <SFML/Graphics.hpp>
#include <bits/stdc++.h>
#include "TileMap.h"
// define the level with an array of tile indices
const int level[] = {
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3,
0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0,
0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0,
0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0,
2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1,
0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1,
};
int main() {
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
sf::RenderWindow window(sf::VideoMode(512, 256), "title", sf::Style::Default, settings);
window.setVerticalSyncEnabled(true);
//create the tilemap from the level definition
TileMap map;
if (!map.load("../access/pic/graphics-vertex-array-tilemap-tileset.png",
sf::Vector2u(32, 32), level, 16, 8)) {
std::cerr << "fail to load tile map" << std::endl;
return -1;
}
while (window.isOpen()) {
sf::Event event{};
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
std::cout << "success exit" << std::endl;
window.close();
break;
default:
break;
}
}
window.clear();
window.draw(map);
window.display();
}
return 0;
}
示例:粒子系統
第二個示例實現了另一個常見的實體:粒子系統。這個很簡單,沒有紋理,參數盡量少。它演示了sf::Points
原始類型與每幀都變化的動態頂點數組的使用。
class ParticleSystem : public sf::Drawable, public sf::Transformable
{
public:
ParticleSystem(unsigned int count) :
m_particles(count),
m_vertices(sf::Points, count),
m_lifetime(sf::seconds(3.f)),
m_emitter(0.f, 0.f)
{
}
void setEmitter(sf::Vector2f position)
{
m_emitter = position;
}
void update(sf::Time elapsed)
{
for (std::size_t i = 0; i < m_particles.size(); ++i)
{
// update the particle lifetime
Particle& p = m_particles[i];
p.lifetime -= elapsed;
// if the particle is dead, respawn it
if (p.lifetime <= sf::Time::Zero)
resetParticle(i);
// update the position of the corresponding vertex
m_vertices[i].position += p.velocity * elapsed.asSeconds();
// update the alpha (transparency) of the particle according to its lifetime
float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds();
m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255);
}
}
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the transform
states.transform *= getTransform();
// our particles don't use a texture
states.texture = NULL;
// draw the vertex array
target.draw(m_vertices, states);
}
private:
struct Particle
{
sf::Vector2f velocity;
sf::Time lifetime;
};
void resetParticle(std::size_t index)
{
// give a random velocity and lifetime to the particle
float angle = (std::rand() % 360) * 3.14f / 180.f;
float speed = (std::rand() % 50) + 50.f;
m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed);
m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000);
// reset the position of the corresponding vertex
m_vertices[index].position = m_emitter;
}
std::vector<Particle> m_particles;
sf::VertexArray m_vertices;
sf::Time m_lifetime;
sf::Vector2f m_emitter;
};
還有一個使用它的小演示:
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(512, 256), "Particles");
// create the particle system
ParticleSystem particles(1000);
// create a clock to track the elapsed time
sf::Clock clock;
// run the main loop
while (window.isOpen())
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
// make the particle system emitter follow the mouse
sf::Vector2i mouse = sf::Mouse::getPosition(window);
particles.setEmitter(window.mapPixelToCoords(mouse));
// update it
sf::Time elapsed = clock.restart();
particles.update(elapsed);
// draw it
window.clear();
window.draw(particles);
window.display();
}
return 0;
}
下面是我自己打的多文件版本 使用了c++11 random庫
ParticleSystem.h
//
// Created by Satar07 on 2022-05-22.
//
#ifndef GM_PARTICLESYSTEM_H
#define GM_PARTICLESYSTEM_H
#include <bits/stdc++.h>
#include <SFML/Graphics.hpp>
class ParticleSystem : public sf::Drawable, public sf::Transformable {
public:
explicit ParticleSystem(unsigned count);
void setEmitter(sf::Vector2f position);
void update(sf::Time elapsed);
private:
void draw(sf::RenderTarget &target, sf::RenderStates states) const override;
void resetParticle(std::size_t index);
private:
struct Particle {
sf::Vector2f velocity{}; //速度
sf::Time lifeTime{};
};
std::vector<Particle> m_particles; //粒子
sf::VertexArray m_vertices;
sf::Time m_lifeTime;
sf::Vector2f m_emitter; //發射器
std::default_random_engine m_randomEngine;
};
#endif //GM_PARTICLESYSTEM_H
ParticleSystem.cpp
//
// Created by Satar07 on 2022-05-22.
//
#include "ParticleSystem.h"
ParticleSystem::ParticleSystem(unsigned int count)
: m_particles(count), m_vertices(sf::Points, count),
m_lifeTime(sf::seconds(3.f)), m_emitter(0.f, 0.f) {
m_randomEngine.seed(time(nullptr) % 114514);
}
void ParticleSystem::setEmitter(sf::Vector2f position) {
m_emitter = position;
}
void ParticleSystem::update(sf::Time elapsed) {
for (std::size_t i = 0; i < m_particles.size(); i++) {
//update the particle
Particle &p = m_particles[i];
p.lifeTime -= elapsed;
//if the particle is dead, respawn it
if (p.lifeTime <= sf::Time::Zero)
resetParticle(i);
//update the position of the corresponding vertex
m_vertices[i].position += p.velocity * elapsed.asSeconds();
//update the alpha of the particle according to its lifetime
float ratio = p.lifeTime.asSeconds() / m_lifeTime.asSeconds();
m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255);
}
}
void ParticleSystem::draw(sf::RenderTarget &target, sf::RenderStates states) const {
//apply the transform
states.transform *= getTransform();
//our particle don't use a texture
states.texture = nullptr;
//draw the vertex array
target.draw(m_vertices, states);
}
void ParticleSystem::resetParticle(std::size_t index) {
//give a random velocity and lifetime to the particle
static std::uniform_real_distribution<float> uniAngle(0, 3.1415 * 2);
static std::uniform_real_distribution<float> uniSpeed(50, 100);
static std::uniform_int_distribution uniTime(1000, 3000);
float angle = uniAngle(m_randomEngine);
float speed = uniSpeed(m_randomEngine);
m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed);
m_particles[index].lifeTime = sf::milliseconds(uniTime(m_randomEngine));
//reset the position of the corresponding vertex
m_vertices[index].position = m_emitter;
}
main.cpp
//
// Created by Satar07 on 2022-05-22.
//
#include <SFML/Graphics.hpp>
#include <bits/stdc++.h>
#include "ParticleSystem.h"
int main() {
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
sf::RenderWindow window(sf::VideoMode(512, 256), "Particle", sf::Style::Default, settings);
window.setVerticalSyncEnabled(true);
// create the particle system
ParticleSystem particles(5000);
// create a clock to track the elapsed time
sf::Clock clock;
// run the main loop
while (window.isOpen()) {
// handle events
sf::Event event{};
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed)
window.close();
}
// make the particle system emitter follow the mouse
sf::Vector2i mouse = sf::Mouse::getPosition(window);
particles.setEmitter(window.mapPixelToCoords(mouse));
// update it
sf::Time elapsed = clock.restart();
particles.update(elapsed);
// draw it
window.clear();
window.draw(particles);
window.display();
}
return 0;
}
位置、旋轉、縮放:變換實體
轉換 SFML 實體
所有 SFML 類(精靈、文本、形狀)都使用相同的轉換介面:sf::Transformable
. 這個基類提供了一個簡單的 API 來移動、旋轉和縮放你的實體。它沒有提供最大的靈活性,而是定義了一個易於理解和使用的介面,它涵蓋了所有用例的 99% ——對於剩餘的 1%,請參閱最後幾章。
sf::Transformable
(及其所有派生類)定義了四個屬性:position、rotation、scale 和origin。它們都有各自的 getter 和 setter。這些變換組件都是相互獨立的:如果你想改變實體的方向,你只需要設置它的旋轉屬性,你不必關心當前的位置和比例。
位置
位置是實體在 2D 世界中的位置。我認為不需要更多解釋:)。
// 'entity' can be a sf::Sprite, a sf::Text, a sf::Shape or any other transformable class
// set the absolute position of the entity
entity.setPosition(10.f, 50.f);
// move the entity relatively to its current position
entity.move(5.f, 5.f);
// retrieve the absolute position of the entity
sf::Vector2f position = entity.getPosition(); // = (15, 55)
默認情況下,實體相對於其左上角定位。稍後我們將看到如何使用 ‘origin’ 屬性來改變它。
旋轉
旋轉是實體在 2D 世界中的方向。它以度為單位,按順時針順序定義(因為 Y 軸在 SFML 中指向下方)。
// 'entity' can be a sf::Sprite, a sf::Text, a sf::Shape or any other transformable class
// set the absolute rotation of the entity
entity.setRotation(45.f);
// rotate the entity relatively to its current orientation
entity.rotate(10.f);
// retrieve the absolute rotation of the entity
float rotation = entity.getRotation(); // = 55
請注意,當您調用 SFML 時,它總是返回 [0, 360) 範圍內的角度getRotation
。
與位置一樣,默認情況下圍繞左上角執行旋轉,但這可以通過設置原點來更改。
比例
比例因子允許調整實體的大小。默認比例為 1。將其設置為小於 1 的值會使實體變小,大於 1 會使其變大。也允許使用負比例值,以便您可以鏡像實體。
// 'entity' can be a sf::Sprite, a sf::Text, a sf::Shape or any other transformable class
// set the absolute scale of the entity
entity.setScale(4.f, 1.6f);
// scale the entity relatively to its current scale
entity.scale(0.5f, 0.5f);
// retrieve the absolute scale of the entity
sf::Vector2f scale = entity.getScale(); // = (2, 0.8)
原點
原點是其他三個變換的中心點。實體的位置是其原點的位置,它的旋轉是圍繞原點執行的,並且比例也相對於原點應用。默認情況下,它是實體的左上角(點 (0, 0)),但您可以將其設置為實體的中心,或實體的任何其他角。
為簡單起見,所有三個轉換組件只有一個來源。這意味著,例如,在圍繞其中心旋轉實體時,您不能相對於其左上角定位實體。如果您需要做這些事情,請查看下一章。
// 'entity' can be a sf::Sprite, a sf::Text, a sf::Shape or any other transformable class
// set the origin of the entity
entity.setOrigin(10.f, 20.f);
// retrieve the origin of the entity
sf::Vector2f origin = entity.getOrigin(); // = (10, 20)
請注意,更改原點也會更改實體在螢幕上的繪製位置,即使它的位置屬性沒有更改。如果您不明白為什麼,請再閱讀本教程!
自定義類
sf::Transformable
不僅適用於 SFML 類,它還可以是您自己的類的基礎(或成員)。
class MyGraphicalEntity : public sf::Transformable
{
// ...
};
MyGraphicalEntity entity;
entity.setPosition(10.f, 30.f);
entity.setRotation(110.f);
entity.setScale(0.5f, 0.2f);
要檢索實體的最終變換(繪製時通常需要),請調用該getTransform
函數。該函數返回一個 sf::Transform
對象。有關它的解釋,以及如何使用它來轉換 SFML 實體,請參見下文。
如果您不需要/想要sf::Transformable
介面提供的完整功能集,請不要猶豫,直接將其用作成員,並在其之上提供您自己的功能。它不是抽象的,因此可以實例化它而不是只能將其用作基類。
自定義轉換
該類sf::Transformable
易於使用,但也受到限制。一些用戶可能需要更大的靈活性。他們可能需要將最終轉換指定為各個轉換的自定義組合。對於這些用戶,可以使用較低級別的類:sf::Transform
. 它只不過是一個 3×3 矩陣,因此它可以表示 2D 空間中的任何變換。
有很多方法可以構造一個sf::Transform
:
- 通過使用預定義函數進行最常見的轉換(平移、旋轉、縮放)
- 通過結合兩個變換
- 通過直接指定其 9 個元素
這裡有一些例子:
// the identity transform (does nothing)
sf::Transform t1 = sf::Transform::Identity;
// a rotation transform
sf::Transform t2;
t2.rotate(45.f);
// a custom matrix
sf::Transform t3(2.f, 0.f, 20.f,
0.f, 1.f, 50.f,
0.f, 0.f, 1.f);
// a combined transform
sf::Transform t4 = t1 * t2 * t3;
您也可以將多個預定義的轉換應用於同一個轉換。它們都將按順序組合。請注意,通過組合多個變換來變換對象相當於以相反的順序應用每個操作。最後一個操作(這裡scale
)首先應用,並且會受到程式碼中高於它的操作的影響(例如,第二個是translate(-10.f, 50.f)
)。
sf::Transform t;
t.translate(10.f, 100.f);
t.rotate(90.f);
t.translate(-10.f, 50.f);
t.scale(0.5f, 0.75f);
回到正題:如何將自定義變換應用於圖形實體?十分簡單:將其傳遞給繪圖函數。
window.draw(entity, transform);
…這實際上是一條捷徑:
sf::RenderStates states;
states.transform = transform;
window.draw(entity, states);
如果您的實體是一個sf::Transformable
(精靈、文本、形狀),其中包含其自己的內部變換,則內部和傳遞的變換將組合起來以產生最終的變換。
邊界框
在轉換實體並繪製它們之後,您可能希望使用它們執行一些計算,例如檢查碰撞。
SFML 實體可以給你他們的邊界框。邊界框是包含屬於實體的所有點的最小矩形,其邊與 X 軸和 Y 軸對齊。
邊界框在實現碰撞檢測時非常有用:對一個點或另一個軸對齊的矩形的檢查可以非常快速地完成,並且它的區域與真實實體的區域足夠接近以提供良好的近似。
// get the bounding box of the entity
sf::FloatRect boundingBox = entity.getGlobalBounds();
// check collision with a point
sf::Vector2f point = ...;
if (boundingBox.contains(point))
{
// collision!
}
// check collision with another box (like the bounding box of another entity)
sf::FloatRect otherBox = ...;
if (boundingBox.intersects(otherBox))
{
// collision!
}
該函數之所以命名,是getGlobalBounds
因為它返回全局坐標系中實體的邊界框,即在應用了所有變換(位置、旋轉、比例)之後。
還有另一個函數返回實體在其局部坐標系中的邊界框(在應用其轉換之前) getLocalBounds
:例如,此函數可用於獲取實體的初始大小,或執行更具體的計算。
對象層次結構(場景圖)
使用前面看到的自定義轉換,實現對象層次結構變得容易,其中子對象相對於其父對象進行轉換。您所要做的就是在繪製它們時將組合變換從父級傳遞到子級,一直到您到達最終的可繪製實體(精靈、文本、形狀、頂點數組或您自己的可繪製對象)。
// the abstract base class
class Node
{
public:
// ... functions to transform the node
// ... functions to manage the node's children
void draw(sf::RenderTarget& target, const sf::Transform& parentTransform) const
{
// combine the parent transform with the node's one
sf::Transform combinedTransform = parentTransform * m_transform;
// let the node draw itself
onDraw(target, combinedTransform);
// draw its children
for (std::size_t i = 0; i < m_children.size(); ++i)
m_children[i]->draw(target, combinedTransform);
}
private:
virtual void onDraw(sf::RenderTarget& target, const sf::Transform& transform) const = 0;
sf::Transform m_transform;
std::vector<Node*> m_children;
};
// a simple derived class: a node that draws a sprite
class SpriteNode : public Node
{
public:
// .. functions to define the sprite
private:
virtual void onDraw(sf::RenderTarget& target, const sf::Transform& transform) const
{
target.draw(m_sprite, transform);
}
sf::Sprite m_sprite;
};
使用著色器添加特殊效果
介紹
著色器是在顯示卡上執行的小程式。與使用 OpenGL 提供的一組固定狀態和操作相比,它以一種更靈活、更簡單的方式為程式設計師提供了對繪圖過程的更多控制。有了這種額外的靈活性,著色器被用來創建複雜的效果,如果不是不可能的話,用常規的 OpenGL 函數來描述:每像素光照、陰影(Per-pixel lighting, shadows, etc)等。今天的顯示卡和更新版本的 OpenGL 已經完全是著色器了。基於,並且您可能知道的一組固定狀態和函數(稱為「固定管道」)已被棄用,並且將來可能會被刪除。
著色器是用 GLSL(OpenGL Shading Language)編寫的,它與 C 程式語言非常相似。
有兩種類型的著色器:頂點著色器(vertex shaders)和片段(或像素)著色器(fragment (or pixel) shaders)。頂點著色器針對每個頂點運行,而片段著色器針對每個生成的片段(像素)運行。根據你想要達到什麼樣的效果,你可以提供一個頂點著色器,一個片段著色器,或者兩者都提供。
要了解著色器的作用以及如何有效地使用它們,了解渲染管道的基礎知識非常重要。您還必須學習如何編寫 GLSL 程式並找到好的教程和示例以開始使用。您還可以查看 SFML SDK 附帶的「著色器」示例。
本教程將只關注 SFML 特定部分:載入和應用著色器——而不是編寫它們。
載入著色器
在 SFML 中,著色器由sf::Shader
類表示。它同時處理頂點和片段著色器:一個sf::Shader
對象是兩者的組合(或者只有一個,假如沒有提供另一個)。
儘管著色器已經司空見慣,但仍有一些舊顯示卡可能不支援它們。您應該在程式中做的第一件事是檢查系統上是否著色器可用:
if (!sf::Shader::isAvailable())
{
// shaders are not available...
}
如果返回 ,任何使用該類的嘗試都sf::Shader
將失敗。 sf::Shader::isAvailable()
永遠返回false
載入著色器的最常見方法是從磁碟上的文件中載入,這是通過loadFromFile
函數完成的。
sf::Shader shader;
// load only the vertex shader
if (!shader.loadFromFile("vertex_shader.vert", sf::Shader::Vertex))
{
// error...
}
// load only the fragment shader
if (!shader.loadFromFile("fragment_shader.frag", sf::Shader::Fragment))
{
// error...
}
// load both shaders
if (!shader.loadFromFile("vertex_shader.vert", "fragment_shader.frag"))
{
// error...
}
著色器源包含在簡單的文本文件中(如您的 C++ 程式碼)。他們的擴展並不重要,它可以是任何你想要的,你甚至可以省略它。「.vert」和「.frag」只是可能擴展的例子。
著色器也可以通過函數直接從字元串載入loadFromMemory
。如果您想將著色器源直接嵌入到您的程式中,這將很有用。
const std::string vertexShader = \
"void main()" \
"{" \
" ..." \
"}";
const std::string fragmentShader = \
"void main()" \
"{" \
" ..." \
"}";
// load only the vertex shader
if (!shader.loadFromMemory(vertexShader, sf::Shader::Vertex))
{
// error...
}
// load only the fragment shader
if (!shader.loadFromMemory(fragmentShader, sf::Shader::Fragment))
{
// error...
}
// load both shaders
if (!shader.loadFromMemory(vertexShader, fragmentShader))
{
// error...
}
最後,與所有其他 SFML 資源一樣,著色器也可以使用loadFromStream
從自定義輸入流中載入 。
如果載入失敗,不要忘記檢查標準錯誤輸出(控制台)以查看來自 GLSL 編譯器的詳細報告。
使用著色器
使用著色器很簡單,只需將其作為附加參數傳遞給draw
函數即可。
window.draw(whatever, &shader);
將變數傳遞給著色器
像任何其他程式一樣,著色器可以接受參數,以便它能夠在一次繪製到另一次繪製時表現出不同的行為。這些參數被聲明為全局變數,稱為著色器中的統一變數。
統一變數是溝通GPU和CPU之間的重要橋樑。
uniform float myvar;
void main()
{
// use myvar...
}
統一變數可以由 C++ 程式設置,使用類中setUniform
函數的各種重載sf::Shader
。
shader.setUniform("myvar", 5.f);
setUniform
的重載支援 SFML 提供的所有類型:
float
(GLSL typefloat
)2 floats, sf::Vector2f
(GLSL typevec2
)3 floats, sf::Vector3f
(GLSL typevec3
)4 floats
(GLSL typevec4
)sf::Color
(GLSL typevec4
)sf::Transform
(GLSL typemat4
)sf::Texture
(GLSL typesampler2D
)
GLSL 編譯器優化了未使用的變數(這裡,「未使用」的意思是「不參與最終頂點/像素的計算」)。因此,如果您在測試期間調用setUniform
時收到諸如 Failed to find variable “xxx” in shader 之類的錯誤消息,請不要感到驚訝。
最小著色器
您不會在這裡學習如何編寫 GLSL 著色器,但您必須知道 SFML 向著色器提供什麼輸入以及它希望您用它做什麼。
頂點著色器
SFML有一個固定的頂點格式,由sf::Vertex
結構描述。SFML頂點包含2D位置、顏色和2D紋理坐標。這是您將在頂點著色器中獲得的精確輸入,存儲在內置的gl_Vertex
、gl_Color
和gl_MultiTexCoord0
變數中(您不需要聲明它們)。
void main()
{
// transform the vertex position
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
// transform the texture coordinates
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
// forward the vertex color
gl_FrontColor = gl_Color;
}
位置通常需要通過模型視圖和投影矩陣進行轉換,其中包含與當前視圖相結合的實體轉換。紋理坐標需要通過紋理矩陣進行轉換(這個矩陣可能對你沒有任何意義,它只是一個 SFML 實現細節)。最後,只需要轉換前景色。當然,如果你不使用它們,你可以忽略紋理坐標和/或顏色。
然後,所有這些變數將由顯示卡在圖元上進行插值,並傳遞給片段著色器。
片段著色器
片段著色器的功能非常相似:它接收紋理坐標和生成片段的顏色。沒有位置了,此時顯示卡已經計算出片段的最終光柵位置。但是,如果您處理帶紋理的實體,您還需要當前紋理。
uniform sampler2D texture;
void main()
{
// lookup the pixel in the texture
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
// multiply it by the color
gl_FragColor = gl_Color * pixel;
}
當前紋理不是自動的,您需要像對待其他輸入變數一樣對待它,並從 C++ 程式中顯式設置它。由於每個實體都可以有不同的紋理,更糟糕的是,您可能無法獲取它並將其傳遞給著色器,SFML 提供了一個特殊的setUniform
函數重載來為您完成這項工作。
shader.setUniform("texture", sf::Shader::CurrentTexture);
這個特殊參數自動將要繪製的實體的紋理設置為具有給定名稱的著色器變數。每次繪製新實體時,SFML 都會相應地更新著色器紋理變數。
如果你想看到很好的著色器示例,可以查看 SFML SDK 中的著色器示例。
在 OpenGL 程式碼中使用 sf::Shader
如果您使用的是 OpenGL 而不是 SFML 的圖形實體,您仍然可以將sf::Shader
其用作 OpenGL 程式對象的包裝器並在您的 OpenGL 程式碼中使用它。
要激活sf::Shader
用於繪圖(相當於glUseProgram
),您必須調用bind
靜態函數:
sf::Shader shader;
...
// bind the shader
sf::Shader::bind(&shader);
// draw your OpenGL entity here...
// bind no shader
sf::Shader::bind(NULL);
使用視圖控制 2D 相機
什麼是視圖?
在遊戲中,比窗口本身大得多的關卡並不少見。你看到的只是其中的一小部分。這通常是 RPG、平台遊戲和許多其他類型的情況。開發人員可能會忘記的是,他們在 2D 世界中定義實體,而不是直接在窗口中。窗口只是一個視圖,它顯示了整個世界的特定區域。並行繪製同一個世界的多個視圖,或者將世界繪製到紋理而不是窗口上是完全可以的。世界本身並沒有改變,改變的只是它被看到的方式。
由於在窗口中看到的只是整個 2D 世界的一小部分,因此您需要一種方法來指定在窗口中顯示世界的哪個部分。此外,您可能還想定義該區域在窗口中的顯示位置/方式。這是 SFML 視圖的兩個主要特徵。
總而言之,如果您想滾動、旋轉或縮放您的世界,視圖就是您所需要的。它們也是創建分屏和迷你地圖的關鍵。
定義視圖
在 SFML 中封裝視圖的類是sf::View
. 可以通過定義查看區域直接構建:
// create a view with the rectangular area of the 2D world to show
sf::View view1(sf::FloatRect(200.f, 200.f, 300.f, 200.f));
// create a view with its center and size
sf::View view2(sf::Vector2f(350.f, 300.f), sf::Vector2f(300.f, 200.f));
這兩個定義是等效的:兩個視圖都將顯示 2D 世界的相同區域,即以點 (350, 300) 為中心的 300×200 矩形。
如果您不想在構造時定義視圖或想稍後修改它,您可以使用等效的設置器:
sf::View view1;
view1.reset(sf::FloatRect(200.f, 200.f, 300.f, 200.f));
sf::View view2;
view2.setCenter(sf::Vector2f(350.f, 300.f));
view2.setSize(sf::Vector2f(200.f, 200.f));
定義視圖後,您可以對其進行轉換以使其顯示 2D 世界的平移/旋轉/縮放版本。
移動(滾動)視圖
與可繪製實體不同,例如位置由左上角定義的精靈或形狀(並且可以更改為任何其他點),視圖總是由它們的中心操作——這更方便。這就是為什麼改變視圖位置的函數被命名為setCenter
,而不是 setPosition
。
// move the view at point (200, 200)
view.setCenter(200.f, 200.f);
// move the view by an offset of (100, 100) (so its final position is (300, 300))
view.move(100.f, 100.f);
旋轉視圖
要旋轉視圖,請使用該setRotation
功能。
// rotate the view at 20 degrees
view.setRotation(20.f);
// rotate the view by 5 degrees relatively to its current orientation (so its final orientation is 25 degrees)
view.rotate(5.f);
縮放(縮放)視圖
放大(或縮小)視圖是通過調整它的大小來完成的,因此要使用的功能是setSize
.
// resize the view to show a 1200x800 area (we see a bigger area, so this is a zoom out)
view.setSize(1200.f, 800.f);
// zoom the view relatively to its current size (apply a factor 0.5, so its final size is 600x400)
view.zoom(0.5f);
定義視圖的查看方式
現在您已經定義了 2D 世界的哪個部分可以在窗口中看到,讓我們定義它的顯示位置。默認情況下,查看的內容佔據整個窗口。如果視圖與窗口大小相同,則所有內容都以 1:1 呈現。如果視圖小於或大於窗口,則所有內容都會縮放以適合窗口。
此默認行為適用於大多數情況,但有時可能需要更改。例如,要在多人遊戲中拆分螢幕,您可能想要使用兩個視圖,每個視圖只佔據一半的窗口。您還可以通過將整個世界繪製到在窗口一角的小區域中呈現的視圖來實現小地圖。顯示視圖內容的區域稱為視口。
要設置視圖的視口,您可以使用該setViewport
功能。
// define a centered viewport, with half the size of the window
view.setViewport(sf::FloatRect(0.25f, 0.25, 0.5f, 0.5f));
您可能已經注意到一些非常重要的事情:視口不是以像素為單位定義的,而是作為窗口大小的比率。這更方便:它允許您不必跟蹤調整大小事件以便在每次窗口大小更改時更新視口的大小。它也更直觀:無論如何,您可能會將視口定義為整個窗口區域的一小部分,而不是固定大小的矩形。
使用視口,可以直接分割多人遊戲的螢幕:
// player 1 (left side of the screen)
player1View.setViewport(sf::FloatRect(0.f, 0.f, 0.5f, 1.f));
// player 2 (right side of the screen)
player2View.setViewport(sf::FloatRect(0.5f, 0.f, 0.5f, 1.f));
…或小地圖:
// the game view (full window)
gameView.setViewport(sf::FloatRect(0.f, 0.f, 1.f, 1.f));
// mini-map (upper-right corner)
minimapView.setViewport(sf::FloatRect(0.75f, 0.f, 0.25f, 0.25f));
使用視圖
要使用視圖繪製某些東西,您必須在調用setView
您正在繪製的目標的函數(sf::RenderWindow
或sf::RenderTexture
)之後繪製它。
// let's define a view
sf::View view(sf::FloatRect(0.f, 0.f, 1000.f, 600.f));
// activate it
window.setView(view);
// draw something to that view
window.draw(some_sprite);
// want to do visibility checks? retrieve the view
sf::View currentView = window.getView();
...
在您設置另一個視圖之前,該視圖一直處於活動狀態。這意味著總是有一個視圖來定義目標中出現的內容以及繪製的位置。如果您沒有顯式設置任何視圖,則渲染目標將使用其自己的默認視圖,該視圖與其大小 1:1 匹配。getDefaultView
您可以使用該函數獲取渲染目標的默認視圖 。如果您想基於它定義自己的視圖,或者恢復它以在場景頂部繪製固定實體(如 GUI),這將很有用。
// create a view half the size of the default view
sf::View view = window.getDefaultView();
view.zoom(0.5f);
window.setView(view);
// restore the default view
window.setView(window.getDefaultView());
當您調用setView
時,渲染目標會生成視圖的副本,並且不會存儲指向所傳遞視圖的指針。這意味著無論何時更新視圖,都需要setView
再次調用以應用修改。
不要害怕複製視圖或動態創建它們,它們不是昂貴的對象(它們只是持有一些浮點數)。
調整窗口大小時顯示更多
由於默認視圖在窗口創建後永遠不會改變,因此查看的內容始終相同。因此,當調整窗口大小時,所有內容都會被壓縮/拉伸到新大小。
如果您希望根據窗口的新大小而不是這種默認行為來顯示更多/更少的內容,那麼您所要做的就是用窗口的大小更新視圖的大小。
// the event loop
sf::Event event;
while (window.pollEvent(event))
{
...
// catch the resize events
if (event.type == sf::Event::Resized)
{
// update the view to the new size of the window
sf::FloatRect visibleArea(0.f, 0.f, event.size.width, event.size.height);
window.setView(sf::View(visibleArea));
}
}
坐標轉換
當您使用自定義視圖或不使用上述程式碼調整窗口大小時,顯示在目標上的像素不再匹配 2D 世界中的單位。例如,單擊像素 (10, 50) 可能會命中您世界的點 (26.5, -84)。您最終不得不使用轉換函數將像素坐標映射到世界坐標: mapPixelToCoords
.
// get the current mouse position in the window
sf::Vector2i pixelPos = sf::Mouse::getPosition(window);
// convert it to world coordinates
sf::Vector2f worldPos = window.mapPixelToCoords(pixelPos);
默認情況下,mapPixelToCoords
使用當前視圖。如果要使用非活動視圖轉換坐標,可以將其作為附加參數傳遞給函數。
相反,將世界坐標轉換為像素坐標,也可以使用該mapCoordsToPixel
函數。