面向懒惰程序员的-C--20-教程-四-

83 阅读29分钟

面向懒惰程序员的 C++20 教程(四)

原文:C++20 for Lazy Programmers

协议:CC BY-NC-SA 4.0

十、数组和enum

本章包括:数值序列(数组)、枚举类型、天气数据和棋盘游戏。

数组

“如果你不喜欢这些地方的天气,”老前辈眼睛一闪,说道,“等几分钟。”

让我们看看他是不是对的。我们将以一分钟为间隔测量十次温度,并观察其变化。我会这样开始:

double temp1; sout << "Enter a temperature: "; sin >> temp1;
double temp2; sout << "Enter a temperature: "; sin >> temp2;
double temp3; sout << "Enter a temperature: "; sin >> temp3;

那很快就过时了。也许有更好的方法来存储十个数字。这是:

constexpr int MAX_NUMBERS = 10;

double temperatures[MAX_NUMBERS]; // an array of 10 doubles

数组的 BNF 语法是 <基类型> <数组名称> [ <数组大小>];其中 <基类型> 是你想要的数组, <数组大小> 是你想要的数量。数组大小应该是一个常量整数。

现在我们有一个temperatures数组,从第 0 个开始,到第 9 个结束。(C++ 从 0 开始计数。)

要使用数组的一个元素,只需说出:

temperatures[3] = 33.6;
sout << temperatures[3];

temperatures是一个由double组成的数组,所以你可以在任何可以使用double的地方使用temperatures[3]——因为这就是它。

注意这里,[]中的数字不是数组的大小——那只是在声明中!而是你想要哪种元素。这个“index”应该是类似于int的某种可数类型,我们通常用int s:

// Get the numbers
for (int i = 0; i < MAX_NUMBERS; ++i)
{
    sout << "Enter a temperature: ");
    ssin >> temperatures[i];
}

For 循环是处理数组的一种自然方式,因为它们可以很容易地遍历每个元素。这个循环从 0 开始,只要i小于 10 ( MAX_NUMBERS,就一直循环下去,所以我们会看到temperatures[0]temperatures[1]等等,一直到temperatures[9]——都是 10。

你习惯了这样计数:一个 N 元素的数组从第 0 个开始,到第 N-1 个结束。也许你很快就会从 0 开始你的待办事项列表。

示例 10-1 是一个用于读入和传回温度的完整程序。

// Program to read in and print back out numbers
//              -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

int main (int argc, char** argv)
{
    constexpr int MAX_NUMBERS = 10;

    sout << "Enter " << MAX_NUMBERS
         << " temperatures to make your report.\n\n";

    double temperatures [MAX_NUMBERS];

    // Get the numbers
    for (int i = 0; i < MAX_NUMBERS; ++i)
    {
        sout << "Enter the next temperature: ";
        ssin >> temperatures[i];
    }

    // Print the numbers
    sout << "You entered ";
    for (int i = 0; i < MAX_NUMBERS; ++i)
        sout << temperatures[i] << ' ';

    sout << "\nHit any key to end.\n";

    SSDL_WaitKey ();

    return 0;
}

Example 10-1Reading/writing a list of numbers using an array

我们通常初始化变量。下面是如何使用数组实现这一点:

// where MAX_NUMS = 4...
double Numbers[MAX_NUMS] = {0.1, 2.2, 0.5, 0.75};1

不幸的是,括号中的列表只在初始化时有效。我们以后不能将Numbers设置为一组括号中的值。我们必须一次处理一个元素,可能会使用 for 循环,就像示例 10-1 中那样。

我不需要将MAX_NUMS放在[]中,因为 C++ 可以计算值并推断大小:

double temperatures[] = {32.6, 32.6, 32.7, 32.7, 32.7,
                         32.7, 32.7, 32.7, 32.7, 32.7};
    // I think the old guy was messing with us

如果给的值太少,它会用 0 填充其余的值: 2

double temperatures[MAX_NUMS] = {};// If it's really 0's,
                                   //   I hope we're using Fahrenheit

数组的肮脏小秘密:使用内存地址

数组不像其他变量那样存储在内存中。

数组变量实际上是一个地址:包含元素的内存块的地址。当你声明一个数组时,这使得计算机做这些事情:分配数组变量(图 10-1 左图);分配一块内存来存储元素(右图 10-1);并将第 0 个元素的地址,即该块的开始地址,放入新的数组变量中。 3

img/477913_2_En_10_Fig1_HTML.png

图 10-1

数组在内存中的存储方式

这就是 C++ 从数组索引 0 开始的原因。为了计算第i 元素的地址,它将i* sizeof (int),即i乘以一个int的大小,添加到数组的位置。如果你从 1 开始,那就得加上(i-1)*sizeof(int))。C++,尤其是它的祖先 C,喜欢尽可能高效地做事,即使在过程中牺牲了一些清晰性。

那么在图 10-1 中,数组大小存储在哪里呢?如何查询以后是否需要?

我不能。C++ 在分配内存时使用数组大小,但是之后,如果你想要那个数字——比方说,为了确保你不会在数组中走得太远——你必须自己跟踪它(目前)。如果您声明了一个包含五个元素的数组,并试图访问第五个元素(因为 C++ 使用基于 0 的计数,所以第五个元素不存在),它会让您。这意味着你正在读取一大块内存,而这些内存是用来做其他事情的。

如果像在A[5] = 0中那样写入那块内存,情况会更糟。如果这样做,可能会覆盖构成其他变量的数据。

防错法

  • 你的循环在数组中超出了一个元素。我敢打赌,比较运算符是罪魁祸首——通常都是这样!变化

    for (int i = 0; i < = N; ++i)

    for (int i = 0; i <

  • 变量的值改变了,但你没有告诉它。你可能对一个数组使用了太大的索引,从而覆盖了一个不同的变量。

  • **你的程序崩溃(停止运行)。**在 Unix 中,你得到Segmentation fault: core dumped。在其他地方,您可能会看到一个窗口,显示“<您的程序>已经停止工作”或者(在 Visual Studio 中)“抛出异常”

    此时一个可能的原因是…使用了一个对数组来说太大的索引。

Exercises

在下文中,如果出现问题,一定要使用调试器。

  1. 编写一个程序,从用户那里获得一周的所有七个每日高温和每日低温,并告诉用户哪一天有最低低温和最高高温。

  2. 做同样的程序,但不要问用户;使用{}初始化数组。

  3. 给定一个char的数组(使用{}初始化),报告字符是否按字母顺序排列。

  4. 读入一个整数列表,并反向打印出来。

作为函数参数的数组

当我想把一个变量传递给一个函数时,我所做的就是复制声明(没有分号)并用它来定义参数。也就是说,为了声明x,我会写int x;,所以如果我想把x发送到函数f,我会写void f (int x);

数组也是如此:

void f (int myArray[ARRAY_SIZE]);

示例 10-2 显示了在数组中查找最小温度的函数。

double lowestTemp(double temperatures[MAX_NUMBERS])
// returns lowest entry in temperatures
{
    double result = temperatures[0];

    for (int i = 0; i < MAX_NUMBERS; ++i)
        if (temperatures[i] < result)
            result = temperatures[i];

    return result;
}

Example 10-2lowestTemp, taking an array and returning its smallest element

这就是所谓的

sout << "The lowest temp was " << lowestTemp (temperatures);

根据我们目前所知,让一个函数返回一个数组是不可能的。现在我们只是把它们作为参数传入。

改变或不改变的数组参数

我不必对数组使用&,即使我想改变数组的内容。原因如下。

记住一个数组变量并不是所有不同的元素;是 0 号的地址(见图 10-1 )。如果你不使用&,你不能改变它——这没问题。我们不想改变地址;我们只想改变的内容——它们不是传入的东西!随心所欲地修改它们——当函数返回时,你的修改就会出现。

你可能会说,“这难道不违反安全吗?如果被调用的函数改变了它们,而它们本不应该被改变,那该怎么办?”说得好。解决方法如下:

double lowestTemp (const double temperatures[ARRAY_SIZE])
// returns lowest entry in temperatures
{
   ...

将数组声明为const可以确保lowestTemp不能改变它的元素——这是一个需要养成的好习惯。

数组参数和可重用性

因为 C++ 不关心你的数组有多大,当你把它传递给一个函数时,它完全忽略了在[]之间给定的大小。C++ 不在乎。这意味着相同的函数可以用于相同基类型的任何数组,而不管大小如何。

这里有一个版本的lowestTemp,它不会把你限制在某个特定的尺寸:

double lowestTemp (const double temperatures[], int arraySize)
// returns lowest entry in temperatures

{
   bool result = temperatures[0];

   for (int i = 0; i < arraySize; ++i)
       ...
}

现在让我们更加灵活。正如第七章所建议的,让你的函数通用化、通用化,从而可重用是很好的。我们称之为lowestTemp的函数没有理由只对温度起作用。更一般地编写它,如示例 10-3 ,你可以在另一个程序中使用它。代码重用,耶。

double minimum(const double elements[], int arraySize)
// returns lowest entry in elements
{
    double result = elements[0];

    for (int i = 0; i < arraySize; ++i)
        if (elements[i] < result)
            result = elements[i];

    return result;
}

Example 10-3minimum, a function that can be used for any double array, any size

防错法

  • 函数调用时,编译器报错“从 int 到 int*的无效转换”或“int 到 int[]”。它需要一个数组,但只得到一个值。我们不是给了它一个数组吗?在这个例子中

    minimum (temperatures[NUMTEMPS], NUMTEMPS);

    …我没有。我给了它NUMTEMPS th 温度,不管那是什么。

    这个问题是混淆了[]的两种用法。当声明时,在[]之间的是数组大小。在其他时候,它是我们想要访问的元素。

    由于数组的名称是temperatures,而不是temperatures[NUMTEMPS],我将把它传入:

    minimum (temperatures, NUMTEMPS);

Golden Rule of [ ] for Arrays

在数组引用中的[]之间是你想要的元素(除了在声明的时候——然后是数组的大小)。

Exercises

在这些练习中,如果出现问题,记得使用您最喜欢的调试器。

  1. 编写一个maximum函数来对应示例 10-3 中的minimum,并使用它们来查找给定温度数组中的范围,从而回答本章开始时提出的问题:天气变化有多快。如果范围超过半度,打印“你是对的;这里的天气变化真快!”

  2. 对于一个月的温度,报告最高的最高,最低的最低,和最大差距的一天。

  3. (更难)写一个程序,将一个给定的月份的最高和最低温度绘制成图形(你会想用{}的初始化数组;每次输入数值会很麻烦),显示 X 轴和 Y 轴上的日期和温度,并用点来标记数据点。你肯定想先写算法。

枚举类型

为了准备在棋盘游戏或一年中的几个月使用扑克牌或彩色棋子,我们来看看快速制作几个常量符号的方法:enum

enum class Suit {CLUBS, DIAMONDS, HEARTS, SPADES}; // Playing card

相当于

constexpr int CLUBS    = 0, //  we start at 0
              DIAMONDS = 1, //  and go up by one for each new symbol
              HEARTS   = 2,
              SPADES   = 3;

但是编写起来更快,并且还创建了一个新的类型Suit,因此您可以声明该类型的变量:

Suit firstCardSuit = Suit::HEARTS, secondCardSuit = Suit::SPADES;
               // Yes, we have to put Suit:: first,
               //    but we'll fix that in a few pages

firstCardSuit是什么*,真的吗?真的是Suit!但是,是的,它非常像一个int。为什么不干脆做成int?澄清:当你宣布一个int时,不清楚你是否真的想把它当成一套牌。如果你把它声明为Suit,那就很明显了。*

在 BNF 中,enum声明是enum class{<值列表> };

根据本书中使用的命名约定,我们创建的新类型是大写的(像Suit,但不像int;标准中内置的类型是小写的)。

我们还可以指定符号对应的整数:

enum class Rank {ACE=1, JACK=11, QUEEN, KING}; // Playing card rank

我们没有为QUEEN指定一个值,所以它一直从JACK开始计数:QUEEN是 12;KING是 13。

enum值意味着标签而不是数字,所以尽管你可以分配给enum变量(=)并比较它们(==<<=等)。),你不能轻易和他们做数学;不能用++--+-*/%。不能用sout打印,也不能用ssin阅读。你不能给它们赋值int(myRank = 8不会编译)。那么它们有什么好处呢?

有时候你不需要做那些事情。

如果你一定要做数学,有一个变通办法,但不好玩。(我们会在第十九章找到更好的方法。)需要给myRank一个值 8?使用铸件:

myRank = Rank (8);

需要继续下一个吗?再次铸造:

myRank = Rank (int(myRank)+1); // ack, that's a lot of casting!

想打印一个enum怎么办?编译器不可能知道我们想要如何打印一个Suit,所以我们会告诉它:

void print (Suit suit) // prints a Suit

{
    switch (suit)
    {
    case Suit::CLUBS   : sout << 'C';   break;
    case Suit::DIAMONDS: sout << 'D';   break;
    case Suit::HEARTS  : sout << 'H';   break;
    case Suit::SPADES  : sout << 'S';   break;
    default            : sout << '?';   break;
    }
}

好了,就这样。我已经受够了老是打字。我会告诉 C++ 在Suit中查找这个符号,而不用我指定(例如 10-4 )。

void print (Suit suit) // prints a Suit
{
    using enum

Suit;

    switch (suit)
    {
    case CLUBS   : sout << 'C';   break;
    case DIAMONDS: sout << 'D';   break;
    case HEARTS  : sout << 'H';   break;
    case SPADES  : sout << 'S';   break;
    default      : sout << '?';   break;
    }
}

Example 10-4A function for printing playing card suit

using enum是漂亮的出血边缘。 4 如果你的编译器不支持它,只需省去单词class,稍后它会让你省去Suit:::

enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}; // Playing card suit
...
switch (suit)
{
case CLUBS: ...;                             //No need for "Suit::"

比旧版本更安全,但两者都有效。

Extra

如果您的编译器还不支持using enum,源代码会在编译可能导致问题的代码行之前测试是否支持:

// Suit and Rank enumeration types
#ifdef __cpp_using_enum
                 // if __cpp_using_enum is defined, that is,
                 // if the compiler supports "using enum

,"
                 //    then use enum class
enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES };
enum class Rank { ACE = 1, JACK = 11, QUEEN, KING };
#else            // otherwise use plain enum
enum       Suit { CLUBS, DIAMONDS, HEARTS, SPADES };
enum       Rank { ACE = 1, JACK = 11, QUEEN, KING };
#endif
...
#ifdef __cpp_using_enum //do the "using enum," if supported
    using enum Suit;
#endif

这让我们可以使用可用的新特性,如果不可用,我们可以找到其他方法来编写代码。其他最近添加的功能也有类似的“宏名”,比如(比如)__cpp_concepts。在写作的时候,你可以在 https://en.cppreference.com/w/cpp/feature_test 找到一个完整的列表。

但是在编写你自己的程序时,你可以忽略这一点,使用你的编译器已经准备好的enum版本。

防错法

C++ 一点也不在乎你的某个enum类型的变量是否超出了你列出的值:

r = Rank (-5000);

解决办法是,嗯,不要那样做。

Exercises

  1. 声明棋子的枚举类型:国王、王后、主教、骑士、车和卒。

  2. 为太阳系中的行星声明一个枚举类型。地球是第三颗行星,所以调整你的编号,使EARTH是 3,所有其他行星也正确编号。

  3. 写一个函数printRank,给定一个Rank,适当地打印它——作为A, 2, 3, 4, 5, 6, 7 8, 9, 10, J, Q, or K。这个练习和下一个练习在第十九章的纸牌游戏例子中会很有用。

  4. 编写一个函数readRank,它使用与练习 3 中相同的格式返回它读入的Rank。是的,这是一个问题,有些输入是数字,有些是字母——那么你需要什么类型的变量来处理这两者呢?

  5. 写一个函数来播放这些风格的音乐:幽灵、狂欢节、外星人或者任何你喜欢的风格(关于音乐的复习见第二章)。传入一个enum参数来告诉它是哪种样式。

    编写另一个程序,根据用户点击的框选择使用哪种样式……另一个程序绘制框供用户点击。

    把它们放在一起就成了音乐播放器。

多维数组

不是所有的数组都是简单的列表。您可以拥有二维或多维数组。

这是一个井字游戏棋盘的数组:一个 3 × 3 的网格。每个正方形可以包含一个 X、一个 O 或什么都不包含:

constexpr int MAX_COLS = 3, MAX_ROWS = 3;
enum class Square { EMPTY, X, O };
Square board[MAX_ROWS][MAX_COLS];

要将第 1 行第 2 列中的方块设置为X,我们说board[1][2] = Square::X;并检查第row col 个方块,我们说

if (board[row][col] == Square::X) ...

图 10-2 展示了 C++ 如何在内存中排列数组。首先,我们有第 0 ,从 0 到最后一列。然后是第 1 排第 1 排,然后是第 2 排第 2 排

img/477913_2_En_10_Fig2_HTML.png

图 10-2

二维数组在内存中的排列方式。(这次我省略了实际地址,以强调我们不必知道它们)

每一行都有MAX_COLS个方块,所以为了得到board[1][2],C++ 计算出它是 1 * MAX_COLS + 2 =向下 5 个方块。从图 10-2 中的初始元素开始向下数五,我们就到了board[1][2],这就是我们想要的。

展示公告板

画板的两个基本步骤是什么?

draw the board itself (the grid)
draw the X's and O's on the board

画网格只是做一些线,这里就不花时间了。我们可以像往常一样,一点一点地分解绘制 x 和 Os:

for every row
    draw the row

我们如何画这条线?让我们提炼一下:

for every row
    for every column
        draw the square

我们怎么画正方形呢?上次优化:

for every row
    for every column
        if board[row][column] contains X draw an X
        else if it has an O draw an O

示例 10-5 是结果程序。输出如图 10-3 所示。

// Program to do a few things with a Tic-Tac-Toe board
//              -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

// Dimensions of board and text notes
constexpr int MAX_ROWS     =   3, MAX_COLS    =   3,
              ROW_WIDTH    = 100, COL_WIDTH   = 100,
              BOARD_HEIGHT = 300, BOARD_WIDTH = 300;
                 // enough room for 3x3 grid, given these widths

constexpr int TEXT_LINE_HEIGHT = 20;

// A Square is a place in the TicTacToe board
enum class Square { EMPTY, X, O };

// Displaying the board
void display (const Square board[MAX_ROWS][MAX_COLS]);

int main(int argc, char** argv)
{
    using enum Square;

    // Shrink the display to fit our board
    //  allowing room for 2 lines of text at the bottom;
    //  set title
    SSDL_SetWindowSize     (BOARD_WIDTH,
                            BOARD_HEIGHT + TEXT_LINE_HEIGHT * 2);
    SSDL_SetWindowTitle    ("Hit any key to end.");

    // Colors
    SSDL_RenderClear       (SSDL_CreateColor(30, 30, 30)); //charcoal
    SSDL_SetRenderDrawColor(SSDL_CreateColor(245, 245, 220)); //beige

    // The board, initialized to give X 3 in a row

    Square board[MAX_ROWS][MAX_COLS] =
            { {EMPTY,5 

EMPTY,    X},
              {EMPTY,     X, EMPTY},
              {    X,     O,     O} };

    display (board);           // display it

    // Be sure the user knows what he's seeing is the right result
    SSDL_RenderText("You should see 3 X's diagonally, ",
                    0, MAX_ROWS * ROW_WIDTH);
    SSDL_RenderText("and two O's in the bottom row.",
                    0, MAX_ROWS * ROW_WIDTH + TEXT_LINE_HEIGHT);

    SSDL_WaitKey();

    return 0;
}

void display (const Square board[MAX_ROWS][MAX_COLS])
{
    // Make 'em static: loaded once, and local to the only function
    //   that needs 'em. What's not to like?
    static const SSDL_Image X_IMAGE = SSDL_LoadImage("media/X.png");
    static const SSDL_Image O_IMAGE = SSDL_LoadImage("media/O.png");

    // draw the X's and O's
    for (int row = 0; row < MAX_ROWS; ++row)
        for (int col = 0; col < MAX_COLS; ++col)
            switch (board[row][col])
            {

            case Square::X: SSDL_RenderImage(X_IMAGE,
                                     col*COL_WIDTH, row*ROW_WIDTH);
                            break;
            case Square::O: SSDL_RenderImage(O_IMAGE,
                                     col*COL_WIDTH, row*ROW_WIDTH);
            }

    // draw the lines for the board: first vertical, then horizontal
    // doing this last stops X and O bitmaps from covering the lines
    constexpr int LINE_THICKNESS = 5;

    SSDL_RenderFillRect(COL_WIDTH     - LINE_THICKNESS / 2, 0,
                        LINE_THICKNESS, BOARD_HEIGHT);
    SSDL_RenderFillRect(COL_WIDTH * 2 - LINE_THICKNESS / 2, 0,
                        LINE_THICKNESS, BOARD_HEIGHT);
    SSDL_RenderFillRect(0, ROW_WIDTH  - LINE_THICKNESS / 2,
                        BOARD_WIDTH, LINE_THICKNESS);
    SSDL_RenderFillRect(0, ROW_WIDTH*2- LINE_THICKNESS / 2,
                        BOARD_WIDTH, LINE_THICKNESS);
}

Example 10-5Initializing and displaying a Tic-Tac-Toe (noughts and crosses) board

为了编写display的参数列表,我们在()之间复制ticTacToeBoard的定义

但是与一维数组不同的是,你不能随意省略[]之间的数字。正如我们在图 10-2 中看到的,C++ 使用MAX_COLS来确定元素的内存位置。你可以省略掉第一个维度,但这并不能让它变得更清楚,所以我没有。

img/477913_2_En_10_Fig3_HTML.jpg

图 10-3

井字游戏棋盘

Tip

示例 10-5 不仅仅显示适当的输出;它打印出输出应该是什么。这是不是矫枉过正?

我不这么认为。在屏幕上看到结果要比搜索代码容易得多。稍后我们会看到更懒惰的测试方法。

二维以上的数组

在前面的例子中,我们的数组是二维的。我们能有三维数组吗?4-D?你可能想要多少维度就有多少维度。

若要初始化三维数组,请使用另一组嵌套的{}

但我发现的三维阵列的唯一用途是 1971 年基于文本的星际迷航游戏(在“象限”之间与克林贡船只战斗)和三维井字游戏。我从来没有发现四维数组的用处。如果你找到了,别告诉我。有些事情我不想知道。

防错法

  • 你的二维数组中的东西进入了错误的位置。当你指的是col时使用row或者当你指的是row时使用col可能会导致这种情况。

    最好的预防是在你所谓的行和列中保持一致:不要有时用rowcol、有时用xy,有时用ij。永远用rowcol。也可以用row1col1rowStartcolStart,但总是名称中带rowcol的东西。

Exercises

在这些练习和后续练习中,如果出现问题,请记住使用调试器。

img/477913_2_En_10_Figa_HTML.jpg

  1. 做一个棋盘:八行八列,明暗相间的方格。

  2. 将棋子放在棋盘上,进行初始游戏配置:如图所示,交替放置方块。

img/477913_2_En_10_Figb_HTML.jpg

  1. 对于棋盘,创建一个函数,对给定颜色的棋盘进行计数并返回计数。

  2. …现在是一个决定哪一面有更多棋子的函数。如果两者都没有更多,它可以返回EMPTY

  3. 编写一个函数,该函数采用一个棋盘、一个棋子的位置和一个方向LEFTRIGHT(使用enum),并返回该棋子是否可以在该方向上移动。一个棋子可以对角向前移动一格到一个空的方格,或者对角向前移动两格,跳过对手的棋子到一个空的方格。

  4. (更难)在游戏内存中,你有(比如说)八副牌,每副牌显示一个相同的图像。他们面朝下在一个 4 × 4 的格子里发牌;玩家选两张,把它们翻过来,如果它们是一样的,那两张卡就被拿走。在相对较少的回合中找到所有匹配的对子,你就赢了。

    制作一个程序来玩这个游戏。(肯定先写算法。)让用户点击一副牌;通过用“卡片正面”图像替换“卡片背面”图像来显示卡片;等待用户看到卡片正面(使用SSDL_Delay);然后,如果有一个匹配,不替换图像,增加玩家的分数,否则用“卡片背面”图像再次替换它们。根据是否匹配播放不同的声音。重复,直到所有的卡都匹配或玩家已经超过了一些最大的回合数。

    您将需要代码来识别鼠标在一个框区的点击。

  5. (用力)写一个完整的井字游戏。对于计算机移动,你可以选择一个随机的位置进行下一次移动。或者你可以做一些更难的事情,让计算机计算出什么是好的一步。

  6. (硬)玩连四。在这个游戏中,你有一个最初的空格子,玩家轮流将代币放入顶行。代币会自动尽可能远地下落:它不能越过最下面一行,也不能进入被占领的方块。赢家是在任何方向连续得到四个的人。

Footnotes 1

如果你愿意,可以省略=:double Numbers[] {0.1, 2.2, 0.5, 0.75};

  2

“零初始化。”如果数组成员的类型为 0(或 0.0),它们将被设置为 0。

  3

在这个例子中,我用十六进制的(基数 16)写地址,这是惯例。每行增加 4 的原因是我假设int占用了 4 个内存位置,也就是 4 字节。这些在这里都不重要,但是我不想让 C++ 之神嘲笑我的图。

  4

出血边缘:如此新,它可能不可靠或可靠的支持。

  5

记住,如果你的编译器不兼容 C++20,你需要把Square::<Emphasis FontCategory="NonProportional"> here (or skip the word <Emphasis FontCategory="NonProportional">class放在enum类型声明中。

 

十一、使用结构和精灵的动画

是时候拍些电影了(不久之后,还有街机游戏)。我们需要更多的功能。

struct年代

struct是一种捆绑信息的方式:

struct <name>
{
    <variable declaration>*
};

例如,这里有一种我们已经需要了一段时间的类型:几何点。它有两部分,x 和 y:

struct Point2D
{
   int x_, y_;
};

(后面的_是一个约定,意思是“其他事物的成员”我们将在第十六章中看到为什么这是值得考虑的。)

这个版本更好。我们将在struct中构建默认值。0 是一个很好的默认值:

struct Point2D
{
   int x_= 0, y_= 0;
};

现在我们可以使用新的类型来声明点:

Point2D p0; // The x_and y_ members are both 0
            // since we made that the default

你可以像初始化数组一样初始化一个struct——用一个有支撑的列表: 1

Point2D p1 = {0, 5};               // x is 0, y is 5

…但与数组不同的是,您可以在初始化后使用{}创建一个新值:

p1         = {1, 5};               // now x is 1
functionThatExpectsAPoint ({2,6}); // make a Point2D on the fly

要获取Point2D的各个部分,请使用。:

p1.x_ += AMOUNT_TO_MOVE_X;
p1.y_ += AMOUNT_TO_MOVE_Y;
SSDL_RenderDrawCircle (p1.x_, p1.y_, RADIUS);

应该可以了。

为什么有struct s?

  • 清晰:把一个点想成一个点,比想成一个 x 和一个 y 更容易。

  • 更短的参数列表:检测鼠标点击是否在框内的函数不再需要像

    bool containsClick (int x, int y,
                        int xLeft, int xRight,
                        int yTop,  int yBottom);
    
    

    中那样有六个参数

而是三个,就像

  • 数组:假设你希望你的宇宙中有多个对象(看起来很有可能!).每个都有一个 x,y 坐标。你怎么能把这些排列起来呢?将 x 和 y 捆绑成一个Point2D,并有一个这样的数组:
bool containsClick (Point2D p,
                    Point2D upperLeft, Point2D lowerRight);

Point2D myObjects[MAX_OBJECTS];

要初始化它们,您可以使用{}初始化列表:

Point2D myObjects[MAX_OBJECTS] = {{1, 5}, {2, 3}};

示例 11-1 显示了这种新型的用途。输出如图 11-1 所示。

img/477913_2_En_11_Fig1_HTML.jpg

图 11-1

阶梯计划

//  Program to draw a staircase
//              -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

struct Point2D  // A struct to hold a 2-D point
{
    int x_, y_;
};

int main (int argc, char** argv)
{
    SSDL_SetWindowSize  (400, 200);
    SSDL_SetWindowTitle ("Stairway example:  Hit a key to end");

    constexpr int MAX_POINTS        = 25;
    constexpr int STAIR_STEP_LENGTH = 15;

    Point2D myPoints [MAX_POINTS];

    int x = 0;                          // Start at lower left corner
    int y = SSDL_GetWindowHeight()-1;   //  of screen

    for (int i = 0; i < MAX_POINTS; ++i) // Fill an array with points
    {
        myPoints[i] = { x, y };

        // On iteration 0, go up (change Y)
        // On iteration 1, go right
        // then up, then right, then up...

        if (i%2 == 0)                    // If i is even...
            y -= STAIR_STEP_LENGTH;
        else
            x += STAIR_STEP_LENGTH;
    }

    for (int i = 0; i < MAX_POINTS-1; ++i) // Display the staircase
        // The last iteration draws a line from point
        //   i to point i+1... which is why we stop a
        //   little short.  We don't want to refer to
        //   the (nonexistent) point # MAX_POINTS.
        SSDL_RenderDrawLine (   myPoints[i  ].x_, myPoints[i  ].y_,
                                myPoints[i+1].x_, myPoints[i+1].y_);

    SSDL_WaitKey();

    return 0;
}

Example 11-1Staircase program, illustrating struct Point2D

Exercises

  1. 编写并测试前面给出的containsClick函数(点击并输入信息的函数)。

  2. Make an array of Point2Ds, and set the value of each with this function:

    Point2D pickRandomPoint (int range)
    {
       Point2D where;
    
       where.x_ =
         rand()%range + rand()%range + rand()%range;
       where.y_ =
         rand()%range + rand()%range + rand()%range;
    
       return where;
    }
    
    

    展示它们,注意:它们是均匀分布的吗?这显示了当你对随机数求和时会发生什么。

structwhile拍电影

想想电影是怎么拍出来的。你看到一个接一个的静止画面,但是它们来得如此之快,看起来就像一个连续的运动图像。

我们会做同样的事情。一部真正的电影有特定的速度——每秒帧数——所以运动的速率总是相同的。我们将告诉 C++ 也保持一个恒定的帧速率。

这里有一个粗略的版本:

SSDL_SetFramesPerSecond (70); // Can change this,
                              //  or leave at the default of 60

while (SSDL_IsNextFrame ())
{
    SSDL_DefaultEventHandler ();

    SSDL_RenderClear ();    // erase previous frame

    // display things (draw shapes and images, print text, etc.)

    // update variables if needed

    // get input, if relevant...
}

等待足够的时间,以进入电影的下一帧。它还会刷新屏幕。它将每秒 60 帧,除非我们用SSDL_SetFramesPerSecond改变它。如果用户试图通过关闭窗口或按下退出键来退出,则SSDL_IsNextFrame返回false,,循环结束。

但是必须要检查那些退出消息。之前我们用的是SSDL_WaitKeySSDL_WaitMouseSSDL_Delay。因为我们现在没有使用它们,所以必须用其他东西来检查退出消息。

这个东西就是SSDL_DefaultEventHandler,它处理事件——来自操作系统的消息告诉程序,“发生了一些你可能关心的事情”,比如退出请求:

void SSDL_DefaultEventHandler ()
{
    SDL_Event event;

    while (SSDL_PollEvent (event))
        switch (event.type)
     {
     case SDL_QUIT:    // clicked the X on the window? Let's quit
            SSDL_DeclareQuit(); break;
     case SDL_KEYDOWN: // User hit Escape? Let's quit
            if (SSDL_IsKeyPressed (SDLK_ESCAPE)) SSDL_DeclareQuit();
     }
}

SDL_Event是一个struct,它存储 SDL 识别的任何类型事件的信息。SSDL_PollEvent获取下一个可用的事件,如果有的话,如果没有则失败;但是如果找到一个,它会将信息存储在event中,然后由switch语句决定如何处理它。

让我们在程序中使用它来让一个球在屏幕上来回移动。呜-呼!输出如图 11-2 所示。

img/477913_2_En_11_Fig2_HTML.jpg

图 11-2

在屏幕上来回移动的球

// Program to make a circle move back and forth across the screen
//          -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

constexpr int RADIUS = 20;     // Ball radius & speed
constexpr int SPEED  =  5;     //  ...move 5 pixels for every frame

enum class Direction { LEFT=-1, RIGHT=1 };
// Why -1 for left?  Because left means going in the minus direction.
// See where we update the x_ in the main loop for how this can work

struct Point2D
{
    int x_=0, y_=0;
};

struct Ball             // A ball is an X, Y location,
{                       // and a direction, left or right
    Point2D   location_;
    Direction direction_;
};

int main (int argc, char** argv)
{
    SSDL_SetWindowTitle ("Back-and-forth ball example.  "
                         "Hit Esc to exit.");

    // initialize ball position; size; rate and direction of movement
    Ball ball;
    ball.location_  = { SSDL_GetWindowWidth () / 2,
                        SSDL_GetWindowHeight() / 2 };
    ball.direction_ = Direction::RIGHT;

    constexpr int FRAMES_PER_SECOND = 70;
    SSDL_SetFramesPerSecond (FRAMES_PER_SECOND);

    while (SSDL_IsNextFrame ())
    {
        SSDL_DefaultEventHandler ();

        // *** DISPLAY THINGS ***
        SSDL_RenderClear ();    // first, erase previous frame

        // then draw the ball

        SSDL_RenderDrawCircle (ball.location_.x_, ball.location_.y_, RADIUS);

        // *** UPDATE THINGS ***
        // update ball's x position based on speed
        //   and current direction
        ball.location_.x_ += int(ball.direction_)*SPEED;

        // if ball moves off screen, reverse its direction
        if      (ball.location_.x_ >= SSDL_GetWindowWidth())
            ball.direction_ = Direction::LEFT;
        else if (ball.location_.x_ < 0)
            ball.direction_ = Direction::RIGHT;
    }

    return 0;
}

Example 11-2A ball moving back and forth across the screen

如果我们希望不止一个物体运动呢?我们可以有一个数组Ball并使用 for 循环来初始化它们,显示它们,等等。

越来越长,越来越不清楚,所以我将把几个任务放在它们各自的功能中。示例 11-3 的输出如图 11-3 所示。

img/477913_2_En_11_Fig3_HTML.jpg

图 11-3

多个移动球的示例

// Program to make circles move back and forth across the screen
//          -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

constexpr int RADIUS = 20;     // Ball radius & speed
constexpr int SPEED  =  5;     //  ...move 5 pixels for every frame

enum class Direction { LEFT=-1, RIGHT=1 };

struct Point2D
{
    int x_=0, y_=0;
};

struct Ball             // A ball is an X, Y location,
{                       // and a direction, left or right
    Point2D   location_;
    Direction direction_;
};

// Ball functions

void initializeBalls (      Ball balls[], int howMany);
void drawBalls       (const Ball balls[], int howMany);
void moveBalls       (      Ball balls[], int howMany);
void bounceBalls     (      Ball balls[], int howMany);

int main (int argc, char** argv)
{
    SSDL_SetWindowTitle ("Back-and-forth balls example.  "
                         "Hit Esc to exit.");

    // initialize balls' position, size, and rate and direction
    constexpr int MAX_BALLS = 3;
    Ball balls [MAX_BALLS];
    initializeBalls (balls, MAX_BALLS);

    constexpr int FRAMES_PER_SECOND = 70;
    SSDL_SetFramesPerSecond(FRAMES_PER_SECOND);

    while (SSDL_IsNextFrame ())
    {
        SSDL_DefaultEventHandler ();

        // *** DISPLAY THINGS ***
        SSDL_RenderClear ();           // first, erase previous frame
        drawBalls  (balls, MAX_BALLS);

        // *** UPDATE  THINGS ***
        moveBalls  (balls, MAX_BALLS);
        bounceBalls(balls, MAX_BALLS); // if ball moves offscreen,
                                       //  reverse its direction
    }

    return 0;
}

// Ball functions

void initializeBalls (Ball balls[], int howMany)
{
    for (int i = 0; i < howMany; ++i)
    {
        balls[i].location_ = { i * SSDL_GetWindowWidth () / 3,
                               i * SSDL_GetWindowHeight() / 3
                                 + SSDL_GetWindowHeight() / 6 };
        balls[i].direction_   = Direction::RIGHT;
    }
}

void drawBalls  (const Ball balls[], int howMany)
{
    for (int i = 0; i < howMany; ++i)
        SSDL_RenderDrawCircle (balls[i].location_.x_,
                               balls[i].location_.y_, RADIUS);
}

// update balls' x position based on speed and current direction
void moveBalls  (Ball balls[], int howMany)
{
    for (int i = 0; i < howMany; ++i)
        balls[i].location_.x_ += int (balls[i].direction_)*SPEED;
}

void bounceBalls(Ball balls[], int howMany)
{
    // if any ball moves off screen, reverse its direction
    for (int i = 0; i < howMany; ++i)
        if      (balls[i].location_.x_ >= SSDL_GetWindowWidth())
            balls[i].direction_ = Direction::LEFT;
        else if (balls[i].location_.x_ <  0)
            balls[i].direction_ = Direction::RIGHT;
}

Example 11-3An example with multiple moving balls

Exercises

  1. Make the balls capable of moving in other directions. A ball is no longer just an x, y location and a direction; it’s an x, y location and an x, y velocity. Each time you go through the main loop, you’ll update the location based on the velocity:

    for (int i = 0; i < MAX_BALLS; ++i)
    {
       balls[i].location_.x_ +=
                    balls[i].velocity_.x_;
       balls[i].location_.y_ +=
                    balls[i].velocity_.y_;
    }
    
    

    当速度碰到左墙或右墙时,速度的 x 分量总是反向的——如果速度碰到地板或天花板,速度的 y 分量也是如此。用Point2D代替velocity_也可以,或者你可以为它创建一个新的struct

    每当球击中墙壁时添加音效。

  2. Now let’s add gravity. Velocity doesn’t just change each time you hit a wall; it changes in each iteration of the loop, like so:

    for (int i = 0; i < MAX_BALLS; ++i)
         balls[i].velocity_.y_+= GRAVITY;
                     // adjust velocity for gravity
    
    

    现在球应该更真实地移动。

  3. 现在加上摩擦力。每当球碰到墙时,它的速度并不完全相反;而是反过来,只是比原来小了一点。这将使球在每次碰撞后变慢。

鬼怪;雪碧

圈子够多了。让我们移动图像。

我们已经有了图像,但它们只是放在那里。精灵是移动的图像:它们可以移动、旋转、翻转和做其他事情。以下是基本情况。

创建精灵就像创建图像一样:

SSDL_Sprite mySprite = SSDL_LoadImage ("filename.png");

你现在可以用SSDL_SetSpriteLocation设置它的位置,用SSDL_SetSpriteSize设置它的大小。

在示例 11-4 中,我使用 sprite 将一条鱼放在屏幕中间——也许我要制作一个视频水族馆——并使用其他 sprite 函数打印一些关于它的信息。与 sprite 相关的代码会突出显示。输出如图 11-4 所示。

// Program to place a fish sprite on the screen
//              -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

using namespace std;

int main (int argc, char** argv)
{
    // Set up window characteristics
    constexpr int WINDOW_WIDTH = 600, WINDOW_HEIGHT = 300;
    SSDL_SetWindowSize  (WINDOW_WIDTH, WINDOW_HEIGHT);
    SSDL_SetWindowTitle ("Sprite example 1\.  Hit Esc to exit.");

    // initialize colors
    const SSDL_Color AQUAMARINE(100, 255, 150); // the water

    // initialize the sprite's image and location

    SSDL_Sprite fishSprite = SSDL_LoadImage("media/discus-fish.png");
    SSDL_SetSpriteLocation (fishSprite,
                            SSDL_GetWindowWidth ()/2,
                            SSDL_GetWindowHeight()/2);

    // *** Main loop ***
    while (SSDL_IsNextFrame ())
    {
        // Look for quit messages
        SSDL_DefaultEventHandler ();

        // Clear the screen for a new frame in our "movie"
        SSDL_RenderClear (AQUAMARINE);

        // Draw crosshairs in the center
        SSDL_SetRenderDrawColor (BLACK);
        SSDL_RenderDrawLine (0, SSDL_GetWindowHeight()/2,
                             SSDL_GetWindowWidth (),
                             SSDL_GetWindowHeight()/2);
        SSDL_RenderDrawLine (SSDL_GetWindowWidth ()/2,  0,
                             SSDL_GetWindowWidth ()/2,
                             SSDL_GetWindowHeight());

        // and print the statistics on the fish
        SSDL_SetCursor (0, 0);  // reset cursor each time or
                                //   the messages will run off
                                //   the screen!
        sout << "Sprite info\n";
        sout << "X:\t"
             << SSDL_GetSpriteX
     (fishSprite) << endl;
        sout << "Y:\t"
             << SSDL_GetSpriteY
     (fishSprite) << endl;
        sout << "Width:\t"
             << SSDL_GetSpriteWidth
(fishSprite) << endl;
        sout << "Height:\t"
             << SSDL_GetSpriteHeight
(fishSprite) << endl;

        // Show that fish
        SSDL_RenderSprite
(fishSprite);
    }

    return 0;
}

Example 11-4Program to draw a fish, using a sprite

img/477913_2_En_11_Fig4_HTML.jpg

图 11-4

一个精灵和它目前的一些规格

我认为这条鱼太大了。根据程序,它的宽度是 225,高度是 197。我们可以把SSDL_SetSpriteSize放在主循环之前,来调整它的大小: 2

constexpr int FISH_WIDTH = 170, FISH_HEIGHT = 150;
SSDL_SetSpriteSize (fishSprite, FISH_WIDTH, FISH_HEIGHT);

img/477913_2_En_11_Fig5_HTML.jpg

图 11-5

一个精灵,调整大小

如图 11-5 所示:现在我要它居中。我给它的 x,y 位置是中心的…但那是图像的左上角。

下面是偏移精灵的调用,因此它以我们给定的点为中心作为它的位置:

SSDL_SetSpriteOffset (fishSprite, FISH_WIDTH/2, FISH_HEIGHT/2);

如果它看起来仍然偏离中心,我可以用数字来得到不同的偏移量。

我不会重复整个程序,但示例 11-5 显示了改变精灵大小和居中的代码行。结果如图 11-6 所示。

img/477913_2_En_11_Fig6_HTML.jpg

图 11-6

一个精灵,调整大小并居中

int main (int argc, char** argv)
{
    ...

    // Init size and offset. Image is offset so fish looks centered.
    constexpr int FISH_WIDTH = 170, FISH_HEIGHT = 150;
    SSDL_SetSpriteSize   (fishSprite, FISH_WIDTH, FISH_HEIGHT);

    // This offset looks right on the screen, so I'll use it:
    SSDL_SetSpriteOffset (fishSprite,
                          FISH_WIDTH/2, int(FISH_HEIGHT*0.55));
    ...

    return 0;
}

Example 11-5Code to resize and center a sprite

你可以用精灵做其他事情:旋转,水平或垂直翻转,或者只使用原始图像的一部分。您还可以对它们做任何您可以对图像做的事情——例如,SSDL_RenderImage (mySprite)。这将忽略精灵的其他特征(位置,大小等)。)并且只使用图像方面。

现在你有了这个,你就可以(几乎)制作自己的街机游戏了。

防错法

  • 雪碧没有出现。以下是可能的原因:
    • 图像未加载。你在错误的文件夹中寻找,在文件名中打了一个错别字,或者正在使用一个坏的或不兼容的图像。

    • 它出现了,但不在屏幕上。从SSDL_GetSpriteXSSDL_GetSpriteY得到什么数字?确保他们在射程内。

Exercises

表 11-1

常见的 sprite 命令。完整列表见附录 h。

| `SSDL_Sprite` `mySprite = SSDL_LoadImage ("image.png");` | 这就是如何创建一个。 | | `void``SSDL_SetSpriteLocation` | 设置精灵在屏幕上的位置。 | | `void``SSDL_SetSpriteSize` | …以及它的大小。 | | `void``SSDL_SetSpriteOffset` | …它的偏移量。 | | `void``SSDL_SetSpriteRotation` | …它的旋转角度。 | | `void``SSDL_RenderSprite` | 在当前位置绘制精灵。 | | `int``SSDL_GetSpriteX` | 返回精灵在屏幕上的`x`位置。 | | `int``SSDL_GetSpriteY` | …和它的`y`。 |
  1. 制作一个视频水族馆:一个背景和来回移动的鱼(面向它们去的任何方向,所以你需要SSDL_SpriteFlipHorizontal)。

  2. 做上一节的练习 2 或 3(弹跳球),但是不要画圆圈,而是用一个篮球的图像。让篮球旋转(见表 11-1 了解你需要的功能)。

Footnotes 1

的初始值设定项太灵活了。您可以省略=:

Point2D p1 {0, 5};

你可以省略一些初始化器,它将使用你的默认值(或者 0,如果你没有做任何的话):

Point2D p2 {3}; //x_ becomes 3, y_ stays at 0

如果你的编译器是 C++20 兼容的,你可以放入成员标签:

Point2D p3 = { .x_= 0, .y_=5 };

Point2D p4 = { .y_ = 5 }; //leave x_ to its default

我通常选择简单的版本。

  2

在你的图形编辑器中设置更有效,但是如果需要的话,我想展示如何在 SSDL 中调整大小。

 

十二、制作一个街机游戏:输入,碰撞,并把它们放在一起

在这一章中,我们将制作我们自己的 2-D 街机游戏,把我们到目前为止所获得的浪费时间的体验放在一起,让其他人偷懒,这样我们就可以发光——或者类似的事情。我们需要的新事物是更好的鼠标和键盘交互以及对象的碰撞。

确定输入状态

老鼠

我们已经可以等待鼠标点击,并获得其坐标…但街机游戏不等人。

假设我们想让我们的武器在鼠标按键按下时持续开火。我们需要一种方法来检测按钮被按下,而不需要停下来等待。这就行了。

| `int SSDL_GetMouseClick ();` | 如果没有按钮被按下,返回 0;`SDL_BUTTON_LMASK`(左键被按下)、`SDL_BUTTON_MMASK`(中间)或`SDL_BUTTON_RMASK`(右边)。 |

...如在

if (SSDL_GetMouseClick () != 0) // mouse button is down
{
    x = SSDL_GetMouseX(); y = SSDL_GetMouseY();

    // do whatever you wanted to do if mouse button is down
}

在给出一个例子之前,让我们看看如何检查键盘的状态。

键盘

ssin等待你按回车键。这对于街机游戏来说是行不通的;我们想知道一个键是否一被按下就被按下。函数SSDL_IsKeyPressed告知给定的键是否被按下——任何键,包括与字母无关的键,如 Shift 和 Ctrl:

| `bool SSDL_IsKeyPressed (``SDL_Keycode` | 返回`key`当前是否被按下。 |

尽管这个函数接受的许多键值都与您预期的相符(0 键用'0',A 键用'a'——但 A 键用'A'!),并不总是很明显,所以最好用他们的正式名字。在撰写本文时,完整的列表在wiki.libsdl.org/SDL_Keycode;一些列在表 12-1 中。示例 12-1 展示了如何使用它们。

表 12-1

SDL 的选定键码

| `SDLK_1``SDLK_2``...``SDLK_a``SDLK_b``...` | `SDLK_F1``SDLK_F2``...` | `SDLK_ESCAPE``SDLK_BACKSPACE``SDLK_RETURN``SDLK_SPACE` | `SDLK_LEFT``SDLK_RIGHT``SDLK_UP``SDLK_DOWN` | `SDLK_LSHIFT``SDLK_RSHIFT``SDLK_LCTRL``SDLK_RCTRL` |
// Program to identify some keys, and mouse buttons, being pressed
//       -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

int main (int argc, char** argv)
{
    while (SSDL_IsNextFrame ())
    {
        SSDL_DefaultEventHandler ();

        SSDL_RenderClear ();    // Clear the screen

        SSDL_SetCursor (0, 0);  // And start printing at the top

        sout << "What key are you pressing? ";
        sout << "Control, Shift, Caps lock, space, F1?\n";

        if (SSDL_IsKeyPressed (SDLK_LCTRL))   sout << "Left ctrl ";
        if (SSDL_IsKeyPressed (SDLK_RCTRL))   sout << "Right ctrl ";
        if (SSDL_IsKeyPressed (SDLK_LSHIFT))  sout << "Left shift ";
        if (SSDL_IsKeyPressed (SDLK_RSHIFT))  sout << "Right shift ";
        if (SSDL_IsKeyPressed (SDLK_CAPSLOCK))sout << "Caps lock ";
        if (SSDL_IsKeyPressed (SDLK_SPACE))   sout << "Space bar ";
        if (SSDL_IsKeyPressed (SDLK_F1))      sout << "F1 ";
        if (SSDL_IsKeyPressed (SDLK_ESCAPE))  break;
        sout << "\n";

        if (SSDL_GetMouseClick () == SDL_BUTTON_LMASK)
            sout << "Left mouse button down\n";
        if (SSDL_GetMouseClick () == SDL_BUTTON_RMASK)
            sout << "Right mouse button down\n";

        sout << "(Hit Esc to exit.)";
    }

    return 0;
}

Example 12-1A program to detect control keys, Shift, Caps Lock, space bar, and F1

防错法

  • 你按下了一个功能键,但没有任何反应。在某些键盘上,你还必须按住 Fn 键。

  • **您同时按下多个键,但只有一些键会显示出来;你按下一个键,它不会注册鼠标按钮。**键盘“重影”正在丢失按键,因为键盘一次只能处理这么多。它也可能会失去鼠标点击。在写的时候,如果你在意,可以在 https://keyboardtester.co/ 测试你的键盘鼠标。

将 Ctrl、Shift 和 Alt 与其他键一起使用可能是安全的——这是意料之中的。

事件

有时我们并不关心鼠标按钮当前是向上还是向下,只关心它是否被点击过。也许你每次点击都能从你的 BFG 中得到一个镜头。或者您的程序使用鼠标来打开或关闭声音,如下面的代码所做的那样(或者至少尝试这样做):

while (SSDL_IsNextFrame ())
{
    SSDL_DefaultEventHandler ();

    if (SSDL_GetMouseClick ()) toggleSound (); // not gonna work

    ...
}

问题是,电脑很快。假设你的鼠标点击需要十分之一秒。以每秒 60 帧的速度,在你松开按钮之前,声音会打开和关闭六次,如果它以你想要的方式结束,那才是好运。

我们需要的是将点击本身检测为鼠标点击事件,就像第十一章中处理的退出事件。我们将有一个替代者SSDL_DefaultEventHandler来检测该事件并将其报告给main,这样main就可以切换音乐(例如 12-2;截图如图 12-1 。

void myEventHandler (bool& mouseClicked) // replaces SSDL_DefaultEventHandler
{
    SDL_Event event;
    mouseClicked = false;   // We'll soon know if mouse was clicked

    while (SSDL_PollEvent (event))
        switch (event.type)
        {
        case SDL_QUIT:            SSDL_DeclareQuit();
            break;
        case SDL_KEYDOWN:         if (SSDL_IsKeyPressed (SDLK_ESCAPE))
                                      SSDL_DeclareQuit();
            break;
        case SDL_MOUSEBUTTONDOWN: mouseClicked = true; // It was!
            break;
        }
}

// and the following in main:

    while (SSDL_IsNextFrame ())
    {
        bool mouseWasClicked;
        myEventHandler (mouseWasClicked);

        if (mouseWasClicked) toggleSound ();

        ....
    }

Example 12-2Making your own event handler. For the complete program, see source code, ch12 folder; the project is 2-aliensBuzzOurWorld

img/477913_2_En_12_Fig1_HTML.jpg

图 12-1

一个用鼠标点击来切换声音的程序

这种思维方式——事件驱动——是 Windows、iOS 和 Android 等重要操作系统编程的核心。

冷却时间和寿命

假设我们想要一个效果在某件事情发生后持续一会儿。也许有一种视觉效果在鼠标点击创造出来后只存在一秒钟(它的“寿命”)。或者你的 BFG 需要等待一段时间才能再次开火,无论你点击得多疯狂——这是一个“冷却”期。

我们将有一个整数framesLeftTillItsOver,当鼠标被点击时,它将被设置为你想要延迟的帧数。

这个推理不行:

framesLeftTillItsOver = 0      //effect is currently inactive
while SSDL_IsNextFrame ()
    handle events

    SSDL_RenderClear ()
    draw things

    if framesLeftTillItsOver == 0 && mouseWasClicked
        framesLeftTillItsOver = HOWEVER MANY FRAMES WE WANT IT TO LAST
        draw the visual effect

    --framesLeftTillItsOver // 1 frame closer to disappearance

倒计时是可以的,但是当你绘制效果时,SSDL_RenderClear会在下一次迭代中删除它!

我们必须将改变视觉效果的状态(从关到开)的与绘制它的区分开来;它们是独立的行动。这将起作用:

framesLeftTillItsOver = 0      //effect is currently inactive
while SSDL_IsNextFrame ()
    handle events

    SSDL_RenderClear ()
    draw things including, if it's on, the visual effect
      (consider it to be on if framesLeftTillItsOver > 0)

    if effect is on (that is, framesLeftTillItsOver > 0)
        -- framesLeftTillItsOver; // 1 frame closer to disappearance
    else if mouseWasClicked
        framesLeftTillItsOver = HOWEVER MANY FRAMES WE WANT IT TO LAST

Tip

根据经验,在主动画循环中,有三个独立的部分:处理事件、绘图和更新变量。顺序并不重要,因为它们最终都会完成;重要的是,不要在更新部分绘制,不要在绘制部分检查事件,等等。

在示例 12-3 中,当你点击鼠标时,程序会在你点击的任何地方放置一个飞溅的图像。一秒钟后,它会删除图像,让您再次点击。

// Program that makes a splat wherever you click

//      -- from _C++20 for Lazy Programmers_

#include "SSDL.h"

void myEventHandler (bool& mouseClicked);

int main (int argc, char** argv)
{
    SSDL_SetWindowTitle ("Click the mouse to see and hear a splat; "
                         "hit Esc to end.");

    const SSDL_Sound SPLAT_SOUND =
        SSDL_LoadWAV ("media/445117__breviceps__cartoon-splat.wav");

    // Set up sprite with image and a size, and offset its reference
    //  point so it'll be centered on our mouse clicks
    SSDL_Sprite splatSprite = SSDL_LoadImage ("media/splat.png");

    constexpr int SPLAT_WIDTH=50, SPLAT_HEIGHT=50;
    SSDL_SetSpriteSize  (splatSprite, SPLAT_WIDTH,   SPLAT_HEIGHT);
    SSDL_SetSpriteOffset(splatSprite, SPLAT_WIDTH/2, SPLAT_HEIGHT/2);

    while (SSDL_IsNextFrame ())
    {
        static int framesLeftTillSplatDisappears =  0;
        static constexpr int SPLAT_LIFETIME      = 60;  // It lasts one //    second

        // Handle events
        bool isMouseClick;
        myEventHandler (isMouseClick);

        // Display things
        SSDL_RenderClear();
        if (framesLeftTillSplatDisappears > 0)
            SSDL_RenderSprite(splatSprite);

        // Update things: process clicks and framesLeft
        if (framesLeftTillSplatDisappears > 0) // if splat is active
            --framesLeftTillSplatDisappears;   //  keep counting down

        else if (isMouseClick)      // if not, and we have a click...
        {
                                    // Reset that waiting time
            framesLeftTillSplatDisappears = SPLAT_LIFETIME;

                                    // Play splat sound
            SSDL_PlaySound (SPLAT_SOUND);

            SSDL_SetSpriteLocation  // move splat sprite to
                (splatSprite,       //  location of mouse click

                 SSDL_GetMouseX(),
                 SSDL_GetMouseY());
        }
    }

    return 0;
}

void myEventHandler (bool& mouseClicked)
{
   // exactly the same as in Example 12-2
}

Example 12-3Using a visual effect with specified duration: splatter on the screen

Exercises

  1. 修改示例 12-3 中的程序,以允许多个飞溅同时存在;你可以每秒发射一次,但每次飞溅持续 5 秒。

  2. 一个粒子喷泉是一组不断产生的粒子,向各个方向运动。这就是电脑游戏和暴雨中火焰产生的原因。你也可以做一个泡泡喷泉或烟火。

    让每个粒子从相同的位置开始,有一个初始随机速度。随着主循环的每次迭代,用SSDL_RenderDrawPoint绘制粒子。同样更新它的位置,如果你想让它看起来更像火焰,使用重力,但是方向相反;火焰粒子倾向于向上飞而不是向下飞。最后,当一个粒子已经存在了一定数量的帧,重置它到它的起点,让它再次去。

碰撞

在制作我们自己的游戏之前,我们还需要一样东西:碰撞。

用 SSDL 精灵检测碰撞很容易:

| `int` `SSDL_SpriteHasIntersection``(const SSDL_Sprite& a, const SSDL_Sprite& b);` | 返回子画面`a`和`b`是否重叠。 |

就像:

if (SSDL_SpriteHasIntersection (robotSprite, playerSprite))
    playerDead = true;

精灵的碰撞很容易,但并不总是准确的。因为你的精灵可能有很大一部分是透明的,你可能会发现 SDL 认为两个精灵在碰撞,即使可见的部分没有接触。没有一个理智的人会认为图 12-2 (a)中的糖果和万圣节篮子是冲突的——但 SSDL 会。

img/477913_2_En_12_Fig2_HTML.png

图 12-2

精灵之间的碰撞,由SSDL_SpriteHasIntersection (a)和函数inCollision (b)的基于圆的方法评估

这里有一个简单的解决方法:找出两点(可能是精灵的中心)之间的距离,如果该距离小于我们给物体的半径之和aSizebSize——也就是说,如果边界圆相交(图 12-2 (b)),就认为发生了碰撞:

bool inCollision (Point2D A, Point2D B, int aSize, int bSize)
{
    float aToBDistance = distance (A.x_, A.y_, B.x_, B.y_);
                             // see Chapter 7's first set of exercises --
                             //    or Example 12-7 below --
                             //    for the distance function
    return (aToBDistance < aSize + bSize);
}

使用最适合你的精灵的方法。

大游戏

本章的其余部分是关于创建街机游戏:首先是我的,然后是你的。

我这里有一个在篮子里抓万圣节糖果的游戏(例子 12-4 到 12-8 )。如果你抓到足够多,你就赢了;错过太多,你就会死。它有声音、背景、愚蠢的图形和键盘交互(使用箭头键),为了显示鼠标的使用,我允许用户切换平视显示器(HUD ),显示失误和失误的统计数据。为了说明如何指定效果的生存期,一个浮动的“Yum!”当你抓到糖果时,信息会出现一会儿。输出如图 12-3 所示。

// Program to catch falling Hallowe'en candy

//       -- from _C++20 for Lazy Programmers_

#include <cmath> // for sqrt
#include "SSDL.h"

// dimensions of screen and screen locations
constexpr int SCREEN_WIDTH=675, SCREEN_HEIGHT=522; // dimensions of bkgd

constexpr int CANDY_START_HEIGHT =  15;  // where candy falls from

constexpr int MARGIN             =  25;  // As close to the left/right 
                                         //  edges of the screen as moving //  objects are allowed to get

constexpr int BOTTOM_LINE        = 480; // Where last line of text is printed
                                        //  on instruction & splash screens

// dimensions of important objects
constexpr int CANDY_WIDTH  = 60, CANDY_HEIGHT  = 20;
constexpr int BASKET_WIDTH = 70, BASKET_HEIGHT = 90;

// how many candies you can catch or miss before winning/losing
constexpr int MAX_CAUGHT   = 10, MAX_MISSED    = 10;
                                 // If you change this, change
                                 //  printInstructions too
                                 //  because it specifies this

// fonts for splash screens and catch/miss statistics
constexpr int SMALL_FONT_SIZE  = 12,
              MEDIUM_FONT_SIZE = 24,
              LARGE_FONT_SIZE  = 36;

const SSDL_Font SMALL_FONT
    = SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
                     SMALL_FONT_SIZE);
const SSDL_Font MEDIUM_FONT

    = SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
                     MEDIUM_FONT_SIZE);
const SSDL_Font LARGE_FONT
    = SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
                     LARGE_FONT_SIZE);

// how far our victory/defeat messages are from left side of screen
constexpr int FINAL_SCREEN_MESSAGE_OFFSET_X = 40;

// background
const SSDL_Image BKGD_IMAGE
    = SSDL_LoadImage("media/haunted-house.jpg");

// sounds and music
const SSDL_Music BKGD_MUSIC
    = SSDL_LoadMUS("media/159509__mistersherlock__halloween-graveyd-short.mp3");
const SSDL_Sound THUNK_SOUND
    = SSDL_LoadWAV("media/457741__osiruswaltz__wall-bump-1.wav");
const SSDL_Sound DROP_SOUND
    = SSDL_LoadWAV("media/388284__matypresidente__water-drop-short.wav");

// structs
struct Point2D { int x_ = 0, y_ = 0; };

using Vector2D = Point2D;1

struct Object
{
    Point2D     loc_;
    int         rotation_      = 0;

    Vector2D    velocity_;
    int         rotationSpeed_ = 0;

    SSDL_Sprite sprite_;
};

// major functions called by the main program

bool playGame            ();

// startup/ending screens to communicate with user
void printInstructions   ();
void displayVictoryScreen();
void displayDefeatScreen ();

int main (int argc, char** argv)
{
    // set up window and font
    SSDL_SetWindowTitle ("Catch the falling candy");
    SSDL_SetWindowSize  (SCREEN_WIDTH, SCREEN_HEIGHT);

    // prepare music
    SSDL_VolumeMusic (int (MIX_MAX_VOLUME * 0.1));
    SSDL_PlayMusic   (BKGD_MUSIC);

    // initial splash screen
    printInstructions ();

    // The game itself
    bool isVictory = playGame ();

    // final screen:  victory or defeat

    SSDL_RenderClear (BLACK);
    SSDL_HaltMusic   ();

    if (isVictory) displayVictoryScreen ();
    else           displayDefeatScreen  ();

    SSDL_RenderTextCentered("Click mouse to end",
                            SCREEN_WIDTH/2, BOTTOM_LINE, SMALL_FONT);
    SSDL_WaitMouse();  // because if we wait for a key, we're likely
                       //  to have left or right arrow depressed
                       //  when we reach this line... and we'll never
                       //  get to read the final message

    return 0;
}

//// Startup/ending screens to communicate with user ////

void printInstructions ()
{
    constexpr int LINE_HEIGHT = 40;

    SSDL_SetRenderDrawColor (WHITE);
    SSDL_RenderTextCentered ("Catch 10 treats in ",
              SCREEN_WIDTH/2,              0, MEDIUM_FONT);
    SSDL_RenderTextCentered ("your basket to win",
              SCREEN_WIDTH/2, LINE_HEIGHT   , MEDIUM_FONT);
    SSDL_RenderTextCentered ("Miss 10 treats and",
              SCREEN_WIDTH/2, LINE_HEIGHT*3 , MEDIUM_FONT);
    SSDL_RenderTextCentered ("the next treat is YOU",
              SCREEN_WIDTH/2, LINE_HEIGHT*4 , MEDIUM_FONT);

    SSDL_RenderTextCentered ("Use arrow keys to move",
              SCREEN_WIDTH/2, LINE_HEIGHT*6 , MEDIUM_FONT);
    SSDL_RenderTextCentered ("left and right",
              SCREEN_WIDTH/2, LINE_HEIGHT*7 , MEDIUM_FONT);

    SSDL_RenderTextCentered ("Click mouse to",
              SCREEN_WIDTH/2, LINE_HEIGHT*9 , MEDIUM_FONT);
    SSDL_RenderTextCentered ("toggle stats display",
              SCREEN_WIDTH/2, LINE_HEIGHT*10, MEDIUM_FONT);

    SSDL_RenderTextCentered ("Hit any key to continue",
              SCREEN_WIDTH/2, BOTTOM_LINE,    SMALL_FONT);

    SSDL_WaitKey      ();
}

void displayVictoryScreen ()
{
    // sound and picture
    static const SSDL_Sound VICTORY_SOUND
        = SSDL_LoadWAV ("media/342153__robcro6010__circus-theme-short.wav");
    SSDL_PlaySound (VICTORY_SOUND);

    static const SSDL_Image GOOD_PUMPKIN
        = SSDL_LoadImage ("media/goodPumpkin.png");
    SSDL_RenderImage(GOOD_PUMPKIN, SCREEN_WIDTH / 4, 0);

    // victory message

    SSDL_SetRenderDrawColor (WHITE);
    SSDL_RenderText ("Hooah!",
                     FINAL_SCREEN_MESSAGE_OFFSET_X, SCREEN_HEIGHT/4,
                     LARGE_FONT);
    constexpr int LINE_DISTANCE_Y = 96; // an arbitrarily chosen number...
    SSDL_RenderText ("You won!",
                     FINAL_SCREEN_MESSAGE_OFFSET_X,
                     SCREEN_HEIGHT/4+LINE_DISTANCE_Y,
                     LARGE_FONT);
}

void displayDefeatScreen ()
{
    // sound and picture
    static const SSDL_Sound DEFEAT_SOUND
        = SSDL_LoadWAV ("media/326813__mrose6__echoed-screams-short.wav");
    SSDL_PlaySound (DEFEAT_SOUND);

    static const SSDL_Image SAD_PUMPKIN
        = SSDL_LoadImage ("media/sadPumpkin.png");
    SSDL_RenderImage (SAD_PUMPKIN, SCREEN_WIDTH / 4, 0);

    // defeat message
    SSDL_SetRenderDrawColor (WHITE);
    SSDL_RenderText ("Oh, no!", FINAL_SCREEN_MESSAGE_OFFSET_X,
                     SCREEN_HEIGHT/4, LARGE_FONT);
}

Example 12-4Falling candy program, part one of five. The complete program in source code’s ch12 folder is 4-thru-8-bigGame

到目前为止,我们已经有了这个项目的大致轮廓。我在Object struct中放了很多信息:位置、速度、精灵信息和旋转。有些并不总是需要的——例如,只有糖果会旋转——但是只有一种类型的Object会让事情变得更简单。

///////////////////// Initializing /////////////////////////

void resetCandyPosition(Object& candy);

void initializeObjects (Object& basket, Object& candy, Object& yumMessage)
{
    // load those images
    SSDL_SetSpriteImage (candy.sprite_,
                        SSDL_LoadImage ("media/candy.png"));
    SSDL_SetSpriteImage (basket.sprite_,
                        SSDL_LoadImage ("media/jack-o-lantern.png"));
    SSDL_SetSpriteImage (yumMessage.sprite_,
                        SSDL_LoadImage ("media/yum.png"));

    // two images are the wrong size; we resize them.
    SSDL_SetSpriteSize (candy.sprite_,   CANDY_WIDTH,  CANDY_HEIGHT);
    SSDL_SetSpriteSize (basket.sprite_, BASKET_WIDTH, BASKET_HEIGHT);

    // move 'em so they're centered on the coords we set for them
    SSDL_SetSpriteOffset (candy.sprite_,
                          CANDY_WIDTH/2,  CANDY_HEIGHT/2);
    SSDL_SetSpriteOffset (basket.sprite_,
                          BASKET_WIDTH/2, BASKET_HEIGHT/2);

    // put the objects in their starting positions
    basket.loc_.x_ = SCREEN_WIDTH / 2;
    basket.loc_.y_ = SCREEN_HEIGHT - BASKET_HEIGHT/2;
    SSDL_SetSpriteLocation (basket.sprite_,
                            basket.loc_.x_, basket.loc_.y_);
    resetCandyPosition (candy);

    // (We don't care about yumMessage position till we make one)

    // And set velocities
    // basket's can't be specified till we check inputs
    constexpr int CANDY_SPEED = 11;      //11 pixels per frame, straight down
    candy.velocity_.y_ = CANDY_SPEED;    //11 per frame straight down
                                         //Increase speeds for faster game
    yumMessage.velocity_ = { 1, -1 };    //Up and to the right

    // And rotational speeds
    candy.rotationSpeed_ = 1;            //Candy spins slightly
}

/////////////////////////// Drawing /////////////////////////////

//Display all 3 objects (2 if yumMessage is currently not visible)
void renderObjects (Object basket, Object candy, Object yumMessage,
                    bool showYumMessage)
{
    SSDL_RenderSprite (basket.sprite_);
    SSDL_RenderSprite ( candy.sprite_);
    if (showYumMessage) SSDL_RenderSprite (yumMessage.sprite_);
}

void renderStats(int Caught, int Missed)
{
    // Stats boxes, for reporting how many candies caught and missed
    SSDL_SetRenderDrawColor(BLACK);
    constexpr int BOX_WIDTH = 90, BOX_HEIGHT = 25;
    SSDL_RenderFillRect(0, 0,                        // Left box
        BOX_WIDTH, BOX_HEIGHT);
    SSDL_RenderFillRect(SCREEN_WIDTH - BOX_WIDTH, 0, // Right box
        SCREEN_WIDTH - 1, BOX_HEIGHT);

    // Statistics themselves
    SSDL_SetRenderDrawColor(WHITE);
    SSDL_SetFont           (SMALL_FONT);

    SSDL_SetCursor(0, 0);                            // Left box
    sout << "Caught: " << Caught;

    SSDL_SetCursor(SCREEN_WIDTH - BOX_WIDTH, 0);     // Right box
    sout << "Missed: " << Missed;
}

Example 12-5Falling candy program, part two of five

先前在示例 12-5 中声明并在示例 12-6 中定义的resetCandyPosition,以随机的 X 位置开始屏幕顶部的糖果。它在initializeObjects中被调用,在handleCatchingCandyhandleMissingCandy中再次被调用。

在两个黑盒上打印你抓到或错过的棋子数量,以便于阅读。

//////////////// Moving objects in the world ///////////////////

void resetCandyPosition (Object& candy)    // When it's time to drop
                                           //  another candy...
{
                                           // Put it at a random X location
    candy.loc_.x_ = MARGIN + rand() % (SCREEN_WIDTH - MARGIN);
    candy.loc_.y_ = CANDY_START_HEIGHT;    // at the top of the screen

    SSDL_SetSpriteLocation (candy.sprite_, candy.loc_.x_, candy.loc_.y_);
}

void moveObject (Object& object)
{
    object.loc_.x_ += object.velocity_.x_; // Every frame, move object
    object.loc_.y_ += object.velocity_.y_; //   as specified
    SSDL_SetSpriteLocation (object.sprite_, object.loc_.x_, object.loc_.y_);

                                           // ...and spin as specified
    object.rotation_ += object.rotationSpeed_;
    object.rotation_ %= 360;           // angle shouldn't go over 360
                                       // (unlike sin and cos, SDL/SSDL
                                       // functions use angles in degrees)

    SSDL_SetSpriteRotation (object.sprite_, object.rotation_);
}

void moveBasket (Object& basket, int basketSpeed)
{
    // Let user move basket with left and right arrows
    if (SSDL_IsKeyPressed (SDLK_LEFT )) basket.loc_.x_ -= basketSpeed;
    if (SSDL_IsKeyPressed (SDLK_RIGHT)) basket.loc_.x_ += basketSpeed;

    // ...but don't let the basket touch the sides of the screen
    if (basket.loc_.x_ < MARGIN)
        basket.loc_.x_ = MARGIN;
    if (basket.loc_.x_ > SCREEN_WIDTH - MARGIN)
        basket.loc_.x_ = SCREEN_WIDTH - MARGIN;

    // Tell the sprite about our changes on X
    SSDL_SetSpriteLocation (basket.sprite_,
                            basket.loc_.x_, basket.loc_.y_);
}

Example 12-6Falling candy program, part three of five

moveObject叫上了糖果和百胜!消息。球员控制篮筐,所以需要它自己的moveBasket功能。moveBasketSSDL_IsKeyPressed检查左右箭头的状态,并相应地移动篮子,用MARGIN确保篮子不会离开屏幕。

////////What happens when a candy is caught or missed ////////

// Some math functions we need a lot...
int sqr (int num) { return num * num; }

double distance (Point2D a, Point2D b)
{
    return sqrt(sqr(b.x_ - a.x_) + sqr(b.y_ - a.y_));
}

// Circular collision detection, better for round-ish objects
bool inCollision (Point2D a, Point2D b, int aSize, int bSize)
{
    return (distance(a, b) < aSize/2 + bSize/2);
}

// Detect and handle collisions between basket and candy,
//  and update numberCaught

bool handleCatchingCandy (Object basket, Object& candy, Object& yumMessage,
                          int& numberCaught)
{
    if (inCollision (basket.loc_, candy.loc_, CANDY_WIDTH, BASKET_WIDTH))
    {
        SSDL_PlaySound (THUNK_SOUND);

        ++numberCaught;

        resetCandyPosition (candy);

        yumMessage.loc_.x_    = basket.loc_.x_;
        yumMessage.loc_.y_    = basket.loc_.y_;

        return true;
    }
    else return false;
}

// Detect and handle when candy goes off bottom of screen,
//  and update numberMissed
void handleMissingCandy (Object& candy, int& numberMissed)
{
                                 // you missed it: it went off screen
    if (candy.loc_.y_ >= SCREEN_HEIGHT)
    {
        SSDL_PlaySound (DROP_SOUND);

        ++numberMissed;

        resetCandyPosition (candy);
    }
}

Example 12-7Falling candy program, part four of five

如果篮子和糖果发生碰撞,handleCatchingCandy会将糖果重置到屏幕的顶部,定位好吃的!消息,并返回 true,这样main将知道开始倒计时framesLeftTillYumDisappears,保持 Yum!看得见一秒钟。

如果糖果落在屏幕底部——如果错过了—handleMissingCandy也会将糖果重置到屏幕顶部。

无论哪种方式,统计数据都会得到适当的更新。

///////////////////// Events /////////////////////

void myEventHandler(bool& mouseClicked)
{
    SSDL_Event event;

    while (SSDL_PollEvent(event))
        switch (event.type)
        {
        case SDL_QUIT:            SSDL_DeclareQuit(); break;
        case SDL_KEYDOWN:         if (SSDL_IsKeyPressed(SDLK_ESCAPE))
                                      SSDL_DeclareQuit();
                                  break;
        case SDL_MOUSEBUTTONDOWN: mouseClicked = true;
        }
}

///// ** The game itself ** ////

bool playGame ()
{
    bool isVictory          = false;      // Did we win?  Not yet
    bool isDefeat           = false;      // Did we lose? Not yet
    bool letsDisplayStats   = true;       // Do we show stats on screen?
                                          //   Yes, for now

    int numberCaught = 0,                 // So far no candies
        numberMissed = 0;                 //   caught or missed

     // Initialize sprites
    Object basket, candy, yumMessage;
    initializeObjects (basket, candy, yumMessage);

    // Main game loop
    while (SSDL_IsNextFrame () && ! isVictory && ! isDefeat)
    {
        constexpr int FRAMES_FOR_YUM_MESSAGE = 60;
        static int framesLeftTillYumDisappears = 0;

        // Handle input events
        bool mouseClick = false; myEventHandler (mouseClick);
        if (mouseClick) letsDisplayStats = ! letsDisplayStats;

        // Display the scene

        SSDL_RenderImage(BKGD_IMAGE, 0, 0);
        renderObjects   (basket, candy, yumMessage,
                         framesLeftTillYumDisappears>0);
        if (letsDisplayStats) renderStats (numberCaught, numberMissed);

        // Updates:

        // Move objects in the scene
        constexpr int BASKET_SPEED = 7;    //7 pixels per frame, left or right
        moveBasket(basket, BASKET_SPEED);
        moveObject(candy); moveObject(yumMessage);

                                           // Did you catch a candy?
        if (handleCatchingCandy(basket, candy, yumMessage, numberCaught))
            framesLeftTillYumDisappears = FRAMES_FOR_YUM_MESSAGE;

        if (numberCaught >= MAX_CAUGHT)
            isVictory = true;
        else                               //   ...or did it go off screen?
        {
            handleMissingCandy (candy, numberMissed);
            if (numberMissed >= MAX_MISSED)
                isDefeat = true;           // You just lost!
        }

        // Update yum message
        if (framesLeftTillYumDisappears > 0)  // if yumMessage is active
            --framesLeftTillYumDisappears;    //   keep counting down
    }

    return isVictory;
}

Example 12-8Falling candy program, part five of five

当我们获得胜利或失败时,主循环停止。它分为活动部分、显示部分和更新部分。在事件部分,该语句

if (mouseClick) letsDisplayStats = ! letsDisplayStats;

切换是否显示统计信息。

处理好吃的!消息被适当地分发:它就像其他对象一样显示,但是它的生命周期在 update 部分持续递减。

img/477913_2_En_12_Fig3_HTML.jpg

图 12-3

万圣节糖果游戏

对于更容易或更难的游戏,在主游戏循环之前调用SSDL_SetFramesPerSecond,或者调整BASKET_SPEEDCANDY_SPEED

防错法

  • You’ve got a feature that’s supposed to display for a while but never shows up, something like the splat from earlier in this chapter. Maybe your code looks like this:

    while (SSDL_IsNextFrame ())
    {
       ...
       if (mouseClick)
       {
           framesLeftTillSplatDisappears = SPLAT_LIFETIME;
           while (framesLeftTillSplatDisappears > 0)
           {
    
              // display the splat
              --framesLeftTillSplatDisappears;
           }
        }
        ...
    }
    
    

    它显示,好的——一次又一次,直到framesLeftTillSplatDisappears达到 0——这一切都发生在六十分之一秒内!它显示得太快了,你根本没机会看到它。

    问题是适应这种新的事件驱动的思维方式。我们不希望程序完成所有的 splat 显示,然后继续下一个六十分之一秒;我们希望它设置显示,并在其他事情发生时让帧通过(包括用户看到 splat)。

    一个好的经验法则是避免在主动画循环中循环一个应该花费时间的动作。只需设置它,并让主循环用连续的帧更新它。

    另一个好的规则是来自设计 splat 程序部分的提示;保持主循环的各个部分独立:处理事件,然后显示,然后更新。

    So a way to fix this program is

    while (SSDL_IsNextFrame ())
    {
       // Events section
       ...
       if (mouseClick)
           framesLeftTillSplatDisappears = SPLAT_LIFETIME;
       ...
    
       // Display section
       //   display the splat
       ...
    
        // Update section
        if (framesLeftTillSplatDisappears > 0)
            --framesLeftTillSplatDisappears;
        ...
    }
    
    
  • **你不能让一个新功能工作。**尝试一个能做的程序(一个来自文本或互联网或你之前做的东西的样本程序,甚至是一个带有空main的程序),然后逐渐改变,直到它成为新程序。

  • 您刚刚添加了一个新功能,但现在什么都没用了。以下是一些建议:

    • 如第一章所述,保留一份备份记录,当你修改时,你整个文件夹的副本,这样如果出错了,你可以回到以前的版本。比拔头发好玩多了。

    • 从一个行为不端的函数中删除所有代码。然后放一半回去。如果不好的行为又回来了,只放四分之一回去;如果没有,请重新添加更多代码。继续下去,直到你找到问题所在。

    • **试验毁灭。**如果我完全想不出哪里出了问题,我会复制一个文件夹(可能命名为“ttd”),做一个备份,然后删除代码,尤其是我认为与错误无关的代码。错误还在吗?如果没有,我已经找到了问题所在!但如果是这样,重复,仍然备份,直到程序如此之短,除了错误什么都没有了。不管怎样,我都在追踪引起麻烦的代码。

      有时我会找到两到三行代码,然后确定问题出在编译器上。它发生了。(那我换个方式写程序。)如果结果是一些愚蠢的事情,我很高兴完成了,而不是恨我自己犯了一个愚蠢的错误。

    • 识别版本之间的差异。也许其中一个有你想要的特性,但有你不想要的缺陷。一份精确的差异报告可以帮助你缩小你感兴趣的范围。在 Unix 中,diff file1 file2列出了不同的行。在 Windows 系统中, WinDiff 是微软的一个很棒的程序(你可能已经有了),它也能做到这一点。两者都适用于单个文件或整个文件夹。

    • **把问题讲给一个真正能听的人:一只鸭子。**也许如果你向专家解释你的问题,它会变得清晰。当然,但是如果身边没有专家呢?认真地和一只橡皮鸭交谈。详细说明问题。当你这样做时,你可能会找到你的解决办法。橡皮鸭调试是个东西,目前甚至有自己的网站( rubberduckdebugging.com )。

如需更多提示,请查阅第九章末尾的反欺诈部分。

Exercises

在这些练习和后续练习中,请提前计划,并在出现问题时使用调试器。

  1. 让这些球弹跳起来,就像第十一章一样…让你的鼠标控制屏幕上的一个小玩家。避开弹跳球。

  2. 制作一个太空游戏:一个不明飞行物从头顶飞过,在你还击的同时投下导弹。你需要一排导弹。

  3. 制作一个游乐场鸭子射击游戏的版本。为了让它更有趣,你可以用慢速导弹(从你的准星直接射向鸭子,但是要花一秒钟到达那里)。

  4. 做一把可以旋转的枪,放在屏幕中间,射杀从随机方向过来的坏人。

  5. 制作自己的游戏,可以是现有街机游戏的副本,也可以是自己的创意。

Footnotes 1

使用 newTypeName = existingType 使 newTypeName 成为 existingType 的别名。根据需要使用以保持清晰。

你可能还会看到这样的旧样式:typedef existing type new typename;。