第1章 欢迎阅读
# 第1章 欢迎阅读《C++游戏编程入门(第三版)》
让我们开启这段激动人心的旅程,使用C++和基于OpenGL的SFML库,为个人电脑编写精彩的游戏。本书第三版着重于改进和拓展你将学到的知识。从最基础的变量开始,到循环、面向对象编程、标准模板库、SFML库的特性,再到C++的新功能,所有这些基础知识都进行了补充和扩展。在本书结尾,你不仅能拥有四款可玩的游戏,还将打下坚实深厚的C++基础。
本章主要内容如下:
- 首先,我们将介绍本书中要构建的四款游戏。第一款游戏与上一版完全相同,能帮助我们学习C++基础,比如变量、循环和条件判断。第二款和第三款游戏是在上一版的基础上进行了增强、修改和完善,而第四款游戏则是全新的。在我看来,这款新游戏比上一版的最后两款游戏加起来都更具可玩性和学习价值。
- 接下来的内容很重要,你将了解为什么应该使用C++来学习游戏编程,甚至学习其他编程类型。出于多种原因,使用C++来学习游戏开发是最佳选择之一。
- 然后,我们会探索SFML库以及它与C++的关系。
- 没人喜欢企业宣传,在这里你也不会看到这类内容。不过,了解一下微软Visual Studio,以及本书为何使用它,还是很有必要的。
- 接下来,就是搭建开发环境了。不得不承认,这有点枯燥,但我们会一步步快速完成。一旦你操作过一次,以后就再也不用重新学习了。
- 之后,我们将规划并准备第一个游戏项目——《木材!!!》。
- 再接下来,我们将编写本书的第一段C++代码,并完成游戏的第一阶段,让其能够运行并绘制出漂亮的背景,是不是很期待!在下一章,我们会进一步探索,开始移动图形。本章所学的知识将为我们在第一款游戏开发中取得更快进展奠定良好基础。
- 最后,我们将介绍在学习C++和游戏编程过程中,如何处理可能遇到的各种问题,比如配置错误、编译错误、链接错误和程序漏洞等。
当然,你可能首先想知道读完这本“大部头”后能收获什么成果。那么,让我们先来详细了解一下要制作的游戏吧。
你可以在GitHub仓库中找到本章的源代码:https://github.com/PacktPublishing/Beginning-C-Game-Programming-Third-Edition/tree/main/Timber
# 我们将制作的游戏
我们将逐步学习超快速的C++语言基础知识,然后将这些新知识运用到我们要制作的四款游戏中,为它们添加酷炫的功能,整个学习过程会很顺利。
以下是本书的四个项目:
# 木材!!!
第一款游戏是一款令人上瘾、节奏紧凑的游戏,模仿了大获成功的《伐木工》(Timberman)。在制作《木材!!!》这款游戏的过程中,我们将学习C++的所有基础入门知识,同时打造一款真正可玩的游戏。在完成游戏并添加一些最后的增强功能后,它的样子如下:
图1.1:木材游戏
《伐木工》可以在http://store.steampowered.com/app/398710/找到。
# 乒乓
《乒乓》(Pong)是最早的电子游戏之一。它是展示游戏对象动画、玩家输入和碰撞检测基本原理的绝佳例子。我们将制作一个简单的复古版《乒乓》游戏,探索类和面向对象编程的概念。在第7章结束时,它的样子如下:
图1.2:乒乓游戏
玩家将使用屏幕底部的球拍,把球打回屏幕顶部。如果你感兴趣,可以在这里了解《乒乓》的历史:https://en.wikipedia.org/wiki/Pong。
# 僵尸竞技场
接下来,我们将制作一款紧张刺激的僵尸生存射击游戏,与Steam平台上热门的《超过9000僵尸!》(Over 9,000 Zombies!)类似,你可以在http://store.steampowered.com/app/273500/了解更多相关信息。玩家将手持机关枪,必须击退一波又一波不断增多的僵尸。这一切都发生在一个随机生成的滚动世界中:
图1.3:僵尸竞技场游戏
为了实现这个游戏,我们将学习面向对象编程如何让我们编写和维护庞大的代码库(大量代码)。游戏中有许多令人兴奋的功能,比如数百个敌人、速射武器、拾取物,以及每波结束后可以升级的角色。
# 平台游戏
最后一款游戏是一款名为《奔跑》(Run)的平台游戏。借助我们将学到的C++技能,以及SFML库的强大功能,这款游戏将拥有更多精彩的特性。来看看完成后的游戏:
图1.4:平台游戏
游戏特性包括逼真的着色器背景、视差滚动的城市景观、空间化(有方向感)音效、小地图、动画玩家角色、下雨天气效果、音乐、弹出式菜单等等。最棒的是,最终游戏的代码结构可复用,你可以利用它来创造并添加自己的功能。
# 为何在2024年应使用C++学习游戏编程
上面的标题也可以写成 “为什么用游戏编程来学习C++……”,因为在我看来,C++、游戏编程和编程初学者是完美的组合。让我们在关注游戏和初学者的同时,更详细地了解C++:
- 速度:C++以其高性能和高效率著称。在游戏开发中,性能至关重要。C++允许你编写接近CPU和GPU本地语言的代码,这使得它非常适合任何对性能要求苛刻的场景,游戏开发正是其中之一。这是因为C++代码会被转换为本地可执行指令。当我们编写包含成百、上千甚至几十万实体的游戏时,这正是我们所需要的。在最后一章,即第21章,我们将了解C++如何通过着色器程序直接与GPU交互。
- 跨平台开发:C++几乎可以在任何地方运行,这意味着你编写的代码几乎不用大幅修改,就能在各种平台上编译和运行。本书主要关注Windows平台,但我们在本书中学到和编写的所有内容,只需做些小修改,就能在macOS和Linux上运行。C++在下一代主机游戏开发中也有广泛应用,甚至在移动设备上也能发挥作用。编译意味着将我们的C++代码转换为CPU能识别的二进制机器指令。
- 众多游戏引擎和库:许多游戏引擎和库都是用C++编写的,或者提供C++应用程序编程接口(API ,Application Programming Interface)。学习C++能让你接触到最广泛的游戏开发工具和资源,比如虚幻引擎(Unreal Engine),以及像Vulcan、OpenGL、DirectX和Metal这样快速且优秀的图形库,还有像Box2D这样的物理库、像IMGUI这样的UI工具,以及用于合作和多人游戏的网络库,比如RakNet、Enet和SFML自带的网络功能。
- 底层控制:C++能对硬件资源进行底层控制,这对优化游戏性能至关重要。在游戏开发中,你可能需要管理内存、优化渲染管线,并掌控游戏运行的系统,而C++提供了实现这些操作的灵活性和能力。如果管理内存和渲染管线听起来很复杂,那我可以向你保证,一切都会很简单。我们会在第10章和第21章分别以完全适合初学者的方式介绍这两个主题。了解如何控制这些强大的功能,不仅不会让你感到困惑,反而会让你充满力量,掌控自己的编程之路。
- 文档和支持:围绕C++游戏开发有一个活跃的社区,有大量的资源、教程和论坛可以帮助你学习和解决问题。如果你遇到C++相关的问题,我敢保证你不是第一个遇到的,在网上快速搜索一下,几乎总能找到解决方案。ChatGPT也是解决C++问题的得力助手。
- 学习C++确实有挑战,但循序渐进,很容易掌握:在解决问题的过程中努力钻研,最后成功将其转化为令人兴奋的游戏玩法功能,这种感觉非常有成就感。游戏开发通常涉及看似复杂的算法、数据结构和原理,但C++通过面向对象编程(OOP,Object-Oriented Programming)中的标准模板库(STL,Standard Template Library )和类,将复杂性分解为易于管理的部分。我们将分别在第6章和第10章介绍OOP和STL。
- C++是行业标准:正是由于上述原因,C++在游戏开发行业中被广泛应用。熟悉C++能让你更轻松地与其他开发者协作、理解现有代码库、在不同游戏引擎之间切换,还能帮助你在这个行业中找到高薪工作。
有人可能会批评说,与其他一些编程语言相比,C++的学习曲线更陡峭。如果你是编程或游戏开发的新手,在深入学习C++之前,也许可以考虑先从更适合初学者的语言入手,比如用于Unity开发的C#,或者用于简单游戏项目的Python。这种说法有一定道理,但如今已经不像过去那么绝对了。C++在不断发展,近年来引入了许多改进,使得学习更加简单,开发速度也大幅提升。例如,在过去十年里,引入了像auto
这样的新关键字、像箭头运算符(spaceship operator)这样听起来很有趣的逻辑运算符,以及像lambda表达式、协程和智能指针这样的语言结构,这些都极大地简化并加速了C++开发。
总之,我认为不把C++作为第一门编程语言来学习可能是个错误。如果你想让学习变得既有趣又有收获,那么通过游戏来学习是再明智不过的选择。最后,如果你想成为一名独立游戏开发者,或者进入顶尖游戏工作室工作,除非你有其他非常明确的规划,否则C++就是你的不二之选。
既然C++如此出色,有这么多途径和库可供选择,那我们为什么要选择SFML库呢?
# SFML库
SFML即简单快速多媒体库(Simple Fast Media Library)。它不是唯一用于游戏和多媒体开发的C++库。虽然也有理由选择其他库,但对我来说,SFML每次都能满足需求。首先,它是用面向对象的C++编写的。面向对象的C++有很多优点,在阅读本书的过程中,你会逐渐体会到。
SFML也很容易上手,对于初学者来说是个不错的选择。同时,如果你是专业开发者,它也有潜力用于打造最高质量的2D游戏。所以,初学者可以放心使用SFML,不用担心随着经验增长,需要重新学习新的语言或库。如果你想开发3D游戏,在转向虚幻引擎之前,C++和SFML是很好的入门选择。顺便说一句,你可以使用SFML和OpenGL开发3D游戏,但大多数SFML库都专注于2D,本书也是如此。
或许最大的好处是,现代大多数C++编程都使用OOP。我读过的每一本C++入门指南都在使用和教授OOP。实际上,OOP几乎是所有编程语言的未来(也是现在)。那么,如果你刚开始学习C++,为什么要选择其他方式呢?
SFML拥有几乎涵盖2D游戏开发所有需求的库。SFML基于OpenGL运行,而OpenGL也可用于开发3D游戏。当你希望游戏能在多个平台上运行时,OpenGL实际上是免费使用的图形库。使用SFML时,你实际上也在自动使用OpenGL。
SFML能让你实现以下功能:
- 2D图形和动画,包括滚动的游戏世界。
- 音效和音乐播放,包括高质量的方向音效。
- 通过键盘、鼠标和游戏手柄进行输入处理。
- 在线多人游戏功能。
- 相同的代码可以在所有主流桌面操作系统以及移动设备上编译和链接!
经过大量研究发现,即使对于专业开发者来说,也没有更适合用C++为个人电脑开发2D游戏的方式了,尤其对于初学者而言,如果你想在有趣的游戏环境中学习C++,SFML就更合适了。C++,没问题。SFML,也没问题。不过,我们肯定想避开那些大公司的控制,对吧?
# 微软Visual Studio
Visual Studio是一款集成开发环境(Integrated Development Environment,IDE)。它提供了简洁且功能丰富的界面,既能简化游戏开发流程,又能让开发者轻松使用其高级功能。初学者可以借助代码补全和语法高亮等功能,更高效地学习C++。几乎可以说,Visual Studio是最先进的免费C++集成开发环境。微软免费提供这款软件,并非是为过去的过错寻求谅解,而是希望你未来能使用其付费高级版本。所以,我们现在就先好好利用这些免费资源吧。
Visual Studio配备了功能强大的调试器,支持断点和调用堆栈等功能。你可以在Visual Studio中运行游戏,并让它在你指定的位置暂停。这时,你就能检查代码中的变量值,还能逐行执行代码。这让初学者更容易理解代码的运行机制,排查那些原本几乎无法解决的问题。
IntelliSense是Visual Studio的代码建议和实时错误检查工具。它能即时高亮显示错误,并自动给出你可能想要输入的内容,帮助刚接触C++的人更快地学习这门语言。这不仅是初学者的得力学习工具,对专业开发者来说,也能大幅提高开发效率。
Visual Studio拥有庞大且活跃的社区,有许多教程、论坛和资源,能帮助初学者使用Visual Studio开展C++和SFML项目。
Visual Studio还有很多高级功能。随着你知识的增长和追求的提升,Visual Studio也能满足你的新需求。它与Git等流行的版本控制系统(Version Control Systems,VCSs)集成,让多个程序员协作管理大型项目变得轻松上手。Visual Studio的性能分析功能可以监测游戏的内存和CPU使用情况,有助于你改进和优化游戏。
Visual Studio几乎算得上是行业标准。作为使用最广泛的C++集成开发环境之一,它拥有大量用户。这意味着初学者能在网上找到许多针对Visual Studio的帮助和教程。顺便说一句,通常你最后才会想到去微软官网寻求Visual Studio的支持。掌握Visual Studio的使用方法,对未来的雇主来说可能是一项加分技能。
Visual Studio把预处理、编译和链接的复杂过程封装起来,只需按下一个按钮就能完成这些操作。此外,它还提供了简洁的用户界面,方便我们输入代码,管理数量众多的代码文件和其他项目资源。
虽然我们极力称赞了Visual Studio的优点,但确实,任何能用Visual Studio创建的游戏,也都可以通过开源工具来实现。Visual Studio只是让初学者的学习过程更轻松。如果你未来打算换用更“小众”的工具集,从Visual Studio切换过去,也会比直接使用其他工具更加容易。
Visual Studio有高级版本,售价高达数百美元,但我们用免费的Visual Studio 2022社区版就足以完成本书中的所有游戏开发。在撰写本书时,这是最新的免费版本。如果你阅读本书时已经有了更新的版本,我建议你使用新版本。因为Visual Studio不仅向后兼容性强,而且多年来用户界面也保持得相当一致。这意味着你既能从新版本的新功能中受益,又能轻松按照本书的内容进行学习。
在接下来的章节中,我们将搭建开发环境。首先讨论一下,如果你使用的是Mac或Linux操作系统,需要做些什么。
# Mac和Linux系统呢?
我们要制作的游戏可以在Windows、Mac和Linux系统上运行!不同平台的代码完全相同。不过,每个版本都需要在目标平台上进行编译和链接,而且本书的教程无法为Mac和Linux系统的用户提供帮助。
对于完全零基础的初学者来说,很难说这本书完全适合Mac和Linux系统的用户。不过,我猜,如果你是Mac或Linux系统的忠实用户,对自己的操作系统很熟悉,那你很可能会成功完成学习。你遇到的大多数额外挑战可能集中在开发环境、SFML库的初始设置,以及第一个项目的搭建上。
为此,我强烈推荐以下教程,希望它们能替代接下来大约10页的内容,直到“规划《木材!!!》”这部分。从这部分开始,本书的内容就适用于所有操作系统了。
Linux系统的用户,可以阅读这个教程来替代接下来的几个章节:https://www.sfml-dev.org/tutorials/2.5/start-linux.php。
Mac系统的用户,请阅读这个教程来开启学习之旅:https://www.sfml-dev.org/tutorials/2.5/start-osx.php 。
# 安装Visual Studio 2022
要开始创建游戏,我们首先得安装Visual Studio 2022。安装Visual Studio的过程非常简单,差不多就是下载文件,然后点击几个按钮的事。只要你选择了正确的版本,安装过程就没什么难度。在选择版本时,我会明确指出正确的选项。
需要注意的是,这些年来,微软很可能会更改获取Visual Studio的名称、外观和下载页面。他们可能还会调整用户界面的布局,让接下来的安装说明过时。不过,根据我的经验,微软会尽力保持不同版本之间的一致性。而且,我们为每个项目配置的设置对于C++和SFML来说至关重要。所以,即便微软对Visual Studio做出了重大改动,仔细理解本章的安装说明,应该还是能顺利完成安装的。
下面开始安装Visual Studio:
首先,你需要一个微软账户和登录信息。如果你有Hotmail、Windows、Xbox或MSN账户,那你已经有微软账户了。如果没有,可以在这里免费注册一个:https://login.live.com/ 。
在撰写本书时(2024年5月),Visual Studio 2022是最新版本,希望在一段时间内本章内容都不会过时。要开始安装,访问https://visualstudio.microsoft.com/ ,找到Visual Studio的下载链接。下图展示了我访问上述链接时页面的样子:
图1.5:下载Visual Studio
找到Visual Studio的下载选项,从下拉菜单中选择“Community 2022”。注意,除了社区版之外,其他版本都是收费的,不是免费的。图片中显示的“Visual Studio Code”选项也不是我们本书所需的。点击“保存”按钮,下载就开始了。
下载完成后,双击下载文件来运行安装程序。授权Visual Studio对电脑进行更改后,等待安装程序下载一些文件,并进入安装的下一阶段。
很快,你会被问到要将Visual Studio安装在哪里。选择一个可用存储空间至少有50GB的硬盘。网上很多资料说,小于50GB的空间也能安装,但等你开始创建项目后就会发现,50GB的空间能确保你有足够的空间用于未来的开发。准备好后,找到“使用C++的桌面开发”选项并选中它。接着,点击“安装”按钮。这一步可能需要一些时间才能完成。
现在,我们准备好关注SFML库,然后开启第一个项目了。
# 设置SFML库
这个简短的教程将指导你下载SFML文件,这样我们就能在项目中使用SFML库的功能了。此外,我们还会了解如何使用SFML动态链接库(DLL,Dynamic-Link Library )文件,让编译后的目标代码能与SFML协同运行。按照以下步骤设置SFML库:
- 访问SFML官网的这个链接:http://www.sfml-dev.org/download.php 。点击“Latest stable version”(最新稳定版本)按钮,如下图所示:
图1.6:下载SFML 2.6
- 等你读到本书时,最新版本很可能已经更新了。但只要你做好下一步,这就不是问题。我们要下载32位版本。这可能有点违反直觉,因为你很可能(最常见的情况)使用的是64位电脑。我们下载32位版本的原因是,32位应用程序可以在32位和64位机器上运行。此外,我们需要下载适用于Visual Studio 22的版本。点击下图中显示的“Download”(下载)按钮:
图1.7:下载SFML 17_22
- 下载完成后,在安装Visual Studio的磁盘根目录下创建一个文件夹,命名为“SFML”。同时,在同一磁盘根目录下再创建一个文件夹,命名为“VS Projects”。
- 最后,解压下载的SFML文件。在桌面上进行解压操作。我下载的文件名为“SFML-2.6.0-windows-vc17-32-bit.zip”,但你下载的文件名可能因为SFML版本更新而有所不同。解压完成后,可以删除压缩包文件。这时,桌面上会留下一个文件夹,其名称反映了你下载的SFML版本。双击这个文件夹查看内容,我这里是一个名为“SFML-2.6.0”的文件夹。现在再双击进入这个文件夹。
下图展示了我的SFML文件夹的内容。你的文件夹内容应该和我的类似。
图1.8:SFML文件夹内容
复制这个文件夹中的所有内容,然后粘贴到你在第3步创建的“SFML”文件夹中。在本书后续内容中,我将这个文件夹简称为“你的SFML文件夹”。
现在,我们准备好在Visual Studio中使用C++和SFML库了。
# 在Visual Studio 2022中创建新项目
设置项目的过程比较繁琐,所以我们会一步步来,帮助你尽快熟悉:
- 像启动其他应用程序一样启动Visual Studio,点击它的图标就行。默认的安装选项会在Windows开始菜单中创建一个Visual Studio 2022的图标。你会看到以下窗口:
图1.9:在VS 2022中启动新项目
- 点击上图中高亮显示的“Create a new project”(创建新项目)按钮。你会看到“Create a new project”(创建新项目)窗口,如下图所示:
图1.10:创建新项目屏幕
- 在“Create a new project”(创建新项目)窗口中,我们需要选择要创建的项目类型。我们要创建的是一个控制台应用程序,它没有菜单、选择框或其他与Windows相关的元素,所以选择“Console App”(控制台应用),如上图中高亮显示的那样,然后点击“Next”(下一步)按钮。接着,你会看到“Configure your new project”(配置新项目)窗口。完成接下来的三个步骤后,“Configure your new project”(配置新项目)窗口如下图所示:
图1.11:配置新项目
- 在“Configure your new project”(配置新项目)窗口的“Project name”(项目名称)字段中输入“Timber”。注意,这会使Visual Studio自动将“Solution name”(解决方案名称)字段配置为相同的名称。
- 在“Location”(位置)字段中,浏览并选择我们在上一个教程中创建的“VS Projects”文件夹。这里将是存放我们所有项目文件的地方。
- 勾选“Place solution and project in the same directory”(将解决方案和项目放在同一目录中)选项。
- 注意,上图展示的是完成上述三个步骤后的窗口样子。完成这些步骤后,点击“Create”(创建)按钮。项目就会生成,其中包含一些C++代码。下图展示的是我们在本书中都将在此进行操作的界面:
图1.12:Visual Studio代码编辑器
- 现在,我们要配置项目,使其能够使用我们放在“SFML”文件夹中的文件。在主菜单中,选择“Project”(项目) | “Timber properties…”(Timber属性…)。你会看到以下窗口:
图1.13:Timber属性页
在上图中,“OK”(确定)、“Cancel”(取消)和“Apply”(应用)按钮显示不完全。这可能是Visual Studio在我的屏幕分辨率下出现的显示问题,希望你的按钮显示正常。不管按钮显示是否和我的一样,按照教程继续操作就可以。
接下来,我们将开始配置项目属性。由于这些步骤比较复杂,我会用新的步骤列表来详细说明。
# 配置项目属性
在这个阶段,你应该已经打开了“Timber属性页”窗口,如前一节末尾的截图所示。现在,我们将开始配置一些属性,并参考下面的注释截图:
图1.14:配置项目属性
在本节中,我们将添加一些复杂且重要的项目设置。这是比较繁琐的部分,但每个项目我们只需要做一次,而且每次做的时候都会变得更轻松、更快捷。我们需要做的是告诉Visual Studio从哪里找到SFML的一种特殊类型的代码文件。我所说的特殊类型的文件是头文件(header file)。头文件定义了SFML代码的格式,这样当我们使用SFML代码时,编译器就知道如何处理它。请注意,头文件与主源代码文件不同,它们包含在扩展名为.hpp的文件中。当我们在第二个项目中最终开始添加自己的头文件时,这一切都会变得更加清晰。此外,我们需要告诉Visual Studio可以在哪里找到SFML库文件。为了实现这些,在“Timber属性页”窗口中,执行以下三个步骤,这些步骤在前面的截图中已编号:
首先(1),从“配置”下拉菜单中选择“所有配置”,并检查右侧的“平台”下拉菜单中是否选择了“Win32”。
其次(2),从左侧菜单中选择“C/C++”,然后选择“常规”。
第三步(3),找到“附加包含目录”编辑框,输入你的SFML文件夹所在的盘符,后面跟着“\SFML\include”。如果你把SFML文件夹放在D盘,要输入的完整路径如前面的截图所示,即
D:\SFML\include
。如果你把SFML放在不同的盘,请相应更改路径。点击“应用”保存到目前为止的配置。
现在,仍在同一窗口中,执行以下步骤,参考下面的注释截图。首先(1),选择“链接器”,然后选择“常规”。
现在,找到“附加库目录”编辑框(2),输入你的SFML文件夹所在的盘符,后面跟着“\SFML\lib”。所以,如果你把SFML文件夹放在D盘,要输入的完整路径也如下面的截图所示,即
D:\SFML\lib
。如果你把SFML放在不同的盘,请相应更改路径:
)
图1.15:附加库目录
7. 点击“应用”保存到目前为止的配置。
8. 在此阶段的最后,仍在同一窗口中,执行以下步骤,参考下面的注释截图。将“配置”下拉菜单(1)切换到“调试”,因为我们将在调试模式下运行和测试我们的游戏。
图1.16:链接器输入配置 9. 选择“链接器”,然后选择“输入”(2)。 10. 找到“附加依赖项”编辑框(3),在最左侧点击它。现在,复制并粘贴/输入以下内容:sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib; 在指定位置。要格外小心,将光标准确地放在正确的位置,不要覆盖已经存在的任何文本。 11. 点击“确定”。 12. 点击“应用”,然后点击“确定”。
呼,就是这样!我们已经成功配置了Visual Studio,可以继续规划Timber!!!项目了。
# 规划Timber!!!
无论何时制作游戏,最好先用笔和纸进行规划。如果你都不清楚游戏在屏幕上的运行方式,又怎么可能用代码实现它呢?
此时,如果你还没有做过,我建议你去看一下《伐木工》(Timberman)游戏的视频,这样你就能了解我们的目标。如果你觉得预算允许,那就买一份来玩一玩。它在Steam上经常以不到1美元的价格出售:http://store.steampowered.com/app/398710/ 。
定义游戏玩法的游戏功能和对象被称为游戏机制(mechanics)。这个游戏的基本机制如下:
- 时间总是在流逝。
- 你可以通过砍树获得更多时间。
- 砍树会导致树枝掉落。
- 玩家必须避开掉落的树枝。
- 重复以上过程,直到时间耗尽或玩家被树枝压死。
在这个阶段就期望你规划C++代码显然有点不现实。当然,这是一本C++初学者指南的第一章。不过,我们可以查看一下我们将使用的所有资源,并大致了解一下我们的C++代码需要实现哪些功能。
看一下这个游戏的注释截图:
图1.17:Timber游戏截图
你可以看到,我们有以下功能:
- 玩家得分:玩家每次砍倒一根木头,就会得到一分。他们可以使用左箭头键或右箭头(光标)键砍木头。
- 玩家角色:每次玩家砍树时,他们会根据所使用的光标键移动到树的同一侧或停留在同一侧。因此,玩家必须注意选择在哪一侧砍树。
- 当玩家砍树时,玩家角色的手中会出现一个简单的斧头图形。
- 不断缩短的时间条:每次玩家砍树时,会有少量时间添加到不断缩短的时间条中。
- 致命的树枝:玩家砍树的速度越快,赢得的时间就越多,但树枝向下掉落的速度也越快,因此玩家被压死的可能性就越大。树枝在树顶随机生成,每次砍树时向下移动。
- 当玩家被压死时(这种情况会经常发生),会出现一个墓碑图形。
- 被砍倒的木头:玩家砍树时,被砍倒的木头图形会从玩家身边飞速离开。
- 仅用于装饰:有三朵飘浮的云,会在随机高度和速度下飘动,还有一只蜜蜂,只会四处飞舞。
- 背景:所有这些都发生在一个漂亮的背景上。
简而言之,玩家必须疯狂砍树以获得分数并避免时间耗尽。作为一个有点反常但又有趣的结果,他们砍树的速度越快,就越有可能被压死。
我们现在已经知道了游戏的外观、玩法以及游戏机制背后的设计思路。现在,我们可以开始构建游戏了。按照以下步骤进行:
- 现在,我们需要将SFML的.dll文件复制到主项目目录中。我的主项目目录是
D:\VS Projects\Timber
。它是在上一个教程中由Visual Studio创建的。如果你把VS Projects文件夹放在了其他地方,就在那里执行这一步骤。我们需要复制到项目文件夹中的文件在你的SFML\bin
文件夹中。分别打开这两个位置的窗口,选中SFML\bin
文件夹中的所有文件,如下图所示:
图1.18:选择所有需要的文件
2. 现在,将选中的文件复制并粘贴到项目文件夹中,即D:\VS Projects\Timber
。
3. 项目现在已经设置好了,可以开始了。你将看到以下界面。我对这个截图进行了注释,以便你开始熟悉Visual Studio。我们很快会再次回到这里,深入了解所有这些区域以及其他内容:
图1.19:在哪里输入代码
你的布局可能与前面截图中的略有不同,因为和大多数应用程序一样,Visual Studio的窗口是可定制的。花点时间找到“解决方案资源管理器”窗口,并进行调整,使其内容清晰易读,如前面的截图所示。
我们很快就会回来开始编码。但首先,我们将探索一下我们将使用的项目资源。
# 项目资源
资源(Assets)是制作游戏所需的任何东西。在我们的例子中,这些资源包括以下内容:
- 用于在屏幕上绘制文本的字体
- 用于不同动作的一些音效,如砍树、死亡和时间耗尽
- 一些图形,也就是纹理(textures),用于角色、背景、树枝和其他游戏对象
这个游戏所需的所有图形和声音都包含在本书的下载包中。可以在Chapter 1/graphics
和Chapter 1/sound
文件夹中找到相应的文件。
所需的字体没有提供。这是因为我想避免任何可能的版权问题。不过,这不会造成问题,因为我会告诉你具体在哪里以及如何选择和下载字体。
# 制作自己的音效
音效(Sound effects,FX)可以从诸如Freesound(www.freesound.org)这样的网站免费下载,但通常情况下,如果你要出售游戏,其使用许可不允许你使用这些音效。另一种选择是使用一款名为BFXR的开源软件,其官网是www.bfxr.net,它可以帮助你生成许多不同的音效,这些音效归你所有,你可以随意使用。
# 将资源添加到项目中
一旦你决定了要使用哪些资源,就该将它们添加到项目中了。以下说明假设你使用的是本书下载包中提供的所有资源。如果你使用自己的资源,只需用你自己的相应声音或图形文件替换,文件名保持不变:
- 浏览到项目文件夹,即
D:\VS Projects\Timber
。 - 在这个文件夹中创建三个新文件夹,分别命名为
graphics
、sound
和fonts
。 - 从下载包中,将
Chapter 1/graphics
的全部内容复制到D:\VS Projects\Timber\graphics
文件夹中。 - 从下载包中,将
Chapter 1/sound
的全部内容复制到D:\VS Projects\Timber\sound
文件夹中。 - 现在,在你的网络浏览器中访问http://www.1001freefonts.com/komika_poster.font ,下载Komika Poster字体。
- 解压下载的压缩文件,将
KOMIKAP_.ttf
文件添加到D:\VS Projects\Timber\fonts
文件夹中。
让我们来看一下这些资源,尤其是图形资源,这样我们就能想象在C++代码中使用它们时的场景。
# 探索资源
图形资源构成了游戏场景的各个部分。如果你查看图形资源,应该很清楚它们将在游戏的哪些地方使用:
图1.20:资源
声音文件均为.wav格式。这些文件包含了我们将在游戏中的特定事件中播放的音效。它们都是使用BFXR生成的,具体如下:
chop.wav
:一种类似斧头砍树的声音death.wav
:一种类似复古的“失败”音效out_of_time.wav
:当玩家因时间耗尽而失败(而不是被压死)时播放的声音
我们已经查看了所有资源,包括图形资源,现在我们将简短讨论一下屏幕分辨率以及如何在屏幕上定位图形。
# 理解屏幕坐标和内部坐标
在开始实际的C++编码之前,让我们先谈谈坐标。我们在显示器上看到的所有图像都是由像素(pixels)组成的。像素是微小的光点,它们组合在一起形成了我们在屏幕上看到的图像。
显示器有许多不同的分辨率,例如,一台普通的显示器可能水平方向有1920个像素,垂直方向有1080个像素。
像素是从屏幕左上角开始编号的。从下面的图表中可以看到,在我们1920×1080的示例中,水平(x)轴上的编号从0到1919,垂直(y)轴上的编号从0到1079:
图1.21:屏幕坐标和内部坐标
因此,一个特定且精确的屏幕位置可以通过x和y坐标来确定。我们通过将游戏对象,如背景、角色、子弹和文本,绘制到屏幕上的特定位置来创建游戏。
这些位置是由像素的坐标来确定的。看一下下面这个假设的例子,展示我们如何在屏幕的大致中心坐标处进行绘制。对于1920×1080的屏幕,这个位置是在(960, 540):
图1.22:绘制中心坐标
除了屏幕坐标,我们的游戏对象也各自有类似的坐标系统。与屏幕坐标系统一样,它们的内部或局部坐标也是从左上角的(0, 0)开始。
在前面的图像中,我们可以看到角色的(0, 0)位置绘制在了屏幕的(960, 540)处。一个可视化的2D游戏对象,如角色或僵尸,被称为精灵(Sprite)。精灵通常由一个图像文件构成。所有精灵都有所谓的原点(origin)。
如果我们将一个精灵绘制到屏幕上的特定位置,那么位于这个特定位置的是精灵的原点。精灵的(0, 0)坐标就是它的原点。下面的图像展示了这一点:
图1.23:带有原点的精灵示意图
因此,在显示角色绘制到屏幕上的图像中,尽管我们将图像绘制在了中心位置(960, 540),但它看起来却偏右且偏下。
了解这一点很重要,因为它将帮助我们理解用于绘制所有图形的坐标。
请注意,在现实世界中,游戏玩家的屏幕分辨率千差万别,我们的游戏需要尽可能适用于更多的分辨率。在第三个项目中,我们将了解如何使我们的游戏动态适应几乎任何分辨率。在这个第一个项目中,我们需要假设屏幕分辨率为1920×1080或更高。
现在,我们可以编写第一段C++代码并查看其运行效果了。
# 开始编写游戏代码
如果Visual Studio尚未打开,请打开它。在Visual Studio主窗口的“最近使用的项目”列表中,左键单击Timber项目将其打开。
在右侧找到“解决方案资源管理器”窗口。在“源文件”文件夹下找到Timber.cpp文件。“.cpp”代表C++。
删除代码窗口中的全部内容,并添加以下代码,这样你就和文档内容一致了。你可以像在任何文本编辑器或文字处理软件中那样操作;如果你愿意,甚至可以复制粘贴。完成编辑后,我们再来探讨这些代码:
// 这是我们游戏的起始点
int main()
{
return 0;
}
2
3
4
5
这个简单的C++程序是一个很好的起点。让我们逐行分析。
# 用注释使代码更易理解
代码的第一行如下:
// 这是我们游戏的起始点
任何以两个斜杠(//)开头的代码行都是注释,编译器会忽略它们。因此,这行代码不会执行任何操作。它用于留下一些信息,方便我们日后查看代码时使用。注释在该行结束,所以下一行的内容不属于注释。还有一种注释类型,称为多行注释或C风格注释,可用于编写跨越多行的注释。在本章后面我们会看到一些这样的注释。在本书中,我会添加数百条注释,帮助补充上下文信息并进一步解释代码。
# 主函数
我们在代码中看到的下一行是:
int main()
int
是一种数据类型。C++有许多数据类型,它们代表不同类型的数据。int
表示整数。先记住这点,我们稍后再详细讨论。
main()
部分是后面代码段的名称。这段代码由起始花括号({)和下一个结束花括号(})括起来。
所以,这两个花括号{...}之间的所有内容都是main
的一部分。我们把这样的一段代码称为函数(function)。
每个C++程序都有一个主函数,它是整个程序执行(运行)的起点。随着我们深入学习本书内容,最终我们的游戏会包含许多代码文件。然而,始终只会有一个主函数,无论我们编写什么代码,游戏总是从主函数起始花括号内的第一行代码开始执行。
目前,不必担心函数名后面奇怪的括号()。在第4章“循环、数组、开关、枚举和函数——实现游戏机制”中,当我们从全新且更有趣的角度了解函数时,会进一步讨论它们。
让我们仔细看看主函数中的这一行代码。
# 代码呈现与语法
再次查看我们主函数的全部内容:
int main() {
return 0;
}
2
3
我们可以看到,在main
函数内部,只有一行代码return 0;
。在深入了解这行代码的作用之前,我们先看看它的呈现方式。这很有用,因为它有助于我们编写易于阅读且与代码其他部分区分开的代码。
首先,注意return 0;
向右缩进了一个制表符。这清楚地表明它是main
函数的内部代码。随着代码长度增加,我们会发现缩进代码和留出空白对于保持代码可读性至关重要。
接下来,注意行尾的标点符号。分号(;)告诉编译器这是一条指令的结束,后面的内容是一条新指令。我们把以分号结尾的指令称为语句(statement)。
注意,编译器并不在意分号和下一条语句之间是否换行或留有空格。然而,每条语句不换行编写会使代码难以阅读,而完全省略分号会导致语法错误,游戏将无法编译和运行。
一段代码通常通过缩进与其他部分区分开来,这样的代码段称为代码块(block)。
现在你对主函数、缩进代码以保持整洁以及在每条语句末尾加分号的概念有了一定了解,接下来我们深入探究return 0;
语句的具体作用。
# 从函数返回值
实际上,在我们的游戏情境中,return 0;
几乎不执行任何操作。然而,这个概念很重要。当我们使用return
关键字时,无论它后面是否跟有值,它都是一条指令,用于让程序执行跳回/返回到最初调用该函数的代码处。
通常,调用函数的代码可能是我们代码中其他地方的另一个函数。但在这个例子中,是操作系统调用了主函数。所以,当执行return 0;
时,主函数退出,整个程序结束。
由于return
关键字后面跟着0,这个值也会被返回给操作系统。我们可以将0改为其他值,那么返回给操作系统的就是修改后的值。
在编程术语中,我们说调用函数的代码启动了函数,而函数返回了一个值。
你现在不必完全理解关于函数的所有这些信息。在这里介绍只是为了让你有个初步了解。在第一个项目中,我们会详细介绍函数的全部细节。在继续之前,关于函数还有最后一点需要说明。还记得int main()
中的int
吗?它告诉编译器,从main
函数返回的值的类型必须是int
(整数)。我们可以返回任何符合int
类型的值,比如0、1、999、6358等等。如果我们尝试返回非int
类型的值,比如12.76,代码将无法编译,游戏也无法运行。
函数可以返回多种不同类型的值,甚至包括我们自定义的类型!不过,必须按照我们刚刚介绍的方式,让编译器知道返回值的类型。这些关于函数的背景知识会让我们后续的学习更加顺利。
# 运行游戏
此时你甚至可以运行游戏。在Visual Studio的快速启动栏中,点击“本地Windows调试器”按钮即可运行。或者,你也可以使用F5快捷键:
图1.24:“本地Windows调试器”按钮
确保“本地Windows调试器”按钮旁边的版本设置为x86,如下图所示。这意味着我们的程序将是32位的,与我们下载的SFML版本匹配。
图1.25:确保以x86模式运行
运行后你只会看到一个黑色屏幕。如果黑色屏幕没有自动关闭,你可以按任意键关闭它。这个窗口是C++控制台,我们可以用它来调试游戏。目前我们还不需要进行调试。实际情况是,我们的程序启动后,从main
函数的第一行代码return 0;
开始执行,然后立即返回到操作系统。
现在我们已经编写并运行了最简单的程序。接下来,我们添加更多代码,打开一个游戏最终将显示在其中的窗口。
# 使用SFML打开窗口
现在,让我们添加一些代码。下面的代码将使用SFML打开一个窗口,Timber!!!游戏最终将在这个窗口中运行。窗口宽1920像素、高1080像素,并且是全屏显示(没有边框和标题)。
在现有代码中输入下面突出显示的新代码,然后我们一起分析:
// Include important libraries here
#include <SFML/Graphics.hpp>
// Make code easier to type with "using namespace" using namespace sf;
// This is where our game starts from 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
现在我们逐行分析这段代码,以便理解它。
# 引入SFML的功能
在新代码中,我们首先注意到的是#include
指令。
#include
指令指示Visual Studio在编译前包含或添加另一个文件的内容。这样做的效果是,一些我们没有编写的代码在运行程序时会成为程序的一部分。将其他文件中的代码添加到我们的代码中的过程称为预处理,不出所料,这个过程由一个叫做预处理器(preprocessor)的工具执行。.hpp
文件扩展名表示它是一个头文件(header file)。
因此,#include <SFML/Graphics.hpp>
告诉预处理器包含SFML
文件夹中的Graphics.hpp
文件的内容。这个SFML
文件夹就是我们在设置项目时创建的文件夹。
这一行代码从文件中添加了代码,使我们能够使用SFML的一些功能。当我们开始编写自己的独立代码文件并使用#include
来调用它们时,具体的实现方式会更加清晰。
在本书中,我们最常包含的文件是SFML头文件,通过这些头文件,我们可以使用所有炫酷的游戏编码功能。我们还会使用#include
来访问C++标准库头文件。这些头文件让我们能够使用C++语言本身的核心功能。
目前重要的是,只要添加这一行代码,我们就可以使用SFML提供的许多新功能。
下一行新代码是using namespace sf;
。我们稍后再讨论这行代码的作用。
# 面向对象编程、类和对象
在本书的学习过程中,我们会深入讨论面向对象编程(Object-Oriented Programming,OOP)、类和对象。下面是一个简要介绍,以便我们理解目前的代码。
我们已经知道OOP代表面向对象编程。OOP是一种编程范式,即一种编码方式。在大多数编程语言的编程领域中,OOP通常被认为是编写代码的最佳方式,甚至是唯一的专业方式。注意,我说的是“大多数”,也有例外情况。
OOP引入了许多编码概念,其中类(classes)和对象(objects)是最基础的。在编写代码时,只要有可能,我们都希望编写可复用、可维护且安全的代码。实现这一目标的方法是将代码组织成类。我们将在第6章“面向对象编程——开始编写Pong游戏”中学习如何做到这一点。
目前关于类我们只需要知道,一旦编写好了类,我们不会直接在游戏中执行类的代码;相反,我们从类中创建可使用的对象。
例如,如果我们需要100个僵尸非玩家角色(non-player characters,NPC),我们可以精心设计并编写一个名为Zombie
的类,然后从这个类创建任意数量的僵尸对象。每个僵尸对象都具有相同的功能和内部数据类型,但每个僵尸对象都是一个独立且不同的实体。
为了进一步说明这个假设的僵尸示例,但不展示Zombie
类的代码,我们可能会基于Zombie
类创建一个新对象,如下所示:
Zombie z1;
现在,z1
对象就是一个完整编码且可运行的僵尸对象。然后我们可以这样做:
Zombie z2; Zombie z3; Zombie z4; Zombie z5;
现在我们有了五个独立的僵尸实例,但它们都基于精心编写的同一个类。在回到刚才编写的代码之前,我们再深入一步。我们的僵尸对象既可以包含行为(由函数定义),也可以包含数据,这些数据可能代表僵尸的生命值、速度、位置或移动方向等信息。例如,我们可以编写Zombie
类的代码,以便像这样使用僵尸对象:
z1.attack(player); z2.growl(); z3.headExplode();
再次注意,目前这些僵尸代码都是假设的。不要将这些代码输入到Visual Studio中,否则会产生一堆错误。
我们设计类是为了以最适合游戏目标的方式使用其中的数据和行为。例如,我们可以设计类,以便在创建每个僵尸对象时为其数据赋值。
假设我们在创建每个僵尸时需要为其分配一个唯一的名字和以米每秒为单位的速度。通过精心编写Zombie
类的代码,我们可以这样编写代码:
// Dave was a 100 metre Olympic champion before infection
// He moves at 10 metres per second Zombie z1("Dave", 10);
// Gill had both of her legs eaten before she was infected
// She drags along at .01 metres per second Zombie z2("Gill", .01);
2
3
4
关键在于类具有很强的灵活性,一旦编写好了类,我们就可以通过创建类的对象/实例来使用它们。正是通过类以及从类创建的对象,我们才能充分利用SFML的强大功能。没错,我们也会编写自己的类,包括Zombie
类。
让我们回到刚才编写的实际代码。
# 使用命名空间sf
在进一步研究VideoMode
和RenderWindow
之前(你现在可能已经猜到,它们是SFML提供的类),我们先来了解using namespace sf;
这行代码的作用。
当我们创建一个类时,会将其放在一个命名空间(namespace)中。这样做是为了将我们的类与其他人编写的类区分开来。以VideoMode
类为例。
在像Windows这样的环境中,完全有可能已经有人编写了一个名为VideoMode
的类。通过使用命名空间,我们和SFML的程序员可以确保类名不会冲突。
使用VideoMode
类的完整方式如下:
sf::VideoMode...
using namespace sf;
使我们能够在代码中省略sf::
前缀。如果不这样做,仅在这个简单的游戏中就会出现100多处sf::
。它不仅使我们的代码更易读,还缩短了代码长度。
# SFML的VideoMode和RenderWindow
在主函数中,我们现在有两条新注释和两行可执行代码。第一行可执行代码如下:
VideoMode vm(1920, 1080);
这段代码从VideoMode
类创建了一个名为vm
的对象,并设置了两个内部值1920和1080。这些值代表玩家屏幕的分辨率。
下一行新代码如下:
RenderWindow window(vm, "Timber!!!", Style::Fullscreen);
在上一行代码中,我们从SFML提供的RenderWindow
类创建了一个名为window
的新对象。此外,我们还在window
对象中设置了一些值。
首先,vm
对象用于初始化window
的一部分。一开始这可能会让人感到困惑。然而,请记住,一个类可以根据其创建者的需求变得多种多样且灵活。没错,有些类可以包含其他类的实例。
如果你理解了这个概念,目前不必完全明白它的具体工作原理。我们编写一个类,然后从这个类创建可使用的对象,这有点像建筑师绘制蓝图。你当然不能把家具、孩子和狗都搬进蓝图里,但你可以根据蓝图建造一所房子(或多所房子)。在这个类比中,类就像蓝图,对象就像房子。
接下来,我们使用"Timber!!!"
这个值为窗口命名。然后,我们使用预定义的Style::FullScreen
值使window
对象全屏显示。
Style::FullScreen
是SFML中定义的值。它很有用,因为我们无需记住内部代码用于表示全屏的整数值。这种类型的值在编程术语中称为常量(constant)。下一章将介绍常量及其与C++紧密相关的变量(variables)。
让我们看看window
对象的实际效果。
# 运行游戏
此时你可以再次运行游戏。你会看到一个更大的黑色屏幕一闪而过然后消失。这就是我们刚刚编写代码创建的1920×1080的全屏窗口。遗憾的是,实际情况仍然是我们的程序启动后,从main
函数的第一行代码开始执行,创建了这个很酷的新游戏窗口,然后执行到return 0;
,并立即返回到操作系统。
接下来,我们添加一些代码,这些代码将构成本书中每个游戏的基本结构。这就是所谓的游戏循环(game loop)。
# 游戏循环
在本节中,我们需要让程序实现以下功能。我们需要一种方法让程序持续运行,直到玩家想要退出。同时,随着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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
接下来,我们分析解释刚刚添加的代码。
# while循环
新代码中我们首先看到的是:
while (window.isOpen()) {
新代码中最后看到的是一个结束花括号}
。我们创建了一个while
循环。while
循环起始花括号({)和结束花括号(})之间的所有内容将不断重复
# C风格代码注释
在while
循环里,我们能看到一段乍一看有点像ASCII艺术的内容:
/*
**************************************** Handle the player's input
****************************************
*/
2
3
4
ASCII艺术是一种小众但有趣的用计算机文本创建图像的方式。你可以在这个链接了解更多相关内容:https://en.wikipedia.org/wiki/ASCII_art 。
上述代码其实只是另一种类型的注释,被称为C风格注释。这种注释以/*
开头,以*/
结尾,中间的内容仅用于提供信息,不会参与编译。我用这段稍微复杂点的文本,是为了清楚表明代码文件各部分的功能。当然,你现在应该能明白,接下来的代码都与处理玩家输入有关。
跳过几行代码,你会发现另一个C风格注释,它表明该部分代码用于更新场景。
再跳到下一个C风格注释处,就能清楚知道哪里是绘制所有图形的代码。下面我们详细了解一下这些部分。
# 输入、更新、绘制、重复
虽然第一个项目采用了最简单的游戏循环版本,但每个游戏的代码都离不开这些阶段。我们来梳理一下这些步骤:
- 获取玩家输入(如果有的话)。
- 根据人工智能、物理效果或玩家输入等因素更新场景。
- 绘制当前场景。
- 以足够快的频率重复这些步骤,打造出一个具有交互性、流畅且充满动画效果的游戏世界。
现在,我们来看看游戏循环中具体实现这些功能的代码。
# 检测按键
首先,在以“Handle the player's input”(处理玩家输入)这段文本为标识的代码部分,有如下代码:
if (Keyboard::isKeyPressed(Keyboard::Escape)) {
window.close();
}
2
3
这段代码用于检查Esc键当前是否被按下。如果按下了,高亮显示的代码就会使用window
对象关闭窗口。这样,下次while
循环开始时,程序会发现window
对象已关闭,就会跳到while
循环右花括号之后的代码处,游戏也就随之退出。我们将在第2章“变量、运算符与决策:动画精灵”中更全面地讨论if
语句。
# 清除并绘制场景
目前,“Update the scene”(更新场景)部分没有代码,所以我们直接看“Draw the scene”(绘制场景)部分。首先要做的是用下面这段代码擦除上一帧的动画:
window.clear();
接下来,我们应该绘制游戏中的所有对象,但目前还没有任何游戏对象。
下一行代码是:
window.display();
当我们绘制所有游戏对象时,其实是将它们绘制到一个隐藏的表面上,准备进行显示。window.display()
代码会将显示内容从之前显示的表面切换到新更新(之前隐藏)的表面。这样,玩家永远不会看到绘制过程,因为在切换之前,所有精灵都已添加到这个表面上。这也确保了在切换场景之前,场景内容是完整的,避免了被称为“画面撕裂”的图形错误。这个过程被称为双缓冲。
还要注意,所有这些绘制和清除功能都是通过我们从SFML的RenderWindow
类创建的window
对象来实现的。
# 运行游戏
运行游戏后,你会看到一个空白的全屏窗口,直到按下Esc键才会关闭。
这是一个不错的进展。目前,我们有了一个可执行程序,它能打开窗口并循环运行,等待玩家按下Esc键退出。现在,我们可以着手绘制游戏的背景图像了。
# 绘制游戏背景
现在,我们将在游戏中看到一些图形。我们要做的是创建一个精灵(sprite),首先创建的将是游戏背景精灵,然后在清除窗口和显示/切换窗口内容之间绘制它。
# 使用纹理准备精灵
SFML的RenderWindow
类让我们创建了window
对象,该对象具备游戏窗口所需的所有功能。
现在我们再了解两个SFML类,它们用于将精灵绘制到屏幕上。其中一个类叫Sprite
,这应该并不意外。另一个类是Texture
。纹理(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()) {
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
值得注意的是,这段代码在循环之前,因为它只需要执行一次。首先,我们用SFML的Texture
类创建一个名为textureBackground
的对象:
Texture textureBackground;
创建好之后,我们就可以用textureBackground
对象将graphics
文件夹中的图像加载到textureBackground
中,代码如下:
textureBackground.loadFromFile("graphics/background.png");
这里我们只需要指定graphics/background
,因为路径是相对于我们创建文件夹并添加图像的Visual Studio工作目录的。
接下来,用下面的代码创建一个SFML的Sprite
类对象,名为spriteBackground
:
Sprite spriteBackground;
然后,将Texture
对象(backgroundTexture
)和Sprite
对象(backgroundSprite
)关联起来,代码如下:
spriteBackground.setTexture(textureBackground);
最后,将spriteBackground
对象定位在window
对象中的(0,0)
坐标处,也就是左上角:
spriteBackground.setPosition(0,0);
由于graphics
文件夹中的background.png
图像宽1920像素、高1080像素,它会恰好填满整个屏幕。注意,前面这行代码并不会显示精灵,只是设置了它的位置,为显示做准备。
现在,backgroundSprite
对象就可以用来显示背景图像了。
你肯定很好奇为什么要这么麻烦。这是由显卡和OpenGL的工作方式决定的。
纹理会占用图形内存,而图形内存是有限的资源。此外,将图像加载到GPU内存的过程非常慢——虽然不会慢到你能明显看到加载过程,或者让电脑明显卡顿,但也足够慢到不能在游戏循环的每一帧都进行加载。所以,将纹理(textureBackground
)与游戏循环中会操作的代码分离是很有必要的。
在开始移动图形时你会发现,我们会用精灵来实现移动。所有由Texture
类创建的对象会一直存储在GPU中,等待关联的Sprite
对象指示其显示位置。在后续项目中,我们还会让多个不同的Sprite
对象复用同一个Texture
对象,这样能有效利用GPU内存。
综上所述:
- 纹理加载到GPU的过程很慢。
- 纹理一旦加载到GPU上,访问速度就会很快。
- 我们将
Sprite
对象和纹理关联起来。 - 我们(通常在“更新场景”部分)操作
Sprite
对象的位置和方向。 - 我们绘制
Sprite
对象,进而显示与之关联的Texture
对象(通常在“绘制场景”部分)。
所以,现在我们只需要利用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();
2
3
4
5
6
7
8
新添加的代码只是在清除显示内容和显示新绘制场景之间,用window
对象绘制spriteBackground
对象。
现在我们知道了什么是精灵,知道可以将纹理与之关联,然后在屏幕上定位并绘制它。现在可以再次运行游戏,看看这些代码的效果了。
# 运行游戏
如果现在运行程序,我们就能看到游戏初现雏形的迹象:
图1.26:运行游戏
目前这个状态肯定拿不到年度游戏奖,但至少我们已经迈出了第一步!
下面来看看本章以及本书后续内容中可能出现的一些问题。
# 处理错误
每个项目都会遇到问题和错误,这是必然的!问题越难,解决时就越有成就感。经过几个小时的努力,新的游戏功能终于实现,那种感觉真的很棒。要是没有这些挑战,一切就没那么有价值了。
在阅读本书的过程中,你可能会遇到一些难题。保持冷静,相信自己能克服困难,然后着手解决问题。
记住,无论遇到什么问题,很可能已经有人遇到过同样的问题。用简洁的语句描述你的问题或错误,然后在谷歌或ChatGPT中搜索。你会惊讶于这种方式解决问题的速度和准确性,因为通常已经有人为你解决了同样的问题。
话虽如此,如果你在完成本章内容时遇到困难,这里有一些提示可以帮你起步。
# 配置错误
本章中出现问题最可能的原因是配置错误。在设置Visual Studio、SFML和项目本身的过程中,你可能已经注意到,有大量的文件名、文件夹和设置都必须正确无误。只要有一个设置错误,就可能引发多种错误,而且错误提示信息可能无法明确指出具体问题。
如果空白项目出现黑屏且无法正常运行,重新开始可能会更简单。确保所有文件名和文件夹都适合你的特定设置,然后让代码中最简单的部分先运行起来,也就是屏幕闪一下黑色然后关闭的部分。如果这部分能正常运行,那么问题可能就不是出在配置上。
# 编译错误
编译错误可能是我们接下来最常遇到的错误。检查一下你的代码是否和我的完全一样,尤其要注意行末的分号,以及类名和对象名的大小写细微差别。如果还是不行,打开下载文件中的代码文件,复制粘贴代码。
虽然本书中的代码可能存在输入错误,但这些代码文件都源自实际运行的项目,肯定是能正常工作的!
# 链接错误
链接错误最可能是因为缺少SFML的.dll文件。你把所有.dll文件都复制到项目文件夹了吗?
# 程序漏洞
当代码能运行,但运行结果不符合预期时,就说明出现了程序漏洞。调试其实也可以很有趣。修复的漏洞越多,游戏就会越好,你一天的工作也就越有成就感。解决漏洞的关键在于尽早发现它们!为此,我建议每次实现新功能后都运行并测试游戏。发现漏洞的时间越早,导致漏洞的代码在你脑海里就越清晰。在本书中,我们会在每个可能的阶段运行代码,查看运行结果。
# 总结
这一章颇具挑战性。配置集成开发环境(IDE)以使用C++库确实有点麻烦,而且耗时较长。对于刚接触编程的人来说,类和对象的概念也确实有点难以理解。
不过现在,我们可以专注于C++、SFML和游戏开发了。随着本书内容的推进,我们会学到更多C++知识,实现越来越有趣的游戏功能。同时,我们会进一步学习函数、类和对象等内容,让它们不再那么神秘。
在本章中,我们收获颇丰,包括概述了一个带有主函数的基本C++程序,构建了一个简单的游戏循环,该循环能监听玩家输入,并将一个精灵(及其关联的纹理)绘制到屏幕上。
在下一章,我们将学习绘制更多精灵并为它们制作动画所需的C++知识。
# 常见问题解答
以下是一些你可能会有的疑问:
- 问:目前的内容我理解起来有点困难,我适合学编程吗?
- 答:设置开发环境并理解面向对象编程(OOP)的概念,可能是你在本书中要做的最困难的事情。如果你的游戏能正常运行(比如能绘制背景),那就可以继续学习下一章了。
- 问:一直在讲OOP、类和对象,内容太多了,有点破坏学习体验。
- 答:别担心。我们会不断提到OOP、类和对象。在第6章“面向对象编程——开始制作《乒乓》游戏”中,我们将真正深入学习OOP。目前你只需要知道,SFML编写了很多有用的类,我们可以通过这些类创建可用的对象来使用这些代码。等你对OOP有了更多了解,就会更得心应手。
- 问:我真的不太理解函数相关的内容。
- 答:没关系,我们会经常讲到函数,之后也会更深入地学习。你只需要知道,调用函数时,函数中的代码会被执行,当函数执行完毕(遇到
return
语句),程序会跳回到调用它的代码处。