原文:
annas-archive.org/md5/8b22c2649bdec9fa4ee716ae82ae0bb1
译者:飞龙
前言
这本书是关于以有趣的方式学习 C ++编程。从零开始,您将学习 C ++的基础知识,如变量和循环,直到高级主题,如继承和多态。您学到的一切都将被应用到构建三个完全可玩的游戏中。
这是我们这本书的三个项目。
Timber!!!
第一个游戏是一个令人上瘾的,快节奏的模仿非常成功的伐木工的游戏,store.steampowered.com/app/398710/
。我们的游戏 Timber!!!将让我们同时学习所有 C ++的基础知识,同时构建一个真正可玩的游戏。
僵尸竞技场
接下来,我们将构建一个疯狂的僵尸生存射击游戏,类似于 Steam 的热门游戏 Over 9,000 Zombies,store.steampowered.com/app/273500/
。玩家将拥有一把机关枪,并必须抵御不断增长的僵尸浪潮。所有这些将发生在一个随机生成的滚动世界中。为了实现这一点,我们将学习面向对象编程以及它如何使我们能够拥有一个大量的易于编写和维护的代码库。期待令人兴奋的功能,如数百个敌人,快速射击武器,拾取物品以及每一波后都可以“升级”的角色。
Thomas was Late
第三个游戏将是一个时尚而具有挑战性的单人和合作解谜平台游戏。它基于非常受欢迎的游戏 Thomas was Alone,store.steampowered.com/app/220780/
。期待学习一些酷炫的主题,如粒子效果,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 VertexArray
类,它允许我们构建一个大图像,可以使用单个图像文件中的多个图像非常快速和高效地绘制到屏幕上。到本章结束时,我们将拥有一个可扩展的、随机的、滚动的背景,使用引用和VertexArray
对象。
我们现在将讨论:
-
C++引用
-
SFML
VertexArrays
-
编写随机滚动背景
第八章,指针、标准模板库和纹理管理,在本章中,我们将学到很多,同时也会为游戏做很多工作。我们将首先学习指针这一基本的 C++主题。指针是保存内存地址的变量。通常,指针将保存另一个变量的内存地址。这听起来有点像引用,但我们将看到它们更加强大,我们将使用指针来处理不断增多的僵尸群。
我们还将学习标准模板库(STL),这是一组允许我们快速、轻松地实现常见数据管理技术的类。
一旦我们理解了 STL 的基础知识,我们就能够利用这些新知识来管理游戏中的所有纹理,因为如果我们有 1000 个僵尸,我们实际上不想为每一个加载一份僵尸图形到 GPU 中。
我们还将深入了解面向对象编程,并使用静态函数,这是一个可以在没有类实例的情况下调用的类函数。同时,我们将看到如何设计一个类,以确保只能存在一个实例。当我们需要保证代码的不同部分将使用相同的数据时,这是理想的。
在这一章中,我们将:
-
学习指针
-
学习 STL
-
使用静态函数和单例类实现 Texture Holder 类
-
实现一个指向一群僵尸的指针
-
编辑一些现有的代码,使用 TextureHolder 类为玩家和背景
第九章, 碰撞检测、拾取物品和子弹,到目前为止,我们已经实现了游戏的主要视觉部分。我们有一个可控制的角色在一个充满追逐他的僵尸的竞技场中奔跑。问题是它们彼此之间没有互动。僵尸可以毫无阻碍地穿过玩家。我们需要检测僵尸和玩家之间的碰撞。
如果僵尸能够伤害并最终杀死玩家,那么给玩家一些子弹是公平的。然后我们需要确保子弹能够击中并杀死僵尸。
同时,如果我们为子弹、僵尸和玩家编写碰撞检测代码,那么现在是添加一个用于健康和弹药拾取物品的类的好时机。
以下是我们将要做的事情和我们将要涵盖的顺序:
-
射击子弹
-
添加准星并隐藏鼠标指针
-
生成拾取物品
-
检测碰撞
第十章, 分层视图和实现 HUD,在这一章中,我们将看到 SFML 视图的真正价值。我们将添加大量的 SFML 文本对象,并像之前在 Timber!!!项目中那样操纵它们。新的是我们将使用第二个视图实例来绘制 HUD。这样,HUD 将始终整齐地定位在主游戏动作的顶部,而不管背景、玩家、僵尸和其他游戏对象在做什么。
我们将做以下事情:
-
在主页/游戏结束屏幕上添加文本和背景
-
在升级屏幕上添加文本
-
创建第二个视图
-
添加 HUD
第十一章, 音效、文件 I/O 和完成游戏,我们快要完成了。这一小节将演示我们如何使用 C++标准库轻松操作存储在硬盘上的文件,我们还将添加音效。当然,我们知道如何添加音效,但我们将讨论在代码中play
的调用应该放在哪里。我们还将收尾一些松散的地方,使游戏完整。在这一章中,我们将做以下事情:
-
保存和加载最高分
-
添加音效
-
允许玩家升级
-
创建无尽的多波次
第十二章 ,抽象和代码管理-更好地利用 OOP,在本章中,我们将首次查看本书的最终项目。该项目将具有高级功能,例如与玩家位置相关的从扬声器发出的定向声音。它还将具有分屏合作游戏。此外,该项目将介绍着色器的概念,这是用另一种语言编写的直接在图形卡上运行的程序。到第十六章 结束时,您将拥有一个完全功能的多人平台游戏,其风格类似于经典游戏 Thomas Was Alone。本章的主要重点将是启动项目,特别是探索如何构造代码以更好地利用 OOP。以下是本章的详细信息。
-
介绍最终项目 Thomas Was Late,包括游戏功能和项目资产
-
详细讨论我们将如何改进代码结构,与之前的项目相比
-
编写 Thomas Was Late 游戏引擎
-
实现分屏功能
第十三章 ,高级 OOP-继承和多态,在本章中,我们将通过查看继承和多态的略微更高级的概念,进一步扩展我们对 OOP 的知识。然后,我们将能够使用这些新知识来实现我们游戏的明星角色 Thomas 和 Bob。以下是我们将更详细地涵盖的内容:
-
学习如何使用继承扩展和修改类
-
使用多态将类的对象视为多种类型的类
-
学习抽象类以及设计从未实例化的类如何实际上是有用的
-
构建一个抽象的
PlayableCharacter
类 -
使用继承与
Thomas
和Bob
类 -
将 Thomas 和 Bob 添加到游戏项目
第十四章 ,构建可玩关卡和碰撞检测,本章可能是本项目中最令人满意的章节之一。原因是到最后,我们将拥有一个可玩的游戏。尽管还有一些功能需要实现(声音、粒子效果、HUD、着色器效果),但 Bob 和 Thomas 将能够奔跑、跳跃和探索世界。此外,您将能够通过简单地在文本文件中创建平台和障碍物,轻松创建几乎任何大小或复杂度的自己的关卡设计。我们将通过以下主题实现所有这些:
-
探索如何在文本文件中设计关卡
-
构建
LevelManager
类,该类将从文本文件加载关卡,将其转换为我们的游戏可以使用的数据,并跟踪关卡细节,如生成位置、当前关卡和允许的时间限制 -
更新游戏引擎以使用
LevelManager
-
编写一个多态函数来处理 Bob 和 Thomas 的碰撞检测
第十五章 ,声音空间化和 HUD,在本章中,我们将添加所有的音效和 HUD。我们在之前的两个项目中都做过这个,但这次我们会有所不同。我们将探索声音空间化的概念,以及 SFML 如何使这个本来复杂的概念变得简单易行;此外,我们将构建一个 HUD 类来封装我们的代码,将信息绘制到屏幕上。
我们将按照以下顺序完成这些任务:
-
什么是空间化?
-
SFML 如何处理空间化
-
构建一个
SoundManager
类 -
部署发射器
-
使用
SoundManager
类 -
构建
HUD
类 -
使用
HUD
类
第十六章, 扩展 SFML 类、粒子系统和着色器,在这一章中,我们将探讨 C++ 中扩展其他人类的概念。更具体地说,我们将研究 SFML Drawable
类以及将其用作我们自己类的基类的好处。我们还将浅尝 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 指令包含其他上下文。”
代码块设置如下:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出都将按如下方式书写:
# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample /etc/asterisk/cdr_mysql.conf
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“单击下一步按钮将您移至下一个屏幕。”
注意
警告或重要提示将以如下方式显示在一个框中。
提示
提示和技巧显示如下。
第一章:C++,SFML,Visual Studio 和开始第一个游戏
欢迎来到《开始 C++游戏编程》。我将立即让你开始你的写作之旅,使用 C++和OpenGL-powered SFML为 PC 编写出色的游戏。
这是一个相当庞大的第一章,但我们将学到我们需要的一切,以便让我们第一个游戏的第一部分运行起来。在本章中,我们将涵盖以下内容:
-
了解我们将构建的游戏
-
学习一些关于 C++的知识
-
探索 SFML 及其与 C++的关系
-
查看我们将在整本书中使用的 Visual Studio 软件
-
设置游戏开发环境
-
创建一个可重用的项目模板,这将节省大量时间
-
计划并准备第一个游戏项目,伐木者!!!
-
编写本书的第一个 C++代码,并制作一个可运行的游戏来绘制背景
游戏
我们将逐步学习超快的 C++语言的基础知识,然后将新知识应用到实践中,因此应该相当容易地为我们正在构建的三款游戏添加酷炫的功能。
提示
如果你在本章的任何内容上遇到困难,请查看最后的处理错误和常见问题解答部分。
这是我们书中的三个项目:
伐木者!!!
第一个游戏是一款令人上瘾、节奏快速的《伐木工》的克隆版本,该游戏可以在store.steampowered.com/app/398710/
找到。我们的游戏《伐木者!!!》将在构建一个真正可玩的游戏的同时,向我们介绍所有 C++的基础知识。当我们完成并添加了一些最后一刻的增强功能时,我们的游戏版本将是这个样子。
僵尸竞技场
接下来,我们将构建一个疯狂的僵尸生存射击游戏,类似于 Steam 的热门游戏《超过 9000 只僵尸》,该游戏可以在store.steampowered.com/app/273500/
找到。玩家将拥有一把机关枪,并必须抵御不断增长的僵尸浪潮。所有这些将发生在一个随机生成的滚动世界中。为了实现这一点,我们将学习面向对象编程以及它如何使我们能够拥有一个庞大的代码库(大量代码),易于编写和维护。期待令人兴奋的功能,如数百个敌人、快速射击武器、拾取物品以及每一波后都可以“升级”的角色。
托马斯迟到了
第三款游戏将是一款时尚而具有挑战性的益智平台游戏,可以作为单人和合作游玩。它基于非常受欢迎的游戏《托马斯是孤独的》,该游戏可以在store.steampowered.com/app/220780/
找到。期待学习有关粒子效果、OpenGL 着色器和分屏合作多人游戏等酷炫主题。
提示
如果你现在想玩任何游戏,可以在Runnable Games
文件夹中的下载包中进行。只需双击相应的.exe
文件。请注意,在这个文件夹中,你可以运行已完成的游戏,也可以从任何章节的部分完成状态中运行任何游戏。
让我们开始介绍 C++、Visual Studio 和 SFML!
了解 C++
你可能会有一个问题,为什么要使用 C++?C++很快,非常快。使其成为这种情况的原因是我们编写的代码直接转换为机器可执行指令。这些指令构成了游戏。可执行游戏包含在一个.exe
文件中,玩家只需双击即可运行。
这个过程中有几个步骤。首先,预处理器查看我们的代码中是否需要包含其他代码,并在必要时添加它。接下来,编译器程序将所有代码编译成目标文件。最后,一个名为链接器的第三个程序将所有目标文件连接成可执行文件,这就是我们的游戏。
此外,C++既是一个成熟的语言,同时又非常现代化。C++是一种面向对象的编程语言,这意味着我们可以以一种经过验证的方式编写和组织我们的代码,使我们的游戏高效且易于管理。这些好处以及这种必要性将随着我们在书中的进展而显现。
我提到的大部分其他代码都是 SFML,我们将在接下来的一分钟内了解更多关于 SFML 的信息。我刚刚提到的预处理器、编译器和链接器程序都是 Visual Studio 集成开发环境(IDE)的一部分。
Microsoft Visual Studio
Visual Studio 隐藏了预处理、编译和链接的复杂性。它将所有这些封装成一个按钮。此外,它为我们提供了一个流畅的用户界面,让我们输入我们的代码并管理将成为大量代码文件和其他项目资产的选择。
虽然有高级版本的 Visual Studio 需要花费数百美元,但我们可以在免费的Express 2015 for Desktop版本中构建我们的三个游戏。
SFML
**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++。
设置开发环境
现在你对我们将如何制作这些游戏有了一些了解,是时候设置开发环境,让我们开始编码了。
那么 Mac 和 Linux 呢?
我们制作的游戏可以在 Windows、Mac 和 Linux 上运行!我们使用的代码对于每个平台都是相同的。然而,每个版本都需要在其预期的平台上进行编译和链接,而 Visual Studio 将无法帮助我们处理 Mac 和 Linux。
说这本书完全适合 Mac 和 Linux 用户,尤其是完全的初学者,可能有些不公平。尽管,我猜,如果你是一个热衷于 Mac 或 Linux 的用户,并且对你的操作系统感到舒适,你将遇到的大部分额外挑战将在开发环境、SFML 和第一个项目的初始设置中。
为此,我强烈推荐以下教程,希望能替代接下来的大约 10 页(大约),直到*Planning Timber!!!*部分,当这本书应该再次适用于所有操作系统。
对于 Linux,阅读这篇概述:www.sfml-dev.org/tutorials/2.0/start-linux.php
。
对于 Linux,阅读这篇逐步指导:en.sfml-dev.org/forums/index.php?topic=9808.0
。
在 Mac 上,阅读这篇教程以及链接的文章:www.edparrish.net/common/sfml-.osx.html
。
在你的桌面上安装 Visual Studio Express 2015
安装 Visual Studio 几乎和下载一个文件并点击几下按钮一样简单。然而,如果我们仔细地按照我们的步骤来做,这将对我们有所帮助。因此,我将一步一步地走过安装过程。
微软 Visual Studio 网站表示你需要 5GB 的硬盘空间。然而,根据经验,我建议你至少需要 10GB 的可用空间。此外,这些数字有些模糊。如果你计划将其安装在辅助硬盘上,你仍然需要主硬盘上至少 5GB 的空间,因为无论你选择在哪里安装 Visual Studio,它也需要这个空间。
注意
总结这种模糊的情况:如果你打算将 Visual Studio 安装到主硬盘上,那么在主硬盘上必须有完整的 10GB 空间是必不可少的。另一方面,如果你打算安装到辅助硬盘上,确保你的主硬盘上有 5GB 的空间,辅助硬盘上有 10GB 的空间。是的,愚蠢,我知道!
-
你需要的第一件事是一个微软账户和登录详情。如果你有 Hotmail 或 MSN 邮箱地址,那么你已经有了一个。如果没有,你可以在这里免费注册一个:
login.live.com/
。 -
访问这个链接:
www.visualstudio.com/en-us/downloads/download-visual-studio-vs.aspx
。点击Visual Studio 2015,然后点击Express 2015 for desktop,然后点击Downloads按钮。下一个截图显示了三个点击的位置: -
等待短暂的下载完成,然后运行下载的文件。现在你只需要按照屏幕上的指示进行操作。但是,请记下你选择安装 Visual Studio 的文件夹。如果你想和我做的一样,就在你喜欢的硬盘上创建一个名为
Visual Studio 2015
的新文件夹,并安装到这个文件夹中。整个过程可能需要一段时间,取决于你的互联网连接速度。 -
当你看到下一个屏幕时,点击Launch并输入你的微软账户登录详情。
现在我们可以转向 SFML。
设置 SFML
这个简短的教程将带你下载 SFML 文件,使我们能够包含库中包含的功能。此外,我们将看到如何使用 SFML DLL 文件,这将使我们编译的目标代码能够与 SFML 一起运行。
-
访问 SFML 网站上的这个链接:
www.sfml-dev.org/download.php
。点击下一个显示的Latest stable version按钮。 -
当您阅读本指南时,最新版本几乎肯定已经更改。只要您正确执行下一步,这并不重要。我们要下载Visual C++ 2014的 32 位版本。这可能听起来有些违反直觉,因为我们刚刚安装了 Visual Studio 2015,您可能(最常见)有一台 64 位 PC。我们选择此下载的原因是因为 Visual C++ 2014 是 Visual Studio 2015 的一部分(Visual Studio 提供的不仅仅是 C++),我们将以 32 位构建游戏,以便它们在 32 位和 64 位机器上运行。为了明确起见,单击以下下载:
-
下载完成后,在安装 Visual Studio 的相同驱动器的根目录创建一个名为
SFML
的文件夹。还在安装 Visual Studio 的相同驱动器的根目录创建另一个文件夹,并将其命名为Visual Studio Stuff
。我们将在这里存储各种与 Visual Studio 相关的东西,因此Visual Studio Stuff
似乎是一个不错的名字。为了明确起见,这是在完成此步骤后我的硬盘的样子: -
显然,您在截图中突出显示的三个文件夹之间的文件夹可能与我的完全不同。现在我们准备好了即将创建的所有项目,创建一个新文件夹在
Visual Studio Stuff
内。将新文件夹命名为Projects
。 -
最后,解压 SFML 下载。在桌面上进行此操作。解压完成后,可以删除
zip
文件夹。您将在桌面上留下一个单独的文件夹。其名称将反映您下载的 SFML 版本。我的称为SFML-2.3.2-windows-vc14-32-bit
。您的文件名可能反映了一个更新的版本。双击此文件夹以查看内容,然后再次双击进入下一个文件夹(我的称为SFML-2.3.2
)。以下截图显示了当选择了整个内容时,我的SFML-2.3.2
文件夹的内容是什么样子的。您的应该看起来一样。 -
复制前面截图中所见的整个文件夹的内容,并将所有内容粘贴/拖放到第 3 步中创建的
SFML
文件夹中。在本书的其余部分,我将简称此文件夹为您的 SFML 文件夹。
现在我们准备在 Visual Studio 中开始使用 C++和 SFML。
创建可重用的项目模板
由于设置项目是一个相当繁琐的过程,我们将创建一个项目,然后将其保存为 Visual Studio 模板。这将节省我们每次开始新游戏时相当大量的工作。因此,如果您发现下一个教程有点乏味,请放心,您将永远不需要再次这样做:
-
启动 Visual Studio,在新项目窗口中,单击Visual C++旁边的小下拉箭头以显示更多选项,然后单击Win32,再单击Win32 控制台应用程序。您可以在下一个截图中看到所有这些选择。
-
现在,在新项目窗口的底部,在**名称:**字段中键入
HelloSFML
。 -
接下来,浏览到我们在上一篇教程中创建的
Visual Studio Stuff\Projects\
文件夹。这将是我们保存所有项目文件的位置。所有模板都是基于实际项目的。因此,我们将有一个名为HelloSFML
的项目,但我们将做的唯一事情就是从中制作一个模板。 -
完成上述步骤后,单击确定。下一个截图显示了应用程序设置窗口。选中控制台应用程序的复选框,并将其他选项保持如下所示。
-
单击完成,Visual Studio 将创建新项目。
-
接下来,我们将添加一些相当复杂和重要的项目设置。这是费力的部分,但由于我们将创建一个模板,我们只需要做一次。我们需要告诉 Visual Studio,或者更具体地说,Visual Studio 的代码编译器,从哪里找到 SFML 的特殊类型的代码文件。我所指的特殊类型的文件是头文件。头文件定义了 SFML 代码的格式。因此,当我们使用 SFML 代码时,编译器知道如何处理它。请注意,头文件与主源代码文件不同,并且它们包含在扩展名为
.hpp
的文件中。(当我们最终开始在第二个项目中添加自己的头文件时,所有这些将变得更清晰)。此外,我们需要告诉 Visual Studio 它在哪里可以找到 SFML 库文件。从 Visual Studio 的主菜单中选择项目 | HelloSFML 属性。 -
在生成的HelloSFML 属性页窗口中,执行下一截图中标记的步骤。
-
从配置:下拉菜单中选择所有配置。
-
从左侧菜单中选择C/C++,然后选择常规。
-
定位附加包含目录编辑框,并输入您的 SFML 文件夹所在的驱动器号,然后加上
\SFML\include
。如果您的SFML
文件夹位于 D 驱动器上,则要输入的完整路径如截图所示:D:\SFML\include
。如果您将 SFML 安装到其他驱动器上,则需要更改路径。 -
点击应用以保存到目前为止的配置。
-
现在,在同一窗口中,执行下一截图中标记的步骤。选择链接器,然后选择常规。
-
找到附加库目录编辑框,并输入您的
SFML
文件夹所在的驱动器号,然后加上\SFML\lib
。因此,如果您的SFML
文件夹位于 D 驱动器上,则要输入的完整路径如截图所示:D:\SFML\lib
。如果您将 SFML 安装到其他驱动器上,则需要更改路径。 -
点击应用以保存到目前为止的配置。
-
最后,在同一窗口中,执行下一截图中标记的步骤。将配置:下拉菜单(1)切换到调试,因为我们将在调试模式下运行和测试游戏。
-
选择链接器,然后选择输入(2)。
-
找到附加依赖项编辑框(3),并在最左侧点击进入。现在复制并粘贴/输入以下内容:
sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;
。再次要非常小心地将光标放置在正确的位置,并且不要覆盖已经存在的任何文本。 -
点击确定。
-
让我们从
HelloSFML
项目中创建一个模板,这样我们就永远不必再做这个略显乏味的任务了。创建可重用的项目模板非常简单。在 Visual Studio 中选择文件 | 导出模板…。然后,在导出模板向导窗口中,确保选择了项目模板选项,然后选择HelloSFML项目作为要创建模板的项目选项。 -
点击下一步,然后点击完成。
哦,就是这样!下次我们创建项目时,我会告诉您如何从这个模板中创建。现在让我们构建 Timber!!!
规划 Timber!!!
每当制作游戏时,最好都要先用铅笔和纸开始。如果您不确定游戏在屏幕上的工作方式,又怎么可能在代码中使其正常工作呢?
提示
此时,如果你还没有这样做,我建议你去观看一段 Timberman 的游戏视频,这样你就可以看到我们的目标是什么。如果你的预算允许,那就买一份来玩玩。在 Steam 上通常会以不到一美元的价格出售。store.steampowered.com/app/398710/
.
游戏的特性和物体,定义了游戏玩法,被称为机制。游戏的基本机制是:
-
时间总是在流逝。
-
通过砍树来获得更多时间。
-
砍树会导致树枝掉落。
-
玩家必须避开掉落的树枝。
-
重复直到时间用完或玩家被压扁。
在这个阶段期望你计划 C++代码显然有点傻。当然,这是 C++初学者指南的第一章。然而,我们可以看一下我们将使用的所有资源以及我们需要让我们的 C++做我们想要的事情的概述。
看一下游戏的注释截图:
你可以看到我们有以下特性:
-
**玩家当前得分:**每次玩家砍一根木头,他就会得到一个点。他可以用左箭头或右箭头砍木头。
-
**玩家角色:**每次玩家砍的时候,他会移动/停留在树的同一侧。因此,玩家必须小心选择砍树的哪一侧。当玩家砍的时候,一个简单的斧头图形会出现在玩家角色的手中。
-
**缩小的时间条:**每次玩家砍的时候,一小段时间将被添加到不断缩小的时间条上。
-
**致命的树枝:**玩家砍得越快,他得到的时间就越多,但树枝也会更快地从树上掉下来,因此他被压扁的可能性就越大。树枝在树顶随机生成,并且每次砍树都会向下移动。
-
当玩家被压扁时,他会经常被压扁,一个墓碑图形会出现。
-
**被砍的木头:**当玩家砍的时候,一个被砍的木头图形会从玩家身边飞走。
-
有三朵漂浮的云,它们会以随机的高度和速度飘动,还有一只蜜蜂,除了四处飞来飞去什么也不做。
-
所有这些都发生在一个漂亮的背景上。
因此,简而言之,玩家必须疯狂地砍树来获得积分,并避免时间用尽。作为一个略微扭曲但有趣的结果,他砍得越快,他被压扁的可能性就越大。
现在我们知道游戏的外观,玩法以及游戏机制背后的动机。我们可以继续开始构建它。
从模板创建项目
现在创建一个新项目非常容易。只需在 Visual Studio 中按照这些简单的步骤操作:
-
从主菜单中选择文件 | 新项目。
-
确保在左侧菜单中选择Visual C++,然后从所呈现的选项列表中选择HelloSFML。下一个截图应该能清楚地说明这一点。
-
在名称:字段中键入
Timber
,并确保选中为解决方案创建目录选项。现在点击确定。 -
现在我们需要将 SFML 的
.dll
文件复制到主项目目录中。我的主项目目录是D:\Visual Studio Stuff\Projects\Timber\Timber
。它是在上一步中由 Visual Studio 创建的。如果你把你的Projects
文件夹放在其他地方,那么就在那里执行这一步。我们需要复制到项目文件夹中的文件位于你的SFML\bin
文件夹中。打开两个位置的窗口,并按照左侧下一个截图中显示的要求文件进行突出显示。 -
现在将突出显示的文件复制并粘贴到上一张截图右侧的项目文件夹中。
项目现在已经设置好,准备就绪。您将能够在下一个截图中看到屏幕。我已经对截图进行了注释,这样您就可以开始熟悉 Visual Studio 了。我们很快会重新访问所有这些区域以及其他区域。
您的布局可能与截图略有不同,因为 Visual Studio 的窗口,像大多数应用程序一样,是可定制的。花些时间找到右侧的Solution Explorer窗口,并调整它使其内容清晰明了,就像前面的截图一样。
我们很快会回到这里开始编码。
项目资产
资产是制作游戏所需的任何东西。在我们的情况下,资产包括:
-
屏幕上的书写字体
-
不同动作的音效,如砍伐、死亡和时间耗尽
-
角色、背景、树枝和其他游戏对象的图形
游戏所需的所有图形和声音都包含在下载包中。它们可以在相应的Chapter 1/graphics
和Chapter 1/sound
文件夹中找到。
所需的字体尚未提供。这是因为我想避免任何可能的许可歧义。不过这不会造成问题,因为我将向您展示确切的位置和方式来选择和下载字体。
尽管我将提供资产本身或获取它们的信息,但您可能希望自己创建和获取它们。
外包资产
有许多网站可以让您与艺术家、声音工程师甚至程序员签约。其中最大的之一是www.upwork.com。您可以免费加入该网站并发布您的工作。您需要清晰地解释您的要求,以及说明您愿意支付多少。然后您可能会得到许多承包商竞标做这项工作。请注意,有很多不合格的承包商,他们的工作可能令人失望,但如果您选择得当,您可能会找到一个称职、热情和物有所值的人或公司来完成工作。
制作自己的音效
可以从网站(如www.freesound.org)免费下载音效,但通常许可证不允许您在出售游戏时使用它们。另一个选择是使用名为 BFXR 的开源软件,该软件可以帮助您生成许多不同的音效,这些音效是您自己保留并随意使用的。
将资产添加到项目
一旦您决定要使用哪些资产,就该是将它们添加到项目的时候了。下面的说明将假定您正在使用书籍下载包中提供的所有资产。如果您使用自己的资产,只需用您自己的相应音效或图形文件替换,文件名完全相同即可。
-
浏览到 Visual
D:\Visual Studio Stuff\Projects\Timber\Timber
。 -
在此文件夹中创建三个新文件夹,并将它们命名为
graphics
、sound
和fonts
。 -
从下载包中,将
Chapter 1/graphics
的整个内容复制到D:\Visual Studio Stuff\Projects\Timber\Timber\graphics
文件夹中。 -
从下载包中,将
Chapter 1/sound
的整个内容复制到D:\Visual Studio Stuff\Projects\Timber\Timber\sound
文件夹中。 -
现在访问:
www.1001freefonts.com/komika_poster.font
在您的网络浏览器中下载Komika Poster字体。 -
解压缩下载的内容,并将
KOMIKAP_.ttf
文件添加到D:\Visual Studio Stuff\Projects\Timber\Timber\fonts
文件夹中。
让我们来看看这些资产,特别是图形,这样我们在使用它们在我们的 C++代码中时可以更好地可视化发生了什么。
探索资产
图形资产构成了我们的《伐木者!!!》游戏屏幕的各个部分。看一看这些图形资产,就能清楚地知道它们在我们的游戏中将被使用在哪里。
声音文件都是.wav
格式。这些文件包含了我们在游戏中特定事件播放的音效。它们都是用 BFXR 生成的。它们包括:
-
chop.wav
:一种像斧头(复古斧头)砍树的声音 -
death.wav
:一种有点像复古“失败”声音的声音。 -
out_of_time.wav
:当玩家因时间耗尽而失败时播放,而不是被压扁
理解屏幕和内部坐标
在我们进行实际的 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 项目(如果尚未打开)。
我们将要做的第一件事是重命名我们的主代码文件。它目前被称为HelloSFML.cpp
,我们将把它重命名为更合适的Timber.cpp
。.cpp
代表 C++。
-
在右侧找到解决方案资源管理器窗口。
-
在源文件文件夹下找到
HelloSFML.cpp
文件。 -
右键单击
HelloSFML.cpp
,选择重命名。 -
编辑文件名为
Timber.cpp
,然后按Enter。
在代码窗口中进行一些微小的编辑,以便你的代码与下面显示的完全相同。你可以像使用任何文本编辑器或文字处理软件一样进行编辑;如果你愿意,甚至可以复制粘贴。在进行了轻微的编辑之后,我们可以讨论它们:
// Include important C++ libraries here
#include "stdafx.h"
int main()
{
return 0;
}
这个简单的 C++程序是一个很好的起点。让我们逐行来看一下
用注释使代码更清晰
正如你所看到的,唯一需要更改的代码是顶部的一点点。第一行代码是这样的:
// Include important C++ libraries here
任何以//
开头的代码行都是注释,编译器会忽略它们。因此,这行代码什么也不做。它用于在以后回到代码时留下我们可能会发现有用的任何信息。注释在行尾结束,因此下一行的任何内容都不是注释的一部分。还有另一种类型的注释叫做多行或c 风格注释,它可以用来留下占据多于一行的注释。我们将在本章后面看到一些这样的注释。在本书中,我将留下数百条注释,以帮助添加上下文并进一步解释代码。
#include Windows 基本组件
现在你知道注释是用来干什么的,你可能可以猜到下一行代码是做什么的。这里再次给出:
#include "stdafx.h"
#include
指令告诉 Visual Studio 在编译之前包含或添加另一个文件的内容。这样做的效果是,当我们运行程序时,一些我们没有自己编写的其他代码将成为我们程序的一部分。将其他文件中的代码添加到我们的代码中的过程称为预处理,或许不足为奇的是,这是由一个叫做预处理器的东西执行的。文件扩展名.h
代表头文件。
你可能想知道这段代码会做什么?stdafx.h
文件实际上包含了更多的#include
指令。它将我们程序所需的所有必要代码添加到我们的程序中,以便在 Windows 上运行我们的程序。我们永远不需要看到这个文件,绝对不需要关心它里面有什么。我们只需要在我们制作的每个游戏的顶部添加一行代码。
对我们来说更重要和相关的是,值得讨论#include
指令的原因是,我们将在代码文件的顶部添加许多#include
指令。这是为了包含我们将使用和费力理解的代码。
我们将包含的主要文件是 SFML 头文件,它为我们提供了所有酷炫的游戏编码功能。我们还将使用#include
来访问C++标准库头文件。这些头文件为我们提供了访问 C++语言核心功能的权限。
这是两行解决了,让我们继续。
主函数
我们在代码中看到的下一行是这样的:
int main()
代码int
被称为类型。C++有许多类型,它们代表不同类型的数据。int
是整数或整数。记住这一点,我们一会儿会回来讨论它。
main()
代码部分是随后的代码部分的名称。这段代码在开放的花括号{
和下一个闭合的花括号}
之间标出。
因此,这些花括号{...}
之间的所有内容都是main
的一部分。我们把这样的一段代码称为函数。
每个 C++程序都有一个main
函数,它是整个程序执行(运行)的起点。随着我们在书中的进展,最终我们的游戏将有许多代码文件。然而,只会有一个main
函数,无论我们写什么代码,我们的游戏总是从main
函数的开放花括号内的第一行代码开始执行。
现在,不要担心跟在函数名()
后面的奇怪括号。我们将在第四章中进一步讨论它们:循环、数组、开关、枚举和函数-实现游戏机制,在那里我们将以全新和更有趣的方式看到函数。
让我们仔细看看Main
函数中的一行代码。
演示和语法
再次看看我们的Main
函数的全部内容:
int main()
{
return 0;
}
我们可以看到,在Main
中只有一行代码,return 0;
。在我们继续了解这行代码的作用之前,让我们看看它是如何呈现的。这很有用,因为它可以帮助我们准备编写易于阅读和区分的代码,与我们代码的其他部分。
首先注意到return 0;
向右缩进了一个制表符。这清楚地标志着它是main
函数内部的一部分。随着我们的代码长度增加,我们将看到缩进我们的代码和留下空白将是保持可读性的关键。
接下来,注意一下行末的标点符号。分号;
告诉编译器这是指令的结束,其后的任何内容都是新的指令。我们称以分号终止的指令为语句
。
请注意,编译器不在乎你在分号和下一条语句之间留下一个新行甚至一个空格。然而,不为每个语句开启新行将导致代码难以阅读,而完全忽略分号将导致语法错误,使得游戏无法编译或运行。
一起的一段代码,通常由其与部分的缩进表示,称为块。
现在你已经对main
函数的概念感到舒适,缩进你的代码以保持整洁,并在每个语句的末尾加上一个分号,我们可以继续找出return 0;
语句实际上是做什么的。
从函数返回值
实际上,在我们的游戏中,return 0;
几乎没有做任何事情。然而,这个概念是重要的。当我们使用return
关键字时,无论是单独使用还是后面跟着一个值,它都是一个指示程序执行跳转/返回到最初启动函数的代码的指令。
通常启动函数的代码将是我们代码中其他地方的另一个函数。然而,在这种情况下,是操作系统启动了main
函数。因此,当执行return 0;
时,main
函数退出,整个程序结束。
由于在return
关键字后面有一个 0,这个值也被发送到操作系统。我们可以将零的值更改为其他值,那个值将被发送回去。
我们说启动函数的代码调用函数,并且函数返回值。
你现在不需要完全掌握所有这些函数信息。这里只是介绍它是有用的。在我们继续之前,还有一个关于函数的最后一件事。还记得int Main()
中的int
吗?那告诉编译器Main
返回的值的类型必须是int
(整数/整数)。我们可以返回任何符合int
的值。也许是 0、1、999、6358 等等。如果我们尝试返回一个不是 int 的值,比如 12.76,那么代码将无法编译,游戏也无法运行。
函数可以返回各种不同类型的值,包括我们自己发明的类型!然而,这种类型必须以我们刚刚看到的方式告知编译器。
这些关于函数的背景信息将使我们在进展中更加顺利。
运行游戏
你现在可以运行游戏。通过点击 Visual Studio 快速启动栏中的本地 Windows 调试器按钮,或者使用F5快捷键来运行。
你将只看到一个黑屏的闪烁。这个闪烁是 C++控制台,我们可以用它来调试我们的游戏。现在我们不需要这样做。正在发生的是我们的程序启动,从Main
的第一行开始执行,当然是return 0;
,然后立即退出返回到操作系统。
使用 SFML 打开一个窗口
现在让我们添加一些更多的代码。接下来的代码将打开一个窗口,Timber!!!最终将在其中运行。窗口将是 1920 像素宽,1080 像素高,并且将是全屏的(没有边框或标题)。
输入下面突出显示的新代码到现有代码中,然后我们将对其进行检查。在输入(或复制和粘贴)时,尝试弄清楚发生了什么:
// Include important C++ libraries here
#include "stdafx.h"
#include <SFML/Graphics.hpp>
// Make code easier to type with "using namespace"
using namespace sf;
int main()
{
// Create a video mode object
VideoMode vm(1920, 1080);
// Create and open a window for the game RenderWindow
window(vm, "Timber!!!", Style::Fullscreen);
return 0;
}
包括 SFML 功能
在我们的新代码中,我们注意到的第一件事是另一个略有不同的#include
指令。#include <SFML/Graphics.hpp>
告诉预处理器包含文件夹中名为SFML
的文件夹中包含的Graphics.hpp
文件的内容。
所以这行代码的作用是添加来自上述文件的代码,这使我们可以访问 SFML 的一些功能。当我们开始编写自己的独立代码文件并使用#include
来使用它们时,它的实现方式将变得更加清晰。
注意
如果你想知道预处理器指令中包含文件名的<filename.hpp>
和"filename.h"
之间的区别,<...>
是用于我们文件夹结构中包含的文件,比如 SFML 文件或我们自己编写的任何文件。"..."
是用于包含在 Visual Studio 中的文件。此外,.hpp
文件扩展名只是.h
文件的更加面向 C++的版本,而.h
文件更像是 C 风格的扩展名。这两种风格和文件扩展名最终都会做同样的事情,并且在我们的游戏中都能正常工作。
目前重要的是,我们有一大堆新的功能由 SFML 提供,可供使用。下一行是using namespace sf;
。我们将在几段时间内回到这行代码的作用。
面向对象编程,类,对象
随着我们继续阅读本书,我们将更全面地讨论面向对象编程、类和对象。接下来是最简短的介绍,以便我们能够理解发生了什么。
我们已经知道 OOP 代表面向对象编程。OOP 是一种编程范式,一种编码方式。OOP 通常被全球范围内的编程界所接受,在几乎每种语言中,作为编写代码的最佳、如果不是唯一的专业方式。
面向对象编程引入了许多编码概念,但它们所有的基础都是类和对象。当我们编写代码时,我们希望尽可能地编写可重用的代码。我们这样做的方式是将我们的代码结构化为一个类。我们将在第六章中学习如何做到这一点:面向对象编程,类和 SFML 视图。
目前我们只需要知道关于类的一切,一旦我们编写了我们的类,我们不仅仅执行该代码作为游戏的一部分,而是创建可用的对象从类中。
例如,如果我们想要一百个僵尸非玩家角色(NPCs),我们可以仔细设计和编写一个名为Zombie
的类,然后从这个单个类中创建任意数量的僵尸对象。每个僵尸对象都具有相同的功能和内部数据类型,但每个僵尸对象都是一个独立的实体。
进一步以假设的僵尸示例为例,但不显示任何Zombie
类的代码,我们可以像这样创建一个基于Zombie
类的新对象:
Zombie z1;
现在,对象z1
是一个完全编码和功能的Zombie
对象。然后我们可以这样做:
Zombie z2;
Zombie z3;
Zombie z4;
Zombie z5;
现在我们有五个独立的僵尸,但它们都是基于一个精心编写的类。在我们回到刚刚编写的代码之前,让我们再进一步。我们的僵尸可以包含行为(由函数定义)以及可能代表僵尸健康、速度、位置或行进方向等事物的数据。例如,我们可以编写我们的Zombie
类,使我们能够像这样使用我们的僵尸对象:
z1.attack(player);
z2.growl();
z3.headExplode();
注意
再次注意,所有这些僵尸代码目前都是假设的。不要将这些代码输入 Visual Studio;它只会产生一堆错误。
我们将设计我们的类,以便我们可以以最合适的方式使用数据和行为来满足我们游戏的目标。例如,我们可以设计我们的类,以便我们可以在创建每个僵尸对象时为数据分配值。
也许我们需要在创建每个僵尸时分配一个唯一的名称和以米每秒为单位的速度。Zombie
类的仔细编码可以使我们编写这样的代码:
// Dave was a 100 meter Olympic champion before infection
// He moves at 10 meters per second
Zombie z1("Dave", 10);
// Gill had both of her legs eaten before she was infected
// She drags along at .01 meters per second
Zombie z2("Gill", .01);
重点是类几乎是无限灵活的,一旦我们编写了类,我们就可以通过创建对象来使用它们。正是通过类和我们从中创建的对象,我们将利用 SFML 的强大功能。是的,我们还将编写我们自己的类,包括一个Zombie
类。
让我们回到我们刚刚编写的真正代码。
使用 sf 命名空间
在我们继续更仔细地查看VideoMode
和RenderWindow
之前,您可能已经猜到,这些都是 SFML 提供的类,我们将学习using namespace sf;
这行代码的作用。
当我们创建一个类时,我们是在一个命名空间中创建的。我们这样做是为了区分我们编写的类和其他人编写的类。考虑一下VideoMode
类。在 Windows 等环境中,完全有可能有人已经编写了一个名为VideoMode
的类。通过使用命名空间,我们和 SFML 程序员可以确保类的名称永远不会冲突。
使用VideoMode
类的完整方式如下:
sf::VideoMode...
代码using namespace sf;
使我们可以在代码中的任何地方省略前缀sf::
。如果没有它,在这个简单的游戏中将会有超过 100 个sf::
的实例。它还使我们的代码更易读,同时也更短。
SFML VideoMode 和 RenderWindow
在Main
函数中,我们现在有两个新的注释和两行新的实际代码。第一行实际代码是这样的:
VideoMode vm(1920, 1080);
这段代码创建了一个名为vm
的对象,从名为VideoMode
的类中创建,并设置了内部值1920
和1080
。这些值代表玩家屏幕的分辨率。
下一行新的代码是这样的:
RenderWindow window(vm, "Timber!!!", Style::Fullscreen);
在前一行代码中,我们正在从 SFML 提供的名为RenderWindow
的类中创建一个名为window
的新对象。此外,我们正在设置窗口对象内部的一些值。
首先,vm
对象用于初始化window
的一部分。起初这可能看起来令人困惑。然而,请记住,类可以像其创建者想要的那样多样化和灵活。是的,有些类可以包含其他类。
提示
此时不必完全理解这是如何工作的,只要您能理解这个概念就可以了。我们编写一个类,然后从该类中创建可用的对象。有点像建筑师可能会绘制蓝图。您当然不能把所有家具、孩子和狗都搬进蓝图中;但您可以根据蓝图建造一座房子(或者多座房子)。在这个类比中,类就像蓝图,对象就像房子。
接下来,我们使用值 Timber!!!来给窗口命名。我们使用预定义的值Style::FullScreen
来使我们的window
对象全屏显示。
提示
Style::FullScreen
是 SFML 中定义的一个值。这样做是为了我们不需要记住内部代码用来表示全屏的整数。这种类型的值的编码术语是常量
。常量及其近亲 C++中的变量将在下一章中介绍。
让我们看看我们的窗口对象在运行中的样子。
运行游戏
在这一点上,您可以再次运行游戏。您会看到一个更大的黑屏一闪而过。这就是我们刚刚编写的 1920 x 1080 全屏窗口。不幸的是,我们的程序仍然是从Main
的第一行开始执行,创建了一个很酷的新游戏窗口,然后到达return 0;
,立即退出到操作系统。
主游戏循环
我们需要一种方法来保持程序运行,直到玩家想要退出。同时,随着我们在 Timber!!!中的进展,我们应该清楚地标出代码的不同部分将放在哪里。此外,如果我们要阻止游戏退出,我们最好提供一种让玩家在准备好退出时退出的方法。否则游戏将永远进行下去!
添加高亮代码,放入现有代码中,然后我们将一起讨论它们:
int main()
{
// Create a video mode object
VideoMode vm(1920, 1080);
// Create and open a window for the game
RenderWindow window(vm, "Timber!!!", Style::Fullscreen);
while (window.isOpen())
{
/*
****************************************
Handle the players input
****************************************
*/
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
/*
****************************************
Update the scene
****************************************
*/
/*
****************************************
Draw the scene
****************************************
*/
// Clear everything from the last frame
window.clear();
// Draw our game scene here
// Show everything we just drew
window.display();
}
return 0;
}
While 循环
在新代码中,我们看到的第一件事是:
while (window.isOpen())
{
在新代码中,我们看到的最后一件事是一个闭合的}
。我们创建了一个while
循环。在while
循环的开头{
和结尾}
之间的所有内容将会一遍又一遍地执行,可能会永远执行下去。
仔细看一下下一个代码中突出显示的while
循环的括号(...)
之间的部分:
while (window.isOpen())
这段代码的完整解释将等到我们在第四章讨论循环和条件时再说:循环、数组、开关、枚举和函数-实现游戏机制。现在重要的是,当window
对象被设置为关闭时,代码的执行将跳出while
循环并进入下一个语句。窗口如何关闭将很快涵盖。
下一个声明当然是return 0;
,这结束了我们的游戏。
现在我们知道我们的while
循环会快速循环执行其中的代码,直到我们的窗口对象被设置为关闭。
C 风格的代码注释
在 while 循环内部,我们看到了乍一看可能有点像 ASCII 艺术的东西:
/*
****************************************
Handle the player's input
****************************************
*/
注意
ASCII 艺术是一种利用计算机文本创建图像的小众但有趣的方式。您可以在这里阅读更多信息:en.wikipedia.org/wiki/ASCII_art
.
先前的代码只是另一种类型的注释。这种注释被称为 C 风格注释。注释以/*
开头,以*/
结尾。中间的任何内容只是用于信息,不会被编译。我使用了这种略微复杂的文本,以确保清楚地表明我们将在代码文件的这部分做什么。当然,您现在可以推断出接下来的任何代码都将与处理玩家的输入有关。
跳过几行代码,您会看到我们有另一个 C 风格的注释,宣布在代码的这部分,我们将更新场景。
跳到下一个 C 风格的注释,很明显我们将在那里绘制所有的图形。
输入、更新、绘制、重复
尽管这个第一个项目使用了最简单的游戏循环版本,但每个游戏都需要在代码中经历这些阶段:
-
获取玩家的输入(如果有)。
-
根据人工智能、物理或玩家的输入更新场景。
-
绘制当前场景。
-
以足够快的速度重复以上步骤,以创建一个平滑和动画的游戏世界。
现在让我们看看实际在游戏循环中执行的代码。
检测按键
首先,在标记为处理玩家输入
的部分中,我们有以下代码:
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
这段代码检查当前是否按下了Escape键。如果是,突出显示的代码使用window
对象关闭自身。现在,下一次while
循环开始时,它将看到window
对象已关闭,并跳到while
循环的结束大括号}
后面的代码,游戏将退出。我们将在第二章更全面地讨论if
语句:变量、运算符和决策-动画精灵。
清除和绘制场景
目前在更新场景
部分没有代码,所以让我们继续到绘制场景
部分。
我们要做的第一件事是使用以下代码擦除先前的动画帧:
window.clear();
现在我们要做的是绘制游戏中的每一个对象。然而,目前我们没有任何游戏对象。
下一行代码是:
window.display();
当我们绘制所有游戏对象时,我们将它们绘制到一个隐藏的表面上,准备好显示。代码window.display()
从先前显示的表面翻转到新更新的(先前隐藏的)表面。这样,玩家永远不会看到绘图过程,因为表面上添加了所有精灵。它还保证了在翻转之前场景将会完整。这可以防止图形故障,称为撕裂。这个过程称为双缓冲。
还要注意,所有这些绘制和清除功能都是使用我们的window
对象执行的,该对象是从 SFML 的RenderWindow
类创建的。
运行游戏
运行游戏,您将得到一个空白的全屏窗口,直到您按下Esc键。
绘制游戏背景
最后,我们将在游戏中看到一些真正的图形。我们需要做的是创建一个精灵。我们将创建的第一个精灵将是游戏背景。然后我们可以在清除窗口和显示/翻转窗口之间绘制它。
使用纹理准备精灵
SFML 的RenderWindow
类允许我们创建window
对象来处理游戏窗口所需的所有功能。
现在我们将探索另外两个 SFML 类,它们将负责在屏幕上绘制精灵。其中一个类,也许不足为奇的是,被称为Sprite
。另一个类被称为Texture
。纹理是存储在图形处理单元(GPU)上的图形。
从Sprite
类创建的对象需要从Texture
类创建的对象才能将自己显示为图像。添加以下突出显示的代码。尝试弄清楚发生了什么。然后我们将逐行解释:
int main()
{
// Create a video mode object
VideoMode vm(1920, 1080);
// Create and open a window for the game
RenderWindow window(vm, "Timber!!!", Style::Fullscreen);
// Create a texture to hold a graphic on the GPU
Texture textureBackground;
// Load a graphic into the texture
textureBackground.loadFromFile("graphics/background.png);
// Create a sprite
Sprite spriteBackground;
// Attach the texture to the sprite
spriteBackground.setTexture(textureBackground);
// Set the spriteBackground to cover the screen
spriteBackground.setPosition(0,0);
while (window.isOpen())
{
首先,我们从 SFML 的Texture
类创建一个名为textureBackground
的对象。
Texture textureBackground;
完成后,我们可以使用textureBackground
对象从我们的graphics
文件夹加载图形到textureBackground
中,就像这样:
textureBackground.loadFromFile("graphics/background.png");
提示
我们只需要指定graphics/background
作为路径,因为路径是相对于我们创建文件夹并添加图像的 Visual Studio 工作目录的。
接下来,我们使用以下代码从 SFML 的Sprite
类创建一个名为spriteBackground
的对象:
Sprite spriteBackground;
然后,我们可以将纹理对象textureBackground
与精灵对象spriteBackground
关联起来,就像这样:
spriteBackground.setTexture(textureBackground);
最后,我们可以将spriteBackground
对象定位在window
对象的坐标0,0
处:
spriteBackground.setPosition(0,0);
background.png
图形在graphics
文件夹中的尺寸为 1920 像素宽,1080 像素高,它将完全填满整个屏幕。只是请注意,这行代码并不实际显示精灵,它只是设置好位置,以便在显示时使用。
现在,backgroundSprite
对象可以用来显示背景图形。当然,您几乎肯定想知道为什么我们不得不以这种复杂的方式做事。原因是因为显卡和 OpenGL 的工作方式。
纹理占用图形内存,而这种内存是有限的资源。此外,将图形加载到 GPU 内存中的过程非常缓慢。并不是缓慢到可以看到它发生,或者在发生时会明显减慢您的 PC,但足够缓慢,以至于您无法在游戏循环的每一帧中都这样做。因此,将实际纹理textureBackground
与在游戏循环期间我们将操纵的任何代码分离开来是有用的。
当我们开始移动我们的图形时,您将会看到我们将使用精灵。任何从Texture
类创建的对象都将愉快地停留在 GPU 上,只等待一个关联的Sprite
对象告诉它们在哪里显示自己。在以后的项目中,我们还将重复使用相同的Texture
对象与多个不同的Sprite
对象,这样可以有效地利用 GPU 内存。
总之:
-
纹理加载到 GPU 上非常缓慢
-
一旦纹理存储在 GPU 上,访问速度非常快
-
我们将精灵对象与纹理关联起来
-
我们通常在“更新场景”部分操纵精灵对象的位置和方向。
-
我们绘制
Sprite
对象,然后显示与其关联的纹理(通常在“绘制场景”部分)。
所以现在我们需要做的就是使用我们的window
对象提供的双缓冲系统来绘制我们的新Sprite
对象(spriteBackground
),然后我们实际上应该能够看到我们的游戏在运行。
双缓冲背景精灵
最后,我们需要在游戏循环中的适当位置绘制该精灵及其相关纹理。
提示
请注意,当我展示的代码都来自同一个块时,我不添加缩进,因为这会减少书中文本的换行次数。缩进是暗示的。请查看下载包中的代码文件,以查看缩进的完整用法。
添加突出显示的代码:
/*
****************************************
Draw the scene
****************************************
*/
// Clear everything from the last run frame
window.clear();
// Draw our game scene here
window.draw(spriteBackground);
// Show everything we just drew
window.display();
新的代码行只是使用window
对象来绘制spriteBackground
对象,在清除显示并显示新绘制的场景之间。
运行游戏
现在运行程序,您将看到我们正在进行真正的游戏的第一个迹象。
它目前还不能在 Steam 上获得绿光,但至少我们已经在路上了!
让我们看看本章可能出现的一些问题,以及随着书的进行我们将继续进行的工作。
处理错误
每个项目都会出现问题和错误,这是肯定的!问题越棘手,解决它时就会越令人满意。经过数小时的挣扎后,一个新的游戏功能终于实现,会让人真正兴奋。如果没有挣扎,它可能会变得不那么值得。
在本书的某个时候,可能会遇到一些困难。保持冷静,相信自己能够克服它,然后开始工作。
请记住,无论您遇到什么问题,您都极不可能是世界上第一个遇到同样问题的人。想出一个简洁的句子来描述您的问题或错误,然后在 Google 中输入。您会惊讶地发现,有人很快、准确地解决了您的问题,而且经常会有人已经为您解决了问题。
话虽如此,在这里有一些提示(双关语;请参阅第八章:指针、标准模板库和纹理管理),以便在您努力使本章工作时帮助您入门。
配置错误
本章中问题最有可能的原因是配置错误。您可能已经在设置 Visual Studio、SFML、项目模板和项目本身的过程中注意到,有很多文件名、文件夹和设置需要完全正确。只要有一个错误的设置,就可能导致多种错误,其中文本并没有清楚地说明出了什么问题。
如果您无法使从模板创建可重用模板部分中的黑屏空项目运行起来,可能更容易重新开始该部分。确保所有的文件名和文件夹适合于您特定的设置,然后让代码的最简单部分运行起来(屏幕闪烁黑色然后关闭的部分)。如果您能够达到这个阶段,那么配置可能不是问题所在。
编译错误
编译错误可能是我们未来经历的最常见的错误。检查您的代码是否与我的相同,特别是行尾的分号和类和对象名称的大小写的微妙变化。如果一切都失败了,打开下载包中的代码文件并复制粘贴。虽然书中可能存在代码拼写错误,但代码文件是从实际工作的项目中制作的 - 它们绝对有效!
链接错误
链接错误很可能是由于缺少 SFML 的.dll
文件造成的。您是否将它们全部复制到了从模板创建项目的项目文件夹中?
错误
当您的代码工作时发生错误时,这就是错误。调试实际上可以很有趣。您消灭的错误越多,您的游戏就会越好,您一天的工作就会越令人满意。解决错误的诀窍是尽早找到它们!为此,我建议每次实现新功能时都运行和玩游戏。您越早发现错误,原因就越可能新鲜在您的脑海中。在本书中,我们将在每个可能的阶段运行代码以查看结果。
常见问题解答
以下是一些可能会让你困惑的问题:
Q)我对目前呈现的内容感到困难。我适合编程吗?
A)设置开发环境并理解 OOP 作为一个概念可能是你在这本书中做的最艰难的事情。只要你的游戏正常运行(绘制背景),你就可以继续进行下一章。
Q)所有关于面向对象编程(OOP)、类和对象的讨论都太多了,有点破坏了整个学习体验。
A)别担心。我们会不断地回到面向对象编程、类和对象。在第六章:面向对象编程、类和 SFML 视图中,我们将真正开始掌握整个面向对象编程的东西。你现在需要理解的是,SFML 已经编写了大量有用的类,我们可以通过从这些类创建可用对象来使用这些代码。
Q)我真的不懂这个函数的东西。
A)没关系,我们会再次回到这个问题,并且会更彻底地学习函数。你只需要知道,当一个函数被调用时,它的代码被执行,当它完成时(达到return
语句),程序会跳回调用它的代码。
总结
那是一个相当具有挑战性的章节,也许我让它变得如此苛刻了一点。配置 IDE 以使用 C++库可能有点棘手和耗时。同时,众所周知,对于编程新手来说,类和对象的概念可能有点棘手。
现在我们已经到了这个阶段,我们可以完全专注于 C++、SFML 和游戏。随着章节的进展,我们将学习更多的 C++,以及如何实现越来越有趣的游戏功能。在这个过程中,我们将进一步研究诸如函数、类和对象之类的东西,以帮助更好地揭开它们的神秘面纱。接下来,我们将学习所有绘制更多精灵并对它们进行动画处理所需的 C++知识。
第二章:变量、运算符和决策 - 动画精灵
在本章中,我们将在屏幕上进行更多的绘图,为了实现这一点,我们需要学习一些 C++的基础知识。
这里有什么:
-
学习所有关于 C++变量的知识
-
了解如何操作变量中存储的值
-
添加一个静态树,准备好供玩家砍伐
-
绘制和动画一个蜜蜂和三朵云
C++变量
变量是我们的 C++游戏存储和操作值的方式。如果我们想知道玩家有多少生命值,那么我们就需要一个变量。也许你想知道当前波中还剩下多少僵尸?那也是一个变量。如果您需要记住获得特定高分的玩家的名字,你猜对了,我们也需要一个变量。游戏结束了还是还在进行?是的,那也是一个变量。
变量是内存中位置的命名标识符。因此,我们可以将一个变量命名为numberOfZombies
,该变量可以指向存储表示当前波中剩余僵尸数量的值的内存位置。
计算机系统寻址内存位置的方式是复杂的。编程语言使用变量以人性化的方式管理我们在内存中的数据。
我们对变量的简要讨论意味着必须有不同类型的变量。
变量类型
C++有各种各样的变量类型(请参阅有关变量的下一个提示)。很容易花一个整章的时间来讨论它们。接下来是本书中最常用的类型的表格。然后我们将看看如何实际使用每种变量类型。
类型 | 值的示例 | 解释 |
---|---|---|
Int | -42 ,0 ,1 ,9826 ,等等。 | 整数整数。 |
Float | -1.26f ,5.8999996f ,10128.3f | 浮点值,精度高达 7 位数字。 |
Double | 925.83920655234 ,1859876.94872535 | 浮点值,精度高达 15 位数字。 |
Char | a ,b ,c ,1 ,2 ,3 (包括? ,~ ,# 等共 128 个符号) | ASCII 表中的任何符号(请参阅有关变量的下一个提示)。 |
Bool | 真或假 | Bool 代表布尔值,只能是true 或false 。 |
String | 大家好!我是一个字符串。 | 从单个字母或数字到整本书的任何文本值。 |
编译器必须告诉变量是什么类型,以便为其分配正确的内存量。对于您使用的每个变量,使用最佳和最合适的类型是一个良好的实践。然而,在实践中,您通常可以提升一个变量。也许您只需要一个具有五个有效数字的浮点数?如果您将其存储为double
,编译器不会抱怨。然而,如果您尝试将float
或double
存储在int
中,它将更改/转换值以适应int
。随着我们在书中的进展,我将澄清在每种情况下使用的最佳变量类型是什么,我们甚至会看到一些有意转换/转换变量类型的情况。
在上面的表中,还有一些额外的细节值得注意,包括所有float
值旁边的f
后缀。这个f
告诉编译器该值是float
类型而不是double
。没有f
前缀的浮点值被假定为double
。有关此内容的更多信息,请参阅有关变量的下一个提示。
如前所述,还有许多其他类型。如果您想了解更多关于类型的信息,请参阅有关变量的下一个提示。
常量
有时我们需要确保一个值永远不会被改变。为了实现这一点,我们可以使用const
关键字声明和初始化一个常量:
const float PI = 3.141f;
const int PLANETS_IN_SOLAR_SYSTEM = 8;
const int NUMBER_OF_ENEMIES = 2000;
习惯上,常量的声明都是大写的。前面常量的值永远不能被改变。我们将在第四章中看到一些常量的实际应用:循环、数组、开关、枚举和函数 - 实现游戏机制。
用户定义的类型
用户定义的类型比我们刚刚看到的类型要先进得多。当我们在 C++中谈论用户定义的类型时,通常是指类。我们在上一章中简要讨论了类及其相关对象。我们可以在一个单独的文件中编写代码,有时甚至是在两个单独的文件中。然后我们将能够声明、初始化和使用它们。我们将把如何定义/创建我们自己的类型留到第六章:面向对象编程、类和 SFML 视图。
声明和初始化变量
到目前为止,我们知道变量用于存储游戏中需要的数据/值。例如,一个变量可以表示玩家拥有的生命值或玩家的姓名。我们还知道这些变量可以表示各种不同类型的值,比如int
、float
、bool
等。当然,我们还没有看到如何实际使用变量。
创建和准备新变量有两个阶段。这两个阶段称为声明和初始化。
声明变量
我们可以在 C++中这样声明变量:
// What is the player's score?
int playerScore;
// What is the players first initial
char playerInitial;
// What is the value of pi
float valuePi;
// Is the player alive or dead?
bool isAlive;
初始化变量
现在我们已经用有意义的名称声明了变量,我们可以用适当的值初始化这些变量,就像这样:
playerScore = 0;
playerInitial = 'J';
valuePi = 3.141f;
isAlive = true;
一步声明和初始化
当适合我们时,我们可以将声明和初始化步骤合并为一步:
int playerScore = 0;
char playerInitial = 'J';
float valuePi = 3.141f;
bool isAlive = true;
提示
变量提示正如承诺的那样,这是关于变量的提示。如果你想看到完整的 C++类型列表,那么请查看这个网页:www.tutorialspoint.com/cplusplus/cpp_data_types.htm
。如果你想深入讨论浮点数、双精度和f
后缀,那么请阅读这篇文章:www.cplusplus.com/forum/beginner/24483/
。如果你想了解 ASCII 字符代码的方方面面,那么这里有更多信息:www.cplusplus.com/doc/ascii/
。请注意,这些链接是给好奇的读者的,我们已经讨论了足够的内容以便继续进行。
声明和初始化用户定义的类型
我们已经看到了如何声明和初始化一些 SFML 定义的类型的示例。由于我们可以创建/定义这些类型(类)的方式非常灵活,因此我们声明和初始化它们的方式也是多种多样的。以下是前一章中关于声明和初始化用户定义的类型的一些提醒。
创建一个类型为VideoMode
的对象,名为vm
,并用两个int
值1920
和1080
进行初始化:
// Create a video mode object
VideoMode vm(1920, 1080);
创建一个类型为Texture
的对象,名为textureBackground
,但不进行任何初始化:
// Create a texture to hold a graphic on the GPU
Texture textureBackground;
请注意,即使我们没有建议使用哪些特定值来初始化textureBackground
,某些变量可能已在内部设置。对象是否需要/具有在此时给出初始化值的选项完全取决于类的编码方式,几乎是无限灵活的。这进一步表明,当我们开始编写自己的类时,会有一些复杂性。幸运的是,这也意味着我们将有重大的权力来设计我们的类型/类,使它们正是我们需要的来制作我们的游戏!将这种巨大的灵活性添加到 SFML 设计的类中,我们的游戏的潜力几乎是无限的。
在本章中,我们还将看到 SFML 提供的一些用户创建的类型/类,以及本书中的更多内容。
操作变量
到目前为止,我们确切地知道了变量是什么,主要类型是什么,以及如何声明和初始化它们,但我们仍然不能做太多事情。我们需要操作我们的变量,加上它们,减去它们,乘以它们,除以它们,并测试它们。
首先,我们将处理如何操作它们,稍后我们将看看我们如何以及为什么测试它们。
C++算术和赋值运算符
为了操作变量,C++有一系列算术运算符和赋值运算符。幸运的是,大多数算术和赋值运算符使用起来相当直观,而那些不直观的则很容易解释。为了让我们开始,让我们先看一张算术运算符表,然后是一张我们将在本书中经常使用的赋值运算符表:
算术运算符 | 解释 |
---|---|
+ | 加法运算符可用于将两个变量或值的值相加。 |
- | 减法运算符可用于从另一个变量或值中减去一个变量或值的值。 |
* | 乘法运算符可以将变量和值的值相乘。 |
/ | 除法运算符可以除以变量和值的值。 |
% | 取模运算符将一个值或变量除以另一个值或变量,以找到操作的余数。 |
现在是赋值运算符的时候了:
赋值运算符 | 解释 |
---|---|
= | 我们已经见过这个了。这是赋值运算符。我们用它来初始化/设置变量的值。 |
+= | 将右侧的值加到左侧的变量上。 |
-= | 从左侧的变量中减去右侧的值。 |
*= | 将右侧的值乘以左侧的变量。 |
/= | 将右侧的值除以左侧的变量。 |
++ | 递增运算符;将变量加 1 |
-- | 递减运算符;从变量中减去 1 |
注意
从技术上讲,除了=, --
和++
之外,上述所有运算符都被称为复合赋值运算符,因为它们包含多个运算符。
现在我们已经看到了一系列算术和赋值运算符,我们实际上可以看到如何通过组合运算符、变量和值来操作我们的变量形成表达式。
通过表达式完成任务
表达式是变量、运算符和值的组合。使用表达式,我们可以得出一个结果。此外,正如我们很快将看到的那样,我们可以在测试中使用表达式。这些测试可以用来决定我们的代码接下来应该做什么。首先,让我们看一些可能在游戏代码中看到的简单表达式:
// Player gets a new high score
hiScore = score;
或者
// Set the score to 100
score = 100;
看一下加法运算符,与赋值运算符一起使用:
// Add to the score when an alien is shot
score = aliensShot + wavesCleared;
或者
// Add 100 to whatever the score currently is
score = score + 100;
请注意,在运算符的两侧使用相同的变量是完全可以接受的。
看一下减法运算符与赋值运算符的结合。下面的代码从减法运算符右侧的值中减去左侧的值。它通常与赋值运算符一起使用,例如:
// Uh oh lost a life
lives = lives - 1;
或者
// How many aliens left at end of game
aliensRemaining = aliensTotal - aliensDestroyed;
这是我们可能使用除法运算符的方式。下面的代码将左边的数字除以右边的数字。同样,它通常与赋值运算符一起使用,如下所示:
// Make the remaining hit points lower based on swordLevel
hitPoints = hitPoints / swordLevel;
或者
// Give player something back for recycling a block
recycledValueOfBlock = originalValue / .9f;
显然,在前面的例子中,变量recycledValueOfBlock
需要是float
类型,以准确存储这样的计算结果。
也许并不令人惊讶,我们可以像这样使用乘法运算符:
// answer is equal to 100 - of course
answer = 10 * 10;
或者
// biggerAnswer = 1000 - of course
biggerAnswer = 10 * 10 * 10;
注意
顺便说一下,你是否曾经想过 C++是怎么得到它的名字的?C++是 C 语言的扩展。它的发明者 Bjarne Stroustrup 最初称其为C with classes,但名称发生了变化。如果您感兴趣,请阅读 C++的故事:www.cplusplus.com/info/history/
。
现在,让我们看看增量运算符的运行情况。这是一个非常巧妙的方法,可以将1
添加到我们游戏变量的值中。
看一下这段代码:
// Add one to myVariable
myVariable = myVariable + 1;
它产生了与这段代码相同的结果:
// Much neater and quicker
myVariable ++;
递减运算符--
,你猜对了,是从某个数值中减去1
的一个非常巧妙的方法:
playerHealth = playerHealth -1;
这与这个是一样的:
playerHealth --;
让我们看看一些操作符的运行情况,然后我们可以继续构建 Timber!!!游戏:
someVariable = 10;
// Multiply the variable by 10 and put the answer back in variable
someVariable *= 10;
// someVariable now equals 100
// Divide someVariable by 5 put the answer back into the variable
someVariable /= 5;
// someVariable now equals 20
// Add 3 to someVariable and put the answer back into the variable
someVariable += 3;
// someVariable now equals 23
// Take 25 from someVariable and put the answer back into the variable
someVariable -= 25;
// someVariable now equals -2
现在是时候向我们的游戏添加一些更多的精灵了。
添加云,树和嗡嗡蜜蜂
首先我们将添加一棵树。这将非常容易。之所以容易是因为树不会移动。我们将使用与我们在上一章绘制背景时完全相同的过程。
准备树
添加下面突出显示的代码。注意未突出显示的代码,这是我们已经编写的代码。这应该帮助您确定新代码应该在设置背景位置之后立即输入,但在主游戏循环开始之前。在您添加新代码之后,我们将回顾新代码的实际情况:
int main()
{
// Create a video mode object
VideoMode vm(1920, 1080);
// Create and open a window for the game
RenderWindow window(vm, "Timber!!!", Style::Fullscreen);
// Create a texture to hold a graphic on the GPU
Texture textureBackground;
// Load a graphic into the texture
textureBackground.loadFromFile("graphics/background.png");
// Create a sprite
Sprite spriteBackground;
// Attach the texture to the sprite
spriteBackground.setTexture(textureBackground);
// Set the spriteBackground to cover the screen
spriteBackground.setPosition(0, 0);
// Make a tree sprite
Texture textureTree;
textureTree.loadFromFile("graphics/tree.png");
Sprite spriteTree;
spriteTree.setTexture(textureTree);
spriteTree.setPosition(810, 0);
while (window.isOpen())
{
我们刚刚添加的五行代码(不包括注释)做了以下事情:
-
首先,我们创建了一个名为
textureTree
的Texture
类型对象。 -
接下来,我们从
tree.png
图形文件中将图形加载到纹理中。 -
接下来,我们声明了一个名为
spriteTree
的Sprite
类型对象。 -
现在,我们将
textureTree
与spriteTree
关联起来。每当我们绘制spriteTree
时,它将显示textureTree
纹理,这是一个漂亮的树形图形。 -
最后,我们使用 x 轴上的坐标
810
和 y 轴上的坐标 0 设置了树的位置。
让我们继续处理蜜蜂对象,这几乎是以相同的方式处理的。
准备蜜蜂
下一个代码与树代码之间的差异很小但很重要。由于蜜蜂需要移动,我们还声明了两个与蜜蜂相关的变量。在所示的位置添加突出显示的代码,并看看我们如何使用变量beeActive
和beeSpeed
:
// Make a tree sprite
Texture textureTree;
textureTree.loadFromFile("graphics/tree.png");
Sprite spriteTree;
spriteTree.setTexture(textureTree);
spriteTree.setPosition(810, 0);
// Prepare the bee
Texture textureBee;
textureBee.loadFromFile("graphics/bee.png");
Sprite spriteBee;
spriteBee.setTexture(textureBee);
spriteBee.setPosition(0, 800);
// Is the bee currently moving?
bool beeActive = false;
// How fast can the bee fly
float beeSpeed = 0.0f;
while (window.isOpen())
{
我们创建蜜蜂的方式与我们创建背景和树的方式完全相同。我们使用Texture
和Sprite
,并将两者关联起来。请注意,在以前的蜜蜂代码中,有一些我们以前没有见过的新代码。有一个用于确定蜜蜂是否活动的bool
变量。请记住,bool
变量可以是true
或false
。我们暂时将beeActive
初始化为false
。
接下来,我们声明一个名为beeSpeed
的新float
变量。这将保存我们的蜜蜂在屏幕上飞行的速度,以像素为单位每秒。
很快我们将看到如何使用这两个新变量来移动蜜蜂。在我们这样做之前,让我们以几乎相同的方式设置一些云。
准备云
添加下面显示的突出显示的代码。研究新代码,尝试弄清楚它将做什么:
// Prepare the bee
Texture textureBee;
textureBee.loadFromFile("graphics/bee.png");
Sprite spriteBee;
spriteBee.setTexture(textureBee);
spriteBee.setPosition(0, 800);
// Is the bee currently moving?
bool beeActive = false;
// How fast can the bee fly
float beeSpeed = 0.0f;
// make 3 cloud sprites from 1 texture
Texture textureCloud;
// Load 1 new texture
textureCloud.loadFromFile("graphics/cloud.png");
// 3 New sprites with the same texture
Sprite spriteCloud1;
Sprite spriteCloud2;
Sprite spriteCloud3;
spriteCloud1.setTexture(textureCloud);
spriteCloud2.setTexture(textureCloud);
spriteCloud3.setTexture(textureCloud);
// Position the clouds off screen
spriteCloud1.setPosition(0, 0);
spriteCloud2.setPosition(0, 250);
spriteCloud3.setPosition(0, 500);
// Are the clouds currently on screen?
bool cloud1Active = false;
bool cloud2Active = false;
bool cloud3Active = false;
// How fast is each cloud?
float cloud1Speed = 0.0f;
float cloud2Speed = 0.0f;
float cloud3Speed = 0.0f;
while (window.isOpen())
{
我们刚刚添加的代码中唯一有点奇怪的是,我们只有一个Texture
类型的对象。多个Sprite
对象共享一个纹理是完全正常的。一旦Texture
存储在 GPU 内存中,它就可以与Sprite
对象快速关联。只有在loadFromFile
代码中加载图形的初始操作相对较慢。当然,如果我们想要三个不同形状的云,那么我们就需要三个纹理。
除了轻微的纹理问题,我们刚刚添加的代码与蜜蜂相比并没有什么新的。唯一的区别是有三个云精灵,三个用于确定每朵云是否活动的bool
变量和三个用于保存每朵云速度的float
变量。
绘制树、蜜蜂和云
最后,我们可以通过在绘图部分添加这个突出显示的代码将它们全部绘制到屏幕上:
/*
****************************************
Draw the scene
****************************************
*/
// Clear everything from the last run frame
window.clear();
// Draw our game scene here
window.draw(spriteBackground);
// Draw the clouds
window.draw(spriteCloud1);
window.draw(spriteCloud2);
window.draw(spriteCloud3);
// Draw the tree
window.draw(spriteTree);
// Draw the insect
window.draw(spriteBee);
// Show everything we just drew
window.display();
绘制三朵云、蜜蜂和树的方式与绘制背景的方式完全相同。然而,请注意我们绘制不同对象到屏幕的顺序。我们必须在背景之后绘制所有图形,否则它们将被覆盖,而且我们必须在树之前绘制云,否则它们在树前飘来飘去会看起来有点奇怪。蜜蜂无论在树前还是树后看起来都可以。我选择在树前画蜜蜂,这样它就可以试图分散我们的伐木工的注意力,有点像真正的蜜蜂可能会做的。
运行 Timber!!!并对树、三朵云和一只蜜蜂感到敬畏,它们什么都不做!它们看起来像是在为比赛排队,蜜蜂倒着飞。
利用我们对运算符的了解,我们可以尝试移动我们刚刚添加的图形,但有一些问题。首先,真实的云和蜜蜂以不均匀的方式移动。它们没有固定的速度或位置。尽管它们的位置和速度是由风速或蜜蜂可能的匆忙程度等因素决定的,但对于一般观察者来说,它们所采取的路径和速度似乎是随机的。
随机数
随机数在游戏中有很多用途。也许你可以用它们来确定玩家得到的是什么牌,或者从敌人的健康中减去多少范围内的伤害。正如暗示的那样,我们将使用随机数来确定蜜蜂和云的起始位置和速度。
在 C++中生成随机数
为了生成随机数,我们需要使用更多的 C++函数,确切地说是两个。现在不要向游戏中添加任何代码。让我们只看一下语法和一些假设代码所需的步骤。
计算机实际上不能选择随机数。它们只能使用算法/计算来选择一个看起来是随机的数字。为了使这个算法不断返回相同的值,我们必须种子随机数生成器。种子可以是任何整数,尽管每次需要一个唯一的随机数时,它必须是一个不同的种子。看一下这段代码,它种子了随机数生成器:
// Seed the random number generator with the time
srand((int)time(0));
上面的代码使用time
函数从 PC 获取时间,就像这样time(0)
。对time
函数的调用被封装为要发送到srand
函数的值。其结果是当前时间被用作种子。
由于略显不寻常的(int)
语法,前面的代码看起来有点复杂。这样做的目的是将从time
返回的值转换/转型为int
。在这种情况下,这是srand
函数所必需的。
注意
从一种类型转换为另一种类型称为转换。
因此,总结一下,前一行代码发生了什么:
-
它使用
time
获取时间 -
它将其转换为类型
int
-
它将这个结果值发送给
srand
,用于生成随机数
当然,时间是不断变化的。这使得time
函数成为种子随机数生成器的好方法。然而,想想如果我们多次并且在很短的时间内种子随机数生成器,以至于time
返回相同的值会发生什么?当我们给云动画时,我们将看到并解决这个问题。
在这个阶段,我们可以创建一个在范围内的随机数,并将其保存到一个变量中以备后用:
// Get the random number & save it to a variable called number
int number = (rand() % 100);
注意我们分配一个值给number
的奇怪方式。通过使用取模运算符%
和值100
,我们要求在将rand
返回的数字除以100
后得到余数。当你除以100
时,你可能得到的最大数字是 99。最小的可能数字是 0。因此,前面的代码将生成一个在 0 到 99 之间的数字。这个知识对于为我们的蜜蜂和云生成随机速度和起始位置将非常有用。
我们很快就会做到这一点,但我们首先需要学习如何在 C++中做出决定。
使用 if 和 else 做决定
C++的if
和else
关键字是让我们做决定的关键。实际上,在上一章中,当我们在每一帧中检测到玩家是否按下了Esc键时,我们已经看到了if
的作用:
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
到目前为止,我们已经看到了如何使用算术和赋值运算符来创建表达式。现在我们可以看到一些新的运算符。
逻辑运算符
逻辑运算符将通过构建可以测试为真或假的表达式来帮助我们做出决定。起初,这可能看起来像是一个非常狭窄的选择,不足以满足高级 PC 游戏中可能需要的选择。一旦我们深入挖掘,我们将看到我们实际上可以只用几个逻辑运算符就能做出所有需要的决定。
下面是一个最有用的逻辑运算符的表格。看一下它们及其相关的例子,然后我们将看看如何使用它们。
逻辑运算符 | 名称和例子 |
---|---|
== | 比较运算符测试相等性,要么为真,要么为假。例如,表达式(10 == 9) 是假的。10 显然不等于 9。 |
! | 这是逻辑非运算符。表达式(! (2 + 2 == 5)) 。这是真的,因为2 + 2 不等于5 。 |
!= | 这是另一个比较运算符,但与= 比较运算符不同。这测试是否不相等。例如,表达式(10 != 9) 是真的。10 不等于9 。 |
> | 另一个比较运算符 - 实际上还有几个。这测试某物是否大于其他某物。表达式(10 > 9) 是真的。 |
< | 你猜对了。这测试小于的值。表达式(10 < 9) 是假的。 |
>= | 这个运算符测试一个值是否大于或等于另一个值,如果其中一个为真,结果就为真。例如,表达式(10 >= 9) 是真的。表达式(10 >= 10) 也是真的。 |
<= | 像前一个运算符一样,这个运算符测试两个条件,但这次是小于或等于。表达式(10 <= 9) 是假的。表达式(10 <= 10) 是真的。 |
&& | 这个运算符称为逻辑与。它测试表达式的两个或多个单独部分,两个部分都必须为真,结果才为真。逻辑 AND 通常与其他运算符一起用于构建更复杂的测试。表达式((10 > 9) && (10 < 11)) 是真的,因为两个部分都为真,所以表达式为真。表达式((10 > 9) && (10 < 9)) 是假的,因为表达式的一部分为真,另一部分为假。 |
|| | 这个运算符称为逻辑或,它与逻辑 AND 类似,只是表达式的两个或多个部分中至少有一个为真,表达式才为真。让我们看看我们上面使用的最后一个例子,但用|| 替换&& 。表达式((10 > 9) || (10 < 9)) 现在为真,因为表达式的一部分为真。 |
让我们来认识一下 C++的if
和else
关键字,它们将使我们能够充分利用所有这些逻辑运算符。
C++的 if 和 else
让我们把之前的例子变得不那么抽象。见识一下 C++的if
关键字。我们将使用if
和一些运算符以及一个小故事来演示它们的用法。接下来是一个虚构的军事情况,希望它比之前的例子更具体。
如果他们过桥了,就射击他们!
队长垂危,知道他剩下的部下经验不足,决定编写一个 C++程序,在他死后传达他的最后命令。部队必须在等待增援时守住桥的一侧。
队长想要确保他的部队理解的第一个命令是:
“如果他们过桥了,就射击他们!”
那么,我们如何在 C++中模拟这种情况呢?我们需要一个bool
变量:isComingOverBridge
。下一段代码假设isComingOverBridge
变量已经被声明并初始化为true
或false
。
然后我们可以这样使用if
:
if(isComingOverBridge)
{
// Shoot them
}
如果isComingOverBridge
变量等于true
,则大括号{...}
内的代码将运行。如果不是,则程序在if
块之后继续运行,而不运行其中的代码。
或者做这个
队长还想告诉他的部队,如果敌人没有过桥就待在原地。
现在我们可以介绍另一个 C++关键字,else
。当if
的评估结果不为true
时,我们可以使用else
来明确执行某些操作。
例如,要告诉部队如果敌人没有过桥就待在原地,我们可以写下这段代码:
if(isComingOverBridge)
{
// Shoot them
}
else
{
// Hold position
}
然后队长意识到问题并不像他最初想的那么简单。如果敌人过桥,但是人数太多怎么办?他的小队将被压垮和屠杀。所以,他想出了这段代码(这次我们也会使用一些变量。):
bool isComingOverBridge;
int enemyTroops;
int friendlyTroops;
// Initialize the previous variables, one way or another
// Now the if
if(isComingOverBridge && friendlyTroops > enemyTroops)
{
// shoot them
}
else if(isComingOverBridge && friendlyTroops < enemyTroops)
{
// blow the bridge
}
else
{
// Hold position
}
上面的代码有三种可能的执行路径。第一种是如果敌人从桥上过来,友军人数更多:
if(isComingOverBridge && friendlyTroops > enemyTroops)
第二种是如果敌军正在过桥,但人数超过友军:
else if(isComingOveBridge && friendlyTroops < enemyTroops)
然后第三种可能的结果,如果其他两种都不为true
,则由最终的else
捕获,没有if
条件。
读者挑战
你能发现上述代码的一个缺陷吗?这可能会让一群经验不足的部队陷入完全混乱的状态吗?敌军和友军人数完全相等的可能性没有被明确处理,因此将由最终的else
处理。最终的else
是用于没有敌军的情况。我想任何有自尊心的队长都会期望他的部队在这种情况下战斗。他可以改变第一个if
语句以适应这种可能性:
if(isComingOverBridge && friendlyTroops >= enemyTroops)
最后,队长最后关心的是,如果敌人拿着白旗过桥投降,然后被立即屠杀,那么他的士兵最终会成为战争罪犯。显而易见的是需要 C++代码。使用wavingWhiteFlag
布尔变量,他写下了这个测试:
if (wavingWhiteFlag)
{
// Take prisoners
}
但是放置这段代码的问题并不太清楚。最后,队长选择了以下嵌套解决方案,并将wavingWhiteFlag
的测试更改为逻辑非,就像这样:
if (!wavingWhiteFlag)
{
// not surrendering so check everything else
if(isComingOverTheBridge && friendlyTroops >= enemyTroops)
{
// shoot them
}
else if(isComingOverTheBridge && friendlyTroops < enemyTroops)
{
// blow the bridge
}
}
else
{
// this is the else for our first if
// Take prisoners
{
// Holding position
这表明我们可以嵌套if
和else
语句以创建相当深入和详细的决策。
我们可以继续使用if
和else
做出更复杂的决定,但我们已经看到的足够作为介绍。值得指出的是,通常解决问题的方法不止一种。通常正确的方法是以最清晰和最简单的方式解决问题。
我们正在接近拥有所有我们需要的 C++知识,以便能够为我们的云和蜜蜂制作动画。我们还有一个最后的动画问题要讨论,然后我们可以回到游戏中。
时间
在我们移动蜜蜂和云之前,我们需要考虑时间。正如我们已经知道的,主游戏循环一遍又一遍地执行,直到玩家按下Esc键。
我们还学到了 C++和 SFML 非常快。事实上,我的老旧笔记本电脑每秒执行一个简单的游戏循环(比如当前的循环)大约有五千次。
帧率问题
让我们考虑一下蜜蜂的速度。为了讨论的目的,我们可以假装我们要以每秒 200 像素的速度移动它。在一个宽度为 1920 像素的屏幕上,它大约需要 10 秒才能横穿整个宽度,因为 10 乘以 200 等于 2000(接近 1920)。
此外,我们知道我们可以用setPosition(...,...)
来定位我们的精灵中的任何一个。我们只需要把 x 和 y 坐标放在括号里。
除了设置精灵的位置,我们还可以获取精灵的位置。例如,要获取蜜蜂的水平 x 坐标,我们将使用这段代码:
int currentPosition = spriteBee.getPosition().x;
蜜蜂的当前 x 坐标现在存储在currentPosition
中。要将蜜蜂向右移动,我们可以将 200(我们预期的速度)除以 5000(我笔记本电脑上的近似帧率)的适当分数添加到currentPosition
中,就像这样:
currentPosition += 200/5000;
现在我们可以使用setPosition
来移动我们的蜜蜂。它将每帧平滑地从左到右移动 200 除以 5000 像素。但是这种方法有两个大问题。
帧率是我们的游戏循环每秒处理的次数。也就是说,我们处理玩家的输入、更新游戏对象并将它们绘制到屏幕上的次数。我们将在本书的其余部分扩展并讨论帧率的影响。
我的笔记本电脑上的帧率可能并不总是恒定的。蜜蜂可能看起来像是断断续续地在屏幕上加速。
当然,我们希望我们的游戏能够吸引更广泛的受众,而不仅仅是我的笔记本电脑!每台 PC 的帧率都会有所不同,至少会略有不同。如果你有一台非常老旧的 PC,蜜蜂看起来会像被铅压住,如果你有最新的游戏设备,它可能会是一个模糊的涡轮蜜蜂。
幸运的是,这个问题对每个游戏来说都是一样的,SFML 提供了一个解决方案。理解解决方案的最简单方法是实施它。
SFML 帧率解决方案
现在我们将测量并使用帧率来控制我们的游戏。要开始实施这个,只需在主游戏循环之前添加这段代码:
// How fast is each cloud?
float cloud1Speed = 0;
float cloud2Speed = 0;
float cloud3Speed = 0;
// Variables to control time itself
Clock clock;
while (window.isOpen())
{
在前面的代码中,我们声明了一个Clock
类型的对象,并将其命名为clock
。类名以大写字母开头,对象名(我们实际使用的)以小写字母开头。对象名是任意的,但clock
似乎是一个合适的名字,嗯,一个时钟的名字。我们很快也会在这里添加一些与时间相关的变量。
现在,在我们的游戏代码的更新部分添加这个突出显示的代码:
/*
****************************************
Update the scene
****************************************
*/
// Measure time
Time dt = clock.restart();
/*
****************************************
Draw the scene
****************************************
*/
clock.restart()
函数,正如你所期望的那样,重新启动时钟。我们希望每一帧都重新启动时钟,以便我们可以计算每一帧花费的时间。此外,它返回自上次我们重新启动时钟以来经过的时间。
因此,在前面的代码中,我们声明了一个Time
类型的对象,称为dt
,并使用它来存储clock.restart()
函数返回的值。
现在,我们有一个名为dt
的Time
对象,它保存了自上次更新场景并重新启动时经过的时间。也许你能看出这是怎么回事。
让我们向游戏添加一些更多的代码,然后我们将看看我们可以用dt
做些什么。
注意
dt
代表增量时间,即两次更新之间的时间。
移动云和蜜蜂
让我们利用自上一帧以来经过的时间,为蜜蜂和云注入生命。这将解决在不同 PC 上拥有一致的帧速率的问题。
给蜜蜂注入生命
我们想要做的第一件事是在特定高度和特定速度下设置蜜蜂。我们只想在蜜蜂不活动时才这样做。因此,我们将下一个代码放在一个if
块中。检查并添加下面突出显示的代码,然后我们将讨论它:
/*
****************************************
Update the scene
****************************************
*/
// Measure time
Time dt = clock.restart();
// Setup the bee
if (!beeActive)
{
// How fast is the bee
srand((int)time(0));
beeSpeed = (rand() % 200) + 200;
// How high is the bee
srand((int)time(0) * 10);
float height = (rand() % 500) + 500;
spriteBee.setPosition(2000, height);
beeActive = true;
}
/*
****************************************
Draw the scene
****************************************
*/
现在,如果蜜蜂不活动,就像游戏刚开始时一样,if(!beeActive)
将为true
,上面的代码将按照以下顺序执行以下操作:
-
给随机数生成器设定种子
-
获取一个在 199 和 399 之间的随机数,并将结果赋给
beeSpeed
-
再次给随机数生成器设定种子
-
在 499 和 999 之间获取一个随机数,并将结果赋给一个名为
height
的新的float
变量 -
将蜜蜂的位置设置为 x 轴上的
2000
(刚好在屏幕右侧)和 y 轴上等于height
的值 -
将
beeActive
设置为 true
注意
请注意,height
变量是我们在游戏循环内声明的第一个变量。此外,因为它是在if
块内声明的,所以在if
块外部实际上是“不可见”的。对于我们的用途来说,这是可以接受的,因为一旦我们设置了蜜蜂的高度,我们就不再需要它了。这种影响变量的现象称为作用域。我们将在第四章中更全面地探讨这一点:循环、数组、开关、枚举和函数 - 实现游戏机制。
如果我们运行游戏,蜜蜂实际上还不会发生任何事情,但现在蜜蜂是活跃的,我们可以编写一些代码,当beeActive
为true
时运行。
添加下面突出显示的代码,可以看到,这段代码在beeActive
为true
时执行。这是因为在if(!beeActive)
块之后有一个else
:
// Set up the bee
if (!beeActive)
{
// How fast is the bee
srand((int)time(0) );
beeSpeed = (rand() % 200) + 200;
// How high is the bee
srand((int)time(0) * 10);
float height = (rand() % 1350) + 500;
spriteBee.setPosition(2000, height);
beeActive = true;
}
else
// Move the bee
{
spriteBee.setPosition(
spriteBee.getPosition().x -
(beeSpeed * dt.asSeconds()),
spriteBee.getPosition().y);
// Has the bee reached the right hand edge of the screen?
if (spriteBee.getPosition().x < -100)
{
// Set it up ready to be a whole new cloud next frame
beeActive = false;
}
}
/*
****************************************
Draw the scene
****************************************
*/
在else
块中发生以下事情。
使用以下标准更改蜜蜂的位置。setPosition
函数使用getPosition
函数获取蜜蜂当前的 x 坐标。然后将beeSpeed * dt.asSeconds()
添加到该坐标。
beeSpeed
变量的值是每秒多个像素,并且是在先前的if
块中随机分配的。dt.asSeconds()
的值将是一个小于 1 的分数,表示动画上一帧的持续时间。
假设蜜蜂当前的 x 坐标是 1000。现在假设一个相当基本的 PC 以每秒 5000 帧的速度循环。这意味着dt.asSeconds
将是 0.0002。再假设beeSpeed
被设置为最大的 399 像素每秒。那么决定setPosition
用于 x 坐标的值的代码可以解释如下:
1000 - 0.0002 x 399
因此,蜜蜂在 x 轴上的新位置将是 999.9202。我们可以看到,蜜蜂非常平稳地向左飘移,每帧不到一个像素。如果帧速率波动,那么公式将产生一个新的值来适应。如果我们在每秒只能达到 100 帧或者每秒能达到一百万帧的 PC 上运行相同的代码,蜜蜂将以相同的速度移动。
setPosition
函数使用getPosition().y
来确保蜜蜂在整个活动周期内保持完全相同的 y 坐标。
我们刚刚添加的else
块中的最终代码是这样的:
// Has the bee reached the right hand edge of the screen?
if (spriteBee.getPosition().x < -100)
{
// Set it up ready to be a whole new cloud next frame
beeActive = false;
}
这段代码在每一帧(当beeActive
为true
时)测试,蜜蜂是否已经从屏幕的左侧消失。如果getPosition
函数返回小于-100,那么它肯定已经超出了玩家的视野。当发生这种情况时,beeActive
被设置为false
,在下一帧,一个新的蜜蜂将以新的随机高度和新的随机速度飞行。
尝试运行游戏,看着我们的蜜蜂忠实地从右到左飞行,然后再次回到右侧,高度和速度都不同。几乎就像每次都是一只新的蜜蜂。
提示
当然,真正的蜜蜂会在你专心砍树时黏在你身边,让你烦恼很久。在下一个项目中,我们将制作一些更聪明的游戏角色。
现在我们将以非常相似的方式让云移动。
吹云
我们想要做的第一件事是在特定高度和特定速度设置第一朵云。只有在云处于非活动状态时才想要这样做。因此,我们将下一个代码包装在if
块中。在我们为蜜蜂添加代码之后,检查并添加突出显示的代码,然后我们将讨论它。它几乎与我们用于蜜蜂的代码完全相同:
else
// Move the bee
{
spriteBee.setPosition(
spriteBee.getPosition().x -
(beeSpeed * dt.asSeconds()),
spriteBee.getPosition().y);
// Has the bee reached the right hand edge of the screen?
if (spriteBee.getPosition().x < -100)
{
// Set it up ready to be a whole new bee next frame
beeActive = false;
}
}
// Manage the clouds
// Cloud 1
if (!cloud1Active)
{
// How fast is the cloud
srand((int)time(0) * 10);
cloud1Speed = (rand() % 200);
// How high is the cloud
srand((int)time(0) * 10);
float height = (rand() % 150);
spriteCloud1.setPosition(-200, height);
cloud1Active = true;
}
/*
****************************************
Draw the scene
****************************************
*/
我们刚刚添加的代码与蜜蜂代码之间唯一的区别是我们使用不同的精灵并为我们的随机数使用不同的范围。此外,我们使用*10
来对 time(0)返回的结果进行操作,以便确保每个云都得到不同的种子。当我们下一步编写其他云移动代码时,您将看到我们分别使用*20
和*30
。
现在我们可以在云处于活动状态时采取行动。我们将在else
块中这样做。与if
块一样,代码与蜜蜂的代码完全相同,只是所有代码都作用于云而不是蜜蜂:
// Manage the clouds
if (!cloud1Active)
{
// How fast is the cloud
srand((int)time(0) * 10);
cloud1Speed = (rand() % 200);
// How high is the cloud
srand((int)time(0) * 10);
float height = (rand() % 150);
spriteCloud1.setPosition(-200, height);
cloud1Active = true;
}
else
{
spriteCloud1.setPosition(
spriteCloud1.getPosition().x +
(cloud1Speed * dt.asSeconds()),
spriteCloud1.getPosition().y);
// Has the cloud reached the right hand edge of the screen?
if (spriteCloud1.getPosition().x > 1920)
{
// Set it up ready to be a whole new cloud next frame
cloud1Active = false;
}
}
/*
****************************************
Draw the scene
****************************************
*/
现在我们知道该怎么做了,我们可以复制相同的代码用于第二和第三朵云。在第一朵云的代码之后立即添加处理第二和第三朵云的突出代码:
...
// Cloud 2
if (!cloud2Active)
{
// How fast is the cloud
srand((int)time(0) * 20);
cloud2Speed = (rand() % 200);
// How high is the cloud
srand((int)time(0) * 20);
float height = (rand() % 300) - 150;
spriteCloud2.setPosition(-200, height);
cloud2Active = true;
}
else
{
spriteCloud2.setPosition(
spriteCloud2.getPosition().x +
(cloud2Speed * dt.asSeconds()),
spriteCloud2.getPosition().y);
// Has the cloud reached the right hand edge of the screen?
if (spriteCloud2.getPosition().x > 1920)
{
// Set it up ready to be a whole new cloud next frame
cloud2Active = false;
}
}
if (!cloud3Active)
{
// How fast is the cloud
srand((int)time(0) * 30);
cloud3Speed = (rand() % 200);
// How high is the cloud
srand((int)time(0) * 30);
float height = (rand() % 450) - 150;
spriteCloud3.setPosition(-200, height);
cloud3Active = true;
}
else
{
spriteCloud3.setPosition(
spriteCloud3.getPosition().x +
(cloud3Speed * dt.asSeconds()),
spriteCloud3.getPosition().y);
// Has the cloud reached the right hand edge of the screen?
if (spriteCloud3.getPosition().x > 1920)
{
// Set it up ready to be a whole new cloud next frame
cloud3Active = false;
}
}
/*
****************************************
Draw the scene
****************************************
*/
现在你可以运行游戏,云将随机连续地在屏幕上漂移,蜜蜂将在从右到左飞行后重新出现在右侧。
提示
所有这些云和蜜蜂处理似乎有点重复?我们将看看如何节省大量输入并使我们的代码更易读。在 C++中,有处理相同类型的变量或对象的多个实例的方法。这些被称为数组,我们将在第四章中学习:循环、数组、开关、枚举和函数-实现游戏机制。在项目结束时,一旦我们学习了数组,我们将讨论如何改进我们的云代码。
看一看与本章主题相关的一些常见问题解答。
常见问题解答
问:为什么我们在蜜蜂到达-100 时将其设置为非活动状态?为什么不是零,因为零是窗口的左侧?
答:蜜蜂的图形宽 60 像素,其原点位于左上像素。因此,当蜜蜂以 x 等于零的原点绘制时,整个蜜蜂图形仍然在屏幕上供玩家看到。等到它到达-100 时,我们可以确信它肯定已经超出了玩家的视野。
问:我怎么知道我的游戏循环有多快?
答:为了衡量这一点,我们需要学习更多的东西。我们将在第五章中添加测量和显示当前帧速率的功能:碰撞、声音和结束条件-使游戏可玩。
总结
在本章中,我们了解到变量是内存中的命名存储位置,我们可以在其中保存特定类型的值。类型包括int
、float
、double
、bool
、String
和char
。
我们可以声明和初始化我们需要的所有变量,以存储我们游戏的数据。一旦我们有了变量,我们就可以使用算术和赋值运算符来操作它们,并使用逻辑运算符在测试中使用它们。与if
和else
关键字一起使用,我们可以根据游戏中的当前情况分支我们的代码。
利用所有这些新知识,我们制作了一些云和一只蜜蜂的动画。在下一章中,我们将继续使用这些技能,为玩家添加HUD(抬头显示)并增加更多的输入选项,同时通过时间条来直观地表示时间。