Skip to content

Latest commit

 

History

History
324 lines (251 loc) · 13.4 KB

File metadata and controls

324 lines (251 loc) · 13.4 KB

七、动态碰撞检测与物理——完成乒乓球游戏

在本章中,我们将编写第二个类。我们将看到,尽管球显然与球棒有很大不同,但我们将使用完全相同的技术将球的外观和功能封装在Ball类中,就像我们对球棒和Bat类所做的一样。然后,我们将通过编写一些动态碰撞检测和记分代码,为乒乓球游戏添加最后的润色。这听起来可能很复杂,但正如我们所预期的,SFML 将使事情比其他方式简单得多。

本章将介绍以下主题:

  • 编写 Ball 类的代码
  • 使用 Ball 类
  • 碰撞检测和计分
  • 运行游戏

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

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

您将注意到的第一件事是成员变量与Bat类的相似性。位置、外观和速度都有一个成员变量,就像球员的球拍一样,它们是相同的类型(Vector2fRectangleShapefloat)。他们甚至有相同的名字(分别是m_Positionm_Shapem_Speed)。此类成员变量之间的区别在于,方向由两个float变量处理,这两个变量将跟踪水平和垂直运动。这些是m_DirectionXm_DirectionY

请注意,我们将需要编写八个函数来激活这个球。有一个与类同名的构造函数,我们将使用它初始化一个Ball实例。有三个函数的名称和用法与Bat类相同。它们是getPositiongetShapeupdategetPositiongetShape函数将与main函数共享球的位置和外观,并且main函数将调用update函数,以允许Ball类每帧更新一次其位置。

其余功能控制球的移动方向。当检测到屏幕两侧发生碰撞时,将从main调用reboundSides函数;当球击中球员球棒或屏幕顶部时,将调用reboundBatOrTop函数;当球击中屏幕底部时,将调用reboundBottom函数。

当然,这些仅仅是声明,所以让我们编写 C++,它实际上在 PosiT0x 文件中完成了工作。

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

将以下代码添加到Ball.cpp

#include "Ball.h"
// This the constructor function
Ball::Ball(float startX, float startY)
{
    m_Position.x = startX;
    m_Position.y = startY;
    m_Shape.setSize(sf::Vector2f(10, 10));
    m_Shape.setPosition(m_Position);
}

在前面的代码中,我们为Ball 类头文件添加了所需的include指令。与类同名的构造函数接收两个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;
}

在前面的代码中,我们正在对Ball类的三个 getter 函数进行编码。它们各自向main函数返回一些内容。第一个getPosition使用m_Shape上的getGlobalBounds函数返回FloatRect实例。这将用于碰撞检测。

getShape函数返回m_Shape,以便可以在游戏循环的每一帧中绘制它。getXVelocity函数告诉main函数球的运动方向,我们很快就会知道这对我们有多有用。因为我们不需要得到垂直速度,所以没有相应的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;
}

在前面的代码中,名称以rebound…开头的三个函数处理球与不同位置碰撞时发生的情况。在reboundSides功能中,m_DirectionX将其值反转,这将产生使正值为负值和负值为正值的效果,从而反转(水平)球的移动方向。reboundBatOrTop的作用与m_DirectionY完全相同,但与m_DirectionY作用相同,其作用是将球垂直移动的方向反转。reboundBottom功能将球重新定位在屏幕的顶部中心并向下发送。这正是我们想要的,在球员错过了一个球并且球打到了屏幕底部之后。

最后,对于Ball类,添加更新函数,如下所示:

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

在前面的代码中,m_Position.ym_Position.x使用适当的方向速度、速度和当前帧完成所需的时间进行更新。然后使用新更新的m_Position值更改m_Shape RectangleShape实例所在的位置。

Ball课程结束了,让我们付诸行动吧。

使用 Ball 类

要将球付诸行动,请添加以下代码以使main函数中的Ball类可用:

#include "Ball.h"

添加以下突出显示的代码行,以使用我们刚刚编写的构造函数声明和初始化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;

添加与突出显示位置完全相同的以下代码:

/*
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());

在前面的代码中,我们只需在ball实例上调用update。球将相应地重新定位。

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

/*
Draw the bat, the ball and the HUD
*********************************************
*********************************************
*********************************************
*/
window.clear();
window.draw(hud);
window.draw(bat.getShape());
window.draw(ball.getShape());
window.display();

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

碰撞检测与评分

不同于木材!!!游戏当我们简单地检查最低位置的一个分支是否与玩家角色在同一侧时,在这个游戏中,我们需要从数学上检查球与球棒的交点,或者球与屏幕四个边中的任何一个的交点。

让我们看看一些可以实现这一点的假设代码,以便了解我们正在做什么。然后,我们将转向 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  
        } 
}

我们不需要写这段代码;但是,我们将使用 SFMLintersects函数,该函数适用于FloatRect对象。回想或回顾BatBall课程;它们都有一个getPosition函数,返回对象当前位置的FloatRect。我们将了解如何使用getPositionintersects进行所有碰撞检测。

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

/*
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;
 }
}

在前面的代码中,第一个if条件检查球是否击中屏幕底部:

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

如果球的顶部位置大于窗口的高度,则球已从球员视图的底部消失。作为响应,ball.reboundBottom函数被调用。请记住,在此功能中,球将重新定位在屏幕顶部。此时,玩家已失去一条生命,lives变量递减。

第二个if条件检查玩家是否已耗尽生命(lives < 1。如果是这种情况,分数重置为 0,生命数重置为 3,游戏重新开始。在下一个项目中,我们将学习如何保持和显示玩家的最高分数。

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

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

在前面的代码中,我们检测到球的顶部撞击屏幕的顶部。当这种情况发生时,球员将获得一分并调用ball.reboundBatOrTop,这将反转垂直移动方向并将球送回屏幕底部。

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

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

在前面的代码中,if条件检测到球的左侧与屏幕的左侧碰撞,或球的右侧(左+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();
}

在前面的代码中,intersects功能用于确定球是否击中球棒。当这种情况发生时,我们使用与屏幕顶部碰撞相同的功能来反转球的垂直移动方向。

运行游戏

您现在可以运行游戏并在屏幕上弹起球。当你用球棒击球时,得分会增加,而当你错过它时,生命会减少。当lives为 0 时,分数将重置,lives将返回到 3,如下所示:

总结

祝贺这是第二场比赛完成了!我们本可以为该游戏添加更多功能,如合作游戏、高分、音效等,但我只想用最简单的示例介绍类和动态碰撞检测。现在我们在游戏开发者的武库中已经有了这些主题,我们可以进入一个更激动人心的项目和更多的游戏开发主题。

在下一章中,我们将规划僵尸竞技场游戏,了解 SFMLView类,它作为虚拟摄像机进入我们的游戏世界,并编写更多的类。

常见问题

Q) 这场比赛不是有点安静吗?

A) 我没有在这个游戏中添加音效,因为我想在使用我们的第一个类并学习利用时间平滑地设置所有游戏对象的动画时,使代码尽可能短。如果要添加声音效果,则只需将.wav 文件添加到项目中,使用 SFML 加载声音,并在每个碰撞事件中播放声音效果。我们将在下一个项目中这样做。

Q) 游戏太简单了!我怎样才能使球加速一点?

A) 有很多方法可以让游戏更具挑战性。一种简单的方法是在Ball类的reboundBatOrTop函数中添加一行代码,以提高速度。例如,以下代码将在每次调用函数时将球的速度提高 10%:

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

球会很快变快。然后你需要设计一种方法,当玩家失去所有生命时,将速度重置回300.0f。您可以在Ball类中创建一个新函数,可能称为resetSpeed,并在代码检测到玩家已经失去了最后的生命时从main开始调用它。