当前位置:网站首页 > 技术博客 > 正文

游戏开发编程



原文:

译者:飞龙

协议:CC BY-NC-SA 4.0

这本书是关于以有趣的方式学习 C ++编程。从零开始,您将学习 C ++的基础知识,如变量和循环,直到高级主题,如继承和多态。您学到的一切都将被应用到构建三个完全可玩的游戏中。

这是我们这本书的三个项目。

第一个游戏是一个令人上瘾的,快节奏的模仿非常成功的伐木工的游戏,。我们的游戏 Timber!!!将让我们同时学习所有 C ++的基础知识,同时构建一个真正可玩的游戏。

接下来,我们将构建一个疯狂的僵尸生存射击游戏,类似于 Steam 的热门游戏 Over 9,000 Zombies,。玩家将拥有一把机关枪,并必须抵御不断增长的僵尸浪潮。所有这些将发生在一个随机生成的滚动世界中。为了实现这一点,我们将学习面向对象编程以及它如何使我们能够拥有一个大量的易于编写和维护的代码库。期待令人兴奋的功能,如数百个敌人,快速射击武器,拾取物品以及每一波后都可以“升级”的角色。

第三个游戏将是一个时尚而具有挑战性的单人和合作解谜平台游戏。它基于非常受欢迎的游戏 Thomas was Alone,。期待学习一些酷炫的主题,如粒子效果,OpenGL 着色器和分屏合作多人游戏。

第一章,“C ++,SFML,Visual Studio 和开始第一个游戏”,这是一个相当庞大的第一章,但我们将学到我们需要的一切,以便让我们的第一个游戏的第一部分运行起来。以下是我们将要做的事情:

  • 了解我们将要构建的游戏更多信息
  • 学习一些关于 C ++的知识
  • 探索 SFML 及其与 C ++的关系
  • 查看我们将在整本书中使用的软件 Visual Studio
  • 设置游戏开发环境
  • 创建一个可重复使用的项目模板,这将节省大量时间
  • 计划并准备第一个游戏项目,伐木者!!!
  • 编写本书的第一个 C ++代码,并制作一个可运行的游戏,绘制一个背景

第二章,“变量,运算符和决策-动画精灵”,在本章中,我们将在屏幕上进行更多的绘制,并且为了实现这一点,我们需要学习一些 C ++的基础知识。以下是我们将要做的事情:

  • 学习所有关于 C ++变量的知识
  • 了解如何操作存储在变量中的值
  • 添加一个静态树,准备让玩家砍伐
  • 绘制和动画一个蜜蜂和三朵云

第三章,“C++字符串,SFML 时间-玩家输入和 HUD”,在本章中,我们将花大约一半的时间学习如何操作文本并在屏幕上显示它,另一半时间看计时和视觉时间条如何通知玩家并在游戏中制造紧迫感。我们将涵盖:

  • 暂停和重新开始游戏
  • C ++字符串
  • SFML 文本和 SFML 字体类
  • 为 Timber!!!添加 HUD
  • 为 Timber!!!添加一个时间条

第四章,循环、数组、开关、枚举和函数-实现游戏机制,这一章可能包含比书中其他任何章节都多的 C++信息。它充满了基本概念,将极大地提高我们的理解。它还将开始阐明一些我们一直略过的模糊领域,比如函数和游戏循环。一旦我们探索了 C++语言的一系列必需知识,我们将利用我们所知道的一切来使主要游戏机制——树枝移动。到本章结束时,我们将准备好进入最后阶段,完成 Timber!!!。准备好接下来的主题:

  • 循环
  • 数组
  • 使用开关进行决策
  • 枚举
  • 开始使用函数
  • 创建和移动树枝

第五章,碰撞、声音和结束条件-使游戏可玩,这是第一个项目的最后阶段。到本章结束时,你将拥有你的第一个完成的游戏。一旦你让 Timber!!!运行起来,请务必阅读本章的最后一节,因为它将提出改进游戏的建议:

  • 添加其余的精灵
  • 处理玩家输入
  • 动画飞行原木
  • 处理死亡
  • 添加音效
  • 添加功能和改进 Timber!!!

第六章,面向对象编程、类和 SFML 视图,这是本书最长的一章。有相当多的理论,但这些理论将使我们有能力开始有效地使用面向对象编程。此外,我们将不会浪费任何时间来充分利用这些理论。在探索 C++面向对象编程之前,我们将了解并计划我们的下一个游戏项目。我们将做以下事情:

  • 计划僵尸竞技场游戏
  • 了解面向对象编程和类
  • 编写 Player 类
  • 了解 SFML View 类
  • 构建僵尸竞技场游戏引擎
  • 让 Player 类投入使用

第七章,C++引用、精灵表和顶点数组,在本章中,我们将探索 C++引用,它允许我们处理变量和对象,否则超出范围。此外,引用将帮助我们避免在函数之间传递大型对象,这是一个缓慢的过程。这是一个缓慢的过程,因为每次这样做时,都必须复制变量或对象。

掌握了关于引用的新知识,我们将看看 SFML 类,它允许我们构建一个大图像,可以使用单个图像文件中的多个图像非常快速和高效地绘制到屏幕上。到本章结束时,我们将拥有一个可扩展的、随机的、滚动的背景,使用引用和对象。

我们现在将讨论:

  • C++引用
  • SFML
  • 编写随机滚动背景

第八章,指针、标准模板库和纹理管理,在本章中,我们将学到很多,同时也会为游戏做很多工作。我们将首先学习指针这一基本的 C++主题。指针是保存内存地址的变量。通常,指针将保存另一个变量的内存地址。这听起来有点像引用,但我们将看到它们更加强大,我们将使用指针来处理不断增多的僵尸群。

我们还将学习标准模板库(STL),这是一组允许我们快速、轻松地实现常见数据管理技术的类。

一旦我们理解了 STL 的基础知识,我们就能够利用这些新知识来管理游戏中的所有纹理,因为如果我们有 1000 个僵尸,我们实际上不想为每一个加载一份僵尸图形到 GPU 中。

我们还将深入了解面向对象编程,并使用静态函数,这是一个可以在没有类实例的情况下调用的类函数。同时,我们将看到如何设计一个类,以确保只能存在一个实例。当我们需要保证代码的不同部分将使用相同的数据时,这是理想的。

在这一章中,我们将:

  • 学习指针
  • 学习 STL
  • 使用静态函数和单例类实现 Texture Holder 类
  • 实现一个指向一群僵尸的指针
  • 编辑一些现有的代码,使用 TextureHolder 类为玩家和背景

第九章, 碰撞检测、拾取物品和子弹,到目前为止,我们已经实现了游戏的主要视觉部分。我们有一个可控制的角色在一个充满追逐他的僵尸的竞技场中奔跑。问题是它们彼此之间没有互动。僵尸可以毫无阻碍地穿过玩家。我们需要检测僵尸和玩家之间的碰撞。

如果僵尸能够伤害并最终杀死玩家,那么给玩家一些子弹是公平的。然后我们需要确保子弹能够击中并杀死僵尸。

同时,如果我们为子弹、僵尸和玩家编写碰撞检测代码,那么现在是添加一个用于健康和弹药拾取物品的类的好时机。

以下是我们将要做的事情和我们将要涵盖的顺序:

  • 射击子弹
  • 添加准星并隐藏鼠标指针
  • 生成拾取物品
  • 检测碰撞

第十章, 分层视图和实现 HUD,在这一章中,我们将看到 SFML 视图的真正价值。我们将添加大量的 SFML 文本对象,并像之前在 Timber!!!项目中那样操纵它们。新的是我们将使用第二个视图实例来绘制 HUD。这样,HUD 将始终整齐地定位在主游戏动作的顶部,而不管背景、玩家、僵尸和其他游戏对象在做什么。

我们将做以下事情:

  • 在主页/游戏结束屏幕上添加文本和背景
  • 在升级屏幕上添加文本
  • 创建第二个视图
  • 添加 HUD

第十一章, 音效、文件 I/O 和完成游戏,我们快要完成了。这一小节将演示我们如何使用 C++标准库轻松操作存储在硬盘上的文件,我们还将添加音效。当然,我们知道如何添加音效,但我们将讨论在代码中的调用应该放在哪里。我们还将收尾一些松散的地方,使游戏完整。在这一章中,我们将做以下事情:

  • 保存和加载最高分
  • 添加音效
  • 允许玩家升级
  • 创建无尽的多波次

第十二章 ,抽象和代码管理-更好地利用 OOP,在本章中,我们将首次查看本书的最终项目。该项目将具有高级功能,例如与玩家位置相关的从扬声器发出的定向声音。它还将具有分屏合作游戏。此外,该项目将介绍着色器的概念,这是用另一种语言编写的直接在图形卡上运行的程序。到第十六章 结束时,您将拥有一个完全功能的多人平台游戏,其风格类似于经典游戏 Thomas Was Alone。本章的主要重点将是启动项目,特别是探索如何构造代码以更好地利用 OOP。以下是本章的详细信息。

  • 介绍最终项目 Thomas Was Late,包括游戏功能和项目资产
  • 详细讨论我们将如何改进代码结构,与之前的项目相比
  • 编写 Thomas Was Late 游戏引擎
  • 实现分屏功能

第十三章 ,高级 OOP-继承和多态,在本章中,我们将通过查看继承和多态的略微更高级的概念,进一步扩展我们对 OOP 的知识。然后,我们将能够使用这些新知识来实现我们游戏的明星角色 Thomas 和 Bob。以下是我们将更详细地涵盖的内容:

  • 学习如何使用继承扩展和修改类
  • 使用多态将类的对象视为多种类型的类
  • 学习抽象类以及设计从未实例化的类如何实际上是有用的
  • 构建一个抽象的类
  • 使用继承与和类
  • 将 Thomas 和 Bob 添加到游戏项目

第十四章 ,构建可玩关卡和碰撞检测,本章可能是本项目中最令人满意的章节之一。原因是到最后,我们将拥有一个可玩的游戏。尽管还有一些功能需要实现(声音、粒子效果、HUD、着色器效果),但 Bob 和 Thomas 将能够奔跑、跳跃和探索世界。此外,您将能够通过简单地在文本文件中创建平台和障碍物,轻松创建几乎任何大小或复杂度的自己的关卡设计。我们将通过以下主题实现所有这些:

  • 探索如何在文本文件中设计关卡
  • 构建类,该类将从文本文件加载关卡,将其转换为我们的游戏可以使用的数据,并跟踪关卡细节,如生成位置、当前关卡和允许的时间限制
  • 更新游戏引擎以使用
  • 编写一个多态函数来处理 Bob 和 Thomas 的碰撞检测

第十五章 ,声音空间化和 HUD,在本章中,我们将添加所有的音效和 HUD。我们在之前的两个项目中都做过这个,但这次我们会有所不同。我们将探索声音空间化的概念,以及 SFML 如何使这个本来复杂的概念变得简单易行;此外,我们将构建一个 HUD 类来封装我们的代码,将信息绘制到屏幕上。

我们将按照以下顺序完成这些任务:

  • 什么是空间化?
  • SFML 如何处理空间化
  • 构建一个类
  • 部署发射器
  • 使用类
  • 构建类
  • 使用 类

第十六章, 扩展 SFML 类、粒子系统和着色器,在这一章中,我们将探讨 C++ 中扩展其他人类的概念。更具体地说,我们将研究 SFML 类以及将其用作我们自己类的基类的好处。我们还将浅尝 OpenGL 着色器的主题,并看看如何使用另一种语言(GLSL)编写代码,该代码可以直接在图形卡上运行,从而产生可能以其他方式不可能实现的平滑图形效果。像往常一样,我们还将使用我们的新技能和知识来增强当前项目。

以下是我们将按顺序涵盖的主题列表:

  • SFML 可绘制
  • 构建粒子系统
  • OpenGl 着色器和 GLSL
  • 在《Thomas Was Late》游戏中使用着色器

第十七章,“在你离开之前...”,快速讨论接下来可能要做的事情。

  • Windows 7 Service Pack 1、Windows 8 或 Windows 10
  • 1.6 GHz 或更快的处理器
  • 1 GB 的 RAM(对于 x86)或 2 GB 的 RAM(对于 x64)
  • 15 GB 的可用硬盘空间
  • 5400 RPM 硬盘驱动器
  • DirectX 9 兼容的视频卡,支持 1024 x 768 或更高的显示分辨率

本书中使用的所有软件都是免费的。在书中逐步介绍了获取和安装软件的步骤。本书始终在 Windows 上使用 Visual Studio,但有经验的 Linux 用户可能不会在其喜爱的 Linux 编程环境中运行代码和按照说明出现问题。

如果以下任何情况描述您,本书非常适合您:您完全不了解 C++ 编程,或需要初学者级别的复习课程;如果您想学习制作游戏或者只是想以一种引人入胜的方式学习 C++;如果您有志于有朝一日发布游戏,也许是在 Steam 上;或者如果您只是想玩得开心,并以您的创作给朋友留下深刻印象。

在本书中,您将找到一些区分不同信息种类的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"我们可以通过使用 include 指令包含其他上下文。"

代码块设置如下:

 
  

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

 
  

任何命令行输入或输出都将按如下方式书写:

 
  

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“单击下一步按钮将您移至下一个屏幕。”

警告或重要提示将以如下方式显示在一个框中。

提示和技巧显示如下。

欢迎来到《开始 C++游戏编程》。我将立即让你开始你的写作之旅,使用 C++和OpenGL-powered SFML为 PC 编写出色的游戏。

这是一个相当庞大的第一章,但我们将学到我们需要的一切,以便让我们第一个游戏的第一部分运行起来。在本章中,我们将涵盖以下内容:

  • 了解我们将构建的游戏
  • 学习一些关于 C++的知识
  • 探索 SFML 及其与 C++的关系
  • 查看我们将在整本书中使用的 Visual Studio 软件
  • 设置游戏开发环境
  • 创建一个可重用的项目模板,这将节省大量时间
  • 计划并准备第一个游戏项目,伐木者!!!
  • 编写本书的第一个 C++代码,并制作一个可运行的游戏来绘制背景

我们将逐步学习超快的 C++语言的基础知识,然后将新知识应用到实践中,因此应该相当容易地为我们正在构建的三款游戏添加酷炫的功能。

如果你在本章的任何内容上遇到困难,请查看最后的处理错误和常见问题解答部分。

这是我们书中的三个项目:

第一个游戏是一款令人上瘾、节奏快速的《伐木工》的克隆版本,该游戏可以在找到。我们的游戏《伐木者!!!》将在构建一个真正可玩的游戏的同时,向我们介绍所有 C++的基础知识。当我们完成并添加了一些最后一刻的增强功能时,我们的游戏版本将是这个样子。

伐木者!!!

接下来,我们将构建一个疯狂的僵尸生存射击游戏,类似于 Steam 的热门游戏《超过 9000 只僵尸》,该游戏可以在找到。玩家将拥有一把机关枪,并必须抵御不断增长的僵尸浪潮。所有这些将发生在一个随机生成的滚动世界中。为了实现这一点,我们将学习面向对象编程以及它如何使我们能够拥有一个庞大的代码库(大量代码),易于编写和维护。期待令人兴奋的功能,如数百个敌人、快速射击武器、拾取物品以及每一波后都可以“升级”的角色。

僵尸竞技场

第三款游戏将是一款时尚而具有挑战性的益智平台游戏,可以作为单人和合作游玩。它基于非常受欢迎的游戏《托马斯是孤独的》,该游戏可以在找到。期待学习有关粒子效果、OpenGL 着色器和分屏合作多人游戏等酷炫主题。

托马斯迟到了

如果你现在想玩任何游戏,可以在文件夹中的下载包中进行。只需双击相应的文件。请注意,在这个文件夹中,你可以运行已完成的游戏,也可以从任何章节的部分完成状态中运行任何游戏。

让我们开始介绍 C++、Visual Studio 和 SFML!

你可能会有一个问题,为什么要使用 C++?C++很快,非常快。使其成为这种情况的原因是我们编写的代码直接转换为机器可执行指令。这些指令构成了游戏。可执行游戏包含在一个文件中,玩家只需双击即可运行。

这个过程中有几个步骤。首先,预处理器查看我们的代码中是否需要包含其他代码,并在必要时添加它。接下来,编译器程序将所有代码编译成目标文件。最后,一个名为链接器的第三个程序将所有目标文件连接成可执行文件,这就是我们的游戏。

此外,C++既是一个成熟的语言,同时又非常现代化。C++是一种面向对象的编程语言,这意味着我们可以以一种经过验证的方式编写和组织我们的代码,使我们的游戏高效且易于管理。这些好处以及这种必要性将随着我们在书中的进展而显现。

我提到的大部分其他代码都是 SFML,我们将在接下来的一分钟内了解更多关于 SFML 的信息。我刚刚提到的预处理器、编译器和链接器程序都是 Visual Studio 集成开发环境(IDE)的一部分。

Visual Studio 隐藏了预处理、编译和链接的复杂性。它将所有这些封装成一个按钮。此外,它为我们提供了一个流畅的用户界面,让我们输入我们的代码并管理将成为大量代码文件和其他项目资产的选择。

虽然有高级版本的 Visual Studio 需要花费数百美元,但我们可以在免费的Express 2015 for Desktop版本中构建我们的三个游戏。

Simple Fast Media Library(SFML)不是唯一的 C++游戏和多媒体库。可能有人会主张使用其他库,但对我来说,SFML 似乎是最好的选择。首先,它是使用面向对象的 C++编写的。这样做的好处是多方面的。随着你在书中的进展,你将体验到大部分这些好处。

SFML 非常容易上手,因此对于初学者来说是一个很好的选择。同时,如果你是专业人士,它也有潜力构建最高质量的 2D 游戏。因此,初学者可以开始使用 SFML,而不必担心随着经验的增长需要重新开始学习新的语言/库。

也许最大的好处是大多数现代 C++编程都使用面向对象编程。我读过的每一本 C++初学者指南都使用并教授面向对象编程。事实上,面向对象编程几乎是所有语言中编码的未来(和现在)。因此,如果你从头开始学习 C++,为什么要以其他方式学习呢?

SFML 几乎为 2D 游戏中你可能想做的任何事情提供了模块(代码)。SFML 使用 OpenGL 工作,它也可以制作 3D 游戏。OpenGL 是游戏的事实上免费使用的图形库,当你希望它们在多个平台上运行时。当你使用 SFML 时,你自动使用 OpenGL。

SFML 大大简化了:

  • 2D 图形和动画,包括滚动游戏世界。
  • 包括高质量的定向声音在内的音效和音乐播放。
  • 在线多人游戏功能
  • 相同的代码可以在所有主要桌面操作系统上进行编译和链接,很快也可以在移动设备上进行。

广泛的研究并没有发现任何更适合的方式来为 PC 构建 2D 游戏,即使对于专业开发人员来说也是如此,尤其是如果你是初学者,并且想在有趣的游戏环境中学习 C++。

现在你对我们将如何制作这些游戏有了一些了解,是时候设置开发环境,让我们开始编码了。

我们制作的游戏可以在 Windows、Mac 和 Linux 上运行!我们使用的代码对于每个平台都是相同的。然而,每个版本都需要在其预期的平台上进行编译和链接,而 Visual Studio 将无法帮助我们处理 Mac 和 Linux。

说这本书完全适合 Mac 和 Linux 用户,尤其是完全的初学者,可能有些不公平。尽管,我猜,如果你是一个热衷于 Mac 或 Linux 的用户,并且对你的操作系统感到舒适,你将遇到的大部分额外挑战将在开发环境、SFML 和第一个项目的初始设置中。

为此,我强烈推荐以下教程,希望能替代接下来的大约 10 页(大约),直到Planning Timber!!!部分,当这本书应该再次适用于所有操作系统。

对于 Linux,阅读这篇概述:。

对于 Linux,阅读这篇逐步指导:。

在 Mac 上,阅读这篇教程以及链接的文章:。

安装 Visual Studio 几乎和下载一个文件并点击几下按钮一样简单。然而,如果我们仔细地按照我们的步骤来做,这将对我们有所帮助。因此,我将一步一步地走过安装过程。

微软 Visual Studio 网站表示你需要 5GB 的硬盘空间。然而,根据经验,我建议你至少需要 10GB 的可用空间。此外,这些数字有些模糊。如果你计划将其安装在辅助硬盘上,你仍然需要主硬盘上至少 5GB 的空间,因为无论你选择在哪里安装 Visual Studio,它也需要这个空间。

总结这种模糊的情况:如果你打算将 Visual Studio 安装到主硬盘上,那么在主硬盘上必须有完整的 10GB 空间是必不可少的。另一方面,如果你打算安装到辅助硬盘上,确保你的主硬盘上有 5GB 的空间,辅助硬盘上有 10GB 的空间。是的,愚蠢,我知道!

  1. 你需要的第一件事是一个微软账户和登录详情。如果你有 Hotmail 或 MSN 邮箱地址,那么你已经有了一个。如果没有,你可以在这里免费注册一个:。
  2. 访问这个链接:。点击Visual Studio 2015,然后点击Express 2015 for desktop,然后点击Downloads按钮。下一个截图显示了三个点击的位置:Installing Visual Studio Express 2015 on your desktop
  3. 等待短暂的下载完成,然后运行下载的文件。现在你只需要按照屏幕上的指示进行操作。但是,请记下你选择安装 Visual Studio 的文件夹。如果你想和我做的一样,就在你喜欢的硬盘上创建一个名为的新文件夹,并安装到这个文件夹中。整个过程可能需要一段时间,取决于你的互联网连接速度。
  4. 当你看到下一个屏幕时,点击Launch并输入你的微软账户登录详情。Installing Visual Studio Express 2015 on your desktop

现在我们可以转向 SFML。

这个简短的教程将带你下载 SFML 文件,使我们能够包含库中包含的功能。此外,我们将看到如何使用 SFML DLL 文件,这将使我们编译的目标代码能够与 SFML 一起运行。

  1. 访问 SFML 网站上的这个链接:。点击下一个显示的Latest stable version按钮。Setting up SFML
  2. 当您阅读本指南时,最新版本几乎肯定已经更改。只要您正确执行下一步,这并不重要。我们要下载Visual C++ 2014的 32 位版本。这可能听起来有些违反直觉,因为我们刚刚安装了 Visual Studio 2015,您可能(最常见)有一台 64 位 PC。我们选择此下载的原因是因为 Visual C++ 2014 是 Visual Studio 2015 的一部分(Visual Studio 提供的不仅仅是 C++),我们将以 32 位构建游戏,以便它们在 32 位和 64 位机器上运行。为了明确起见,单击以下下载:设置 SFML
  3. 下载完成后,在安装 Visual Studio 的相同驱动器的根目录创建一个名为的文件夹。还在安装 Visual Studio 的相同驱动器的根目录创建另一个文件夹,并将其命名为。我们将在这里存储各种与 Visual Studio 相关的东西,因此似乎是一个不错的名字。为了明确起见,这是在完成此步骤后我的硬盘的样子:设置 SFML
  4. 显然,您在截图中突出显示的三个文件夹之间的文件夹可能与我的完全不同。现在我们准备好了即将创建的所有项目,创建一个新文件夹在内。将新文件夹命名为。
  5. 最后,解压 SFML 下载。在桌面上进行此操作。解压完成后,可以删除文件夹。您将在桌面上留下一个单独的文件夹。其名称将反映您下载的 SFML 版本。我的称为。您的文件名可能反映了一个更新的版本。双击此文件夹以查看内容,然后再次双击进入下一个文件夹(我的称为)。以下截图显示了当选择了整个内容时,我的文件夹的内容是什么样子的。您的应该看起来一样。设置 SFML
  6. 复制前面截图中所见的整个文件夹的内容,并将所有内容粘贴/拖放到第 3 步中创建的文件夹中。在本书的其余部分,我将简称此文件夹为您的 SFML 文件夹。

现在我们准备在 Visual Studio 中开始使用 C++和 SFML。

由于设置项目是一个相当繁琐的过程,我们将创建一个项目,然后将其保存为 Visual Studio 模板。这将节省我们每次开始新游戏时相当大量的工作。因此,如果您发现下一个教程有点乏味,请放心,您将永远不需要再次这样做:

  1. 启动 Visual Studio,在新项目窗口中,单击Visual C++旁边的小下拉箭头以显示更多选项,然后单击Win32,再单击Win32 控制台应用程序。您可以在下一个截图中看到所有这些选择。创建可重用的项目模板
  2. 现在,在新项目窗口的底部,在名称:字段中键入。
  3. 接下来,浏览到我们在上一篇教程中创建的文件夹。这将是我们保存所有项目文件的位置。所有模板都是基于实际项目的。因此,我们将有一个名为的项目,但我们将做的唯一事情就是从中制作一个模板。创建可重用的项目模板
  4. 完成上述步骤后,单击确定。下一个截图显示了应用程序设置窗口。选中控制台应用程序的复选框,并将其他选项保持如下所示。创建可重用的项目模板
  5. 单击完成,Visual Studio 将创建新项目。
  6. 接下来,我们将添加一些相当复杂和重要的项目设置。这是费力的部分,但由于我们将创建一个模板,我们只需要做一次。我们需要告诉 Visual Studio,或者更具体地说,Visual Studio 的代码编译器,从哪里找到 SFML 的特殊类型的代码文件。我所指的特殊类型的文件是头文件。头文件定义了 SFML 代码的格式。因此,当我们使用 SFML 代码时,编译器知道如何处理它。请注意,头文件与主源代码文件不同,并且它们包含在扩展名为的文件中。(当我们最终开始在第二个项目中添加自己的头文件时,所有这些将变得更清晰)。此外,我们需要告诉 Visual Studio 它在哪里可以找到 SFML 库文件。从 Visual Studio 的主菜单中选择项目 | HelloSFML 属性
  7. 在生成的HelloSFML 属性页窗口中,执行下一截图中标记的步骤。
  8. 配置:下拉菜单中选择所有配置
  9. 从左侧菜单中选择C/C++,然后选择常规
  10. 定位附加包含目录编辑框,并输入您的 SFML 文件夹所在的驱动器号,然后加上。如果您的文件夹位于 D 驱动器上,则要输入的完整路径如截图所示:。如果您将 SFML 安装到其他驱动器上,则需要更改路径。创建可重用的项目模板
  11. 点击应用以保存到目前为止的配置。
  12. 现在,在同一窗口中,执行下一截图中标记的步骤。选择链接器,然后选择常规
  13. 找到附加库目录编辑框,并输入您的文件夹所在的驱动器号,然后加上。因此,如果您的文件夹位于 D 驱动器上,则要输入的完整路径如截图所示:。如果您将 SFML 安装到其他驱动器上,则需要更改路径。创建可重用的项目模板
  14. 点击应用以保存到目前为止的配置。
  15. 最后,在同一窗口中,执行下一截图中标记的步骤。将配置:下拉菜单(1)切换到调试,因为我们将在调试模式下运行和测试游戏。
  16. 选择链接器,然后选择输入(2)。
  17. 找到附加依赖项编辑框(3),并在最左侧点击进入。现在复制并粘贴/输入以下内容:。再次要非常小心地将光标放置在正确的位置,并且不要覆盖已经存在的任何文本。
  18. 点击确定创建可重用的项目模板
  19. 让我们从项目中创建一个模板,这样我们就永远不必再做这个略显乏味的任务了。创建可重用的项目模板非常简单。在 Visual Studio 中选择文件 | 导出模板...。然后,在导出模板向导窗口中,确保选择了项目模板选项,然后选择HelloSFML项目作为要创建模板的项目选项。
  20. 点击下一步,然后点击完成

哦,就是这样!下次我们创建项目时,我会告诉您如何从这个模板中创建。现在让我们构建 Timber!!!

每当制作游戏时,最好都要先用铅笔和纸开始。如果您不确定游戏在屏幕上的工作方式,又怎么可能在代码中使其正常工作呢?

此时,如果你还没有这样做,我建议你去观看一段 Timberman 的游戏视频,这样你就可以看到我们的目标是什么。如果你的预算允许,那就买一份来玩玩。在 Steam 上通常会以不到一美元的价格出售。 .

游戏的特性和物体,定义了游戏玩法,被称为机制。游戏的基本机制是:

  • 时间总是在流逝。
  • 通过砍树来获得更多时间。
  • 砍树会导致树枝掉落。
  • 玩家必须避开掉落的树枝。
  • 重复直到时间用完或玩家被压扁。

在这个阶段期望你计划 C++代码显然有点傻。当然,这是 C++初学者指南的第一章。然而,我们可以看一下我们将使用的所有资源以及我们需要让我们的 C++做我们想要的事情的概述。

看一下游戏的注释截图:

规划 Timber!!!

你可以看到我们有以下特性:

  • 玩家当前得分:每次玩家砍一根木头,他就会得到一个点。他可以用左箭头或右箭头砍木头。
  • 玩家角色:每次玩家砍的时候,他会移动/停留在树的同一侧。因此,玩家必须小心选择砍树的哪一侧。当玩家砍的时候,一个简单的斧头图形会出现在玩家角色的手中。
  • 缩小的时间条:每次玩家砍的时候,一小段时间将被添加到不断缩小的时间条上。
  • 致命的树枝:玩家砍得越快,他得到的时间就越多,但树枝也会更快地从树上掉下来,因此他被压扁的可能性就越大。树枝在树顶随机生成,并且每次砍树都会向下移动。
  • 当玩家被压扁时,他会经常被压扁,一个墓碑图形会出现。
  • 被砍的木头:当玩家砍的时候,一个被砍的木头图形会从玩家身边飞走。
  • 有三朵漂浮的云,它们会以随机的高度和速度飘动,还有一只蜜蜂,除了四处飞来飞去什么也不做。
  • 所有这些都发生在一个漂亮的背景上。

因此,简而言之,玩家必须疯狂地砍树来获得积分,并避免时间用尽。作为一个略微扭曲但有趣的结果,他砍得越快,他被压扁的可能性就越大。

现在我们知道游戏的外观,玩法以及游戏机制背后的动机。我们可以继续开始构建它。

现在创建一个新项目非常容易。只需在 Visual Studio 中按照这些简单的步骤操作:

  1. 从主菜单中选择文件 | 新项目
  2. 确保在左侧菜单中选择Visual C++,然后从所呈现的选项列表中选择HelloSFML。下一个截图应该能清楚地说明这一点。从模板创建项目
  3. 名称:字段中键入,并确保选中为解决方案创建目录选项。现在点击确定
  4. 现在我们需要将 SFML 的文件复制到主项目目录中。我的主项目目录是。它是在上一步中由 Visual Studio 创建的。如果你把你的文件夹放在其他地方,那么就在那里执行这一步。我们需要复制到项目文件夹中的文件位于你的文件夹中。打开两个位置的窗口,并按照左侧下一个截图中显示的要求文件进行突出显示。从模板创建项目
  5. 现在将突出显示的文件复制并粘贴到上一张截图右侧的项目文件夹中。

项目现在已经设置好,准备就绪。您将能够在下一个截图中看到屏幕。我已经对截图进行了注释,这样您就可以开始熟悉 Visual Studio 了。我们很快会重新访问所有这些区域以及其他区域。

从模板创建项目

您的布局可能与截图略有不同,因为 Visual Studio 的窗口,像大多数应用程序一样,是可定制的。花些时间找到右侧的Solution Explorer窗口,并调整它使其内容清晰明了,就像前面的截图一样。

我们很快会回到这里开始编码。

资产是制作游戏所需的任何东西。在我们的情况下,资产包括:

  • 屏幕上的书写字体
  • 不同动作的音效,如砍伐、死亡和时间耗尽
  • 角色、背景、树枝和其他游戏对象的图形

游戏所需的所有图形和声音都包含在下载包中。它们可以在相应的和文件夹中找到。

所需的字体尚未提供。这是因为我想避免任何可能的许可歧义。不过这不会造成问题,因为我将向您展示确切的位置和方式来选择和下载字体。

尽管我将提供资产本身或获取它们的信息,但您可能希望自己创建和获取它们。

有许多网站可以让您与艺术家、声音工程师甚至程序员签约。其中最大的之一是www.upwork.com。您可以免费加入该网站并发布您的工作。您需要清晰地解释您的要求,以及说明您愿意支付多少。然后您可能会得到许多承包商竞标做这项工作。请注意,有很多不合格的承包商,他们的工作可能令人失望,但如果您选择得当,您可能会找到一个称职、热情和物有所值的人或公司来完成工作。

可以从网站(如www.freesound.org)免费下载音效,但通常许可证不允许您在出售游戏时使用它们。另一个选择是使用名为 BFXR 的开源软件,该软件可以帮助您生成许多不同的音效,这些音效是您自己保留并随意使用的。

一旦您决定要使用哪些资产,就该是将它们添加到项目的时候了。下面的说明将假定您正在使用书籍下载包中提供的所有资产。如果您使用自己的资产,只需用您自己的相应音效或图形文件替换,文件名完全相同即可。

  1. 浏览到 Visual 。
  2. 在此文件夹中创建三个新文件夹,并将它们命名为、和。
  3. 从下载包中,将的整个内容复制到文件夹中。
  4. 从下载包中,将的整个内容复制到文件夹中。
  5. 现在访问: 在您的网络浏览器中下载Komika Poster字体。
  6. 解压缩下载的内容,并将文件添加到文件夹中。

让我们来看看这些资产,特别是图形,这样我们在使用它们在我们的 C++代码中时可以更好地可视化发生了什么。

图形资产构成了我们的《伐木者!!!》游戏屏幕的各个部分。看一看这些图形资产,就能清楚地知道它们在我们的游戏中将被使用在哪里。

探索资产

声音文件都是格式。这些文件包含了我们在游戏中特定事件播放的音效。它们都是用 BFXR 生成的。它们包括:

  • :一种像斧头(复古斧头)砍树的声音
  • :一种有点像复古“失败”声音的声音。
  • :当玩家因时间耗尽而失败时播放,而不是被压扁

在我们进行实际的 C++编码之前,让我们谈谈坐标。我们在监视器上看到的所有图像都是由像素组成的。像素是一小点光,它们组合在一起形成我们看到的图像。

有许多不同的监视器分辨率,但是举个例子,一个相当典型的游戏玩家的监视器可能在水平上有 1920 个像素,在垂直上有 1080 个像素。

像素从屏幕的左上角开始编号。正如你从下一个图表中看到的,我们的 1920 x 1080 的示例在水平(x)轴上从 0 到 1919,垂直(y)轴上从 0 到 1079 编号。

理解屏幕和内部坐标

因此,特定而准确的屏幕位置可以通过 x 和 y 坐标来确定。我们通过在屏幕的特定位置绘制游戏对象,比如背景、角色、子弹和文本,来创建我们的游戏。这些位置由像素的坐标来确定。看一看下面这个假设性的例子,我们可能在屏幕的中心坐标,大约在 960, 540 的位置绘制。

理解屏幕和内部坐标

除了屏幕坐标,我们的游戏对象也将有自己类似的坐标系统。与屏幕坐标系统一样,它们的内部本地坐标从左上角的 0,0 开始。

在上一个屏幕截图中,我们可以看到角色的 0,0 点被绘制在屏幕的 960, 540 位置。

视觉上,2D 游戏对象,比如角色或者僵尸,被称为精灵。精灵通常由图像文件制作而成。所有精灵都有所谓的原点

如果我们在屏幕的特定位置绘制一个精灵,原点将位于这个特定位置。精灵的 0,0 坐标就是原点。下一个屏幕截图演示了这一点。

理解屏幕和内部坐标

这就是为什么在显示角色绘制到屏幕的截图中,尽管我们在中心位置(960, 540)绘制了图像,它看起来有点偏右和向下的原因。

在我们进行第一个项目时,我们只需要牢记这是如何工作的。

请注意,在现实世界中,玩家有各种各样的屏幕分辨率,我们的游戏需要尽可能适应其中的许多。在第二个项目中,我们将看到如何使我们的游戏动态适应几乎任何分辨率。在这个第一个项目中,我们需要假设屏幕分辨率是 1920 x 1080。如果你的屏幕分辨率与此不同,不要担心,因为我为 Timber!!!游戏的每一章提供了单独的代码。这些代码文件几乎是相同的,只是在开头添加和交换了一些代码行。如果你有较低分辨率的屏幕,那么只需按照假设 1920 x 1080 分辨率的书中的代码进行操作,当试玩游戏时,你可以从每一章的文件夹中复制和粘贴代码文件。实际上,一旦在本章中添加了额外的代码行,无论你的屏幕分辨率如何,其余的代码都将是相同的。我为每一章提供了低分辨率的代码,只是为了方便起见。我们将在第二个项目中讨论这几行代码是如何发挥作用的(缩放屏幕)。备用代码将适用于分辨率低至 960 x 540,因此几乎可以在任何 PC 或笔记本电脑上使用。

现在我们可以编写我们的第一个 C++代码,很快我们就会看到它在运行中。

如果尚未打开 Visual Studio,请打开它,从主 Visual Studio 窗口的最近列表中左键单击打开 Timber 项目(如果尚未打开)。

我们将要做的第一件事是重命名我们的主代码文件。它目前被称为,我们将把它重命名为更合适的。代表 C++。

  1. 在右侧找到解决方案资源管理器窗口。
  2. 源文件文件夹下找到文件。
  3. 右键单击,选择重命名
  4. 编辑文件名为,然后按Enter

在代码窗口中进行一些微小的编辑,以便你的代码与下面显示的完全相同。你可以像使用任何文本编辑器或文字处理软件一样进行编辑;如果你愿意,甚至可以复制粘贴。在进行了轻微的编辑之后,我们可以讨论它们:

 
  

这个简单的 C++程序是一个很好的起点。让我们逐行来看一下

正如你所看到的,唯一需要更改的代码是顶部的一点点。第一行代码是这样的:

 
  

任何以开头的代码行都是注释,编译器会忽略它们。因此,这行代码什么也不做。它用于在以后回到代码时留下我们可能会发现有用的任何信息。注释在行尾结束,因此下一行的任何内容都不是注释的一部分。还有另一种类型的注释叫做多行c 风格注释,它可以用来留下占据多于一行的注释。我们将在本章后面看到一些这样的注释。在本书中,我将留下数百条注释,以帮助添加上下文并进一步解释代码。

现在你知道注释是用来干什么的,你可能可以猜到下一行代码是做什么的。这里再次给出:

 
  

指令告诉 Visual Studio 在编译之前包含或添加另一个文件的内容。这样做的效果是,当我们运行程序时,一些我们没有自己编写的其他代码将成为我们程序的一部分。将其他文件中的代码添加到我们的代码中的过程称为预处理,或许不足为奇的是,这是由一个叫做预处理器的东西执行的。文件扩展名代表头文件。

你可能想知道这段代码会做什么?文件实际上包含了更多的指令。它将我们程序所需的所有必要代码添加到我们的程序中,以便在 Windows 上运行我们的程序。我们永远不需要看到这个文件,绝对不需要关心它里面有什么。我们只需要在我们制作的每个游戏的顶部添加一行代码。

对我们来说更重要和相关的是,值得讨论指令的原因是,我们将在代码文件的顶部添加许多指令。这是为了包含我们将使用和费力理解的代码。

我们将包含的主要文件是 SFML 头文件,它为我们提供了所有酷炫的游戏编码功能。我们还将使用来访问C++标准库头文件。这些头文件为我们提供了访问 C++语言核心功能的权限。

这是两行解决了,让我们继续。

我们在代码中看到的下一行是这样的:

 
  

代码被称为类型。C++有许多类型,它们代表不同类型的数据。是整数或整数。记住这一点,我们一会儿会回来讨论它。

代码部分是随后的代码部分的名称。这段代码在开放的花括号和下一个闭合的花括号之间标出。

因此,这些花括号之间的所有内容都是的一部分。我们把这样的一段代码称为函数

每个 C++程序都有一个函数,它是整个程序执行(运行)的起点。随着我们在书中的进展,最终我们的游戏将有许多代码文件。然而,只会有一个函数,无论我们写什么代码,我们的游戏总是从函数的开放花括号内的第一行代码开始执行。

现在,不要担心跟在函数名后面的奇怪括号。我们将在第四章中进一步讨论它们:循环、数组、开关、枚举和函数-实现游戏机制,在那里我们将以全新和更有趣的方式看到函数。

让我们仔细看看函数中的一行代码。

再次看看我们的函数的全部内容:

 
  

我们可以看到,在中只有一行代码,。在我们继续了解这行代码的作用之前,让我们看看它是如何呈现的。这很有用,因为它可以帮助我们准备编写易于阅读和区分的代码,与我们代码的其他部分。

首先注意到向右缩进了一个制表符。这清楚地标志着它是函数内部的一部分。随着我们的代码长度增加,我们将看到缩进我们的代码和留下空白将是保持可读性的关键。

接下来,注意一下行末的标点符号。分号告诉编译器这是指令的结束,其后的任何内容都是新的指令。我们称以分号终止的指令为。

请注意,编译器不在乎你在分号和下一条语句之间留下一个新行甚至一个空格。然而,不为每个语句开启新行将导致代码难以阅读,而完全忽略分号将导致语法错误,使得游戏无法编译或运行。

一起的一段代码,通常由其与部分的缩进表示,称为

现在你已经对函数的概念感到舒适,缩进你的代码以保持整洁,并在每个语句的末尾加上一个分号,我们可以继续找出语句实际上是做什么的。

实际上,在我们的游戏中,几乎没有做任何事情。然而,这个概念是重要的。当我们使用关键字时,无论是单独使用还是后面跟着一个值,它都是一个指示程序执行跳转/返回到最初启动函数的代码的指令。

通常启动函数的代码将是我们代码中其他地方的另一个函数。然而,在这种情况下,是操作系统启动了函数。因此,当执行时,函数退出,整个程序结束。

由于在关键字后面有一个 0,这个值也被发送到操作系统。我们可以将零的值更改为其他值,那个值将被发送回去。

我们说启动函数的代码调用函数,并且函数返回值。

你现在不需要完全掌握所有这些函数信息。这里只是介绍它是有用的。在我们继续之前,还有一个关于函数的最后一件事。还记得中的吗?那告诉编译器返回的值的类型必须是(整数/整数)。我们可以返回任何符合的值。也许是 0、1、999、6358 等等。如果我们尝试返回一个不是 int 的值,比如 12.76,那么代码将无法编译,游戏也无法运行。

函数可以返回各种不同类型的值,包括我们自己发明的类型!然而,这种类型必须以我们刚刚看到的方式告知编译器。

这些关于函数的背景信息将使我们在进展中更加顺利。

你现在可以运行游戏。通过点击 Visual Studio 快速启动栏中的本地 Windows 调试器按钮,或者使用F5快捷键来运行。

运行游戏

你将只看到一个黑屏的闪烁。这个闪烁是 C++控制台,我们可以用它来调试我们的游戏。现在我们不需要这样做。正在发生的是我们的程序启动,从的第一行开始执行,当然是,然后立即退出返回到操作系统。

现在让我们添加一些更多的代码。接下来的代码将打开一个窗口,Timber!!!最终将在其中运行。窗口将是 1920 像素宽,1080 像素高,并且将是全屏的(没有边框或标题)。

输入下面突出显示的新代码到现有代码中,然后我们将对其进行检查。在输入(或复制和粘贴)时,尝试弄清楚发生了什么:

 
  

在我们的新代码中,我们注意到的第一件事是另一个略有不同的指令。告诉预处理器包含文件夹中名为的文件夹中包含的文件的内容。

所以这行代码的作用是添加来自上述文件的代码,这使我们可以访问 SFML 的一些功能。当我们开始编写自己的独立代码文件并使用来使用它们时,它的实现方式将变得更加清晰。

如果你想知道预处理器指令中包含文件名的和之间的区别,是用于我们文件夹结构中包含的文件,比如 SFML 文件或我们自己编写的任何文件。是用于包含在 Visual Studio 中的文件。此外,文件扩展名只是文件的更加面向 C++的版本,而文件更像是 C 风格的扩展名。这两种风格和文件扩展名最终都会做同样的事情,并且在我们的游戏中都能正常工作。

目前重要的是,我们有一大堆新的功能由 SFML 提供,可供使用。下一行是。我们将在几段时间内回到这行代码的作用。

随着我们继续阅读本书,我们将更全面地讨论面向对象编程、类和对象。接下来是最简短的介绍,以便我们能够理解发生了什么。

我们已经知道 OOP 代表面向对象编程。OOP 是一种编程范式,一种编码方式。OOP 通常被全球范围内的编程界所接受,在几乎每种语言中,作为编写代码的最佳、如果不是唯一的专业方式。

面向对象编程引入了许多编码概念,但它们所有的基础都是对象。当我们编写代码时,我们希望尽可能地编写可重用的代码。我们这样做的方式是将我们的代码结构化为一个类。我们将在第六章中学习如何做到这一点:面向对象编程,类和 SFML 视图

目前我们只需要知道关于类的一切,一旦我们编写了我们的类,我们不仅仅执行该代码作为游戏的一部分,而是创建可用的对象从类中。

例如,如果我们想要一百个僵尸非玩家角色NPCs),我们可以仔细设计和编写一个名为的类,然后从这个单个类中创建任意数量的僵尸对象。每个僵尸对象都具有相同的功能和内部数据类型,但每个僵尸对象都是一个独立的实体。

进一步以假设的僵尸示例为例,但不显示任何类的代码,我们可以像这样创建一个基于类的新对象:

 
  

现在,对象是一个完全编码和功能的对象。然后我们可以这样做:

 
  

现在我们有五个独立的僵尸,但它们都是基于一个精心编写的类。在我们回到刚刚编写的代码之前,让我们再进一步。我们的僵尸可以包含行为(由函数定义)以及可能代表僵尸健康、速度、位置或行进方向等事物的数据。例如,我们可以编写我们的类,使我们能够像这样使用我们的僵尸对象:

 
  

再次注意,所有这些僵尸代码目前都是假设的。不要将这些代码输入 Visual Studio;它只会产生一堆错误。

我们将设计我们的类,以便我们可以以最合适的方式使用数据和行为来满足我们游戏的目标。例如,我们可以设计我们的类,以便我们可以在创建每个僵尸对象时为数据分配值。

也许我们需要在创建每个僵尸时分配一个唯一的名称和以米每秒为单位的速度。类的仔细编码可以使我们编写这样的代码:

 
  

重点是类几乎是无限灵活的,一旦我们编写了类,我们就可以通过创建对象来使用它们。正是通过类和我们从中创建的对象,我们将利用 SFML 的强大功能。是的,我们还将编写我们自己的类,包括一个类。

让我们回到我们刚刚编写的真正代码。

在我们继续更仔细地查看和之前,您可能已经猜到,这些都是 SFML 提供的类,我们将学习这行代码的作用。

当我们创建一个类时,我们是在一个命名空间中创建的。我们这样做是为了区分我们编写的类和其他人编写的类。考虑一下类。在 Windows 等环境中,完全有可能有人已经编写了一个名为的类。通过使用命名空间,我们和 SFML 程序员可以确保类的名称永远不会冲突。

使用类的完整方式如下:

 
  

代码使我们可以在代码中的任何地方省略前缀。如果没有它,在这个简单的游戏中将会有超过 100 个的实例。它还使我们的代码更易读,同时也更短。

在函数中,我们现在有两个新的注释和两行新的实际代码。第一行实际代码是这样的:

 
  

这段代码创建了一个名为的对象,从名为的类中创建,并设置了内部值和。这些值代表玩家屏幕的分辨率。

下一行新的代码是这样的:

 
  

在前一行代码中,我们正在从 SFML 提供的名为的类中创建一个名为的新对象。此外,我们正在设置窗口对象内部的一些值。

首先,对象用于初始化的一部分。起初这可能看起来令人困惑。然而,请记住,类可以像其创建者想要的那样多样化和灵活。是的,有些类可以包含其他类。

此时不必完全理解这是如何工作的,只要您能理解这个概念就可以了。我们编写一个类,然后从该类中创建可用的对象。有点像建筑师可能会绘制蓝图。您当然不能把所有家具、孩子和狗都搬进蓝图中;但您可以根据蓝图建造一座房子(或者多座房子)。在这个类比中,类就像蓝图,对象就像房子。

接下来,我们使用值 Timber!!!来给窗口命名。我们使用预定义的值来使我们的对象全屏显示。

是 SFML 中定义的一个值。这样做是为了我们不需要记住内部代码用来表示全屏的整数。这种类型的值的编码术语是。常量及其近亲 C++中的变量将在下一章中介绍。

让我们看看我们的窗口对象在运行中的样子。

在这一点上,您可以再次运行游戏。您会看到一个更大的黑屏一闪而过。这就是我们刚刚编写的 1920 x 1080 全屏窗口。不幸的是,我们的程序仍然是从的第一行开始执行,创建了一个很酷的新游戏窗口,然后到达,立即退出到操作系统。

我们需要一种方法来保持程序运行,直到玩家想要退出。同时,随着我们在 Timber!!!中的进展,我们应该清楚地标出代码的不同部分将放在哪里。此外,如果我们要阻止游戏退出,我们最好提供一种让玩家在准备好退出时退出的方法。否则游戏将永远进行下去!

添加高亮代码,放入现有代码中,然后我们将一起讨论它们:

 
  

在新代码中,我们看到的第一件事是:

 
  

在新代码中,我们看到的最后一件事是一个闭合的。我们创建了一个循环。在循环的开头和结尾之间的所有内容将会一遍又一遍地执行,可能会永远执行下去。

仔细看一下下一个代码中突出显示的循环的括号之间的部分:

 
  

这段代码的完整解释将等到我们在第四章讨论循环和条件时再说:循环、数组、开关、枚举和函数-实现游戏机制。现在重要的是,当对象被设置为关闭时,代码的执行将跳出循环并进入下一个语句。窗口如何关闭将很快涵盖。

下一个声明当然是,这结束了我们的游戏。

现在我们知道我们的循环会快速循环执行其中的代码,直到我们的窗口对象被设置为关闭。

在 while 循环内部,我们看到了乍一看可能有点像 ASCII 艺术的东西:

 
  

ASCII 艺术是一种利用计算机文本创建图像的小众但有趣的方式。您可以在这里阅读更多信息: .

先前的代码只是另一种类型的注释。这种注释被称为 C 风格注释。注释以开头,以结尾。中间的任何内容只是用于信息,不会被编译。我使用了这种略微复杂的文本,以确保清楚地表明我们将在代码文件的这部分做什么。当然,您现在可以推断出接下来的任何代码都将与处理玩家的输入有关。

跳过几行代码,您会看到我们有另一个 C 风格的注释,宣布在代码的这部分,我们将更新场景。

跳到下一个 C 风格的注释,很明显我们将在那里绘制所有的图形。

尽管这个第一个项目使用了最简单的游戏循环版本,但每个游戏都需要在代码中经历这些阶段:

  1. 获取玩家的输入(如果有)。
  2. 根据人工智能、物理或玩家的输入更新场景。
  3. 绘制当前场景。
  4. 以足够快的速度重复以上步骤,以创建一个平滑和动画的游戏世界。

现在让我们看看实际在游戏循环中执行的代码。

首先,在标记为的部分中,我们有以下代码:

 
  

这段代码检查当前是否按下了Escape键。如果是,突出显示的代码使用对象关闭自身。现在,下一次循环开始时,它将看到对象已关闭,并跳到循环的结束大括号后面的代码,游戏将退出。我们将在第二章更全面地讨论语句:变量、运算符和决策-动画精灵

目前在部分没有代码,所以让我们继续到部分。

我们要做的第一件事是使用以下代码擦除先前的动画帧:

 
  

现在我们要做的是绘制游戏中的每一个对象。然而,目前我们没有任何游戏对象。

下一行代码是:

 
  

当我们绘制所有游戏对象时,我们将它们绘制到一个隐藏的表面上,准备好显示。代码从先前显示的表面翻转到新更新的(先前隐藏的)表面。这样,玩家永远不会看到绘图过程,因为表面上添加了所有精灵。它还保证了在翻转之前场景将会完整。这可以防止图形故障,称为撕裂。这个过程称为双缓冲

还要注意,所有这些绘制和清除功能都是使用我们的对象执行的,该对象是从 SFML 的类创建的。

运行游戏,您将得到一个空白的全屏窗口,直到您按下Esc键。

最后,我们将在游戏中看到一些真正的图形。我们需要做的是创建一个精灵。我们将创建的第一个精灵将是游戏背景。然后我们可以在清除窗口和显示/翻转窗口之间绘制它。

SFML 的类允许我们创建对象来处理游戏窗口所需的所有功能。

现在我们将探索另外两个 SFML 类,它们将负责在屏幕上绘制精灵。其中一个类,也许不足为奇的是,被称为。另一个类被称为。纹理是存储在图形处理单元GPU)上的图形。

从类创建的对象需要从类创建的对象才能将自己显示为图像。添加以下突出显示的代码。尝试弄清楚发生了什么。然后我们将逐行解释:

 
  

首先,我们从 SFML 的类创建一个名为的对象。

 
  

完成后,我们可以使用对象从我们的文件夹加载图形到中,就像这样:

 
  

我们只需要指定作为路径,因为路径是相对于我们创建文件夹并添加图像的 Visual Studio 工作目录的。

接下来,我们使用以下代码从 SFML 的类创建一个名为的对象:

 
  

然后,我们可以将纹理对象与精灵对象关联起来,就像这样:

 
  

最后,我们可以将对象定位在对象的坐标处:

 
  

图形在文件夹中的尺寸为 1920 像素宽,1080 像素高,它将完全填满整个屏幕。只是请注意,这行代码并不实际显示精灵,它只是设置好位置,以便在显示时使用。

现在,对象可以用来显示背景图形。当然,您几乎肯定想知道为什么我们不得不以这种复杂的方式做事。原因是因为显卡和 OpenGL 的工作方式。

纹理占用图形内存,而这种内存是有限的资源。此外,将图形加载到 GPU 内存中的过程非常缓慢。并不是缓慢到可以看到它发生,或者在发生时会明显减慢您的 PC,但足够缓慢,以至于您无法在游戏循环的每一帧中都这样做。因此,将实际纹理与在游戏循环期间我们将操纵的任何代码分离开来是有用的。

当我们开始移动我们的图形时,您将会看到我们将使用精灵。任何从类创建的对象都将愉快地停留在 GPU 上,只等待一个关联的对象告诉它们在哪里显示自己。在以后的项目中,我们还将重复使用相同的对象与多个不同的对象,这样可以有效地利用 GPU 内存。

总之:

  • 纹理加载到 GPU 上非常缓慢
  • 一旦纹理存储在 GPU 上,访问速度非常快
  • 我们将精灵对象与纹理关联起来
  • 我们通常在“更新场景”部分操纵精灵对象的位置和方向。
  • 我们绘制对象,然后显示与其关联的纹理(通常在“绘制场景”部分)。

所以现在我们需要做的就是使用我们的对象提供的双缓冲系统来绘制我们的新对象(),然后我们实际上应该能够看到我们的游戏在运行。

最后,我们需要在游戏循环中的适当位置绘制该精灵及其相关纹理。

请注意,当我展示的代码都来自同一个块时,我不添加缩进,因为这会减少书中文本的换行次数。缩进是暗示的。请查看下载包中的代码文件,以查看缩进的完整用法。

添加突出显示的代码:

 
  

新的代码行只是使用对象来绘制对象,在清除显示并显示新绘制的场景之间。

现在运行程序,您将看到我们正在进行真正的游戏的第一个迹象。

运行游戏

它目前还不能在 Steam 上获得绿光,但至少我们已经在路上了!

让我们看看本章可能出现的一些问题,以及随着书的进行我们将继续进行的工作。

每个项目都会出现问题和错误,这是肯定的!问题越棘手,解决它时就会越令人满意。经过数小时的挣扎后,一个新的游戏功能终于实现,会让人真正兴奋。如果没有挣扎,它可能会变得不那么值得。

在本书的某个时候,可能会遇到一些困难。保持冷静,相信自己能够克服它,然后开始工作。

请记住,无论您遇到什么问题,您都极不可能是世界上第一个遇到同样问题的人。想出一个简洁的句子来描述您的问题或错误,然后在 Google 中输入。您会惊讶地发现,有人很快、准确地解决了您的问题,而且经常会有人已经为您解决了问题。

话虽如此,在这里有一些提示(双关语;请参阅第八章:指针、标准模板库和纹理管理),以便在您努力使本章工作时帮助您入门。

本章中问题最有可能的原因是配置错误。您可能已经在设置 Visual Studio、SFML、项目模板和项目本身的过程中注意到,有很多文件名、文件夹和设置需要完全正确。只要有一个错误的设置,就可能导致多种错误,其中文本并没有清楚地说明出了什么问题。

如果您无法使从模板创建可重用模板部分中的黑屏空项目运行起来,可能更容易重新开始该部分。确保所有的文件名和文件夹适合于您特定的设置,然后让代码的最简单部分运行起来(屏幕闪烁黑色然后关闭的部分)。如果您能够达到这个阶段,那么配置可能不是问题所在。

编译错误可能是我们未来经历的最常见的错误。检查您的代码是否与我的相同,特别是行尾的分号和类和对象名称的大小写的微妙变化。如果一切都失败了,打开下载包中的代码文件并复制粘贴。虽然书中可能存在代码拼写错误,但代码文件是从实际工作的项目中制作的 - 它们绝对有效!

链接错误很可能是由于缺少 SFML 的文件造成的。您是否将它们全部复制到了从模板创建项目的项目文件夹中?

当您的代码工作时发生错误时,这就是错误。调试实际上可以很有趣。您消灭的错误越多,您的游戏就会越好,您一天的工作就会越令人满意。解决错误的诀窍是尽早找到它们!为此,我建议每次实现新功能时都运行和玩游戏。您越早发现错误,原因就越可能新鲜在您的脑海中。在本书中,我们将在每个可能的阶段运行代码以查看结果。

以下是一些可能会让你困惑的问题:

Q)我对目前呈现的内容感到困难。我适合编程吗?

A)设置开发环境并理解 OOP 作为一个概念可能是你在这本书中做的最艰难的事情。只要你的游戏正常运行(绘制背景),你就可以继续进行下一章。

Q)所有关于面向对象编程(OOP)、类和对象的讨论都太多了,有点破坏了整个学习体验。

A)别担心。我们会不断地回到面向对象编程、类和对象。在第六章:面向对象编程、类和 SFML 视图中,我们将真正开始掌握整个面向对象编程的东西。你现在需要理解的是,SFML 已经编写了大量有用的类,我们可以通过从这些类创建可用对象来使用这些代码。

Q)我真的不懂这个函数的东西。

A)没关系,我们会再次回到这个问题,并且会更彻底地学习函数。你只需要知道,当一个函数被调用时,它的代码被执行,当它完成时(达到语句),程序会跳回调用它的代码。

那是一个相当具有挑战性的章节,也许我让它变得如此苛刻了一点。配置 IDE 以使用 C++库可能有点棘手和耗时。同时,众所周知,对于编程新手来说,类和对象的概念可能有点棘手。

现在我们已经到了这个阶段,我们可以完全专注于 C++、SFML 和游戏。随着章节的进展,我们将学习更多的 C++,以及如何实现越来越有趣的游戏功能。在这个过程中,我们将进一步研究诸如函数、类和对象之类的东西,以帮助更好地揭开它们的神秘面纱。接下来,我们将学习所有绘制更多精灵并对它们进行动画处理所需的 C++知识。

在本章中,我们将在屏幕上进行更多的绘图,为了实现这一点,我们需要学习一些 C++的基础知识。

这里有什么:

  • 学习所有关于 C++变量的知识
  • 了解如何操作变量中存储的值
  • 添加一个静态树,准备好供玩家砍伐
  • 绘制和动画一个蜜蜂和三朵云

变量是我们的 C++游戏存储和操作值的方式。如果我们想知道玩家有多少生命值,那么我们就需要一个变量。也许你想知道当前波中还剩下多少僵尸?那也是一个变量。如果您需要记住获得特定高分的玩家的名字,你猜对了,我们也需要一个变量。游戏结束了还是还在进行?是的,那也是一个变量。

变量是内存中位置的命名标识符。因此,我们可以将一个变量命名为,该变量可以指向存储表示当前波中剩余僵尸数量的值的内存位置。

计算机系统寻址内存位置的方式是复杂的。编程语言使用变量以人性化的方式管理我们在内存中的数据。

我们对变量的简要讨论意味着必须有不同类型的变量。

C++有各种各样的变量类型(请参阅有关变量的下一个提示)。很容易花一个整章的时间来讨论它们。接下来是本书中最常用的类型的表格。然后我们将看看如何实际使用每种变量类型。

类型 值的示例 解释 Int ,,,,等等。 整数整数。 Float ,, 浮点值,精度高达 7 位数字。 Double , 浮点值,精度高达 15 位数字。 Char ,,,,,(包括,,等共 128 个符号) ASCII 表中的任何符号(请参阅有关变量的下一个提示)。 Bool 真或假 Bool 代表布尔值,只能是或。 String 大家好!我是一个字符串。 从单个字母或数字到整本书的任何文本值。

编译器必须告诉变量是什么类型,以便为其分配正确的内存量。对于您使用的每个变量,使用最佳和最合适的类型是一个良好的实践。然而,在实践中,您通常可以提升一个变量。也许您只需要一个具有五个有效数字的浮点数?如果您将其存储为,编译器不会抱怨。然而,如果您尝试将或存储在中,它将更改/转换值以适应。随着我们在书中的进展,我将澄清在每种情况下使用的最佳变量类型是什么,我们甚至会看到一些有意转换/转换变量类型的情况。

在上面的表中,还有一些额外的细节值得注意,包括所有值旁边的后缀。这个告诉编译器该值是类型而不是。没有前缀的浮点值被假定为。有关此内容的更多信息,请参阅有关变量的下一个提示。

如前所述,还有许多其他类型。如果您想了解更多关于类型的信息,请参阅有关变量的下一个提示。

有时我们需要确保一个值永远不会被改变。为了实现这一点,我们可以使用关键字声明和初始化一个常量

 
  

习惯上,常量的声明都是大写的。前面常量的值永远不能被改变。我们将在第四章中看到一些常量的实际应用:循环、数组、开关、枚举和函数 - 实现游戏机制

用户定义的类型比我们刚刚看到的类型要先进得多。当我们在 C++中谈论用户定义的类型时,通常是指类。我们在上一章中简要讨论了类及其相关对象。我们可以在一个单独的文件中编写代码,有时甚至是在两个单独的文件中。然后我们将能够声明、初始化和使用它们。我们将把如何定义/创建我们自己的类型留到第六章:面向对象编程、类和 SFML 视图

到目前为止,我们知道变量用于存储游戏中需要的数据/值。例如,一个变量可以表示玩家拥有的生命值或玩家的姓名。我们还知道这些变量可以表示各种不同类型的值,比如、、等。当然,我们还没有看到如何实际使用变量。

创建和准备新变量有两个阶段。这两个阶段称为声明初始化

我们可以在 C++中这样声明变量:

 
  

现在我们已经用有意义的名称声明了变量,我们可以用适当的值初始化这些变量,就像这样:

 
  

当适合我们时,我们可以将声明和初始化步骤合并为一步:

 
  

变量提示正如承诺的那样,这是关于变量的提示。如果你想看到完整的 C++类型列表,那么请查看这个网页:。如果你想深入讨论浮点数、双精度和后缀,那么请阅读这篇文章:。如果你想了解 ASCII 字符代码的方方面面,那么这里有更多信息:。请注意,这些链接是给好奇的读者的,我们已经讨论了足够的内容以便继续进行。

我们已经看到了如何声明和初始化一些 SFML 定义的类型的示例。由于我们可以创建/定义这些类型(类)的方式非常灵活,因此我们声明和初始化它们的方式也是多种多样的。以下是前一章中关于声明和初始化用户定义的类型的一些提醒。

创建一个类型为的对象,名为,并用两个值和进行初始化:

 
  

创建一个类型为的对象,名为,但不进行任何初始化:

 
  

请注意,即使我们没有建议使用哪些特定值来初始化,某些变量可能已在内部设置。对象是否需要/具有在此时给出初始化值的选项完全取决于类的编码方式,几乎是无限灵活的。这进一步表明,当我们开始编写自己的类时,会有一些复杂性。幸运的是,这也意味着我们将有重大的权力来设计我们的类型/类,使它们正是我们需要的来制作我们的游戏!将这种巨大的灵活性添加到 SFML 设计的类中,我们的游戏的潜力几乎是无限的。

在本章中,我们还将看到 SFML 提供的一些用户创建的类型/类,以及本书中的更多内容。

到目前为止,我们确切地知道了变量是什么,主要类型是什么,以及如何声明和初始化它们,但我们仍然不能做太多事情。我们需要操作我们的变量,加上它们,减去它们,乘以它们,除以它们,并测试它们。

首先,我们将处理如何操作它们,稍后我们将看看我们如何以及为什么测试它们。

为了操作变量,C++有一系列算术运算符赋值运算符。幸运的是,大多数算术和赋值运算符使用起来相当直观,而那些不直观的则很容易解释。为了让我们开始,让我们先看一张算术运算符表,然后是一张我们将在本书中经常使用的赋值运算符表:

算术运算符 解释 加法运算符可用于将两个变量或值的值相加。 减法运算符可用于从另一个变量或值中减去一个变量或值的值。 乘法运算符可以将变量和值的值相乘。 除法运算符可以除以变量和值的值。 取模运算符将一个值或变量除以另一个值或变量,以找到操作的余数。

现在是赋值运算符的时候了:

赋值运算符 解释 我们已经见过这个了。这是 赋值运算符。我们用它来初始化/设置变量的值。 将右侧的值加到左侧的变量上。 从左侧的变量中减去右侧的值。 将右侧的值乘以左侧的变量。 将右侧的值除以左侧的变量。 递增运算符;将变量加 1 递减运算符;从变量中减去 1

从技术上讲,除了和之外,上述所有运算符都被称为复合赋值运算符,因为它们包含多个运算符。

现在我们已经看到了一系列算术和赋值运算符,我们实际上可以看到如何通过组合运算符、变量和值来操作我们的变量形成表达式

表达式是变量、运算符和值的组合。使用表达式,我们可以得出一个结果。此外,正如我们很快将看到的那样,我们可以在测试中使用表达式。这些测试可以用来决定我们的代码接下来应该做什么。首先,让我们看一些可能在游戏代码中看到的简单表达式:

 
  

或者

 
  

看一下加法运算符,与赋值运算符一起使用:

 
  

或者

 
  

请注意,在运算符的两侧使用相同的变量是完全可以接受的。

看一下减法运算符与赋值运算符的结合。下面的代码从减法运算符右侧的值中减去左侧的值。它通常与赋值运算符一起使用,例如:

 
  

或者

 
  

这是我们可能使用除法运算符的方式。下面的代码将左边的数字除以右边的数字。同样,它通常与赋值运算符一起使用,如下所示:

 
  

或者

 
  

显然,在前面的例子中,变量需要是类型,以准确存储这样的计算结果。

也许并不令人惊讶,我们可以像这样使用乘法运算符:

 
  

或者

 
  

顺便说一下,你是否曾经想过 C++是怎么得到它的名字的?C++是 C 语言的扩展。它的发明者 Bjarne Stroustrup 最初称其为C with classes,但名称发生了变化。如果您感兴趣,请阅读 C++的故事:。

现在,让我们看看增量运算符的运行情况。这是一个非常巧妙的方法,可以将添加到我们游戏变量的值中。

看一下这段代码:

 
  

它产生了与这段代码相同的结果:

 
  

递减运算符,你猜对了,是从某个数值中减去的一个非常巧妙的方法:

 
  

这与这个是一样的:

 
  

让我们看看一些操作符的运行情况,然后我们可以继续构建 Timber!!!游戏:

 
  

现在是时候向我们的游戏添加一些更多的精灵了。

首先我们将添加一棵树。这将非常容易。之所以容易是因为树不会移动。我们将使用与我们在上一章绘制背景时完全相同的过程。

添加下面突出显示的代码。注意未突出显示的代码,这是我们已经编写的代码。这应该帮助您确定新代码应该在设置背景位置之后立即输入,但在主游戏循环开始之前。在您添加新代码之后,我们将回顾新代码的实际情况:

 
  

我们刚刚添加的五行代码(不包括注释)做了以下事情:

  • 首先,我们创建了一个名为的类型对象。
  • 接下来,我们从图形文件中将图形加载到纹理中。
  • 接下来,我们声明了一个名为的类型对象。
  • 现在,我们将与关联起来。每当我们绘制时,它将显示纹理,这是一个漂亮的树形图形。
  • 最后,我们使用 x 轴上的坐标和 y 轴上的坐标 0 设置了树的位置。

让我们继续处理蜜蜂对象,这几乎是以相同的方式处理的。

下一个代码与树代码之间的差异很小但很重要。由于蜜蜂需要移动,我们还声明了两个与蜜蜂相关的变量。在所示的位置添加突出显示的代码,并看看我们如何使用变量和:

 
  

我们创建蜜蜂的方式与我们创建背景和树的方式完全相同。我们使用和,并将两者关联起来。请注意,在以前的蜜蜂代码中,有一些我们以前没有见过的新代码。有一个用于确定蜜蜂是否活动的变量。请记住,变量可以是或。我们暂时将初始化为。

接下来,我们声明一个名为的新变量。这将保存我们的蜜蜂在屏幕上飞行的速度,以像素为单位每秒。

很快我们将看到如何使用这两个新变量来移动蜜蜂。在我们这样做之前,让我们以几乎相同的方式设置一些云。

添加下面显示的突出显示的代码。研究新代码,尝试弄清楚它将做什么:

 
  

我们刚刚添加的代码中唯一有点奇怪的是,我们只有一个类型的对象。多个对象共享一个纹理是完全正常的。一旦存储在 GPU 内存中,它就可以与对象快速关联。只有在代码中加载图形的初始操作相对较慢。当然,如果我们想要三个不同形状的云,那么我们就需要三个纹理。

除了轻微的纹理问题,我们刚刚添加的代码与蜜蜂相比并没有什么新的。唯一的区别是有三个云精灵,三个用于确定每朵云是否活动的变量和三个用于保存每朵云速度的变量。

最后,我们可以通过在绘图部分添加这个突出显示的代码将它们全部绘制到屏幕上:

 
  

绘制三朵云、蜜蜂和树的方式与绘制背景的方式完全相同。然而,请注意我们绘制不同对象到屏幕的顺序。我们必须在背景之后绘制所有图形,否则它们将被覆盖,而且我们必须在树之前绘制云,否则它们在树前飘来飘去会看起来有点奇怪。蜜蜂无论在树前还是树后看起来都可以。我选择在树前画蜜蜂,这样它就可以试图分散我们的伐木工的注意力,有点像真正的蜜蜂可能会做的。

运行 Timber!!!并对树、三朵云和一只蜜蜂感到敬畏,它们什么都不做!它们看起来像是在为比赛排队,蜜蜂倒着飞。

绘制树、蜜蜂和云

利用我们对运算符的了解,我们可以尝试移动我们刚刚添加的图形,但有一些问题。首先,真实的云和蜜蜂以不均匀的方式移动。它们没有固定的速度或位置。尽管它们的位置和速度是由风速或蜜蜂可能的匆忙程度等因素决定的,但对于一般观察者来说,它们所采取的路径和速度似乎是随机的。

随机数在游戏中有很多用途。也许你可以用它们来确定玩家得到的是什么牌,或者从敌人的健康中减去多少范围内的伤害。正如暗示的那样,我们将使用随机数来确定蜜蜂和云的起始位置和速度。

为了生成随机数,我们需要使用更多的 C++函数,确切地说是两个。现在不要向游戏中添加任何代码。让我们只看一下语法和一些假设代码所需的步骤。

计算机实际上不能选择随机数。它们只能使用算法/计算来选择一个看起来是随机的数字。为了使这个算法不断返回相同的值,我们必须种子随机数生成器。种子可以是任何整数,尽管每次需要一个唯一的随机数时,它必须是一个不同的种子。看一下这段代码,它种子了随机数生成器:

 
  

上面的代码使用函数从 PC 获取时间,就像这样。对函数的调用被封装为要发送到函数的值。其结果是当前时间被用作种子。

由于略显不寻常的语法,前面的代码看起来有点复杂。这样做的目的是将从返回的值转换/转型为。在这种情况下,这是函数所必需的。

从一种类型转换为另一种类型称为转换

因此,总结一下,前一行代码发生了什么:

  • 它使用获取时间
  • 它将其转换为类型
  • 它将这个结果值发送给,用于生成随机数

当然,时间是不断变化的。这使得函数成为种子随机数生成器的好方法。然而,想想如果我们多次并且在很短的时间内种子随机数生成器,以至于返回相同的值会发生什么?当我们给云动画时,我们将看到并解决这个问题。

在这个阶段,我们可以创建一个在范围内的随机数,并将其保存到一个变量中以备后用:

 
  

注意我们分配一个值给的奇怪方式。通过使用取模运算符和值,我们要求在将返回的数字除以后得到余数。当你除以时,你可能得到的最大数字是 99。最小的可能数字是 0。因此,前面的代码将生成一个在 0 到 99 之间的数字。这个知识对于为我们的蜜蜂和云生成随机速度和起始位置将非常有用。

我们很快就会做到这一点,但我们首先需要学习如何在 C++中做出决定。

C++的和关键字是让我们做决定的关键。实际上,在上一章中,当我们在每一帧中检测到玩家是否按下了Esc键时,我们已经看到了的作用:

 
  

到目前为止,我们已经看到了如何使用算术和赋值运算符来创建表达式。现在我们可以看到一些新的运算符。

逻辑运算符将通过构建可以测试为真或假的表达式来帮助我们做出决定。起初,这可能看起来像是一个非常狭窄的选择,不足以满足高级 PC 游戏中可能需要的选择。一旦我们深入挖掘,我们将看到我们实际上可以只用几个逻辑运算符就能做出所有需要的决定。

下面是一个最有用的逻辑运算符的表格。看一下它们及其相关的例子,然后我们将看看如何使用它们。

逻辑运算符 名称和例子 比较运算符测试相等性,要么为真,要么为假。例如,表达式是假的。10 显然不等于 9。 这是逻辑 运算符。表达式。这是真的,因为不等于。 这是另一个比较运算符,但与比较运算符不同。这测试是否 不相等。例如,表达式是真的。不等于。 另一个比较运算符 - 实际上还有几个。这测试某物是否大于其他某物。表达式是真的。 你猜对了。这测试小于的值。表达式是假的。 这个运算符测试一个值是否大于或等于另一个值,如果其中一个为真,结果就为真。例如,表达式是真的。表达式也是真的。 像前一个运算符一样,这个运算符测试两个条件,但这次是小于或等于。表达式是假的。表达式是真的。 这个运算符称为逻辑 。它测试表达式的两个或多个单独部分,两个部分都必须为真,结果才为真。逻辑 AND 通常与其他运算符一起用于构建更复杂的测试。表达式是真的,因为两个部分都为真,所以表达式为真。表达式是假的,因为表达式的一部分为真,另一部分为假。 这个运算符称为逻辑 ,它与逻辑 AND 类似,只是表达式的两个或多个部分中至少有一个为真,表达式才为真。让我们看看我们上面使用的最后一个例子,但用替换。表达式现在为真,因为表达式的一部分为真。

让我们来认识一下 C++的和关键字,它们将使我们能够充分利用所有这些逻辑运算符。

让我们把之前的例子变得不那么抽象。见识一下 C++的关键字。我们将使用和一些运算符以及一个小故事来演示它们的用法。接下来是一个虚构的军事情况,希望它比之前的例子更具体。

队长垂危,知道他剩下的部下经验不足,决定编写一个 C++程序,在他死后传达他的最后命令。部队必须在等待增援时守住桥的一侧。

队长想要确保他的部队理解的第一个命令是:

“如果他们过桥了,就射击他们!”

那么,我们如何在 C++中模拟这种情况呢?我们需要一个变量:。下一段代码假设变量已经被声明并初始化为或。

然后我们可以这样使用:

 
  

如果变量等于,则大括号内的代码将运行。如果不是,则程序在块之后继续运行,而不运行其中的代码。

队长还想告诉他的部队,如果敌人没有过桥就待在原地。

现在我们可以介绍另一个 C++关键字,。当的评估结果不为时,我们可以使用来明确执行某些操作。

例如,要告诉部队如果敌人没有过桥就待在原地,我们可以写下这段代码:

 
  

然后队长意识到问题并不像他最初想的那么简单。如果敌人过桥,但是人数太多怎么办?他的小队将被压垮和屠杀。所以,他想出了这段代码(这次我们也会使用一些变量。):

 
  

上面的代码有三种可能的执行路径。第一种是如果敌人从桥上过来,友军人数更多:

 
  

第二种是如果敌军正在过桥,但人数超过友军:

 
  

然后第三种可能的结果,如果其他两种都不为,则由最终的捕获,没有条件。

你能发现上述代码的一个缺陷吗?这可能会让一群经验不足的部队陷入完全混乱的状态吗?敌军和友军人数完全相等的可能性没有被明确处理,因此将由最终的处理。最终的是用于没有敌军的情况。我想任何有自尊心的队长都会期望他的部队在这种情况下战斗。他可以改变第一个语句以适应这种可能性:

 
  

最后,队长最后关心的是,如果敌人拿着白旗过桥投降,然后被立即屠杀,那么他的士兵最终会成为战争罪犯。显而易见的是需要 C++代码。使用布尔变量,他写下了这个测试:

 
  

但是放置这段代码的问题并不太清楚。最后,队长选择了以下嵌套解决方案,并将的测试更改为逻辑非,就像这样:

 
  

这表明我们可以嵌套和语句以创建相当深入和详细的决策。

我们可以继续使用和做出更复杂的决定,但我们已经看到的足够作为介绍。值得指出的是,通常解决问题的方法不止一种。通常正确的方法是以最清晰和最简单的方式解决问题。

我们正在接近拥有所有我们需要的 C++知识,以便能够为我们的云和蜜蜂制作动画。我们还有一个最后的动画问题要讨论,然后我们可以回到游戏中。

在我们移动蜜蜂和云之前,我们需要考虑时间。正如我们已经知道的,主游戏循环一遍又一遍地执行,直到玩家按下Esc键。

我们还学到了 C++和 SFML 非常快。事实上,我的老旧笔记本电脑每秒执行一个简单的游戏循环(比如当前的循环)大约有五千次。

让我们考虑一下蜜蜂的速度。为了讨论的目的,我们可以假装我们要以每秒 200 像素的速度移动它。在一个宽度为 1920 像素的屏幕上,它大约需要 10 秒才能横穿整个宽度,因为 10 乘以 200 等于 2000(接近 1920)。

此外,我们知道我们可以用来定位我们的精灵中的任何一个。我们只需要把 x 和 y 坐标放在括号里。

除了设置精灵的位置,我们还可以获取精灵的位置。例如,要获取蜜蜂的水平 x 坐标,我们将使用这段代码:

 
  

蜜蜂的当前 x 坐标现在存储在中。要将蜜蜂向右移动,我们可以将 200(我们预期的速度)除以 5000(我笔记本电脑上的近似帧率)的适当分数添加到中,就像这样:

 
  

现在我们可以使用来移动我们的蜜蜂。它将每帧平滑地从左到右移动 200 除以 5000 像素。但是这种方法有两个大问题。

帧率是我们的游戏循环每秒处理的次数。也就是说,我们处理玩家的输入、更新游戏对象并将它们绘制到屏幕上的次数。我们将在本书的其余部分扩展并讨论帧率的影响。

我的笔记本电脑上的帧率可能并不总是恒定的。蜜蜂可能看起来像是断断续续地在屏幕上加速

当然,我们希望我们的游戏能够吸引更广泛的受众,而不仅仅是我的笔记本电脑!每台 PC 的帧率都会有所不同,至少会略有不同。如果你有一台非常老旧的 PC,蜜蜂看起来会像被铅压住,如果你有最新的游戏设备,它可能会是一个模糊的涡轮蜜蜂。

幸运的是,这个问题对每个游戏来说都是一样的,SFML 提供了一个解决方案。理解解决方案的最简单方法是实施它。

现在我们将测量并使用帧率来控制我们的游戏。要开始实施这个,只需在主游戏循环之前添加这段代码:

 
  

在前面的代码中,我们声明了一个类型的对象,并将其命名为。类名以大写字母开头,对象名(我们实际使用的)以小写字母开头。对象名是任意的,但似乎是一个合适的名字,嗯,一个时钟的名字。我们很快也会在这里添加一些与时间相关的变量。

现在,在我们的游戏代码的更新部分添加这个突出显示的代码:

 
  

函数,正如你所期望的那样,重新启动时钟。我们希望每一帧都重新启动时钟,以便我们可以计算每一帧花费的时间。此外,它返回自上次我们重新启动时钟以来经过的时间。

因此,在前面的代码中,我们声明了一个类型的对象,称为,并使用它来存储函数返回的值。

现在,我们有一个名为的对象,它保存了自上次更新场景并重新启动时经过的时间。也许你能看出这是怎么回事。

让我们向游戏添加一些更多的代码,然后我们将看看我们可以用做些什么。

代表增量时间,即两次更新之间的时间。

让我们利用自上一帧以来经过的时间,为蜜蜂和云注入生命。这将解决在不同 PC 上拥有一致的帧速率的问题。

我们想要做的第一件事是在特定高度和特定速度下设置蜜蜂。我们只想在蜜蜂不活动时才这样做。因此,我们将下一个代码放在一个块中。检查并添加下面突出显示的代码,然后我们将讨论它:

 
  

现在,如果蜜蜂不活动,就像游戏刚开始时一样,将为,上面的代码将按照以下顺序执行以下操作:

  • 给随机数生成器设定种子
  • 获取一个在 199 和 399 之间的随机数,并将结果赋给
  • 再次给随机数生成器设定种子
  • 在 499 和 999 之间获取一个随机数,并将结果赋给一个名为的新的变量
  • 将蜜蜂的位置设置为 x 轴上的(刚好在屏幕右侧)和 y 轴上等于的值
  • 将设置为 true

请注意,变量是我们在游戏循环内声明的第一个变量。此外,因为它是在块内声明的,所以在块外部实际上是“不可见”的。对于我们的用途来说,这是可以接受的,因为一旦我们设置了蜜蜂的高度,我们就不再需要它了。这种影响变量的现象称为作用域。我们将在第四章中更全面地探讨这一点:循环、数组、开关、枚举和函数 - 实现游戏机制

如果我们运行游戏,蜜蜂实际上还不会发生任何事情,但现在蜜蜂是活跃的,我们可以编写一些代码,当为时运行。

添加下面突出显示的代码,可以看到,这段代码在为时执行。这是因为在块之后有一个:

 
  

在块中发生以下事情。

使用以下标准更改蜜蜂的位置。函数使用函数获取蜜蜂当前的 x 坐标。然后将添加到该坐标。

变量的值是每秒多个像素,并且是在先前的块中随机分配的。的值将是一个小于 1 的分数,表示动画上一帧的持续时间。

假设蜜蜂当前的 x 坐标是 1000。现在假设一个相当基本的 PC 以每秒 5000 帧的速度循环。这意味着将是 0.0002。再假设被设置为最大的 399 像素每秒。那么决定用于 x 坐标的值的代码可以解释如下:

 
  

因此,蜜蜂在 x 轴上的新位置将是 999.9202。我们可以看到,蜜蜂非常平稳地向左飘移,每帧不到一个像素。如果帧速率波动,那么公式将产生一个新的值来适应。如果我们在每秒只能达到 100 帧或者每秒能达到一百万帧的 PC 上运行相同的代码,蜜蜂将以相同的速度移动。

函数使用来确保蜜蜂在整个活动周期内保持完全相同的 y 坐标。

我们刚刚添加的块中的最终代码是这样的:

 
  

这段代码在每一帧(当为时)测试,蜜蜂是否已经从屏幕的左侧消失。如果函数返回小于-100,那么它肯定已经超出了玩家的视野。当发生这种情况时,被设置为,在下一帧,一个新的蜜蜂将以新的随机高度和新的随机速度飞行。

尝试运行游戏,看着我们的蜜蜂忠实地从右到左飞行,然后再次回到右侧,高度和速度都不同。几乎就像每次都是一只新的蜜蜂。

当然,真正的蜜蜂会在你专心砍树时黏在你身边,让你烦恼很久。在下一个项目中,我们将制作一些更聪明的游戏角色。

现在我们将以非常相似的方式让云移动。

我们想要做的第一件事是在特定高度和特定速度设置第一朵云。只有在云处于非活动状态时才想要这样做。因此,我们将下一个代码包装在块中。在我们为蜜蜂添加代码之后,检查并添加突出显示的代码,然后我们将讨论它。它几乎与我们用于蜜蜂的代码完全相同:

 
  

我们刚刚添加的代码与蜜蜂代码之间唯一的区别是我们使用不同的精灵并为我们的随机数使用不同的范围。此外,我们使用来对 time(0)返回的结果进行操作,以便确保每个云都得到不同的种子。当我们下一步编写其他云移动代码时,您将看到我们分别使用和。

现在我们可以在云处于活动状态时采取行动。我们将在块中这样做。与块一样,代码与蜜蜂的代码完全相同,只是所有代码都作用于云而不是蜜蜂:

 
  

现在我们知道该怎么做了,我们可以复制相同的代码用于第二和第三朵云。在第一朵云的代码之后立即添加处理第二和第三朵云的突出代码:

 
  

现在你可以运行游戏,云将随机连续地在屏幕上漂移,蜜蜂将在从右到左飞行后重新出现在右侧。

吹云

所有这些云和蜜蜂处理似乎有点重复?我们将看看如何节省大量输入并使我们的代码更易读。在 C++中,有处理相同类型的变量或对象的多个实例的方法。这些被称为数组,我们将在第四章中学习:循环、数组、开关、枚举和函数-实现游戏机制。在项目结束时,一旦我们学习了数组,我们将讨论如何改进我们的云代码。

看一看与本章主题相关的一些常见问题解答。

问:为什么我们在蜜蜂到达-100 时将其设置为非活动状态?为什么不是零,因为零是窗口的左侧?

答:蜜蜂的图形宽 60 像素,其原点位于左上像素。因此,当蜜蜂以 x 等于零的原点绘制时,整个蜜蜂图形仍然在屏幕上供玩家看到。等到它到达-100 时,我们可以确信它肯定已经超出了玩家的视野。

问:我怎么知道我的游戏循环有多快?

答:为了衡量这一点,我们需要学习更多的东西。我们将在第五章中添加测量和显示当前帧速率的功能:碰撞、声音和结束条件-使游戏可玩

在本章中,我们了解到变量是内存中的命名存储位置,我们可以在其中保存特定类型的值。类型包括、、、、和。

我们可以声明和初始化我们需要的所有变量,以存储我们游戏的数据。一旦我们有了变量,我们就可以使用算术和赋值运算符来操作它们,并使用逻辑运算符在测试中使用它们。与和关键字一起使用,我们可以根据游戏中的当前情况分支我们的代码。

利用所有这些新知识,我们制作了一些云和一只蜜蜂的动画。在下一章中,我们将继续使用这些技能,为玩家添加HUD抬头显示)并增加更多的输入选项,同时通过时间条来直观地表示时间。

在本章中,我们将花大约一半的时间学习如何操作文本并在屏幕上显示它,另一半时间将用于研究时间和视觉时间条如何在游戏中制造紧迫感。

我们将涵盖以下主题:

  • 暂停和重新开始游戏
  • C++字符串
  • SFML 文本和 SFML 字体类
  • 为 Timber!!!添加 HUD
  • 为 Timber!!!添加时间条

随着接下来三章的游戏进展,代码显然会变得越来越长。因此,现在似乎是一个很好的时机,考虑未来并在我们的代码中添加更多结构。我们将添加这种结构以使我们能够暂停和重新开始游戏。

我们将添加代码,以便在游戏首次运行时暂停。玩家将能够按下Enter键来启动游戏。然后游戏将运行,直到玩家被压扁或时间用尽。此时游戏将暂停并等待玩家按下Enter键,以重新开始。

让我们一步一步地设置这个。首先,在主游戏循环之外声明一个新的名为的变量,并将其初始化为:

 
  

现在,每当游戏运行时,我们都有一个名为的变量,它将是。

接下来,我们将添加另一个语句,其中表达式将检查Enter键当前是否被按下。如果被按下,它将将设置为。在我们其他处理键盘的代码之后添加突出显示的代码:

 
  

现在我们有一个名为的,它起初是,但当玩家按下Enter键时会变为。此时,我们必须使我们的游戏循环根据的当前值做出适当的响应。

这就是我们将要进行的步骤。我们将使用语句包装整个更新部分的代码,包括我们在上一章中编写的用于移动蜜蜂和云的代码。

请注意,在下一段代码中,只有当不等于时,块才会执行。换句话说,游戏在暂停时不会移动/更新。

这正是我们想要的。仔细看看添加新的语句以及相应的左花括号和右花括号的确切位置。如果它们放错地方,事情将不会按预期工作。

添加突出显示的代码以包装代码的更新部分,密切关注下面显示的上下文。我在一些行上添加了来表示隐藏的代码。显然,不是真正的代码,不应该添加到游戏中。您可以通过周围未突出显示的代码来确定要放置新代码(突出显示)的位置,即开头和结尾:

 
  

请注意,当您放置新的块的右花括号时,Visual Studio 会自动调整所有缩进,以保持代码整洁。

现在您可以运行游戏,直到按下Enter键之前一切都是静态的。现在可以开始为我们的游戏添加功能,只需记住当玩家死亡或时间用尽时,我们需要将设置为。

在上一章中,我们初步了解了 C++字符串。我们需要更多地了解它们,以便实现玩家的 HUD。

在上一章中,我们简要提到了字符串,并且了解到字符串可以包含从单个字符到整本书的字母数字数据。我们没有研究声明、初始化或操作字符串。所以现在让我们来做。

声明字符串变量很简单。我们声明类型,然后是名称:

 
  

一旦我们声明了一个字符串,我们就可以为它赋值。

与常规变量一样,要为字符串赋值,我们只需放置名称,然后是赋值运算符,然后是值:

 
  

注意,值需要用引号括起来。与常规变量一样,我们也可以在一行中声明和赋值:

 
  

这就是我们如何改变我们的字符串变量。

我们可以使用指令为我们的字符串提供一些额外的功能。类使我们能够将一些字符串连接在一起。当我们这样做时,它被称为连接

 
  

除了使用对象外,字符串变量甚至可以与不同类型的变量连接在一起。下面的代码开始揭示了字符串对我们可能非常有用:

 
  

运算符是一个位运算符。然而,C++允许您编写自己的类,并在类的上下文中重写特定运算符的功能。类已经这样做了,使运算符按照它的方式工作。复杂性被隐藏在类中。我们可以使用它的功能而不必担心它是如何工作的。如果你感到有冒险精神,你可以阅读关于运算符重载的内容:。为了继续项目,你不需要更多的信息。

现在我们知道了 C++字符串的基础知识,以及我们如何使用,我们可以看到如何使用一些 SFML 类来在屏幕上显示它们。

在我们实际添加代码到我们的游戏之前,让我们简要讨论一下和类以及一些假设的代码。

在屏幕上绘制文本的第一步是拥有一个字体。在第一章中,我们将一个字体文件添加到了项目文件夹中。现在我们可以将字体加载到 SFML 对象中,准备使用。

要这样做的代码看起来像这样:

 
  

在前面的代码中,我们首先声明了对象,然后加载了一个实际的字体文件。请注意,是一个假设的字体,我们可以使用项目文件夹中的任何字体。

一旦我们加载了一个字体,我们就需要一个 SFML 对象:

 
  

现在我们可以配置我们的对象。这包括大小、颜色、屏幕上的位置、包含消息的字符串,当然,将其与我们的对象关联起来:

 
  

让我们给 Timber 添加一个 HUD!!!

现在我们已经了解了足够关于字符串、SFML 和 SFML ,可以开始实现 HUD 了。

我们需要做的下一件事是在代码文件的顶部添加另一个指令。正如我们所学到的,类为将字符串和其他变量类型组合成一个字符串提供了一些非常有用的功能。

添加下面高亮代码的一行:

 
  

接下来我们将设置我们的 SFML 对象。一个将包含一条消息,我们将根据游戏状态进行变化,另一个将包含分数,并且需要定期更新。

声明和对象的下一个代码加载字体,将字体分配给对象,然后添加字符串消息、颜色和大小。这应该从我们在上一节讨论中看起来很熟悉。此外,我们添加了一个名为的新变量,我们可以操纵它来保存玩家的分数。

请记住,如果你在第一章中选择了不同的字体,你需要更改代码的部分以匹配你在文件夹中拥有的文件。

添加高亮代码,我们就可以准备好继续更新 HUD 了:

 
  

下面的代码可能看起来有点复杂,甚至复杂。然而,当你稍微分解一下时,它实际上非常简单。检查并添加新代码,然后我们将一起讨论:

 
  

我们有两个类型的对象将显示在屏幕上。我们希望将定位在左上角并留有一点填充。这并不困难;我们只需使用,它就会在左上角定位,并留有 20 像素的水平和垂直填充。

然而,定位并不那么容易。我们希望将其定位在屏幕的正中间。最初这可能看起来不是问题,但我们记得我们绘制的一切的原点都是左上角。因此,如果我们简单地将屏幕的宽度和高度除以二,并在中使用结果,那么文本的左上角将位于屏幕的中心,并且会不整齐地向右边展开。

我们需要一种方法来将的中心设置为屏幕的中心。您刚刚添加的看起来相当恶劣的代码重新定位了的原点到其自身的中心。为了方便起见,这里是当前讨论的代码:

 
  

首先,在这段代码中,我们声明了一个名为的新的类型的对象。正如其名称所示,对象保存了一个带有浮点坐标的矩形。

然后,代码使用函数来使用包装的矩形的坐标来初始化。

接下来的代码行,由于它相当长,分成了四行,使用函数将原点(我们绘制的点)更改为的中心。当然,保存了一个矩形,它完全匹配包装的坐标。然后,执行下一行代码:

 
  

现在,将被整齐地定位在屏幕的正中间。每次更改的文本时,我们将使用完全相同的代码,因为更改消息会改变的大小,因此其原点需要重新计算。

接下来,我们声明了一个名为的类型的对象。请注意,我们使用了完整的名称,包括命名空间。我们可以通过在代码文件顶部添加来避免这种语法。然而,我们没有这样做,因为我们很少使用它。看一下代码,将其添加到游戏中,然后我们可以更详细地讨论一下。由于我们只希望在游戏暂停时执行此代码,请确保将其与其他代码一起添加到块中,如下所示:

 
  

我们使用和运算符提供的特殊功能,它将变量连接到中。因此,代码的效果是创建一个包含和值的字符串,它们被连接在一起。例如,当游戏刚开始时,等于零,所以将保存值。如果发生变化,将在每一帧适应。

接下来的代码简单地显示/设置了中包含的字符串到。

 
  

现在可以绘制到屏幕上了。

接下来的代码绘制了两个对象(和),但请注意,绘制的代码包含在一个语句中。这个语句导致只有在游戏暂停时才绘制。

添加下面显示的突出代码:

 
  

现在我们可以运行游戏,看到我们的 HUD 绘制在屏幕上。您将看到SCORE = 0和 PRESS ENTER TO START!消息。当您按下Enter时,后者将消失。

添加得分和消息

如果您想要看到分数更新,请在循环中的任何位置添加临时代码。如果您添加了这行临时代码,您将看到分数迅速上升,非常快!

添加得分和消息

如果您添加了临时代码,请务必在继续之前将其删除。

由于时间是游戏中的一个关键机制,必须让玩家意识到它。他需要知道自己被分配的六秒即将用完。这将在游戏接近结束时给他一种紧迫感,并且如果他表现得足够好以保持或增加剩余时间,他会有一种成就感。

在屏幕上绘制剩余秒数并不容易阅读(当专注于分支时),也不是实现目标的特别有趣的方式。

我们需要的是一个时间条。我们的时间条将是一个简单的红色矩形,在屏幕上显眼地展示。它将从宽度开始,但随着时间的流逝迅速缩小。当玩家剩余时间达到零时,时间条将完全消失。

同时添加时间条的同时,我们将添加必要的代码来跟踪玩家剩余的时间,并在他用完时间时做出响应。让我们一步一步地进行。

从前面的声明中添加突出显示的代码:

 
  

首先,我们声明了一个类型的对象,并将其命名为。是一个适合绘制简单矩形的 SFML 类。

接下来,我们添加了一些变量,和。我们分别将它们初始化为和。这些变量将帮助我们跟踪每一帧需要绘制的大小。

接下来,我们使用函数设置的大小。我们不只是传入我们的两个新的变量。首先,我们创建一个类型的新对象。然而,这里的不同之处在于,我们没有给新对象命名。我们只是用我们的两个浮点变量初始化它,并直接传递给函数。

是一个持有两个变量的类。它还有一些其他功能,将在整本书中介绍。

之后,我们使用函数将颜色设置为红色。

我们在前面的代码中对做的最后一件事是设置它的位置。y 坐标非常直接,但我们设置 x 坐标的方式略微复杂。这里是计算:

 
  

代码首先将除以。然后将除以。最后从前者中减去后者。

结果使在屏幕上漂亮地水平居中。

我们要讨论的代码的最后三行声明了一个名为的新对象,一个名为的新,它初始化为,以及一个听起来奇怪的名为的,我们将进一步讨论。

变量是用除以初始化的。结果恰好是每秒需要缩小的像素数量。这在我们每一帧调整的大小时会很有用。

显然,我们需要在玩家开始新游戏时重置剩余时间。这样做的逻辑位置是Enter键按下。我们也可以同时将重置为零。现在让我们通过添加这些突出显示的代码来做到这一点:

 
  

现在,每一帧我们都必须减少剩余时间的数量,并相应地调整的大小。在更新部分添加以下突出显示的代码,如下所示:

 
  

首先,我们用这段代码减去了玩家剩余的时间与上一帧执行所花费的时间:

 
  

然后,我们用以下代码调整了的大小:

 
  

的 x 值是用乘以初始化的。这产生了与玩家剩余时间相关的正确宽度。高度保持不变,在没有任何操作的情况下使用。

当然,我们必须检测时间是否已经用完。现在,我们将简单地检测时间是否已经用完,暂停游戏,并更改的文本。稍后我们会在这里做更多的工作。在我们添加的先前代码之后添加突出显示的代码,我们将更详细地查看它:

 
  

逐步执行先前的代码:

  • 首先,我们用测试时间是否已经用完
  • 然后我们将设置为,这样我们的代码的更新部分将被执行的最后一次(直到玩家再次按Enter)。
  • 然后我们更改的消息,计算其新的中心以设置为其原点,并将其定位在屏幕中心。

最后,在代码的这一部分,我们需要绘制。在这段代码中,没有任何新的东西,我们以前见过很多次。只需注意我们在树之后绘制,这样它就可见。添加突出显示的代码来绘制时间条:

 
  

现在您可以运行游戏。按Enter开始,并观察时间条平稳地消失到无。

添加时间条

游戏暂停,时间用完了!!消息将出现。

添加时间条

当然,您可以再次按Enter从头开始运行整个游戏。

Q) 我可以预见,通过精灵的左上角定位有时可能会不方便。

A) 幸运的是,您可以选择使用精灵的哪个点作为定位/原点像素,就像我们使用函数设置一样。

Q) 代码变得相当长,我很难跟踪一切的位置。

A) 是的,我同意。在下一章中,我们将看到我们可以组织我们的代码的第一种方式,使其更易读。当我们学习编写 C++函数时,我们将看到这一点。此外,当我们学习关于 C++数组时,我们将学习一种处理相同类型的多个对象/变量(如云)的新方法。

在本章中,我们学习了关于字符串、SFML 和 SFML 。它们使我们能够在屏幕上绘制文本,为玩家提供了 HUD。我们还使用了,它允许我们连接字符串和其他变量来显示分数。

我们探索了 SFML 类,它正是其名称所暗示的。我们使用了类型的对象和一些精心计划的变量来绘制一个时间条,直观地显示玩家剩余的时间。一旦我们实现了砍树和移动的树枝可以压扁玩家,时间条将产生紧张感和紧迫感。

接下来,我们将学习一系列新的 C++特性,包括循环、数组、切换、枚举和函数。这将使我们能够移动树枝,跟踪它们的位置,并压扁玩家。

本章可能包含的 C++信息比书中的任何其他章节都要多。它充满了将极大地推动我们的理解的基本概念。它还将开始阐明我们一直略微忽略的一些模糊领域,例如函数和游戏循环。

一旦我们探索了整个 C++语言必需品清单,然后我们将利用我们所知道的一切来使主要游戏机制-树枝移动。在本章结束时,我们将准备进入最后阶段并完成《伐木者》。

我们将研究以下主题:

  • 循环
  • 数组
  • 使用开关进行决策
  • 枚举
  • 开始使用函数
  • 创建和移动树枝

在编程中,我们经常需要做同样的事情超过一次。到目前为止,我们看到的明显例子是我们的游戏循环。在剥离所有代码的情况下,我们的游戏循环看起来像这样:

 
  

有几种不同类型的循环,我们将看看最常用的。这种类型的循环的正确术语是循环。

循环非常简单。回想一下语句及其表达式,这些表达式评估为或。我们可以在循环的条件表达式中使用相同的运算符和变量的组合。

与语句一样,如果表达式为,则代码执行。然而,与循环相比,C++代码将继续执行,直到条件为。看看这段代码:

 
  

这是以前的代码中发生的事情。在循环之外,声明并初始化为。然后循环开始。它的条件表达式是。因此,循环将继续循环执行其主体中的代码,直到条件评估为。这意味着上面的代码将执行 100 次。

在循环的第一次通过中,等于 100,然后等于 99,然后等于 98,依此类推。但一旦等于零,当然不再大于零。然后代码将跳出循环并继续运行,在闭合大括号之后。

就像语句一样,循环可能不会执行一次。看看这个:

 
  

此外,表达式的复杂性或可以放入循环主体的代码量没有限制。考虑游戏循环的这种假设变体:

 
  

以前的循环将继续执行,直到或之一等于零。一旦发生其中一个条件,表达式将评估为,程序将从循环之后的第一行代码继续执行。

值得注意的是,一旦进入循环的主体,即使表达式在中途评估为,它也将至少完成一次,因为在代码尝试开始另一个传递之前不会再次测试。例如:

 
  

以前的循环体将执行一次。我们还可以设置一个永远运行的循环,毫不奇怪地称为无限循环。这是一个例子:

 
  

如果您觉得上面的循环令人困惑,只需字面理解。当条件为时,循环执行。嗯,总是,因此将继续执行。

我们可能会使用无限循环,以便我们可以决定何时从循环中退出,而不是在表达式中。当我们准备离开循环主体时,我们将使用关键字来做到这一点。也许会像这样:

 
  

你可能也能猜到,我们可以在 循环和其他循环类型中结合使用任何 C++ 决策工具,比如 、,以及我们即将学习的 。考虑这个例子:

 
  

我们可以花很长时间来研究 C++ 循环的各种排列,但在某个时候我们想要回到制作游戏。所以让我们继续前进,看看另一种类型的循环。

循环的语法比 循环稍微复杂一些,因为它需要三个部分来设置。先看看代码,然后我们将把它分解开来:

 
  

这是 循环条件的所有部分的作用。

声明和初始化 条件 每次迭代前更改

为了进一步澄清,这里有一个表格来解释前面 循环例子中的所有三个关键部分。

部分 描述 声明和初始化 我们创建一个新的 变量 ,并将其初始化为 0 条件 就像其他循环一样,它指的是必须为循环执行的条件 循环通过每次迭代后更改 在这个例子中, 表示每次迭代时 增加/递增 1

我们可以改变 循环来做更多的事情。下面是另一个简单的例子,从 10 开始倒数:

 
  

循环控制初始化、条件评估和控制变量。我们将在本章后面在我们的游戏中使用 循环。

如果一个变量是一个可以存储特定类型值的盒子,比如 、 或 ,那么我们可以把数组看作是一整行盒子。盒子的行可以是几乎任何大小和类型,包括类的对象。然而,所有的盒子必须是相同的类型。

在最终项目中,一旦我们学习了更高级的 C++,就可以规避在每个盒子中使用相同类型的限制。

这个数组听起来可能对我们在第二章中的云有用:变量、运算符和决策 - 动画精灵。那么我们如何创建和使用数组呢?

我们可以这样声明一个 类型变量的数组:

 
  

现在我们有一个名为 的数组,可以存储十个 值。然而,目前它是空的。

为了向数组的元素添加值,我们可以使用我们已经熟悉的类型的语法,结合一些新的语法,称为数组表示法。在下面的代码中,我们将值 存储到数组的第一个元素中:

 
  

要在第二个元素中存储值 ,我们写下这段代码:

 
  

我们可以将值 存储在最后一个元素中,如下所示:

 
  

请注意,数组的元素始终从零开始,直到数组大小减 1。与普通变量类似,我们可以操作数组中存储的值。唯一的区别是我们会使用数组表示法来做到这一点,因为虽然我们的数组有一个名字 ,但是单独的元素没有名字。

在下面的代码中,我们将第一个和第二个元素相加,并将答案存储在第三个元素中:

 
  

数组也可以与常规变量无缝交互,比如下面的例子:

 
  

我们可以快速地向元素添加值,比如这个使用 数组的例子:

 
  

现在值 , 和 分别存储在第一、第二和第三位置。请记住,使用数组表示法访问这些值时,我们将使用 [0]、[1] 和 [2]。

还有其他方法来初始化数组的元素。这个稍微抽象的例子展示了使用 循环将值 0 到 9 放入 数组中:

 
  

该代码假设 之前已经被初始化为至少包含 个 变量。

我们可以在任何常规变量可以使用的地方使用数组。例如,它们可以在表达式中使用,如下所示:

 
  

数组在游戏代码中的最大好处可能是在本节开始时暗示的。数组可以保存对象(类的实例)。假设我们有一个类,并且我们想要存储大量的。我们可以像在这个假设的例子中那样做:

 
  

数组现在保存了大量类的实例。每个实例都是一个独立的、活着的(有点),呼吸着的、自主决定的对象。然后我们可以循环遍历数组,在游戏循环的每一次通过中,移动僵尸,检查它们的头是否被斧头砍中,或者它们是否设法抓住了玩家。

如果当时我们知道数组,它们将非常适合处理我们的云。我们可以拥有任意数量的云,并且编写的代码比我们为我们的三朵微不足道的云所做的要少。

要查看完整的改进的云代码,并且看它实际运行,可以查看下载包中《伐木工》(代码和可玩游戏)的增强版本。或者您可以在查看代码之前尝试使用数组实现云。

了解所有这些数组内容的最佳方法是看它们的实际应用。当我们实现我们的树枝时,我们将会看到它们的应用。

现在我们将保留我们的云代码,以便尽快回到游戏中添加功能。但首先让我们再看一下使用进行更多 C++决策的内容。

我们已经看到了,它允许我们根据表达式的结果来决定是否执行一段代码块。有时,在 C++中做决定可能有其他更好的方法。

当我们必须基于一系列可能的结果做出决定时,其中不涉及复杂的组合或广泛的数值范围,通常情况下会使用。我们可以在以下代码中看到决策的开始:

 
  

在前面的例子中,可以是一个实际的表达式或一个变量。然后,在花括号内,我们可以根据表达式的结果或变量的值做出决定。我们可以使用和关键字来实现这一点:

 
  

在前面的抽象例子中,您可以看到,每个表示一个可能的结果,每个表示该的结束以及执行离开块的地方。

我们还可以选择使用关键字而不带值,以便在没有任何语句评估为时运行一些代码。以下是一个例子:

 
  

作为的最后一个不太抽象的例子,考虑一个复古的文本冒险游戏,玩家输入一个字母,比如、、或来向北、东、南或西移动。块可以用来处理玩家的每个可能的输入,就像我们在这个例子中看到的那样:

 
  

了解我们学到的关于的一切最好的方法是将它与我们正在学习的所有其他新概念一起应用。

枚举是逻辑集合中所有可能值的列表。C++枚举是列举事物的好方法。例如,如果我们的游戏使用的变量只能在特定范围的值中,而且这些值在逻辑上可以形成一个集合或一组,那么枚举可能是合适的。它们将使您的代码更清晰,更不容易出错。

在 C++中声明类枚举,我们使用两个关键字和,然后是枚举的名称,然后是枚举可以包含的值,用一对花括号括起来。

例如,检查这个枚举声明。请注意,按照惯例,将枚举的可能值全部大写声明是常见的。

 
  

注意,此时我们还没有声明任何的实例,只是类型本身。如果这听起来有点奇怪,可以这样想:SFML 创建了、和类,但要使用这些类中的任何一个,我们必须声明一个对象/实例。

此时我们已经创建了一个名为的新类型,但我们还没有它的实例。所以现在让我们创建它们:

 
  

接下来是对我们即将添加到 Timber!!!中的代码类型的 sneak preview。我们将想要跟踪树的哪一侧有分支或玩家,因此我们将声明一个名为的枚举,如以下示例所示:

 
  

我们可以将玩家定位在左侧,如下所示:

 
  

我们可以使分支位置数组的第四级(数组从零开始)根本没有分支,如下所示:

 
  

我们也可以在表达式中使用枚举:

 
  

我们将再看一个重要的 C++主题,然后我们将回到编写游戏的代码。

那么 C++函数到底是什么?函数是一组变量、表达式和控制流语句(循环和分支)。事实上,我们迄今为止在书中学到的任何代码都可以在函数中使用。我们编写的函数的第一部分称为签名。以下是一个示例函数签名:

 
  

如果我们添加一对大括号,里面包含一些函数实际执行的代码,那么我们就有了一个完整的函数,一个定义:

 
  

然后我们可以在代码的其他部分使用我们的新函数,如下所示:

 
  

当我们使用一个函数时,我们说我们调用它。在我们调用的地方,我们的程序的执行分支到该函数中包含的代码。函数将运行直到达到结尾或被告知。然后代码将从函数调用后的第一行继续运行。我们已经在使用 SFML 提供的函数。这里不同的是,我们将学习编写和调用我们自己的函数。

这是另一个函数的例子,包括使函数返回到调用它的代码的代码:

 
  

调用上述函数的方式可能如下所示:

 
  

显然,我们不需要编写函数来将两个变量相加,但这个例子帮助我们更深入地了解函数的工作原理。首先我们传入值和。在函数签名中,值被赋给,值被赋给。

在函数体内,变量和相加并用于初始化新变量。行就是这样。它将存储在中的值返回给调用代码,导致被初始化为值。

请注意,上面示例中的每个函数签名都有所不同。之所以如此,是因为 C++函数签名非常灵活,允许我们构建我们需要的函数。

函数签名的确切方式定义了函数必须如何被调用以及函数必须如何返回值,这值得进一步讨论。让我们给该签名的每个部分命名,这样我们就可以将其分解成部分并学习它们。

以下是一个函数签名,其各部分由其正式的技术术语描述:

 
  

以下是我们可以用于每个部分的一些示例:

  • 返回类型:、、 等,或任何 C++类型或表达式
  • 函数名称:, , , 等等
  • 参数:,

现在让我们依次看看每个部分。

返回类型,顾名思义,是从函数返回到调用代码的值的类型:

 
  

在我们稍微沉闷但有用的示例中,签名中的返回类型是。函数将一个值返回给调用它的代码,这个值将适合在一个变量中。返回类型可以是我们到目前为止看到的任何 C++类型,或者是我们还没有看到的类型之一。

然而,函数不一定要返回一个值。在这种情况下,签名必须使用关键字作为返回类型。当使用关键字时,函数体不得尝试返回一个值,否则将导致错误。但是,它可以使用没有值的关键字。以下是一些返回类型和关键字的组合:

 
  

另一个可能性如下:

 
  

以下代码给出了更多可能的函数示例。一定要阅读注释以及代码:

 
  

上面的最后一个函数示例是我们 C++代码即将到来的一个预览,并且演示了我们也可以将用户定义的类型,称为对象,传递到函数中对它们进行计算。

我们可以像这样依次调用上面的每个函数:

 
  

不要担心关于函数的奇怪语法,我们很快就会看到像这样的真实代码。简单地说,我们将使用返回值(或)作为表达式,直接在语句中。

函数名称,当我们设计自己的函数时,可以是几乎任何东西。但最好使用单词,通常是动词,来清楚地解释函数将要做什么。例如,看看这个函数:

 
  

上面的示例是完全合法的,并且可以工作,但是下面的函数名称更加清晰:

 
  

接下来,让我们更仔细地看一下如何与函数共享一些值。

我们知道函数可以将结果返回给调用代码。如果我们需要与函数共享一些来自调用代码的数据值呢?参数允许我们与函数共享值。实际上,我们在查看返回类型时已经看到了参数的示例。我们将更仔细地看一下相同的示例:

 
  

在上面的示例中,参数是和。请注意,在函数主体的第一行中,我们使用,就好像它们已经声明和初始化了变量一样。那是因为它们确实是。函数签名中的参数是它们的声明,调用函数的代码初始化它们。

重要的行话说明

请注意,我们在函数签名括号中引用的变量被称为参数。当我们从调用代码中将值传递到函数中时,这些值被称为参数。当参数到达时,它们被称为参数,并用于初始化真正可用的变量:

此外,正如我们在先前的示例中部分看到的,我们不必只在参数中使用。我们可以使用任何 C++类型。我们还可以使用尽可能少的参数列表来解决我们的问题,但是将参数列表保持短并且易于管理是一个很好的做法。

正如我们将在未来的章节中看到的,我们已经在这个入门教程中留下了一些更酷的函数用法,这样我们就可以在进一步学习函数主题之前学习相关的 C++概念。

主体部分是我们一直在避免的部分,比如:

 
  

但实际上,我们已经完全知道在这里该做什么!到目前为止,我们学到的任何 C++代码都可以在函数体中工作。

我们已经看到了如何编写函数,也看到了如何调用函数。然而,我们还需要做一件事才能使它们工作。所有函数都必须有一个原型。原型是使编译器意识到我们的函数的东西;没有原型,整个游戏将无法编译。幸运的是,原型很简单。

我们可以简单地重复函数的签名,后面跟一个分号。但是要注意的是,原型必须出现在任何尝试调用或定义函数之前。因此,一个完全可用的函数的最简单示例如下。仔细看看注释以及函数的不同部分在代码中的位置:

 
  

前面的代码演示了以下内容:

  • 原型在函数之前
  • 使用函数的调用,正如我们可能期望的那样,位于函数内部
  • 定义在函数之后/外部

请注意,当定义出现在函数使用之前时,我们可以省略函数原型直接进入定义。然而,随着我们的代码变得越来越长并且跨越多个文件,这几乎永远不会发生。我们将一直使用单独的原型和定义。

让我们看看如何保持我们的函数有组织性。

值得指出的是,如果我们有多个函数,特别是如果它们相当长,我们的文件很快就会变得难以控制。这违背了函数的意图。我们将在下一个项目中看到的解决方案是,我们可以将所有函数原型添加到我们自己的头文件(或)中。然后我们可以在另一个文件中编写所有函数的代码,然后在我们的主文件中简单地添加另一个指令。通过这种方式,我们可以使用任意数量的函数,而不需要将它们的任何代码(原型或定义)添加到我们的主代码文件中。

我们应该讨论的另一点是作用域。如果我们在函数中声明一个变量,无论是直接声明还是作为参数之一,那么该变量在函数外部是不可用/可见的。此外,函数外部声明的任何变量在函数内部也是看不到/使用不了的。

我们应该通过参数/参数和返回值在函数代码和调用代码之间共享值。

当一个变量不可用,因为它来自另一个函数,就说它是不在作用域内。当它可用和可用时,就说它在作用域内。

实际上,在 C++中,只有在块内声明的变量才在该块内有效!这包括循环和块。在的顶部声明的变量在中的任何地方都是有效的。在游戏循环中声明的变量只在游戏循环内有效,依此类推。在函数或其他块中声明的变量称为局部变量。我们写的代码越多,这一点就越有意义。每当我们在代码中遇到作用域问题时,我都会讨论一下,以澄清事情。在下一节中将会出现这样的问题。还有一些 C++的基本知识,会让这个问题变得更加明显。它们被称为引用指针,我们将在第七章中学习:C++ 引用、精灵表和顶点数组和第八章中学习:指针、标准模板库和纹理管理

关于函数,我们还有很多东西可以学习,但我们已经了解足够的知识来实现游戏的下一部分。如果所有技术术语,如参数、签名和定义等等,还没有完全理解,不要担心。当我们开始使用它们时,概念会变得更清晰。

你可能已经注意到,我们一直在调用函数,特别是 SFML 函数,通过在函数名之前附加对象的名称和一个句号,如下例所示:

 
  

然而,我们对函数的整个讨论都是在没有任何对象的情况下调用函数。我们可以将函数编写为类的一部分,也可以将其编写为独立的函数。当我们将函数编写为类的一部分时,我们需要该类的对象来调用函数,而当我们有一个独立的函数时,我们不需要。

我们将在一分钟内编写一个独立的函数,并且我们将在第六章中编写以函数开头的类:面向对象编程、类和 SFML 视图。到目前为止,我们对函数的所有了解在这两种情况下都是相关的。

接下来,正如我在过去大约十七页中一直承诺的那样,我们将使用所有新的 C++技术来绘制和移动树上的一些树枝。

将此代码添加到函数之外。为了绝对清楚,我的意思是在代码之前:

 
  

我们刚刚用新代码实现了很多事情:

  • 首先,我们为一个名为的函数声明了一个函数原型。我们可以看到它不返回值(),并且它接受一个名为的参数。我们将很快编写函数定义,然后我们将看到它确切地做了什么。
  • 接下来,我们声明了一个名为的常量,并将其初始化为。树上将有六个移动的树枝,很快我们将看到对我们有多有用。
  • 接下来,我们声明了一个名为的对象数组,可以容纳六个精灵。
  • 之后,我们声明了一个名为的新枚举,有三个可能的值,、和。这将用于描述个别树枝的位置,以及在我们的代码中的一些地方描述玩家的位置。
  • 最后,在之前的新代码中,我们初始化了一个类型的数组,大小为(6)。为了清楚地说明这实现了什么;我们将有一个名为的数组,其中包含六个值。这些值中的每一个都是类型,可以是、或。

当然,你真正想知道的是为什么常量、两个数组和枚举被声明在函数之外。通过在之上声明它们,它们现在具有全局范围。或者,换句话说,常量、两个数组和枚举在整个游戏中都有范围。这意味着我们可以在函数和函数中的任何地方访问和使用它们。请注意,将所有变量尽可能地局部化到实际使用它们的地方是一个好的做法。将所有东西都变成全局变量可能看起来很有用,但这会导致难以阅读和容易出错的代码。

现在我们将准备好我们的六个对象,并将它们加载到数组中。在我们的游戏循环之前添加以下突出显示的代码:

 
  

之前的代码没有使用任何新概念。首先,我们声明了一个 SFML 对象,并将图形加载到其中。

接下来,我们创建一个循环,将设置为零,并在每次循环通过时递增,直到不再小于。这是完全正确的,因为是 6,而数组的位置是 0 到 5。

在循环中,我们使用为数组中的每个设置,然后用将其隐藏在屏幕外。

最后,我们使用将原点(绘制时所在的点)设置为精灵的中心。很快,我们将旋转这些精灵,并且将原点设置在中心意味着它们将很好地围绕旋转,而不会使精灵移出位置。

在下面的代码中,我们根据数组中的位置和相应的数组中的的值,设置数组中所有精灵的位置。添加高亮代码并尝试理解它,然后我们可以详细讨论一下:

 
  

我们刚刚添加的代码是一个大的循环,将设置为零,每次通过循环递增,并持续进行,直到不再小于 6。

在循环内,设置了一个名为的新的变量,其值为。这意味着第一个树枝的高度为 0,第二个为 150,第六个为 750。

接下来是一系列和块的结构。看一下剥离了代码的结构:

 
  

第一个使用数组来查看当前树枝是否应该在左边。如果是的话,它会将数组中的相应设置为屏幕上适合左边(610 像素)和当前的位置。然后它将精灵翻转度,因为图形默认向右悬挂。

只有在树枝不在左边时才执行。它使用相同的方法来查看它是否在右边。如果是的话,树枝就会被绘制在右边(1330 像素)。然后将精灵旋转为 0 度,以防它之前是 180 度。如果 x 坐标看起来有点奇怪,只需记住我们将树枝精灵的原点设置为它们的中心。

最后的假设,正确地,当前的必须是,并将树枝隐藏在屏幕外的像素处。

此时,我们的树枝已经就位,准备绘制。

在这里,我们使用另一个循环,从 0 到 5 遍历整个数组,并绘制每个树枝精灵。添加以下高亮代码:

 
  

当然,我们还没有编写实际移动所有树枝的函数。一旦我们编写了该函数,我们还需要解决何时以及如何调用它的问题。让我们解决第一个问题并编写该函数。

我们已经在函数上面添加了函数原型。现在我们编写实际的函数定义,该函数将在每次调用时将所有树枝向下移动一个位置。我们将这个函数分为两部分编写,以便更容易地检查发生了什么。

在函数的右花括号后添加函数的第一部分:

 
  

在函数的第一部分中,我们只是将所有的树枝向下移动一个位置,一次一个,从第六个树枝开始。这是通过使循环从 5 计数到 0 来实现的。代码实现了实际的移动。

在前面的代码中,另一件需要注意的事情是,当我们将位置 4 的树枝移动到位置 5,然后将位置 3 的树枝移动到位置 4,依此类推,我们需要在位置 0 添加一个新的树枝,这是树的顶部。

现在我们可以在树的顶部生成一个新的树枝。添加高亮代码,然后我们将讨论它:

 
  

在函数的最后部分,我们使用传入函数调用的整数变量。我们这样做是为了确保随机数始终不同,并且我们将在下一章中看到这个值是如何得到的。

接下来,我们生成一个介于零和四之间的随机数,并将结果存储在变量中。现在我们使用作为表达式进行。

语句意味着,如果等于零,那么我们在树的顶部左侧添加一个新的分支。如果等于 1,那么分支就在右侧。如果是其他任何值(2、3 或 4),那么确保在顶部不会添加任何分支。左、右和无的平衡使得树看起来很真实,游戏运行得相当不错。你可以很容易地改变代码,使分支更频繁或更少。

即使为我们的分支编写了所有这些代码,我们仍然无法在游戏中看到任何一个分支。这是因为在我们实际调用之前,我们还有更多的工作要做。

如果你现在真的想看到一个分支,你可以添加一些临时代码,并在游戏循环之前调用该函数五次,每次使用一个独特的种子:

 
  

现在你可以看到分支在它们的位置上。但是如果分支实际上要移动,我们需要定期调用。

移动分支

在继续之前不要忘记删除临时代码。

现在我们可以把注意力转向玩家,并真正调用函数。

Q) 你提到了几种类型的 C++循环。

A) 是的,看一下这个循环的教程和解释:

Q) 我可以假设我是数组的专家吗?

A) 就像本书中的许多主题一样,总是有更多的东西可以学习。你已经了解足够的关于数组的知识来继续,但如果你还想了解更多,请查看这个更详细的数组教程:。

Q) 我可以假设我是函数的专家吗?

A) 就像本书中的许多主题一样,总是有更多的东西可以学习。你已经了解足够的关于函数的知识来继续,但如果想了解更多,请查看这个教程:。

虽然这不是最长的一章,但可能是我们涵盖最多 C++知识的一章。我们研究了不同类型的循环,比如和循环。我们学习了处理大量变量和对象的数组,而不费吹灰之力。我们还学习了枚举和。也许这一章最重要的概念是允许我们组织和抽象游戏代码的函数。随着书的继续,我们将在更多地方深入研究函数。

现在我们有一个完全可用的树,我们可以在这个项目的最后一章中完成游戏。

这是第一个项目的最后阶段。在本章结束时,您将拥有您的第一个完成的游戏。一旦您运行了 Timber!!!,一定要阅读本章的最后一节,因为它将提出改进游戏的建议。我们将讨论以下主题:

  • 添加其余的精灵
  • 处理玩家输入
  • 动画飞行原木
  • 处理死亡
  • 添加音效
  • 添加功能并改进 Timber!!!

让我们同时为玩家的精灵添加代码,以及一些更多的精灵和纹理。这下面的相当大的代码块还为玩家被压扁时添加了一个墓碑精灵,一个用来砍伐的斧头精灵,以及一个可以在玩家砍伐时飞走的原木精灵。

请注意,在对象之后,我们还声明了一个变量,以跟踪玩家当前站立的位置。此外,我们为对象添加了一些额外的变量,包括、和,用于存储原木的移动速度以及它当前是否在移动。还有两个相关的常量变量,用于记住左右两侧的理想像素位置。

像以前那样,在代码之前添加下一个代码块。请注意,下一个清单中的所有代码都是新的,而不仅仅是突出显示的代码。我没有为下一个代码块提供任何额外的上下文,因为应该很容易识别。突出显示的代码是我们刚刚讨论过的代码。

在行之前添加整个代码,并在脑海中记住我们简要讨论过的突出显示的行。这将使本章其余的代码更容易理解:

 
  

现在我们可以绘制所有新的精灵。

在我们添加移动玩家和使用所有新精灵的代码之前,让我们先绘制它们。这样,当我们添加代码来更新/改变/移动精灵时,我们将能够看到发生了什么。

添加突出显示的代码以绘制四个新的精灵:

 
  

运行游戏,你会看到我们在场景中的新精灵。

绘制玩家和其他精灵

我们现在离一个可运行的游戏非常接近了。

许多不同的事情取决于玩家的移动,比如何时显示斧头,何时开始动画原木,以及何时将所有的树枝移动到一个地方。因此,为玩家砍伐设置键盘处理是有意义的。一旦完成这一点,我们就可以将刚才提到的所有功能放入代码的同一部分。

让我们思考一下我们如何检测键盘按键。在每一帧中,我们测试特定的键盘键当前是否被按下。如果是,我们就采取行动。如果按下Esc键,我们退出游戏,或者如果按下Enter键,我们重新开始游戏。到目前为止,这对我们的需求已经足够了。

然而,当我们尝试处理砍树时,这种方法存在问题。这个问题一直存在,只是直到现在才变得重要。根据您的 PC 有多强大,游戏循环可能每秒执行数千次。在游戏循环中每次按下键时,都会检测到并执行相关代码。

实际上,每次按下Enter重新开始游戏时,您很可能会重新开始游戏超过一百次。这是因为即使是最短暂的按键按下也会持续相当长的时间。您可以通过运行游戏并按住Enter键来验证这一点。请注意,时间条不会移动。这是因为游戏一遍又一遍地重新启动,每秒甚至数千次。

如果我们不对玩家的砍伐采取不同的方法,那么只需一次尝试的砍伐就会在短短的时间内将整棵树砍倒。我们需要更加复杂一些。我们将允许玩家进行砍伐,然后在他这样做时禁用检测按键的代码。然后我们将检测玩家何时从按键上移开手指,然后重新启用按键检测。以下是清晰列出的步骤:

  1. 等待玩家使用左右箭头键砍伐木头。
  2. 当玩家砍伐时,禁用按键检测。
  3. 等待玩家从按键上移开手指。
  4. 重新启用砍伐检测。
  5. 从步骤 1 重复。

这可能听起来很复杂,但借助 SFML 的帮助,这将非常简单。让我们现在一步一步地实现这个。

添加代码中的突出显示行,声明一个变量和,用于确定何时监听砍伐动作和何时忽略它们:

 
  

现在我们已经设置好了布尔值,可以继续下一步了。

现在我们准备处理砍伐,将突出显示的代码添加到开始新游戏的块中:

 
  

在之前的代码中,我们使用循环将树设置为没有分支。这对玩家是公平的,因为如果游戏从他的头顶上方开始,那将被认为是不公平的。然后我们简单地将墓碑移出屏幕,玩家移动到左侧的起始位置。这个新代码的最后一件事是将设置为。我们现在准备好接收砍伐按键了。

现在我们可以准备处理左右方向键的按下。添加这个简单的块,只有当为时才执行:

 
  

现在,在我们刚刚编写的块中,添加突出显示的代码来处理玩家在键盘上按下右箭头键()时发生的情况:

 
  

在上面的代码中发生了很多事情,让我们逐步进行。首先,我们检测玩家是否在树的右侧砍伐。如果是,我们将设置为。我们将在代码的后面对的值做出响应。

然后我们用将分数加 1。下一行代码有点神秘,但实际上我们只是增加了剩余时间的数量。我们正在奖励玩家采取行动。然而,对于玩家来说,问题在于分数越高,增加的时间就越少。您可以通过调整这个公式来使游戏变得更容易或更难。

然后,斧头移动到右侧位置,使用,玩家精灵也移动到右侧位置。

接下来,我们调用将所有的分支向下移动一个位置,并在树的顶部生成一个新的随机分支(或空格)。

然后,移动到起始位置,伪装成树,它的变量设置为负数,这样它就会向左飞去。此外,设置为,这样我们即将编写的移动木头的代码就会在每一帧中使木头动起来。

最后,被设置为。此时,玩家无法再进行砍伐。我们已经解决了按键被频繁检测的问题,很快我们将看到如何重新启用砍伐。

现在,在我们刚刚编写的块内,添加突出显示的代码来处理玩家在键盘上按下左箭头键()时发生的情况:

 
  

前面的代码与处理右侧砍伐的代码完全相同,只是精灵的位置不同,并且变量设置为正值,使得木头向右飞去。

为了使上述代码在第一次砍伐之后继续工作,我们需要检测玩家何时释放键,并将设置回。

这与我们迄今为止看到的按键处理略有不同。SFML 有两种不同的方式来检测玩家的键盘输入。我们已经看到了第一种方式。它是动态和瞬时的,正是我们需要立即对按键做出响应的。

下面的代码使用了另一种方法。输入下一个突出显示的代码到部分的顶部,然后我们将逐步讲解它:

 
  

首先,我们声明了一个名为的类型的对象。然后我们调用函数,传入我们的新对象。函数将数据放入对象中,描述了操作系统事件。这可能是按键、释放键、鼠标移动、鼠标点击、游戏控制器动作或发生在窗口本身的事件(例如调整大小等)。

我们将代码包装在循环中的原因是因为队列中可能存储了许多事件。函数将这些事件一个接一个地加载到中。我们将在循环中的每次通过中看到当前事件,如果我们感兴趣,就会做出响应。当返回时,这意味着队列中没有更多事件,循环将退出。

当释放一个键并且游戏没有暂停时,这个条件()为。

在块中,我们将设置回,并将斧头精灵隐藏在屏幕外。

现在您可以运行游戏,惊叹于移动的树木、摆动的斧头和动画的玩家。然而,它不会压扁玩家,砍伐时木头也需要移动。

当玩家砍木头时,被设置为,因此我们可以将一些代码包装在一个块中,只有当为时才执行。此外,每次砍木头都会将设置为正数或负数,因此木头准备好朝着正确的方向飞离树。

在我们更新分支精灵之后,添加下面突出显示的代码:

 
  

代码通过使用获取精灵的当前 x 和 y 位置,然后分别使用和乘以加到其上,来设置精灵的位置。

在每一帧中移动木头精灵后,代码使用块来查看精灵是否已经从左侧或右侧消失在视野中。如果是,木头就会移回到起点,准备下一次砍伐。

如果您运行游戏,您将能够看到木头飞向屏幕的适当一侧。

动画砍伐的木头和斧头

现在是一个更敏感的话题。

每个游戏都必须以不好的方式结束,要么是玩家时间用完(这已经处理过了),要么是被分支压扁。

检测玩家被压扁非常简单。我们只想知道数组中的最后一个分支是否等于。如果是,玩家就死了。

添加检测这一点的突出代码,然后我们将讨论玩家被压扁时的所有操作:

 
  

在玩家死亡后,代码的第一件事是将设置为。现在循环将完成这一帧,并且在玩家开始新游戏之前不会再次运行循环的更新部分。

然后我们将墓碑移动到靠近玩家站立的位置,并将玩家精灵隐藏在屏幕外。

我们将的字符串设置为,然后使用通常的技术将其居中显示在屏幕上。

现在您可以运行游戏并真正玩它。这张图片显示了玩家的最终得分和他的墓碑,以及SQUISHED消息。

处理死亡

还有一个问题。只是我吗,还是有点安静?

我们将添加三种声音。每种声音都将在特定的游戏事件上播放。每当玩家砍伐时播放简单的重击声音,当玩家时间用尽时播放沮丧的失败声音,当玩家被压扁致死时播放复古的压碎声音。

SFML 使用两种不同的类来播放声音效果。第一个类是类。这个类保存了来自声音文件的实际音频数据。它是负责将文件加载到 PC 的 RAM 中,以一种无需进一步解码工作即可播放的格式。

一会儿,当我们为声音效果编写代码时,我们将看到,一旦我们有了一个包含我们声音的对象,我们将创建另一个类型为的对象。然后,我们可以将这个对象与对象关联起来。然后,在我们的代码中适当的时刻,我们将能够调用适当对象的函数。

很快我们将看到,加载和播放声音的 C++代码真的很简单。然而,我们需要考虑的是何时调用函数。我们的代码中何处将调用函数?以下是我们想要实现的一些功能:

  • 砍伐声音可以从按下左右光标键时调用
  • 死亡声音可以从检测到树木将玩家搅碎的块中播放
  • 时间用尽的声音可以从检测到小于零的块中播放

现在我们可以编写我们的声音代码。

首先,我们添加另一个指令,以使 SFML 与声音相关的类可用。添加下面突出显示的代码:

 
  

现在我们声明三个不同的对象,将三个不同的声音文件加载到它们中,并将三个不同的对象与相关的对象关联起来。添加下面突出显示的代码:

 
  

现在我们可以播放我们的第一个声音效果。在检测到玩家按下左光标键的块旁边添加如下一行代码:

 
  

在下一个以开头的代码块的末尾添加完全相同的代码,以使玩家在树的左侧砍伐时发出砍伐声音。

找到处理玩家时间用尽的代码,并添加下一个突出显示的代码,以播放与时间相关的音效:

 
  

最后,当玩家被压扁时播放死亡声音,将下面突出显示的代码添加到执行当底部树枝与玩家同侧时的块中:

 
  

就是这样!我们已经完成了第一个游戏。在我们继续进行第二个项目之前,让我们讨论一些可能的增强功能。

看看 Timber!!!项目的这些建议的增强功能。您可以在下载包的文件夹中看到增强功能的效果:

  1. 加快代码速度:我们的代码中有一部分正在减慢我们的游戏。对于这个简单的游戏来说无所谓,但我们可以通过将代码放在仅偶尔执行的块中来加快速度。毕竟,我们不需要每秒更新得分数百次!
  2. 调试控制台:让我们添加一些文本,以便我们可以看到当前的帧速率。与得分一样,我们不需要经常更新这个。每一百帧更新一次就足够了。
  3. 在背景中添加更多的树:只需添加一些更多的树精灵并将它们绘制在看起来不错的位置(你可以在相机附近放一些,远一些)。
  4. 改善 HUD 文本的可见性:我们可以在分数和 FPS 计数器后面绘制简单的对象;黑色并带有一些透明度看起来会很好。
  5. 使云代码更有效率:正如我们已经提到过几次的,我们可以利用我们对数组的知识使云代码变得更短。

看看游戏中额外的树、云和文本的透明背景。

改善游戏和代码

要查看这些增强的代码,请查看下载包中的“伐木工增强版”文件夹。

Q)我承认,对于云的数组解决方案更有效率。但是我们真的需要三个单独的数组吗,一个用于活动,一个用于速度,一个用于精灵本身吗?

A)如果我们查看各种对象的属性/变量,例如对象,我们会发现它们很多。精灵有位置、颜色、大小、旋转等等。但如果它们有、,甚至更多的话就更完美了。问题在于 SFML 的程序员不可能预测我们将如何使用他们的类。幸运的是,我们可以制作自己的类。我们可以制作一个名为的类,其中有一个布尔值用于和一个整数用于速度。我们甚至可以给我们的类一个 SFML 的对象。然后我们甚至可以进一步简化我们的云代码。我们将在下一章中设计我们自己的类。

在本章中,我们为《伐木工》游戏添加了最后的修饰和图形。如果在读这本书之前,你从未编写过一行 C++代码,那么你可以为自己鼓掌。在短短的五章中,你已经从零基础到一个可运行的游戏。

然而,我们不会为自己的成就而沾沾自喜太久,因为在下一章中,我们将直接转向一些稍微更复杂和更全面的 C++,这可以用来构建更复杂和更全面的游戏。

这是本书最长的章节。有相当多的理论,但这些理论将使我们有能力开始有效地使用面向对象编程(OOP)。此外,我们不会浪费时间来将理论付诸实践。在探索 C++ OOP 之前,我们将了解并计划我们的下一个游戏项目。

以下是我们将在接下来的章节中要做的事情:

  • 规划“僵尸竞技场”游戏
  • 学习面向对象编程和类
  • 编写类
  • 了解 SFML 的类
  • 构建僵尸竞技场游戏引擎
  • 让类开始工作

此时,如果你还没有的话,我建议你去观看《超过 9000 只僵尸》()和《血色之地》()的视频。

我们的游戏显然不会像这两个示例那样深入或先进,但我们将拥有相同的基本功能和游戏机制:

  • 显示一些细节的“HUD”,比如得分、最高分、弹夹中的子弹、剩余子弹、玩家生命和剩余待杀僵尸数
  • 玩家将在疯狂逃离僵尸的同时射击它们
  • 在使用鼠标瞄准枪支的同时,使用 W、A、S 和 D 键在滚动世界中移动
  • 在每个级别之间,选择一个会影响游戏成功方式的“升级”。
  • 收集“拾取物”以恢复生命和弹药
  • 每一波都会带来更多的僵尸和更大的竞技场

将有三种类型的僵尸需要消灭。它们将具有不同的属性,如外观、生命和速度。我们将称它们为追逐者、膨胀者和爬行者。看一下游戏的注释截图,看看一些功能的运作以及组成游戏的组件和资源:

规划和开始僵尸竞技场游戏

以下是关于每个编号点的更多信息:

  1. 得分和最高分。这些与 HUD 的其他部分一起将在一个称为“视图”的单独图层中绘制。最高分将被保存并加载到文件中。
  2. 这是一个将在竞技场周围建造墙壁的纹理。这个纹理包含在一个称为“精灵表”的单个图形中,还有其他背景纹理(3、5 和 6)。
  3. 精灵表中的两个泥浆纹理之一。
  4. 这是一个“弹药拾取物”。玩家获得这个拾取物后将获得更多弹药。还有一个“生命拾取物”。玩家可以选择在僵尸波之间升级这些拾取物。
  5. 精灵表中的草纹理。
  6. 精灵表中的第二个泥浆纹理。
  7. 曾经有僵尸的地方现在是一滩血迹。
  8. HUD 的底部部分。从左到右依次是代表弹药的图标、弹夹中的子弹数量、备用子弹数量、生命条、当前僵尸波数以及本波剩余僵尸数量。
  9. 玩家角色。
  10. 玩家用鼠标瞄准的准星。
  11. 一个移动缓慢但力量强大的膨胀僵尸
  12. 一个移动速度稍快但较弱的爬行僵尸。还有一个追逐者僵尸,速度非常快但很弱。不幸的是,在它们被全部杀死之前,我没能在截图中找到一个。

我们有很多事情要做,还有新的 C++技能要学习。让我们从创建一个新项目开始。

现在创建一个新项目非常容易。只需在 Visual Studio 中按照这些简单的步骤进行:

  1. 从主菜单中选择“文件”|“新建项目”。
  2. 确保在左侧菜单中选择了“Visual C++”,然后从所呈现的选项列表中选择“HelloSFML”。下一张图片应该能清楚地说明这一点:从模板创建项目
  3. 名称:字段中,键入,并确保为解决方案创建目录选项已被选中。现在点击确定
  4. 现在我们需要将 SFML 的文件复制到主项目目录中。我的主项目目录是。这个文件夹是由 Visual Studio 在上一步中创建的。如果您将文件夹放在其他地方,请在那里执行此步骤。我们需要复制到文件夹中的文件位于您的文件夹中。为每个位置打开一个窗口,并突出显示所需的文件。

现在将突出显示的文件复制并粘贴到项目中。项目现在已经设置好,准备好了。

该项目中的资产比以前的游戏更多样化和丰富。资产包括:

  • 屏幕上的字体
  • 不同动作的音效,如射击、装弹或被僵尸击中。

游戏所需的角色、僵尸、背景和声音的所有图形都包含在下载包中。它们分别可以在和文件夹中找到。

所需的字体尚未提供。这是因为我想避免任何关于许可证的可能歧义。不过这不会造成问题,因为我将向您展示确切的位置和方式来选择和下载字体。

虽然我将提供资产本身或获取它们的信息,但您可能希望自己创建和获取它们。

图形资产构成了我们的僵尸竞技场游戏场景的一部分。看一下图形资产,应该清楚我们的游戏中它们将被用在哪里:

探索资产

然而,可能不太明显的是,其中包含四个不同的图像。这是我之前提到的精灵表,我们将看到如何使用它来节省内存并提高游戏速度,详见第七章,C++参考、精灵表和顶点数组

声音文件都是格式。这些文件包含了我们在游戏中的某些事件中播放的音效。它们是:

  • :僵尸与玩家接触时播放的声音
  • :玩家触摸(收集)健康提升(拾取)时播放的声音
  • :玩家在每波僵尸之间选择增加属性(power-up)时播放的声音
  • :令玩家知道他们已装入新弹药的满意点击声
  • :指示未能装入新子弹的不太令人满意的声音
  • :射击声音
  • :像僵尸被子弹击中的声音

一旦您决定使用哪些资产,就该是将它们添加到项目中的时候了。下面的说明将假定您正在使用本书下载包中提供的所有资产。如果您使用自己的资产,只需用您自己的适当的声音或图形文件替换本书中使用的完全相同的文件名即可:

  1. 浏览到。
  2. 在此文件夹中创建三个新文件夹,分别命名为、和。
  3. 从下载包中,将文件夹的全部内容复制到文件夹中。
  4. 从下载包中,将文件夹的全部内容复制到文件夹中。
  5. 现在在您的网络浏览器中访问,并下载Zombie Control字体。

提取压缩下载的内容,并将文件添加到文件夹。现在是时候学习更多的 C++了,这样我们就可以开始为 Zombie Arena 编写代码了。

OOP 是一种编程范式,我们可以认为它几乎是编码的标准方式。的确,有非 OOP 的编码方式,甚至有一些非 OOP 的游戏编码语言和库。然而,从零开始,就像这本书所做的那样,没有理由以其他方式做事。当 OOP 的好处变得明显时,你将永远不会回头看。

OOP 将会:

  • 使我们的代码更易管理,更改或更新
  • 使我们的代码更快,更可靠地编写
  • 使其可以轻松使用其他人的代码(如 SFML)

我们已经看到了第三个好处的实际效果。让我们通过引入一个需要解决的问题来看一下前两个好处。我们面临的问题是当前项目的复杂性。让我们考虑一个单一的僵尸以及我们需要让它在游戏中运行的内容:

  • 水平和垂直位置
  • 大小
  • 它所面对的方向
  • 每种僵尸类型的不同纹理
  • 精灵
  • 每种僵尸类型的不同速度
  • 每种僵尸类型的不同生命值
  • 跟踪每个僵尸的类型
  • 碰撞检测数据
  • 智能(追逐玩家)
  • 僵尸是活着的还是死了?

这个列表可能为一个僵尸提供了大约十几个变量!我们可能需要整个数组来管理僵尸群。那么机枪的所有子弹、拾取物品和不同的升级呢?简单的 Timber!!!游戏到最后开始变得有点难以管理,可以推测这个更复杂的射击游戏可能会更糟!

幸运的是,处理复杂性并不是一个新问题,C++从一开始就被设计为解决这种复杂性。

OOP 是一种编程方式,它涉及将我们的需求分解成比整体更易管理的块。

每个块是自包含的,但可能被其他程序重复使用,同时作为一个整体一起工作。

这些块就是我们所说的对象。当我们计划和编写一个对象时,我们使用一个

类可以被认为是对象的蓝图。

我们实现一个类的对象。这被称为类的实例。想象一下一个房子的蓝图。你不能住在里面,但你可以建造一座房子。你建造了它的一个实例。通常,当我们为我们的游戏设计类时,我们会写一些代表现实世界事物的类。在这个项目中,我们将为玩家、僵尸、子弹等编写类。然而,OOP 不仅仅是这样。

OOP 是一种做事的方式,一种定义最佳实践的方法。

OOP 的三个核心原则是封装多态继承。这可能听起来很复杂,但实际上,一步一步地进行,它是相当简单的。

封装意味着保护代码的内部工作,使其不受使用它的代码的干扰。你可以通过只允许你选择的变量和函数来访问来实现这一点。这意味着只要暴露的部分仍然以相同的方式被访问,你的代码就可以随时更新、扩展或改进,而不会影响使用它的程序。

举个例子,通过适当的封装,如果 SFML 团队需要更新他们的类的工作方式,这并不重要。只要函数签名保持不变,我们就不必担心内部发生了什么。更新之前编写的代码仍然可以在更新后继续工作。

多态性使我们能够编写不太依赖于我们试图操作的类型的代码。这将使我们的代码更清晰、更高效。多态性意味着不同的形式。如果我们编码的对象可以是多种类型的东西,那么我们就可以利用这一点。多态性在第十二章中的最终项目中将会得到应用,抽象和代码管理-更好地利用 OOP。一切都会变得更清晰。

就像听起来的那样,继承意味着我们可以利用其他人类的所有功能和好处,包括封装和多态性,同时进一步调整他们的代码以适应我们的情况。我们将在第十二章中的最终项目中使用继承,抽象和代码管理 - 更好地利用 OOP

当正确编写时,所有这些 OOP 都允许您添加新功能,而无需过多担心它们与现有功能的交互。当您必须更改类时,其自包含(封装)的特性意味着对程序的其他部分的影响较少,甚至可能为零。

您可以使用其他人的代码(例如 SFML 类),而无需知道甚至关心其内部工作原理。

OOP,以及扩展的 SFML,使您能够编写使用复杂概念的游戏,例如多个摄像机、多人游戏、OpenGL、定向声音等等。所有这些都可以轻松实现。

使用继承,您可以创建多个相似但不同版本的类,而无需从头开始编写类。

由于多态性,您仍然可以使用原始对象类型的函数来处理新对象。

所有这些都是有道理的。而且,正如我们所知,C++从一开始就考虑了所有这些 OOP。

OOP 和制作游戏(或任何其他类型的应用程序)的最终成功关键,除了决心成功外,还包括规划和设计。重要的不仅仅是了解所有 C++、SFML 和 OOP 主题,而是将所有这些知识应用到编写结构良好、设计良好的代码中。本书中的代码按照适合在游戏环境中学习各种 C++主题的顺序和方式呈现。结构化代码的艺术和科学称为设计模式。随着代码变得越来越长和复杂,有效使用设计模式将变得更加重要。好消息是,我们不需要自己发明这些设计模式。随着我们的项目变得更加复杂,我们需要了解它们。最终章节将更多地介绍设计模式。

在这个项目中,我们将学习和使用基本类和封装,而在最终项目中,我们将更加大胆地使用继承、多态性和其他与 C++相关的 OOP 特性。

类是一堆代码,可以包含函数、变量、循环和我们已经学过的所有其他 C++语法。每个新类将在其自己的代码文件中声明,文件名与类名相同,其函数将在其自己的文件中定义。当我们实际编写一些类时,这将变得更清晰。

一旦我们编写了一个类,我们可以使用它来创建任意数量的对象。记住,类是蓝图,我们根据蓝图制作对象。房子不是蓝图,就像对象不是类。它是从类制作的对象。

您可以将对象视为变量,将类视为类型。

当然,谈论 OOP 和类时,我们实际上还没有看到任何代码。所以现在让我们来解决这个问题。

让我们用一个不同的游戏例子来看看,比如僵尸竞技场。考虑一下最基本的游戏,乒乓球。一个弹球的球拍。球拍将是一个很好的类候选。

如果你不知道乒乓球是什么,那就看看这个链接:

看一下一个假设的文件:

 
  

乍一看,代码可能看起来有点复杂,但当解释时,我们会发现其中几乎没有新概念。

首先要注意的是使用关键字声明了一个新类,后面跟着类的名称,整个声明被大括号括起来,后面跟着一个分号:

 
  

现在看看变量的声明和它们的名称:

 
  

所有的名称都以为前缀。这不是必需的,但这是一个很好的约定。作为类的一部分声明的变量称为成员变量。以为前缀使得当我们处理成员变量时变得非常明显。当我们为我们的类编写函数时,我们将开始看到局部变量和参数。约定将证明自己是有用的。

还要注意的是,所有的变量都在以关键字开头的代码部分中。扫一眼之前的示例代码,注意类代码的主体分为两个部分:

 
  

和关键字控制了我们的类的封装。任何私有的东西都不能被类的实例或对象的用户直接访问。如果你正在为其他人设计一个类来使用,你不希望他们能够随意改变任何东西。

这意味着我们的四个成员变量不能被中的游戏引擎直接访问。它们可以通过类的代码间接访问。对于和变量,这是相当容易接受的,只要我们不需要改变球拍的大小。然而,和成员变量需要被访问,否则我们怎么移动球拍呢?

这个问题在代码的部分得到了解决:

 
  

该类提供了两个公共函数,可以与类型的对象一起使用。当我们看到这些函数的定义时,我们将看到这些函数如何操纵私有变量。

总之,我们有一堆无法访问的(私有)变量,不能从函数中使用。这是很好的,因为封装使我们的代码更少出错,更易维护。然后,我们通过提供两个公共函数来解决移动球拍的问题,间接访问和变量。

中的代码可以调用这些函数,但函数内部的代码控制着变量的具体修改方式。

让我们来看看函数的定义。

我们将在本书中编写的函数定义都将放在一个单独的文件中,与类和函数声明分开。我们将使用与类相同名称的文件和文件扩展名。因此,在我们的假设示例中,下一个代码将放在一个名为的文件中。看一下这个非常简单的代码,其中只有一个新概念:

 
  

首先要注意的是,我们必须使用包含指令来包含类中的类和函数声明。

我们在这里看到的新概念是作用域解析运算符的使用。由于函数属于一个类,我们必须通过在函数名前加上类名和来编写签名部分。和。

实际上,我们之前已经简要看到了作用域解析运算符。每当我们声明一个类的对象并且之前没有使用。

还要注意,我们可以把函数的定义和声明放在一个文件中,就像这样:

 
  

然而,当我们的类变得更长(就像我们的第一个 Zombie Arena 类一样),将函数定义分离到它们自己的文件中会更有组织性。此外,头文件被认为是公共的,并且通常用于文档目的,如果其他人将使用我们编写的代码。

尽管我们已经看到了与类相关的所有代码,但我们实际上还没有使用这个类。我们已经知道如何做到这一点,因为我们已经多次使用了 SFML 类。

首先,我们会像这样创建一个的实例:

 
  

对象拥有我们在中声明的所有变量。我们只是不能直接访问它们。然而,我们可以使用它的公共函数来移动我们的挡板,就像这样:

 
  

或者像这样:

 
  

请记住,是一个,因此它拥有所有的成员变量和所有的可用函数。

我们可以决定在以后的某个日期将我们的Pong游戏改为多人游戏。在函数中,我们可以改变代码以拥有两个挡板。可能像这样:

 
  

非常重要的是要意识到,每个实例都是具有自己独特变量集的单独对象。

简单的 Pong 挡板示例是介绍类基础知识的好方法。类可以像一样简单和简短,但它们也可以更长,更复杂,并且本身包含其他对象。

在制作游戏时,假设的类中缺少一个重要的东西。对于所有这些私有成员变量和公共函数来说可能还好,但我们如何绘制任何东西呢?我们的 Pong 挡板也需要一个精灵和一个纹理。

我们可以以与在中包含它们相同的方式在我们的类中包含其他对象。

这是代码中部分的更新版本,其中包括一个成员和一个成员。请注意,该文件还需要相关的 SFML 包含指令,以便该代码能够编译。

 
  

新问题立即出现。如果和是私有的,那么我们怎么在函数中绘制它们呢?

我们需要提供一个函数,允许访问以便绘制。仔细看看公共部分的新函数声明。

 
  

先前的代码声明了一个名为的函数。要注意的重要事情是返回一个对象。我们很快就会看到的定义。

如果你很敏锐,你也会注意到在任何时候我们都没有加载纹理或调用来将纹理与精灵关联起来。

当一个类被编码时,编译器会创建一个特殊的函数。我们在代码中看不到这个函数,但它确实存在。它被称为构造函数。当我们需要编写一些代码来准备一个对象供使用时,通常一个很好的地方就是构造函数。当我们希望构造函数做的事情不仅仅是创建一个实例时,我们必须替换编译器提供的默认(看不见的)构造函数。

首先,我们提供一个构造函数声明。请注意,构造函数没有返回类型,甚至没有。还要注意,我们可以立即看到它是构造函数,因为函数名与类名相同。

 
  

下面的代码显示了中的新函数定义(和构造函数):

 
  

在先前的代码中,我们使用构造函数来加载纹理并将其与精灵关联起来。请记住,这个函数是在声明类型的对象时调用的。更具体地说,当执行代码时,构造函数被调用。

在函数中,只有一行代码将的副本返回给调用代码。

我们还可以在构造函数中为我们的对象进行其他设置工作,并且在构建我们的第一个真正的类时会这样做。

如果您想看看函数如何被使用,中的代码将如下所示:

 
  

上一行代码假设我们有一个名为的 SFML 对象。由于返回一个类型的对象,上一行代码的工作方式与在中声明 sprite 的方式完全相同。现在我们有了一个通过其公共函数提供受控访问的封装良好的类。

我发现当我阅读跳来跳去的代码文件的书时,我经常发现很难准确地理解发生了什么。接下来是假设的和的完整清单,以便在继续之前仔细研究它们:

 
  

 
  

在本书的其余部分,我们将不断回顾类和面向对象编程。然而,现在我们已经知道足够的知识来开始我们的第一个真正的 Zombie Arena 游戏类。

让我们考虑一下我们的类需要做什么。该类需要知道自己可以移动多快,当前在游戏世界中的位置以及拥有多少生命值。由于类在玩家眼中被表示为一个 2D 图形角色,该类将需要一个和一个对象。

此外,尽管此时可能不明显,我们的类还将受益于了解游戏运行的整体环境的一些细节。这些细节包括屏幕分辨率、构成竞技场的瓦片大小以及当前竞技场的整体大小。

由于类将全权负责每帧更新自身,它需要知道玩家在任何给定时刻的意图。例如,玩家当前是否按住特定的键盘方向键?或者玩家当前是否按住多个键盘方向键?布尔变量来确定WASD键的状态将是必不可少的。

很明显,我们将需要在我们的新类中使用相当多的变量。通过学习了关于面向对象编程的所有知识,我们当然会将所有这些变量设置为私有的。这意味着我们必须在适当的时候从函数中提供访问。

我们将使用一大堆函数,以及一些其他函数来设置我们的对象。这些函数相当多;实际上,在这个类中有 21 个函数。起初这可能看起来有点令人生畏,但我们将逐个查看它们,并看到其中大多数只是设置或获取其中一个私有变量。

有一些相当深入的函数,比如,它将从函数中每帧调用一次,以及,它将处理一些私有变量的初始化。然而,正如我们将看到的,它们都不复杂,并且将被详细描述。

继续进行的最佳方式是编写头文件。这将使我们有机会查看所有私有变量并检查所有函数签名。请特别注意返回值和参数类型,因为这将使理解函数定义中的代码变得更容易。

在“解决方案资源管理器”中右键单击“头文件”,然后选择添加 | 新建项目...。在“添加新项目”窗口中,通过左键单击头文件(),然后在“名称”字段中输入。最后,单击添加按钮。现在我们准备为我们的第一个类编写头文件。

通过添加声明来开始编写类,包括开放和关闭的大括号,然后是一个分号:

 
  

现在让我们添加所有私有成员变量。根据我们已经讨论的内容,看看你能否弄清楚它们每一个将要做什么。我们将逐个讨论它们:

 
  

之前的代码声明了我们所有的成员变量。有些是常规变量,有些是对象本身。请注意,它们都在类的部分下,并且因此不能直接从类外部访问。

还要注意,我们使用了将前缀添加到所有非常量变量的命名约定。前缀将在编写函数定义时提醒我们,它们是成员变量,并且与我们将在一些函数中创建的一些局部变量以及与函数参数不同。

所有变量的用途都是明显的,比如、和,它们分别用于玩家的当前位置、纹理和精灵。此外,每个变量(或变量组)都有注释,以便明确它们的用途。

然而,它们为什么需要以及它们将在什么上下文中使用可能并不那么明显。例如,是一个类型的对象,用于记录玩家上次受到僵尸攻击的时间。我们将用于的用途很明显,但同时,为什么我们可能需要这些信息并不明显。

随着我们将游戏的其余部分拼凑在一起,每个变量的上下文将变得更加清晰。现在重要的是要熟悉变量的名称和类型,以便在项目的其余部分中跟随进行时无忧。

您不需要记住变量名称和类型,因为我们在使用它们时会讨论所有代码。您需要花时间仔细查看它们,并对它们有一点熟悉。此外,随着我们的进行,如果有任何地方看起来不清楚,回头参考这个头文件可能是值得的。

现在我们可以添加一整长串的函数。添加以下所有突出显示的代码,看看你能否弄清楚它们的作用。密切关注每个函数的返回类型、参数和名称。这对于理解我们将在项目的其余部分中编写的代码至关重要。它们告诉我们关于每个函数的什么信息?添加以下突出显示的代码,然后我们将对其进行检查:

 
  

首先注意,所有函数都是公共的。这意味着我们可以使用类的实例从函数中调用所有这些函数,代码如下:。

假设是类的一个完全设置好的实例,之前的代码将返回的副本。将这段代码放入真实的上下文中,我们可以在函数中编写如下代码:

 
  

之前的代码会在正确的位置绘制玩家图形,就好像精灵是在函数中声明的一样。这就像我们之前对假设的类所做的一样。

在我们继续在相应的文件中实现(编写定义)这些函数之前,让我们依次仔细看看每一个:

  • 此函数如其名称所示。它将准备好对象供使用,包括将其放在起始位置(生成)。请注意,它不返回任何数据,但它有三个参数。它接收一个名为的,它将是当前级别的大小和位置,一个将包含屏幕分辨率的,以及一个将保存背景瓦片大小的。
  • : 一旦我们让玩家能够在波之间升级,当他们死亡时,我们需要能够夺走并重置这些能力。
  • : 此函数只做一件事,即返回玩家上次被僵尸击中的时间。在检测碰撞时,我们将使用此函数,它将确保玩家不会因与僵尸接触而受到过多惩罚。
  • : 此函数返回描述包含玩家图形的矩形的水平和垂直浮点坐标的。这对于碰撞检测再次非常有用。
  • : 这与略有不同,因为它是一个,只包含玩家图形中心的 X 和 Y 位置。
  • : 中的代码有时需要知道玩家当前面向的方向(以度为单位)。三点钟为零度,顺时针增加。
  • : 如前所述,此函数返回代表玩家的精灵的副本。
  • , , , : 这四个函数没有返回类型或参数。它们将从函数中调用,然后类将能够在按下WASD键时采取行动。
  • , , , : 这四个函数没有返回类型或参数。它们将从函数中调用,然后类将能够在释放WASD键时采取行动。
  • : 这将是整个类中唯一相对较长的函数。它将从每帧调用一次。它将做一切必要的工作,以确保玩家对象的数据已更新,以便进行碰撞检测和绘制。请注意它不返回数据,但接收自上一帧以来经过的时间量,以及一个,其中包含鼠标指针或准星的水平和垂直屏幕位置。

请注意,这些是整数屏幕坐标,与浮点世界坐标不同。

  • : 当玩家选择使玩家更快时,可以从升级屏幕调用的函数。
  • : 当玩家选择使玩家更强壮(拥有更多健康)时,可以从升级屏幕调用的另一个函数。
  • : 与前一个函数相比,这个函数的一个微妙但重要的区别在于它将增加玩家的健康值,直到当前设置的最大值。当玩家拾取健康道具时,将使用此函数。
  • : 由于健康水平如此动态,我们需要能够确定玩家在任何给定时刻有多少健康。此函数返回一个包含该值的。与变量一样,现在应该清楚每个函数的用途。此外,与变量一样,随着项目的进展,使用其中一些函数的原因和确切上下文只有在我们进行项目时才会显现。

您不需要记住函数名称、返回类型或参数,因为我们将在使用它们时讨论所有代码。您需要花时间仔细查看它们,以及之前的解释,并对它们更加熟悉一些。此外,随着我们的进行,如果有任何地方看起来不清楚,回头参考这个头文件可能是值得的。

现在我们可以继续进行我们函数的核心部分,即定义。

最后,我们可以开始编写实际执行我们类工作的代码。

右键单击解决方案资源管理器中的源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击选择C++文件(,然后在名称字段中键入。最后,单击添加按钮。现在我们准备好为我们的第一个类编写文件了。

以下是必要的包含指令,后面是构造函数的定义。记住,当我们首次实例化类型的对象时,构造函数将被调用。将此代码添加到文件中,然后我们可以更仔细地查看:

 
  

在构造函数中,当然,它与类名相同且没有返回类型,我们编写了一些代码,开始设置对象以便随时使用。

要非常清楚:当我们从函数中编写这段代码时,这段代码将运行。

 
  

暂时不要添加上述代码。

我们只需从相关的常量中初始化、和。然后将玩家图形加载到中,将与关联,并将的原点设置为中心。

请注意神秘的注释,表明我们将返回到纹理的加载以及一些重要的相关问题。一旦我们发现了问题并学到了更多的 C++知识,我们将改变处理这个纹理的方式。我们将在第八章中进行这样的操作,指针、标准模板库和纹理管理

接下来,我们将编写函数。我们只会创建类的一个实例。然而,我们需要在当前关卡中生成它,每一波都需要。这就是函数将为我们处理的内容。将以下代码添加到文件中。确保仔细检查细节并阅读注释:

 
  

前面的代码首先将和的值初始化为传入的高度和宽度的一半。这样做的效果是将玩家移动到关卡的中心,而不管其大小如何。

接下来,我们将传入的所有坐标和尺寸复制到相同类型的成员对象中。当前竞技场的大小和坐标的细节使用如此频繁,这样做是有道理的。现在我们可以使用来执行任务,比如确保玩家不能穿过墙壁。此外,我们将传入的复制到成员变量中,以达到相同的目的。我们将在函数中看到和的作用。

最后两行代码将的参数中的屏幕分辨率复制到的成员变量中。

现在添加函数的非常简单的代码。当玩家死亡时,我们将使用它来重置他们可能使用的任何升级:

 
  

直到我们几乎完成项目时,我们才会编写实际调用函数的代码,但是当我们需要时,它已经准备好了。

在下一段代码中,我们将添加另外两个函数。它们将处理玩家被僵尸击中时发生的情况。我们将能够调用并传入当前游戏时间。我们还将能够查询玩家上次被击中的时间,通过调用。当我们有了一些僵尸时,这些函数将如何有用将变得明显!

将这两个新函数添加到文件中,然后我们将更仔细地检查 C++:

 
  

函数的代码非常简单。返回中存储的任何值。

函数有点更深入和微妙。首先,语句检查传入的时间是否比中存储的时间晚 200 毫秒。如果是,就用传入的时间更新,并且从当前值中减去。这个语句中的最后一行代码是。请注意,子句只是向调用代码返回。

这个函数的整体效果是,每秒只能从玩家身上扣除最多五次健康点。请记住,我们的游戏循环可能每秒运行数千次。在这种情况下,如果没有限制,僵尸只需要与玩家接触一秒钟,就会扣除成千上万的健康点。 函数控制和限制了这种情况。它还通过返回或让调用代码知道是否已经注册了新的命中。

这段代码意味着我们将在函数中检测僵尸和玩家之间的碰撞。然后我们将调用“player.hit()”来确定是否扣除任何健康点。

接下来,对于类,我们将实现一堆 getter 函数。它们使我们能够将数据整洁地封装在类中,同时使它们的值可用于函数。

在上一个代码块之后添加以下代码,然后我们将讨论每个函数的确切作用:

 
  

前面的代码非常直接。前面的五个函数中的每一个都返回我们的成员变量的值。仔细看看每个,并熟悉哪个函数返回哪个值。

接下来的八个简短的函数使键盘控件(我们将从中使用)能够改变我们的对象中包含的数据。将代码添加到文件中,然后我将总结它的工作原理:

 
  

前面的代码有四个函数(,,,),它们将相关的布尔变量(,,,)设置为。另外四个函数(,,,)则相反,将相同的“布尔”变量设置为。现在,类的实例可以清楚地知道哪些键被按下,哪些没有。

下一个函数是做所有繁重工作的函数。 函数将在我们游戏循环的每一帧上调用一次。添加接下来的代码,然后我们将详细讨论它。如果你跟着前面的八个函数,并且记得我们如何为“Timber!!!”项目中的云动画,你可能会发现以下大部分代码都很容易理解:

 
  

前面代码的第一部分移动了玩家精灵。四个语句检查与移动相关的“布尔”变量(,,,)中哪些是 true,并相应地更改和。与“Timber!!!”项目相同的计算移动量的公式被使用。

“位置(+或-)速度*经过的时间。”

在这四个语句之后,调用并传入。精灵现在已经根据该帧的正确量进行了调整。

接下来的四个语句检查或是否超出了当前竞技场的任何边缘。请记住,当前竞技场的范围是在函数中存储在中的。让我们看看这四个语句中的第一个,以便理解它们所有的含义:

 
  

前面的代码测试了 是否大于 减去一个瓷砖的大小()。当我们创建背景图形时,这个计算将检测玩家是否偏离墙壁。

当 语句为真时,计算 用于初始化 。这使得玩家图形的中心无法偏离右侧墙壁的左侧边缘。

我们刚刚讨论过的 语句后面的下三个 语句对其他三面墙做了同样的事情。

代码的最后两行计算并设置玩家精灵的旋转角度(面向)。这行代码可能看起来有点复杂,但它只是使用了十分成熟的三角函数,即使用了准星的位置( 和 )和屏幕中心( 和 )。

如何使用这些坐标以及 Pi(3.141)是非常复杂的,这就是为什么它被包装在一个方便的函数中供我们使用。如果您想更详细地探索三角函数,可以在上这样做。 类的最后三个函数使玩家速度提高 20%,生命值增加 20%,并分别增加传入的玩家生命值。

将此代码添加到 文件的末尾,然后我们将仔细查看:

 
  

}

在前面的代码中, 和 函数分别增加了存储在 和 中的值。这些值通过将起始值乘以 0.2 并加上当前值来增加 20%。这些函数将在玩家在关卡之间选择要改进的角色属性时,从 函数中调用。

从 中的 参数中获取一个 值。这个 值将由一个名为 的类提供,我们将在第九章中编写,碰撞检测、拾取物品和子弹。 成员变量增加了传入的值。然而,对于玩家来说有一个陷阱。 语句检查 是否超过了 ,如果超过了,则将其设置为 。这意味着玩家不能简单地从拾取物品中获得无限的生命值。他们必须在关卡之间谨慎平衡他们选择的升级。

当然,我们的 类实际上无法做任何事情,直到我们实例化它并在游戏循环中让它工作。在这之前,让我们先了解一下游戏摄像机的概念。

在我看来,SFML 类是最整洁的类之一。如果在完成本书后,您制作游戏而不使用媒体或游戏库,您将真正注意到缺少 。

类允许我们将游戏视为发生在自己的世界中,具有自己的属性。我是什么意思?当我们创建游戏时,通常是在尝试创建一个虚拟世界。那个虚拟世界很少,如果有的话,是以像素为单位的,很少,如果有的话,会与玩家的显示器像素数完全相同。我们需要一种方式来抽象我们正在构建的虚拟世界,以便它可以是我们喜欢的任何大小或形状。

另一种看待 SFML 的方式是作为玩家查看我们虚拟世界的一部分的摄像机。大多数游戏都会有多个摄像机或对世界的视图。

例如,考虑一个分屏游戏,两个玩家可以在同一个世界的不同部分,不同时间。

或者考虑一个游戏,屏幕上有一个小区域代表整个游戏世界,但是在非常高的层次上,或者缩小,就像一个迷你地图。

即使我们的游戏比前两个示例简单得多,不需要分屏或迷你地图,我们可能还是想要创建一个比正在播放的屏幕更大的世界。当然,这就是僵尸竞技场的情况。

如果我们不断地移动游戏摄像机以显示虚拟世界的不同部分(通常是跟踪玩家),HUD 会发生什么?如果我们绘制分数和其他屏幕 HUD 信息,然后滚动世界以跟随玩家,那么分数将相对于该摄像机移动。

SFML 类很容易实现所有这些功能,并且通过非常简单的代码解决了这个问题。关键是为每个摄像机创建一个实例。也许为迷你地图创建一个,为滚动游戏世界创建一个,然后为 HUD 创建一个。

的实例可以根据需要移动、调整大小和定位。因此,主可以跟踪玩家,迷你地图视图可以保持在屏幕的固定缩小角落,而 HUD 可以覆盖整个屏幕并且永远不会移动,尽管主可以随着玩家的移动而移动。

让我们看一些使用几个实例的代码。

这段代码是为了介绍类。不要将此代码添加到僵尸竞技场项目中。

创建并初始化几个实例:

 
  

前面的代码创建了两个填充 1920 x 1080 监视器的对象。现在我们可以在保持完全不变的情况下对进行一些魔术操作:

 
  

当我们操纵视图的属性时,我们就像之前展示的那样。当我们向视图绘制精灵、文本或其他对象时,我们必须明确将视图设置为窗口的当前视图:

 
  

现在我们可以在该视图中绘制我们想要的一切:

 
  

玩家可能在任何坐标。这并不重要,因为是围绕图形中心的。

现在我们可以将 HUD 绘制到中。请注意,就像我们按照从后到前的顺序绘制单个元素(背景、游戏对象、文本等)一样,我们也按照从后到前的顺序绘制视图。因此,HUD 在主游戏之后绘制:

 
  

最后,我们可以以通常的方式绘制或显示窗口和当前帧的所有视图:

 
  

如果您想要深入了解 SFML View,超出了这个项目所需的范围,包括如何实现分屏和迷你地图,那么 Web 上最好的指南是在官方 SFML 网站上上。

现在我们已经了解了,我们可以开始编写僵尸竞技场函数,并真正使用我们的第一个。在第十章中,分层视图和实现 HUD,我们将为 HUD 介绍第二个实例,修复它,并将其层叠在主的顶部。

在这个游戏中,我们将需要一个稍微升级的游戏引擎在中。特别是,我们将有一个名为的枚举,它将跟踪游戏的当前状态。然后,在整个中,我们可以包装我们的代码的部分,以便在不同的状态下发生不同的事情。

解决方案资源管理器中右键单击文件,然后选择重命名。将名称更改为。这将是包含我们的函数和实例化和控制所有类的代码的文件。

我们从现在熟悉的函数和一些包含指令开始。请注意,增加了一个类的包含指令。

将以下代码添加到文件中:

 
  

前面的代码除了行之外没有任何新内容,这意味着我们现在可以在我们的代码中使用类。

让我们充实一下我们的游戏引擎。接下来的代码做了很多事情。在添加代码时,请务必阅读注释,以了解发生了什么。然后我们将详细讨论它。

在函数的开头添加突出显示的代码:

 
  

让我们快速浏览一下我们刚刚输入的代码的每个部分。在函数的内部,我们有这段代码:

 
  

前面的代码创建了一个名为的新枚举类。然后代码创建了一个名为的实例。枚举现在可以是声明中定义的四个值之一。这些值是、、和。这四个值将正是我们需要的,用于跟踪和响应游戏在任何给定时间可能处于的不同状态。请注意,不可能同时保存多个值。

紧接着,我们添加以下代码:

 
  

前面的代码声明了一个名为的。我们通过调用函数来初始化的两个变量(和)分别为和。对象现在保存了游戏运行的显示器的分辨率。最后一行代码使用适当的分辨率创建了一个名为的新。

接下来的代码创建了一个 SFML 对象。视图的位置(最初)位于显示器像素的确切坐标处。如果我们要在当前位置使用这个进行一些绘图,它将完全没有任何效果。然而,我们最终将开始移动这个视图,以便关注玩家需要看到的游戏世界的部分。然后,当我们开始使用一个保持固定的第二个(用于 HUD)时,我们将看到这个如何跟踪动作,而另一个保持静态以显示 HUD:

 
  

接下来,我们创建一个来处理计时和一个名为的对象,它将保持游戏经过的总时间。随着项目的进展,我们将引入更多的变量和对象来处理计时:

 
  

接下来的代码声明了两个向量。一个包含两个浮点数,名为,另一个包含两个整数,名为。鼠标指针有点反常,因为它存在于两个不同的坐标空间。如果你愿意,你可以把它们想象成平行宇宙。首先,当玩家在世界中移动时,我们需要跟踪十字准星在世界中的位置。这些将是浮点坐标,并将存储在中。当然,显示器本身的实际像素坐标永远不会改变。它们将始终是 0,0 到水平分辨率-1,垂直分辨率-1。我们将使用存储在中的整数来跟踪鼠标指针相对于这个坐标空间的位置:

 
  

最后,我们要使用我们的类。这行代码将导致构造函数()执行。如果您想要刷新对这个函数的记忆,请参考:

 
  

这个对象将保存起始的水平和垂直坐标以及宽度和高度。一旦初始化,我们将能够通过诸如、、和的代码访问当前竞技场的大小和位置详情:

 
  

我们之前添加的代码的最后部分当然是我们的主游戏循环:

 
  

您可能已经注意到,代码变得相当长了。让我们谈谈这种不便之处。

使用类和函数进行抽象的一个优点是,我们的代码文件的长度(行数)可以减少。尽管我们将在这个项目中使用超过十几个代码文件,但中的代码长度在最后仍然会变得有点难以控制。在最终项目中,我们将探讨更多抽象和管理代码的方法。

现在,使用这个提示来保持事情的可管理性。请注意,在 Visual Studio 的代码编辑器的左侧,有许多+和-符号,其中一个显示在下一个图像中:

管理代码文件

每个代码块(、、等)都会有一个符号。您可以通过单击+和-符号来展开和折叠这些块。我建议将当前不在讨论中的所有代码都折叠起来。这将使事情变得更清晰。

此外,我们可以创建自己的可折叠块。我建议将主游戏循环开始之前的所有代码制作成一个可折叠块。为此,选择代码,右键单击,然后选择Outlining | Hide Selection,如下图所示:

管理代码文件

现在您可以单击+和-符号来展开和收缩块。每次我们在主游戏循环之前添加代码(这将经常发生),您可以展开代码,添加新行,然后再次折叠。当折叠时,代码看起来像下面这张图片:

管理代码文件

这比以前更容易管理。

正如您所看到的,前面代码的最后部分是游戏循环,。现在我们将把注意力转向这一部分。具体来说,我们将编写游戏循环的输入处理部分。

我们将添加的下一个代码非常长。这并不复杂,我们将在一分钟内仔细研究它。

只需添加下面代码中显示的突出显示的代码到主游戏循环中:

 
  

在前面的代码中,我们实例化了一个类型的对象。我们将使用,就像在 Timber!!!项目中一样,来轮询系统事件。为此,我们将前一个块的其余代码放入一个带有条件的循环中。这将在每一帧中保持循环,直到没有更多事件需要处理为止。

在这个循环内,我们处理我们感兴趣的事件。首先,我们测试事件。如果在游戏处于状态时按下Enter键,那么我们将切换到。

如果在游戏处于状态时按下Enter键,那么我们将切换到并重新启动。在从切换到后重新启动的原因是,当游戏暂停时,经过的时间仍然会累积。如果我们不重新启动时钟,那么所有对象会更新它们的位置,就好像帧花了很长时间。随着我们在这个文件中完善其余代码,这一点将变得更加明显。

然后我们有一个测试,看看在状态下是否按下了Enter键。如果是的话,那么将被改变为。

请注意,状态是显示主屏幕的状态。因此,状态是玩家刚刚死亡后以及玩家第一次运行应用程序时的状态。每个游戏中玩家首先要做的事情是选择一个属性来提升(升级)。

在前面的代码中,有一个最终的条件来测试状态是否为。这个块是空的,我们将在整个项目中添加代码到其中。

由于我们将在整个项目中的许多不同部分添加代码到这个文件,因此值得花时间了解游戏可能处于的不同状态以及我们在哪里处理它们。根据需要,折叠和展开不同的、和块也会非常有益。

花些时间彻底熟悉我们刚刚编写的、和块。我们将经常参考它们。

接下来,在前面的代码之后,仍然在游戏循环内,仍然在处理输入,添加这段突出显示的代码。注意现有的代码(未突出显示)显示了新代码的确切位置:

 
  

在前面的代码中,我们首先测试玩家是否按下了Esc键。如果按下,游戏窗口将被关闭。

接下来,在一个大的块内,我们依次检查WASD键。如果按下某个键,我们调用相应的函数。如果没有按下,则调用相关的函数。

这段代码确保在每一帧中,玩家对象将准确更新哪些W、ASD键被按下,哪些没有被按下。和函数将信息存储在成员布尔变量(、、、)中。然后类会根据这些布尔值在每帧中响应函数,我们将在游戏循环的更新部分调用它。

现在我们可以处理键盘输入,以便玩家在每局游戏开始和每波之间升级。添加并学习下面突出显示的代码,然后我们将讨论它。

 
  

在前面的代码中,所有代码都包含在一个测试中,以查看的当前值是否为,我们处理键盘键1、2、3、4、56。在每个块中,我们只需将设置为。我们将在第十一章音效、文件 I/O 和完成游戏中稍后添加处理每个升级选项的代码。

这段代码的作用是:

  1. 如果是,等待按下1、2、3、4、56键。
  2. 当按下时,将更改为。
  3. 当状态改变时,在块内,嵌套的块将运行。
  4. 在此块中,我们设置的位置和大小,为,将所有信息传递给,并重新启动。

现在我们有了一个真正的生成的玩家对象,它知道自己的环境并可以响应按键。我们现在可以在每次循环中更新场景。

确保将游戏循环的输入处理部分的代码整理好,因为我们现在已经完成了。接下来的代码在游戏循环的更新部分。添加并学习下面突出显示的代码,然后我们可以讨论它:

 
  

首先注意,所有先前的代码都包含在一个测试中,以确保游戏处于状态。如果游戏暂停、结束或玩家正在选择升级,我们不希望这段代码运行。

首先,我们重新启动时钟,并将上一帧所用的时间存储在变量中:

 
  

接下来,我们将上一帧所用的时间添加到游戏已运行的累积时间中:

 
  

现在,我们使用函数返回的值初始化一个名为的。对于大多数帧,这将是一个小数。这非常适合传递给函数,用于计算玩家精灵的移动量。

现在我们可以使用函数初始化。

你可能会对获取鼠标位置的略微不寻常的语法感到好奇?这被称为静态函数。如果我们在一个类中用关键字定义一个函数,我们可以使用类名调用该函数,而无需类的实例。C++面向对象编程有很多这样的怪癖和规则。随着我们的学习,我们会看到更多。

然后,我们使用 SFML 的函数在上初始化。我们在本章前面讨论了这个函数时,正在讨论类。

此时,我们现在可以调用并传入和鼠标的位置,这是必需的。

我们将玩家的新中心存储在名为的中。目前,这是未使用的,但在项目的后期我们会用到它。

然后,我们可以用代码将视图居中于玩家最新位置的中心。

现在我们可以将玩家绘制到屏幕上。添加这个突出显示的代码,将主游戏循环的绘制部分分成不同的状态:

 
  

在前面的代码中,部分,我们清除屏幕,将窗口视图设置为,然后用绘制玩家精灵。

在处理完所有不同的状态之后,代码以通常的方式显示场景,使用。

您可以运行游戏,看到我们的玩家角色在响应鼠标移动时旋转。

当您运行游戏时,您需要按Enter来开始游戏,然后输入16之间的数字来模拟选择升级选项。然后游戏将开始。

您还可以在(空的)500 x 500 像素的竞技场内移动玩家。您可以在屏幕中央看到我们孤独的玩家,如下所示:

开始编写主游戏循环

但是,您无法感受到任何移动,因为我们还没有实现背景。我们将在下一章中实现。

问题)我注意到我们已经编写了许多类的函数,但我们并没有使用。

答案)我们不再需要不断返回类,我们已经添加了整个代码,这是我们在整个项目中需要的。到第十一章结束时,音效,文件 I/O 和完成游戏,我们将充分利用所有这些功能。

问题)我学过其他语言,C++中的 OOP 看起来简单得多。

答案)这是面向对象编程及其基本原理的介绍。它不仅仅是这样。我们将在整本书中学习更多面向对象编程的概念和细节。

呼!这是一个漫长的过程。在本章中,我们学到了很多。我们发现了面向对象编程的基础知识,包括如何使用封装来控制类外部代码如何访问成员变量。我们建立了我们的第一个真正的类,并在即将成为我们新游戏 Zombie Arena 的开始中使用了它。

如果围绕 OOP 和类的一些细节不是很清楚,不要太担心。我这么说的原因是因为我们将在本书的剩余部分中制作类,我们使用它们越多,它们就会变得越清晰。

在下一章中,我们将通过探索精灵表来构建我们的竞技场背景。我们还将学习 C++引用,它允许我们操纵变量,即使它们超出了范围(在另一个函数中)。

在第四章中,我们谈到了作用域。在函数或内部代码块中声明的变量只在该函数或块中具有作用域(可以被看到或使用)。仅使用我们目前拥有的 C++知识,这可能会导致问题。如果我们需要处理一些复杂对象,这些对象在中是必需的,我们该怎么办?这可能意味着所有的代码都必须在中。

在本章中,我们将探讨C++引用,它允许我们处理变量和对象,否则它们将超出作用域。此外,引用将帮助我们避免在函数之间传递大型对象,这是一个缓慢的过程。这是一个缓慢的过程,因为每次这样做时,都必须制作变量或对象的副本。

掌握了关于引用的这些新知识后,我们将看一下 SFML 类,它允许我们构建一个大图像,可以使用来自单个图像文件的多个图像快速有效地绘制到屏幕上。在本章结束时,我们将拥有一个可扩展的、随机的、滚动的背景,使用引用和一个对象。

我们现在将讨论以下主题:

  • C++引用
  • SFML 顶点数组
  • 编写随机和滚动的背景

当我们向函数传递值或从函数返回值时,这正是我们所做的。通过传递/返回。发生的情况是变量持有的值的副本被制作,并发送到函数中使用。

这具有双重意义:

  • 如果我们希望函数对变量进行永久性更改,那么这个系统对我们来说就不好了。
  • 当制作副本以作为参数传递或从函数返回时,会消耗处理能力和内存。对于一个简单的,甚至可能是一个精灵,这是相当微不足道的。然而,对于一个复杂的对象,也许是整个游戏世界(或背景),复制过程将严重影响我们游戏的性能。

引用是这两个问题的解决方案。引用是一种特殊类型的变量。引用指的是另一个变量。一个例子将是有用的:

 
  

在上面的代码中,我们声明并初始化了一个常规的,名为。然后我们声明并初始化了一个引用,名为。跟随类型的引用运算符确定正在声明一个引用。

引用名称前面的前缀是可选的,但对于记住我们正在处理引用是有用的。

现在我们有一个名为的,它存储值,以及一个引用,名为,它指的是。

我们对所做的任何事情都可以通过看到,我们对所做的任何事情实际上都是在做。看一下以下代码:

 
  

在前面的代码中,我们声明了一个名为的。接下来,我们声明了一个引用,名为,它指的是。请记住,我们对所做的任何事情都可以被看到,我们对所做的任何事情实际上都是在做。

因此,当我们像这样增加分数时:

 
  

分数变量现在存储值 11。此外,如果我们输出,它也将输出 11。以下代码行如下:

 
  

现在实际上持有值 12,因为我们对所做的任何事情实际上都是对做的。

如果您想知道这是如何工作的,那么在下一章中讨论指针时将会有更多揭示。但简单来说,您可以将引用视为存储计算机内存中的位置/地址。内存中的位置与其引用的变量存储其值的位置相同。因此,对引用或变量的操作具有完全相同的效果。

现在,更重要的是更多地讨论引用的原因。使用引用有两个原因,我们已经提到过。这里再次总结一下:

  • 更改/读取另一个函数中变量/对象的值,否则超出范围
  • 传递/返回而不制作副本(因此更有效)

研究这段代码,然后我们可以讨论它:

 
  

先前的代码以和两个函数的原型开始。函数接受三个变量,而函数接受两个变量和一个引用。

当调用函数并传入变量,和时,将复制这些值并操作新的本地变量以添加(,和)。因此,中的仍然为零。

当调用函数时,和再次按值传递。但是,是按引用传递的。当将加到的值分配给引用时,实际上发生的是该值被分配回函数中的。

很明显,我们永远不需要实际使用引用来处理如此简单的事情。但是,它确实演示了按引用传递的机制。

先前的代码演示了如何使用引用来使用另一个作用域中的代码来更改变量的值。除了非常方便之外,按引用传递也非常高效,因为不会进行复制。使用引用传递的示例有点模糊,因为太小,没有真正的效率提升。在本章后期,我们将使用引用传递整个级别布局,效率提升将是显著的。

引用有一个需要注意的地方!您必须在创建引用时将其分配给一个变量。这意味着它并不完全灵活。现在不要担心这个问题。我们将在下一章中进一步探讨引用以及它们更灵活(稍微更复杂)的关系,指针。

这对于来说并不重要,但对于类的大对象来说可能很重要。当我们实现僵尸竞技场游戏的滚动背景时,我们将使用这种确切的技术。

我们几乎准备好实现滚动背景了。我们只需要学习关于 SFML 顶点数组和精灵表。

精灵表是一组图像,可以是动画帧或完全独立的图形,包含在一个图像文件中。仔细观察包含四个单独图像的精灵表,这些图像将用于绘制僵尸竞技场的背景:

什么是精灵表?

SFML 允许我们以与本书中迄今为止的每个纹理完全相同的方式加载精灵表作为常规纹理。当我们将多个图像加载为单个纹理时,GPU 可以更有效地处理它。

实际上,现代 PC 可以处理这四个纹理而不使用精灵表。由于我们的游戏将逐渐对硬件要求更高,因此值得使用这些技术。

当我们从精灵表中绘制图像时,我们需要确保引用我们需要的精灵表部分的精确像素坐标:

什么是精灵表?

上一张图标记了每个部分/瓦片在精灵表中位置的坐标。这些坐标称为纹理坐标。我们将在我们的代码中使用这些纹理坐标来绘制我们需要的部分。

首先,我们需要问:什么是顶点?顶点是单个图形点,一个坐标。这个点由水平和垂直位置定义。顶点的复数是顶点。然后,顶点数组是整个顶点的集合。

在 SFML 中,顶点数组中的每个顶点还具有颜色和相关的额外顶点(一对坐标)称为纹理坐标。纹理坐标是我们想要使用的图像在精灵表中的位置。我们很快将看到如何使用单个顶点数组定位图形并选择要在每个位置显示的精灵表的一部分。

SFML 类可以保存不同类型的顶点集。但是每个只能保存一种类型的集。我们使用适合场合的集类型。

视频游戏中常见的场景包括但不限于以下基元类型:

  • :每个点一个单独的顶点。
  • 线:每组两个顶点定义线的起点和终点。
  • 三角形:每个点三个顶点。在使用的成千上万个中,这可能是复杂的 3D 模型或成对创建简单矩形(如精灵)中最常见的。
  • 四边形:每组四个顶点,一种方便的方式来从精灵表中映射矩形区域。

在这个项目中,我们将使用四边形。

僵尸竞技场背景将由随机排列的方形图像组成。您可以将此排列视为地板上的瓦片。

在这个项目中,我们将使用带有四边形集的顶点数组。每个顶点将是四个(一个四边形)的集的一部分。每个顶点将定义背景瓦片的一个角落。每个纹理坐标将根据精灵表中特定图像的适当值进行保持。

让我们看一些代码来开始。这不是我们在项目中将使用的确切代码,但它非常接近,并使我们能够在转向我们将使用的实际实现之前研究顶点数组。

就像我们创建类的实例时一样,我们声明我们的新对象。以下代码声明了一个名为背景的类型的新对象:

 
  

我们希望让我们的实例知道我们将使用哪种类型的基元。请记住,点、线、三角形和四边形都有不同数量的顶点。通过设置来保存特定类型,将可以知道每个基元的起始位置。在我们的情况下,我们想要四边形。以下是将执行此操作的代码:

 
  

与常规的 C++数组一样,需要设置大小。但是,更加灵活。它允许我们在游戏运行时更改其大小。大小可以在声明的同时配置,但是我们的背景需要随着每一波扩展。类通过函数提供了这种功能。以下是将设置我们的竞技场大小为 10x10 个瓦片大小的代码:

 
  

在上一行代码中,第一个是宽度,第二个是高度,是四边形中的顶点数。我们可以直接传入 400,但是像这样显示计算清楚我们正在做什么。当我们真正编写项目时,我们将进一步声明每个计算部分的变量。

现在我们有一个准备好配置其数百个顶点。以下是我们如何设置前四个顶点(第一个四边形)的位置坐标:

 
  

这是我们如何将这些相同顶点的纹理坐标设置为精灵表中的第一个图像。图像文件中的这些坐标是(在左上角)到(在右下角):

 
  

如果我们想将纹理坐标设置为精灵表中的第二个图像,我们将编写如下代码:

 
  

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

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

代码需要非常智能。它需要知道何时是边缘瓷砖,以便可以使用精灵表中的墙图像。它还需要使用适当的变量,知道精灵表中每个背景瓷砖的位置以及所需竞技场的总体大小。

我们将通过将所有代码放在单独的函数和单独的文件中,使这种复杂性变得可管理。我们将通过使用 C++引用,使在中可用。

我们很快就会谈到这些细节。您可能已经注意到,在任何时候我们都没有关联纹理(使用顶点数组的精灵表)。

我们可以以与加载任何其他纹理相同的方式加载精灵表作为纹理,如下面的代码所示:

 
  

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

 
  

前面的代码比将每个瓷砖作为单独精灵绘制要高效得多。

在继续之前,请注意之前看起来有点奇怪的。您可能会立刻想到这与引用有关。这里发生的是,我们传递纹理的地址而不是实际的纹理。我们将在下一章中了解更多关于这个的知识。

现在我们可以利用我们对引用和顶点数组的知识来实现 Zombie Arena 项目的下一个阶段。

我们将创建一个在单独文件中创建背景的函数。我们将确保通过使用顶点数组引用,背景将可用(在范围内)到函数。

由于我们将编写其他与函数共享数据的函数,我们将在一个新的头文件中提供这些函数的原型,并在中包含它们(使用包含指令)。

为了实现这一点,让我们首先制作新的头文件。在解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,突出显示(通过左键单击)头文件(),然后在名称字段中键入。最后点击添加按钮。现在我们准备好为我们的新函数编写头文件。

在这个新的头文件中,添加以下突出显示的代码,包括函数原型:

 
  

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

现在我们可以创建一个新的文件,在其中我们将编写函数定义。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,突出显示(通过左键单击)C++文件),然后在名称字段中键入。最后点击添加按钮。现在我们准备好编写将创建我们的背景的函数定义。

将以下代码添加到文件中,然后我们将对其进行审查:

 
  

在前面的代码中,我们编写了函数签名以及标记函数主体的大括号。

在函数主体中,我们声明并初始化了三个新的常量,用于保存我们在函数其余部分需要引用的值。它们是、和。常量指的是精灵表中每个图块的像素大小。指的是精灵表中不同图块的数量。我们可以向精灵表中添加更多图块,将更改为匹配,即将仍然有效。指的是每个四边形中有四个顶点。与反复输入数字相比,使用这个常量更不容易出错,这点更清晰。

然后,我们声明并初始化了两个变量,和。这些变量可能看起来显而易见,因为它们的用途。它们的名称已经透露了它们的用途,但值得指出的是,它们指的是世界在图块数量上的宽度和高度,而不是像素。和变量通过将传入的竞技场的高度和宽度除以常量来初始化。

接下来,我们将首次使用我们的引用。请记住,我们对所做的任何事情实际上都是对传入的变量所做的,该变量在函数中是可见的(或者当我们编写它时将可见)。

首先,我们准备使用将顶点数组设置为四边形,然后通过调用将其调整为合适的大小。我们向函数传递的结果,这恰好等于我们在准备完成后顶点数组的数量。

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

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

 
  

我们刚刚添加的代码通过使用嵌套的循环来遍历顶点数组,首先遍历前四个顶点。,等等。

我们使用数组表示法访问数组中的每个顶点。等等。使用数组表示法,我们调用函数。

在函数中,我们传递每个顶点的水平和垂直坐标。我们可以通过使用、和的组合来以编程方式计算这些坐标。

在前面的代码结束时,我们通过使用代码将定位到下一个嵌套循环的位置,使其向前移动四个位置(加四)。

当然,所有这些只是设置了我们顶点的坐标;它并没有从精灵表中分配纹理坐标。这是我们接下来要做的事情。

为了清楚地表明新代码放在哪里,我已经在我们刚刚编写的所有代码的上下文中显示了它。添加并学习突出显示的代码:

 
  

前面的代码设置了每个顶点在精灵表中的坐标。请注意有点长的 if 条件。该条件检查当前四边形是否是竞技场中的第一个或最后一个四边形。如果是,则意味着它是边界的一部分。然后我们可以使用一个简单的公式,使用和来从精灵表中选择墙壁纹理。

逐个初始化数组表示法和成员,以为每个顶点分配墙纹理在精灵表中的适当角落。

以下代码包含在块中。这意味着每次通过嵌套的 for 循环时,当四边形不代表边界/墙砖时,它将运行。在现有代码中添加突出显示的代码,然后我们可以检查它:

 
  

前面的新代码首先使用一个公式来为随机数生成器提供种子,每次通过循环时都会有不同的公式。然后,变量用一个介于 0 和之间的数字进行初始化。这正是我们随机选择瓦片类型所需要的。

代表泥土或草。名称是任意的。

现在,通过将乘以来声明和初始化一个名为的变量。现在我们在精灵表中有一个垂直参考点,指向当前四边形随机选择的纹理的起始高度。

现在,我们使用一个简单的公式,涉及和,来为纹理的每个角分配精确的坐标到适当的顶点。

现在我们可以让我们的新函数在游戏引擎中发挥作用了。

我们已经完成了棘手的事情,这将很简单。有三个步骤:

  1. 创建一个。
  2. 在每个波次升级后初始化它。
  3. 在每一帧中绘制它。

添加以下突出显示的代码来声明一个名为的,并加载作为纹理:

 
  

添加以下代码来调用函数,传入作为引用和作为值。请注意在突出显示的代码中,我们还修改了初始化变量的方式。按照突出显示的代码精确添加:

 
  

请注意,我们已经替换了这行代码,因为我们直接从函数的返回值中获取了该值。

为了以后的代码清晰起见,你应该删除这行代码及其相关的注释。我只是将它注释掉,以便为新代码提供更清晰的上下文。

最后,是时候开始绘制了。这很简单。我们只需要调用并传递以及纹理:

 
  

如果你想知道前面那个奇怪的符号是什么意思,那么一切将在下一章中变得清晰起来。

你现在可以按照下图运行游戏:

使用背景

请注意,玩家精灵在竞技场范围内平稳滑动和旋转。尽管主要代码中绘制了一个小竞技场,但函数可以创建我们告诉它的任何大小的竞技场。我们将在第十一章中看到比屏幕更大的竞技场:声音效果,文件 I/O 和完成游戏

以下是一些可能在你脑海中的问题:

Q)你能再总结一下这些参考资料吗?

A)你必须立即初始化引用,并且不能将其更改为引用另一个变量。使用引用与函数一起,这样你就不会在副本上工作。这对效率很有好处,因为它避免了制作副本,并帮助我们更容易地将代码抽象成函数。

Q)有没有一种简单的方法来记住使用引用的主要好处?

A)为了帮助你记住引用的用途,考虑一下这首简短的韵文:

移动大对象可能会使我们的游戏变得卡顿,通过引用传递比复制更快。

在本章中,我们发现了 C++引用,它们是特殊的变量,充当另一个变量的别名。当我们通过引用而不是值传递变量时,我们对引用所做的任何工作都会发生在调用函数中的变量上。

我们还学习了关于顶点数组,并创建了一个充满四边形的顶点数组,以从精灵表中绘制瓦片作为背景。

当然,房间里的大象是,我们的僵尸游戏没有任何僵尸。现在让我们通过学习 C++指针和标准模板库来解决这个问题。

在这一章中,我们将学到很多,也会在游戏中完成很多工作。我们将首先学习关于指针的基本 C++主题。指针是保存内存地址的变量。通常,指针将保存另一个变量的内存地址。这听起来有点像引用,但我们将看到它们更加强大。我们还将使用指针来处理一个不断扩大的僵尸群。

我们还将学习标准模板库(STL),这是一组允许我们快速轻松地实现常见数据管理技术的类集合。

一旦我们理解了 STL 的基础知识,我们就能够利用这些新知识来管理游戏中的所有纹理,因为如果我们有 1000 个僵尸,我们实际上不希望为每一个加载一份僵尸图形到 GPU 中。

我们还将深入研究面向对象编程,并使用静态函数,这是一个类的函数,可以在没有类实例的情况下调用。同时,我们将看到如何设计一个类,以确保只能存在一个实例。当我们需要保证代码的不同部分将使用相同的数据时,这是理想的。

在这一章中,我们将学习以下主题:

  • 学习关于指针
  • 学习关于 STL
  • 使用静态函数和单例类实现类
  • 实现一个指向一群僵尸的指针
  • 编辑一些现有的代码,使用类为玩家和背景

在学习 C++编程时,指针可能会引起挫折。但实际上,这个概念很简单。

指针是一个保存内存地址的变量。

就是这样!没有什么需要担心的。对初学者可能引起挫折的是语法,我们用来处理指针的代码。考虑到这一点,我们将逐步介绍使用指针的代码的每个部分。然后你可以开始不断地掌握它们。

在这一部分,我们实际上会学到比这个项目需要的更多关于指针。在下一个项目中,我们将更多地使用指针。尽管如此,我们只是浅尝辄止。强烈建议进一步学习,我们将在最后一章更多地谈论这个问题。

我很少建议记忆事实、数字或语法是学习的最佳方式。然而,记忆与指针相关的相当简短但至关重要的语法可能是值得的。这样它就会深深地扎根在我们的大脑中,我们永远不会忘记它。然后我们可以讨论为什么我们需要指针,并研究它们与引用的关系。指针的类比可能会有所帮助。

如果一个变量就像一座房子,它的内容就是它所持有的值,那么指针就是房子的地址。

我们在上一章中学到,当我们将值传递给函数,或者从函数返回值时,实际上是在制作一个完全与之前相同的新房子。我们正在复制传递给函数或从函数返回的值。

此时,指针可能开始听起来有点像引用。那是因为它们有点像引用。然而,指针更加灵活、强大,并且有它们自己特殊和独特的用途。这些特殊和独特的用途需要特殊和独特的语法。

与指针相关的主要运算符有两个。第一个是取地址运算符:

 
  

第二个是解引用运算符:

 
  

现在我们将看一下我们如何使用这些运算符与指针。

你会注意到的第一件事是地址运算符与引用运算符相同。为了增加一个渴望成为 C++游戏程序员的人的困境,这两个运算符在不同的上下文中做不同的事情。从一开始就知道这一点是很有价值的。如果你盯着一些涉及指针的代码看,感觉自己要发疯,知道这一点:

你是完全理智的!你只需要看看上下文的细节。

现在你知道,如果有什么东西不清楚和立即明显,那不是你的错。指针不是清晰和立即明显的,但仔细观察上下文会揭示发生了什么。

有了这个知识,你需要比以前的语法更加关注指针,以及这两个运算符是什么(地址运算符和解引用),我们现在可以开始看一些真正的指针代码了。

确保在继续之前已经记住了这两个运算符。

要声明一个新的指针,我们使用解引用运算符以及指针将要保存的变量的类型。看一下代码,我们将进一步讨论它:

 
  

这段代码声明了一个名为的新指针,可以保存类型变量的地址。请注意,我说的是可以保存类型的变量。与其他变量一样,指针也需要初始化一个值才能正确使用它。与其他变量一样,名称是任意的。

通常习惯上,将指针的名称前缀为。这样在处理指针时更容易记住,并且可以将它们与常规变量区分开来。

解引用运算符周围使用的空格是可选的(因为 C++在语法上很少关心空格),但建议使用,因为它有助于可读性。看一下以下三行代码,它们做的事情完全相同。

我们刚刚在前面的例子中看到的格式,带有解引用运算符紧挨着类型:

 
  

解引用运算符两侧的空格是可选的。

 
  

解引用运算符紧挨着指针的名称:

 
  

了解这些可能性是值得的,这样当你阅读代码时,也许在网上,你会明白它们都是一样的。在本书中,我们将始终使用与类型紧挨着的解引用运算符的第一个选项。

就像常规变量只能成功地包含适当类型的数据一样,指针也应该只保存适当类型的变量的地址。

指向类型的指针不应该保存 String、Zombie、Player、Sprite、float 或任何其他类型的地址。

接下来我们可以看到如何将变量的地址存入指针中。看一下以下代码:

 
  

在前面的代码中,我们声明了一个名为的变量,并将其初始化为。尽管我们以前从未讨论过,但这个变量必须在计算机内存中的某个地方。它必须有一个内存地址。

我们可以使用地址运算符访问这个地址。仔细看前面代码的最后一行。我们用的地址初始化了,就像这样:

 
  

我们的现在保存了常规变量的地址。在 C++术语中,我们说指向 health。

我们可以通过将传递给一个函数来使用它,这样函数就可以处理,就像我们用引用一样。如果我们只是这样做,指针就没有存在的理由了。

指针,不像引用,可以重新初始化以指向不同的地址。看一下以下代码:

 
  

现在指向变量。

当然,我们的指针名称现在有点模糊,可能应该被称为。在这里要理解的关键是我们可以进行这种重新赋值。

到目前为止,我们实际上还没有使用指针来做任何其他事情,而只是简单地指向(保存内存地址)。 让我们看看如何访问指针指向的地址存储的值。 这将使它们真正有用。

因此,我们知道指针保存内存中的地址。 如果我们在游戏中输出这个地址,也许在我们的 HUD 中,声明并初始化后,它可能看起来像这样:。

它只是一个值。 一个代表内存中地址的值。 在不同的操作系统和硬件类型上,这些值的范围会有所不同。 在本书的上下文中,我们从不需要直接操作地址。 我们只关心指向的地址存储的值是什么。

变量使用的实际地址是在游戏执行时(在运行时)确定的,因此,在编写游戏时,无法知道变量的地址以及指针中存储的值。

我们通过使用解引用运算符访问指针指向的地址存储的值。 以下代码直接操作了一些变量,并使用了指针。 试着跟着走,然后我们会解释一下。

警告! 接下来的代码毫无意义(有点刻意)。 它只是演示使用指针。

 
  

在前面的代码中,我们声明了两个 int 变量,和。 然后我们分别用零和十初始化它们。 接下来,我们声明了两个指向的指针。 它们是和。 我们在声明它们的同时初始化它们,以保存(指向)变量和的地址。

接下来,我们以通常的方式给加上十分,。 然后我们看到,通过在指针上使用解引用运算符,我们可以访问指向的地址存储的值。 以下代码实际上改变了由指向的变量存储的值:

 
  

前面代码的最后一部分解引用了两个指针,将指向的值分配为指向的值:

 
  

和现在都等于。

我们可以用指针做更多的事情。 以下是一些有用的事情。

到目前为止,我们所见过的所有指针都指向作用域仅限于它们创建的函数的内存地址。 因此,如果我们声明并初始化一个指向局部变量的指针,当函数返回时,指针、局部变量和内存地址都会消失。 它超出了作用域。

到目前为止,我们一直在使用预先决定的固定内存量。 此外,我们一直在使用的内存由操作系统控制,变量在我们调用和返回函数时会丢失和创建。 我们需要的是一种使用始终在作用域内的内存的方法,直到我们完成为止。 我们希望拥有可以自己调用并负责的内存。

当我们声明变量(包括指针)时,它们位于称为堆栈的内存区域中。 还有另一个内存区域,尽管由操作系统分配/控制,但可以在运行时分配。 这另一个内存区域称为自由存储,有时也称为

堆上的内存没有特定函数的作用域。 从函数返回不会删除堆上的内存。

这给了我们很大的力量。 通过访问计算机运行游戏的资源所限制的内存,我们可以规划具有大量对象的游戏。 在我们的情况下,我们想要一个庞大的僵尸群。 然而,正如蜘蛛侠的叔叔会毫不犹豫地提醒我们的那样,伴随着巨大的力量而来的是巨大的责任

让我们看看如何使用指针来利用自由存储器上的内存,以及在完成后如何将该内存释放回操作系统。

要创建一个指向堆上值的指针,首先我们需要一个指针:

 
  

在上一行代码中,我们声明了一个指针,就像我们以前看到的那样,但是由于我们没有将其初始化为指向一个变量,而是将其初始化为。我们这样做是因为这是一个好习惯。考虑解引用一个指针(更改它指向的地址的值),当你甚至不知道它指向什么时。这将是编程等同于去射击场,蒙住某人的眼睛,让他转个圈,然后告诉他射击。通过将指针指向空(),我们不会对其造成任何伤害。

当我们准备在自由存储器上请求内存时,我们使用关键字,如下面的代码行所示:

 
  

指针现在保存了在自由存储器上的内存地址,该内存大小刚好可以容纳一个值。

任何分配的内存在程序结束时都会被返回。然而,重要的是要意识到,除非我们释放它,否则这段内存永远不会被释放(在我们的游戏执行中)。如果我们继续从自由存储器中获取内存而不归还,最终它将耗尽并且游戏会崩溃。

我们不太可能因为偶尔从自由存储器中获取大小的内存块而耗尽内存。但是,如果我们的程序有一个频繁执行请求内存的函数或循环,最终游戏将变慢然后崩溃。此外,如果我们在自由存储器上分配了大量对象并且没有正确管理它们,那么这种情况可能会很快发生。

下面的代码行,将之前由指向的自由存储器上的内存返回(删除):

 
  

现在,之前由指向的内存不再属于我们,我们必须采取预防措施。尽管内存已经返回给操作系统,但仍然保存着这段内存的地址,这段内存不再属于我们。

下面的代码行确保不能用于尝试操作或访问这段内存:

 
  

如果指针指向的地址无效,则称为野指针悬空指针。如果您尝试对悬空指针进行解引用,如果幸运的话,游戏会崩溃,并且会收到内存访问违规错误。如果不幸的话,您将创建一个非常难以找到的错误。此外,如果我们使用自由存储器上的内存超出函数生命周期,我们必须确保保留指向它的指针,否则我们将泄漏内存。

现在我们可以声明指针并将它们指向自由存储器上新分配的内存。我们可以通过对它们进行解引用来操作和访问它们指向的内存。当我们完成后,我们可以将内存返回到自由存储器,并且我们知道如何避免悬空指针。

让我们看看指针的一些更多优势。

首先,我们需要编写一个具有指针在签名中的函数,如下面的代码:

 
  

前面的函数只是对指针进行解引用,并将存储在指定地址的值加一。

现在我们可以使用该函数,并显式地传递一个变量的地址或另一个指向变量的指针:

 
  

现在,如前面的代码所示,在函数内部,我们实际上正在操作来自调用代码的变量,并且可以使用变量的地址或指向该变量的指针来这样做。

指针不仅适用于常规变量。我们还可以声明指向用户定义类型(如我们的类)的指针。这是我们声明指向类型为的对象的指针的方法:

 
  

我们甚至可以直接从指针访问对象的成员函数,就像下面的代码一样:

 
  

在这个项目中,我们不需要使用指向对象的指针,我们将在下一个项目中更加仔细地探讨它们。

数组和指针有一些共同之处。数组名是一个内存地址。更具体地说,数组的名称是数组中第一个元素的内存地址。换句话说,数组名指向数组的第一个元素。理解这一点的最好方法是继续阅读,看下一个例子。

我们可以创建一个指向数组保存的类型的指针,然后使用指针以与我们使用数组完全相同的方式使用相同的语法:

 
  

这也意味着一个具有接受指针原型的函数也接受指针指向的类型的数组。当我们建立我们不断增加的僵尸群时,我们将利用这一事实。

关于指针和引用之间的关系,编译器在实现我们的引用时实际上使用指针。这意味着引用只是一个方便的工具(在幕后使用指针)。你可以把引用看作是一种自动变速箱,适合在城里开车,而指针是一种手动变速箱,更复杂,但正确使用时能够获得更好的结果/性能/灵活性。

指针有时有点棘手。事实上,我们对指针的讨论只是对这个主题的一个介绍。要想熟练掌握它们,唯一的方法就是尽可能多地使用它们。在完成这个项目时,你需要理解关于指针的以下内容:

  • 指针是存储内存地址的变量。
  • 我们可以将指针传递给函数,直接从调用函数的范围内调用函数中操作值。数组是第一个元素的内存地址。我们可以将这个地址作为指针传递,因为这正是它的作用。
  • 我们可以使用指针指向自由存储器上的内存。这意味着我们可以在游戏运行时动态分配大量内存。

为了进一步使指针的问题变得神秘,C++最近进行了升级。现在有更多的方法来使用指针。我们将在最后一章学习一些关于智能指针的知识。

还有一个主题要讨论,然后我们可以再次开始编写僵尸竞技场项目。

STL 是一组数据容器和操作我们放入这些容器中的数据的方法。更具体地说,它是一种存储和操作不同类型的 C++变量和类的方法。

我们可以将不同的容器视为定制和更高级的数组。STL 是 C++的一部分。它不是一个可选的需要设置的东西,比如 SFML。

STL 是 C++的一部分,因为它的容器和操作它们的代码对许多应用程序需要使用的许多类型的代码至关重要。

简而言之,STL 实现了我们和几乎每个 C++程序员几乎肯定需要的代码,至少在某个时候可能会经常需要。

如果我们要编写自己的代码来包含和管理我们的数据,那么我们不太可能像编写 STL 的人那样高效地编写它。

因此,通过使用 STL,我们保证使用最佳编写的代码来管理我们的数据。甚至 SFML 也使用 STL。例如,在幕后,类使用 STL。

我们所需要做的就是从可用的容器中选择正确的类型。通过 STL 可用的容器类型包括以下内容:

  • 向量:就像一个带有助推器的数组。动态调整大小,排序和搜索。这可能是最有用的容器。
  • 列表:允许对数据进行排序的容器。
  • Map:一种允许用户将数据存储为键/值对的关联容器。这是一种数据是查找另一种数据的关键的地方。地图也可以增长和缩小,以及进行搜索。
  • Set:一个容器,保证每个元素都是唯一的。

有关 STL 容器类型和解释的完整列表,请访问以下链接:

在僵尸竞技场游戏中,我们将使用地图。

如果您想一窥 STL 为我们节省的复杂性,那么请看一下这个教程,该教程实现了列表将要做的事情。请注意,该教程仅实现了列表的最简单的基本功能:。

我们可以很容易地看到,如果我们探索 STL,我们将节省大量时间,并且最终会得到一个更好的游戏。让我们更仔细地看看如何使用 Map,然后我们将看到它在僵尸竞技场游戏中对我们有多有用。

Map是一个动态可调整大小的容器。我们可以轻松地添加和删除元素。与 STL 中的其他容器相比,地图的特殊之处在于我们访问其中的数据的方式。

地图中的数据是成对存储的。考虑这样一种情况,您登录到一个帐户,可能使用用户名和密码。地图非常适合查找用户名,然后检查相关密码的值。

地图也可以用于诸如帐户名称和数字,或者公司名称和股价等事物。

请注意,当我们使用 STL 中的 Map 时,我们决定形成键值对的值的类型。这些值可以是数据类型,如 string 和 int,例如帐户号码,用户名和密码等字符串,或者用户定义的类型,如对象。

接下来是一些真实的代码,让我们熟悉地图。

这是我们如何声明一个 Map 的方式:

 
  

前一行代码声明了一个名为的新,它具有 String 对象的键,每个键将引用一个 int 值。

现在我们可以存储字符串到数据类型(如 int)的键值对,接下来我们将看到如何做到这一点。

让我们继续向帐户添加键值对:

 
  

现在有一个可以使用 John 作为键访问的地图条目。以下代码向帐户添加了另外两个条目:

 
  

我们的地图中有三个条目。让我们看看如何访问帐户号码。

我们访问数据的方式与添加数据的方式完全相同,即使用键。例如,我们可以将键存储的值赋给一个新的 int,就像这样的代码:

 
  

int 变量现在存储值。我们可以对存储在地图中的值做任何我们可以对该类型的值做的事情。

从我们的地图中取值也很简单。下一行代码删除了键及其关联的值:

 
  

让我们看看我们可以用 Map 做些什么。

我们可能想知道我们的地图中有多少键值对。下一行代码就是这样做的:

 
  

现在,int 变量 size 保存的值是 2。这是因为 accounts 保存了和 Wilson 的值,我们删除了 John。

地图最相关的特性是使用键查找值的能力。我们可以这样测试特定键的存在与否:

 
  

在前面的代码中,“!= accounts.end”用于确定键是否存在或不存在。如果搜索的键在地图中不存在,那么将成为语句的结果。

我们已经看到了如何使用循环来循环/迭代数组的所有值。如果我们想对 Map 做类似的事情怎么办?

以下代码显示了我们如何循环遍历 accounts Map 的每个键值对,并为每个帐户号码加一:

 
  

for 循环的条件可能是前面代码中最有趣的部分。条件的第一部分是最长的部分。如果我们把代码分解开来,它会更容易理解。

代码是一种类型。我们声明了一个适用于具有和键值对的的。迭代器的名称是。我们将从返回的值赋给。迭代器现在保存了中的第一个键值对。

循环的条件的其余部分工作如下。代码表示循环将继续直到达到的末尾,只是在循环中每次通过时步进到下一个键值对。

在循环内,访问键值对的第二个元素,将值加一。请注意,我们可以使用访问键(它是键值对的第一部分)。

在循环的条件中的代码相当冗长,特别是类型。C++提供了一种简洁的方法来减少冗长,即使用关键字。使用关键字,我们可以改进前面的代码如下:

 
  

auto 关键字指示编译器自动为我们推断类型。这将在我们编写的下一个类中特别有用。

与本书中涵盖的几乎每个 C++概念一样,STL 是一个庞大的主题。已经有整整一本书专门讨论 STL。然而,到目前为止,我们已经了解到足够的知识来构建一个使用 STL Map 来存储 SFML 对象的类。然后我们可以通过使用文件名作为键的键值对来检索/加载纹理。

为什么我们要增加这种额外的复杂性,而不是像到目前为止一样继续使用类,随着我们的进行,这将变得明显。

成千上万的僵尸代表了一个新的挑战。不仅加载、存储和操作三种不同僵尸纹理的成千上万个副本会占用大量内存,还会占用大量处理能力。我们将创建一个新类型的类来解决这个问题,并允许我们只存储每种纹理的一个副本。

我们还将以这样的方式编写类,使得它只能有一个实例。这种类型的类被称为单例

单例是一种设计模式,一种已被证明有效的代码结构方式。

此外,我们还将编写类,以便可以直接通过类名在我们的游戏代码中的任何地方使用它,而无需访问实例。

创建新的头文件。在解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(通过左键单击)头文件( ,然后在名称字段中输入。

将以下代码添加到文件中,然后我们可以讨论它:

 
  

在前面的代码中,注意我们为 STL 中的包含了一个包含指令。我们声明了一个包含 String 和 SFML 键值对的。这个被称为。

在前面的代码中,接下来是这行:

 
  

前一行代码非常有趣。我们声明了一个指向类型对象的静态指针,称为。这意味着类有一个与自身相同类型的对象。不仅如此,因为它是静态的,所以可以通过类本身使用,而无需类的实例。当我们编写相关的文件时,我们将看到如何使用它。

在类的部分,我们有构造函数的原型。构造函数不带参数,并且像通常一样没有返回类型。这与默认构造函数相同。我们将使用定义来覆盖默认构造函数,使我们的单例工作如我们所希望的那样。

我们还有另一个名为的函数。让我们再次看一下签名,并分析到底发生了什么:

 
  

首先,注意函数返回一个的引用。这意味着将返回一个引用,这是有效的,因为它避免了对可能是相当大的图形进行复制。还要注意函数声明为。这意味着该函数可以在没有类实例的情况下使用。该函数以作为常量引用作为参数。这样做的效果是双重的。首先,操作是有效的,其次,因为引用是常量的,所以它是不可改变的。

现在我们可以创建一个新的文件,其中包含函数定义。这将使我们能够看到我们新类型的函数和变量背后的原因。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项窗口中,通过左键单击突出显示C++文件),然后在名称字段中键入。最后,单击添加按钮。我们现在准备编写类的代码。

添加以下代码,然后我们可以讨论它:

 
  

在前面的代码中,我们将指向类型的指针初始化为。在构造函数中,代码确保等于。如果不是,则游戏将退出执行。然后代码将指针分配给此实例。现在考虑一下这段代码发生在哪里。代码在构造函数中。构造函数是我们从类中创建对象实例的方式。因此,实际上我们现在有一个指向的指针,指向自身的唯一实例。

将最后一部分代码添加到文件中。接下来的注释比代码更多。在添加代码时,请检查代码并阅读注释,然后我们可以一起讨论:

 
  

您可能会注意到前面代码中的第一件事是关键字。关键字在前一节中有解释。

如果您想知道替换的实际类型是什么,请看一下前面代码中每次使用后面的注释。

在代码的开头,我们获取了对的引用。然后我们尝试获取一个迭代器,该迭代器表示传入的文件名()所代表的键值对。如果我们找到匹配的键,我们返回的纹理。否则,我们将纹理添加到中,然后将其返回给调用代码。

诚然,类引入了许多新概念(单例、函数、常量引用、和关键字)和语法。再加上我们刚刚学习了指针和 STL,这一部分的代码可能有点令人生畏。

重点是现在我们有了这个类,我们可以在代码中随意使用纹理,而不必担心内存不足或者在特定函数或类中访问特定纹理。我们很快就会看到如何使用。

现在我们有了类,以确保我们的僵尸纹理易于获取,并且只加载到 GPU 一次,我们可以着手创建一整群僵尸。

我们将把僵尸存储在一个数组中,由于构建和生成一群僵尸的过程涉及相当多的代码行,因此将其抽象为一个单独的函数是一个很好的选择。很快我们将编写函数,但首先,当然,我们需要一个类。

构建代表僵尸的类的第一步是在头文件中编写成员变量和函数原型。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,突出显示(单击左键)头文件(.h),然后在名称字段中键入。

将以下代码添加到文件中:

 
  

先前的代码声明了类的所有私有成员变量。在先前的代码顶部,我们有三个常量变量来保存每种类型僵尸的速度。一个非常缓慢的爬行者,一个稍快的膨胀者,以及一个相当快的追逐者。我们可以尝试调整这三个常量的值,以帮助平衡游戏的难度级别。值得一提的是,这三个值仅用作每种僵尸类型速度的起始值。正如我们将在本章后面看到的,我们将从这些值中以一小百分比变化每个僵尸的速度。这样可以防止相同类型的僵尸在追逐玩家时聚集在一起。

接下来的三个常量确定了每种僵尸类型的生命值。请注意,膨胀者是最坚韧的,其次是爬行者。为了平衡,追逐者僵尸将是最容易被杀死的。

接下来我们有两个更多的常量和,这些将帮助我们确定每个僵尸的个体速度。当我们编写文件时,我们将看到具体如何做到这一点。

在这些常量之后,我们声明了一堆变量,这些变量应该看起来很熟悉,因为我们在类中有非常相似的变量。、、和变量分别代表了僵尸对象的位置、精灵、速度和生命值。

最后,在先前的代码中,我们声明了一个布尔值,当僵尸活着并追捕时为,但当其生命值降到零时为,它只是我们漂亮背景上的一滩血迹。

现在来完成文件。添加下面突出显示的函数原型,然后我们将讨论它们:

 
  

在先前的代码中,有一个函数,我们可以在僵尸被子弹击中时调用它。该函数可以采取必要的步骤,比如从僵尸身上减少生命值(减少的值)或者将其杀死(将设置为 false)。

函数返回一个布尔值,让调用代码知道僵尸是活着还是死了。我们不希望对走过血迹时发生碰撞检测或从玩家身上减少生命值。

函数接受一个起始位置、一个类型(爬行者、膨胀者或追逐者,用一个整数表示),以及一个种子,用于一些我们将在下一节中看到的随机数生成。

就像在类中一样,类有和函数,用于获取代表僵尸所占空间的矩形和可以在每一帧绘制的精灵。

上一个代码中的最后一个原型是方法。我们可能已经猜到它会接收自上一帧以来的经过的时间,但也要注意它接收了一个名为的。这个向量确实是玩家中心的确切坐标。我们很快就会看到我们如何使用这个向量来追逐玩家。

接下来我们将编写 Zombie 类的实际功能,即函数定义。

创建一个新的文件,其中包含函数定义。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击C++文件),然后在名称字段中键入。最后,单击添加按钮。我们现在准备好编写类了。

现在将以下代码添加到文件中:

 
  

首先添加必要的包含指令,然后添加这一行。您可能还记得我们在一些情况下在对象声明前面加上了。这个指令意味着我们在这个文件中的代码不需要这样做。

现在添加以下代码,这是函数的定义。添加后,请仔细研究代码,然后我们将逐步讲解:

 
  

函数的第一件事是基于传入的类型进行。在块内,为每种僵尸类型都有一个 case。根据类型和相应的纹理,速度和生命值被初始化为相关的成员变量。

有趣的是,我们使用静态的函数来分配纹理。这意味着无论我们生成多少僵尸,GPU 的内存中最多只会有三种纹理。

前面代码的最后三行(不包括注释)分别执行以下操作:

  • 用作参数传入的变量来初始化随机数生成器。
  • 使用函数和和常量声明和初始化浮点变量。结果是一个介于零和一之间的分数,可以用来使每个僵尸的速度都是独特的。我们之所以要这样做,是因为我们不希望僵尸们太过拥挤。
  • 现在我们可以将乘以,这样我们就得到了一个速度在这种特定类型的僵尸速度常量的百分比内的僵尸。

解决了速度之后,我们将和中传入的位置分别赋给和。

前面列表中的最后两行代码设置了精灵的原点为中心,并使用向量来设置精灵的位置。

现在将以下代码添加到文件中,用于函数:

 
  

函数非常简单。将减一,然后检查是否小于零。

如果小于零,将设置为 false,将僵尸的纹理替换为血迹,并返回 true 给调用代码,这样它就知道僵尸现在已经死了。

如果僵尸幸存下来,返回 false。

添加下面的三个 getter 函数,它们只是将一个值返回给调用代码:

 
  

前面的三个函数相当容易理解,也许除了函数使用函数来获取之外,这个例外。这个函数返回给调用代码。

最后,为类添加函数的代码;仔细查看代码,然后我们将逐步讲解:

 
  

首先将和复制到本地变量和中。

接下来有四个语句。它们测试僵尸是否在当前玩家位置的左侧、右侧、上方或下方。这四个语句在评估为时,使用通常的公式来适当地调整僵尸的和值。更具体地说,代码是。

在四个语句之后,被移动到它的新位置。

然后我们使用与之前用于玩家和鼠标指针的相同计算;不过这次是用于僵尸和玩家。这个计算找到了面向玩家的僵尸所需的角度。

最后,我们调用来实际旋转僵尸精灵。请记住,这个函数将在游戏的每一帧中为每个(活着的)僵尸调用。

现在我们有了一个类来创建一个活着的、攻击的和可杀死的僵尸,我们想要生成一整群它们。

为了实现这一点,我们将编写一个单独的函数,并使用指针,以便我们可以引用在中声明但在不同范围内配置的我们的僵尸群。

在 Visual Studio 中打开文件,并添加下面显示的突出显示的代码行:

 
  

现在我们有了一个原型,我们可以编写函数定义了。

创建一个新的文件,其中包含函数定义。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(通过左键单击)C++文件(),然后在名称字段中键入。最后,单击添加按钮。

将下面显示的代码添加到文件中并学习它。之后,我们将把它分解成块并讨论它:

 
  

让我们再次逐步查看所有以前的代码。

首先我们添加了现在熟悉的包含指令:

 
  

接下来是函数签名。请注意,函数必须返回一个指向对象的指针。我们将创建一个对象的数组。一旦我们创建了这个僵尸群,我们将返回这个数组。当我们返回数组时,实际上是返回数组的第一个元素的地址。这与本章前面学到的内容相同,也就是指针。函数签名还显示我们有两个参数。第一个参数将是当前僵尸群所需的僵尸数量,第二个参数是一个,用于保存当前竞技场的大小,以便创建这个僵尸群。

在函数签名之后,我们声明了一个名为的指向类型的指针,并用数组的第一个元素的内存地址进行初始化,这个数组是我们在堆上动态分配的。

 
  

接下来的代码简单地将竞技场的边界复制到、、和中。我们从右边和底部减去 20 像素,同时在顶部和左边加上 20 像素。我们使用这四个局部变量来帮助定位每个僵尸。我们进行了 20 像素的调整,以防止僵尸出现在墙上。

 
  

现在我们进入一个循环,该循环将遍历从零到的每个对象在僵尸数组中的元素:

 
  

在循环内,代码的第一件事是初始化随机数生成器,然后生成一个介于零和三之间的随机数。这个数字存储在变量中。我们将使用变量来决定僵尸是在竞技场的左侧、顶部、右侧还是底部生成。我们还声明了两个变量和。这两个变量将临时保存当前僵尸的实际水平和垂直坐标。

 
  

在循环中,我们有一个块,包含四个语句。注意语句分别为 0、1、2 和 3,而 switch 语句中的参数是 side。在每个 case 块内,我们使用一个预定值(minX、maxX、minY 或 maxY)和一个随机生成的值来初始化 x 和 y。仔细观察每个预定值和随机值的组合,你会发现它们适合将当前僵尸随机放置在竞技场的左侧、顶部、右侧或底部。这样做的效果是,每个僵尸可以在竞技场的外边缘随机生成:

 
  

在循环内部,我们再次初始化随机数生成器,并生成一个介于 0 和 2 之间的随机数。我们将这个数字存储在 type 变量中。type 变量将决定当前僵尸是 Chaser、Bloater 还是 Crawler。

确定类型后,我们在数组中的当前对象上调用函数。作为提醒,传入函数的参数确定了僵尸的起始位置和僵尸的类型。看似任意的被传入,因为它被用作一个唯一的种子,可以在适当的范围内随机变化僵尸的速度。这样可以防止我们的僵尸聚集在一起,而不是形成一群:

 
  

循环对中包含的每个僵尸重复一次,然后返回数组。再次提醒,数组只是它自身的第一个元素的地址。数组是在堆上动态分配的,因此在函数返回后它将持续存在:

 
  

现在我们可以让僵尸活过来。

我们有一个类和一个函数来随机生成一群僵尸。我们有单例作为一种简洁的方式来保存仅三个纹理,可以用于数十甚至数千个僵尸。现在我们可以在中将僵尸群添加到我们的游戏引擎中。

添加以下突出显示的代码以包含类。然后,在内部,我们初始化了唯一的实例,可以在游戏的任何地方使用:

 
  

接下来几行突出显示的代码声明了一些控制变量,用于波开始时僵尸的数量、仍需杀死的僵尸数量,当然还有一个名为的指针,我们将其初始化为。

添加突出显示的代码:

 
  

接下来,在部分嵌套的部分中,我们添加以下代码:

  • 将初始化为。随着项目的进展,这将最终变得动态,并基于当前波数。
  • 删除任何已分配的内存,否则每次调用都会占用越来越多的内存,而不释放先前僵尸群的内存
  • 然后调用并将返回的内存地址分配给
  • 将初始化为,因为在这一点上我们还没有杀死任何僵尸

添加我们刚刚讨论过的突出显示的代码:

 
  

现在将以下突出显示的代码添加到文件中:

 
  

新代码所做的一切就是循环遍历僵尸数组,检查当前僵尸是否还活着,如果是的话,就用必要的参数调用它的函数。

添加以下代码来绘制所有的僵尸:

 
  

先前的代码循环遍历所有的僵尸,并调用函数以允许方法发挥作用。我们不检查僵尸是否还活着,因为即使僵尸已经死亡,我们也希望绘制血迹。

在主函数的末尾,我们确保删除了我们的指针,尽管从技术上讲这并非必要,因为游戏即将退出,操作系统将在语句之后回收所有使用的内存:

 
  

您可以运行游戏,看到僵尸在竞技场的边缘生成。它们会立即以各自的速度直奔玩家而去。为了好玩,我增加了竞技场的大小,并将僵尸数量增加到 1000。

将僵尸群带回生命(重新活过来)

这将以失败告终!

请注意,由于我们在第六章中编写的代码,您还可以使用Enter键暂停和恢复僵尸群的袭击:面向对象编程,类和 SFML 视图

既然我们有了类,我们可能会一致地使用它来加载所有的纹理。让我们对加载背景精灵表和玩家纹理的现有代码进行一些非常小的修改。

在文件中,找到这段代码:

 
  

删除先前突出显示的代码,并用以下突出显示的代码替换,该代码使用我们的新类:

 
  

在文件中,在构造函数内,找到这段代码:

 
  

删除先前突出显示的代码,并用使用我们的新类的以下代码替换。此外,添加包含指令以将头文件添加到文件中。新代码如下所示,突出显示在上下文中:

 
  

从现在开始,我们将使用类加载所有纹理。

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

Q)指针和引用有什么区别?

A)指针就像带有助推器的引用。指针可以更改指向不同变量(内存地址),以及指向自由存储器上动态分配的内存。

Q)数组和指针有什么关系?

A)数组实际上是指向它们第一个元素的常量指针。

Q)您能提醒我一下关键字和内存泄漏吗?

A)当我们使用关键字在自由存储器上使用内存时,即使创建它的函数已经返回并且所有局部变量都消失了,它仍然存在。当我们使用自由存储器上的内存时,我们必须释放它。因此,如果我们使用自由存储器上的内存,我们希望它在函数的生命周期之外持续存在,我们必须确保保留指向它的指针,否则我们将泄漏内存。这就像把所有的东西放在我们的房子里然后忘记我们住在哪里一样!当我们从返回僵尸数组时,就像是把接力棒(内存地址)从传递给。这就像是说好的,这是你的一群僵尸 - 现在它们是你的责任了。我们不希望我们的 RAM 中有任何泄漏的僵尸,所以我们必须记得在指向动态分配内存的指针上调用。

您可能已经注意到,这些僵尸似乎并不那么危险。它们只是漂浮在玩家身边,而不留下任何伤痕。目前这是件好事,因为玩家没有办法自卫。

在下一章中,我们将制作另外两个类。一个用于弹药和生命值的拾取,另一个用于玩家可以射击的子弹。在完成这些之后,我们将学习如何检测碰撞,以便子弹和僵尸造成一些伤害,并且玩家可以收集拾取物品。

到目前为止,我们已经实现了游戏的主要视觉方面。我们有一个可控的角色在一个充满追逐他的僵尸的竞技场中奔跑。问题是它们彼此之间没有互动。僵尸可以毫无阻碍地穿过玩家。我们需要检测僵尸和玩家之间的碰撞。

如果僵尸能够伤害并最终杀死玩家,那么给玩家一些子弹是公平的。然后我们需要确保子弹能够击中并杀死僵尸。

同时,如果我们正在为子弹、僵尸和玩家编写碰撞检测代码,那么现在是添加用于健康和弹药拾取的类的好时机。

以下是我们将要做的事情以及我们将涵盖的主题顺序:

  • 射击子弹
  • 添加准星并隐藏鼠标指针
  • 生成拾取物品
  • 检测碰撞

我们将使用 SFML 的类来直观表示子弹。我们将编写一个类,其中包含一个成员以及其他成员数据和函数。我们将分几步向游戏中添加子弹:

  1. 首先,我们将编写文件。这将显示成员数据的所有细节和函数的原型。
  2. 接下来,我们将编写文件,其中当然将包含类所有函数的定义。当我们逐步进行时,我将解释类型的对象将如何工作和被控制。
  3. 最后,在函数中,我们将声明一个完整的子弹数组。我们还将实现射击的控制方案,管理玩家剩余的弹药,并进行重新加载。

让我们从第一步开始。

要创建新的头文件,右键单击 解决方案资源管理器中的头文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击头文件(,然后在名称字段中键入。

在文件中,添加以下私有成员变量以及类声明。然后我们可以运行并解释它们的用途:

 
  

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

接下来,我们声明了一个名为的,因为我们为每颗子弹使用了一个简单的非纹理图形,有点像我们在 Timber!!!中为时间条所做的那样。

代码然后声明了一个,它将跟踪子弹当前是否在空中飞行。这将使我们能够决定是否需要在每帧调用其函数,以及我们是否需要运行碰撞检测检查。

变量将(你可能猜到了)保存子弹的像素速度。它被初始化为的值,这有点随意,但效果很好。

接下来我们有另外两个变量,和。由于移动子弹的计算比移动僵尸或玩家的计算稍微复杂一些,我们将受益于这两个变量,我们将对它们进行计算。它们将用于决定每帧子弹位置的水平和垂直变化。

最后,对于前面的代码,我们有另外四个变量(、、和),它们将稍后初始化以保存子弹的水平和垂直位置的最大和最小值。

很可能有些变量的需求并不立即显而易见,但当我们在文件中看到它们各自发挥作用时,它们将变得更清晰。

现在将所有公共函数原型添加到文件中:

 
  

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

首先是函数,当然是构造函数。在这个函数中,我们将为每个实例设置好准备行动。

函数将在子弹已经在行动但需要停止时被调用。

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

函数的用途可以从其名称中得知,但它的工作方式值得讨论。现在,只需注意它有四个参数将被传入。这四个值代表子弹的起始(玩家所在位置)水平和垂直位置,以及垂直和水平目标位置(准星所在位置)。

函数返回一个,表示子弹的位置。这个函数将用于检测与僵尸的碰撞。您可能还记得来自第八章:指针、标准模板库和纹理管理中,僵尸也有一个函数。

接下来我们有函数,它返回一个类型的对象。正如我们讨论过的,每个子弹在视觉上都由一个对象表示。因此,函数将被用来获取当前状态的副本,以便绘制它。

最后,也希望如预期的那样,有函数,它有一个参数,表示自上次调用以来经过的一秒钟的时间。方法将在每一帧改变子弹的位置。

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

现在我们可以创建一个新的文件,其中包含函数定义。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击C++文件()来突出显示,然后在名称字段中键入。最后,单击添加按钮。我们现在准备好编写类了。

添加以下代码,这是包含指令和构造函数。我们知道这是构造函数,因为函数的名称与类名相同:

 
  

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

接下来是更实质性的函数。将以下代码添加到文件中,研究它,然后我们可以讨论它:

 
  

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

首先让我们回顾一下签名。函数接收子弹的起始和目标水平和垂直位置。调用代码将根据玩家精灵的位置和准星的位置提供这些值。这里是它的签名:

 
  

在函数内部,我们将设置为,并使用参数和定位子弹。这里是那段代码:

 
  

现在我们使用一些简单的三角学来确定子弹的行进斜率。子弹的水平和垂直进展必须根据在子弹起始和目标之间绘制的线的斜率而变化。变化的速率不能相同,否则非常陡峭的射击将在水平位置到达之前到达垂直位置,对于较浅的射击则相反。

以下代码首先根据一条直线的方程推导出斜率。然后它检查斜率是否小于零,如果是,则乘以。这是因为传入的起始和目标坐标可以是负数或正数,我们总是希望每帧的进度量是正数。乘以只是将负数变成它的正数等价物,因为负数乘以负数得正数。实际的行进方向将在函数中处理,通过在这个函数中得到的正值进行加减。

接下来,我们通过将我们的子弹速度()除以斜率加一来计算水平到垂直距离的比率。这将允许我们根据子弹所指向的目标,每帧正确地改变子弹的水平和垂直位置。

最后,在代码的这一部分,我们为和赋值:

 
  

以下代码要简单得多。我们只是设置了子弹可以到达的最大水平和垂直位置。我们不希望子弹一直飞下去。我们将在函数中看到这一点,我们会测试子弹是否已经超过了它的最大或最小位置:

 
  

以下代码将代表子弹的RectangleShape移动到其起始位置。我们像以前经常做的那样使用函数:

 
  

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

 
  

函数只是将变量设置为。函数返回当前这个变量的值。所以我们可以看到让子弹飞出去,让它停下来,让我们知道当前的状态是什么。

函数返回一个,我们将看到如何使用每个游戏对象的来检测碰撞,很快就会看到。

最后,对于之前的代码,返回一个,所以我们可以在每一帧中绘制子弹。

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

 
  

在函数中,我们使用和乘以自上一帧以来的时间来移动子弹。记住,这两个变量的值是在函数中计算的,并且表示移动子弹所需的斜率(彼此的比率)。然后我们使用函数来实际移动。

在中我们做的最后一件事是测试子弹是否已经超过了它的最大射程。稍微复杂的语句检查和与在函数中计算的最大和最小值。这些最大和最小值存储在,,和中。如果测试为真,则设置为。

类已经完成。现在我们可以看看如何在函数中射击一些子弹。

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

  1. 为类添加必要的包含指令。
  2. 添加一些控制变量和一个数组来保存一些实例。
  3. 处理玩家按下R键重新装填。
  4. 处理玩家按下鼠标左键发射子弹。
  5. 在每一帧中更新所有正在飞行的子弹。
  6. 在每一帧中绘制正在飞行中的子弹。

添加包含指令以使 Bullet 类可用:

 
  

让我们继续下一步。

这里有一些变量来跟踪子弹、弹夹大小、备用/剩余子弹、弹夹中的子弹、当前射速(每秒开始为一颗),以及上一颗子弹被射击的时间。

添加突出显示的代码,我们可以继续看到本节中所有这些变量的实际运行情况:

 
  

接下来,让我们处理玩家按下R键时会发生什么,这个键用于重新装弹。

现在我们处理与射击子弹相关的玩家输入。首先,我们将处理按下R键重新装弹。我们使用 SFML 事件来实现。

添加下面突出显示的代码块。为了确保代码放在正确的位置,提供了大量上下文来展示。研究代码,然后我们可以讨论它:

 
  

先前的代码嵌套在游戏循环的事件处理部分()中,只有在游戏实际进行时执行的代码块内()。很明显,我们不希望在游戏结束或暂停时玩家重新装弹,通过描述的新代码实现了这一点。

在新代码本身中,我们首先测试是否按下了R键,使用。一旦检测到按下R键,剩下的代码就会执行。以下是、和块的结构:

 
  

先前的结构允许我们处理三种可能的情况。

  • 玩家按下了,并且他们有比弹夹能装下的更多的备用子弹。在这种情况下,弹夹被重新填充,备用子弹的数量减少。
  • 玩家有一些备用子弹,但不足以完全填满弹夹。在这种情况下,弹夹将填满玩家拥有的尽可能多的备用子弹,并且备用子弹的数量被设置为零。
  • 玩家按下了 R,但他们没有备用子弹。对于这种情况,我们实际上不需要改变变量。但是当我们在第十一章中实现声音时,我们会在这里播放声音效果,所以我们留下了空的块。

最后,让我们实际射击一颗子弹。

接下来,我们可以处理按下鼠标左键来实际射击子弹。添加下面突出显示的代码并仔细研究它:

 
  

所有先前的代码都包裹在一个语句中,只有当按下鼠标左键时执行,。请注意,即使玩家只是按住按钮,代码也会重复执行。我们现在要讨论的代码控制射速。

在先前的代码中,我们检查游戏中经过的总时间()减去玩家上次射击子弹的时间()是否大于除以当前射速,以及玩家弹夹中至少有一颗子弹。我们使用是因为这是一秒钟内的毫秒数。

如果这个测试成功,那么实际射击子弹的代码就会执行。射击子弹很容易,因为我们在类中已经做了所有的工作。我们只需在数组中的当前子弹上调用。我们传入玩家和准星的当前水平和垂直位置。子弹将由类的函数中的代码进行配置和发射。

我们所要做的就是跟踪子弹数组。首先我们增加变量。然后我们检查是否用语句发射了最后一颗子弹()。如果是最后一颗子弹,我们将设置为零。如果不是最后一颗子弹,那么下一颗子弹就准备好了,只要射速允许并且玩家按下鼠标左键。

最后,对于之前的代码,我们将子弹发射的时间存储在中,并减少。

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

添加高亮代码来循环遍历子弹数组,检查子弹是否在飞行,如果是,调用它的更新函数:

 
  

最后,我们可以绘制所有的子弹。

添加高亮代码来循环遍历数组,检查子弹是否在飞行中,如果是,就绘制它:

 
  

运行游戏来尝试子弹。注意你可以连续射击六次,然后需要按R重新装填。明显缺少的是弹夹中子弹数量和备用子弹数量的一些视觉指示。另一个问题是玩家很快就会用尽子弹,特别是因为子弹根本没有停止力。它们直接穿过僵尸。再加上玩家期望以鼠标指针而不是精确的准星瞄准,我们明显还有工作要做。

在下一章中,我们将通过 HUD 给出视觉反馈。接下来我们将用一个准星替换鼠标光标,然后在此之后生成一些拾取物品来补充子弹和生命值。最后,在本章中,我们将处理碰撞检测,使子弹和僵尸造成伤害,并使玩家能够真正获得拾取物品。

添加一个准星很容易,只需要一个新的概念。添加高亮代码,然后我们可以运行它:

 
  

首先我们在对象上调用函数。然后我们加载一个,声明一个,并以通常的方式初始化它。此外,我们将精灵的原点设置为它的中心,以使子弹飞向中心更加方便和简单,正如你所期望的那样。

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

 
  

接下来,正如你可能期望的那样,我们可以为每一帧绘制准星。在指定位置添加高亮代码行。这行代码不需要解释,但它在所有其他游戏对象之后的位置很重要,这样它就会被绘制在最上面:

 
  

现在你可以运行游戏,看到酷炫的准星,而不是鼠标光标:

给玩家一个准星

注意子弹是如何整齐地穿过准星中心的。射击机制的工作方式类似于允许玩家选择从腰部射击或瞄准射击。如果玩家保持准星靠近中心,他可以快速射击和转身,但必须仔细判断远处僵尸的位置。

或者,玩家可以直接将准星悬停在远处僵尸的头部,进行精确射击;然而,如果僵尸从另一个方向袭击,那么他就需要更远地移动准星。

对游戏的一个有趣改进是为每一枪增加一点小的随机不准确性。这种不准确性可能会在波之间的升级中得到缓解。

我们将编写一个类,其中有一个成员以及其他成员数据和函数。我们将在几个步骤中向我们的游戏中添加拾取物品:

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

让我们从第 1 步开始。

要创建新的头文件, 解决方案资源管理器右键单击 头文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击头文件( ,然后在名称字段中键入。

在文件中添加并学习以下代码,然后我们可以逐步进行:

 
  

之前的代码声明了类的所有私有变量。虽然这些变量的名称应该很直观,但为什么需要这么多变量可能并不明显。让我们从顶部开始逐个讲解:

  • :这个常量变量用于设置所有生命值拾取物的起始值。这个值将用于初始化变量,在游戏过程中需要对其进行操作。
  • :这个常量变量用于设置所有弹药拾取物的起始值。这个值将用于初始化变量,在游戏过程中需要对其进行操作。
  • :这个变量是拾取物在消失后重新生成前要等多久。它将用于初始化变量,在游戏过程中可以对其进行操作。
  • :这个变量确定拾取物在生成和消失之间持续多长时间。和前面三个常量一样,它有一个与之关联的非常量,可以在游戏过程中进行操作。它用于初始化。
  • :这是用来直观表示对象的精灵。
  • :这将保存当前竞技场的大小,以帮助拾取物在合理的位置生成。
  • :这个拾取物值多少生命值或弹药?当玩家升级生命值或弹药拾取物的值时会使用这个值。
  • :这将是生命值或弹药的零或一。我们本可以使用一个枚举类,但对于只有两个选项来说,这似乎有点杀鸡用牛刀。
  • :拾取物当前是否生成?
  • :拾取物生成后多长时间了?
  • :拾取物消失后多长时间了?
  • :这个拾取物在生成后应该存活多久?
  • :这个拾取物在消失后应该等多久才重新出现?

请注意,这个类的大部分复杂性是由于变量生成时间及其可升级的特性。如果拾取物在收集后只是重新生成并具有固定值,那么这将是一个非常简单的类。我们需要我们的拾取物可以升级,所以玩家被迫制定策略来通过僵尸的波次。

然后,在文件中添加以下公共函数原型。确保熟悉新代码,以便我们可以逐步进行:

 
  

让我们简要讨论每个函数定义:

  • 第一个函数是构造函数,以类的名称命名。注意它只接受一个参数。这将用于初始化它将是什么类型的拾取物(生命值还是弹药)。
  • 函数接收一个。这个函数将在每个波次开始时为每个实例调用。然后对象将知道它们可以生成的区域。
  • 函数当然会处理生成拾取物。
  • 函数,就像在、和类中一样,将返回一个代表游戏世界中对象当前位置的。
  • 函数返回一个对象,使得拾取物可以在每一帧中被绘制。
  • 函数接收上一帧所用的时间。它使用这个值来更新它的私有变量,并决定何时生成和取消生成。
  • 函数返回一个布尔值,让调用代码知道拾取物当前是否已生成。
  • 函数在检测到与玩家的碰撞时将被调用。然后类代码可以准备在适当的时间重新生成。请注意,它返回一个,以便调用代码知道拾取物的价值是健康还是弹药。
  • 函数将在玩家选择在游戏的升级阶段升级拾取物的属性时被调用。

现在我们已经浏览了成员变量和函数原型,应该很容易跟着我们编写函数定义。

现在我们可以创建一个新的文件,其中包含函数定义。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击C++文件( 突出显示,然后在名称字段中键入。最后,单击添加按钮。我们现在准备好编写类的代码了。

将此处显示的代码添加到文件中。确保审查代码,以便我们可以讨论它:

 
  

在之前的代码中,我们添加了熟悉的包含指令。然后我们添加了构造函数。我们知道这是构造函数,因为它与类名相同。

构造函数接收一个名为的,代码的第一件事就是将从接收到的值赋给。之后,有一个块,检查是否等于。如果是,将与健康拾取纹理相关联,将设置为。

如果不等于,块将把弹药拾取纹理与相关联,并将的值赋给。

在块之后,代码使用函数将的原点设置为中心,并将和分别赋给和。

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

接下来我们将添加函数。在添加时检查代码:

 
  

我们刚刚编写的函数只是简单地复制了传入的对象的值,但在左侧和顶部增加了五十,右侧和底部减少了五十。现在 Pickup 对象已经知道它可以生成的区域。函数然后调用自己的函数,为每一帧的绘制和更新做最后的准备。

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

 
  

函数执行准备拾取物所需的一切。首先它为随机数生成器设置种子,并获取对象的水平和垂直位置的随机数。请注意,它使用和作为可能水平和垂直位置的范围。

设置为零,因此在取消生成之前允许的时间长度被重置。变量设置为,因此当我们从中调用时,我们将得到一个积极的响应。最后,通过移动到位置,准备绘制到屏幕上。

在以下代码块中,我们有三个简单的 getter 函数。函数返回当前位置的,返回本身的副本,根据对象当前是否生成返回或。

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

 
  

接下来我们将编写函数。当玩家触摸/碰撞(获得)拾取物时,将从中调用此函数。在函数之后添加函数:

 
  

函数将设置为,所以我们知道此刻不要绘制和检查碰撞。设置为零,因此再次开始生成的倒计时从头开始,返回给调用代码,以便调用代码可以处理添加额外的弹药或生命值。

接下来是函数,它将我们迄今为止看到的许多变量和函数联系在一起。添加并熟悉函数,然后我们可以讨论它:

 
  

函数分为四个块,每帧考虑执行一次:

  • 如果为 true,则执行块——。这段代码将本帧的时间添加到,以跟踪拾取物已经生成的时间。
  • 相应的块,如果为,则执行。此块将本帧所花费的时间添加到,以跟踪拾取物自上次取消生成(隐藏)以来等待的时间。
  • 另一个块,当生成的拾取物已经存在的时间超过应该存在的时间时执行——。这个块将设置为,并将重置为零。现在块 2 将执行,直到再次生成的时间到来。
  • 最后一个块,当自上次取消生成以来等待的时间超过必要的等待时间,并且拾取物当前未生成时执行——。当执行此块时,是时候再次生成了,并调用生成函数。

这四个测试和代码控制着拾取物的隐藏和显示。

最后,添加函数的定义:

 
  

函数测试拾取物的类型,无论是生命值还是弹药,然后将的初始值的 50%添加到其中。在块之后的两行增加了拾取物生成的时间和玩家等待生成之间的时间。

当玩家在状态下选择升级拾取物时,将调用此函数。我们的类已经准备就绪。

经过所有那些辛苦工作实现类之后,我们现在可以继续在游戏引擎中编写代码,真正将一些拾取物放入游戏中。

我们首先在文件中添加一个包含指令:

 
  

在以下代码中,我们添加了两个实例,一个称为,另一个称为。我们分别将值和传递给构造函数,以便它们被初始化为正确类型的拾取物。添加我们刚刚讨论过的突出显示的代码:

 
  

在键盘处理的状态中,添加在嵌套的代码块中显示的突出行:

 
  

先前的代码简单地将传递给每个拾取物的函数。拾取物现在知道它们可以生成的位置。这段代码对于每个新波次都会执行,因此随着竞技场的大小增长,对象将得到更新。

以下代码简单地为每个对象在每一帧调用函数:

 
  

游戏循环的绘制部分中的以下代码检查拾取物当前是否生成,如果是,则绘制它。添加我们刚讨论过的突出显示的代码:

 
  

现在您可以运行游戏并看到拾取物的生成和消失。但是,您目前无法实际拾取它们。

使用 Pickup 类

现在我们已经在游戏中有了所有的对象,是时候让它们相互作用(碰撞)了。

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

  • 类有一个函数。当僵尸与玩家发生碰撞时,我们将调用它。
  • 类有一个函数。当子弹与僵尸发生碰撞时,我们将调用它。
  • 类有一个函数。当玩家与拾取物发生碰撞时,我们将调用它。

如果需要,回顾一下每个函数的工作原理。现在我们只需要检测碰撞并调用适当的函数。我们将使用矩形相交来检测碰撞。这种类型的碰撞检测非常简单(特别是使用 SFML)。我们可以想象绘制一个虚拟的矩形——我们可以称之为碰撞框边界矩形——围绕我们想要测试碰撞的对象,然后测试它们是否相交。如果它们相交,我们就有了碰撞:

检测碰撞

从前面的图像中可以看出,这还远非完美。但在这种情况下已经足够了。要实现这种方法,我们只需要使用两个对象碰撞框的 x 和 y 坐标进行相交测试。

检测两个矩形相交的代码看起来可能是这样的。不要使用以下代码。这仅用于演示目的:

 
  

然而,我们不需要编写这段代码。我们将使用 SFML 的函数,它适用于对象。回想一下、、和类,它们都有一个函数,返回对象当前位置的。我们将看到如何使用和来进行所有的碰撞检测。

我们将分三个代码部分处理这个问题,它们将依次跟在游戏引擎更新部分的末尾。

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

  • 是否有僵尸被击中?
  • 玩家是否被僵尸触碰?
  • 玩家是否触碰到了拾取物?

首先让我们添加几个变量和。然后当杀死僵尸时我们可以改变它们。添加以下代码:

 
  

现在让我们开始检测僵尸是否与子弹发生碰撞。

以下代码可能看起来很复杂,但当我们逐步进行时,我们会发现这实际上并不是我们以前没有见过的东西。在每帧更新拾取物后,添加以下代码。然后我们可以逐步进行:

 
  

在接下来的部分中,我们将再次看到所有的僵尸和子弹碰撞检测代码。我们将一点一点地进行讨论。首先注意嵌套的循环的结构(去掉代码后)如下:

 
  

该代码循环遍历每一颗子弹(从 0 到 99),对于每一个僵尸(从 0 到的前一个)。

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

  1. 使用以下代码检查当前子弹是否在飞行中,当前僵尸是否仍然活着:
 
  
  1. 假设僵尸还活着,子弹正在飞行,我们使用以下代码测试矩形相交:
 
  

如果当前子弹和僵尸发生了碰撞,那么我们会采取一些步骤。

  1. 使用以下代码停止子弹:
 
  
  1. 通过调用其函数向当前僵尸注册一次命中。请注意,函数返回一个,让调用代码知道僵尸是否已经死亡。这显示在以下代码行中:
 
  

在此块内,检测僵尸是否死亡而不仅仅是受伤时,我们执行以下操作:

  • 将增加十
  • 如果分数超过(击败),则更改
  • 将减少一个
  • 检查是否所有僵尸都死了,,如果是,则更改为

这是我们刚讨论的内的代码块:

 
  

这样就处理了僵尸和子弹。您可以运行游戏并看到血液。当然,在我们在下一章中实现 HUD 之前,您不会看到分数。

这段代码比僵尸和子弹碰撞检测要简短和简单得多。在我们编写的先前代码之后添加以下突出显示的代码:

 
  

我们通过使用循环遍历所有僵尸来检测僵尸是否与玩家发生碰撞。对于每个活着的僵尸,代码使用函数来测试与玩家的碰撞。发生碰撞时,我们调用。然后我们通过调用来检查玩家是否死亡。如果玩家的健康值等于或小于零,则我们将更改为。

您可以运行游戏并检测碰撞。但是,由于尚未添加 HUD 或音效,因此不清楚是否发生了碰撞。此外,我们需要在玩家死亡并开始新游戏时做更多工作。因此,尽管游戏运行,但目前的结果并不特别令人满意。我们将在接下来的两章中改进这一点。

玩家与两个物品之间的碰撞检测代码如下。在我们添加的先前代码之后添加以下突出显示的代码:

 
  

先前的代码使用两个简单的语句来查看或是否被玩家触碰。

如果已收集了健康物品,则函数使用从函数返回的值来增加玩家的健康水平。

如果弹药捡起已被收集,那么将增加返回的值。

您可以运行游戏,杀死僵尸并收集物品!请注意,当您的健康值等于零时,游戏将进入状态并暂停。要重新开始,您需要按Enter,然后输入16之间的数字。当我们实现 HUD、主屏幕和升级屏幕时,这些步骤对玩家来说将是直观和简单的。我们将在下一章中这样做。

以下是您可能会问的一些问题:

Q)是否有更好的碰撞检测方法?

A)是的。有许多更多的碰撞检测方法,包括但不限于以下方法:

  • 可以将对象分成多个更适合精灵形状的矩形。对于 C++来说,每帧检查成千上万个矩形是完全可管理的。特别是当您使用邻居检查等技术来减少每帧所需的测试数量时。
  • 对于圆形对象,可以使用半径重叠方法。
  • 对于不规则多边形,可以使用交叉数算法。

所有这些技术都可以在以下网站上进行调查:

  • 邻居检查
  • 半径重叠方法
  • 穿越数算法

这是一个忙碌的章节,但我们取得了很多成就。我们不仅通过两个新的类为游戏添加了子弹和拾取物,而且还使所有的物体按照应有的方式进行交互,当它们相互碰撞时进行检测。

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

在本章中,我们将看到 SFML Views的真正价值。我们将添加大量的 SFML 对象,并像在Timber!!!项目中一样操纵它们。新的是,我们将使用第二个视图实例来绘制 HUD。这样,HUD 将始终整齐地定位在主游戏动作的顶部,而不管背景、玩家、僵尸和其他游戏对象在做什么。

这是我们将要做的事情:

  • 在主页/游戏结束屏幕上添加文本和背景
  • 在升级屏幕上添加文本
  • 创建第二个视图
  • 添加 HUD

在本章中,我们将操纵一些字符串。这样我们就可以格式化 HUD 和升级屏幕。

添加下一个高亮显示的指令,以便我们可以创建一些对象来实现这一点:

 
  

接下来添加这段相当冗长但易于解释的代码。为了帮助确定应该添加代码的位置,新代码已经高亮显示,而现有代码没有。您可能需要调整一些文本/元素的位置/大小以适应您的屏幕:

 
  

先前的代码非常简单,没有什么新东西。它基本上创建了一堆 SFML 对象。它分配它们的颜色和大小,然后格式化它们的位置,使用我们之前见过的函数。

最重要的是,我们创建了另一个名为的对象,并将其初始化为适应屏幕的分辨率。

正如我们所看到的,主视图对象随着玩家的移动而滚动。相比之下,我们永远不会移动。这样做的结果是,只要在绘制 HUD 元素之前切换到这个视图,我们就会产生这样的效果:游戏世界在下方滚动,而玩家的 HUD 保持静止。

类比一下,您可以想象在电视屏幕上放置一张带有一些文字的透明塑料片。电视将继续正常播放移动图片,而塑料片上的文字将保持在同一位置,不管下面发生了什么。

然而,下一件要注意的事情是,高分并没有以任何有意义的方式设置。我们需要等到下一章,当我们调查文件 I/O 以保存和检索高分时。

值得注意的另一点是,我们声明并初始化了一个名为的,它将是玩家剩余生命的视觉表示。这将几乎与上一个项目中的时间条工作方式完全相同,当然,它代表的是生命而不是时间。

在先前的代码中,有一个名为的新精灵,它为我们将在屏幕左下角旁边绘制的子弹和弹夹统计数据提供了上下文。

虽然我们刚刚添加的大量代码没有什么新的或技术性的,但一定要熟悉细节,特别是变量名,以便更容易跟随本章的其余部分。

正如您所期望的,我们将在代码的更新部分更新 HUD 变量。然而,我们不会在每一帧都这样做。原因是这是不必要的,而且还会减慢我们的游戏循环速度。

举个例子,考虑这样一种情况:玩家杀死了一个僵尸并获得了一些额外的分数。无论对象中的分数是在千分之一秒、百分之一秒,甚至十分之一秒内更新,玩家都不会察觉到任何区别。这意味着没有必要在每一帧重新构建我们设置给对象的字符串。

因此,我们可以确定何时以及多久更新 HUD,添加以下变量:

 
  

在先前的代码中,我们有变量来跟踪自上次更新 HUD 以来经过了多少帧,以及我们希望在 HUD 更新之间等待的帧数间隔。

现在我们可以使用这些新变量并实际上每帧更新 HUD。然而,直到我们开始操纵最终变量(例如)在下一章中,我们才会真正看到所有 HUD 元素的变化。

按照以下所示,在游戏循环的更新部分中添加突出显示的代码:

 
  

在新代码中,我们更新了精灵的大小,增加了对象,然后增加了变量。

接下来,我们开始一个块,测试是否大于我们存储在中的首选间隔。

在这个块中是所有操作发生的地方。首先,我们为需要设置为对象的每个字符串声明一个字符串流对象。

然后我们依次使用这些字符串流对象,并使用函数将结果设置为适当的对象。

最后,在退出块之前,将设置回零,以便计数可以重新开始。

现在,当我们重新绘制场景时,新值将出现在玩家的 HUD 中。

接下来三个代码块中的所有代码都在游戏循环的绘制阶段中。我们只需要在主游戏循环的绘制部分的适当状态下绘制适当的对象。

在状态下,添加以下突出显示的代码:

 
  

在上一个代码块中需要注意的重要事情是,我们切换到了 HUD 视图。这会导致所有东西都以我们给 HUD 的每个元素的精确屏幕位置绘制。它们永远不会移动。

在状态下,添加以下突出显示的代码:

 
  

在状态下,添加以下突出显示的代码:

 
  

在状态下,添加以下突出显示的代码:

 
  

现在我们可以运行游戏,并在游戏过程中看到我们的 HUD 更新。

绘制 HUD,主页和升级屏幕

这显示了主页/游戏结束屏幕上的HI SCORE和得分:

绘制 HUD,主页和升级屏幕

接下来,我们看到文本显示玩家的升级选项,尽管这些选项目前还没有任何作用。

绘制 HUD,主页和升级屏幕

在这里,我们在暂停屏幕上看到了一条有用的消息:

绘制 HUD,主页和升级屏幕

SFML Views 比这个简单的 HUD 更强大。要了解 SFML Views 的潜力以及它们的易用性,可以查看 SFML 网站关于的教程。

这里可能会有一个让您在意的问题:

Q)我在哪里可以看到类的更多功能?

A)查看下载包中Zombie Arena游戏的增强版。您可以使用键盘光标键旋转和缩放操作。警告!旋转场景会使控制变得笨拙,但您可以看到类可以做的一些事情。

FAQ

缩放和旋转功能是在主游戏循环的输入处理部分中只用了几行代码就实现的。您可以在下载包的文件夹中查看代码,或者从文件夹中运行增强版。

这是一个快速简单的章节。我们看到了如何使用显示不同类型的变量持有的值,然后使用第二个 SFML对象在主游戏动作的顶部绘制它们。

我们现在几乎完成了僵尸竞技场。所有的截图都显示了一个小竞技场,没有充分利用整个显示器。在这个项目的最后阶段,我们将加入一些最后的修饰,比如升级、音效和保存最高分。竞技场可以随后扩大到与显示器相同的大小甚至更大。

我们快要完成了。这一小节将演示如何使用 C++标准库轻松操作存储在硬盘上的文件,我们还将添加音效。当然,我们知道如何添加音效,但我们将讨论在代码中的调用应该放在哪里。我们还将解决一些问题,使游戏完整。

在本章中,我们将学习以下主题:

  • 保存和加载最高分
  • 添加音效
  • 允许玩家升级
  • 创建永无止境的多波

文件 I/O,或输入/输出,是一个相当技术性的主题。幸运的是,由于它在编程中是一个如此常见的需求,有一个库可以为我们处理所有的复杂性。与我们为 HUD 连接字符串一样,是标准库通过提供了必要的功能。

首先,我们以与包含相同的方式包含:

 
  

现在,在文件夹中添加一个名为的新文件夹。接下来,在此文件夹中右键单击,创建一个名为的新文件。我们将保存玩家的最高分数在这个文件中。您可以打开文件并向其中添加分数。如果您这样做,请确保它是一个相当低的分数,这样我们就可以轻松测试是否击败该分数会导致新分数被添加。确保在完成后关闭文件,否则游戏将无法访问它。

在下一段代码中,我们创建了一个名为的对象,并将刚刚创建的文件夹和文件作为参数传递给它的构造函数。

代码检查文件是否存在并准备好读取。然后我们将文件的内容放入中并关闭文件。添加突出显示的代码:

 
  

现在我们处理保存可能的新最高分。在处理玩家健康小于或等于零的块中,我们创建一个名为的对象,将的值写入文本文件,然后关闭文件:

 
  

您可以玩游戏,您的最高分将被保存。退出游戏并注意,如果您再次玩游戏,您的最高分仍然存在。

让我们制造一些噪音。

在本节中,我们将创建所有我们需要为游戏添加一系列音效的和对象。

首先添加所需的 SFML 包含文件:

 
  

现在继续添加七个和对象,它们加载和准备了我们在第六章中准备的七个音频文件:

 
  

现在七个音效已经准备好播放。我们只需要弄清楚在我们的代码中每个函数的调用应该放在哪里。

我们将添加的下一段代码使玩家可以在波之间升级。由于我们已经完成的工作,这是很容易实现的。

在我们处理玩家输入的状态中添加突出显示的代码:

 
  

玩家现在可以在每次清除一波僵尸时升级。然而,我们目前无法增加僵尸的数量或级别的大小。

在状态的下一部分,在我们刚刚添加的代码之后,修改从到状态改变时运行的代码。

以下是完整的代码。我已经突出显示了要么是新的要么已经稍作修改的行。

添加或修改突出显示的代码:

 
  

前面的代码首先递增变量。然后修改代码,使僵尸的数量和竞技场的大小与的新值相关。最后,我们添加了的调用来播放升级音效。

我们已经通过变量的值确定了竞技场的大小和僵尸的数量。我们还必须在每场新游戏开始时将弹药、枪支、和重置为零。在游戏循环的事件处理部分找到以下代码,并添加高亮显示的代码:

 
  

现在我们可以玩游戏了,玩家可以在不断增大的竞技场中变得更加强大,而僵尸的数量也会不断增加,直到他死亡,然后一切重新开始。

现在我们将添加对函数的其余调用。我们会逐个处理它们,因为准确确定它们的位置对于在正确时刻播放它们至关重要。

在三个地方添加高亮显示的代码,以在玩家按下R键尝试重新装填枪支时播放适当的或声音:

 
  

在处理玩家点击鼠标左键的代码末尾附近添加对的高亮调用:

 
  

在下面的代码中,我们将对的调用包装在一个测试中,以查看函数是否返回。请记住,函数用于测试前 100 毫秒内是否记录了击中。这将导致播放一个快速、重复的、沉闷的声音,但不会太快以至于声音模糊成一个噪音。

在这里添加对的调用:

 
  

当玩家拾取生命值时,我们会播放常规的拾取声音,但当玩家获得弹药时,我们会播放重新装填的声音效果。

在适当的碰撞检测代码中,添加如下高亮显示的两个调用来播放声音:

 
  

在检测子弹与僵尸碰撞的代码部分末尾添加对的调用:

 
  

您现在可以玩完整的游戏,并观看每一波僵尸和竞技场的增加。谨慎选择您的升级:

当射中僵尸时发出尖啸声

恭喜!

以下是您可能会考虑的一些问题:

问:尽管使用了类,我发现代码变得非常冗长和难以管理,再次。

答:最大的问题之一是我们的代码结构。随着我们学习更多的 C++,我们也会学会使代码更易管理,通常更简洁。

问:声音效果似乎有点单调和不真实。如何改进?

答:显著改善玩家从声音中获得的感觉的一种方法是使声音具有方向性,并根据声源到玩家角色的距离改变音量。在下一个项目中,我们将使用 SFML 的高级声音功能。

我们已经完成了僵尸竞技场游戏。这是一次相当的旅程。我们学到了很多 C++基础知识,比如引用、指针、面向对象编程和类。此外,我们还使用了 SFML 来管理摄像机、顶点数组和碰撞检测。我们学会了如何使用精灵表来减少对的调用次数,并提高帧率。使用 C++指针、STL 和一点面向对象编程,我们构建了一个单例类来管理我们的纹理,在下一个项目中,我们将扩展这个想法来管理我们游戏的所有资源。

在本书的结束项目中,我们将探索粒子效果、定向声音和分屏多人游戏。在 C++中,我们还将遇到继承、多态和一些新概念。

在本章中,我们将首次查看本书的最终项目。该项目将具有高级特点,如方向性声音,根据玩家位置从扬声器发出。它还将具有分屏合作游戏。此外,该项目还将引入着色器的概念,这是用另一种语言编写的程序,直接在图形卡上运行。到第十六章结束时,您将拥有一个完全功能的多人平台游戏,以命中经典托马斯独自一人的风格构建。

本章的主要重点将是启动项目,特别是探索如何构建代码结构以更好地利用 OOP。将涵盖以下主题:

  • 最终项目《托马斯迟到》,包括游戏特点和项目资产的介绍
  • 详细讨论我们将如何改进代码结构,与之前的项目相比
  • 编写《托马斯迟到》游戏引擎
  • 实施分屏功能

此时,如果您还没有,我建议您去观看《托马斯独自一人》的视频。请注意其简单但美观的图形。视频还展示了各种游戏挑战,例如使用角色的不同属性(身高,跳跃,力量等)。为了保持我们的游戏简单而不失挑战,我们将比《托马斯独自一人》少一些解谜特点,但将增加需要两名玩家合作玩游戏的挑战。为了确保游戏不会太容易,我们还将让玩家与时间赛跑,这就是我们的游戏名字叫《托马斯迟到》的原因。

我们的游戏不会像我们试图模仿的杰作那样先进,但它将具有一系列令人兴奋的游戏特点:

  • 一个从适合关卡挑战的时间开始倒计时的时钟。
  • 发射火坑会根据玩家的位置发出咆哮声,并在玩家掉下去时重新生成玩家。水坑也有同样的效果,但没有方向性的声音效果。
  • 合作游戏 - 两名玩家必须在规定的时间内将他们的角色带到目标。他们经常需要一起工作,例如,身材较矮,跳跃力较低的鲍勃需要站在他朋友(托马斯)的头上。
  • 玩家将有选择在全屏和分屏之间切换,因此他可以尝试自己控制两个角色。
  • 每个关卡将设计并从文本文件中加载。这将使设计各种各样的关卡变得非常容易。

看看游戏的注释截图,看看一些特点的实际操作和组件/资产,构成了游戏:

托马斯迟到的特点

让我们看看这些特点,并描述一些更多的特点:

  • 截图显示了一个简单的 HUD,详细说明了关卡编号和剩余秒数,直到玩家失败并不得不重新开始关卡。
  • 您还可以清楚地看到分屏合作模式的实际操作。请记住这是可选的。单人玩家可以全屏玩游戏,同时在托马斯和鲍勃之间切换摄像头焦点。
  • 在截图中并不是很清楚(尤其是在打印品中),但是当一个角色死亡时,他会爆炸成星花/烟火般的粒子效果。
  • 水和火砖可以被策略性地放置,使得关卡更有趣,并迫使角色之间合作。更多内容请参见第十四章,“构建可玩关卡和碰撞检测”。
  • 注意 Thomas 和 Bob——它们不仅在高度上不同,而且跳跃能力也有显著不同。这意味着 Bob 依赖于 Thomas 进行大跳跃,可以设计关卡来迫使 Thomas 选择避免碰头的路线。
  • 此外,火砖会发出咆哮声。这些声音将与 Thomas 的位置有关。它们不仅是方向性的,可以从左侧或右侧扬声器发出,而且随着 Thomas 离开或接近源头,声音会变得越来越大或越来越小。
  • 最后,在带注释的截图中,您可以看到背景。如果您将其与文件(本章后面显示)进行比较,您会发现它们是完全不同的。我们将在第十六章,“扩展 SFML 类、粒子系统和着色器”中使用 OpenGL 着色器效果来实现背景中移动的——几乎是冒泡的——效果。

所有这些功能都需要更多的截图,这样我们在编写 C++代码时可以记住最终的产品。

以下截图显示了 Thomas 和 Bob 到达一个火坑,Bob 没有机会跳过去:

“Thomas Was Late”的特点

以下截图显示了 Bob 和 Thomas 合作清除一个危险的跳跃:

“Thomas Was Late”的特点

以下截图显示了我们如何设计需要“信仰之跃”才能达到目标的谜题:

“Thomas Was Late”的特点

以下截图展示了我们如何设计几乎任意大小的压抑洞穴系统。我们还可以设计需要 Bob 和 Thomas 分开并走不同路线的关卡:

“Thomas Was Late”的特点

创建“Thomas Was Late”项目与其他两个项目相同。只需在 Visual Studio 中按照这些简单的步骤进行操作:

  1. 从主菜单中选择文件 | 新建项目
  2. 确保在左侧菜单中选择Visual C++,然后从所呈现的选项列表中选择HelloSFML。以下截图应该可以说明这一点:从模板创建项目
  3. 名称:字段中,键入,并确保为解决方案创建目录选项已被选中。现在点击确定
  4. 现在我们需要将 SFML 的文件复制到主项目目录中。我的主项目目录是。这个文件夹是在上一步中由 Visual Studio 创建的。如果您将文件夹放在其他地方,请在那里执行此步骤。我们需要复制到文件夹中的文件位于您的文件夹中。为每个位置打开一个窗口,并突出显示所需的文件。
  5. 现在将突出显示的文件复制并粘贴到项目中。

项目现在已经设置好,准备就绪。

该项目中的资源比僵尸竞技场游戏中的资源更加丰富和多样。通常,资源包括屏幕上的字体、不同动作的声音效果(如跳跃、达到目标或远处火焰的咆哮)以及 Thomas 和 Bob 的图形以及所有背景砖块的精灵表。

游戏所需的所有资源都包含在下载包中。它们分别位于和文件夹中。

所需的字体没有提供。这是因为我想避免任何可能的许可歧义。不过这不会造成问题,因为我会准确地向你展示在哪里以及如何选择和下载字体。

虽然我会提供资产本身或者获取它们的信息,但你可能也想自己创建和获取它们。

除了我们期望的图形、声音和字体之外,这个游戏还有两种新的资产类型。它们是关卡设计文件和 GLSL 着色器程序。让我们接下来了解一下它们各自的情况。

所有的关卡都是在一个文本文件中创建的。通过使用 0 到 3 的数字,我们可以构建挑战玩家的关卡设计。所有的关卡设计都在与其他资产相同目录下的 levels 文件夹中。现在可以随意偷看一下,但我们将在第十四章中详细讨论,构建可玩关卡和碰撞检测

除了这些关卡设计资产,我们还有一种特殊类型的图形资产,叫做着色器。

着色器是用GLSL(图形库着色语言)编写的程序。不用担心要学习另一种语言,因为我们不需要深入学习就能利用着色器。着色器很特殊,因为它们是完整的程序,与我们的 C++代码分开,由 GPU 每一帧执行。事实上,一些着色器程序每一帧都会运行,对每一个像素!我们将在第十六章中了解更多细节,扩展 SFML 类、粒子系统和着色器。如果你等不及了,可以看一下下载包的文件夹中的文件。

图形资产构成了我们游戏场景的部分。看一下图形资产,就能清楚地知道它们在我们的游戏中将被使用在哪里:

图形资产特写

如果图形上的图块看起来与游戏截图有些不同,那是因为它们部分是透明的,背景透过显示会使它们有些变化。如果背景图与游戏截图中的实际背景完全不同,那是因为我们将编写的着色器程序会每一帧操纵每一个像素,创造一种"熔化"效果。

声音文件都是格式。这些文件包含了我们在游戏中的某些事件中播放的音效。它们如下:

  • :当玩家的头进入火焰并且没有逃脱的机会时会播放这个音效。
  • :水和火一样会导致死亡。这个音效会通知玩家他们需要从关卡的开始重新开始。
  • :这个音效是以单声道录制的。它将根据玩家距离火焰图块的距离以不同的音量播放,并根据玩家相对于火焰图块的左右位置从不同的扬声器播放。显然,我们需要学习一些更多的技巧来实现这个功能。
  • :当玩家跳跃时会播放一个令人愉悦(稍微可预测)的欢呼声。
  • :当玩家(或玩家)将 Thomas 和 Bob 两个角色都带到目标方块时,会播放令人愉悦的胜利音效。

这些音效非常简单直接,你可以很容易地创建自己的音效。如果你打算替换文件,确保将你的声音保存为单声道(而不是立体声)格式。这其中的原因将在第十五章中解释,声音空间化和 HUD

一旦您决定要使用哪些资产,就是将它们添加到项目的时候了。以下说明将假定您使用了书籍下载包中提供的所有资产。

如果您使用自己的资产,只需用您选择的文件替换相应的声音或图形文件,文件名完全相同:

  1. 浏览到 Visual 目录。
  2. 在此文件夹中创建五个新文件夹,并将它们命名为,,,和。
  3. 从下载包中,将的全部内容复制到文件夹中。
  4. 从下载包中,将的全部内容复制到文件夹中。
  5. 现在在您的网络浏览器中访问,并下载Roboto Light字体。
  6. 提取压缩下载的内容,并将文件添加到文件夹中。
  7. 从下载包中,将的全部内容复制到文件夹中。
  8. 从下载包中,将的全部内容复制到文件夹中。

现在我们有了一个新项目,以及整个项目所需的所有资产,我们可以讨论如何构建游戏引擎代码。

到目前为止,在两个项目中都很明显的一个问题是代码变得非常冗长和难以控制。OOP 允许我们将项目分解为称为类的逻辑和可管理的块。

通过引入Engine 类,我们将大大改善此项目中代码的可管理性。Engine 类将具有三个私有函数,分别是,和。这应该听起来非常熟悉。这些函数中的每一个将保存以前全部在函数中的代码的一部分。这些函数将分别在自己的代码文件中,,和中。

类中还将有一个公共函数,可以使用的实例调用。这个函数是,将负责调用,和,每帧游戏调用一次:

构建 Thomas Was Late 代码的结构

此外,由于我们已经将游戏引擎的主要部分抽象为类,我们还可以将许多变量从中移动并将它们作为的成员。要启动我们的游戏引擎,我们只需要创建一个的实例并调用它的函数。这里是一个超级简单的主函数的预览:

 
  

暂时不要添加上述代码。

为了使我们的代码更加可管理和可读,我们还将抽象出加载关卡和碰撞检测等重要任务的责任,放到单独的函数中(在单独的代码文件中)。这两个函数分别是和。我们还将编写其他函数来处理 Thomas Was Late 项目的一些新功能。随着它们的出现,我们将详细介绍它们。

为了更好地利用 OOP,我们将完全将游戏特定领域的责任委托给新的类。您可能还记得以前项目中的声音和 HUD 代码非常冗长。我们将构建一个和类来以更清晰的方式处理这些方面。当我们实现它们时,它们的工作方式将被深入探讨。

游戏关卡本身比以前的游戏更加深入,因此我们还将编写一个类。

正如您所期望的,可玩角色也将使用类制作。但是,对于这个项目,我们将学习更多的 C++,并实现一个类,其中包含 Thomas 和 Bob 的所有常见功能,然后和类,它们将继承这些常见功能,并实现自己的独特功能和能力。这,也许并不奇怪,被称为继承。我将在接下来的第十三章,“高级面向对象编程,继承和多态”中更详细地介绍继承。

我们还将实现许多其他类来执行特定的职责。例如,我们将使用粒子系统制作一些漂亮的爆炸效果。您可能能够猜到,为了做到这一点,我们将编写一个类和一个类。所有这些类都将作为类的成员具有实例。以这种方式做事将使游戏的所有功能都可以从游戏引擎中访问,但将细节封装到适当的类中。

在我们继续查看将创建 Engine 类的实际代码之前,要提到的最后一件事是,我们将重用我们为“Zombie Arena”游戏讨论和编写的类,而不做任何更改。

如前面的讨论所建议的,我们将编写一个名为的类,它将控制并绑定 Thomas Was Late 游戏的不同部分。

我们将首先使上一个项目中的类在这个项目中可用。

我们讨论并编写的类对于这个项目也会很有用。虽然可以直接从上一个项目添加文件(和),而无需重新编码或重新创建文件,但我不想假设您没有直接跳转到这个项目。接下来是非常简要的说明,以及创建类的完整代码清单。如果您想要解释该类或代码,请参阅第八章,“指针、标准模板库和纹理管理”。

如果您完成了上一个项目,并且确实想要从“Zombie Arena”项目中添加该类,只需执行以下操作:在“解决方案资源管理器”窗口中,右键单击“头文件”,然后选择“添加”|“现有项...”。浏览到上一个项目的并选择它。在“解决方案资源管理器”窗口中,右键单击“源文件”,然后选择“添加”|“现有项...”。浏览到上一个项目的并选择它。现在您可以在这个项目中使用类。请注意,文件在项目之间共享,任何更改都将在两个项目中生效。

要从头开始创建类,请在“解决方案资源管理器”中右键单击“头文件”,然后选择“添加”|“新项...”。在“添加新项”窗口中,通过左键单击突出显示(高亮)“头文件(.h)”,然后在“名称”字段中输入。最后,单击“添加”按钮。

将以下代码添加到中:

 
  

在“解决方案资源管理器”中右键单击“源文件”,然后选择“添加”|“新项...”。在“添加新项”窗口中,通过左键单击突出显示(高亮)“C++文件(.cpp)”,然后在“名称”字段中输入。最后,单击“添加”按钮。

将以下代码添加到中:

 
  

我们现在可以开始创建我们的新类了。

和往常一样,我们将从头文件开始,其中包含函数声明和成员变量。请注意,我们将在整个项目中重新访问此文件,以添加更多函数和成员变量。目前,我们将只添加在这个阶段必要的代码。

解决方案资源管理器 中右键单击 头文件,然后选择 添加 | 新建项...。在 添加新项 窗口中,通过左键单击突出显示(高亮) 头文件( ,然后在 名称 字段中键入 。最后,单击 添加 按钮。现在我们准备好为 类编写头文件了。

添加以下成员变量以及函数声明。其中许多我们在其他项目中已经见过,有些我们在 Structuring the Thomas Was Late 代码部分讨论过。注意函数和变量的名称,以及它们是私有的还是公共的。添加以下代码到 文件中,然后我们将讨论它:

 
  

这是所有私有变量和函数的完整概述。在适当的情况下,我会在解释上花费更多时间:

  • : 类的唯一实例。
  • :一个有用的常量,提醒我们精灵表中的每个瓦片都是五十像素宽和五十像素高。
  • :一个有用的常量,使我们对 的操作更不容易出错。事实上,一个四边形中有四个顶点。现在我们不会忘记它了。
  • :一个表示游戏角色每秒向下推动的像素数的常量 值。一旦游戏完成,这是一个非常有趣的值。我们将其初始化为 ,因为这对我们最初的级别设计效果很好。
  • :像我们在所有项目中看到的那样,通常的 对象。
  • SFML 对象,,,,,, 和 :前三个 对象用于全屏视图,游戏的左右分屏视图。我们还为这三个分别有一个单独的 SFML 对象,用于绘制背景。最后一个 对象 ,将在其他六个视图的适当组合上方显示得分、剩余时间和任何玩家的消息。有七个不同的 对象可能会暗示复杂性,但当你看到本章的进展如何处理它们时,你会发现它们非常简单。我们将在本章结束时解决整个分屏/全屏问题。
  • 和 :可以预料到,这组 SFML 和 将用于显示和保存来自图形资源文件夹的背景图形。
  • :这个布尔值将让游戏引擎知道当前级别是否已经开始(通过按下 Enter 键)。一旦玩家开始游戏,他们就没有暂停游戏的选项。
  • :当屏幕是全屏时,它应该以 Thomas(m_Character1 = true)还是 Bob(m_Character1 = false)为中心?最初,它被初始化为 true,以便以 Thomas 为中心。
  • :游戏当前是否以分屏模式进行?我们将使用这个变量来决定如何使用我们之前声明的所有 对象。
  • 变量:这个 变量保存了当前级别剩余的时间。在之前的代码中,它被设置为 用于测试目的,直到我们真正为每个级别设置一个特定的时间。
  • 变量:这个变量是一个 SFML 时间对象。它跟踪游戏已经进行了多长时间。
  • 布尔变量:这个变量用于检查玩家是否刚刚完成或失败了一个关卡。然后我们可以使用它来触发加载下一个关卡或重新开始当前关卡。
  • 函数:这个函数将处理玩家的所有输入,这个游戏中全部来自键盘。乍一看,它似乎直接处理所有的键盘输入。然而,在这个游戏中,我们将直接处理影响 Thomas 或 Bob 的键盘输入,这将直接在和类中进行。我们将调用函数,这个函数将直接处理键盘输入,比如退出、切换到分屏等其他键盘输入。
  • 函数:这个函数将完成我们之前在函数的更新部分中做的所有工作。我们还将从函数中调用一些其他函数,以保持代码的组织性。如果你回顾代码,你会看到它接收一个参数,这个参数将保存自上一帧以来经过的秒数的分数。当然,这正是我们需要更新所有游戏对象的内容。
  • 函数:这个函数将包含以前项目中主函数绘图部分的所有代码。然而,当我们学习使用 SFML 进行其他绘图方式时,会有一些绘图代码不在这个函数中。当我们学习第十六章中的粒子系统时,我们将看到这些新代码,扩展 SFML 类、粒子系统和着色器

现在让我们来看一下所有的公共函数:

  • 构造函数:正如我们所期望的那样,当我们首次声明的实例时,将调用这个函数。它将进行所有的设置和类的初始化。我们很快将在编写文件时看到具体内容。
  • 函数:这是我们需要调用的唯一公共函数。它将触发输入、更新和绘制的执行,完成所有工作。

接下来,我们将看到所有这些函数的定义以及一些变量的作用。

在我们之前的所有类中,我们将所有的函数定义放在文件中,并以类名为前缀。由于我们这个项目的目标是使代码更易管理,我们正在以稍微不同的方式做事情。

在文件中,我们将放置构造函数()和公共函数。所有其他函数将放在它们自己的文件中,文件名清楚地说明了哪个函数放在哪里。只要我们在包含类的所有文件的顶部添加适当的包含指令(),这对编译器来说不会是问题。

让我们开始编写并在中运行它。在解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(单击左键)C++文件(.cpp),然后在名称字段中输入。最后,单击添加按钮。现在我们已经准备好为类编写文件。

这个函数的代码将放在我们最近创建的文件中。

添加以下代码,然后我们可以讨论它:

 
  

我们之前看到的大部分代码都很熟悉。例如,有通常的代码行来获取屏幕分辨率以及创建一个。在前面的代码结束时,我们使用了现在熟悉的代码来加载纹理并将其分配给一个 Sprite。在这种情况下,我们正在加载纹理并将其分配给。

需要一些解释的是函数的四次调用之间的代码。函数将屏幕的一部分分配给 SFML 的对象。但它不使用像素坐标。它使用比例。其中“1”是整个屏幕(宽度或高度),每次调用的前两个值是起始位置(水平,然后垂直),最后两个值是结束位置。

注意,和的位置完全相同,从屏幕的几乎最左侧(0.001)开始,结束于距离中心的两千分之一(0.498)。

和也位于完全相同的位置,从前两个对象的左侧开始(0.5),延伸到屏幕的几乎最右侧(0.998)。

此外,所有视图在屏幕的顶部和底部留下了一小部分空隙。当我们在白色背景上绘制这些对象时,它将产生在屏幕的两侧之间有一条细白线以及屏幕边缘周围有一条细白色边框的效果。

我已经尝试在以下图表中表示这种效果:

编写引擎类构造函数定义

最好的理解方法是完成本章,运行代码,看到它的实际效果。

这个函数的代码将放在我们最近创建的文件中。

在上一个构造函数代码之后立即添加以下代码:

 
  

run 函数是我们引擎的中心-它启动所有其他部分。首先,我们声明一个 Clock 对象。接下来,我们有熟悉的循环,它创建游戏循环。在这个 while 循环内,我们做以下事情:

  1. 重新启动并将上一个循环所花费的时间保存在中。
  2. 跟踪中经过的总时间。
  3. 声明并初始化一个来表示上一帧中经过的秒数的一部分。
  4. 调用。
  5. 调用并传入经过的时间()。
  6. 调用。

所有这些都应该看起来非常熟悉。新的是它包含在函数中。

如前所述,这个函数的代码将放在自己的文件中,因为它比构造函数或函数更复杂。我们将使用并在函数签名前加上以确保编译器了解我们的意图。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,突出显示(通过左键单击)C++文件(,然后在名称字段中输入。最后,单击添加按钮。我们现在准备编写函数的代码。

添加以下代码:

 
  

与之前的两个项目一样,我们每帧都会检查事件队列。同样,我们像以前一样使用来检测特定的键盘键。我们刚刚添加的代码中最重要的是这些键实际上做了什么:

  • 像往常一样,Esc键关闭窗口,游戏将退出。
  • Enter键将设置为 true,最终,这将导致关卡开始。
  • Q键在全屏模式下在和之间切换的值。它将在主的中心之间切换 Thomas 和 Bob。
  • E键在和之间切换。这将导致在全屏和分屏视图之间切换。

大部分键盘功能将在本章结束时完全可用。我们即将能够运行我们的游戏引擎。接下来,让我们编写函数。

如前所述,这个函数的代码将放在自己的文件中,因为它比构造函数或函数更加广泛。我们将使用并在函数签名前加上以确保编译器知道我们的意图。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件(,然后在名称字段中输入。最后,单击添加按钮。现在我们准备为函数编写一些代码。

将以下代码添加到文件中以实现函数:

 
  

首先注意,函数接收上一帧所用时间作为参数。当然,这对于函数履行其职责至关重要。

在这个阶段,前面的代码并没有实现任何可见的效果。它确立了我们将来需要的结构。它从中减去了上一帧所用的时间。它检查时间是否已经用完,如果是,就将设置为。所有这些代码都包裹在一个语句中,只有当为时才执行。原因是,与以前的项目一样,我们不希望在游戏尚未开始时时间推移和对象更新。

随着项目的继续,我们将在这段代码的基础上构建。

如前所述,这个函数的代码将放在自己的文件中,因为它比构造函数或函数更加广泛。我们将使用并在函数签名前加上以确保编译器知道我们的意图。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件(,然后在名称字段中输入。最后,单击添加按钮。现在我们准备为函数添加一些代码。

将以下代码添加到文件中以实现函数:

 
  

在前面的代码中,我们没有看到任何新东西。代码通常从清除屏幕开始。在这个项目中,我们用白色清除屏幕。新的是不同的绘制选项是如何通过条件分隔的,检查屏幕当前是分割还是全屏。

 
  

如果屏幕没有分割,我们在背景()中绘制背景精灵,然后切换到主全屏()。请注意,目前我们实际上并没有在中进行任何绘制。

另一方面,如果屏幕被分割,块中的代码将被执行,我们将用屏幕左侧的背景精灵绘制,然后切换到。

然后,在块中,我们用屏幕右侧的背景精灵绘制,然后切换到。

在刚才描述的结构之外,我们切换到。在这个阶段,我们实际上并没有在中绘制任何东西。

与另外两个(、)最重要的函数一样,我们将经常回到函数。我们将添加需要绘制的游戏新元素。您会注意到,每次我们这样做时,我们都会在主、左和右部分中添加代码。

让我们快速回顾一下类,然后我们可以启动它。

我们已经将以前在函数中的所有代码抽象成了、和函数。这些函数的连续循环以及时间控制都由函数处理。

考虑在 Visual Studio 中保持Input.cppUpdate.cppDraw.cpp标签打开,可能按顺序组织,如下面的截图所示:

到目前为止的引擎类

在项目的过程中,我们将重新审视每一个这些函数,以添加更多的代码。现在我们有了类的基本结构和功能,我们可以在函数中创建一个实例,并看到它的运行。

让我们将文件重命名为。右键单击解决方案资源管理器中的文件,然后选择重命名。将名称更改为。这将是包含我们的函数和实例化类的代码的文件。

将以下代码添加到中:

 
  

我们所做的就是为类添加一个包含指令,声明一个的实例,然后调用它的函数。直到玩家退出并且执行返回到和语句,一切都将由类处理。

这很容易。现在我们可以运行游戏,看到空的背景,无论是全屏还是分屏,最终都将包含所有的动作。

到目前为止,游戏在全屏模式下,只显示了背景:

编写主函数

现在按下E键,你将能够看到屏幕被整齐地分成两半,准备好进行分屏合作游戏:

编写主函数

以下是一些可能会让你困惑的问题。

Q)我不完全理解代码文件的结构。

A)抽象确实可以使我们的代码结构变得不太清晰,但实际的代码本身变得更容易。我们将代码分割成、和,而不是像以前的项目那样把所有东西塞进主函数中。此外,随着我们的进行,我们将使用更多的类来将相关的代码分组在一起。再次学习《构建 Thomas Was Late 代码》部分,特别是图表。

在本章中,我们介绍了 Thomas Was Late 游戏,并为项目的其余部分奠定了理解和代码结构的基础。在解决方案资源管理器中确实有很多文件,但只要我们理解每个文件的目的,我们会发现项目的实现变得更加容易。

在接下来的章节中,我们将学习另外两个基本的 C++主题,继承和多态。我们还将开始利用它们,构建三个类来代表两个可玩角色。

在本章中,我们将通过更深入地了解继承多态的略微更高级的概念来进一步扩展我们对 OOP 的知识。然后,我们将能够使用这些新知识来实现我们游戏的明星角色 Thomas 和 Bob。在本章中,我们将更详细地介绍以下内容:

  • 如何使用继承扩展和修改一个类?
  • 通过多态将一个类的对象视为多种类型的类
  • 抽象类以及设计从未实例化的类实际上可以很有用
  • 构建一个抽象的类
  • 在和类中使用继承
  • 将 Thomas 和 Bob 添加到游戏项目中

我们已经看到了如何通过实例化/创建来使用 SFML 库的类的对象来使用其他人的辛勤工作。但是这整个 OOP 的东西甚至比这更深入。

如果有一个类中有很多有用的功能,但不完全符合我们的要求怎么办?在这种情况下,我们可以从其他类中继承。就像它听起来的那样,继承意味着我们可以利用其他人的类的所有功能和好处,包括封装,同时进一步完善或扩展代码,使其特别适合我们的情况。在这个项目中,我们将从一些 SFML 类中继承并扩展。我们还将对我们自己的类进行同样的操作。

让我们看一些使用继承的代码,

考虑到所有这些,让我们看一个示例类,并看看我们如何扩展它,只是为了看看语法和作为第一步。

首先,我们定义一个要继承的类。这与我们创建其他任何类的方式没有区别。看一下这个假设的类声明:

 
  

在前面的代码中,我们定义了一个类。它有四个私有变量,、、和。它有四个公共函数、、和。我们不需要看函数的定义,它们只是初始化与它们的名称明显相关的适当变量。

我们还可以想象,一个完全实现的类会比这个更加深入。它可能有、等函数。如果我们在一个 SFML 项目中实现了类,它可能会有一个对象,以及一个和一个函数。

这里呈现的简单场景适合学习继承。现在让我们看看一些新的东西,实际上是从类继承。看看这段代码,特别是突出显示的部分:

 
  

通过在类声明中添加代码,继承自。但这到底意味着什么呢?是一个。它拥有的所有变量和函数。然而,继承不仅仅是这样。

还要注意,在前面的代码中,我们声明了一个构造函数。这个构造函数是独有的。我们不仅继承了,还扩展了。类的所有功能(定义)都由类处理,但构造函数的定义必须由类处理。

这是假设的构造函数定义可能是这样的:

 
  

我们可以继续编写一堆其他类,这些类是类的扩展,也许是和。每个类都有完全相同的变量和函数,但每个类也可以有一个独特的构造函数,用于初始化适合类型的变量。可能有非常高的和,但是非常小。可能介于和之间,每个变量的值都是中等的。

好像面向对象编程还不够有用,我们现在可以对现实世界的对象进行建模,包括它们的层次结构。我们通过子类化、扩展和继承其他类来实现这一点。

我们可能想要学习的术语是,被扩展的类是超类,从超类继承的类是子类。我们也可以说类和类。

你可能会发现自己对继承这个问题感到困惑:为什么?原因大致如下:我们可以一次编写通用代码;在父类中,我们可以更新这些通用代码,所有继承它的类也会被更新。此外,子类只能使用公共和受保护的实例变量和函数。因此,如果设计得当,这也进一步增强了封装的目标。

你说过受保护的吗?是的。类变量和函数有一个叫做protected的访问限定符。你可以把受保护的变量看作介于公共和私有之间。这里是访问限定符的快速摘要,以及有关受保护限定符的更多细节:

  • 变量和函数可以被任何人访问和使用。
  • 变量和函数只能被类的内部代码访问/使用。这对封装是有利的,当我们需要访问/更改私有变量时,我们可以提供公共的和函数(如等)。如果我们扩展了一个具有变量和函数的类,那么子类不能直接访问其父类的私有数据。
  • 变量和函数几乎与私有相同。它们不能被类的实例直接访问/使用。但是,它们可以被任何扩展它们所声明的类的类直接使用。因此,它们就像是私有的,除了对子类。

要完全理解受保护的变量和函数是什么以及它们如何有用,让我们先看看另一个主题,然后我们可以看到它们的作用。

多态允许我们编写的代码不那么依赖于我们要操作的类型。这可以使我们的代码更清晰和更高效。多态意味着不同的形式。如果我们编码的对象可以是多种类型的东西,那么我们就可以利用这一点。

对我们来说,多态意味着什么?简化到最简单的定义,多态就是:任何子类都可以作为使用超类的代码的一部分。这意味着我们可以编写更简单、更易于理解的代码,也更容易修改或更改。此外,我们可以为超类编写代码,并依赖于这样一个事实,即在一定的参数范围内,无论它被子类化多少次,代码仍然可以正常工作。

让我们讨论一个例子。

假设我们想要使用多态来帮助编写一个动物园管理游戏,我们需要喂养和照顾动物的需求。我们可能会想要一个名为的函数。我们可能还想将要喂食的动物的实例传递给函数。

当然,动物园有很多种类的动物——、和。有了我们对 C++继承的新知识,编写一个类并让所有不同类型的动物从中继承将是合理的。

如果我们想要编写一个可以将狮子、大象和三趾树懒作为参数传递的函数(),似乎我们需要为每种类型的编写一个函数。然而,我们可以编写多态函数,具有多态返回类型和参数。看看这个假设的函数的定义:

 
  

前面的函数将引用作为参数,这意味着可以将从扩展类构建的任何对象传递给它。

因此,即使今天编写代码并在一周、一个月或一年后创建另一个子类,相同的函数和数据结构仍将起作用。此外,我们可以对子类强制执行一组规则,规定它们可以做什么,不能做什么,以及如何做。因此,一个阶段的良好设计可以影响其他阶段。

但我们真的会想要实例化一个真正的动物吗?

抽象类是一种不能被实例化的类,因此不能成为对象。

我们可能想在这里学习的一些术语是具体类。具体类是指任何不是抽象的类。换句话说,到目前为止我们编写的所有类都是具体类,可以实例化为可用的对象。

那么,这段代码永远不会被使用吗?但这就像支付一个建筑师设计你的房子,然后永远不建造它!

如果我们或类的设计者想要强制其用户在使用其类之前继承它,他们可以将一个类设为抽象。然后,我们就不能从中创建对象;因此,我们必须首先扩展它,然后从子类创建对象。

为此,我们可以使一个函数纯虚,并且不提供任何定义。然后,该函数必须在扩展它的任何类中重写(重新编写)。

让我们看一个例子;这会有所帮助。我们通过添加一个纯虚函数来使一个类成为抽象类,比如这个只能执行通用动作 makeNoise 的抽象类:

 
  

如您所见,我们在函数声明之前添加了 C++关键字,并在函数声明之后添加了。现在,任何扩展/继承自的类都必须重写函数。这可能是有道理的,因为不同类型的动物发出非常不同类型的噪音。我们可能会假设任何扩展类的人都足够聪明,以注意到类不能发出噪音,并且他们将需要处理它,但如果他们没有注意到呢?关键是通过制作一个纯虚函数,我们保证他们会注意到,因为他们必须。

抽象类也很有用,因为有时我们需要一个可以用作多态类型的类,但我们需要保证它永远不能被用作对象。例如,本身并没有太多意义。我们不谈论动物;我们谈论动物的类型。我们不会说,“哦,看那只可爱的、蓬松的、白色的动物!”或者,“昨天我们去宠物店买了一只动物和一个动物床”。这太抽象了。

因此,抽象类有点像一个模板,可以被任何继承它的类使用。如果我们正在构建一个类似于“工业帝国”类型的游戏,玩家管理企业及其员工,我们可能需要一个类,并将其扩展为,,,当然还有。但是一个普通的到底是做什么的?我们为什么要实例化一个?

答案是我们不想实例化一个,但我们可能想要将其用作多态类型,以便在函数之间传递多个子类,并且具有可以容纳所有类型的工作者的数据结构。

所有纯虚函数必须被扩展父类的任何类重写。这意味着抽象类可以提供一些在其所有子类中都可用的常见功能。例如,类可能有,和成员变量。它可能还有函数,这不是纯虚的,在所有子类中都是相同的,但它可能有一个函数,这是纯虚的,必须被重写,因为所有不同类型的将以非常不同的方式。

顺便说一下,virtual,与纯虚相反,是一个可以选择性重写的函数。你声明一个虚函数的方式与声明纯虚函数相同,但是最后不加上。在当前的游戏项目中,我们将使用纯虚函数。

如果这些虚拟、纯虚或抽象的东西有任何不清楚的地方,使用它可能是理解它的最好方法。

现在我们了解了继承、多态和纯虚函数的基础知识,我们将把它们应用起来。我们将构建一个类,该类将拥有游戏中任何角色所需的绝大部分功能。它将有一个纯虚函数。函数在子类中需要有很大的不同,所以这是有道理的。

由于将有一个纯虚函数,它将是一个抽象类,不可能有它的对象。然后我们将构建和类,它们将继承自,实现纯虚函数的定义,并允许我们在游戏中实例化和对象。

通常情况下,创建一个类时,我们将从包含成员变量和函数声明的头文件开始。新的是,在这个类中,我们将声明一些protected成员变量。记住,受保护的变量可以被继承自具有受保护变量的类的类使用,就好像它们是一样。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击头文件( ,然后在名称字段中输入。最后,单击添加按钮。现在我们准备编写类的头文件。

我们将在三个部分添加和讨论文件的内容。首先是protected部分,然后是private,最后是public

在文件中添加下面显示的代码:

 
  

在我们刚刚编写的代码中,要注意的第一件事是所有变量都是。这意味着当我们扩展这个类时,我们刚刚编写的所有变量将对那些扩展它的类可访问。我们将用和类扩展这个类。

除了访问规范之外,之前的代码没有什么新的或复杂的。然而,值得注意的是一些细节。然后随着我们的进展,理解类的工作原理将会变得容易。所以,让我们逐个运行那些变量。

我们有一个相当可预测的,。我们有一个名为的浮点数,它将保存代表角色能够跳跃的时间。数值越大,角色跳得越远/高。

接下来,我们有一个布尔值,当角色跳跃时为,否则为。这将有助于确保角色在空中时不能跳跃。

变量与具有类似的用途。知道角色何时下落将是有用的。

接下来,我们有两个布尔值,如果角色的左键或右键当前被按下,将为 true。这些取决于角色(AD代表 Thomas,左右箭头键代表 Bob)。我们将在和类中看到如何响应这些布尔值。

浮点变量在每一帧为时更新。然后我们就知道已经达到了。

最后一个变量是布尔值。如果在当前帧中启动了跳跃,它将为。这将有助于知道何时播放跳跃音效。

接下来,在文件中添加以下变量:

 
  

在前面的代码中,我们有一些有趣的变量。请记住,这些变量只能被类中的代码直接访问。和类将无法直接访问它们。

变量将保存角色下落的每秒像素数。变量将保存角色每秒左右移动的像素数。

,变量是角色在世界中(而不是屏幕上)的位置,即角色中心的位置。

接下来的四个对象很重要。在Zombie Arena游戏中进行碰撞检测时,我们只是简单地检查两个对象是否相交。每个对象代表整个角色、拾取物或子弹。对于非矩形形状的对象(僵尸和玩家),这有点不准确。

在这个游戏中,我们需要更精确。,,和的对象将保存角色身体不同部位的坐标。这些坐标将在每一帧中更新。

通过这些坐标,我们将能够准确地知道角色何时落在平台上,跳跃时碰到头部,或者与侧面的瓷砖擦肩而过。

最后,我们有。是的,因为它不会被或类直接使用,但是,正如我们所看到的,是的,因为它被直接使用。

现在在文件中添加所有的函数,然后我们将讨论它们:

 
  

让我们谈谈我们刚刚添加的每个函数声明。这将使编写它们的定义更容易跟踪。

  • 函数接收一个名为的和一个名为的。顾名思义,将是角色开始的关卡坐标,将是角色下落的每秒像素数。
  • 当然是我们的纯虚函数。由于有这个函数,任何扩展它的类,如果我们想要实例化它,必须为这个函数提供一个定义。因此,当我们一会儿为写所有函数定义时,我们不会为提供定义。当然,和类中也需要有定义。
  • 函数返回一个,表示整个角色的位置。
  • 函数,以及,和,每个都返回一个,表示角色身体特定部位的位置。这正是我们需要进行详细的碰撞检测。
  • 函数像往常一样,将的副本返回给调用代码。
  • ,,和函数接收一个值,函数将使用该值重新定位角色,并阻止其通过实心瓷砖行走或跳跃。
  • 函数返回一个给调用代码,让它准确知道角色的中心在哪里。这个值当然保存在中。我们将在后面看到,它被类用来围绕适当的角色中心适当地调整。
  • 我们以前多次看到的函数,像往常一样,它接受一个参数,表示当前帧所花费的秒数的一部分。然而,这个函数需要做的工作比以前的函数(来自其他项目)更多。它需要处理跳跃,以及更新表示头部、脚部、左侧和右侧的对象。

现在我们可以为所有函数编写定义,当然,除了。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,突出显示(通过左键单击)C++文件(,然后在名称字段中键入。最后,单击添加按钮。我们现在准备好为类编写文件了。

我们将把代码和讨论分成几个部分。首先,添加包含指令和函数的定义:

 
  

函数使用传入的位置初始化,并初始化。代码的最后一行将移动到其起始位置。

接下来,在上述代码之后立即添加函数的定义:

 
  

代码的前两部分检查或是否为。如果其中任何一个是,将使用与上一个项目相同的公式(经过的时间乘以速度)进行更改。

接下来,我们看看角色当前是否正在执行跳跃。我们从知道这一点。如果这个语句为,代码将执行以下步骤:

  1. 使用更新。
  2. 检查是否仍然小于。如果是,则通过两倍的重力乘以经过的时间更改的 y 坐标。
  3. 在子句中,当不低于时,被设置为。这样做的效果将在下面看到。此外,被设置为。这可以防止我们刚刚讨论的代码执行,因为现在为 false。

块在每帧移动向下。它使用当前的值和经过的时间进行移动。

以下代码(几乎是剩下的所有代码)更新了角色的身体部位,相对于整个精灵的当前位置。查看以下图表,了解代码如何计算角色的虚拟头部、脚部、左侧和右侧的位置:

编写 PlayableCharacter.cpp

代码的最后一行使用函数将精灵移动到正确的位置,以便在函数的所有可能性之后。

现在立即添加、、、、、和函数的定义,紧接在上述代码之后:

 
  

函数返回包装整个精灵的,返回一个包含精灵中心的。请注意,我们将精灵的高度和宽度除以 2,以便动态地得出这个结果。这是因为托马斯和鲍勃的身高不同。

、、和函数返回代表角色身体部位的对象,我们在函数中每帧更新。我们将在下一章中编写使用这些函数的碰撞检测代码

函数像往常一样返回的副本。

最后,对于类,添加、、和函数的定义。在上一段代码之后立即执行:

 
  

每个先前的函数都接收一个值作为参数,用于重新定位精灵的顶部、底部、左侧或右侧。这些值是什么以及如何获得它们将在下一章中看到。每个先前的函数还会重新定位精灵。

最后一个函数是函数,也将用于碰撞检测。它设置了和的必要值来结束跳跃。

现在我们要真正使用继承。我们将为 Thomas 建立一个类,为 Bob 建立一个类。它们都将继承我们刚刚编写的类。然后它们将拥有类的所有功能,包括直接访问其变量。我们还将为纯虚函数添加定义。您将注意到和的函数将不同。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击突出显示(高亮)头文件(),然后在名称字段中键入。最后,单击添加按钮。我们现在准备为类编写头文件。

现在将此代码添加到类中:

 
  

前面的代码非常简短。我们可以看到我们有一个构造函数,并且我们将要实现纯虚拟的函数,所以现在让我们来做。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击突出显示(高亮)C++文件(),然后在名称字段中键入。最后,单击添加按钮。我们现在准备为类编写文件。

将构造函数添加到文件中,如下面的片段所示:

 
  

我们只需要加载图形并将跳跃的持续时间()设置为(几乎半秒)。

添加函数的定义,如下面的片段所示:

 
  

这段代码应该看起来很熟悉。我们正在使用 SFML 的函数来查看W,AD键是否被按下。

当按下W键时,玩家正在尝试跳跃。然后代码使用代码,检查角色是否已经在跳跃,而且也不在下落。当这些测试都为真时,设置为,设置为零,并且设置为。

当前两个测试不为时,将执行子句,并将设置为,设置为。

处理按下AD键的操作就是简单地将和/或设置为或。函数现在将能够处理移动角色。

函数中的最后一行代码返回的值。这将让调用代码知道是否需要播放跳跃音效。

现在我们将编写类,尽管这几乎与类相同,除了具有不同的跳跃能力,不同的,并且在键盘上使用不同的键。

类在结构上与类相同。它继承自,有一个构造函数,并提供了函数的定义。与相比的区别是,我们以不同的方式初始化了一些 Bob 的成员变量,并且我们也以不同的方式处理输入(在函数中)。让我们编写这个类并查看细节。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击突出显示(高亮)头文件( ,然后在名称字段中键入。最后,单击添加按钮。我们现在准备为类编写头文件。

将以下代码添加到文件中:

 
  

前面的代码与文件相同,除了类名和构造函数名。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件( ,然后在名称字段中键入。最后,单击添加按钮。现在我们已经准备好为类编写文件。

将构造函数的代码添加到文件中。请注意,纹理不同(),并且初始化为一个明显较小的值。Bob 现在是他自己独特的自己:

 
  

在构造函数之后立即添加代码:

 
  

请注意,这段代码几乎与类的函数中的代码相同。唯一的区别是我们对不同的按键做出响应(箭头键,箭头键和箭头键用于跳跃)。

现在我们有了一个通过和扩展的类,我们可以在游戏中添加一个和一个实例。

为了能够运行游戏并看到我们的新角色,我们必须声明它们的实例,调用它们的函数,每帧更新它们,并每帧绘制它们。现在让我们来做这些。

打开文件,并添加下面显示的代码行:

 
  

现在我们有了和的实例,它们都是从派生出来的。

现在我们将添加控制这两个角色的能力。这段代码将放在代码的输入部分。当然,对于这个项目,我们有一个专门的函数。打开并添加这段突出显示的代码:

 
  

请注意,以前的代码是多么简单,因为所有的功能都包含在和类中。代码只需要为和类中的每一个添加一个包含指令。然后,在函数中,代码只是调用和上的纯虚拟函数。我们将在第十五章中处理播放跳跃音效,声音空间化和 HUD

这可以分为两部分。首先,我们需要在新关卡开始时生成 Bob 和 Thomas,其次,我们需要每帧更新(通过调用它们的函数)。

随着项目的进展,我们需要在几个不同的地方调用我们的和对象的生成函数。最明显的是,当一个新的关卡开始时,我们需要生成这两个角色。在接下来的章节中,随着在关卡开始时需要执行的任务数量增加,我们将编写一个函数。现在,让我们在函数中调用和的,如下所示的突出显示的代码。添加这段代码,但请记住,这段代码最终将被删除和替换:

 
  

以前的代码只是调用并传入游戏世界中的位置以及重力。代码包裹在一个语句中,检查是否需要新的关卡。实际的生成代码将被移动到一个专门的函数中,但条件将成为完成项目的一部分。此外,被设置为一个相当任意的 10 秒。

接下来,我们将更新 Thomas 和 Bob。我们需要做的就是调用它们的函数,并传入这一帧所花费的时间。

添加下面突出显示的代码:

 
  

现在角色可以移动了,我们需要更新适当的对象,使它们围绕角色居中,并使它们成为关注的中心。当然,直到我们在游戏世界中有一些物体,才能实现实际运动的感觉。

请按照以下片段所示添加突出显示的代码:

 
  

先前的代码处理了两种可能的情况。首先,条件将左侧视图定位在周围,右侧视图定位在周围。当游戏处于全屏模式时执行的子句测试是否为。如果是,则全屏视图()围绕托马斯居中,否则围绕鲍勃居中。您可能还记得玩家可以使用E键在分屏模式和全屏模式之间切换,使用Q键在全屏模式下切换 Bob 和 Thomas。我们在类的函数中编写了这个代码,回到第十二章,抽象和代码管理-更好地利用 OOP

确保文件已打开,并添加如下突出显示的代码,如下片段所示:

 
  

请注意,我们在全屏模式下绘制了托马斯和鲍勃的全屏,左侧和右侧。还要注意,在分屏模式下绘制角色的方式有非常微妙的差异。在绘制屏幕的左侧时,我们切换了绘制角色的顺序,并在鲍勃之后绘制了托马斯。因此,托马斯将始终位于左侧的顶部,鲍勃位于右侧。这是因为左侧为托马斯控制的玩家,右侧为鲍勃控制的玩家。

您可以运行游戏,看到托马斯和鲍勃在屏幕中央:

绘制鲍勃和托马斯

如果您按Q键从托马斯切换焦点到鲍勃,您将看到进行轻微调整。如果您移动其中一个角色向左或向右(托马斯使用AD,鲍勃使用箭头键),您将看到它们相对于彼此移动。

尝试按E键在全屏和分屏之间切换。然后再次尝试移动两个角色以查看效果。在下面的截图中,您可以看到托马斯始终位于左侧窗口的中心,鲍勃始终位于右侧窗口的中心:

绘制鲍勃和托马斯

如果您让游戏运行足够长的时间,角色将每十秒重新生成在它们的原始位置。这是我们在完成游戏时需要的功能的开端。这种行为是由下降到零以下,然后将变量设置为引起的。

还要注意的是,直到我们绘制了层级的细节,我们才能看到移动的完整效果。实际上,虽然看不到,但两个角色都在以每秒 300 像素的速度持续下落。由于摄像头每帧都围绕它们居中,并且游戏世界中没有其他物体,我们看不到这种向下运动。

如果您想自己演示一下,请按照以下代码更改对的调用:

 
  

现在鲍勃没有重力效应,托马斯将明显远离他。如下截图所示:

绘制鲍勃和托马斯

我们将在下一章中添加一些可玩的关卡以进行交互。

Q)我们学习了多态性,但到目前为止,我没有注意到游戏代码中有任何多态性。

A)我们将在下一章中看到多态性的作用,当我们编写一个以作为参数的函数时。我们将看到如何将 Bob 或 Thomas 传递给这个新函数,并且它们将以相同的方式工作。

在这一章中,我们学习了一些新的 C++概念。首先,继承允许我们扩展一个类并获得其所有功能。我们还学到,我们可以将变量声明为受保护的,这将使子类可以访问它们,但它们仍然会被封装(隐藏)在所有其他代码之外。我们还使用了纯虚函数,这使得一个类成为抽象类,意味着该类不能被实例化,因此必须从中继承/扩展。我们还介绍了多态的概念,但需要等到下一章才能在我们的游戏中使用它。

接下来,我们将为游戏添加一些重要功能。在接下来的一章中,Thomas 和 Bob 将会行走、跳跃和下落。他们甚至可以跳在彼此的头上,以及探索一些从文本文件加载的关卡设计。

这一章可能是这个项目中最令人满意的。原因是到最后,我们将有一个可玩的游戏。虽然还有一些功能要实现(声音,粒子效果,HUD 和着色器效果),但鲍勃和托马斯将能够奔跑,跳跃和探索世界。此外,你将能够通过简单地在文本文件中制作平台和障碍物来创建几乎任何大小或复杂度的级别设计。

本章将通过以下主题来实现所有这些内容:

  • 探索如何在文本文件中设计级别
  • 构建一个类,它将从文本文件加载级别,将它们转换为我们的游戏可以使用的数据,并跟踪级别细节,如生成位置,当前级别和允许的时间限制
  • 更新游戏引擎以使用
  • 编写一个多态函数来处理 Bob 和 Thomas 的碰撞检测

记得我们在第十二章中介绍的精灵表吗,抽象和代码管理-更好地利用 OOP。这里再次显示,用数字注释表示我们将构建级别的每个瓦片:

设计一些级别

我将屏幕截图放在灰色背景上,这样你可以清楚地看到精灵表的不同细节。方格背景表示透明度级别。因此,除了数字 1 之外的所有瓦片都会至少显示一点背后的背景:

  • 瓦片 0 是完全透明的,将用于填补没有其他瓦片的空隙
  • 瓷砖 1 是为了托马斯和鲍勃将走的平台
  • 瓷砖 2 是用于火瓦片,瓦片 3 是用于水瓦片
  • 你可能需要仔细查看瓦片 4。它有一个白色的方形轮廓。这是托马斯和鲍勃必须一起到达的级别目标。

在讨论设计级别时,请记住这个屏幕截图。

我们将把这些瓦片号码的组合输入到文本文件中来设计布局。举个例子:

 
  

前面的代码转换为以下级别布局:

设计一些级别

请注意,为了获得前面屏幕截图中显示的视图,我必须缩小。此外,屏幕截图被裁剪了。级别的实际开始看起来像下面的屏幕截图:

设计一些级别

向你展示这些屏幕截图的目的有两个。首先,你可以看到如何使用简单和免费的文本编辑器快速构建级别设计。

只需确保使用等宽字体,这样所有数字都是相同大小。这样设计级别就会更容易。

其次,这些屏幕截图展示了设计的游戏方面。在级别的左侧,托马斯和鲍勃首先需要跳过一个小洞,否则他们将掉入死亡(重生)。然后他们需要穿过大片火焰。鲍勃不可能跳过那么多瓦片。玩家需要共同解决问题。鲍勃清除火瓦片的唯一方法是站在托马斯的头上,然后从那里跳,如下面的屏幕截图所示:

设计一些级别

然后很容易到达目标并进入下一个级别。

我强烈鼓励你完成本章,然后花一些时间设计你自己的级别。

我已经包含了一些级别设计,让你开始。它们在我们在第十二章中添加到项目中的文件夹中,抽象和代码管理-更好地利用 OOP

接下来是游戏的一些缩小视图,以及关卡设计代码的截图。代码的截图可能比重现实际的文本内容更有用。如果您确实想看到代码,只需打开文件夹中的文件。

代码如下所示:

代码声明 来保存当前地图包含的水平和垂直瓦片数的两个整数值。 包含 Bob 和 Thomas 应该生成的世界坐标。请注意,这不是与单位相关的瓦片位置,而是关卡中水平和垂直像素位置。

这是前面的代码将产生的关卡布局:

设计一些关卡

这个关卡是我在第十二章中提到的“信任之跃”关卡,抽象和代码管理-更好地利用 OOP

设计一些关卡

我已经突出显示了平台,因为它们在缩小的截图中不太清晰:

设计一些关卡

提供的设计很简单。游戏引擎将能够处理非常大的设计。您可以自由发挥想象力,构建一些非常大且难以完成的关卡。

当然,这些设计在我们学会如何加载它们并将文本转换为可玩的关卡之前实际上不会做任何事情。此外,在实现碰撞检测之前,将无法站在任何平台上。

首先,让我们处理加载关卡设计。

我们将需要经过多个阶段的编码才能使我们的关卡设计生效。我们将首先编写头文件。这将使我们能够查看和讨论类中的成员变量和函数。

接下来,我们将编写文件,其中将包含所有的函数定义。由于这是一个很长的文件,我们将把它分成几个部分,以便编写和讨论它们。

一旦类完成,我们将在游戏引擎(类)中添加一个实例。我们还将在类中添加一个新函数,我们可以在需要新关卡时从函数中调用。函数不仅将使用实例来加载适当的关卡,还将处理诸如生成玩家角色和准备时钟等方面。

如前所述,让我们通过编写文件来概述。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击头文件( ,然后在名称字段中键入。最后,单击添加按钮。现在我们准备好为类编写头文件了。

添加以下包含指令和私有变量,然后我们将讨论它们:

 
  

设计一些关卡

成员变量是一个浮点数,将用于乘以当前关卡中可用的时间。我们之所以要这样做,是因为通过改变(减少)这个值,我们将在玩家尝试同一关卡时缩短可用时间。例如,如果玩家第一次尝试第一关卡时获得 60 秒,那么 60 乘以 1 当然是 60。当玩家完成所有关卡并再次回到第一关卡时,将减少 10%。然后,当可用时间乘以 0.9 时,玩家可用的时间将是 54 秒。这比 60 少 10%。游戏将逐渐变得更加困难。

浮点变量保存了我们刚刚讨论的原始未修改的时间限制。

您可能已经猜到将保存当前正在播放的关卡编号。

常量将用于标记何时适合再次返回到第一关,并减少的值。

现在添加以下公共变量和函数声明:

 
  

在前面的代码中,有两个常量成员。是一个有用的常量,提醒我们精灵表中的每个瓦片都是五十像素宽和五十像素高。是一个有用的常量,使我们对的操作不那么容易出错。实际上,一个四边形中有四个顶点。现在我们不能忘记它。

、、和函数是简单的 getter 函数,返回我们在前面的代码块中声明的私有成员变量的当前值。

值得仔细研究的一个函数是。这个函数接收一个的引用,就像我们在 Zombie Arena 游戏中使用的那样。该函数可以在上工作,所有的更改都将出现在调用代码中的中。函数返回一个指向指针的指针,这意味着我们可以返回一个地址,该地址是值的二维数组的第一个元素。我们将构建一个值的二维数组,该数组将表示每个关卡的布局。当然,这些 int 值将从关卡设计文本文件中读取。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件( ,然后在名称字段中键入。最后,单击添加按钮。我们现在准备为类编写文件。

由于这是一个相当长的类,我们将把它分成六个部分来讨论。前五个将涵盖函数,第六个将涵盖所有其他内容。

添加以下包含指令和函数的第一部分(共五部分):

 
  

在包含指令之后,代码将和初始化为零。

接下来,增加。随后的语句检查是否大于。如果是,被设置回,并且减少了,以缩短所有关卡允许的时间。

代码然后根据的值进行切换。每个语句都初始化文本文件的名称,该文件包含了关卡设计和 Thomas 和 Bob 的起始位置,以及,这是问题关卡的未修改时间限制。

如果您设计自己的关卡,请在此处添加语句和相应的值。还要编辑文件中的常量。

现在添加函数的第二部分,如所示。在上一段代码之后立即添加代码。在添加代码时,仔细研究代码,以便我们可以讨论它:

 
  

在前面(第二部分)我们刚刚编写的代码中,我们声明了一个名为的对象,它打开了一个流到中包含的文件名。

代码使用循环遍历文件的每一行,但不记录任何内容。它只是通过递增来计算行数。在循环之后,使用将关卡的宽度保存在中。这意味着所有行的长度必须相同,否则我们会遇到麻烦。

此时,我们知道并已保存了中当前关卡的长度和宽度。

现在添加函数的第三部分,如所示。在上一段代码之后立即添加代码。在添加代码时,请仔细研究代码,以便我们讨论它:

 
  

首先,使用其函数清除。使用参数调用的函数将流重置到第一个字符之前。

接下来,我们声明一个指向指针的。请注意,这是使用关键字在自由存储/堆上完成的。一旦我们初始化了这个二维数组,我们就能够将其地址返回给调用代码,并且它将持续存在,直到我们删除它或游戏关闭。

循环从 0 到。在每次循环中,它向堆中添加一个新的值数组,以匹配的值。现在,我们有一个完全配置好的(对于当前关卡)二维数组。唯一的问题是里面什么都没有。

现在添加函数的第四部分,如所示。在上一段代码之后立即添加代码。在添加代码时,请仔细研究代码,以便我们讨论它:

 
  

首先,代码初始化一个名为的,它将一次保存一个关卡设计的行。我们还声明并初始化一个名为的,它将帮助我们计算行数。

循环重复执行,直到超过最后一行。在循环内部有一个循环,它遍历当前行的每个字符,并将其存储在二维数组中。请注意,我们使用准确访问二维数组的正确元素。函数将转换为。这是必需的,因为我们有一个用于而不是的二维数组。

现在添加函数的第五部分,如所示。在上一段代码之后立即添加代码。在添加代码时,请仔细研究代码,以便我们讨论它:

 
  

尽管这是我们将分成五个部分中最长的代码部分,但它也是最直接的。这是因为我们在 Zombie Arena 项目中看到了非常相似的代码。

嵌套的循环循环从零到关卡的宽度和高度。对于数组中的每个位置,将四个顶点放入,并从精灵表中分配四个纹理坐标。顶点和纹理坐标的位置是使用变量、和常量计算的。在内部循环的每次循环结束时,增加,很好地移动到下一个瓷砖上。

关于这个的重要事情是,它是通过引用传递给的。因此,将在调用代码中可用。我们将从类中的代码中调用。

一旦调用了这个函数,类将拥有一个来图形化表示关卡,并且拥有一个值的二维数组,作为关卡中所有平台和障碍物的数值表示。

的其余函数都是简单的 getter 函数,但请花时间熟悉每个函数返回的私有值。添加类的其余函数:

 
  

现在类已经完成,我们可以继续使用它。我们将在类中编写另一个函数来实现。

要清楚,这个函数是类的一部分,尽管它将把大部分工作委托给其他函数,包括我们刚刚构建的类的函数。

首先,让我们在文件中添加新函数的声明,以及一些其他新代码。打开文件,并添加以下文件的摘要快照中显示的突出显示的代码行:

 
  

你可以在先前的代码中看到以下内容:

  • 我们包括了文件
  • 我们添加了一个名为的实例
  • 我们添加了一个名为的
  • 我们添加了一个指向的指针,该指针将保存从返回的二维数组
  • 我们为精灵表添加了一个新的对象
  • 我们添加了函数的声明,现在我们将编写该函数

Solution Explorer中右键单击Source Files,然后选择Add | New Item...。在Add New Item窗口中,通过左键单击突出显示()C++ File,然后在Name字段中键入。最后,单击Add按钮。现在我们准备编写函数。

将函数的代码添加到文件中,然后我们可以讨论它:

 
  

首先,我们将设置为 false,以阻止更新函数的部分执行。接下来,我们循环遍历中的所有水平数组,并将它们删除。在循环之后,我们删除。

代码,调用了并准备了和,以及二维数组。关卡已经设置好,准备就绪。

通过调用初始化了,并使用函数生成了 Thomas 和 Bob,以及从返回的值。

最后,被设置为。正如我们将在几页后看到的那样,被设置为会导致调用。我们只想运行这个函数一次。

打开文件,并在构造函数的末尾添加突出显示的代码,以加载精灵表纹理:

 
  

在先前的代码中,我们只是将精灵表加载到中。

打开文件,并进行以下突出显示的更改和添加:

 
  

实际上,你应该删除而不是注释掉我们不再使用的行。我只是以这种方式向你展示,以便更清楚地看到更改。在先前的语句中,应该只有对的调用。

最后,在我们能够看到本章工作成果之前,打开文件,并进行以下突出显示的添加,以绘制表示关卡的顶点数组:

 
  

请注意,我们需要为所有屏幕选项(全屏、左侧和右侧)绘制。

现在你可以运行游戏了。不幸的是,Thomas 和 Bob 直接穿过了我们精心设计的所有平台。因此,我们无法尝试通过关卡并打败时间。

我们将使用矩形相交和 SFML 相交函数来处理碰撞检测。在这个项目中的不同之处在于,我们将把碰撞检测代码抽象成自己的函数,并且正如我们已经看到的,Thomas 和 Bob 有多个矩形(、、、),我们需要检查碰撞。

要明确,这个函数是 Engine 类的一部分。打开文件,并添加一个名为的函数声明。在下面的代码片段中突出显示了这一点:

 
  

从签名中可以看出,函数接受一个多态参数,即对象。正如我们所知,是抽象的,永远不能被实例化。然而,我们可以用和类继承它。我们将能够将或传递给。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件( ,然后在名称字段中键入。最后,单击添加按钮。现在我们准备编写函数。

将以下代码添加到。请注意,这只是该函数的第一部分:

 
  

首先我们声明一个名为的布尔值。这是函数返回给调用代码的值。它被初始化为。

接下来我们声明一个名为的,并用表示角色精灵整个矩形的相同矩形进行初始化。请注意,我们实际上不会使用这个矩形进行交集测试。之后,我们声明另一个名为的。我们将初始化为一个 50x50 的矩形。我们很快就会看到的使用。

接下来我们看看如何使用。我们通过扩展周围的区域几个块来初始化四个变量、、和。在接下来的四个语句中,我们检查不可能尝试在不存在的瓦片上进行碰撞检测。我们通过确保永远不检查小于零或大于或返回的值来实现这一点。

前面的所有代码所做的是创建一个用于碰撞检测的区域。在角色数百或数千像素远的方块上进行碰撞检测是没有意义的。此外,如果我们尝试在数组位置不存在的地方进行碰撞检测(小于零或大于),游戏将崩溃。

接下来,添加以下处理玩家掉出地图的代码:

 
  

角色要停止下落,必须与平台发生碰撞。因此,如果玩家移出地图(没有平台的地方),它将不断下落。前面的代码检查角色是否与、相交。如果不相交,那么它已经掉出地图,函数会将其发送回起点。

添加以下相当大的代码,然后我们将逐步讲解它的功能:

 
  

前面的代码使用相同的技术做了三件事。它循环遍历了 startX、endX 和 startY、endY 之间包含的所有值。对于每次循环,它都会检查并执行以下操作:

  • 角色是否被烧伤或淹死?代码确定当前被检查的位置是否是火瓦或水瓦。如果角色的头与这些瓦片之一相交,玩家将重新生成。我们还编写了一个空的块,为下一章添加声音做准备。
  • 角色是否触碰了普通瓦片?代码确定当前被检查的位置是否持有普通瓦片。如果它与表示角色各个身体部位的矩形之一相交,相关的函数就会被调用(、、和)。传递给这些函数的值以及函数如何使用这些值重新定位角色是相当微妙的。虽然不必仔细检查这些值来理解代码,但您可能会喜欢查看传递的值,然后参考上一章类的适当函数。这将帮助您准确理解发生了什么。
  • 角色是否触碰到了目标瓦片?这是通过代码来确定的。我们只需要将设置为。类的函数将跟踪托马斯和鲍勃是否同时到达了目标。我们将在中编写这段代码,马上就会。

在函数中添加最后一行代码:

 
  

前面的代码返回,以便调用代码可以跟踪并适当地响应如果两个角色同时到达目标。

现在我们只需要每帧调用一次函数。在文件的代码块中添加以下突出显示的代码:

 
  

先前的代码调用了函数,并检查鲍勃和托马斯是否同时到达了目标。如果是,下一个关卡将通过将设置为来准备好。

您可以运行游戏并走在平台上。您可以到达目标并开始新的关卡。此外,首次,跳跃按钮(W或箭头上)将起作用。

如果您达到目标,下一个关卡将加载。如果您达到最后一关的目标,则第一关将以减少 10%的时间限制加载。当然,由于我们还没有构建 HUD,所以时间或当前关卡没有视觉反馈。我们将在下一章中完成。

然而,许多关卡需要托马斯和鲍勃一起合作。更具体地说,托马斯和鲍勃需要能够爬到彼此的头上。

在文件中添加前面添加的代码后面,即在部分内:

 
  

您可以再次运行游戏,并站在托马斯和鲍勃的头上,以到达以前无法到达的难以到达的地方:

更多碰撞检测

本章中有相当多的代码。我们学会了如何从文件中读取并将文本字符串转换为 char,然后转换为。一旦我们有了一个二维数组的,我们就能够填充一个来在屏幕上显示关卡。然后,我们使用完全相同的二维数组 int 来实现碰撞检测。我们使用了矩形相交,就像我们在僵尸竞技场项目中所做的那样,尽管这次,为了更精确,我们给了每个角色四个碰撞区域,分别代表他们的头部、脚部和左右两侧。

现在游戏完全可玩,我们需要在屏幕上表示游戏的状态(得分和时间)。在下一章中,我们将实现 HUD,以及比目前使用的更高级的音效。

在本章中,我们将添加所有的音效和 HUD。我们在之前的两个项目中都做过这个,但这次我们会有些不同。我们将探讨声音空间定位的概念,以及 SFML 如何使这个本来复杂的概念变得简单;此外,我们将构建一个 HUD 类来封装将信息绘制到屏幕上的代码。

我们将按照以下顺序完成这些任务:

  • 什么是空间定位?
  • SFML 如何处理空间定位
  • 构建一个类
  • 部署发射器
  • 使用类
  • 构建一个类
  • 使用类

空间定位是使某物相对于其所在的空间或内部的行为。在我们的日常生活中,自然界中的一切默认都是空间化的。如果一辆摩托车从左到右呼啸而过,我们会听到声音从一侧变得微弱到大声,当它经过时,它会在另一只耳朵中变得更加显著,然后再次消失在远处。如果有一天早上醒来,世界不再是空间化的,那将异常奇怪。

如果我们能让我们的视频游戏更像现实世界,我们的玩家就能更加沉浸其中。如果玩家能够在远处微弱地听到僵尸的声音,并且当它们靠近时,它们的非人类的哀嚎声从一个方向或另一个方向变得更大声,我们的僵尸游戏将会更有趣。

很明显,空间定位的数学将会很复杂。我们如何计算特定扬声器中的声音有多大声,基于声音来自的方向,以及从玩家(声音的听者)到发出声音的物体(发射器)的距离?

幸运的是,SFML 为我们做了所有复杂的事情。我们只需要熟悉一些技术术语,然后我们就可以开始使用 SFML 来对我们的音效进行空间定位。

为了让 SFML 能够正常工作,我们需要了解一些信息。我们需要知道声音在游戏世界中来自哪里。这个声音的来源被称为发射器。在游戏中,发射器可以是僵尸、车辆,或者在我们当前的项目中,是一个火焰图块。我们已经在游戏中跟踪了对象的位置,所以给 SFML 发射器位置将会非常简单。

我们需要了解的下一个因素是衰减。衰减是波动衰减的速率。你可以简化这个说法,并将其具体化为声音,说衰减是声音减小的速度。这在技术上并不准确,但对于本章的目的来说,这已经足够好了。

最后一个因素我们需要考虑的是听众。当 SFML 对声音进行空间定位时,它是相对于什么进行空间定位的?在大多数游戏中,逻辑的做法是使用玩家角色。在我们的游戏中,我们将使用 Thomas。

SFML 有许多函数,允许我们处理发射器、衰减和听众。让我们假设地看一下它们,然后我们将编写一些代码,真正为我们的项目添加空间化声音。

我们可以设置好一个准备播放的音效,就像我们经常做的那样,如下所示:

 
  

我们可以使用函数设置发射器的位置,如下面的代码所示:

 
  

如前面代码的注释所建议的,你如何获得发射器的坐标可能取决于游戏的类型。就像在 Zombie Arena 项目中所示的那样,这将会非常简单。在这个项目中,当我们设置位置时,我们将面临一些挑战。

我们可以使用以下代码设置衰减级别:

 
  

实际的衰减级别可能有点模糊。您希望玩家得到的效果可能与基于衰减的距离减小音量的准确科学公式不同。通常通过实验来获得正确的衰减级别。一般来说,衰减级别越高,声音级别降至静音的速度就越快。

此外,您可能希望在发射器周围设置一个区域,其中音量根本不会衰减。如果该功能在一定范围之外不合适,或者您有大量的声源并且不想过度使用该功能,您可以这样做。为此,我们可以使用函数,如下所示:

 
  

通过上一行代码,衰减将不会开始计算,直到听者距离发射器像素/单位。

SFML 库中的一些其他有用的函数包括函数。当传入 true 作为参数时,此函数将告诉 SFML 保持播放声音,如下面的代码所示:

 
  

声音将继续播放,直到我们用以下代码结束它:

 
  

不时地,我们会想要知道声音的状态(正在播放或已停止)。我们可以通过函数实现这一点,如下面的代码所示:

 
  

在使用 SFML 进行声音空间化的最后一个方面是什么?听者在哪里?我们可以使用以下代码设置听者的位置:

 
  

上述代码将使所有声音相对于该位置播放。这正是我们需要的远处火瓦或迫近的僵尸的咆哮声,但对于像跳跃这样的常规音效来说,这是一个问题。我们可以开始处理一个发射器来定位玩家的位置,但 SFML 为我们简化了这些操作。每当我们想播放普通声音时,我们只需调用,如下面的代码所示,然后以与迄今为止完全相同的方式播放声音。以下是我们可能播放普通、非空间化的跳跃音效的方式:

 
  

我们只需要在播放任何空间化声音之前再次调用。

现在我们有了广泛的 SFML 声音函数,我们准备为真实制作一些空间化的噪音。

您可能还记得在上一个项目中,所有的声音代码占用了相当多的行数。现在考虑到空间化,它将变得更长。为了使我们的代码易于管理,我们将编写一个类来管理所有声音效果的播放。此外,为了帮助我们进行空间化,我们还将向 Engine 类添加一个函数,但是当我们到达这一点时,我们将在本章后面讨论。

让我们开始编写和检查头文件。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(通过左键单击)头文件( ,然后在名称字段中输入。最后,单击添加按钮。现在我们准备为类编写头文件。

添加并检查以下代码:

 
  

我们刚刚添加的代码中没有什么棘手的地方。有五个对象和八个对象。其中三个对象将播放相同的。这解释了不同数量的/对象的原因。我们这样做是为了能够同时播放多个咆哮声效,具有不同的空间化参数。

请注意,有一个变量,它将帮助我们跟踪这些潜在同时发生的声音中我们应该下一个使用哪一个。

有一个构造函数,在那里我们将设置所有的音效,还有五个函数将播放音效。其中四个函数只是简单地播放普通音效,它们的代码将非常简单。

其中一个函数将处理空间化的音效,并且会更加深入。注意函数的参数。它接收一个,这是发射器的位置,和第二个,这是听众的位置。

现在我们可以编写函数定义。构造函数和函数有相当多的代码,所以我们将分别查看它们。其他函数很简短,所以我们将一次处理它们。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件( ,然后在名称字段中输入。最后,单击添加按钮。现在我们准备好为类编写文件。

在中添加以下代码以包含指令和构造函数:

 
  

在之前的代码中,我们将五个声音文件加载到五个对象中。接下来,我们将八个对象与其中一个对象关联起来。注意、和都将从同一个,中播放。

接下来,我们设置了三种火焰声音的衰减和最小距离。

分别通过实验得到了和的值。一旦游戏运行起来,我鼓励你通过改变这些值来进行实验,看(或者说听)听到的差异。

最后,对于构造函数,我们在每个与火相关的对象上使用了函数。现在当我们调用时,它们将持续播放。

添加下面的函数,然后我们可以讨论它:

 
  

我们首先调用,根据传入的设置听众的位置。

接下来,代码根据的值进入块。每个语句都做完全相同的事情,但是针对、或。

在每个块中,我们使用传入的参数设置了发射器的位置。在每个块的代码的下一部分检查声音当前是否停止,如果是,则播放声音。我们很快就会看到如何得到传递给这个函数的发射器和听众的位置。

函数的最后部分增加了,并确保它只能等于 1、2 或 3,这是块所要求的。

添加这四个简单的函数:

 
  

、和函数只做两件事。首先,它们各自调用,所以音效不是空间化的,使音效成为普通,而不是定向的,然后它们在适当的对象上调用。

这就结束了类。现在我们可以在类中使用它。

打开文件,并添加一个新的类的实例,如下面突出显示的代码所示:

 
  

在这一点上,我们可以使用来调用各种函数。不幸的是,仍然需要做更多的工作来管理发射器(火焰瓦片)的位置。

打开文件,并为函数添加一个新的原型和一个新的 STL of 对象:

 
  

函数以的对象作为参数,以及指向(二维数组)的指针。将保存每个级别中发射器的位置,而数组是我们的二维数组,它保存级别的布局。

函数的工作是扫描的所有元素,并决定在哪里放置发射器。它将其结果存储在中。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击C++文件()并在名称字段中键入来突出显示。最后,单击添加按钮。现在我们可以编写新函数。

添加完整的代码;确保在学习代码时,我们可以讨论它:

 
  

一些代码乍一看可能会很复杂。了解我们用来选择发射器位置的技术将使其变得更简单。在我们的级别中,通常有大块的火瓦。在我设计的一个级别中,有超过 30 个火瓦。代码确保在给定矩形内只有一个发射器。这个矩形存储在中,大小为 300x300 像素()。

该代码设置了一个嵌套的循环,循环遍历以寻找火瓦。当找到一个时,它确保它不与相交。只有这样,它才使用函数向添加另一个发射器。在这样做之后,它还更新以避免获得大量的声音发射器。

让我们发出一些声音。

打开文件,并添加对新的函数的调用,如下面的代码所示:

 
  

要添加的第一个声音是跳跃声音。您可能还记得键盘处理代码位于和类中的纯虚函数中,并且函数在成功启动跳跃时返回。

打开文件,并添加突出显示的代码行,以在 Thomas 或 Bob 成功开始跳跃时播放跳跃声音:

 
  

打开文件,并添加突出显示的代码行,以在 Thomas 和 Bob 同时达到当前级别目标时播放成功声音:

 
  

同样在文件中,我们将添加代码来循环遍历向量,并决定何时需要调用类的函数。

仔细观察新突出显示的代码周围的少量上下文。在完全正确的位置添加此代码是至关重要的:

 
  

以前的代码有点像声音的碰撞检测。每当 Thomas 停留在一个 500x500 像素的矩形内,围绕一个火焰发射器时,就会调用函数,传入发射器和 Thomas 的坐标。函数会完成其余的工作并触发一个空间化的循环声音效果。

打开文件,找到适当的位置,并按照以下所示添加突出显示的代码。这两行突出显示的代码触发了当角色掉入水或火瓦时播放声音效果:

 
  

玩游戏将允许您听到所有声音,包括附近火瓦的酷空间化。

HUD 非常简单,与书中的其他两个项目没有什么不同。我们要做的不同之处在于将所有代码封装在一个新的 HUD 类中。如果我们将所有字体、文本和其他变量声明为这个新类的成员,然后在构造函数中初始化它们并为所有值提供 getter 函数。这将使类清除大量的声明和初始化。

首先,我们将使用所有成员变量和函数声明编写文件。在解决方案资源管理器中右键单击头文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(通过左键单击)头文件(),然后在名称字段中键入。最后,单击添加按钮。现在我们准备为类编写头文件。

将以下代码添加到中:

 
  

在前面的代码中,我们添加了一个实例和三个实例。对象将用于显示提示用户启动、剩余时间和当前级别编号的消息。

公共函数更有趣。首先是构造函数,大部分代码将在其中。构造函数将初始化和对象,并将它们相对于当前屏幕分辨率定位在屏幕上。

三个 getter 函数,、和将返回一个对象,以便能够将它们绘制到屏幕上。

和函数将用于更新和中显示的文本,分别。

现在我们可以编写刚刚概述的所有函数的定义。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,选择(通过左键单击)C++文件( ,然后在名称字段中键入。最后,单击添加按钮。现在我们准备为类编写文件。

添加包含指令和以下代码,然后我们将讨论它:

 
  

首先,我们将水平和垂直分辨率存储在名为的中。接下来,我们从我们在第十二章中添加的目录中加载字体,抽象和代码管理 - 更好地利用面向对象编程

接下来的四行代码设置了的字体、颜色、大小和文本。此后的代码块捕获了包裹的矩形的大小,并进行计算以确定如何将其居中放置在屏幕上。如果您想对代码的这部分进行更详细的解释,请参考第三章:C++字符串、SFML 时间 - 玩家输入和 HUD

构造函数中的最后两个代码块设置了和的字体、文本大小、颜色、位置和实际文本。然而,我们很快就会看到,这两个对象将通过两个 setter 函数进行更新,只要需要就可以更新。

在我们刚刚添加的代码之后,立即添加以下 getter 和 setter 函数:

 
  

前面代码中的前三个函数简单地返回了适当的对象,、和。在屏幕上绘制 HUD 时,我们将很快使用这些函数。最后两个函数和使用函数来更新适当的对象,该值将从类的函数中每 500 帧传入。

完成所有这些后,我们可以在游戏引擎中使用 HUD 类。

打开,为我们的新类添加一个包含,声明新的类的实例,并且声明并初始化两个新的成员变量,用于跟踪我们更新 HUD 的频率。正如我们在前两个项目中学到的那样,我们不需要为每一帧都这样做。

将以下代码添加到中:

 
  

接下来,我们需要在类的函数中添加一些代码。打开并添加突出显示的代码以在每 500 帧更新一次 HUD:

 
  

在之前的代码中,每帧递增。当超过时,执行进入块。在块内,我们使用对象来更新我们的,就像我们在之前的两个项目中所做的那样。然而,正如你可能期望的那样,在这个项目中我们使用了类,所以我们调用和函数,传入对象需要设置的当前值。

块中的最后一步是将设置回零,这样它就可以开始计算下一个更新。

最后,打开文件,并添加高亮代码以在每一帧绘制 HUD:

 
  

之前的代码通过使用 HUD 类的 getter 函数来绘制 HUD。请注意,只有在游戏当前没有进行时才会调用绘制提示玩家开始的消息。

运行游戏并玩几个关卡,看时间倒计时和关卡增加。当你再次回到第一关时,注意你的时间比之前少了 10%。

我们的游戏《Thomas Was Late》不仅可以完全玩得了,还有方向性的音效和简单但信息丰富的 HUD,而且我们还可以轻松添加新的关卡。在这一点上,我们可以说它已经完成了。

添加一些闪光效果会很好。在接下来的章节中,我们将探讨两个游戏概念。首先,我们将研究粒子系统,这是我们如何处理爆炸或其他特殊效果的方法。为了实现这一点,我们需要学习更多的 C++知识,看看我们如何彻底重新思考我们的游戏代码结构。

之后,当我们学习 OpenGL 和可编程图形管线时,我们将为游戏添加最后的点睛之笔。然后,我们将有机会涉足GLSL语言,这使我们能够编写直接在 GPU 上执行的代码,以创建一些特殊效果。

在本章的最后,我们将探讨 C++概念,即扩展其他人的类。更具体地说,我们将研究 SFML 类以及将其用作我们自己类的基类的好处。我们还将浅尝 OpenGL 着色器的主题,并看看如何使用另一种语言OpenGL 着色语言GLSL)编写代码,可以直接在图形卡上运行,可以产生平滑的图形效果,否则可能是不可能的。像往常一样,我们还将利用我们的新技能和知识来增强当前项目。

以下是我们将按顺序涵盖的主题列表:

  • SFML Drawable 类
  • 构建一个粒子系统
  • OpenGl 着色器和 GLSL
  • 在 Thomas Was Late 游戏中使用着色器

类只有一个函数。它也没有变量。此外,它唯一的功能是纯虚拟的。这意味着如果我们从继承,我们必须实现它唯一的功能。这个目的,你可能还记得第十二章,抽象和代码管理-更好地利用 OOP,就是我们可以使用从继承的类作为多态类型。更简单地说,SFML 允许我们对对象做的任何事情,我们都可以用从它继承的类来做。唯一的要求是我们必须为纯虚拟函数提供定义。

一些从继承的类已经包括和(以及其他类)。每当我们使用或时,我们都将它们传递给类的函数。

我们之所以能够在本书中绘制的每个对象都继承自。我们可以利用这一知识来使我们受益。

我们可以用任何我们喜欢的对象从继承,只要我们实现纯虚拟的函数。这也是一个简单的过程。假设从继承的类的头文件()将如下所示:

 
  

在前面的代码中,我们可以看到纯虚拟的函数和一个 Sprite。请注意,没有办法在类的外部访问私有的,甚至没有函数!

文件看起来可能是这样的:

 
  

在前面的代码中,请注意函数的简单实现。参数超出了本书的范围。只需注意参数用于调用并传递以及,另一个参数。

虽然不需要理解参数就能充分利用,但在本书的背景下,你可能会感兴趣。您可以在 SFML 网站上阅读有关 SFML 类的更多信息:

在主游戏循环中,我们现在可以将实例视为,或者从继承的任何其他类:

 
  

正因为是,我们才能将其视为或,并且因为我们覆盖了纯虚拟的函数,一切都按我们想要的方式工作。让我们看看另一种将绘图代码封装到游戏对象中的方法。

还可以通过在我们的类中实现自己的函数来保留所有绘图功能,也许像以下代码一样:

 
  

先前的代码假定代表我们正在绘制的当前类的视觉外观,就像在本项目和上一个项目中一样。假设包含函数的类的实例称为,并且进一步假设我们有一个名为的的实例,然后我们可以使用以下代码从主游戏循环中绘制对象:

 
  

在这个解决方案中,我们将作为参数传递给函数。然后,函数使用来绘制。

这种解决方案似乎比扩展更简单。我们之所以按照建议的方式进行操作(扩展 Drawable)并不是因为这个项目本身有很大的好处。我们很快将用这种方法绘制一个漂亮的爆炸,原因是这是一个很好的学习技巧。

通过本书完成的每个项目,我们都学到了更多关于游戏、C++和 SFML。从一个游戏到下一个游戏,我们所做的最大的改进可能是我们的代码结构——我们使用的编程模式。

如果这本书有第四个项目,我们可能会更进一步。不幸的是,没有,但是想一想如何改进我们的代码。

想象一下,我们游戏中的每个对象都是从一个简单的抽象基类派生出来的。让我们称之为。游戏对象可能会有具体的函数用于和其他函数。它可能会有一个纯虚拟的函数(因为每个对象的更新方式都不同)。此外,考虑继承自。

现在看看这个假设的代码:

 
  

与最终项目相比,上述代码在封装、代码可管理性和优雅性方面有了很大的进步。如果你看一下以前的代码,你会发现有一些未解答的问题,比如碰撞检测的位置在哪里。然而,希望你能看到,进一步的学习(通过构建很多游戏)将是掌握 C++所必需的。

虽然我们不会以这种方式实现整个游戏,但我们将看到如何设计一个类()并将其直接传递给。

在我们开始编码之前,看一看我们要实现的确切内容将会很有帮助。看一下以下的屏幕截图:

构建粒子系统

这是一个纯色背景上的粒子效果的屏幕截图。我们将在游戏中使用这个效果。

我们实现效果的方式如下:

  1. 生成 1,000 个点(粒子),一个在另一个顶部,在选择的像素位置。
  2. 在游戏的每一帧中,以预定但随机的速度和角度将 1,000 个粒子向外移动。
  3. 重复第二步两秒钟,然后使粒子消失。

我们将使用来绘制所有的点,使用作为原始类型来直观表示每个粒子。此外,我们将继承自,以便我们的粒子系统可以自行处理绘制。

类将是一个简单的类,表示 1,000 个粒子中的一个。让我们开始编码。

在“解决方案资源管理器”中右键单击“头文件”,然后选择“添加”|“新项目...”。在“添加新项目”窗口中,突出显示(通过左键单击)“头文件”(.h),然后在“名称”字段中键入。最后,单击“添加”按钮。我们现在准备为类编写头文件。

将以下代码添加到文件中:

 
  

在上述代码中,我们有两个对象。一个表示粒子的水平和垂直坐标,另一个表示水平和垂直速度。

当速度在多个方向上发生变化时,合并的值也定义了一个方向。这就是所谓的速度;因此,Vector2f 被称为。

我们还有一些公共函数。首先是构造函数。它接受一个,将用于让它知道这个粒子将具有什么方向/速度。这意味着系统而不是粒子本身将选择速度。

接下来是函数,它接受前一帧所花费的时间。我们将使用这个时间来精确地移动粒子。

最后两个函数和用于将粒子移动到位置并找出其位置。

当我们编写它们时,所有这些功能都会变得非常清晰。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击C++文件()然后在名称字段中输入,最后,单击添加按钮。我们现在准备为类编写文件。

将以下代码添加到中:

 
  

所有这些函数都使用了我们之前见过的概念。构造函数使用传入的对象设置了和的值。

函数通过将乘以经过的时间()来移动粒子的水平和垂直位置。请注意,为了实现这一点,我们只需将两个对象相加即可。无需分别为xy成员执行计算。

如前所述,函数将使用传入的值初始化对象。函数将返回给调用代码。

我们现在有一个完全功能的类。接下来,我们将编写一个类来生成和控制粒子。

类为我们的粒子效果大部分工作。我们将在类中创建此类的实例。

解决方案资源管理器中右键单击头文件,然后选择添加 | 新项目...。在添加新项目窗口中,通过左键单击头文件()然后在名称字段中输入,最后,单击添加按钮。我们现在准备为类编写头文件。

将类的代码添加到中:

 
  

让我们一点一点地来。首先,注意我们是从继承的。这将使我们能够将我们的实例传递给,因为是。

有一个名为的,类型为。这个将保存每个实例。接下来是一个名为的。这将用于以一堆原语的形式绘制所有粒子。

,变量是每个效果将持续的时间。我们将在构造函数中初始化它。

布尔变量将用于指示粒子系统当前是否正在使用。

接下来,在公共部分,我们有纯虚函数,我们将很快实现它来处理当我们将实例传递给时发生的情况。

函数将准备和。它还将使用它们的速度和初始位置初始化所有对象(由持有)。

函数将循环遍历中的每个实例,并调用它们各自的函数。

函数提供对变量的访问,以便游戏引擎可以查询当前是否正在使用。

让我们编写函数定义来看看内部发生了什么。

解决方案资源管理器中右键单击源文件,然后选择添加 | 新建项...。在添加新项窗口中,通过左键单击C++文件( ,然后在名称字段中输入。最后,单击添加按钮。现在我们准备为类编写文件。

我们将把这个文件分成五个部分来编码和讨论它。按照这里所示的方式添加代码的第一部分:

 
  

在必要的之后,我们有函数的定义。我们使用作为参数调用,以便知道它将处理什么类型的基元。我们使用传入函数的来调整的大小。

循环为速度和角度创建随机值。然后使用三角函数将这些值转换为一个存储在中的向量,即。

如果您想了解三角函数(、和)如何将角度和速度转换为向量,您可以查看这个系列文章:

循环(以及函数)中发生的最后一件事是将向量传递给构造函数。新的实例使用函数存储在中。因此,使用值为的调用意味着我们有一千个实例,具有随机速度,存储在中等待爆炸!

接下来,在中添加函数:

 
  

函数比起一开始看起来要简单得多。首先,减去传入的时间。这样我们就知道两秒已经过去了。声明了一个向量迭代器,用于。

循环遍历中的每个实例。对于每一个粒子,它调用其函数并传入。每个粒子都会更新其位置。粒子更新完毕后,使用粒子的函数更新中的适当顶点。在每次循环结束时,循环中的会递增,准备下一个顶点。

在循环完成后,检查是否是时候关闭效果了。如果两秒已经过去,被设置为。

接下来,添加函数:

 
  

这是我们将调用以启动粒子系统运行的函数。因此,可以预料到,我们将设置为,设置为。我们声明一个 ,用于迭代中的所有对象,然后在循环中这样做。

在循环中,我们将顶点数组中的每个粒子设置为黄色,并将每个位置设置为传入的。请记住,每个粒子的生命都是从完全相同的位置开始的,但它们每个都被分配了不同的速度。

接下来,添加纯虚拟的 draw 函数定义:

 
  

在上面的代码中,我们简单地使用调用,传入和。这正如我们在本章早些时候讨论时所讨论的一样,只是我们传入了我们的,它包含了 1000 个点的基元,而不是假设的飞船 Sprite。

最后,添加函数:

 
  

函数是一个简单的 getter 函数,返回的值。我们将看到这在确定粒子系统的当前状态时是有用的。

让我们的粒子系统工作非常简单,特别是因为我们继承自。

打开并添加一个对象,如下所示的高亮代码:

 
  

接下来,初始化系统。

打开文件,并在构造函数的末尾添加短暂的高亮代码:

 
  

和实例的已经准备就绪。

打开文件,并添加以下高亮代码。它可以直接放在函数的末尾:

 
  

在先前的代码中,只需要调用。请注意,它被包裹在一个检查中,以确保系统当前正在运行。如果它没有运行,更新它就没有意义。

打开文件,其中包含函数。我们在第十五章中编写它时留下了一个注释,构建可玩级别和碰撞检测

从上下文中确定正确的位置,并添加高亮代码,如下所示:

 
  

首先,代码检查粒子系统是否已经运行。如果没有,它会检查当前正在检查的瓷砖是否是水砖或火砖。如果是其中之一,它会检查角色的脚是否接触。当这些语句中的每一个为时,通过调用函数并传入角色中心的位置作为启动效果的坐标来启动粒子系统。

这是最棒的部分。看看绘制有多简单。在检查粒子系统实际运行后,我们直接将实例传递给函数。

打开文件,并在以下代码中显示的所有位置添加高亮代码:

 
  

请注意在先前的代码中,我们必须在所有的左、右和全屏代码块中绘制粒子系统。

运行游戏,将角色的一只脚移动到火砖的边缘。注意粒子系统突然活跃起来:

绘制粒子系统

现在是新的东西。

OpenGLOpen Graphics Library)是一个处理 2D 和 3D 图形的编程库。OpenGL 适用于所有主要的桌面操作系统,也有一个在移动设备上运行的版本 OpenGL ES。

OpenGL 最初发布于 1992 年。它在二十多年的时间里得到了改进和完善。此外,图形卡制造商设计他们的硬件以使其与 OpenGL 良好地配合工作。告诉你这一点的目的不是为了历史课,而是要解释如果你想让游戏在不仅仅是 Windows 上运行,特别是在桌面上的 2D(和 3D)游戏中使用 OpenGL 是一个明显的选择。我们已经在使用 OpenGL,因为 SFML 使用 OpenGL。着色器是在 GPU 上运行的程序,所以让我们接下来了解更多关于它们。

通过 OpenGL,我们可以访问所谓的可编程管线。我们可以将我们的图形发送到的函数中进行绘制,每一帧。我们还可以编写在 GPU 上运行的代码,能够在调用之后独立地操作每个像素。这是一个非常强大的功能。

在 GPU 上运行的这些额外代码称为着色器程序。我们可以编写代码来操作我们图形的几何(位置),这称为顶点着色器。我们还可以编写代码,以独立地操作每个像素的外观,这称为片段着色器

尽管我们不会深入探讨着色器,但我们将使用 GLSL 编写一些着色器代码,并了解一些可能性。

在 OpenGL 中,一切都是点、线或三角形。此外,我们可以将颜色和纹理附加到这些基本几何图形,并且还可以组合这些元素以制作我们今天现代游戏中看到的复杂图形。这些统称为基元。我们可以通过 SFML 基元和,以及我们看到的和类来访问 OpenGL 基元。

除了基元,OpenGL 还使用矩阵。矩阵是一种执行算术的方法和结构。这种算术可以从非常简单的高中水平计算移动(平移)坐标,或者可以非常复杂,执行更高级的数学;例如,将我们的游戏世界坐标转换为 OpenGL 屏幕坐标,GPU 可以使用。幸运的是,正是这种复杂性在幕后由 SFML 处理。

SFML 还允许我们直接处理 OpenGL。如果您想了解更多关于 OpenGL 的信息,可以从这里开始:。如果您想直接在 SFML 中使用 OpenGL,可以阅读以下文章:。

一个应用程序可以有许多着色器。然后我们可以附加不同的着色器到不同的游戏对象上,以创建所需的效果。在这个游戏中,我们只有一个顶点着色器和一个片段着色器。我们将它应用到每一帧的背景上。

然而,当您看到如何将着色器附加到调用时,您会发现添加更多着色器是微不足道的。

我们将按照以下步骤进行:

  1. 首先,我们需要在 GPU 上执行的着色器代码。
  2. 然后我们需要编译该代码。
  3. 最后,我们需要将着色器附加到游戏引擎的绘制函数中的适当绘制调用。

GLSL 是一种独立的语言,它也有自己的类型,可以声明和使用这些类型的变量。此外,我们可以从我们的 C++代码与着色器程序的变量进行交互。

如果对可编程图形管线和着色器的强大功能有更多了解的话,我强烈推荐 Jacobo Rodríguez 的《GLSL Essentials》:。该书探讨了桌面上的 OpenGL 着色器,并且对于具有良好的 C++编程知识并愿意学习不同语言的任何读者来说都非常易懂。

正如我们将看到的,GLSL 与 C++有一些语法相似之处。

这是文件夹中文件中的代码。您不需要编写此代码,因为它是我们在第十二章中添加的资产中的代码,抽象和代码管理-更好地利用 OOP

 
  

前四行(不包括注释)是片段着色器将使用的变量。但它们不是普通的变量。我们首先看到的类型是。这些变量在两个之间的范围内。接下来是变量。这些变量可以直接从我们的 C++代码中操作。我们很快将看到如何做到这一点。

除了和类型之外,每个变量还有一个更常规的类型,用于定义实际数据:

  • 是一个具有四个值的向量
  • 是一个具有两个值的向量
  • 将保存一个纹理
  • 就像 C++中的

函数中的代码是实际执行的内容。如果仔细观察中的代码,你会看到每个变量的使用情况。然而,这段代码的具体作用超出了本书的范围。总之,纹理坐标()和像素/片段的颜色()会受到许多数学函数和操作的影响。请记住,这将在游戏的每一帧中的每个绘制调用中执行,对每个像素都会执行。此外,请注意,会在每一帧中传入不同的值。很快我们就会看到结果,会产生一种波纹效果。

这是文件中的代码。你不需要编写这个代码,因为它是我们在第十二章中添加的资产中的一部分,抽象和代码管理-更好地使用 OOP

 
  

首先,注意两个变量。这些变量与我们在片段着色器中操作的变量是一样的。在函数中,代码会操作每个顶点的位置。代码的工作原理超出了本书的范围,但在幕后进行了一些相当深入的数学运算,如果你感兴趣,那么探索 GLSL 将会很有趣(参见前面的提示)。

现在我们有两个着色器(一个片段着色器和一个顶点着色器)。我们可以在游戏中使用它们。

打开文件。添加突出显示的代码行,将一个名为的 SFML 实例添加到类中:

 
  

现在,引擎对象及其所有函数都可以访问。请注意,一个 SFML 对象将由两个着色器代码文件组成。

添加以下代码,检查玩家的 GPU 是否能处理着色器。如果不能,游戏将退出。

你的电脑必须非常老旧才无法运行。如果你的 GPU 无法处理着色器,请接受我的道歉。

接下来,我们将添加一个 else 子句,如果系统能够处理着色器,则实际加载着色器。打开文件,并将以下代码添加到构造函数中:

 
  

现在我们几乎准备好看到我们的波纹效果了。

打开文件。正如我们在编写着色器时讨论的那样,我们将直接从 C++代码中每帧更新变量。我们使用函数来实现。

添加突出显示的代码以更新着色器的变量,并更改每种可能的绘制场景中的调用:

 
  

最好是实际删除我展示的注释掉的代码行。我只是这样做是为了清楚地表明哪些代码行正在被替换。

运行游戏,你会得到一种怪异的熔岩效果。如果想玩得开心,可以尝试更改背景图像:

!在每一帧更新和绘制着色器

就是这样!我们的第三个也是最后一个游戏完成了。

在大结局中,我们探讨了粒子系统和着色器的概念。虽然我们可能只是看了最简单的情况,但我们还是成功地创建了一个简单的爆炸和一种怪异的熔岩效果。

请查看最终的简短章节,讨论接下来该做什么。

当你第一次翻开这本厚重的书时,最后一页可能看起来很遥远。但我希望这并不太困难!

重点是,你现在在这里,希望你对如何在 C++中构建游戏有很好的见解。

本章的重点不仅是祝贺你取得了很好的成就,还要指出这一页可能不应该是你旅程的终点。如果像我一样,每当你让一个新的游戏特性变得生动起来时,你可能想要学到更多。

也许让你惊讶的是,即使经过了这么多页的内容,我们只是浅尝辄止 C++。即使我们涉及的主题可能需要更深入地讨论,还有许多主题,一些相当重要的主题,我们甚至没有提到。考虑到这一点,让我们来看看接下来可能会发生什么。

如果你绝对必须获得正式的资格,那么唯一的方法就是接受正规教育。当然,这是昂贵且耗时的,我无法提供更多帮助。

另一方面,如果你想在工作中学习,也许是在开始制作最终发布的游戏时,接下来将讨论你可能想要做的事情。

也许我们每个项目面临的最困难的决定是如何构建我们的代码结构。在我看来,关于如何构建你的 C++游戏代码的绝佳信息来源是。其中一些讨论涉及到本书未涉及的概念,但其中很多内容都是完全可以理解的。如果你理解类、封装、纯虚函数和单例模式,那就深入了解这个网站吧。

我在整本书中已经多次提到了 SFML 的网站。如果你还没有访问过,请看一下这个链接:。

当你遇到你不理解的 C++主题(或者甚至从未听说过的主题)时,最简洁和最有组织的 C++教程可以在这个链接找到:

除此之外,还有四本关于 SFML 的书,你可能会感兴趣。它们都是很好的书,但适合的读者有很大不同。以下是这些书的列表,按照从最适合初学者到最技术性的顺序排列:

  • SFML Essentials,作者 Milcho G. Milchev:
  • SFML 蓝图,作者 Maxime Barbier:
  • SFML 游戏开发示例,作者 Raimondas Pupius:
  • SFML 游戏开发,作者 Jan Haller,Henrik Vogelius Hansson 和 Artur Moreira:
  • 你可能还想考虑为你的游戏添加逼真的 2D 物理效果。SFML 与 Box2D 物理引擎完美配合。这是官方网站的链接:http://box2d.org/。下一个链接可能是使用 C++的最佳指南:。
  • 最后,我可以不要脸地为初学游戏程序员推荐我的网站:。

最重要的是,非常感谢购买这本书,继续制作游戏!

版权声明


相关文章:

  • tinyxml读取xml2025-06-30 08:29:59
  • 数据库事务是2025-06-30 08:29:59
  • hsqldb h22025-06-30 08:29:59
  • fcntl.h windows2025-06-30 08:29:59
  • xml文件中注释怎么写2025-06-30 08:29:59
  • 黑客定位找人软件下载2025-06-30 08:29:59
  • leveldb lrucache2025-06-30 08:29:59
  • ad服务器和ldap服务器有什么区别2025-06-30 08:29:59
  • 在线客服系统网站2025-06-30 08:29:59
  • arduino通过l298n控制转速2025-06-30 08:29:59