《译 SFML Essentials 英文版》—— 《第一章》 SFML 入门

2022-11-18 11:45:35 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。


创建窗口


当你开始开发一款游戏时,你可能想做的第一件事便是打开一扇窗口。在SFML中,这再容易不过了。创建窗口只需要一行代码:

代码语言:javascript复制
#include <SFML/Graphics.hpp>

int main()
{
    sf::Window window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
    sf::sleep(sf::seconds(3)); //添加此代码 这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
  
    return 0;
}

● main函数唯一做的是通过调用sf :: Window构造函数初始化窗口变量,之后程序退出。 还可以使用默认构造函数打开一个窗口,然后调用window::create() 该函数,这个函数的参数与构造函数的参数完全相同。如果在已经打开的窗口上调用Window::create ( ),它会关闭窗口并用新的一组参数重新初始化该窗口。

● 请注意 Window 和VideoMode 都在sf命名空间中。SFML中的每个类都位于该命名空间之下,该命名空间将SFML中的所有类与其他库中的类区分开。

如果我们运行示例中的代码,就不会看到太多内容。程序在创建窗口后立即退出。这是因为我们只是创建了一个窗口,而没有对它做任何操作,程序在main ( )方法结束后自然退出。事实上,我们创建了一个窗口,这并不意味着它可以完全正常工作(至少现在还没有)。我们必须根据我们想要它做的事情来对它进行编程。现在,让我们通过延迟窗口的线程来阻止main函数完成。SFML为此提供了一个简单的接口;只需在创建窗口的行之后添加sf::sleep(sf::seconds(3))。现在,窗口在睡眠期间是清晰可见的。

● 我们可以在创建窗口时指定各种配置—— window size, title, style, and graphics settings. 您可能已经注意到我们将两个参数传递 给 Window构造函数 -—— 一个VideoMode实例和一个字符串(标题)。 构造函数实际上最多可以使用四个参数,最后两个是可选的 – Style 和ContextSettings。下一部分将介绍这些参数的含义以及如何使用它们。


VideoMode


● VideoMode类包含显示一个窗口的信息,例如:width, height, and bits per pixel. 最后一个参数是表示每个像素颜色的位数。它的默认值为32,如果我们想要创建一个全屏窗口,所提供的值必须由机器的显示器和显卡支持。如果我们为全屏窗口选择无效的参数,窗口创建将会失败的。可以使用 VideoMode:: isValid() 方法检 查VideoMode 类的有效性,结果返回一个布尔值。

● 如果我们需要根据桌面的大小 (or the pixel depth) 创建窗口,VideoMode::getDesktopMode()是一个静态方法。另一方面,如果我们要在全屏模式下创建一个窗口,我们可能需要使用 VideoMode::getFullScreenModes() 检查可用的分辨率。这将返回video modes 的std::vector,我们可以自己选择其中一种模式,或者让用户决定哪种模式最适合他们。

然而,仅仅指定全屏的VideoMode 还不足以创建全屏窗口。我们也需要设定一个窗口的Style。


Style


● Style参数是一个bit mask 。mask 是 flags 的组合,其中每个flags代表mask 的一个特定位。 在这种情况下,flags存储在 sf::Style 名称空间中的enum中。我们可以使用flags 的组合来创建所需的标志掩码。以下是SFML提供的Styles:

Enum value

描述

sf::Style ::None

这个窗户没有任何装饰,也不能与任何其它的Style 一起使用

sf::Style ::Titlebar

添加一个标题栏

sf::Style ::Resize

这增加了一个最大化按钮。还允许手动调整窗口的大小

sf::Style ::Close

添加一个关闭按钮

sf::Style ::Fullscreen

这将以全屏的形式打开窗口。 请注意,这不能与任何其他样式结合使用并且需要有效的VideoMode

sf::Style ::Default

这将标题栏、大小调整和关闭组合在一起。这是默认样式。

可以通过使用位运算符来组合不同的样式。如果我们想要一个带有标题栏和关闭按钮的窗口,我们可以这样写:

代码语言:javascript复制
sf::Uint32 style = sf::Style::Titlebar | sf::Style::Close;

这里要做的唯一一件事就是将该样式作为Window构造函数的第三个参数传递。


ContextSettings


● 最后一个参数是ContextSettings的一个实例。 它们构成是一组设置的集合,描述了所需的rendering context. SFML使用OpenGL进行底层渲染,因此这些设置与它直接相关。 可用的context设置如下:

  • depthBits ——这指的是深度缓冲区位数。
  • stencilBits —— 这指的是模板缓冲区的位数。
  • antialiasingLevel: —— 这引用了所请求的多重采样级别数 multisampling levels number .
  • majorVersion and minorVersion: —— 这些是指所要求的OpenGL版本

这些设置中的每一个都将在第5章(操作2D摄像机)中得到更详细的解释,您将学习如何使用OpenGL直接渲染物体。


Disabling the mouse cursor (禁用鼠标光标)


● Window类有一个方法,可以在窗口上设置光标的可见性 —— Window :: setMouseCursorVisible()。 对于不使用光标的游戏,或者当我们想要将光标的图像更改为与默认情况不同的内容时,这是非常有用的。


The game loop


每个游戏都需要一个循环。这就是它的动力。否则程序就会结束,我们就看不到太多内容了。下面是一个典型的游戏循环:

代码语言:javascript复制
#include <SFML/Graphics.hpp>

int main()
{
  
    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
    sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
    while (window.isOpen()) //游戏循环
    {
        /*
        处理输入——处理来自输入设备和窗口的事件。
        更新帧——更新场景中的对象
        渲染帧 —— 将场景中的对象渲染到窗口上
         */
    }
  
    return 0;
}

典型的游戏循环有三个主要阶段:

  • 处理输入——处理来自输入设备和窗口的事件。
  • 更新帧——更新场景中的对象
  • 渲染帧 —— 将场景中的对象渲染到窗口上

SFML中的 Input handling 可以通过捕获事件(由窗口分派的事件)或直接查询输入设备的当前状态来完成。这两种方法有不同的用途。例如,我们可能希望关闭按钮按下事件上的窗口,或者只要按下某个键,就将我们的主角向右移动(直接键查询)。

● 在捕捉和使用事件后,我们到达 update frame 阶段。在这个阶段,我们想要推进我们的游戏逻辑,更新我们的世界状态。 在完成对象 update 之后就是循环的最后一个阶段,我们清除从上次绘制的所有内容,并再次渲染屏幕上的每个对象。

●接下来的阶段是 渲染帧。 在这里,我们清除从上次绘制的所有内容,并再次渲染屏幕上的每个对象。

回到我们的游戏循环的例子,它目前没有执行它应该执行的事情,如果我们尝试运行代码,很明显窗口不响应输入,这是因为我们没有执行循环处理输入的三个重要步骤中的第一个步骤 ( 处理输入)。


Event handling (处理事件)


可以通过 bool Window :: pollEvent(sf :: Event&event) 从窗口顺序询问( polled )事件。 如果有一个事件等待处理,该函数将返回true,并且事件变量将填充(filled)事件数据。 如果不是,则该函数返回false。 同样重要的是要注意,一次可能有多个事件; 因此我们必须确保捕获每个可能的事件。 以下是典型的事件循环:

代码语言:javascript复制
  while (window.isOpen()) //游戏循环
    {
        sf::Event event;
        while (window.pollEvent(event))
        {

        }
    }

现在运行代码会产生更满意的结果——可以移动窗口、调整大小和最小化它。然而,仍然有一个问题——关闭按钮不起作用。SFML没有假设在用户点击关闭按钮后窗口应该关闭。也许我们想保存玩家的进度,或者先问问他们是否确定。这意味着我们必须自己实现关闭按钮功能。

在继续之前,请务必注意C 中的Event类包含一个union。 这意味着其中只有一个成员有效。 访问任何其他成员将导致未定义的行为。 我们可以通过查看 event types 来获取有效成员。

event types 在逻辑上可以分为四个部分,具体取决于它们与什么有关:

  • Window
  • Keyboard
  • Mouse
  • Joystick

Window related events


Enum value

Member associated

Description

Event::Closed

None

当操作系统检测到用户想要关闭窗口时触发此事件——关闭按钮、组合键等等。

Event::Resized

Event::size holds the new size of the window

当操作系统检测到窗口已手动调整大小或已使用Window :: setSize()时,将触发此事件。

Event::LostFocus Event::GainedFocus

None

当窗口失去或获得焦点时触发此事件。失去焦点的窗口不会接收键盘事件。

注解:失去焦点(LostFocus)和获取焦点(GainedFocus)是一个鼠标行为,例如当点击数个输入框其中的一个使其处于编辑输入状态的时候就是获得可焦点,当点击其他输入框或者其他区域就会使这个输入框失去焦点。


Keyboard related events


Enum value

Member associated

Description

Event::KeyPressed Event::KeyReleased

Event :: key 保存 按下/释放 的键

当按下或释放焦点窗口上的单个按钮时,将触发此事件。

Event::TextEntered

Event :: text保存 UTF-32 unicode 的字符值

每次输入字符时都会触发此事件。这将从用户输入中生成可打印的字符,对于文本字段非常有用。


Mouse related events


Enum value

Member associated

Description

Event::MouseMoved

Event::mouseMove holds the new mouse position

当鼠标在窗口内更改其位置时会触发此事件。

Event::MouseButtonPressed Event::MouseButtonReleased

Event::mouseButton holds the pressed / released button and the mouse position

在窗口内按下鼠标按钮时会触发此事件。 目前支持五个按钮 —— left, right, middle, extra button 1, and extra button 2.

Event::MouseWheelMoved

Event::mouseWheel 保存了鼠标的滚轮移动了多少时间以及鼠标位置

当滚动轮在窗口内移动时触发此事件


joystick related events


Enum value

Member associated

Description

Event::JoystickConnected Event::JoystickDisconnected

Event :: joystickConnect 保存刚才连接 / 断开的操纵杆的ID

当操纵杆连接或断开时触发此事件。

Event::JoystickButtonPressed Event::JoystickButtonReleased

Event :: joystickButton 保存按下的按钮次数和操纵杆ID

按下操纵杆上的按钮时会触发此操作。 SFML最多支持8个操纵杆,每个操纵杆最多32个按钮。

Event::JoystickMoved

Event :: joystickMove保存移动的坐标轴,新的坐标轴位置和操纵杆ID

当操纵杆的坐标轴移动时触发。可以通过Window::setJoystick threshold()设置移动阈值。SFML最多支持8个轴


Using events


● 在通过调用Window::pollEvent()获得事件之后,我们可以通过查看 event::type 来检查其类型。 该事件的类型为Event :: EventType,它是Event类中的枚举。 以下是典型的关闭事件的实现方式:

代码语言:javascript复制
 while (window.isOpen()) //游戏循环
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
        }
    }

这里,Window :: close()函数将负责关闭窗口。 如果窗口变量超出范围,则调用析构函数,并且关闭窗口。

● 如果我们想处理多个事件,那么使用Switch语句是有意义的,因为它提高了可读性。让我们看看键盘键是如何按下和释放的:

代码语言:javascript复制
#include <SFML/Graphics.hpp>
// 如果我们用Window类,我们必须使用头文件#include <SFML/Window.hpp >

int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
    sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
    sf::CircleShape shape(100.f); //创建图形对象
    shape.setFillColor(sf::Color::Red); //改shape的填充颜色
    while (window.isOpen()) //游戏循环
    {
        sf::Event event;
        while (window.pollEvent(event)) //可以通过  bool Window :: pollEvent(sf :: Event&event) 从窗口查询事件。
            //如果有一个事件等待处理,该函数将返回true,
        {
            switch (event.type)
            {//通过调用Window :: pollEvent()获取事件后,我们可以通过查看Event :: type来检查它的类型。
                //该事件的类型为Event :: EventType,它是Event类中的枚举。
            case sf::Event::EventType::Closed:
                window.close();
                break;
            case sf::Event::EventType::KeyPressed:
                //如果按下空格键,请更改标题
                if (event.key.code == sf::Keyboard::Key::Space) //键盘按下事件
                {
                    window.setTitle("Space pressed"); // 修改该窗口的名字
                }
                break;

            case sf::Event::EventType::KeyReleased:  //键盘松开事件
                //如果空格键又被松开,则改变标题
                if (event.key.code == sf::Keyboard::Key::Space)
                {
                    window.setTitle("Space released");
                }//如果松开了Escape键,则关闭窗口
                else if (event.key.code == sf::Keyboard::Key::Escape)
                {
                    window.close();
                }
                break;
            default:
                break;
            }
        }
        window.clear();
        window.draw(shape);
        window.display();
    }

    return 0;
}

● 上图中的代码演示了每次按下和释放Space键时我们如何捕捉事件以更改窗口的标题。除此之外,当Escape键被释放时,窗口就会关闭。Event :: key包含一个名为code的成员,code是Keyboard :: Key类型的枚举。您可以使用此方法来处理其余的 event types,而不会有太多困难。但是,Event::EventType::TextEntered 的情况更有趣一些。可以以一种相对简单的方式检测和处理单个按键/释放。不过,当涉及到某些特定的字符时,情况就变得有点复杂了。例如,如果我们想检测 ” ! ” 符号已经输入,我们必须查找两个单独的键是否在同一时间被按下 ” Shift” 键 和 “ 1 ” 键。在这种情况下,SFML通过提供简单易用的TextEntered事件为我们节省了大量的工作。

事件仅在按下组合键时才触发; 意味着单个键(例如,只有Shift)可能不会触发事件。 当然,如果单独按下K,事件将被正常触发,并将包含该字符。


看一下这个例子,其中一个字符串是使用TextEntered事件由字符组合而成的,当按下Enter(或Return)按钮时,文本被设置为标题:

代码语言:javascript复制
 #include <SFML/Graphics.hpp>
// 如果我们用Window类,我们必须使用头文件#include <SFML/Window.hpp >

int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
    sf::CircleShape shape(100.f); //创建图形对象
    shape.setFillColor(sf::Color::Red); //改shape的填充颜色

    sf::String buffer;
    while (window.isOpen()) //游戏循环
    {
        sf::Event event;

        while (window.pollEvent(event))
        {
            switch (event.type)
            {
            case sf::Event::EventType::Closed:
                window.close();
                break;
            case sf::Event::EventType::TextEntered:
                //将字符直接添加到字符串中
                buffer  = event.text.unicode;
                break;

            case sf::Event::EventType::KeyReleased:
                //将标题更改为当前缓冲区并清除缓冲区
                if (event.key.code == sf::Keyboard::Key::Return)
                {
                    window.setTitle(buffer);
                    buffer.clear();
                }
                break;
            default:
                break;
            }
        }
    }
        window.clear();
        window.draw(shape);
        window.display();
    return 0;
}

● 注意,我们使用的字符串缓冲区的类型是sf::string,而不是std::string。创建sf::string类是为了自动处理字符串类型和编码之间的转换。请注意,我们使用的字符串缓冲区是sf :: String类型而不是std :: string。 创建sf :: String类是为了自动处理字符串类型和编码之间的转换。 因此,我们不必担心键盘布局上的语言或符号 —— 它可以存储任何语言的任何字符。

要完成 event handling,重要的是还有一种方法可以替代从窗口中提取事件的方式。除了使用Window :: pollEvent()之外,我们还可以使用 bool Window :: waitEvent(Event&event),它阻止线程直到收到一个事件。它只在内部发生错误时返回false(某种类型的错误或异常),否则总是返回true. 当我们要求用户在应用程序继续运行之前执行某些操作时,或者如果我们想在另一个线程上处理输入时,这是非常有用的. 在后一种情况下,只有该线程被阻止,允许游戏循环继续运行。 现在我们已经讨论了事件,让我们继续讨论更有趣的事情。


Shape rendering and transformations (形状的渲染和转换)


● 如果没有要渲染的对象,我们就不需要窗口;如果我们不想使用输入来手动绘制那些对象,我们就不需要事件。SFML为我们在屏幕上渲染对象提供了相当多的方法,我们将在这本书中探索主要内容。在我们开始渲染之前,我们需要确保渲染循环是正确的。


The render frame


● 还记得Winodw class 吗? 它并没有没有多大用处,因为它没有提供绘制SFML形状的接口 。我们必须使用一个名为RenderWindow的类来做到这一点。 此类派生自Window类并添加绘图功能。不过不用担心,它不会从父类中删除任何功能,它只是在其上添加了更多功能。 因此,我们仍然可以创建它,查询事件等,就像我们使用基类Window一样。 以下是具有渲染周期的游戏循环示例:

代码语言:javascript复制
#include <SFML/Graphics.hpp>
int main()
{
   
    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
    sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
    sf::CircleShape shape(100.f); //创建图形对象
    shape.setFillColor(sf::Color::Red); //改shape的填充颜色
    while (window.isOpen()) //游戏循环
    {
        //处理事件
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
        }

        // 更新场景

        // 渲染周期
        window.clear(sf::Color::Black);

        //渲染对象
        window.draw(shape);
        window.display();
    }
    return 0;
}

这里需要注意的是RenderWindow类来自SFML图形模块,这意味着我们必须包含<SFML / Graphics.hpp>,而不是<SFML / Window.hpp>。但是,因为它是从Window类派生的,所以它仍然可以在我们的代码中使用,除了变量类型之外,没有任何改变。

● 如果你有任何游戏编程经验,渲染周期看起来会很简单。基本上是这样的:

  • 清除你要画的画布
  • 绘制画布
  • 显示画布

就像下面这样: window.clear(); window.draw(shape); window.display();

这个渲染程序会发生在每一帧(循环周期)。如果你不熟悉渲染过程,扔掉最后一帧的所有东西,重新渲染场景中的所有对象(即使是那些自上次以来没有改变的对象)可能会显得有点奇怪和浪费。但是,显卡经过了很好的优化,可以处理这种例程,并尽可能地提高效率和性能。避免使用任何其他结构,因为它只会减慢你的速度,而不会带来任何重大的好处。

● 另一件要注意的事情是,我们渲染的模板画布是双缓存的。双缓存画布在渲染中非常普遍。它的工作原理很简单 —— 画布中有两面可以使用。在渲染一帧中我们只使用其中一面 — 即没有在屏幕上显示的那一面。在当前帧渲染完成后,我们翻转画布以把已经有渲染结果的这一面显示到屏幕上。下一帧我们就渲染到画布的另一面,如此反复。双缓存画布技术让我们可以在渲染完成后才显示渲染结果。在sfml中 我们通过调用Windows display() 来显式画布。

除此之外,Window :: display()方法可以让当前线程休眠一段所计算好的时间来实现目标帧数(每秒帧数)。 我们可以通过在程序开头调用Window :: setFramerateLimit()来设置所需的帧数。 该函数不保证将帧数设置成我们设置的帧数,而是设置成近似值。

Window :: clear()清除画布以重新绘制 。 请注意,它采用sf :: Color参数,这是一种颜色的RGBA表示。我们可以通过调用构造函数并单独传递每个值来手动初始化它,也可以使用预先设置的颜色之一。 例如 Color :: Red,Color :: Blue,Color :: Magenta等


Shape drawing


● 现在我们已经熟悉了渲染程序,让我们在屏幕上渲染一些Shapes 。我们将从基本形状开始,并在稍后探索其他选择。当我们想要绘制一个图形时,我们必须首先创建对象。下面是两个形状的初始化代码。把它放在游戏循环之前:

代码语言:javascript复制
    sf::CircleShape circleShape(100.f); //创建圆形对象
    circleShape.setFillColor(sf::Color::Red); //改shape的填充颜色
    circleShape.setOutlineColor(sf::Color::Blue);  //设置框的颜色
    circleShape.setOutlineThickness(3);//设置框的像素数

    sf::RectangleShape rectShape(sf::Vector2f(50, 50)); // 创建矩形对象
    rectShape.setFillColor(sf::Color::Green);

● 在本例中出现了一些新类—— CircleShape、RecTangleShape和 Vector2f 。

● Vector2f类的用途 —— 它是一个包含两个浮点数的2D向量。还有一些类,如Vector2i(用于整数),Vector2u(用于无符号整数),Vector3i(用于保存3D向量的整数)和Vector3f(用于保存3D 向量的浮点数)。

● CircleShape,RectangleShape 和ConvexShape派生自抽象类Shape,类Shape由一组顶点(点)定义。CircleShape 是一个有固定顶点数量的普通多边形。我们可以使用构造函数中的第二个参数(可选的,默认值为30)指定圆的半径。另一方面,RectangleShape 总是有四个顶点。这两种构造函数都有它们的大小 —— 圆的半径和矩形的宽度和高度。

● ConvexShape是一种我们必须显式指定顶点的形状。 顶点数量没有限制,但它们必须形成凸形,否则形状将无法正确绘制。

● 除此之外,shapes 可以有 colors and outlines,可以使用 Shape :: setFillColor(),Shape :: setOutlineColor()和Shape ::setOutlineThickness()进行修改。 最后一个设置outline 的像素数。

● 要渲染前面的shapes,我们可以使用 RenderWindow :: draw()函数。 以下是我们如何将它实现到render frame 中:

代码语言:javascript复制
int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以
   sf::CircleShape circleShape(100); //创建圆形对象
    circleShape.setFillColor(sf::Color::Red); //改shape的填充颜色
    circleShape.setOutlineColor(sf::Color::White);  //设置外框或者说外形的颜色
    circleShape.setOutlineThickness(50);//设置外框或者说外形的厚度

    sf::RectangleShape rectShape(sf::Vector2f(50, 50)); // 创建矩形对象
    rectShape.setFillColor(sf::Color::Green);
   

        window.clear(sf::Color::Black);
        window.draw(circleShape);
        window.draw(rectShape);
        window.display();
        system("pause");
        return 0;
}

● 我们可以立即注意到渲染顺序有很大的不同。首先必须渲染背景对象,然后是前景中的任何对象。在这个例子中,圆首先被渲染,所以它在背景中,而矩形位于前景中圆的顶部。

● 我们可以通过使用 ConvexShape :: setPointCount()函数指定点数来使用 ConvexShape,并使用 ConvexShape :: setPoint()按顺序设置这些点。这里有一个三角形的例子:

代码语言:javascript复制
int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    sf::ConvexShape triangle;
    triangle.setPointCount(3); // 设置triangle 有几个顶点
    triangle.setPoint(0, sf::Vector2f(100, 0)); // 设置第一个顶点的位置
    triangle.setPoint(1, sf::Vector2f(100, 100));
    triangle.setPoint(2, sf::Vector2f(0, 100));

    triangle.setFillColor(sf::Color::Blue); // 设置triangle 的颜色
    triangle.setOutlineColor(sf::Color::Yellow); // 设置 triangle 的边框的颜色
    triangle.setOutlineThickness(3); // 设置 triangle 的边框颜色的厚度

    window.clear();
    window.draw(triangle);
    window.display();
    system("pause");
    return 0;
}

SFML中不支持凹形。 但是,我们仍然可以通过创建多个凸形并在正确的位置渲染它们来绘制凹形。 如果用三角形来做这项工作,这种方法称为三角分割多边形。


Shape transformation


● 我们现在知道如何在屏幕上绘制shapes , 但是,无论我们绘制了多少,它们似乎都会出现在屏幕的左上角。 这意味着我们需要改变shapes 的位置。 可以使用 Shape :: setPosition()函数可以改变形状的位置 。 Shape :: setRotation()可以使形状旋转,Shape ::setScale()可以使形状缩放。 实际上,这些函数都是 sf :: Transformable的一部分,Shape类派生自它。

代码语言:javascript复制
int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "HUANGCHENGTAO!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    sf::RectangleShape rect(sf::Vector2f(50, 50)); //创建一个 RectangleShape 对象
    rect.setFillColor(sf::Color::Red); //设置rect 的颜色
    rect.setPosition(sf::Vector2f(50, 50)); //设置 rect 该形状的位置
    rect.setRotation(30); // 把 rect 该形状 旋转30 度
    rect.setScale(sf::Vector2f(2, 1));

    window.clear();
    window.draw(rect);
    window.display();
    system("pause");
    return 0;
}

注意,我们正在创建一个矩形,实际上是一个正方形,宽度和高度为50像素。但是,我们将它缩放为2:1,因此它的渲染比它的原始大小更长。接下来我们需要提到的是,矩形是轻微倾斜的,这是预期的,因为我们把矩形旋转了30度。在本例中,我们将位置直接设置 (50,50)。 然而, 还有一种方法 可以移动 transformable 对象, 可以使用 Transformable::move() 函数, 该函数 传递一个 vector ,表示我们想要将 transformable 对象从当前位置不断的向某一个方向移动其位置。

● 到目前为止,我们创建的所有东西基本上都是静态的,所以现在让我们为我们的对象添加一点生命。为此,我们需要使用 “ 更新帧 ”,我们现在还不能使用它。这是我们开始“ 渲染帧 ”之前的部分。记住,典型的游戏框架(循环)是这样的:

  • 处理输入——处理来自输入设备和窗口的事件。
  • 更新帧——更新场景中的对象
  • 渲染帧 —— 将场景中的对象渲染到窗口上

● 在渲染对象之前更新对象是很重要的,否则它们的当前状态将无法正确渲染 —— 最后一帧将使用上一帧的状态来渲染。

下一个示例显示了我们如何使用平移和旋转的组合来创建简单的动画:

代码语言:javascript复制
#include <SFML/Graphics.hpp>
// 如果我们用Window类,我们必须使用头文件#include <SFML/Window.hpp >

int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "Animation!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    window.setFramerateLimit(60); //每秒设置目标帧数
    sf::RectangleShape rect(sf::Vector2f(50, 50));
    rect.setFillColor(sf::Color::Red);
    rect.setOrigin(sf::Vector2f(25, 25)); //设置原点
    rect.setPosition(sf::Vector2f(50, 50));

    while (window.isOpen())
    {
        // Handle events here
      

        //Update frame
        rect.rotate(1.5f); //旋转 rect 对象
        rect.move(sf::Vector2f(1, 0));

        //Render frame
        window.clear(sf::Color::Black);
        window.draw(rect);
        window.display();
    }
    system("pause");
    return 0;
}

代码是我截图的,所以不动的, 把代码复制到编译器中,运行,就可以动了。

● 从这个例子我们可以看出,第一个问题是如何以及在何处设置帧速率限制——就在窗口初始化之后。这将限制我们的游戏逻辑接近每秒钟60帧。请记住,这控制了帧速率的上限。 如果帧数超过 1 /60 秒 来完成 (handle events, update objects, and render) ,那么帧数将降到60以下 。然而,使用我们的简单代码,这是极不可能的。

● RectangleShape::setOrigin() 函数: 一个对象的原点决定了它应该如何在屏幕上渲染。它是物体平移、旋转和缩放的原点。

● 在上述代码中我们有一个大小为50 x 50的正方形。该正方形的中心是(25,25),所以我们需要设它为物体的原点。 否则,该对象将开始围绕其默认原点(0,0)旋转。 关于原点的最后一点需要注意的是,它是Transformable类的一部分,因此它的所有派生类都可以访问它。

● 就我们的动画而言,这个过程非常简单。在每一帧中,我们将正方形旋转1.5度并向右移动1个像素。通过将帧速率设置为每秒 60帧 ,我们可以估计,在每秒之后,正方形将旋转大约90度(1.5 x 60),向右移动60像素(1p x 60) . 然而,以这种方式执行游戏逻辑(依赖于帧s数)是非常不可靠和危险的。我们将在第3章中探讨如何在执行动画和游戏逻辑时管理时间。​​​​​​​

现在,让我们看看如何实时控制形状。​​​​​​​


Controlling shapes


● 使形状移动的一种方法是使用事件处理。 当玩家点击某个键时,我们开始移动该对象,并且当该键被释放时我们可以停止移动该对象。 以下是代码示例的屏幕截图:

代码语言:javascript复制
int main()
{

    sf::RenderWindow window(sf::VideoMode(200, 200), "Animation!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    window.setFramerateLimit(60); //每秒设置目标帧数
    sf::RectangleShape rect(sf::Vector2f(50, 50));
    rect.setFillColor(sf::Color::Red);
    rect.setOrigin(sf::Vector2f(25, 25)); //设置原点
    rect.setPosition(sf::Vector2f(50, 50));

    bool moving = false;
    while (window.isOpen())
    {
        // Handle events here
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::EventType::KeyPressed && event.key.code == sf::Keyboard::Right)
            {
                moving = true;
            }
            if (event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Right)
            {
                moving = false;
            }
            if (event.type == sf::Event::EventType::Closed)
            {
                window.close();
            }
        }


        //Update frame
        if (moving)
        {
            rect.rotate(1.5f); //旋转 rect 对象
            rect.move(sf::Vector2f(1, 0)); // moving object
        }
        //Render frame
        window.clear(sf::Color::Black);
        window.draw(rect);
        window.display();
    }
    system("pause");
    return 0;
}

运行结果是我截图的,不会动, 代码复制到编译器中, 按键盘方向右键,正方形就会向右移动, 停止按键,正方形就会停止移动。

● moving变量确认我们是否应该在当前帧中移动对象。 当我们按下或释放右箭头键时,该变量的值会发生变化。这段代码告诉我们—— “当前是否按下了正确的箭头键?” 幸运的是,SFML提供了一个非常简单的接口来检查输入设备( keyboard, mouse, and all joysticks)的当前状态 。

● 检查键 的状态并不比调用单个静态函数—— Keyboard :: isKeyPressed()更难。 当我们传递一个键值作为参数时,我们得到当前是否按下该键的状态。 但是,此功能不考虑窗口的焦点。 因此,想象一下玩家最小化窗口并浏览互联网。 如果玩家按下给定的键,该功能仍将返回true。

● 以下代码更好:

代码语言:javascript复制
        //Update frame
        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) // 这里发生变化
        {
            rect.rotate(1.5f); //旋转 rect 对象
            rect.move(sf::Vector2f(1, 0)); // Move Object
        }

正如您所看到的,我们不需要在这种情况下存储任何键状态 – SFML为我们做了这一点。

● 我们可以用类似的方法检查其他输入设备的状态。

● 最后,有操纵杆。 由于所有函数都是静态的,我们需要使用参数Id指定我们要查找的操纵杆。 功能如下:

Function

Arguments

Description

Joystick :: isConnected()

ID

这个函数检查给定ID的操纵杆是否连接

Joystick :: hasAxis()

ID, axis

这个函数检查操纵杆是否有指定的坐标轴

Joystick :: getButtonCount()

ID

这个函数获取操纵杆上按钮的数量

Joystick :: getAxisPosition()

ID, axis

该函数获取范围[0,1]内的轴的值

Joystick :: isButtonPressed()

ID, button

这个函数检查给定操纵杆上的按钮是否被按下

● 现在让我们讨论最后一个例子,它结合了本章的许多主题。我们采用了一个非常简单的游戏,玩家在游戏中扮演一个绿色方块,他应该在不接触任何红色方块的情况下到达蓝色方块。下面是一个辅助函数,它可以帮助我们很容易地初始化类似的 RectangleShape 对象,而无需重复代码:

代码语言:javascript复制
void initShape(sf::RectangleShape &shape, sf::Vector2f const &pos, sf::Color const &color)
{
    shape.setFillColor(color);
    shape.setPosition(pos);
    shape.setOrigin(shape.getSize() * 0.5f); // The center of the rectangle
}

initShape()函数非常简单 —— 它接受一个shape 、vector 和color 这三个参数,并将它们分配给 RectangleShape对象。 该功能还将shape 的原点设置为其中心。

● 下一步是初始化对象:

代码语言:javascript复制
 sf::RenderWindow window(sf::VideoMode(482, 180), "Bad Squares!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    window.setFramerateLimit(60); //设置每秒目标帧数

    sf::RectangleShape playerRect(sf::Vector2f(50, 50));
    sf::Vector2f startPos = sf::Vector2f(50, 50);
    initShape(playerRect, startPos, sf::Color::Green);

    sf::RectangleShape targetRect(sf::Vector2f(50, 50));
    initShape(targetRect, sf::Vector2f(400, 50), sf::Color::Blue);

    sf::RectangleShape badRect(sf::Vector2f(50, 100));
    initShape(badRect, sf::Vector2f(250, 50), sf::Color::Red);

首先,我们创建一个窗口。其次,我们需要将帧数限制设置为标准的每秒60帧。列表中的下一个变量是sf::Vector2f, 我们将使用它作为玩家的出生点。 在我们初始化了玩家的绿色方块后,我们初始化了蓝色方块,一个蓝色的方块在世界的右边。最后一个形状是红色方块,玩家必须避免。它位于中间某处。

● 更新帧阶段,代码如下:

代码语言:javascript复制
 //Update frame
    playerRect.move(1, 0); //总是向右移动
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down))
    {
        playerRect.move(0, 1);
    }
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up))
    {
        playerRect.move(0, -1);
    }
    //达到了目标。 你赢了。 退出游戏
    if (playerRect.getGlobalBounds().intersects(targetRect.getGlobalBounds()))
    {
        window.close();
    }
    // 你碰到了敌人,输了,重新开始!
    if (playerRect.getGlobalBounds().intersects(badRect.getGlobalBounds()))
    {
        playerRect.setPosition(startPos);
    }

从前面的代码中我们可以清楚地看到玩家总是向右移动——玩家无法控制它。你可以改变这一点,这样玩家就可以出于个人喜好控制所有四个方向。目前,玩家唯一可以移动的方向是上下方向键。

除了输入处理之外,我们还需要检查代码是否具有胜负条件的逻辑。我们需要一种方法来处理这些矩形之间的碰撞检测。 值得庆幸的是,SFML中的所有形状都有两个函数,分别是 Shape :: getGlobalBounds()和Shape :: getLocalBounds(),它们返回sf :: FloatRect,它表示当前形状的全局或局部范围

整个代码是:

代码语言:javascript复制
#include <SFML/Graphics.hpp>
// 如果我们用Window类,我们必须使用头文件#include <SFML/Window.hpp >
void initShape(sf::RectangleShape &shape, sf::Vector2f const &pos, sf::Color const &color)
{
    shape.setFillColor(color);
    shape.setPosition(pos);
    shape.setOrigin(shape.getSize() * 0.5f); // The center of the rectangle
}
int main()
{

    sf::RenderWindow window(sf::VideoMode(482, 180), "Bad Squares!");  //创建窗口代码
   // sf::sleep(sf::seconds(3)); //这样我们就可以创建窗口之后,运行代码可以看到窗口,否则不可以

    window.setFramerateLimit(60); //设置每秒目标帧数

    sf::Vector2f startPos = sf::Vector2f(50, 50);
    sf::RectangleShape playerRect(sf::Vector2f(50, 50));
    initShape(playerRect, startPos, sf::Color::Green);

    sf::RectangleShape targetRect(sf::Vector2f(50, 50));
    initShape(targetRect, sf::Vector2f(400, 50), sf::Color::Blue);

    sf::RectangleShape badRect(sf::Vector2f(50, 100));
    initShape(badRect, sf::Vector2f(250, 50), sf::Color::Red);

    //Update frame
    playerRect.move(1, 0); //总是向右移动
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down))
    {
        playerRect.move(0, 1);
    }
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up))
    {
        playerRect.move(0, -1);
    }
    //达到了目标。 你赢了。 退出游戏
    if (playerRect.getGlobalBounds().intersects(targetRect.getGlobalBounds()))
    {
        window.close();
    }
    // 你碰到了敌人,输了,重新开始!
    if (playerRect.getGlobalBounds().intersects(badRect.getGlobalBounds()))
    {
        playerRect.setPosition(startPos);
    }
    //Render frame
    window.clear();
    window.draw(playerRect);
    window.draw(targetRect);
    window.draw(badRect);
    window.display();
    system("pause");
    return 0;
}

本章完……..

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/210075.html原文链接:https://javaforall.cn

0 人点赞