CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++游戏编程入门(零基础学C++) 说明
  • 第1章 欢迎阅读
  • 第2章 变量、运算符与决策:精灵动画
  • 第3章 C++字符串、SFML时间:玩家输入与平视显示器
  • 第4章 循环、数组、switch语句、枚举和函数:实现游戏机制
  • 第5章 碰撞、音效与结束条件:让游戏可玩
  • 第6章 面向对象编程——开始开发《乒乓》游戏
  • 第7章 AABB碰撞检测与物理效果——完成乒乓球游戏
    • 编写Ball类
    • 使用Ball类
    • 碰撞检测与计分
    • 运行游戏
    • 学习C++中的太空船运算符
    • 总结
    • 常见问题解答
  • 第8章 SFML视图——开启丧尸射击游戏
  • 第9章 C++引用、精灵表和顶点数组
  • 第10章 指针、标准模板库和纹理管理
  • 第11章 编写TextureHolder类并创建一群僵尸
  • 第12章 碰撞检测、拾取物与子弹
  • 第13章 视图分层与平视显示器(HUD)的实现
  • 第14章 音效、文件输入输出与完成游戏制作
  • 第15章 快跑!
  • 第16章 声音、游戏逻辑、对象间通信与玩家
  • 第17章 图形、相机与动作
  • 第18章 为平台、玩家动画和控制功能编写代码
  • 第19章 构建菜单与实现下雨效果
  • 第20章 火球与声音空间化
  • 第21章 视差背景与着色器
目录

第7章 AABB碰撞检测与物理效果——完成乒乓球游戏

# 第7章 AABB碰撞检测与物理效果——完成乒乓球游戏

在本章中,我们将编写第二个类。我们会发现,尽管球与球拍明显大不相同,但我们将使用与处理球拍和Bat类完全相同的技术,把球的外观和功能封装在Ball类中。然后,我们将通过编写一些碰撞检测和计分功能,为乒乓球游戏画上最后的点睛之笔。这听起来可能很复杂,但正如我们所期待的,SFML会让事情比想象中容易得多。

本章我们将涵盖以下主题:

  • 编写Ball类
  • 使用Ball类
  • 碰撞检测和计分
  • 运行游戏
  • 了解C++的箭头运算符(spaceship operator)

我们将从编写表示球的类开始。

# 编写Ball类

首先,我们来编写头文件。在解决方案资源管理器窗口中右键单击“头文件”,选择“添加” | “新建项”。接下来,选择“头文件(.h)”选项,将新文件命名为Ball.h。然后,点击“添加”按钮。现在,我们就可以编写文件内容了。在Ball.h中添加以下代码:

#pragma once

#include <SFML/Graphics.hpp>
using namespace sf;

class Ball
{
private:
    Vector2f m_Position;
    RectangleShape m_Shape;
    float m_Speed = 300.0f;
    float m_DirectionX = .2f;
    float m_DirectionY = .2f;
public:
    Ball(float startX, float startY);
    FloatRect getPosition();
    RectangleShape getShape();
    float getXVelocity();
    void reboundSides();
    void reboundBatOrTop();
    void reboundBottom();
    void update(Time dt);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

你首先会注意到,与Bat类相比,成员变量有相似之处。和玩家的球拍类一样,这里有用于表示位置、外观和速度的成员变量,而且类型相同(分别是Vector2f、RectangleShape和float),甚至名字都一样(分别是m_Position、m_Shape和m_Speed)。这个类的成员变量的不同之处在于,方向是由两个float变量来处理的,它们将跟踪水平和垂直方向的移动,即m_DirectionX和m_DirectionY。

注意,我们需要编写八个函数来让球“动起来”。有一个与类同名的构造函数,我们将用它来初始化Ball类的实例。还有三个函数的名称和用法与Bat类中的相同,即getPosition、getShape和update。getPosition和getShape函数会将球的位置和外观信息传递给主函数,update函数将在主函数中被调用,以便Ball类每帧更新一次球的位置。

其余的函数控制球的运动方向。当检测到球与屏幕两侧发生碰撞时,主函数会调用reboundSides函数;当球击中玩家的球拍或屏幕顶部时,会调用reboundBatOrTop函数;当球击中屏幕底部时,会调用reboundBottom函数。

当然,这些只是声明,接下来让我们在Ball.cpp文件中编写实现功能的C++代码。

我们先创建这个文件,然后就可以开始讨论代码了。在解决方案资源管理器窗口中右键单击“源文件”文件夹,选择“C++文件(.cpp)”,在“名称:”字段中输入Ball.cpp。点击“添加”按钮,新文件就创建好了。在Ball.cpp中添加以下代码:

#include "Ball.h"

//  This  the  constructor function
Ball::Ball(float startX, float startY) : m_Position(startX,startY) {
    m_Shape.setSize(sf::Vector2f(10, 10));
    m_Shape.setPosition(m_Position);
}
1
2
3
4
5
6
7

在上述代码中,我们添加了Ball类头文件所需的包含指令。与类同名的构造函数接收两个float类型的参数,通过初始化列表来初始化m_Position成员的Vector2f实例。然后使用setSize函数设置RectangleShape实例的大小,使用setPosition函数设置其位置。这里设置的大小是宽10像素、高10像素,这个尺寸是随意设定的,但效果不错。位置当然是取自m_Position这个Vector2f实例。

在Ball.cpp函数的构造函数下方添加以下代码:

FloatRect Ball::getPosition()
{
    return m_Shape.getGlobalBounds();
}

RectangleShape Ball::getShape() {
    return m_Shape;
}

float Ball::getXVelocity() {
    return m_DirectionX;
}
1
2
3
4
5
6
7
8
9
10
11
12

在上述代码中,我们编写了Ball类的三个访问器函数(getter function)。它们各自向主函数返回一些信息。第一个函数getPosition,通过对m_Shape调用getGlobalBounds函数返回一个FloatRect实例,这个实例将用于碰撞检测。

getShape函数返回m_Shape,以便在游戏循环的每一帧中绘制球。getXVelocity函数告诉主函数球在水平方向上的运动方向,我们很快就会看到它的用处。由于我们从不需要获取球的垂直速度,所以没有相应的getYVelocity函数,但如果有需要,添加一个也很简单。

在刚才添加的代码下方添加以下函数:

void Ball::reboundSides() {
    m_DirectionX = -m_DirectionX;
}

void Ball::reboundBatOrTop() {
    m_DirectionY = -m_DirectionY;
}

void Ball::reboundBottom() {
    m_Position.y = 0;
    m_Position.x = 500;
    m_DirectionY = -m_DirectionY;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上述代码中,三个以rebound开头的函数处理球在不同位置发生碰撞时的情况。在reboundSides函数中,m_DirectionX的值取反,这会使正值变为负值,负值变为正值,从而(水平方向上)反转球的运动方向。reboundBatOrTop函数对m_DirectionY做同样的操作,(垂直方向上)反转球的运动方向。reboundBottom函数将球重新定位到屏幕顶部中央,并使其向下运动。这正是玩家没接到球,球击中屏幕底部后我们希望的效果。最后,为Ball类添加如下update函数:

void Ball::update(Time dt) {
    //  Update  the  ball's position
    m_Position.y += m_DirectionY * m_Speed * dt.asSeconds();
    m_Position.x += m_DirectionX * m_Speed * dt.asSeconds();
    // Move  the  ball
    m_Shape.setPosition(m_Position);
}
1
2
3
4
5
6
7

在上述代码中,m_Position.y和m_Position.x根据相应的方向、速度、速率以及当前帧完成所需的时间进行更新。更新后的m_Position值随后用于改变m_Shape这个RectangleShape实例的位置。这和我们在第一个项目中移动云朵和蜜蜂的计算方式相同,不同之处在于这个逻辑包含在类里面。如果我们需要改变球的移动方式,只会影响Ball类中的代码。

Ball类编写完成了,接下来让我们让它发挥作用。

# 使用Ball类

为了让球发挥作用,添加以下代码,以便在主函数中使用Ball类:

#include "Ball.h"
1

添加以下突出显示的代码行,使用我们刚刚编写的构造函数声明并初始化Ball类的一个实例:

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

//  Create  a  ball
Ball   ball(1920  /   2 ,   0 ) ;

//  Create  a  Text  object  called HUD
Text hud;
1
2
3
4
5
6
7
8

添加以下精确按照突出显示位置的代码:

/*
Update  the  bat,  the  ball  and  the HUD
****************************************************
****************************************************
****************************************************
*/
//  Update  the  delta  time
Time dt = clock.restart();
bat.update(dt);
ball.update(dt);
//  Update  the HUD  text
std::stringstream ss;
ss << "Score:" << score << "   Lives:" << lives;
hud.setString(ss.str());
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上述代码中,我们只是调用了球实例的update函数,球会相应地重新定位。

添加以下突出显示的代码,在游戏循环的每一帧中绘制球:

/*
Draw  the  bat,  the  ball  and  the HUD
*********************************************
*********************************************
*********************************************
*/
window.clear();
window.draw(hud);
window.draw(bat.getShape());
window.draw(ball.getShape());
window.display();
1
2
3
4
5
6
7
8
9
10
11

此时,你可以运行游戏,球会在屏幕顶部生成并开始向屏幕底部下落。然而,球会消失在屏幕底部,因为我们还没有检测任何碰撞。现在我们来解决这个问题。

# 碰撞检测与计分

与《砍树!!!》(Timber!!! )游戏不同,在那个游戏里,我们只是简单检查最低位置的树枝是否与玩家角色在同一侧,而在这个游戏中,我们需要通过数学方法检查球与球棒是否相交,或者球是否与屏幕的四条边界中的任意一条相交。

我们来看一些假设性的代码,了解一下实现过程,之后再借助SFML库来帮我们解决这个问题。

测试两个矩形是否相交的代码大概如下。不要使用下面这段代码,它仅用于演示:

if(objectA.getPosition().right > objectB.getPosition().left
&& objectA.getPosition().left < objectB.getPosition().right )
{
    //  objectA  is  intersecting  objectB  on x  axis 
    //  But  they  could  be  at  different  heights

    if(objectA.getPosition().top < objectB.getPosition().bottom
    && objectA.getPosition().bottom > objectB.getPosition().top )
    {
        //  objectA  is  intersecting  objectB 
        //  on y  axis  as  well-
        //  Collision  detected
    } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码的第一部分在水平(即x轴)方向上进行条件测试。第一个if语句检查objectA和objectB是否在水平(x轴)方向上相交,它将objectA的右侧(objectA.getPosition().right)与objectB的左侧(objectB.getPosition().left)进行比较,此外,它还会检查objectA的左侧是否在objectB的右侧的左边。如果这两个条件都成立,就意味着在x轴上存在相交。

代码的第二部分嵌套在第一部分的true分支中,在垂直(即y轴)方向上进行条件测试。如果第一个条件满足(也就是在x轴上存在相交),代码就会进入内层的if语句。在这里,它检查objectA和objectB是否也在垂直(y轴)方向上相交,它将objectA的顶部(objectA.getPosition().top)与objectB的底部(objectB.getPosition().bottom)进行比较,并且检查objectA的底部是否在objectB的顶部下方。如果这两个条件都成立,就意味着在y轴上存在相交。

最后,如果x轴和y轴的条件都成立,最内层代码块中的代码就会被执行。这个代码块表明检测到objectA和objectB之间发生了碰撞。这是游戏开发中常用的一种技术,用于检查两个对象(比如游戏角色或物品)在水平和垂直方向上是否重叠或发生碰撞。

这种技术被称为轴对齐包围盒(axis-aligned bounding box,AABB)碰撞检测。由于其计算效率高(即速度快),这种技术在二维图形和游戏开发中被广泛应用。它无法为形状不规则的物体或圆形提供精确的碰撞信息,但即便对于这些类型的物体,在进行更复杂的数学计算之前,AABB通常也会被用作快速的初步检查。

值得庆幸的是,我们不需要编写上述代码,不过,我们将使用SFML库中的intersects函数,该函数适用于FloatRect对象。回想一下Bat类和Ball类,它们都有一个getPosition函数,这个函数会返回一个FloatRect对象,代表该对象当前的位置。我们将看看如何结合使用getPosition和intersects函数来完成所有的碰撞检测。

在主函数的update部分末尾添加以下突出显示的代码:

/*
Update  the  bat,  the  ball  and  the HUD
**************************************
**************************************
**************************************
*/
//  Update  the  delta  time
Time dt = clock.restart(); bat.update(dt);
ball.update(dt);

//  Update  the HUD  text
std::stringstream ss;

ss << "Score:" << score << "   Lives:" << lives;
hud.setString(ss.str());

// Handle  ball  hitting  the  bottom
if  (ball.getPosition().top   >  window.getSize().y)   {
    // reverse  the  ball  direction
    ball. reboundBottom ();
    // Remove  a  life
    lives--;
    //  Check for  zero  lives
    if   (lives  <   1 )   {
        // reset  the  score
        score   =   0;
        // reset  the  lives
        lives   =   3; 
    }
}
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
28
29
30

在上述代码中,第一个if条件检查球是否撞到了屏幕底部:

if (ball.getPosition().top > window.getSize().y)
1

如果球的顶部位置大于窗口的高度,那就意味着球已经移出了玩家的视野底部。作为回应,会调用ball.reboundBottom函数。记住,在这个函数中,球会被重新定位到屏幕顶部。此时,玩家失去一条生命,所以lives变量会减1。

第二个if条件检查玩家是否用完了所有生命(lives < 1)。如果是这种情况,分数会重置为0,生命数量会重置为3,游戏重新开始。在下一个项目中,我们将学习如何记录并显示玩家的最高得分。

在前面的代码下方添加以下代码:

// Handle  ball  hitting  top
if (ball.getPosition().top < 0) {
    ball.reboundBatOrTop();
    // Add  a point  to  the players  score
    score++; 
}
1
2
3
4
5
6

在上述代码中,我们检测到球的顶部撞到了屏幕顶部。发生这种情况时,玩家会获得一分,并且会调用ball.reboundBatOrTop函数,该函数会反转球的垂直运动方向,使球朝屏幕底部返回。

在前面的代码下方添加以下代码:

// Handle  ball  hitting  sides
if (ball.getPosition().left < 0 ||
ball.getPosition().left + ball.getPosition().width> window. getSize().x)
{
    ball.reboundSides(); 
}
1
2
3
4
5
6

在上述代码中,if条件检测到球的左侧与屏幕左侧发生碰撞,或者球的右侧(left + 10)与屏幕右侧发生碰撞。无论哪种情况发生,都会调用ball.reboundSides函数,并且球的水平运动方向会反转。

添加以下代码:

// Has  the  ball  hit  the  bat?
if (ball.getPosition().intersects(bat.getPosition())) {
    // Hit  detected  so  reverse  the  ball  and  score  a point
    ball.reboundBatOrTop(); 
}
1
2
3
4
5

在上述代码中,intersects函数用于判断球是否击中了球棒。发生这种情况时,我们会使用与球撞到屏幕顶部时相同的函数来反转球的垂直运动方向。

# 运行游戏

现在你可以运行游戏,让球在屏幕上四处反弹。当你用球棒击球时,分数会增加;当你没击中球时,生命数量会减少。当生命数量降为0时,分数会重置,lives变量会重新恢复到3,如下所示:

img

图7.1:运行游戏

# 学习C++中的太空船运算符

由于这一章节内容较短,我觉得这是学习更多C++知识的好地方。在当前项目中,我们并不需要这些理论知识。没错,太空船运算符(spaceship operator)是真实存在的,它是C++中另一个巧妙的运算符。

太空船运算符用<=>表示,是C++20版本中新增的内容。它用于对两个对象进行三路比较,也就是说,它能帮助判断一个对象是小于、等于还是大于另一个对象。太空船运算符会返回<、==或>这三个值中的一个,表明两个对象之间的关系。下面介绍它的工作原理。

  • 如果太空船运算符左侧的值小于右侧的值,它会返回一个负数,这表明左侧的值“小于”右侧的值。
  • 如果左侧的值等于右侧的值,它会返回0,这表明两个对象相等。
  • 如果左侧的值大于右侧的值,它会返回一个正数,这表明左侧的值“大于”右侧的值。下面举个例子帮助理解。
int a = 5;  
int b = 10;
//   Next  we  use  the  spaceship  operator
int result = a <=> b; 
if (result < 0)
{
    //  a  is  less  than  b
}
else if (result == 0) {
    //  a  is  equal  to  b
}
else if (result > 0) {
    //  a  is  greater  than  b
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上述代码中,我们声明了两个整数a和b,然后使用太空船运算符<=>对它们进行比较。比较结果存储在int类型的变量result中。记住,返回值可能是负数、零或正数,分别表示a小于、等于或大于b。

然后,我们检查result的值,以此确定a和b之间的关系,并根据结果做出不同的响应。

实际上,这个简化的代码示例隐藏了一些额外的知识。使用太空船运算符的结果实际上会返回一种新的C++类型,叫做strong_ordering。幸运的是,strong_ordering类型可以转换为int类型。strong_ordering类型表示三路比较的结果。

# 总结

恭喜!你完成了第二个游戏!我们本可以为这个游戏添加更多功能,比如合作玩法、高分记录和音效,但我只是想用尽可能简单的示例来介绍类和AABB碰撞检测。现在,这些内容已经成为我们游戏开发者的“武器库”中的一部分,我们可以着手更令人兴奋的项目,学习更多游戏开发知识。

在下一章节中,我们将规划《僵尸竞技场》(Zombie Arena)游戏,学习SFML库中的View类(它在游戏世界中充当虚拟摄像头),并编写更多的类。

# 常见问题解答

问:这个游戏是不是有点太安静了? 答:我没有给这个游戏添加音效,是因为我想在使用第一个类并学习利用时间让所有游戏对象实现流畅动画的同时,尽可能简化代码。如果你想添加音效,只需要将.wav文件添加到项目中,使用SFML库加载声音,并在每个碰撞事件中播放音效即可。我们会在下一个项目中加入声音。 问:这个游戏太简单了!我要怎样才能让球的速度加快一点呢? 答:有很多方法可以增加游戏难度。一种简单的方法是在Ball类的reboundBatOrTop函数中添加一行代码来提高速度。例如,下面这段代码会在每次调用该函数时将球的速度提高10%:

// Speed  up  a  little  bit  on  each  hit
m_Speed = m_Speed * 1.1f;
1
2

这样球的速度会很快变快。之后,你需要想办法在玩家失去所有生命时将速度重置回300.0f。你可以在Ball类中创建一个新函数,比如叫resetSpeed,然后在main函数中检测到玩家失去最后一条生命时调用这个函数。 问:说出AABB碰撞检测的一个优点和一个缺点。 答:AABB碰撞检测计算效率高,适用于像游戏这种需要频繁进行碰撞检测的高要求应用场景。它也很容易理解,不过这是两个优点了。虽然AABB碰撞检测效率高,但它无法为形状不规则的物体提供精确的碰撞信息。

第6章 面向对象编程——开始开发《乒乓》游戏
第8章 SFML视图——开启丧尸射击游戏

← 第6章 面向对象编程——开始开发《乒乓》游戏 第8章 SFML视图——开启丧尸射击游戏→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式