用C++和SFML写游戏-2D 摄像机的使用(7)

本文将会介绍怎么在程序中使用摄像机和 OpenGL 。对摄像机将会深入的介绍,而对于 OpenGL,我们这里只做简单的介绍。OpenGL 是一个很大的主题,一篇文章根本就不能够将它完全介绍清楚。

本文将会介绍以下内容:

  • 什么是摄像机
  • sf::View 操作摄像机
  • 什么是 OpenGL
  • SFML 中使用 OpenGL </br>

    一、什么是摄像机

实际上,在游戏开发中不使用摄像机的可能性很小,它是游戏的基本组成部分。本质上来看,摄像机就是空间中的一个点,通过这个点你可以观察到游戏中的世界。摄像机有很多参数设置,但在本文,我们只关注 SFML 为我们提供的东西。

在开始写代码之前,我们先直观的了解一下摄像机。由于 SFML 主要用于 2D 游戏开发。因此,它的 camera 类主要采用的是正交投影(orthographic projection)。在这种投影下,物体看起来没有什么视觉上的变化。而在透视投影(perspective projection)下,它采用了一种基于人眼看到事物的方式将物体在 2D 平面中展示出来(例如,距离的不同物体的大小也不同)。从下面的图片可以直观的了解一下这两种方式的区别:

显然,在 2D 游戏中使用透视投影是没必要的,在 SFML 中甚至没有提供使用它的方式。使用 OpenGL 同样可以创建摄像机,这个将在后面介绍。

</br>

二、在 SFML 中使用摄像机

当然,我们也不是任何时候都适用摄像机。但是在一些有着庞大世界的 RPG 游戏中,我们就要用到了,因为屏幕的尺寸有限,不能完全的展示这个世界。

想要修改 sf::Window 中的摄像机,需要先解决 sf::View 类。sf::View 的行为就像是摄像机,它限制了玩家能够看到的内容。sf::View 的创建和使用如下:

1
2
3
4
5
auto wSize = window.getSize();
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y));

// 初始化 View
window.setView(view);

View 的构造函数参数 FloatRect 表示的是截取世界的一部分给玩家看。如果 FloatRect 的大小大于当前窗口,那么它将会压缩显示。

</br>

三、用 sf::View 操作摄像机

默认情况下,view 的中心是视图区域的中心,也就是说,如果我们当前窗口大小是(640,480),那么 view 的中心是(320,240)。这时候将物体渲染到 view 上的时候,物体将出现在左上角(0,0)。改变 view 的中心可以使用 View::setCenter() 或者 View::move() 方法。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
auto wSize = window.getSize();
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y));

// 设置 view 中心
view.setCenter(sf::Vector2f(0, 0));

window.setView(view);

sf::Vector2f spriteSize = sf::Vector2f(118, 114);
sf::Sprite sprite(AssetManager::GetTexture("myTexture.png"));
sprite.setTextureRect(sf::IntRect(0, 0, spriteSize.x, spriteSize.y));
sprite.setOrigin(spriteSize * 0.5f);

上面的代码将 view 的中心设置为(0,0),那么世界中的该位置将会出现在屏幕中心。sprite 默认位置是(0,0),因此该精灵会出现在屏幕中心。

通常,将 view 的中心设置在我们主要角色上是一个简单有效的方式去执行摄像机位置逻辑,同样,在每一帧更新的时候只需要一下两行代码:

1
2
view.setCenter(sprite.getPosition());
window.setView(view);
需要注意的是,更新每一帧的时候需要再次调用 `RenderWindow::setView()`,因为**RenderWindow** 只保存 view 的副本,只更改旧的 view 不会影响存储在 **RenderWindow** 中的 view。

</br>

四、旋转和缩放 view

尽管这两个属性很少使用,但是当我们需要实现摄像机的一些特殊功能的时候会派上用场。

旋转的话有两个方法,View::setRotation()View::rotate()。这两个方法的区别是 View::setRotation() 会将物体的旋转角度固定为给定的值,而 View::rotate() 则会在物体的当前旋转度上加上给定的值。

通过一个例子来看看,这里使用了四个精灵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
auto wSize = window.getSize();
sf::View view(sf::FloatRect(0, 0, wSize.x, wSize.y));
view.setCenter(sf::Vector2f(0, 0));
// 设置旋转 45 度
view.setRotation(45);
window.setView(view);

sf::Vector2f spriteSize = sf::Vector2f(118, 114);
auto& texture = AssetManager::GetTexture("myTexture.png");

sf::Sprite sprite1(texture);
sprite1.setOrigin(spriteSize * 0.5f);
sprite1.setPosition(sf::Vector2f(-100, -100));

sf::Sprite sprite2(texture);
sprite2.setOrigin(spriteSize * 0.5f);
sprite2.setPosition(sf::Vector2f(100, -100));


sf::Sprite sprite3(texture);
sprite3.setOrigin(spriteSize * 0.5f);
sprite3.setPosition(sf::Vector2f(100, 100));


sf::Sprite sprite4(texture);
sprite4.setOrigin(spriteSize * 0.5f);
sprite4.setPosition(sf::Vector2f(-100, 100));

分别旋转 0 度45 度,效果如下

旋转 0 度

旋转 45 度

旋转视图(view)在游戏开发方面的用途有限。 它可以用于制作特定事件的动画 - 主角的死亡(随着旋转的慢速放大),受到伤害(相机略微抖动)。 我们也可以使用它来围绕一个居中的角色在一个自上而下的游戏中旋转世界。 通常,旋转很有用,但是使用场所有限。

接下来是缩放,缩放的话可以使用 View::zoom()。传入的参数为 2 的话代表缩小 2 倍,传入 0.5 则是放大 2 倍。也可以通过 View::setSize() 改变 view 的大小实现缩放。

需要注意的是,使用缩放的时候 view 存储的是当前 view 大小,也就是说,如果我们多次调用缩放函数,view 会基于上一次的 view 进行缩放。举个例子,两次使用 zoom(2) 的时候,view 的大小会是 1/2*1/2*size = 1/4*size

</br>

五、视口(viewport)

每个视图(view)都有一个与之关联的视口(viewport)。 视口是显示视图的窗口区域。 该区域由矩形表示,该矩形使用标准化坐标[0 ... 1]。 默认情况下,视口等于视图的大小(0,0,1,1)。 我们可以通过调用View::setViewport()来改变它。 假设我们只想在屏幕的左上象限中渲染场景。 可以这么做:

1
2
3
4
5
6
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);

运行后的结果如下:

</br>

六、坐标转换

RenderWindow::mapPixelToCoords() 方法用于将目标坐标转换为世界坐标。一开始的时候,这两个坐标系下的物体坐标是对应的,但是当我们使用自定义 view 或者改变窗口大小的时候,这个对应关系就不存在了,这时候,需要调用这个方法获取物体的世界坐标。

1
2
3
4
5
6
7
sf::Event ev;
while (window.pollEvent(ev)) {
if (ev.type == sf::Event::MouseButtonPressed) {
sf::Vector2f sceneCoords = window.mapPixelToCoords(
sf::Vector2i(ev.mouseButton.x, ev.mouseButton.y));
}
}

</br>

七、OpenGL

OpenGL 是一种跨平台的图形 API,用作与图形卡通信的接口。 任何图形 API 最重要的特性是它能够在屏幕上渲染对象。 虽然 OpenGL 确实如此,但是它还有许多其他有用的功能。

实际上,SFML 在内部使用 OpenGL 来实现功能。但是,由于 SFML 是一个高级库,因此使用它时将会存在性能损失。在大多数情况下,这部分性能并不是一个大问题,因为使用这样一个高级库的好处是它可以快速地开发应用。但是,有些情况只需要额外的性能来实现目标 FPS。在这种情况下,SFML 提供了一个简单的 OpenGL 代码集成,而不必担心太多事情。

在我们开始使用任何OpenGL调用之前,我们需要先初始化图形上下文。 上下文保存了 OpenGL 运行时的数据(状态,默认帧缓冲等)。这在我们创建窗口实例时自动完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>

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);

while (window.isOpen()) {

}

return 0;
}

要使用 OpenGL 的话,我们需要包含 ,这是 SFML 为我们在所有平台上使用提供的通用头文件。 同样重要的是,如果我们要仅使用OpenGL渲染所有东西,我们就不需要使用熟悉的RenderWindow类。 Window类很适合我们。另一方面,如果出于某种原因我们想要将 OpenGLGraphics 模块一起使用,那么我们可以回到 RenderWindow。 在这个例子中,我们只使用 Window模块

窗口采用ContextSettings的实例(可选参数)具有一下参数

参数 描述 范围
depthBits 深度缓冲器的每像素位数 [0, 8, 16, 24, 32]
stencilBits 模板缓冲区的每像素位数 [0, 8]
majorVersion OpenGL的主要版本 [1 … 4]
minorVersion OpenGL的次要版本 [1 … 5]
antialiasingLevel 多重采样级别 [0 … 16],通常使用 2 的幂次

SFML 并不强制我们一定要使用上面的参数,它有默认值。

当一切都准备好了以后,可以开始写主循环部分的代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (window.isOpen()) {
sf::Event ev;
while (window.pollEvent(ev)) {
// 更新帧

// 设置清除色
glClearColor(1, 0, 0, 1);
// 清除屏幕和深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 绘制物体
window.display();
}
}

由于 SFML 窗口是为使用 OpenGL 而构建的,因此集成它就像前面的代码一样简单。 但是,当涉及到混合Graphics模块OpenGL 时,它会变得稍微混乱。 每次我们在使用 SFML 进行渲染和使用 OpenGL 进行渲染时,我们都必须保存和恢复 OpenGL 状态。 这是使用Graphics模块OpenGL 绘制形状时的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 用 OpenGL 绘制对象

window.pushGLStates();

// 用 SFML 绘制对象

window.popGLStates();

// 继续使用 OpenGL 绘制

window.display();

SFML允许我们在每个应用程序中使用多个窗口。 游戏拥有多窗口是非常罕见的,但是媒体应用程序是这样的。当我们想要在特定窗口中渲染对象时,我们调用RenderWindow::draw()。 但是,当我们想要使用 OpenGL 时,我们需要指定哪个窗口受需要用到 OpenGL,这只是由Window::setActive()完成的。当我们想要在窗口上开始渲染时,我们只需调用setActive()并开始使用 OpenGL

到这里我们的 OpenGL 部分就结束了。

在下一篇文章中,我们将探讨游戏中的声音,音乐和文本的三个概念上简单但却至关重要的特性😃。

文章作者: Sshpark
文章链接: http://sshpark.com.cn/2019/09/07/用C-和SFML写游戏-2D-摄像机的使用(7)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sshpark