SFML 教程&个人笔记
本文大部分来自官方教程的Google翻译 但是加了一点点个人的理解和其他相关知识
转载请注明 原文链接 :https://www.cnblogs.com/Multya/p/16273753.html
本文有什么
这是SFML官方教程的翻译
涉及的模块有
- System module 系统模块
- Window module 窗口模块
- Graphics module 图形模块
其实一共有五个模块 因为太长了所以就先做了三个 Typora都撑不住了。。
把垃圾谷歌翻译调教了一下 引入了一点点其他知识(应该没有私货
还有随机出现的自己手打和官方不一样的程序 (大括号不换行
不保证一定没有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)
记得要把bin下的dll文件复制一份放到最后运行的exe文件夹里面 或者设置path一劳永逸也可以
有机会专门发一篇来讲配置 配置SFML对还是新手的我可是走了很多弯路的(一定
2024 更新
官方放出了cmake模板,现在直接clone下来就可以用了
github链接:https://github.com/SFML/cmake-sfml-project
窗口
打开一个窗口
SFML 中的窗口由 sf::Window
类定义。可以在构建时直接创建和打开窗口:
#include <SFML/Window.hpp>
int main()
{
sf::Window window(sf::VideoMode(800, 600), "My window");
...
return 0;
}
第一个参数video mode定义了窗口的大小(内部大小,没有标题栏和边框)。在这里,我们创建一个大小为 800x600 像素的窗口。
该类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
. 它只不过是一个 3x3 矩阵,因此它可以表示 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) 为中心的 300x200 矩形。
如果您不想在构造时定义视图或想稍后修改它,您可以使用等效的设置器:
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
函数。