目录
Preface
What is a camera?
When should we use a camera?
How does SFML implement a camera?
Manipulating cameras with sf::View
Rotating and scaling a view
Viewports
Mapping coordinates
What is OpenGL?
Should you use OpenGL?
Using OpenGL inside SFML
OpenGL in multiple windows
Summary
● 在这一章中,我们将讨论摄像机和OpenGL,以及如何利用它们为我们带来好处。我们将深入讨论摄像机的主题,但是关于OpenGL,我们只简单提到了它在SFML中的集成。OpenGL API太大,本书无法涵盖,更不用说一章了。如果你不知道如何编写OpenGL代码,或者不希望使用OpenGL,那么跳过本章的最后一部分就可以了。另一方面,如果您想知道OpenGL如何帮助您,请查看本章的第二部分提供的内容。
在本章中,我们将介绍以下主题:
- 什么是相机?
- 使用sf :: View操作相机
- 什么是OpenGL?
- 在SFML中使用OpenGL
● 在游戏的开发中, 很少不用到摄像机的时候。它们是任何游戏必不可少的一部分。本质上,相机是空间中的一个点,通过它你可以看到游戏世界。在2D和3D空间中,有更多与摄像机相关的参数,但是在这一章中,我们将只关注SFML所能提供的。
在开始代码之前,让我们先了解一些事实。由于SFML主要用于2D游戏,所以camera类只使用正交投影。在这个投影中,每个物体看起来似乎并没有透视上的增大; 正如你可能已经猜到的,另一种选择是透视投影,它实际上根据人眼的物理特性改变了物体在屏幕上的显示方式(物体在远处看起来更小,等等)。然而,这个投影主要用于3D游戏,它不属于2D游戏。这里有一个小小的比较:
在2D 游戏中使用透视投影没有多大意义,因为sprite的image 会distorted。这就是为什么SFML不提供使用它的原因。但是,我们总是可以用OpenGL创建一个自定义摄像机,这是我们将在本章后面讨论的主题。
● 我们不一定非要操作摄像机。如果一款游戏只有一个屏幕(例如一款三人行棋游戏),那么修改相机就没有意义了,因为它总是在棋盘的中央是静止的。其他的例子可能包括浏览主菜单——同样,相机的位置是静止的,因此不需要做任何事情。
但是,假设我们正在制作角色扮演游戏(RPG),并且有一个值得探索的大世界。在这种情况下,我们会很容易地想要实现一个摄像机,要么与我们的角色一起移动,要么与之相关。大多数平台游戏的工作方式都是相同的,即使它们拥有不同的关卡。基本上 只要有关卡的游戏 我们都需要使用摄像机
现在我们已经了解了相机的基本概念,让我们来看看如何实现它。
● 如果我们想修改每个sf :: Window实例附带的默认摄像头,我们必须用 sf :: View类 处理。
View类的行为与典型的相机完全一样,通过一组参数限制玩家在世界上所能看到的东西。 这就是我们创建和使用View的方式:
sf::RenderWindow window(sf::VideoMode(482, 180), "Bad Squares!"); //创建窗口代码
auto wSize = window.getSize();
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y));
// Initialize view
window.setView(view);
View类的构造函数接受一个FloatRect参数,该参数设置所想看到的视图区域。如果我们有一个更大的可视区域 那么区域中的内容就会缩降以至窗口可以容纳的下 ,反之亦然。在本例中,该区域与窗口大小匹配,因此不会改变对象的渲染方式。
最后,当我们在View中配置了所有内容时,我们需要通过调用RenderWindow :: setView()来告诉窗口使用它。这会拷贝视图到渲染窗口对象中 所以我们不用保存原始视图 之前我们对纹理也是这样类似处理的.
● 可以说,View类最重要的特性是它能够改变视图区域的中心。默认情况下,view的中心是view区域的中心,这意味着,如果我们的视图区域大小为( 640,480 ),视图的中心将是( 320,240 )。这使得渲染对象的位置(0;0)出现在左上角。这与使用SFML窗口的默认视图时的行为相同。要 修改 视图的中心,我们可以调用 View::setCenter ( ) 或 View : : Move ( );下面是一个例子:
sf::RenderWindow window(sf::VideoMode(482, 180), "Bad Squares!"); //创建窗口代码
auto wSize = window.getSize();
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y)); // 获得 view 区域
//视图以世界点(0; 0)为中心
view.setCenter(sf::Vector2f(0, 0));
// Initialize view
window.setView(view);
sf::Vector2f spriteSize = sf::Vector2f(32, 32);
sf::Sprite sprite(AssetManager::GetTexture("myTexture.png"));
sprite.setOrigin(spriteSize *0.5f);
如果我们的view的中心位置在(0,0), 则该位置将出现在屏幕中心. 代码中的sprite 位于( 0;0 ), 默认情况下,它会出现在屏幕中央;结果如下:
通过将视图对着主角的中心来控制摄像机位置 这样比较简单且高效。这只需要在update frame 时执行两行代码就可以实现了
view.setCenter(sprite.getPosition());
window.setView(view);
注意,我们需要调用RenderWindow::setView()并再次传递view,因为RenderWindow只保存 view的一个副本。仅改变旧的视图实例是不会影响保存在 RenderWindow 中的视图的。
● 我们可以在任何view 实例上执行另外两个transformations —— rotation and scale. 它们的用途都很有限,但当我们需要相机的特殊功能时,它们就派上用场了。
要旋转view,我们调用 view::setRotation() 或 view::rotate() ,这取决于要执行的旋转类型。View::setRotation ( )方法为view的旋转分配一个绝对值,而 View::rotate() 添加传递的旋转值。当我们希望在一段时间内逐渐增加旋转时,通常使用后者。
旋转本身正如人们所期望的那样工作——它围绕view的中心旋转场景(每个对象)。以下是我们要执行的测试的设置:
auto wSize = window.getSize();
//view 以世界点(0; 0)为中心
// view 的大小跟window 一样
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y));
//视图以世界点(0; 0)为中心
view.setCenter(sf::Vector2f(0, 0));
// Initialize view
// Set rotation view.setRotation(...);
window.setView(view);
sf::Vector2f spriteSize = sf::Vector2f(32, 32);
auto &texture = AssetManager::GetTexture("myTexture.png");
//Top left
sf::Sprite sprite1(texture);
sprite1.setOrigin(spriteSize * 0.5f);
sprite1.setPosition(sf::Vector2f(-80, -80));
// Top right
sf::Sprite sprite2(texture);
sprite2.setOrigin(spriteSize * 0.5f);
sprite2.setPosition(sf::Vector2f(80, -80));
//Bottom right
sf::Sprite sprite3(texture);
sprite3.setOrigin(spriteSize * 0.5f);
sprite3.setPosition(sf::Vector2f(80, 80));
// Bottom left
sf::Sprite sprite4(texture);
sprite4.setOrigin(spriteSize * 0.5f);
sprite4.setPosition(sf::Vector2f(-80, 80));
这次,使用不同的构造函数调用view。 我们将传递 center position and size,而不是传递rectangle。 这有着相同的目的,但允许我们更容易地指定中心点。
初始化视图之后,我们在屏幕的四个角上围绕世界坐标点(0,0)创造四个Sprites; 以下是在场景中旋转0度和45度进行的测试:
旋转视图在游戏开发方面的应用有限。它可以用于制作特定事件的animate ——主角的死亡(随着旋转缓慢放大),受到伤害(相机轻微抖动)。我们也可以使用它在一个自上而下的游戏中围绕一个中心人物旋转世界。一般来说,旋转是有用的,但是用例有限。
视图也可以scale。通过缩放view,我们可以显示或多或少的世界,取决于scale的方向。用游戏的术语来说,这个特性通常被称为缩放(zoom)(即: 缩大(zoom in)或缩小(zoom out) 。缩放与视图的大小直接相关。当我们告诉视图显示比窗口大两倍的区域时,我们实际上是放大了两倍。另一方面,如果我们想要近距离显示物体,我们必须使用1/x的缩放因子,其中x是缩放因子。例如,使视图缩小两倍需要1/2 = 0.5。
让我们通过展示两个使用sprite的示例来演示缩放,其中sprite用于旋转:
设置缩放本身的过程可以通过使用缩放因子调用View::zoom(),也可以通过View::setSize()更改视图的大小来完成。实际上,使用View :: rotate()和View :: move()时,View :: zoom()主要用于我们希望在多个帧中进行连续运动的情况。
请注意,View不存储缩放因子,只存储视图的大小,这一点很重要。这意味着使用不等于1的因子作为实参来调用View::zoom()会导致视图的大小每次都会变化,即使我们每次传进的相同的因子。 该方法不设置缩放比例,它只是根据缩放因子修改视图的大小。例如,如果我们以1 / 2的缩放因子调用View::zoom ( )两次,我们将得到:
(1/2) * (1/2) * size = (1/4) * size
我们实际上正在以1/4的缩放因子调用View::zoom()。因为View::zoom()只接受一个因子,所以它使用当前视图大小来估计需要修改视图的宽度和高度。如果我们想通过两个不同的因子来改变宽度和高度,我们必须使用View::setSize ( )和我们自己的宽度和高度值;下面是一个例子:
auto wSize = window.getSize();
sf::View view(sf::Vector2f(0, 0), sf::Vector2f(wSize.x, wSize.y));
//First example
view.setSize(wSize.x * 2, wSize.y);
//Second example
view.setSize(wSize.x, wSize.y * 2);
window.setView(view);
上述代码产生以下结果:
如你说见,这两张图片在x轴或y轴方向被压扁了,这是因为我们试图将超过屏幕所允许两倍数量的像素容纳进去。您可能会认识到这种效果,因为它类似于将纹理放置到表面上,该表面与纹理具有不同的尺寸。
● 每个view都有一个与之相关联的Viewport. viewport是显示视图的窗口区域。该区域由rectangle表示,该rectangle使用标准化坐标[0 ... 1]表示。 默认情况下,视口等于视图的大小( 0,0,1,1 )。我们可以通过调用View : : SetViewport ( )来改变这一点。假设我们只想在屏幕的左上角渲染场景。以下代码将执行此操作:
sf::RenderWindow window(sf::VideoMode(482, 180), "Bad Squares!"); //创建窗口代码
sf::RectangleShape rectShape(sf::Vector2f(300, 250));
auto wSize = window.getSize();
sf::View view(sf::Vector2f(0, 0), sf::Vector2f(wSize.x, wSize.y));
view.setViewport(sf::FloatRect(0, 0, 0.5f, 0.5f));
window.setView(view);
以下是上述代码在宽屏窗口中的结果:
因为我们可以创建多个views,所以我们可以在渲染帧中在它们之间切换,并反复渲染场景。通过这种方式,我们将在不同的views中显示场景。在前面的例子中,我们可以在所有四个象限中渲染相同的场景,但是使用不同的transformations。 为此,我们必须用以下Viewport初始化屏幕四个角落的四个views: 左上角( 0,0,0,0.5,0.5 ),右上角( 0.5,0,0,0.5,0.5 ),左下角( 0,0.5,0.5,0.5 )和右下角( 0.5,0.5,0.5,0.5 )。一旦我们有了这些,我们就可以操作它们的transformation,并将它们放入一个viewList向量中。然后我们的渲染帧如下:
window.clear(sf::Color::Black);
for (auto it = viewList.begin(); it != viewList.end(); ++it)
{
//Set the view
window.setView(*it);
//Render sprites
}
window.display();
结果取决于我们如何选择操纵我们的视图。以下是一个例子,您可以在本书提供的代码示例中找到它:
正如您所看到的,viewports对于同时拥有多个视图非常有用。这使得创建本地分屏多人游戏变得非常容易。除此之外,我们还可以为UI使用不同的视图作为示例。为此,我们用游戏摄像机渲染我们的游戏,然后切换到UI摄像机(它不会随世界移动),并渲染我们游戏的UI。世界地图和小地图是对视图的完美的应用,因为我们可以渲染整个世界到另外一个窗口(或视图)中 。
一般来说,视图极其好用,但是它们有一个弱点——一旦我们操作摄像机就会导致视图内容和窗口坐标对应不起来。例如,如果我们试图处理点击事件,对于不同的视图,鼠标位置将不会指向场景中的相同位置。
● 一旦有一个视图附加到一个窗口上,我们就可以调用Render.::mapPixelToCoords(),从窗口中获取一个位置,并将位置vector转换为场景中的一个位置(世界坐标)。以下是如何在按钮按下事件中将鼠标坐标转换为世界坐标的示例:
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::MouseButtonPressed)
{
sf::Vector2f sceneCoords = window.mapPixelToCoords(sf::Vector2i(event.mouseButton.x,
event.mouseButton.y));
//Do something at that location in the scene
}
}
重要的是要记住,该函数将只与当前应用于窗口的View一起工作。如果我们为窗口使用多个视图,则需要考虑这一点。
我们也可以反过来 - 从场景中的某个位置获取屏幕坐标。 这是由RenderWindow :: mapCoordsToPixel()完成的。当我们想要将一个位置从一个场景映射到其他视图时,这很有用。例如,如果我们想要在角色上显示生命值,我们获取它们在场景中的位置,将位置映射到屏幕像素,然后将该像素映射到我们的UI视图坐标,并在那里显示生命值。这样一来,一切就变得井然有序,无需付出太多努力。
这节包含了视图的主题。本章的下一部分将介绍SFML中的OpenGL代码集成。
● OpenGL是一个跨平台的图形API,用作与显卡交互的接口。任何图形API最重要的特性是它能够在屏幕上渲染对象。虽然OpenGL确实做到了这一点,但它还有许多其他有用的功能。 但是,由于它已存在多年,并且GPU技术发生了巨大变化,并非所有显卡都支持新版本OpenGL的所有特性。
● 公平地说,SFML支持OpenGL提供的许多功能特性。 实际上,SFML在内部使用OpenGL来这些功能特性。然而,由于SFML是一个高级库,使用它总是会带来性能上的损失。在大多数情况下,hit并不是一个大问题,因为这样一个高级库的好处是它非常容易编写。但是,有些情况只需要额外的性能才能实现FPS 游戏。 在这种情况下,SFML提供了一个简单的OpenGL代码集成,而不必担心太多事情。
也许你想一次在屏幕上渲染成千上万的sprites,或者你想在Window类之上添加一个特性。 OpenGL 可以帮助你。
使用OpenGL的另一个原因是创建3D游戏。虽然SFML提供了一些可以在3D环境中使用的工具,但最终它们还不足以创建功能齐全的3D体验( 3D模型、照明、阴影等)。因此,我们必须使用OpenGL。
正如本章开头提到的,OpenGL是一个巨大的API,本章不教您如何使用它的任何特性。它仅给出了如何与SFML一起使用它的一般指南。
● 在开始使用任何OpenGL调用之前,我们需要确保图形context 已经初始化。context 保存允许OpenGL运行的数据(状态、默认帧缓冲区等)。当我们创建窗口实例时,这将自动完成的:
int main()
{
sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.majorVersion = 3;
settings.minorVersion = 0;
settings.antialiasingLevel = 2;
sf::Window window(sf::VideoMode(640, 480), "OpenGL", sf::Style::Default, settings);
//Window 可以在这里接收OpenGL调用
while (window.isOpen())
{
//Game loop
}
return 0;
}
要使用任何OpenGL调用,我们需要包含
请注意,该窗口接受ContextSettings的实例(一个可选参数)。我们在第1章“SFML入门”中对此进行了一些讨论,但让我们更详细地了解这些设置的含义。ContextSettings结构类有以下五个字段,我们可以更改。
Setting | Description | 常用值范围 |
depthBits | 该字段允许我们为深度缓冲区建议每个像素的bit位数 | [0, 8, 16, 24, 32] |
stencilBits | 该字段允许我们为模板缓冲区提供每个像素的bit位数 | [0, 8] |
majorVersion | 该字段允许我们建议OpenGL的主要版本 | [1…4] |
minorVersion | 该字段允许我们建议OpenGL的次要版本 | [1…5] |
antialiasingLevel | 该字段允许我们建议多采样级别 | [0…16]。通常情况下,2的幂可以得到最好的结果——1,2,4,8,16 |
这些值并不强制SFML使用它们,并且如果我们尝试在不支持它们的硬件上使用它们,也不会抛出异常。SFML选择系统支持的最接近(也可能是最佳)选项。 我们可以通过从窗口获取ContextSettings来检查选择了哪些选项:
下面是这个示例结果:
一旦我们设置好了一切,就可以创建我们熟悉的游戏循环:
由于RenderWindow : : pushGLStates ( )是一个高价的操作,我们的GameObjectSFML类看起来非常低效,不是吗?给每个使用SFML的对象都保存和恢复OpenGL状态似乎比较浪费资源。这个问题的一个解决方案是在我们的基类中创建第二个渲染函数,例如,可以将其称为GameObject :: renderGL()。 在我们的主循环中,事情会有所改变:
这样,我们将节省大量不必要的驱动程序调用,并使GameObject类中的代码更加简洁。每一个不使用OpenGL的对象都会将这个方法设置为空,而其他对象会将GameObject::render ( )设置为空。
● SFML允许我们对每个应用程序使用多个窗口。对于游戏来说,有多个窗口是相当罕见的,但是对于媒体应用程序来说就不是这样了。当我们想要在特定窗口中渲染对象时,我们调用RenderWindow :: draw()。 但是,当我们想要使用OpenGL时,我们需要指定哪个窗口受其函数调用的影响。这只是由Window :: setActive()完成的。 当我们想要在窗口上开始渲染时,我们只需调用setActive()并开始使用OpenGL。
● 在这一章中,我们介绍了摄像机的重要性,以及它们在SFML中的功能。我们看到了如何转换View类,以及如何创建设置分屏多人游戏。在本章的第二部分,我们讨论了OpenGL以及它如何与SFML结合。
在下一章中,我们将探讨任何游戏的三个简单概念但最终至关重要的特征——声音、音乐和文本。
本章完.....