Skip to content

Latest commit

 

History

History
642 lines (465 loc) · 32.4 KB

File metadata and controls

642 lines (465 loc) · 32.4 KB

六、面向对象编程——启动乒乓球游戏

在本章中,有相当多的理论,但这些理论将为我们提供开始使用具有一定专业知识的面向对象编程OOP)所需的知识。此外,我们不会浪费任何时间把这个理论很好地应用,因为我们将使用它来编写下一个项目,一个乒乓球游戏。我们将在幕后了解如何通过编码一个类来创建可以用作对象的新类型。首先,我们将看一个简化的乒乓球场景,这样我们可以了解一些基本的类,然后我们将再次开始,并使用我们所学的原则编写一个真实的乒乓球游戏。

在本章中,我们将介绍以下主题:

  • 使用假设的Bat类了解 OOP 和类
  • 开始编写乒乓球游戏,编写一个真实的类来表示玩家的球拍

哎呀

面向对象编程是一种编程范式,我们可以认为它几乎是代码的标准方式。的确,有一些非面向对象的编码方式,甚至还有一些非面向对象的游戏编码语言/库。然而,因为我们是从零开始的,所以没有理由以任何其他方式做事情。

OOP 将执行以下操作:

  • 使我们的代码更易于管理、更改或更新
  • 使我们的代码编写更快、更可靠
  • 使之能够轻松使用其他人的代码(就像我们使用 SFML 一样)

我们已经看到了第三个好处。让我们讨论一下 OOP 到底是什么。

OOP 是一种编程方式,它将我们的需求分解成比整体更易于管理的块。每个块都是自包含的,但可能被其他程序重用,同时与其他块一起作为一个整体工作。这些块就是我们所说的对象。

当我们计划和编码一个对象时,我们使用来完成。

提示

类可以被认为是对象的蓝图。

我们实现了一个类的对象*。这称为类的实例。想想房子的蓝图。你不能住在里面,但你可以用它盖房子。您可以构建房子的一个实例。通常,当我们为游戏设计类时,我们编写类来表示现实世界中的事物。在下一个项目中,我们将为玩家控制的球拍和玩家可以用球拍在屏幕上弹起的球编写类。然而,OOP 不仅仅是这样。*

提示

OOP 是一种做事方式,一种定义最佳实践的方法。

OOP 的三个核心原则是封装多态继承。这听起来可能很复杂,但是,一步一步来,这是相当简单的。

封装

封装意味着确保代码的内部工作不会受到使用它的代码的干扰。您可以通过只允许访问您选择的变量和函数来实现这一点。这意味着您的代码始终可以在不影响使用它的程序的情况下进行更新、扩展或改进,前提是仍然以相同的方式访问暴露的部分。

例如,通过适当的封装,SFML 团队是否需要更新其Sprite类的工作方式无关紧要。如果函数签名保持不变,它们就不必担心内部发生了什么。我们在更新之前编写的代码在更新之后仍然有效。

多态性

多态性允许我们编写更少依赖于我们试图操纵的类型的代码。这将使我们的代码更清晰、更高效。多态性表示不同的形式。如果我们编写的对象可以是多种类型的东西,那么我们可以利用这一点。在这一点上,多态性听起来可能有点像黑魔法。我们将在第四个项目中使用多态性,我们将从第 14 章抽象和代码管理开始,更好地利用 OOP。一切都会变得更加清晰。

继承

就像听起来一样,继承意味着我们可以利用其他人的类的所有特性和好处,包括封装和多态性,同时根据我们的情况进一步细化他们的代码。我们将在使用多态性的同时首次使用继承。

为什么要使用 OOP?

如果编写正确,OOP 允许您添加新功能,而不必担心它们如何与现有功能交互。当您确实需要更改一个类时,它的自包含(封装)特性意味着对程序的其他部分的影响较小,甚至可能为零。

您可以使用其他人的代码(如 SFML 类),而不知道或甚至不关心其内部的工作方式。

面向对象编程(OOP)和扩展的 SFML 允许您编写使用复杂概念的游戏,如多摄像头、多人游戏、OpenGL、定向声音等等,而且无需费吹灰之力。

通过使用继承,您可以创建一个类的多个类似但不同的版本,而无需从头开始创建该类。

由于多态性,您仍然可以将用于原始对象类型的函数用于新对象。

所有这些都很有道理。正如我们所知,C++ 从一开始就设计了所有的 OOP。

提示

除了成功的决心外,OOP 和制作游戏(或任何其他类型的应用)的最终成功关键在于规划和设计。它不是仅仅“知道”所有 C++、SFML 和 OOP 主题,它们将帮助您编写伟大的代码,而是应用所有这些知识来编写结构良好/设计好的代码。本书中的代码以适合于学习游戏上下文中各种 C++ 主题的顺序和方式呈现。构建代码的艺术和科学称为设计模式。随着代码变得越来越长、越来越复杂,有效地使用设计模式将变得越来越重要。好消息是我们不需要自己发明这些设计模式。随着我们的项目变得越来越复杂,我们需要了解它们。随着我们的项目变得越来越复杂,我们的设计模式也将不断发展。

在这个项目中,我们将学习和使用基本类和封装。随着本书的进展,我们将变得更大胆,并使用继承、多态性和其他面向对象编程相关的 C++ 特性。

什么是课堂?

类是一组代码,它可以包含函数、变量、循环以及其他我们已经了解到的 C++ 语法。每个新类将在其自己的.h代码文件中以与该类相同的名称声明,而其函数将在其自己的.cpp文件中定义。

一旦我们编写了一个类,我们就可以使用它从中生成任意多的对象。记住,类就是蓝图,我们根据蓝图创建对象。房子不是蓝图,就像对象不是类一样。由类制成的对象。

提示

可以将对象视为变量,将类视为类型。

当然,关于 OOP 和类的讨论,我们实际上还没有看到任何代码。我们现在来解决这个问题。

乒乓球拍理论

下面是一个假设性的讨论,我们可以通过编写一个 Bat 类来使用 OOP 开始 Pong 项目。不要在项目中添加任何代码,因为为了解释理论,下面的内容过于简化了。在本章后面,我们将对其进行真正的编码。当我们真正开始编写课程时,实际上会有很大的不同,但我们将在这里学习的原则将为我们的成功做好准备。

我们将从探索变量和函数作为类的一部分开始。

类变量和函数声明

一个能弹起球的球棒是第一个优秀的班级候选人。

提示

如果你不知道什么是乒乓球,那么看看这个链接:https://en.wikipedia.org/wiki/Pong

让我们来看看一个假设的 AutoT0x 文件:

class Bat
{
    private:
        // Length of the pong bat
        int m_Length = 100;
        // Height of the pong bat
        int m_Height = 10;
        // Location on x axis
        int m_XPosition;      
        // Location on y axis
        int m_YPosition;      
    public:
        void moveRight();
        void moveLeft();
};

乍一看,代码可能看起来有点复杂,但在对其进行解释后,我们会发现很少有概念没有涉及。

首先要注意的是,使用class关键字声明一个新类,后跟该类的名称,整个声明用大括号括起来,后跟一个结束分号:

class Bat
{
    …
    …
};

现在,让我们看看变量声明及其名称:

// Length of the pong bat
int m_Length = 100; 
// Height of the pong bat
int m_Height = 10;
// Location on x axis
int m_XPosition;      
// Location on y axis
int m_YPosition;

所有名称的前缀均为m_。这个m_前缀不是强制性的,但它是一个很好的惯例。声明为类的一部分的变量称为成员变量。在处理成员变量时,使用m_作为前缀可以让事情变得简单。当我们为类编写函数时,我们将开始看到局部(非成员)变量和参数。m_公约将证明自己是有用的。

另外,请注意,所有变量都位于代码中以private:关键字为首的部分。浏览前面的代码,注意类代码主体分为两部分:

private:
    // more code here
public:
    // More code here

publicprivate关键字控制我们类的封装。类的实例/对象的用户不能直接访问私有的任何内容。如果您正在设计一个供其他人使用的类,您不希望他们能够随意更改任何内容。请注意,成员变量不必是私有的,但只要有可能,就可以通过将其私有化来实现良好的封装。

这意味着我们的四个成员变量(m_Lengthm_Heightm_XPositionm_YPosition)无法通过我们的游戏引擎从main函数直接访问。它们只能通过类的代码间接访问。这是实际的封装。对于m_Lengthm_Height变量,只要我们不需要改变蝙蝠的大小,这是相当容易接受的。然而,需要访问m_XPositionm_YPosition成员变量,或者我们究竟将如何移动蝙蝠?

本规范public:部分解决了该问题,具体如下:

void moveRight();
void moveLeft();

该类提供两个公共函数,可用于Bat类型的对象。当我们查看这些函数的定义时,我们将看到这些函数是如何精确地操作私有变量的。

总之,我们有一系列无法访问的(私有)变量,无法从main函数中使用。这很好,因为封装使我们的代码不那么容易出错,并且更易于维护。然后,我们通过提供两个公共函数来间接访问m_XPositionm_YPosition变量,从而解决移动 bat 的问题。

main函数中的代码可以使用类的实例调用这些函数,但函数中的代码可以精确控制变量的使用方式。

让我们看一下函数定义。

类函数定义

我们将在本书中编写的函数定义将全部放在一个单独的文件中,以说明类和函数声明。我们将使用与类同名的文件和.cpp文件扩展名。例如,下面的代码将放在一个名为Bat.cpp的文件中。请看以下代码,其中只有一个新概念:

#include "Bat.h"
void Bat::moveRight()
{
    // Move the bat a pixel to the right
    xPosition ++ ;
}
void Bat::moveLeft()
{
    // Move the bat a pixel to the left
    xPosition --;
}

首先要注意的是,我们必须使用 include 指令来包含来自Bat.h文件的类和函数声明。

我们在这里看到的新概念是使用范围解析运算符::。由于函数属于一个类,我们必须在函数名前面加上类名以及::来编写签名部分。void Bat::moveLeft()void Bat::moveRight

重要提示

实际上,我们之前已经简单地看到了作用域解析操作符,也就是说,每当我们声明一个类的对象时,我们之前没有使用过using namespace..

请注意,我们可以将函数定义和声明放在一个文件中,如下所示:

class Bat
{
    private:
        // Length of the pong bat
        int m_Length = 100; 
        // Length of the pong bat
        int m_Height = 10;
        // Location on x axis
        int m_XPosition;      
        // Location on y axis
        int m_YPosition;      
    public:
        void Bat::moveRight()
        {
            // Move the bat a pixel to the right
            xPosition ++ ;
        }
        void Bat::moveLeft()
        {
            // Move the bat a pixel to the left
            xPosition --;
        }
};

但是,当我们的类变长时(就像我们的第一个 Zombie Arena 类一样),将函数定义分离到它们自己的文件中会更有条理。此外,头文件被认为是“公共的”,如果其他人将使用我们编写的代码,头文件通常用于文档目的。

但是一旦我们编写了类,我们如何使用它呢?

使用类的实例

尽管我们看到了所有与类相关的代码,但实际上我们并没有使用该类。我们已经知道如何做到这一点,因为我们已经多次使用了 SFML 类。

首先,我们将创建一个Bat类的实例,如下所示:

Bat bat;

bat对象包含我们在Bat.h中声明的所有变量。我们无法直接访问它们。然而,我们可以使用蝙蝠的公共功能移动蝙蝠,如下所示:

bat.moveLeft();

或者我们可以这样移动它:

bat.moveRight();

记住batBat,因此它拥有所有成员变量和所有可用函数。

稍后,我们可能会决定让我们的乒乓球游戏成为多人游戏。在main功能中,我们可以更改代码,使游戏有两个蝙蝠,可能如下所示:

Bat bat;
Bat bat2;

认识到Bat的每一个实例都是具有自己的变量集的独立对象,这一点至关重要。初始化类实例的方法还有很多,下一步,当我们将Bat类编码为 real 时,我们将看到一个例子。

现在,我们可以开始真正的项目了。

创建 Pong 项目

由于建立一个项目是一个复杂的过程,我们将通过它一步一步,就像我们做了木材!!!项目我不会给你看我为木材做的截图!!!项目,但过程是相同的,所以翻转回到 Po.T0. Tyl T1 第 1 章 AUTT2。

  1. 启动 Visual Studio 并单击创建新项目按钮。或者,如果你还有木材!!!项目打开,可选择文件****新建项目
  2. 在接下来显示的窗口中,选择控制台应用并点击下一步按钮。然后您将看到配置新项目窗口。
  3. 配置新项目窗口中,在项目****名称字段中键入Pong。请注意,这会导致 Visual Studio 自动配置解决方案名称字段,使其具有相同的名称。
  4. 位置字段中,浏览到我们在第 1 章中创建的VS Projects文件夹。喜欢木材!!!项目,这将是我们所有项目文件的保存位置。
  5. 选中选项将解决方案和项目放在同一目录中。
  6. 完成这些步骤后,单击创建。这个项目是由 VisualStudio 生成的,包括一些在前面的文件中的 C++ 代码。
  7. 现在,我们将配置该项目以使用我们放在SFML文件夹中的 SFML 文件。从主菜单中选择项目****乒乓球属性…。在此阶段,您应该打开Pong 属性页窗口。
  8. Pong 属性页窗口中,从配置:下拉列表中选择所有配置
  9. 现在,从左侧菜单中选择C/C++,然后选择General
  10. 之后,找到附加包含目录编辑框,键入 SFML 文件夹所在的驱动器号,然后键入\SFML\include。如果您在 D 驱动器上找到了SFML文件夹,则键入的完整路径为D:\SFML\include。如果在其他驱动器上安装了 SFML,请更改路径。
  11. 点击应用保存您目前的配置。
  12. 现在,仍在同一窗口中,执行以下步骤。从左侧菜单中选择链接器,然后选择常规
  13. 现在,找到附加库目录编辑框,键入SFML文件夹所在的驱动器号,然后键入\SFML\lib。因此,如果您在 D 驱动器上找到了您的SFML文件夹,那么输入的完整路径是D:\SFML\lib。如果在其他驱动器上安装了 SFML,请更改路径。
  14. 点击应用保存您目前的配置。
  15. 接下来,仍然在同一窗口中,执行以下步骤。切换配置:下拉菜单至调试,因为我们将在调试模式下运行和测试 Pong。
  16. 选择连接器,然后输入
  17. 找到附加依赖项编辑框,点击最左侧的编辑框。现在,复制并粘贴/键入以下内容:sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;。请格外小心,将光标精确地放置在编辑框当前内容的开头,以免覆盖已有的任何文本。
  18. 点击确定
  19. 点击应用,然后点击确定
  20. 现在,我们需要将 SFML.dll文件复制到主项目目录中。我的主要项目目录是D:\VS Projects\Pong。它是由 VisualStudio 在前面的步骤中创建的。如果您将您的VS Projects文件夹放在其他地方,则改为在那里执行此步骤。我们需要复制到项目文件夹中的文件位于我们的SFML\bin文件夹中。为两个位置中的每个位置打开一个窗口,并突出显示SFML\bin文件夹中的所有文件。
  21. 现在,将突出显示的文件复制粘贴到项目文件夹中,即D:\VS Projects\Pong

我们现在已经配置了项目属性并准备就绪。

我们将在这个游戏中为 HUD(平视显示器)显示一些文本,显示玩家的分数和剩余寿命。为此,我们需要一种字体。

重要提示

下载此免费个人使用字体 http://www.dafont.com/theme.php?cat=302 并解压缩下载。或者可以随意使用您选择的字体。当我们加载字体时,您只需要对代码做一些小的更改。

VS Projects\Pong文件夹中新建一个名为fonts的文件夹,并将DS-DIGIT.ttf文件添加到VS Projects\Pong\fonts文件夹中。

我们现在准备好编码我们的第一个 C++ 类。

编码蝙蝠类

简单的 pongbat 示例是介绍类基础知识的一个好方法。类可以简单而简短,就像前面的Bat类一样,但它们也可以更长、更复杂,并且包含由其他类生成的其他对象。

当涉及到制作游戏时,假设的Bat类中缺少了一些重要的东西。对于所有这些私有成员变量和公共函数来说,这可能很好,但是我们将如何绘制呢?我们的乒乓球拍需要一个精灵,在一些游戏中,它们也需要一个纹理。此外,我们需要一种方法来控制所有游戏对象的动画速率,就像我们在上一个项目中对蜜蜂和云所做的那样。我们可以在类中包含其他对象,其方式与我们在main.cpp文件中包含它们的方式完全相同。让我们为Bat类编写真实代码,这样我们就可以看到如何解决所有这些问题。

编码 Bat.h

首先,我们将对头文件进行编码。右键点击解决方案浏览器窗口中的头文件,选择添加|新项目。接下来,选择头文件(.h)选项并将新文件命名为Bat.h。点击添加按钮。我们现在已经准备好对文件进行编码。

将以下代码添加到Bat.h

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Bat
{
private:
    Vector2f m_Position;
    // A RectangleShape object
    RectangleShape m_Shape;
    float m_Speed = 1000.0f;
    bool m_MovingRight = false;
    bool m_MovingLeft = false;
public:
    Bat(float startX, float startY);
    FloatRect getPosition();
    RectangleShape getShape();
    void moveLeft();
    void moveRight();
    void stopLeft();
    void stopRight();
    void update(Time dt);
};

首先,注意文件顶部的#pragma once声明。这将防止编译器多次处理该文件。随着我们的游戏变得越来越复杂,可能有几十个类,这将加快编译时间。

注意成员变量的名称以及函数的参数和返回类型。我们有一个名为m_PositionVector2f,它将保持球员球棒的水平和垂直位置。我们还有一个 SFMLRectangleShape,它将是屏幕上显示的实际蝙蝠。

有两个布尔成员将跟踪蝙蝠当前移动的方向(如果有),我们有一个名为m_Speedfloat,告诉我们当玩家决定向左或向右移动蝙蝠时,蝙蝠每秒可以移动的像素数。

代码的下一部分需要一些解释,因为我们有一个名为Bat的函数;这与类的名称完全相同。这称为构造函数。

构造函数

当一个类被编码时,编译器会创建一个特殊的函数。我们在代码中没有看到这个函数,但它确实存在。它被称为构造函数。如果我们使用假设的Bat类示例,就会调用该函数。

当我们需要编写一些代码来准备要使用的对象时,通常在构造函数中是一个很好的地方。当我们希望构造函数除了简单地创建一个实例之外做任何事情时,我们必须替换编译器提供的默认(看不见的)构造函数。这就是我们将使用Bat构造函数所做的。

请注意,Bat构造函数接受两个float参数。当我们第一次创建Bat对象时,这非常适合初始化屏幕上的位置。还要注意,构造函数没有返回类型,甚至没有void

我们将很快使用构造函数Bat将此游戏对象置于其起始位置。请记住,此函数是在声明Bat类型的对象时调用的。

继续 Bat.h 解释

接下来是getPosition函数,它返回一个FloatRect,四个点定义一个矩形。然后,我们有getShape,它返回一个RectangleShape。这将用于我们可以返回到主游戏循环m_Shape,以便绘制。

我们还有moveLeftmoveRightstopLeftstopRight功能,用于控制蝙蝠是否、何时以及朝哪个方向运动。

最后,我们有update函数,它接受一个Time参数。此函数将用于计算如何在每帧移动球棒。由于球棒和球的移动方式都非常不同,因此将移动代码封装在类中是有意义的。在main函数中,我们将在游戏的每一帧调用update函数一次。

提示

您可能会猜到Ball类也会有一个update函数。

现在,我们可以编写Bat.cpp,它将实现所有定义并使用成员变量。

编码 Bat.cpp

让我们创建文件,然后开始讨论代码。右键单击解决方案资源管理器窗口中的源文件文件夹。现在,选择 AuthT3 的 C++ 文件(.CPP)AUTT4,并在 AUTT5 的名称中输入 AutoT0}:点击添加按钮,将为我们创建新文件。

我们将此文件的代码分为两部分,以使讨论更简单。

首先,对Bat构造函数进行编码,如下所示:

#include "Bat.h"

// This the constructor and it is called when we create an object
Bat::Bat(float startX, float startY)
{
    m_Position.x = startX;
    m_Position.y = startY;

    m_Shape.setSize(sf::Vector2f(50, 5));
    m_Shape.setPosition(m_Position);
}

在前面的代码中,我们可以看到我们包含了bat.h文件。这使得我们可以使用之前在bat.h中声明的所有函数和变量。

我们实现构造函数是因为我们需要做一些工作来设置实例,而编译器提供的默认的看不见的空构造函数是不够的。请记住,构造函数是我们初始化Bat实例时运行的代码。

请注意,我们使用Bat::Bat语法作为函数名,以明确我们使用的是Bat类中的Bat函数。

此构造函数接收两个floatstartXstartY。接下来我们将这些值分配给m_Position.xm_Position.y。名为m_PositionVector2f现在保存传入的值,因为m_Position是一个成员变量,所以这些值可以在整个类中访问。但是,请注意,m_Position已声明为private,无论如何,在我们的main函数文件中无法直接访问。我们将看看如何尽快解决这个问题。

最后,在构造函数中,我们通过设置其大小和位置来初始化名为m_ShapeRectangleShape。这与我们在乒乓球理论一节中对假设的Bat类进行编码的方式不同。SFMLSprite类具有方便的大小和位置变量,我们可以使用setSizesetPosition函数访问这些变量,因此我们不再需要假设的m_Lengthm_Height

此外,请注意,我们需要改变初始化Bat类的方式(与假设的Bat类相比),以适合我们的自定义构造函数。

我们需要实现Bat类的其余五个函数。在我们刚才讨论的构造函数之后,在Bat.cpp中添加以下代码:

FloatRect Bat::getPosition()
{
    return m_Shape.getGlobalBounds();
}
RectangleShape Bat::getShape()
{
    return m_Shape;
}
void Bat::moveLeft()
{
     m_MovingLeft = true;
}
void Bat::moveRight()
{
    m_MovingRight = true;
}
void Bat::stopLeft()
{
    m_MovingLeft = false;
}
void Bat::stopRight()
{
    m_MovingRight = false;
}
void Bat::update(Time dt)
{
    if (m_MovingLeft) {
        m_Position.x -= m_Speed * dt.asSeconds();
    }
    if (m_MovingRight) {
        m_Position.x += m_Speed * dt.asSeconds();
    }
    m_Shape.setPosition(m_Position);
}

让我们看一下刚才添加的代码。

首先,我们有getPosition功能。它所做的只是向调用它的代码返回一个FloatRect。代码的m_Shape.getGlobalBounds行返回一个用RectangleShape四个角的坐标初始化的FloatRect,即m_Shape。当我们确定球是否击中球棒时,我们将从main函数调用此函数。

接下来是getShape函数。此函数所做的只是将一份m_Shape传递给调用代码。这是必要的,以便我们可以在main函数中绘制蝙蝠。当我们编写公共函数的唯一目的是从类传回私有数据时,我们称之为 getter 函数。

现在,我们可以看看moveLeftmoveRightstopLeftstopRight函数。他们所做的就是适当地设置m_MovingLeftm_MovingRight布尔变量,以便跟踪玩家当前的意图。但是请注意,它们不会对决定位置的RectangleShape实例或FloatRect实例执行任何操作。这正是我们需要的。

Bat类中的最后一个函数是update。我们将在游戏的每一帧调用此函数一次。随着我们的游戏项目变得越来越复杂,update功能将变得越来越复杂。现在,我们需要做的就是调整m_Position,这取决于玩家是向左移动还是向右移动。请注意,用于进行此调整的公式与我们用于更新木材中的蜜蜂和云的公式相同!!!项目代码将速度乘以增量时间,然后将其与位置相加或相减。这会导致蝙蝠相对于帧更新所用的时间移动。接下来,代码用m_Position中的最新值设置m_Shape的位置。

在我们的Bat类中有update函数而不是main函数是封装。而不是更新所有游戏对象在main功能中的位置,就像我们在木材中做的一样!!!项目中,每个对象将负责更新自己。然而,正如我们接下来要做的,我们将从main函数调用这个update函数。

使用 Bat 类并对主要功能进行编码

切换到创建项目时自动生成的main.cpp文件。删除所有自动生成的代码并添加下面的代码。

Pong.cpp文件编码如下:

#include "Bat.h"
#include <sstream>
#include <cstdlib>
#include <SFML/Graphics.hpp>
int main()
{
    // Create a video mode object
    VideoMode vm(1920, 1080);
    // Create and open a window for the game
    RenderWindow window(vm, "Pong", Style::Fullscreen);
    int score = 0;
    int lives = 3;

    // Create a bat at the bottom center of the screen
    Bat bat(1920 / 2, 1080 - 20);
    // We will add a ball in the next chapter
    // Create a Text object called HUD
    Text hud;
    // A cool retro-style font
    Font font;
    font.loadFromFile("fonts/DS-DIGI.ttf");
    // Set the font to our retro-style
    hud.setFont(font);
    // Make it nice and big
    hud.setCharacterSize(75);
    // Choose a color
    hud.setFillColor(Color::White);
    hud.setPosition(20, 20);
    // Here is our clock for timing everything
    Clock clock;
    while (window.isOpen())
    {
        /*
        Handle the player input
        ****************************
        ****************************
        ****************************
        */
        /*
        Update the bat, the ball and the HUD
        *****************************
        *****************************
        *****************************
        */

        /*
        Draw the bat, the ball and the HUD
        *****************************
        *****************************
        *****************************
        */

    }
    return 0;
}

在前面的代码中,结构类似于我们在木材中使用的结构!!!项目但是,第一个例外是,当我们创建Bat类的实例时:

// Create a bat
Bat bat(1920 / 2, 1080 - 20);

前面的代码调用构造函数来创建Bat类的新实例。该代码传入所需的参数,并允许Bat类初始化其在靠近底部屏幕中心的位置。这是我们击球开始的最佳位置。

还请注意,我使用了注释来指示代码的其余部分最终将放置在何处。这一切都在游戏循环中,就像它在森林里一样!!!项目下面是代码的其余部分,只是提醒您:

      /*
        Handle the player input

        /*
        Update the bat, the ball and the HUD


        /*
        Draw the bat, the ball and the HUD

接下来,将代码添加到Handle the player input部分,如下所示:

Event event;
while (window.pollEvent(event))
{
    if (event.type == Event::Closed)
        // Quit the game when the window is closed
        window.close();
}
// Handle the player quitting
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
    window.close();
}
// Handle the pressing and releasing of the arrow keys
if (Keyboard::isKeyPressed(Keyboard::Left))
{
    bat.moveLeft();
}
else
{
    bat.stopLeft();
}
if (Keyboard::isKeyPressed(Keyboard::Right))
{
    bat.moveRight();
}
else
{
    bat.stopRight();
}

前面的代码通过按退出键来处理玩家退出游戏的情况,就像在木材中一样!!!项目接下来,有两个ifelse结构处理球员移动球棒。让我们分析这两种结构中的第一种:

if (Keyboard::isKeyPressed(Keyboard::Left))
{
    bat.moveLeft();
}
else
{
    bat.stopLeft();
}

上述代码将检测播放机是否按下键盘上的左箭头光标键。如果是,则对Bat实例调用moveLeft函数。调用此函数时,true值设置为m_MovingLeft私有布尔变量。但是,如果未按下左箭头键,则调用stopLeft函数,并将m_MovingLeft设置为false

然后在下一个ifelse代码块中重复相同的过程,以处理玩家按下(或不按下)右箭头键。

接下来,在Update the bat the ball and the HUD部分添加以下代码,如下所示:

// Update the delta time
Time dt = clock.restart();
bat.update(dt);
// Update the HUD text
std::stringstream ss;
ss << "Score:" << score << "  Lives:" << lives;
hud.setString(ss.str());

在前面的代码中,我们使用了与木材完全相同的计时技术!!!project,只是这次,我们在Bat实例上调用update并传入增量时间。记住,当Bat类收到增量时间时,它将使用该值根据先前收到的玩家移动指令和所需的球棒速度移动球棒。

接下来,在Draw the bat, the ball and the HUD部分添加以下代码,如下所示:

window.clear();
window.draw(hud);
window.draw(bat.getShape());
window.display();

在前面的代码中,我们清除屏幕,为 HUD 绘制文本,并使用bat.getShape函数从Bat实例中抓取RectangleShape实例并将其绘制到屏幕上。最后,我们调用window.display,就像我们在上一个项目中所做的一样,将 bat 绘制在其当前位置。

在这个阶段,你可以运行游戏,你会看到 HUD 和蝙蝠。使用箭头/光标键可以平滑地左右移动球棒:

恭喜!这是第一类,所有的代码和部署。

总结

在本章中,我们了解了 OOP 的基础知识,例如如何编写和使用类,包括利用封装来控制类外的代码如何访问成员变量,但只能以我们希望的程度和方式访问。这就像 SFML 类一样,它允许我们创建和使用SpriteText实例,但只是按照它们设计的使用方式。

如果关于 OOP 和类的一些细节不完全清楚,不要太担心自己。我之所以这样说,是因为我们将在本书余下的时间里对类进行编码,我们使用它们的次数越多,它们就会变得越清晰。

此外,我们有一个工作蝙蝠和一个平视显示器为我们的乒乓球游戏。

在下一章中,我们将对Ball类进行编码,并使其在屏幕上跳跃。然后,我们将能够添加碰撞检测并完成游戏。

常见问题

问:我已经学习了其他语言,而 OOP 在 C++ 中似乎更简单。这是正确的评估吗?

A) 这是对 OOP 及其基本原理的介绍。还有比这更重要的事情。在本书中,我们将学习更多的 OOP 概念和细节。