Skip to content

Latest commit

 

History

History
1274 lines (1027 loc) · 50.7 KB

File metadata and controls

1274 lines (1027 loc) · 50.7 KB

十一、碰撞检测,拾音器和子弹

到目前为止,我们已经实现了游戏的主要视觉元素。 我们有一个可控制的角色在充满僵尸追逐的竞技场中奔跑。 问题是它们之间没有相互作用。 僵尸可以毫无痕迹地穿过玩家的身体。 我们需要检测僵尸和玩家之间的冲突。

如果僵尸能够伤害并最终杀死玩家,那么我们就应该为玩家的枪支提供一些子弹。 然后我们需要确保子弹能击中并杀死僵尸。

与此同时,如果我们正在为子弹、僵尸和玩家编写碰撞检测代码,那么就应该添加一个关于生命值和弹药拾取器的类。

以下是我们将要做的事情,以及我们将在这一章中讨论的顺序:

  • 发射子弹
  • 添加十字线和隐藏鼠标指针
  • 产卵皮卡
  • 碰撞检测

让我们从Bullet课开始。

编写 Bullet 类

我们将使用 SFMLRectangleShape类直观地表示一个子弹。 我们将编码一个具有RectangleShape成员的Bullet类,以及其他成员数据和函数。 然后,我们将添加子弹到我们的游戏几个步骤,如下:

  1. 首先,我们将对Bullet.h文件进行编码。 这将揭示成员数据的所有细节和函数的原型。
  2. 接下来,我们将编码Bullet.cpp文件,当然,该文件将包含Bullet类的所有函数的定义。 当我们逐步进行时,我将准确地解释Bullet类型的对象如何工作和被控制。
  3. 最后,我们将在main函数中声明一个完整的数组。 我们还将实施射击控制方案,管理玩家的剩余弹药和重新装填。

让我们从第一步开始。

对 Bullet 头文件进行编码

要创建新的头文件,右键单击Solution Explorer中的header Files,选择Add | new Item… 。 在Add New Item窗口中,高亮(通过左键点击)Header File (.h),然后在Name字段中,键入Bullet.h

Bullet.h文件中添加以下私有成员变量和Bullet类声明。 然后我们可以浏览它们并解释它们的用途:

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Bullet
{
private:
    // Where is the bullet?
    Vector2f m_Position;
    // What each bullet looks like
    RectangleShape m_BulletShape;
    // Is this bullet currently whizzing through the air
    bool m_InFlight = false;
    // How fast does a bullet travel?
    float m_BulletSpeed = 1000;
    // What fraction of 1 pixel does the bullet travel, 
    // Horizontally and vertically each frame?
    // These values will be derived from m_BulletSpeed
    float m_BulletDistanceX;
    float m_BulletDistanceY;

    // Some boundaries so the bullet doesn't fly forever
    float m_MaxX;
    float m_MinX;
    float m_MaxY;
    float m_MinY;
// Public function prototypes go here
};

在之前的代码中,第一个成员是名为m_PositionVector2f,它将保存子弹在游戏世界中的位置。

接下来,我们将RectangleShape命名为m_BulletShape,因为我们为每个子弹使用了一个简单的非纹理图像,有点像我们在《Timber!!》中设置的时间条。

然后代码声明Booleanm_InFlight,这将跟踪子弹当前是否在空中呼啸而过。 这将允许我们决定是否需要在每一帧调用它的update函数,以及是否需要运行碰撞检测检查。

变量float``m_BulletSpeed将(你可能猜到)保持子弹将以像素每秒的速度飞行。 它被初始化为1000的值,这有点随意,但它工作得很好。

接下来,我们有另外两个float变量,m_BulletDistanceXm_BulletDistanceY。 因为移动子弹的计算比移动僵尸或玩家的计算要复杂一些,所以我们可以利用这两个变量进行计算。 它们将被用来决定水平和垂直的变化,在每一帧的子弹的位置。

最后,我们还有 4 个float变量(m_MaxXm_MinXm_MaxYm_MinY),这些变量稍后将被初始化以保存子弹的最大和最小位置以及水平和垂直位置。

其中一些变量的需求可能不会立即显现,但当我们在Bullet.cpp文件中看到它们的实际作用时,就会变得更加清楚。

现在,将所有的公共函数原型添加到Bullet.h文件中:

// Public function prototypes go here
public:
 // The constructor
 Bullet();
 // Stop the bullet
 void stop();
 // Returns the value of m_InFlight
 bool isInFlight();
 // Launch a new bullet
 void shoot(float startX, float startY,
 float xTarget, float yTarget);
 // Tell the calling code where the bullet is in the world
 FloatRect getPosition();
 // Return the actual shape (for drawing)
 RectangleShape getShape();
 // Update the bullet each frame
 void update(float elapsedTime);
};

让我们依次运行每个函数,然后我们可以继续编写它们的定义。

首先,我们有Bullet函数,它当然是构造函数。 在这个函数中,我们将设置每个Bullet实例,以便进行操作。

stop函数将在子弹已经开始运行但需要停止时被调用。

函数返回一个布尔值,用于测试子弹当前是否正在飞行。

shoot函数的用途从它的名字就可以看出,但是它的工作原理值得我们进行一些讨论。 现在,只需注意它将传入四个float参数。 这 4 个值代表子弹的起点(即玩家所在位置)水平和垂直位置,以及目标的垂直和水平位置(即十字准星所在位置)。

函数返回一个代表子弹位置的FloatRect。 这个函数将用于检测与僵尸的碰撞。 你可能还记得第 10 章指针、标准模板库和纹理管理,僵尸也有getPosition函数。

接下来是getShape函数,它返回一个RectangleShape类型的对象。 正如我们所讨论的,每颗子弹都由一个RectangleShape物体直观地表示。 因此,getShape函数将用于获取RectangleShape当前状态的一个副本,以便绘制它。

最后,正如预期的那样,有一个update函数,它有一个float参数,该参数表示自上次update调用以来已经过的时间的几分之一秒。 update方法将改变每帧子弹的位置。

让我们看看并编写函数定义。

编码 Bullet 源文件

现在,我们可以创建一个包含函数定义的新.cpp文件。 右键单击Solution Explorer中的Source Files,选择Add | New Item… 。 在Add New Item窗口中,高亮(通过左键点击)c++ File (.cpp),然后在Name字段中,键入Bullet.cpp。 最后,点击添加按钮。 现在我们已经准备好编写类了。

添加以下代码,用于 include 指令和构造函数。 我们知道它是一个构造函数,因为函数与类具有相同的名称:

#include "bullet.h"
// The constructor
Bullet::Bullet()
{
    m_BulletShape.setSize(sf::Vector2f(2, 2));
}

Bullet构造函数唯一需要做的事情就是设置m_BulletShape的大小,也就是RectangleShape对象。 代码将大小设置为两个像素乘两个像素。

接下来,我们将编码更重要的shoot函数。 将以下代码添加到Bullet.cpp文件中并研究它,然后我们就可以讨论它了:

void Bullet::shoot(float startX, float startY,
    float targetX, float targetY)
{
    // Keep track of the bullet
    m_InFlight = true;
    m_Position.x = startX;
    m_Position.y = startY;
    // Calculate the gradient of the flight path
    float gradient = (startX - targetX) / (startY - targetY);
    // Any gradient less than 1 needs to be negative
    if (gradient < 0)
    {
        gradient *= -1;
    }
    // Calculate the ratio between x and y
    float ratioXY = m_BulletSpeed / (1 + gradient);
    // Set the "speed" horizontally and vertically
    m_BulletDistanceY = ratioXY;
    m_BulletDistanceX = ratioXY * gradient;

    // Point the bullet in the right direction
    if (targetX < startX)
    {
        m_BulletDistanceX *= -1;
    }
    if (targetY < startY)
    {
        m_BulletDistanceY *= -1;
    }

    // Set a max range of 1000 pixels
    float range = 1000;
    m_MinX = startX - range;
    m_MaxX = startX + range;
    m_MinY = startY - range;
    m_MaxY = startY + range;

    // Position the bullet ready to be drawn
    m_BulletShape.setPosition(m_Position);
}

为了揭开shoot函数的神秘面纱,我们将把它分解开来,并分块讨论刚刚添加的代码。

首先,让我们提醒自己签名。 shoot函数接收子弹的起始位置和目标水平和垂直位置。 调用代码将基于玩家精灵的位置和十字准星的位置提供这些内容。 再来一遍:

void Bullet::shoot(float startX, float startY,
    float targetX, float targetY)

shoot函数中,我们将m_InFlight设置为true,并使用startXstartY参数定位子弹。 下面是这段代码:

// Keep track of the bullet
m_InFlight = true;
m_Position.x = startX;
m_Position.y = startY;

现在,我们用一点三角函数来确定子弹的飞行梯度。 子弹在水平方向和垂直方向上的前进,必须根据子弹起始点和目标点之间的线的斜率而变化。 变化的速度不能相同,或者非常陡峭的镜头会在垂直位置之前到达水平位置,反之亦然。

下面的代码根据直线方程推导出梯度。 然后,它检查梯度是否小于零,如果小于零,将其乘以-1。 这是因为传入的起始和目标坐标可以为正或负,而我们总是希望每一帧的进程量为正。 乘以-1 只是把负数变成正数,因为负数乘以负数得到正数。 实际的移动方向将在update函数中通过增加或减去我们在该函数中得到的正数来处理。

接下来,我们通过将子弹的速度(m_BulletSpeed)除以 1,再加上坡度来计算水平距离与垂直距离的比值。 这将允许我们根据子弹所朝的目标,在每一帧中改变子弹的水平和垂直位置。

最后,在这部分代码中,我们将值赋给m_BulletDistanceYm_BulletDistanceX:

// Calculate the gradient of the flight path
float gradient = (startX - targetX) / (startY - targetY);
// Any gradient less than zero needs to be negative
if (gradient < 0)
{
    gradient *= -1;
}
// Calculate the ratio between x and y
float ratioXY = m_BulletSpeed / (1 + gradient);
// Set the "speed" horizontally and vertically
m_BulletDistanceY = ratioXY;
m_BulletDistanceX = ratioXY * gradient;

下面的代码简单得多。 我们只是简单地设置了子弹可以到达的最大水平和垂直位置。 我们不希望子弹永远持续下去。 在更新函数中,我们将看到子弹是否通过了它的最大或最小位置:

// Set a max range of 1000 pixels in any direction
float range = 1000;
m_MinX = startX - range;
m_MaxX = startX + range;
m_MinY = startY - range;
m_MaxY = startY + range;

下面的代码将代表子弹的精灵移动到它的起始位置。 我们使用SpritesetPosition函数,就像我们以前经常做的那样:

// Position the bullet ready to be drawn
m_BulletShape.setPosition(m_Position);

接下来,我们有四个简单的函数。 添加stopisInFlightgetPositiongetShape函数:

void Bullet::stop()
{
    m_InFlight = false;
}
bool Bullet::isInFlight()
{
    return m_InFlight;
}
FloatRect Bullet::getPosition()
{
    return m_BulletShape.getGlobalBounds();
}
RectangleShape Bullet::getShape()
{
    return m_BulletShape;
}

stop函数简单地将m_InFlight变量设置为false。 函数返回相同变量当前的值。 因此,我们可以看到,shoot设置子弹运行,stop设置它停止,而isInFlight告知我们当前的状态是什么。

函数返回一个FloatRect。 我们将看到如何使用来自每个游戏对象的FloatRect来检测碰撞。

最后,对于前面的代码,getShape返回一个RectangleShape,这样我们就可以在每一帧中绘制一次子弹。

在开始使用Bullet对象之前,需要实现的最后一个函数是update。 添加以下代码,研究它,然后我们可以讨论它:

void Bullet::update(float elapsedTime)
{
    // Update the bullet position variables
    m_Position.x += m_BulletDistanceX * elapsedTime;
    m_Position.y += m_BulletDistanceY * elapsedTime;
    // Move the bullet
    m_BulletShape.setPosition(m_Position);
    // Has the bullet gone out of range?
    if (m_Position.x < m_MinX || m_Position.x > m_MaxX ||
        m_Position.y < m_MinY || m_Position.y > m_MaxY)
    {
        m_InFlight = false;
    }
}

update函数中,我们使用m_BulletDistanceXm_BulletDistanceY乘以自上一帧以来的时间来移动子弹。 记住,这两个变量的值是在shoot函数中计算出来的,它们代表了以正确的角度移动子弹所需的梯度(彼此之间的比率)。 然后,我们使用setPosition函数实际移动RectangleShape

我们在update中做的最后一件事是测试子弹是否已经超出了它的最大射程。 稍微有点复杂的if语句根据shoot函数中计算的最大值和最小值检查m_Position.xm_Position.y。 这些最大值和最小值存储在m_MinXm_MaxXm_MinYm_MaxY中。 如果测试为真,则将m_InFlight设置为false

Bullet课程结束。 现在,我们将看看如何在main函数中拍摄一些。

让子弹飞

我们将通过以下六个步骤使子弹可用:

  1. Bullet类添加必要的 include 指令。
  2. 添加一些控制变量和一个数组来保存一些Bullet实例。
  3. 让玩家按R重新加载。
  4. 手柄玩家按下鼠标左键发射子弹。
  5. 更新每一帧中所有正在飞行的子弹。
  6. 在每一帧中画出正在飞行的子弹。

包括 Bullet 类

添加 include 指令使 Bullet 类可用:

#include <SFML/Graphics.hpp>
#include "ZombieArena.h"
#include "Player.h"
#include "TextureHolder.h"
#include "Bullet.h"
using namespace sf;

让我们进行下一步。

控制变量和子弹数组

这里有一些变量来记录弹夹大小、备用子弹、子弹、弹夹中剩余的子弹、当前的射击速度(开始时为每秒 1 颗)以及最后一颗子弹发射的时间。

添加以下突出显示的代码。 然后,我们可以继续,并在接下来的部分中看到所有这些变量的作用:

// Prepare for a horde of zombies
int numZombies;
int numZombiesAlive;
Zombie* zombies = NULL;
// 100 bullets should do
Bullet bullets[100];
int currentBullet = 0;
int bulletsSpare = 24;
int bulletsInClip = 6;
int clipSize = 6;
float fireRate = 1;
// When was the fire button last pressed?
Time lastPressed;
// The main game loop
while (window.isOpen())

接下来,让我们来处理当玩家按下R键盘键(用于重新加载剪辑)时会发生什么。

重新装弹

现在,我们将处理与射击子弹相关的玩家输入。 首先,我们将处理按下R键来装弹枪。 我们将使用一个 SFML 事件来做到这一点。

添加以下突出显示的代码。 它通过大量的上下文来显示,以确保代码运行在正确的位置。 研究代码,然后我们可以讨论它:

// Handle events
Event event;
while (window.pollEvent(event))
{
    if (event.type == Event::KeyPressed)
    {
        // Pause a game while playing
        if (event.key.code == Keyboard::Return &&
            state == State::PLAYING)
        {
            state = State::PAUSED;
        }
        // Restart while paused
        else if (event.key.code == Keyboard::Return &&
            state == State::PAUSED)
        {
            state = State::PLAYING;
            // Reset the clock so there isn't a frame jump
            clock.restart();
        }
        // Start a new game while in GAME_OVER state
        else if (event.key.code == Keyboard::Return &&
            state == State::GAME_OVER)
        {
            state = State::LEVELING_UP;
        }
        if (state == State::PLAYING)
        {
 // Reloading
 if (event.key.code == Keyboard::R)
 {
 if (bulletsSpare >= clipSize)
 {
 // Plenty of bullets. Reload.
 bulletsInClip = clipSize;
 bulletsSpare -= clipSize; 
 }
 else if (bulletsSpare > 0)
 {
 // Only few bullets left
 bulletsInClip = bulletsSpare;
 bulletsSpare = 0; 
 }
 else
 {
 // More here soon?!
 }
 }
        }
    }
}// End event polling

前面的代码嵌套在游戏循环的事件处理部分(while(window.pollEvent))中,并且嵌套在只在真正玩游戏时执行的块中(if(state == State::Playing))。 很明显,我们不希望玩家在游戏结束或暂停时重新加载内容,然后包装我们所描述的新代码。

在新代码本身中,我们要做的第一件事是测试用if (event.key.code == Keyboard::R)按下的R键。 一旦我们检测到R键被按下,剩下的代码将被执行。 以下是ifelse ifelse组块的结构:

if(bulletsSpare >= clipSize)
    ...
else if(bulletsSpare > 0)
    ...
else
    ...

前面的结构允许我们处理三种可能的场景,如下所示:

  • 玩家按下了R,他们的子弹比弹夹所能承受的多。 在这种情况下,弹夹被重新填充,备用子弹的数量减少。
  • 玩家有一些备用子弹,但不足以完全填满弹夹。 在这个场景中,玩家可以用尽可能多的备用子弹填充剪辑,而备用子弹的数量被设置为零。
  • 玩家已经按了R,但是他们没有多余的子弹。 对于这个场景,我们实际上不需要更改变量。 然而,我们将在这里扮演一个音效当我们实现声音第十三章,声音效果,文件 I / O,和完成游戏【显示】,所以我们将把空else块准备好了。

现在,让我们发射一颗子弹。

射击子弹

在这里,我们将处理点击鼠标左键发射子弹。 添加以下突出显示的代码,并仔细研究它:

    if (Keyboard::isKeyPressed(Keyboard::D))
    {
        player.moveRight();
    }
    else
    {
        player.stopRight();
    }
 // Fire a bullet
 if (Mouse::isButtonPressed(sf::Mouse::Left))
 {
 if (gameTimeTotal.asMilliseconds()
 - lastPressed.asMilliseconds()
 > 1000 / fireRate && bulletsInClip > 0)
 {
 // Pass the centre of the player 
 // and the centre of the cross-hair
 // to the shoot function
 bullets[currentBullet].shoot(
 player.getCenter().x, player.getCenter().y,
 mouseWorldPosition.x, mouseWorldPosition.y);
 currentBullet++ ;
 if (currentBullet > 99)
 {
 currentBullet = 0;
 }
 lastPressed = gameTimeTotal;
 bulletsInClip--;
 }
 }// End fire a bullet
}// End WASD while playing

所有前面的代码都包装在一个if 语句中,该语句在按下鼠标左键(即if (Mouse::isButtonPressed(sf::Mouse::Left)))时执行。 注意,代码将重复执行,即使玩家只是按住按钮。 我们现在要通过的代码控制着开火的速度。

在前面的代码中,然后检查是否在游戏中运行的总时间(gameTimeTotal)减去时间玩家最后一球一颗子弹(lastPressed)大于 1000,除以当前火灾和球员至少有一颗子弹夹。 我们用 1000 是因为这是一秒的毫秒数。

如果测试成功,将执行实际触发子弹的代码。 射击子弹很容易,因为我们在Bullet课上做了所有艰苦的工作。 我们只需从bullets数组中调用shoot 来处理当前子弹。 我们传递玩家和十字线当前的水平和垂直位置。 子弹将在飞行中通过Bullet类的shoot函数中的代码进行配置和设置。

我们要做的就是跟踪子弹的排列。 我们增加了变量currentBullet。 然后,我们需要检查是否使用if (currentBullet > 99)语句发射了最后一颗子弹(99)。 如果这是最后一颗子弹,我们将currentBullet设为零。 如果这不是最后一颗子弹,那么只要射击速度允许,玩家按下鼠标左键,下一颗子弹就准备好了。

最后,在前面的代码中,我们存储子弹射入lastPressed的时间并减少bulletsInClip

现在,我们可以更新每一颗子弹,每一帧。

每帧更新子弹

添加以下高亮显示的代码来循环子弹数组,检查子弹是否在飞行,如果是,调用它的 update 函数:

    // Loop through each Zombie and update them
    for (int i = 0; i < numZombies; i++)
    {
        if (zombies[i].isAlive())
        {
            zombies[i].update(dt.asSeconds(), playerPosition);
        }
    }
 // Update any bullets that are in-flight
 for (int i = 0; i < 100; i++)
 {
 if (bullets[i].isInFlight())
 {
 bullets[i].update(dtAsSeconds);
 }
 }
}// End updating the scene

最后,我们将画出所有的子弹。

每帧绘制子弹

添加以下高亮显示的代码来循环遍历bullets数组,检查子弹是否在飞行,如果是,绘制它:

/*
 **************
 Draw the scene
 **************
 */
if (state == State::PLAYING)
{
    window.clear();
    // set the mainView to be displayed in the window
    // And draw everything related to it
    window.setView(mainView);
    // Draw the background
    window.draw(background, &textureBackground);
    // Draw the zombies
    for (int i = 0; i < numZombies; i++)
    {
        window.draw(zombies[i].getSprite());
    }
 for (int i = 0; i < 100; i++)
 {
 if (bullets[i].isInFlight())
 {
 window.draw(bullets[i].getShape());
 }
 }
    // Draw the player
    window.draw(player.getSprite());
}

运行游戏来尝试子弹。 请注意,在按R重新装填之前,您可以发射六发子弹。 很明显,缺少的是弹夹中子弹数量和备用子弹数量的可视指示器。 另一个问题是,玩家可能会很快用光子弹,特别是因为子弹没有任何阻止力。 它们直接飞过僵尸。 此外,我们希望玩家瞄准的是鼠标指针而不是精确的十字准星,显然我们还有很多工作要做。

在下一章中,我们将通过 HUD 提供视觉反馈。 接下来我们将用十字准星替换鼠标光标,然后生成一些拾音器来补充子弹和生命值。 最后,在本章中,我们将处理碰撞检测,使子弹和僵尸造成损害,并使玩家能够真正得到拾取。

瞄准目标

添加一个十字准星很简单,只需要一个新概念。 添加以下高亮显示的代码,然后我们可以运行它:

// 100 bullets should do
Bullet bullets[100];
int currentBullet = 0;
int bulletsSpare = 24;
int bulletsInClip = 6;
int clipSize = 6;
float fireRate = 1;
// When was the fire button last pressed?
Time lastPressed;
// Hide the mouse pointer and replace it with crosshair
window.setMouseCursorVisible(true);
Sprite spriteCrosshair;
Texture textureCrosshair = TextureHolder::GetTexture("graphics/crosshair.png");
spriteCrosshair.setTexture(textureCrosshair);
spriteCrosshair.setOrigin(25, 25);
// The main game loop
while (window.isOpen())

首先,我们在window对象上调用setMouseCursorVisible函数。 然后,我们加载一个Texture,声明一个Sprite实例,并按照通常的方式初始化它。 此外,我们将精灵的原点设置在它的中心,以便更方便和更简单地让子弹飞向中间,就像你所期望的那样。

现在,我们需要用鼠标的世界坐标更新每一帧的十字准星。 添加以下高亮显示的代码行,它使用mouseWorldPosition向量来设置每一帧的准星位置:

/*
 ****************
 UPDATE THE FRAME
 ****************
 */
if (state == State::PLAYING)
{
    // Update the delta time
    Time dt = clock.restart();
    // Update the total game time
    gameTimeTotal += dt;
    // Make a decimal fraction of 1 from the delta time
    float dtAsSeconds = dt.asSeconds();
    // Where is the mouse pointer
    mouseScreenPosition = Mouse::getPosition();
    // Convert mouse position to world coordinates of mainView
    mouseWorldPosition = window.mapPixelToCoords(
        Mouse::getPosition(), mainView);
 // Set the crosshair to the mouse world location
 spriteCrosshair.setPosition(mouseWorldPosition);
    // Update the player
    player.update(dtAsSeconds, Mouse::getPosition());

接下来,正如你可能已经预料到的,我们可以在每一帧中绘制十字准星。 在显示的位置中添加以下高亮显示的代码行。 这一行代码无需解释,但它在所有其他游戏对象之后的位置很重要,所以它被绘制在最上面:

/*
 **************
 Draw the scene
 **************
 */
if (state == State::PLAYING)
{
    window.clear();
    // set the mainView to be displayed in the window
    // And draw everything related to it
    window.setView(mainView);
    // Draw the background
    window.draw(background, &textureBackground);
    // Draw the zombies
    for (int i = 0; i < numZombies; i++)
    {
        window.draw(zombies[i].getSprite());
    }
    for (int i = 0; i < 100; i++)
    {
        if (bullets[i].isInFlight())
        {
            window.draw(bullets[i].getShape());
        }
    }
    // Draw the player
    window.draw(player.getSprite());
 //Draw the crosshair
 window.draw(spriteCrosshair);
}

现在,你可以运行游戏,并将看到一个很酷的十字线,而不是鼠标光标:

注意子弹是如何穿过十字准星中心的。 射击机制的运作方式类似于允许玩家选择从臀部射击或向下瞄准。 如果玩家将十字准星保持在中心附近,他们便可以快速开火并转向,但同时也必须小心地判断远处僵尸的位置。

或者,玩家可以将十字准星直接悬停在远处的僵尸头上,并获得准确的命中; 然而,如果僵尸从另一个方向发动攻击,他们就有更多的时间将十字准星向后移动。

游戏的一个有趣改进便是在每次射击中添加少量的随机误差。 这种不准确性也许可以通过两次浪潮之间的升级来缓解。

为拾取程序编写类

在本节中,我们将编码一个具有Sprite成员的Pickup类,以及其他成员数据和函数。 我们将在我们的游戏中添加拾取只需要几个步骤:

  1. 首先,我们将对Pickup.h文件进行编码。 这将揭示成员数据的所有细节和函数的原型。
  2. 然后,我们将编写Pickup.cpp文件,当然,该文件将包含Pickup类的所有函数的定义。 当我们逐步进行时,我将准确地解释Pickup类型的对象如何工作和被控制。
  3. 最后,我们将在main函数中使用Pickup类来生成、更新和绘制它们。

让我们从第一步开始。

编码皮卡头文件

要创建新的头文件,右键单击Solution Explorer中的header Files,选择Add | new Item… 。 在Add New Item窗口中,高亮(通过左键点击)Header File (.h),然后在Name字段中,键入Pickup.h

Pickup.h文件中添加并研究以下代码,然后我们可以遍历它:

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Pickup
{
private:
    //Start value for health pickups
    const int HEALTH_START_VALUE = 50;
    const int AMMO_START_VALUE = 12;
    const int START_WAIT_TIME = 10;
    const int START_SECONDS_TO_LIVE = 5;

    // The sprite that represents this pickup
    Sprite m_Sprite;
    // The arena it exists in
    IntRect m_Arena;
    // How much is this pickup worth?
    int m_Value;

    // What type of pickup is this? 
    // 1 = health, 2 = ammo
    int m_Type;
    // Handle spawning and disappearing
    bool m_Spawned;
    float m_SecondsSinceSpawn;
    float m_SecondsSinceDeSpawn;
    float m_SecondsToLive;
    float m_SecondsToWait;    
// Public prototypes go here
};

前面的代码声明了Pickup类的所有私有变量。 虽然这些名称应该是非常直观的,但为什么需要这么多名称可能并不明显。 让我们从头到尾看一遍:

  • const int HEALTH_START_VALUE = 50:此常量变量用于设置所有生命值拾取器的起始值。 该值将用于初始化变量m_Value ,该变量将在整个游戏过程中进行操作。

  • const int AMMO_START_VALUE = 12:这个常量变量用来设置所有拾取弹药的起始值。 该值将用于初始化变量m_Value,该变量将在整个游戏过程中进行操作。

  • const int START_WAIT_TIME = 10:这个变量决定拾音器在消失后等待多长时间才能重生。 它将用于初始化变量m_SecondsToWait,该变量可以在整个游戏中进行操作。

  • const int START_SECONDS_TO_LIVE = 5:这个变量决定了拾音器在产卵和被取消产卵之间的持续时间。 与前三个常量一样,它也有一个可以在整个游戏过程中被操纵的非常量。 它用来初始化的非常量是m_SecondsToLive

  • Sprite m_Sprite:这是视觉上代表物体的精灵。

  • IntRect m_Arena:这将保持当前竞技场的大小,以帮助拾音器在一个合理的位置产卵。

  • 这辆皮卡的生命值和弹药值多少? 当玩家升级生命值或拾取弹药值时,就会使用这个值。

  • int m_Type:生命值和弹药值分别是 1 或 2。 我们本可以使用枚举类,但对于只有两个选项来说,这似乎有点多余。

  • bool m_Spawned:皮卡现在已经生成了吗?

  • float m_SecondsSinceSpawn:从皮卡产生到现在有多久了?

  • float m_SecondsSinceDeSpawn:皮卡消失多久了?

  • float m_SecondsToLive:这个拾音器应该停留多长时间产卵之前反产卵?

  • float m_SecondsToWait: How long should this pickup stay de-spawned before respawning?

    提示

    请注意,这个类的大部分复杂性是由于变量生成时间和它的可升级性质。 如果拾音器只是在收集时重新生成并具有固定值,这将是一个非常简单的类。 我们需要让道具能够升级,这样玩家就会被迫制定策略,在波涛中前进。

接下来,将以下公共函数原型添加到Pickup.h文件中。 请务必熟悉新代码,以便我们能够浏览它:

// Public prototypes go here
public:
 Pickup::Pickup(int type);
 // Prepare a new pickup
 void setArena(IntRect arena);
 void spawn();
 // Check the position of a pickup
 FloatRect getPosition();
 // Get the sprite for drawing
 Sprite getSprite();
 // Let the pickup update itself each frame
 void update(float elapsedTime);
 // Is this pickup currently spawned?
 bool isSpawned();
 // Get the goodness from the pickup
 int gotIt();
 // Upgrade the value of each pickup
 void upgrade();
};

让我们简要地讨论一下每个函数的定义。

  • 第一个函数是构造函数,以类命名。 注意,它只接受一个int参数。 这将用于初始化拾取的类型(生命值或弹药)。
  • setArena函数接收到IntRect。 这个函数将在每个 wave 开始时对每个Pickup实例调用。 然后,Pickup对象将“知道”它们可以产卵的区域。
  • 当然,spawn函数将处理生成拾取。
  • PlayerZombieBullet类一样,getPosition函数将返回一个FloatRect实例,该实例表示对象在游戏世界中的当前位置。
  • getSprite函数返回一个Sprite对象,该对象允许拾取每帧绘制一次。
  • 函数接收上一帧所花费的时间。 它使用这个值来更新它的私有变量,并决定何时生成和反生成。
  • 函数返回一个布尔值,让调用代码知道当前拾取是否已生成。
  • 当玩家检测到冲突时,将调用gotIt函数。 然后,Pickup类的代码可以为在适当的时间重生做好准备。 注意,它返回一个int值,以便调用代码知道拾取物在生命值或弹药上的“价值”。
  • 当玩家在游戏升级阶段选择升级拾音器的属性时,upgrade函数将被调用。

现在我们已经了解了成员变量和函数原型,在编写函数定义时应该很容易理解。

编写 Pickup 类函数定义

现在,我们可以创建一个包含函数定义的新.cpp文件。 右键单击Solution Explorer中的Source Files,选择Add | New Item… 。 在Add New Item窗口中,高亮(通过左键点击)c++ File (.cpp),然后在Name字段中,键入Pickup.cpp。 最后,点击添加按钮。 现在我们已经准备好编写类了。

将以下代码添加到Pickup.cpp文件中。 一定要检查代码,以便我们可以讨论它:

#include "Pickup.h"
#include "TextureHolder.h"
Pickup::Pickup(int type)
{
    // Store the type of this pickup
    m_Type = type;
    // Associate the texture with the sprite
    if (m_Type == 1)
    {
        m_Sprite = Sprite(TextureHolder::GetTexture(
            "graphics/health_pickup.png"));
        // How much is pickup worth
        m_Value = HEALTH_START_VALUE;
    }
    else
    {
        m_Sprite = Sprite(TextureHolder::GetTexture(
            "graphics/ammo_pickup.png"));
        // How much is pickup worth
        m_Value = AMMO_START_VALUE;
    }
    m_Sprite.setOrigin(25, 25);
    m_SecondsToLive = START_SECONDS_TO_LIVE;
    m_SecondsToWait = START_WAIT_TIME;
}

在前面的代码中,我们添加了熟悉的 include 指令。 然后,我们添加了Pickup构造函数。 我们知道它是构造函数,因为它与类具有相同的名称。

构造函数接收名为typeint,代码所做的第一件事就是将从type接收到的值赋给m_Type。 在此之后,有一个if else块检查m_Type是否等于 1。 如果是,m_Sprite与健康拾取纹理关联,m_Value设置为HEALTH_START_VALUE

如果m_Type不等于 1,else块将拾弹纹理与m_Sprite关联,并将AMMO_START_VALUE值赋给m_Value

if``else块之后,代码使用setOrigin函数将m_Sprite的原点设置到中心,并分别将START_SECONDS_TO_LIVESTART_WAIT_TIME赋值给m_SecondsToLivem_SecondsToWait

构造函数已经成功地准备了一个可以使用的Pickup对象。

现在,我们将添加setArena函数。 在添加代码时检查代码:

void Pickup::setArena(IntRect arena)
{
    // Copy the details of the arena to the pickup's m_Arena
    m_Arena.left = arena.left + 50;
    m_Arena.width = arena.width - 50;
    m_Arena.top = arena.top + 50;
    m_Arena.height = arena.height - 50;
    spawn();
}

我们刚刚编码的setArena函数只是简单地从传入的arena对象中复制值,但在左侧和顶部改变+ 50值,在右侧和底部改变- 50值。 对象现在知道它可以生成的区域。 然后,setArena函数调用自己的spawn函数为绘制和更新每一帧做最后的准备。

接下来是spawn函数。 在setArena函数后添加以下代码:

void Pickup::spawn()
{
    // Spawn at a random location
    srand((int)time(0) / m_Type);
    int x = (rand() % m_Arena.width);
    srand((int)time(0) * m_Type);
    int y = (rand() % m_Arena.height);
    m_SecondsSinceSpawn = 0;
    m_Spawned = true;
    m_Sprite.setPosition(x, y);
}

T0 函数完成所有准备拾取所需的工作。 首先,它为随机数生成器播种,并获得对象的水平和垂直位置的随机数。 注意,它使用了m_Arena.widthm_Arena.height变量作为可能的水平和垂直位置的范围。

变量m_SecondsSinceSpawn被设置为零,以便在取消衍生之前允许的时间长度被重置。 变量m_Spawned被设置为true,这样当我们从main调用isSpawned时,我们将得到一个正响应。 最后,m_SpritesetPosition移动到相应位置,准备被绘制到屏幕上。

在下面的代码块中,我们有三个简单的 getter 函数。 getPosition函数返回当前位置的FloatRect``m_SpritegetSprite返回一个副本m_Sprite,isSpawned,返回truefalse,根据对象是否正在产生。

添加并检查我们刚才讨论的代码:

FloatRect Pickup::getPosition()
{
    return m_Sprite.getGlobalBounds();
}
Sprite Pickup::getSprite()
{
    return m_Sprite;
}
bool Pickup::isSpawned()
{
    return m_Spawned;
}

接下来,我们将对gotIt函数进行编码。 当玩家接触/碰撞拾取物品时,这个函数将从main开始调用。 在isSpawned功能后增加gotIt功能:

int Pickup::gotIt()
{
    m_Spawned = false;
    m_SecondsSinceDeSpawn = 0;
    return m_Value;
}

gotIt函数将m_Spawned设为false,这样我们就知道不再绘制和检查碰撞。 m_SecondsSinceDespawn设置为零,以便重新开始产卵倒计时。 然后将m_Value返回到调用代码,以便调用代码可以处理添加额外的弹药或生命值的问题。

接下来,我们需要编写update函数,它将前面提到的许多变量和函数联系在一起。 添加并熟悉update函数,然后我们可以讨论它:

void Pickup::update(float elapsedTime)
{
    if (m_Spawned)
    {
        m_SecondsSinceSpawn += elapsedTime;
    }
    else
    {
        m_SecondsSinceDeSpawn += elapsedTime;
    }
    // Do we need to hide a pickup?
    if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned)
    {
        // Remove the pickup and put it somewhere else
        m_Spawned = false;
        m_SecondsSinceDeSpawn = 0;
    }
    // Do we need to spawn a pickup
    if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned)
    {
        // spawn the pickup and reset the timer
        spawn();
    }
}

update函数被划分为四个块,在每一帧中执行:

  1. 如果m_Spawned为 true,则执行if块:if (m_Spawned)。 此代码块将此帧的时间添加到m_SecondsSinceSpawned,以跟踪生成拾取的时间。
  2. 一个对应的else块,如果m_Spawned为 false 则执行该块。 此块将此帧所花费的时间添加到m_SecondsSinceDeSpawn,以跟踪自上次去衍生(隐藏)以来拾取程序已经等待了多长时间。
  3. 另一个if块,在生成拾取的时间超过其正常时间时执行:if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned)。 该块将m_Spawned设置为false并将m_SecondsSinceDeSpawn重置为零。 现在,block 2 将执行,直到该再次生成它为止。
  4. 最后一个if块,当从反生成开始等待的时间超过了必要的等待时间,并且拾取当前没有生成时执行:if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned)。 当执行此块时,是时候再次生成拾取,然后调用spawn函数。

这四个测试控制隐藏和显示拾取。

最后,添加upgrade函数的定义:

void Pickup::upgrade()
{
    if (m_Type == 1)
    {
        m_Value += (HEALTH_START_VALUE * .5);
    }
    else
    {
        m_Value += (AMMO_START_VALUE * .5);
    }
    // Make them more frequent and last longer
    m_SecondsToLive += (START_SECONDS_TO_LIVE / 10);
    m_SecondsToWait -= (START_WAIT_TIME / 10);
}

upgrade测试拾音器的类型(生命值或弹药),然后在m_Value上添加 50%(适当的)起始值。 if``else方块后的下两行将增加拾取器的生成时间,并减少玩家在两次刷出之间等待的时间。

当玩家选择在LEVELING_UP状态升级拾音器时,此函数将被调用。

我们的Pickup班可以使用了。

使用 Pickup 类

在完成了所有实现Pickup类的艰苦工作之后,我们现在可以继续在游戏引擎中编写代码,将一些拾取道具放入游戏中。

我们要做的第一件事是在ZombieArena.cpp文件中添加一个 include 指令:

#include <SFML/Graphics.hpp>
#include "ZombieArena.h"
#include "Player.h"
#include "TextureHolder.h"
#include "Bullet.h"
#include "Pickup.h"
using namespace sf;

在下面的代码中,我们将添加两个Pickup实例:一个名为healthPickup,另一个名为ammoPickup。 我们将值 1 和 2 分别传递给构造函数,以便将它们初始化为正确的拾取类型。 添加以下高亮显示的代码,我们刚刚讨论过:

// Hide the mouse pointer and replace it with crosshair
window.setMouseCursorVisible(true);
Sprite spriteCrosshair;
Texture textureCrosshair = TextureHolder::GetTexture(
         "graphics/crosshair.png");
spriteCrosshair.setTexture(textureCrosshair);
spriteCrosshair.setOrigin(25, 25);
// Create a couple of pickups
Pickup healthPickup(1);
Pickup ammoPickup(2);
// The main game loop
while (window.isOpen())

在键盘处理的LEVELING_UP状态下,在嵌套的PLAYING代码块中添加以下高亮显示的行:

if (state == State::PLAYING)
{
    // Prepare the level
    // We will modify the next two lines later
    arena.width = 500;
    arena.height = 500;
    arena.left = 0;
    arena.top = 0;
    // Pass the vertex array by reference 
    // to the createBackground function
    int tileSize = createBackground(background, arena);
    // Spawn the player in the middle of the arena
    player.spawn(arena, resolution, tileSize);
 // Configure the pick-ups
 healthPickup.setArena(arena);
 ammoPickup.setArena(arena);
    // Create a horde of zombies
    numZombies = 10;
    // Delete the previously allocated memory (if it exists)
    delete[] zombies;
    zombies = createHorde(numZombies, arena);
    numZombiesAlive = numZombies;
    // Reset the clock so there isn't a frame jump
    clock.restart();
}

前面的代码只是将arena传递给每个拾取的setArena函数。 拾音器现在知道他们可以产卵的地方。 这段代码对每个新 wave 执行,因此,随着舞台的大小增加,Pickup对象将得到更新。

下面的代码简单地为每一帧上的每个Pickup对象调用update函数:

// Loop through each Zombie and update them
    for (int i = 0; i < numZombies; i++)
    {
        if (zombies[i].isAlive())
        {
            zombies[i].update(dt.asSeconds(), playerPosition);
        }
    }
    // Update any bullets that are in-flight
    for (int i = 0; i < 100; i++)
    {
        if (bullets[i].isInFlight())
        {
            bullets[i].update(dtAsSeconds);
        }
    }
 // Update the pickups
 healthPickup.update(dtAsSeconds);
 ammoPickup.update(dtAsSeconds);
}// End updating the scene

下面的代码在游戏循环的绘制部分检查拾取当前是否生成,如果是,绘制它。 让我们将它添加:

    // Draw the player
    window.draw(player.getSprite());
 // Draw the pick-ups, if currently spawned
 if (ammoPickup.isSpawned())
 {
 window.draw(ammoPickup.getSprite());
 }

 if (healthPickup.isSpawned())
 {
 window.draw(healthPickup.getSprite());
 }
    //Draw the crosshair
    window.draw(spriteCrosshair);
}

现在,你可以运行游戏,并看到拾音器产卵和反产卵。 然而,你还不能把它们捡起来:

现在我们在游戏中拥有了所有的物体,是时候让它们相互作用(碰撞)了。

检测碰撞

我们只需要知道游戏中的特定物体何时接触到其他物体。 然后,我们可以以适当的方式响应该事件。 在我们的类中,我们已经添加了在对象碰撞时将被调用的函数。 它们如下:

  • Player类具有hit功能。 当僵尸与玩家发生碰撞时,我们就会调用它。
  • Zombie类具有hit功能。 当子弹撞到僵尸的时候我们就叫它。
  • Pickup类具有gotIt功能。 当玩家与拾音器发生碰撞时,我们便会调用它。

如果有必要,回顾一下这些函数是如何工作的。 我们现在需要做的就是检测冲突并调用相应的函数。

我们将使用矩形交点来检测碰撞。 这种类型的碰撞检测非常简单(特别是使用 SFML 时)。 我们将使用与《Pong》游戏相同的技术。 下图展示了矩形如何合理准确地代表僵尸和玩家:

我们将在三段代码中处理这个问题,这三段代码将依次进行。 它们都将在游戏引擎的更新部分结束时消失。

对于每一帧,我们需要知道以下三个问题的答案:

  1. 僵尸中枪了吗?
  2. 玩家是否被僵尸碰过?
  3. 玩家是否碰过拾音器?

首先,让我们为scorehiscore添加更多变量。 当僵尸被杀死时,我们可以改变它们。 添加以下代码:

// Create a couple of pickups
Pickup healthPickup(1);
Pickup ammoPickup(2);
// About the game
int score = 0;
int hiScore = 0;
// The main game loop
while (window.isOpen())

现在,让我们从检测僵尸是否与子弹相撞开始。

僵尸中枪了吗?

下面的代码可能看起来很复杂,但是当我们逐步执行它时,我们会发现它并不是我们以前没有见过的。 在调用之后添加以下代码来更新每一帧的拾取。 然后,我们可以看一下:

// Update the pickups
healthPickup.update(dtAsSeconds);
ammoPickup.update(dtAsSeconds);
// Collision detection
// Have any zombies been shot?
for (int i = 0; i < 100; i++)
{
 for (int j = 0; j < numZombies; j++)
 {
 if (bullets[i].isInFlight() && 
 zombies[j].isAlive())
 {
 if (bullets[i].getPosition().intersects
 (zombies[j].getPosition()))
 {
 // Stop the bullet
 bullets[i].stop();
 // Register the hit and see if it was a kill
 if (zombies[j].hit()) 
 {
 // Not just a hit but a kill too
 score += 10;
 if (score >= hiScore)
 {
 hiScore = score;
 }
 numZombiesAlive--;
 // When all the zombies are dead (again)
 if (numZombiesAlive == 0) {
 state = State::LEVELING_UP;
 }
 } 

 }
 }
 }
}// End zombie being shot

在下一节中,我们将再次看到所有僵尸和子弹碰撞检测代码。 我们将一次做一点,以便我们可以讨论它。 首先,注意前面代码中嵌套的for循环的结构(去掉了一些代码),如下所示:

// Collision detection
// Have any zombies been shot?
for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < numZombies; j++)
    {
        ...
        ...
        ...
    }
}

代码循环遍历每个僵尸(0 到< T0)的每个子弹(0 到 99)。

在嵌套的for循环中,我们执行以下操作。

我们使用以下代码检查当前子弹是否在飞行,当前僵尸是否仍然活着:

if (bullets[i].isInFlight() && zombies[j].isAlive())

假设僵尸是活着的,子弹在飞行,我们测试矩形交叉与以下代码:

if (bullets[i].getPosition().intersects(zombies[j].getPosition()))

如果当前的子弹和僵尸相撞了,那么我们将采取一些步骤,如下所述。

用下面的代码停止子弹:

// Stop the bullet
bullets[i].stop();

通过调用当前僵尸的hit函数注册一个命中。 请注意,hit函数返回一个布尔值,让调用代码知道僵尸是否已经死亡。 这在下面的代码行中显示:

// Register the hit and see if it was a kill
if (zombies[j].hit()) {

在这个if块中,它可以检测到僵尸是死的,并且不只是伤害我们,执行以下操作:

  • score上加 10。
  • 如果玩家取得的分数超过(击败)score,则更改hiScore
  • numZombiesAlive减少 1。
  • 检查(numZombiesAlive == 0)是否所有僵尸都死了,如果是,将state改为LEVELING_UP

下面是我们刚刚讨论过的if(zombies[j].hit())内部的代码块:

// Not just a hit but a kill too
score += 10;
if (score >= hiScore)
{
    hiScore = score;
}
numZombiesAlive--;
// When all the zombies are dead (again)
if (numZombiesAlive == 0) 
{
    state = State::LEVELING_UP;
}

僵尸和子弹都解决了。 你现在可以运行游戏并看到血。 当然,除非我们在下一章中执行 HUD,否则你不会看到分数。

玩家是否被僵尸碰过?

这段代码比僵尸和子弹碰撞检测代码短得多,也简单得多。 在我们之前写的代码之后添加以下高亮显示的代码:

}// End zombie being shot
// Have any zombies touched the player 
for (int i = 0; i < numZombies; i++)
{
 if (player.getPosition().intersects
 (zombies[i].getPosition()) && zombies[i].isAlive())
 {
 if (player.hit(gameTimeTotal))
 {
 // More here later
 }
 if (player.getHealth() <= 0)
 {
 state = State::GAME_OVER; 
 }
 }
}// End player touched

在这里,我们通过使用for循环来检测僵尸是否与玩家发生碰撞。 对于每个活着的僵尸,代码使用intersects函数来测试是否与玩家发生碰撞。 当发生碰撞时,我们叫player.hit。 然后,我们通过调用player.getHealth来检查玩家是否已经死亡。 如果玩家的命值等于或小于 0,则将state改为GAME_OVER

你可以运行游戏,碰撞将被检测到。 然而,因为还没有 HUD 或音效,所以还不清楚是否会发生这种情况。 此外,我们还需要在玩家死亡后重新设置游戏,并开始新游戏。 所以,虽然游戏运行了,但现在的结果并不特别令人满意。 我们将在接下来的两章中对此进行改进。

玩家是否碰过拾音器?

这里显示的是玩家与两个拾音器之间的碰撞检测代码。 在我们之前添加的代码之后添加以下高亮显示的代码:

    }// End player touched
 // Has the player touched health pickup
 if (player.getPosition().intersects
 (healthPickup.getPosition()) && healthPickup.isSpawned())
 {
 player.increaseHealthLevel(healthPickup.gotIt());

 }
 // Has the player touched ammo pickup
 if (player.getPosition().intersects
 (ammoPickup.getPosition()) && ammoPickup.isSpawned())
 {
 bulletsSpare += ammoPickup.gotIt();

 }
}// End updating the scene

前面的代码使用两个简单的if 语句来查看healthPickupammoPickup是否被玩家触及。

如果已经收集了生命值,那么player.increaseHealthLevel函数将使用healthPickup.gotIt函数返回的值来增加玩家的生命值。

如果拾取了弹药,那么bulletsSpare将增加ammoPickup.gotIt返回的值。

重要提示

你现在可以运行游戏,杀死僵尸,收集皮卡! 注意,当你的生命值等于 0 时,游戏将进入GAME_OVER状态并暂停。 要重新启动它,您需要按Enter,然后按 1 到 6 之间的数字。 当我们执行 HUD、主屏幕和升级屏幕时,这些步骤对玩家来说将是直观和直接的。 我们将在下一章中这样做。

总结

这是一个忙碌的章节,但我们取得了很多成就。 我们不仅通过两个新职业在游戏中添加了子弹和拾取物,而且我们还通过检测物体之间的碰撞来让所有物体进行互动。

除了这些成就,我们还需要做更多工作来设置每款新游戏,并通过 HUD 向玩家提供反馈。 在下一章中,我们将构建 HUD。

常见问题解答

以下是你可能会想到的一些问题:

Q:有没有更好的碰撞检测方法?

)是的。 有很多方法来做碰撞检测,包括但不限于以下。

  • 你可以将对象划分成多个矩形,以更好地适应精灵的形状。 对于 c++ 来说,每一帧检查数千个矩形是完全可以管理的。 当您使用诸如邻居检查等技术来减少每帧必需的测试数量时,情况尤其如此。
  • 对于圆形物体,可以使用半径重叠方法。
  • 对于不规则多边形,可以使用传递数算法。

如果你愿意,你可以看看以下链接来回顾所有这些技巧: