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

第9章 C++引用、精灵表和顶点数组

# 第9章 C++引用、精灵表和顶点数组

在第4章“循环、数组、switch语句、枚举和函数——实现游戏机制”中,我们讨论了作用域的概念。即,在函数或代码内部块中声明的变量,其作用域(即可以被看到或使用的范围)仅限于该函数或代码块。仅凭借我们目前掌握的C++知识,可能会引发一个问题:如果我们需要处理一些在主函数中使用的复杂对象,该怎么办呢?这可能意味着所有代码都得写在主函数里。

在本章中,我们将探究C++引用。借助它,我们能够处理那些原本超出作用域的变量和对象。不仅如此,引用还能避免在函数之间传递大型对象,因为传递大型对象的过程比较耗时。之所以耗时,是因为每次传递时都必须复制变量或对象。

掌握了引用的新知识后,我们将学习SFML库中的VertexArray类。借助这个类,我们能够构建一个大型图像,它可以将单个图像文件中的多个部分,快速高效地绘制到屏幕上。在本章结束时,我们将利用引用和VertexArray对象,创建出一个可缩放、随机生成且能滚动的背景。

在本章中,我们将讨论以下内容:

  • 理解C++引用
  • SFML顶点数组和精灵表
  • 创建随机生成的滚动背景
  • 使用背景

# 理解C++引用

当我们向函数传递值或从函数返回值时,实际做的就是按值传递/返回。具体过程是,先复制变量所存储的值,然后将复制的值传递给函数使用。

这种方式有两个明显的问题:

  1. 如果我们希望函数对变量进行永久性修改,这种机制就派不上用场了。
  2. 在复制值作为参数传递给函数或从函数返回值时,会消耗处理能力和内存。对于简单的int类型,甚至像Sprite(精灵)这样的类型,这种消耗可能微不足道。然而,对于复杂对象,比如整个游戏世界(或背景),复制过程会严重影响游戏性能。

引用就是解决这两个问题的办法。引用是一种特殊类型的变量,它指向另一个变量。下面的示例能帮助你更好地理解:

int numZombies = 100;
int& rNumZombies = numZombies;
1
2

在上述代码中,我们声明并初始化了一个普通的int类型变量numZombies。接着,声明并初始化了一个int类型引用rNumZombies。类型后面的引用运算符&,表明正在声明一个引用。

引用变量名前面的r前缀是可选的,但它有助于我们记住正在处理的是引用。

现在,我们有了一个存储值为100的int类型变量numZombies,以及一个指向numZombies的int类型引用rNumZombies。

对numZombies所做的任何操作,都能通过rNumZombies体现出来;反之,对rNumZombies所做的任何操作,实际上都是在对numZombies进行操作。看看下面这段代码:

int score = 10;
int& rScore = score;
score++;
rScore++;
1
2
3
4

在这段代码中,我们先声明了一个int类型变量score,接着声明了一个指向score的int类型引用rScore。记住,对score所做的任何操作,rScore都能感知到;对rScore所做的操作,实际上就是在对score进行操作。

所以,当我们像这样对score进行自增操作时:

score++;
1

此时,score变量存储的值变为11。此外,如果输出rScore,输出结果也会是11。接下来的代码是:

rScore++;
1

现在,score实际存储的值变为12,因为对rScore所做的操作,就是在对score进行操作。

如果你想知道这背后的原理,下一章讨论指针时会详细说明。简单来说,可以把引用看作存储在计算机内存中的一个位置/地址,而这个内存位置和它所指向的变量存储值的位置是相同的。因此,对引用或变量进行操作,效果完全一样。

目前,更重要的是了解使用引用的原因。使用引用主要有两个原因,前面我们已经提到过,这里再总结一下:

  1. 在另一个函数中更改/读取变量/对象的值,而这个变量/对象在原本的作用域之外。
  2. 向函数传递参数或从函数返回值时,无需进行复制(因此效率更高)。

研究下面这段代码,之后我们再进行讨论:

void add(int n1, int n2, int a);
void referenceAdd(int n1, int n2, int& a);

int main()
{
    int number1 = 2;
    int number2 = 2;
    int answer = 0;

    add(number1, number2, answer);
    //  answer  equals  zero  because  it  is passed  as  a  copy
    // Nothing  happens  to  answer  in  the  scope  of main

    referenceAdd(number1, number2, answer);
    // Now  answer  is 4  because  it  was passed  by  reference
    // When  the  referenceAdd function  did  this:
    //  answer  =  num1  +  num  2;
    //  It  is  actually  changing  the  value  stored  by  answer

    return 0;
}

// Here  are  the  two function  definitions
//  They  are  exactly  the  same  except  that
//  the  second passes  a  reference  to  a
void add(int n1, int n2, int a)
{
    a = n1 + n2;
    //  a  now  equals 4
    //  But  when  the function  returns  a  is  lost forever
}

void referenceAdd(int n1, int n2, int& a)
{
    a = n1 + n2;
    //  a  now  equals 4
    //  But  a  is  a  reference!
    // So,  it  is  answer,  back  in main,  that  equals 4
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这段代码开头是两个函数add和referenceAdd的声明。add函数接受三个int类型变量,referenceAdd函数则接受两个int类型变量和一个int类型引用。

调用add函数并传入number1、number2和answer变量时,会复制这些变量的值,并在add函数内部操作新的局部变量(即n1、n2和a)。结果,在主函数中的answer变量的值仍然为0。

调用referenceAdd函数时,number1和number2依旧按值传递,但answer是按引用传递。当n1与n2相加的值赋给引用a时,实际上是将这个值赋给了主函数中的answer变量。

对于这么简单的场景,我们可能确实永远都不需要使用引用。不过,这段代码展示了按引用传递的机制。

现在,我们来总结一下关于引用的知识。

# 总结引用

前面的代码展示了如何通过引用,使用另一个作用域中的代码来修改变量的值。按引用传递不仅非常方便,而且效率极高,因为无需进行复制操作。我们使用int类型引用的示例可能有点不太直观,因为int类型的数据量很小,使用引用并没有明显的效率提升。在本章后面的内容中,我们将使用引用来传递整个关卡布局,那时效率提升就会很显著。

使用引用有一个需要注意的地方!在创建引用时,必须将其指向一个变量。这意味着引用的使用并非完全灵活。目前先不用为此担心,下一章我们将进一步探究引用,以及和它相关但更灵活(也稍微复杂一些)的概念,比如指针。

这对于int类型变量来说影响不大,但对于大型类对象而言,可能就很重要了。在本章后面实现“僵尸竞技场”游戏的滚动背景时,我们就会用到这种方法。

接下来,我们将学习顶点数组和精灵表的知识。

# SFML顶点数组和精灵表

我们差不多准备好实现滚动背景了,现在只需学习SFML顶点数组和精灵表的相关知识。

# 什么是精灵表?

精灵表是指将一组图像,无论是动画帧还是独立图形,都包含在一个图像文件中。仔细看看这张精灵表,它包含四个独立的图像,我们将在“僵尸竞技场”游戏中用它们来绘制背景: img 图9.1:精灵表

SFML允许我们像加载本书中其他纹理一样,将精灵表作为常规纹理加载。当我们把多个图像作为单个纹理加载时,GPU(图形处理单元)能更高效地处理。

现代PC即便不使用精灵表,也能处理这四个纹理。不过,学习这些技术还是很有必要的,因为我们开发的游戏对硬件的要求会越来越高。

你也可以把精灵表称为纹理图集。一般来说,精灵表和纹理图集的区别在于,精灵表通常包含某个“事物”(比如角色或背景)的多个帧,而且这些帧的排列通常比较规整,就像我们的精灵表一样。而纹理图集通常包含多个不同事物的纹理,可能涵盖整个关卡甚至整个游戏,其排列可能没那么规整,并且包含不同尺寸的纹理。此外,纹理图集通常会搭配一个文本文件,其中描述了各个纹理的名称、位置和大小,游戏会借助这个文本文件来访问所需的图像。不管你把包含多个图像的图形文件称作什么,将多个图像放在一个文件中,都能加快游戏运行时上传和访问图像的速度。

从精灵表中绘制图像时,我们需要确保引用的是精灵表中所需部分的精确像素坐标,如下所示: img 图9.2:精灵表的像素坐标

上图为精灵表的每个部分/图块标注了坐标以及它们在精灵表中的位置。这些坐标被称为纹理坐标。我们将在代码中使用这些纹理坐标,来绘制所需的特定部分。

# 什么是顶点数组?

首先我们得了解:什么是顶点?顶点是单个图形点,即一个坐标,由水平和垂直位置定义。顶点的复数形式是vertices。顶点数组则是一组顶点的集合。

在SFML中,顶点数组里的每个顶点都有颜色,还有一对相关的额外顶点(即一对坐标),称为纹理坐标。纹理坐标表示我们想在精灵表中使用的图像位置。接下来我们会看到,如何通过单个顶点数组,实现图形定位以及选择精灵表中的特定部分在每个位置显示。

SFML的VertexArray类可以存储不同类型的顶点集,但每个VertexArray通常只存储一种类型的顶点集。我们会根据具体情况选择合适的顶点集类型。

在电子游戏中,常见的情况包括(但不限于)以下基本类型:

  • 点(Point):每个点对应一个顶点。
  • 线(Line):每组包含两个顶点,用于定义线段的起点和终点。
  • 三角形(Triangle):每个点对应三个顶点。这是最常用的类型(在复杂的3D模型中会用到数千个),也可以成对使用来创建简单的矩形,比如精灵。
  • 四边形(Quad):每组包含四个顶点。这是一种方便的方式,用于从精灵表中映射矩形区域。

在这个项目中,我们将使用四边形,因为它恰好满足我们绘制矩形精灵的需求。

# 用图块构建背景

“僵尸竞技场”的背景将由随机排列的方形图像组成。你可以把这种排列想象成地板上的瓷砖。

在这个项目中,我们将使用包含四边形集的顶点数组。每个顶点都是一组四个顶点(即一个四边形)的一部分。每个顶点定义背景图块的一个角,而每个纹理坐标则根据精灵表中的特定图像,存储相应的值。

我们来看一些代码,为后续工作做准备。这不是项目中实际使用的代码,但已经很接近了,能帮助我们在实际实现前,先了解顶点数组的相关知识。

# 构建顶点数组

与创建类的实例时一样,我们声明新对象。以下代码声明了一个VertexArray(顶点数组)类型的新对象,我们将其命名为background:

// 创建一个顶点数组
VertexArray background;
1
2

我们需要让VertexArray实例知道我们将使用哪种基本图形类型。要记住,点、线、三角形和四边形的顶点数量各不相同。通过将VertexArray实例设置为保存特定类型,就能够确定每个基本图形的起始位置。在我们的例子中,我们需要四边形。以下代码可以实现这一点:

// 我们使用哪种基本图形类型
background.setPrimitiveType(Quads);
1
2

与常规的C++数组一样,VertexArray实例也需要设置为特定大小。VertexArray类比常规数组更灵活,它允许我们在游戏运行时更改其大小。虽然大小可以在声明时进行配置,但我们的背景需要随着每一波游戏进程而扩展。VertexArray类通过resize函数提供了这一功能。以下代码将把我们竞技场的大小设置为10×10个图块大小:

// 设置顶点数组的大小
background.resize(10 * 10 * 4);
1
2

在上一行代码中,第一个10是宽度,第二个10是高度,4是一个四边形的顶点数量。我们本可以直接传入400,但像这样展示计算过程能更清楚地表明我们在做什么。在实际编写项目代码时,为了更清晰,我们会更进一步,为计算的每个部分声明变量。

现在我们有了一个VertexArray实例,准备配置其中的数百个顶点。下面是设置前四个顶点(即第一个四边形)位置坐标的方法:

// 为当前四边形中的每个顶点设置位置
background[0].position = Vector2f(0, 0);  
background[1].position = Vector2f(49, 0);  
background[2].position = Vector2f(49,49);  
background[3].position = Vector2f(0, 49);
1
2
3
4
5

下面是将这些顶点的纹理坐标设置为精灵图(sprite sheet)中第一张图像的方法。图像文件中的这些坐标是从左上角的0,0到右下角的49,49:

// 设置每个顶点的纹理坐标
background[0].texCoords = Vector2f(0, 0);  
background[1].texCoords = Vector2f(49, 0);  
background[2].texCoords = Vector2f(49, 49);  
background[3].texCoords = Vector2f(0, 49);
1
2
3
4
5

如果我们想将纹理坐标设置为精灵图中的第二张图像,代码会这样写:

// 设置每个顶点的纹理坐标
background[0].texCoords = Vector2f(0, 50);  
background[1].texCoords = Vector2f(49, 50);  
background[2].texCoords = Vector2f(49, 99);  
background[3].texCoords = Vector2f(0, 99);
1
2
3
4
5

当然,如果我们像这样逐个定义每个顶点,那么即使是配置一个简单的10×10的竞技场,也会花费很长时间。

在实际实现背景时,我们将设计一组嵌套的for循环,遍历每个四边形,随机选择一个背景图像,并分配合适的纹理坐标。

代码需要非常巧妙。它需要知道何时是边缘图块,以便可以使用精灵图中的墙壁图像。它还需要使用合适的变量,这些变量要知道精灵图中每个背景图块的位置,以及所需竞技场的整体大小。

我们会将所有代码放在一个单独的函数和一个单独的文件中,使这种复杂性易于管理。通过使用C++引用,我们可以在main函数中使用VertexArray实例。

我们稍后会详细探讨这些细节。你可能已经注意到,到目前为止,我们还没有将纹理(精灵图)与顶点数组关联起来。现在让我们看看如何做到这一点。

# 使用顶点数组进行绘制

现在我们已经准备好了顶点和纹理坐标,就可以在屏幕上进行绘制了。我们可以像加载其他任何纹理一样,将精灵图作为纹理加载,如下代码所示:

// 为我们的背景顶点数组加载纹理
Texture textureBackground;
textureBackground.loadFromFile("graphics/background_sheet.png");
1
2
3

然后,我们可以通过一次draw调用绘制整个VertexArray:

// 绘制背景
window.draw(background, &textureBackground);
1
2

上述代码比将每个图块作为单独的精灵进行绘制要高效得多。

在继续之前,请注意textureBackground代码前面看起来有点奇怪的&符号。你可能马上会想到这与引用有关。实际情况是,我们传递的是Texture实例的内存地址,而不是实际的Texture实例。我们将在下一章详细学习这方面的知识。

现在我们能够利用对引用和顶点数组的了解,来实现《僵尸竞技场》项目的下一阶段,即随机生成的滚动背景。

# 创建随机生成的滚动背景

在本节中,我们将在一个单独的文件中创建一个生成背景的函数。我们将通过使用顶点数组引用,确保背景在main函数中可用(在作用域内)。

由于我们还会编写其他与main函数共享数据的函数,所以我们会将它们都写在各自的.cpp文件中。我们会在一个新的头文件中提供这些函数的原型,然后在ZombieArena.cpp中使用#include指令包含这个头文件。

为此,让我们创建一个名为ZombieArena.h的新头文件。现在我们准备为新函数编写头文件代码。

在这个新的ZombieArena.h头文件中,添加以下高亮显示的代码,包括函数原型:

#pragma once

#include <SFML/Graphics.hpp> 
using namespace sf;
int createBackground(VertexArray& rVA, IntRect arena);
1
2
3
4
5

上述代码使我们能够编写一个名为createBackground的函数定义。为了与原型匹配,函数定义必须返回一个int值,并接收一个VertexArray引用和一个IntRect对象作为参数。

现在,我们可以创建一个新的.cpp文件,在其中编写函数定义。创建一个名为CreateBackground.cpp的新文件。现在我们准备编写创建背景的函数定义。

将以下代码添加到CreateBackground.cpp文件中,然后我们会进行回顾:

#include "ZombieArena.h"

int createBackground(VertexArray& rVA, IntRect arena) {
    // 我们对rVA所做的任何操作,实际上都是对main函数中的background所做的操作
    // 每个图块/纹理的大小是多少
    const int TILE_SIZE = 50;
    const int TILE_TYPES = 3;
    const int VERTS_IN_QUAD = 4;

    int worldWidth = arena.width / TILE_SIZE;  
    int worldHeight = arena.height / TILE_SIZE;

    // 我们使用哪种基本图形类型?
    rVA.setPrimitiveType(Quads);

    // 设置顶点数组的大小
    rVA.resize(worldWidth * worldHeight * VERTS_IN_QUAD);

    // 从顶点数组的开头开始
    int currentVertex = 0;

    return TILE_SIZE; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上述代码中,我们编写了函数签名,以及标记函数体开始和结束的花括号。

在函数体内,我们声明并初始化了三个新的int常量,用于保存我们在函数其余部分需要用到的值,它们是TILE_SIZE、TILE_TYPES和VERTS_IN_QUAD。

TILE_SIZE常量表示精灵图中每个图块的像素大小。TILE_TYPES常量表示精灵图中不同图块的数量。我们可以在精灵图中添加更多图块,并更改TILE_TYPES以匹配更改,而我们即将编写的代码仍然可以正常工作。VERTS_IN_QUAD表示每个四边形有四个顶点。与总是输入数字4相比,使用这个常量更不容易出错,因为数字4表意不够清晰。

然后,我们声明并初始化了两个int变量:worldWidth和worldHeight。从它们的名字就能明显看出这两个变量的用途,但值得指出的是,它们表示的是游戏世界的宽度和高度,单位是图块数量,而不是像素。worldWidth和worldHeight变量是通过将传入的竞技场的高度和宽度除以TILE_SIZE常量来初始化的。

接下来,我们首次使用引用。要记住,我们对rVA所做的任何操作,实际上都是对传入的变量(在main函数中处于作用域内,或者在我们编写代码时会处于作用域内)所做的操作。

然后,我们使用rVA.setType准备将顶点数组用于四边形,然后通过调用rVA.resize将其调整为合适的大小。我们向resize函数传入worldWidth * worldHeight * VERTS_IN_QUAD的结果,这相当于我们完成准备工作后顶点数组将拥有的顶点数量。

代码的最后一行声明并将currentVertex初始化为零。我们将在遍历顶点数组初始化所有顶点时使用currentVertex。

现在我们可以编写嵌套for循环的第一部分,用于准备顶点数组。添加以下高亮显示的代码,并根据我们对顶点数组的了解,试着弄清楚它的作用:

// 从顶点数组的开头开始
int currentVertex = 0;
for (int w = 0; w < worldWidth; w++) {
    for (int h = 0; h < worldHeight; h++) {
        // 为当前四边形中的每个顶点设置位置
        rVA[currentVertex + 0].position =
            Vector2f(w * TILE_SIZE, h * TILE_SIZE);

        rVA[currentVertex + 1].position =
            Vector2f((w * TILE_SIZE) + TILE_SIZE, h * TILE_SIZE);

        rVA[currentVertex + 2].position =
            Vector2f((w * TILE_SIZE) + TILE_SIZE, (h * TILE_SIZE) + TILE_SIZE);

        rVA[currentVertex + 3].position =
            Vector2f((w * TILE_SIZE), (h * TILE_SIZE) + TILE_SIZE);

        // 为下四个顶点的位置做准备
        currentVertex = currentVertex + VERTS_IN_QUAD; 
    }
}
return TILE_SIZE; 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们刚刚添加的代码通过使用嵌套for循环遍历顶点数组,首先遍历前四个顶点:currentVertex + 1、currentVertex + 2等等。

我们使用数组符号rVA[currentVertex + 0]...等来访问数组中的每个顶点。通过数组符号,我们调用position函数,如rVA[currentVertex + 0].position...。

我们将每个顶点的水平和垂直坐标传递给position函数。我们可以通过结合使用w、h和TILE_SIZE以编程方式计算出这些坐标。

在上述代码的最后,我们通过currentVertex = currentVertex + VERTS_IN_QUAD这行代码将currentVertex推进四个位置(即加四),为下一次遍历嵌套for循环做好准备。

当然,所有这些操作只是设置了顶点的坐标,并没有从精灵图中分配纹理坐标。接下来我们就会做这件事。

为了清楚地展示新代码的位置,我将其与我们刚才编写的所有代码一起展示。添加并研究以下高亮显示的代码:

for (int w = 0; w < worldWidth; w++) {
    for (int h = 0; h < worldHeight; h++) {
        // 为当前四边形中的每个顶点设置位置
        rVA[currentVertex + 0].position =
            Vector2f(w * TILE_SIZE, h * TILE_SIZE);

        rVA[currentVertex + 1].position =
            Vector2f((w * TILE_SIZE) + TILE_SIZE, h * TILE_SIZE);

        rVA[currentVertex + 2].position =
            Vector2f((w * TILE_SIZE) + TILE_SIZE, (h * TILE_SIZE) + TILE_SIZE);

        rVA[currentVertex + 3].position =
            Vector2f((w * TILE_SIZE), (h * TILE_SIZE) + TILE_SIZE);

        // 定义当前四边形在纹理中的位置,  可以是草地、石头、灌木丛或墙壁
        if (h == 0 || h == worldHeight - 1 ||
            w == 0 || w == worldWidth - 1)
        {
            // 使用墙壁纹理
            rVA[currentVertex + 0].texCoords =
                Vector2f(0, 0 + TILE_TYPES * TILE_SIZE);

            rVA[currentVertex + 1].texCoords = Vector2f(TILE_SIZE, 0 +
                TILE_TYPES * TILE_SIZE);

            rVA[currentVertex + 2].texCoords = Vector2f(TILE_SIZE, TILE_SIZE + TILE_TYPES * TILE_SIZE);

            rVA[currentVertex + 3].texCoords = Vector2f(0, TILE_SIZE +
                TILE_TYPES * TILE_SIZE);
        }

        // 为下四个顶点的位置做准备
        currentVertex = currentVertex + VERTS_IN_QUAD; 
    }
}
return TILE_SIZE; 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

前面高亮显示的代码设置了每个顶点在精灵图中对应的坐标。注意那个有点长的if条件。该条件检查当前四边形是否是竞技场中最开始或最末尾的四边形之一。如果是(第一个或最后一个),这意味着它是边界的一部分。然后我们可以使用一个包含TILE_SIZE和TILE_TYPES的简单公式来定位精灵图中的墙壁纹理。

通过数组符号和texCoords成员依次为每个顶点进行初始化,以分配精灵图中墙壁纹理的相应角落。

以下代码包含在else块中。这意味着每次四边形不代表边界/墙壁图块时,它都会遍历嵌套for循环。在现有代码中添加以下高亮显示的代码,然后我们进行分析:

// 定义当前四边形在纹理中的位置,  可以是草地、石头、灌木丛或墙壁
if (h == 0 || h == worldHeight - 1 ||
    w == 0 || w == worldWidth - 1)
{
    // 使用墙壁纹理
    rVA[currentVertex + 0].texCoords =
        Vector2f(0, 0 + TILE_TYPES * TILE_SIZE);

    rVA[currentVertex + 1].texCoords = Vector2f(TILE_SIZE, 0 +
        TILE_TYPES * TILE_SIZE);

    rVA[currentVertex + 2].texCoords = Vector2f(TILE_SIZE, TILE_SIZE + TILE_TYPES * TILE_SIZE);

    rVA[currentVertex + 3].texCoords = Vector2f(0, TILE_SIZE +
        TILE_TYPES * TILE_SIZE);
}
else
{
    // 使用随机的地面纹理
    srand((int)time(0) + h * w - h);

    int mOrG = (rand() % TILE_TYPES);
    int verticalOffset = mOrG * TILE_SIZE;  
    rVA[currentVertex + 0].texCoords =
        Vector2f(0, 0 + verticalOffset);

    rVA[currentVertex + 1].texCoords =
        Vector2f(TILE_SIZE, 0 + verticalOffset);

    rVA[currentVertex + 2].texCoords =
        Vector2f(TILE_SIZE, TILE_SIZE + verticalOffset);

    rVA[currentVertex + 3].texCoords =
        Vector2f(0, TILE_SIZE + verticalOffset);
}

// 为下四个顶点的位置做准备
currentVertex = currentVertex + VERTS_IN_QUAD; 
}
}
return TILE_SIZE; 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

前面高亮显示的代码首先使用一个在每次循环时都会不同的公式为随机数生成器设置种子。然后,mOrG变量被初始化为一个介于0到TILE_TYPES之间的数字。这正是我们随机选择一种图块类型所需要的。

mOrG代表“mud or grass”(泥或草),这个名字是随意取的。

现在,我们声明并初始化一个名为verticalOffset的变量,将mOrG乘以TileSize。现在我们在精灵图中有了一个垂直参考点,它是当前四边形随机选择的纹理的起始高度。

现在,我们使用一个包含TILE_SIZE和verticalOffset的简单公式,为每个顶点分配纹理每个角落的精确坐标。

现在我们可以在游戏引擎中使用这个新函数了。

# 使用背景

我们已经完成了复杂的部分,所以接下来的操作会很简单。有以下三个步骤:

  1. 创建一个顶点数组(VertexArray)。
  2. 在每一波升级后对其进行初始化。
  3. 在每一帧中绘制它。

在添加新代码之前,ZombieArena.cpp文件需要引入新的ZombieArena.h文件。在ZombieArena.cpp文件顶部添加以下包含指令:

#include "ZombieArena.h"
1

现在,添加以下突出显示的代码,声明一个名为background的顶点数组实例,并将background_sheet.png文件作为纹理加载:

//  Create  an  instance  of  the  Player  class
Player player;

//  The  boundaries  of  the  arena
IntRect arena;

//  Create  the  background
VertexArray   background;

//  Load  the  texture for  our  background  vertex array
Texture  textureBackground;
textureBackground.loadFromFile ("graphics/background_sheet.png");

//  The  main  game  loop
while (window.isOpen())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

添加以下代码来调用createBackground函数,将background作为引用传递,并按值传递arena。注意,在突出显示的代码中,我们还修改了初始化tileSize变量的方式。请严格按照显示的内容添加突出显示的代码:

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);
    // We  will modify  this  line  of  code  later
    //  int  tileSize  =  50;
    // Spawn  the player  in  the middle  of  the  arena
    player.spawn(arena, resolution, tileSize);
    //  Reset  the  clock  so  there  isn't  a frame jump
    clock.restart(); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

请注意,我们替换了int tileSize = 50这行代码,因为我们直接从createBackground函数的返回值获取该值。

为了使未来的代码更清晰,你应该删除int tileSize = 50这行代码及其相关注释。我只是将其注释掉,以便为新代码提供更清晰的上下文。

最后,是时候进行绘制了。这真的很简单。我们所要做的就是调用window.draw,并传递顶点数组实例以及textureBackground纹理的内存地址:

/*
**************
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 player
    window.draw(player.getSprite()); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果你对textureBackground前面那个看起来很奇怪的&符号感到疑惑,下一章会为你详细解释。

现在你可以运行游戏了。你会看到以下输出。记得按回车键并选择一个数字键,以跳过我们暂时不可见的菜单: img 图9.3:使用背景

在这里,注意玩家的精灵是如何在游戏区域(arena)范围内平稳地滑动和旋转的。虽然目前主函数中的代码绘制的是一个小的游戏区域,但CreateBackground函数可以创建任意大小的游戏区域。在第14章“音效、文件输入/输出以及完成游戏”中,我们将看到比屏幕还大的游戏区域。

# 总结

在本章中,我们学习了C++引用(references),它是一种特殊的变量,作为其他变量的别名。当我们通过引用而不是按值传递变量时,对引用所做的任何操作都会影响调用函数中的原始变量。

我们还学习了顶点数组,并创建了一个充满四边形的顶点数组,以便从精灵表(sprite sheet)中绘制图块作为背景。

当然,我们的僵尸游戏目前还没有任何僵尸。在下一章中,我们将通过学习C++指针(pointers)和标准模板库(STL,Standard Template Library)来解决这个问题。

# 常见问题解答

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

  • 问:你能再总结一下这些引用吗?
  • 答:你必须立即初始化一个引用,并且它不能被更改为引用另一个变量。在函数中使用引用,这样你操作的就不是变量的副本。这对提高效率很有帮助,因为它避免了复制操作,并且有助于我们更轻松地将代码抽象成函数。
  • 问:有没有简单的方法记住使用引用的主要好处?
  • 答:为了帮助你记住引用的用途,考虑下面这首短诗: 移动大对象会让游戏卡顿,传引用比传副本更快。
第8章 SFML视图——开启丧尸射击游戏
第10章 指针、标准模板库和纹理管理

← 第8章 SFML视图——开启丧尸射击游戏 第10章 指针、标准模板库和纹理管理→

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