构建 Cocos2dx 安卓游戏(全)
原文:
zh.annas-archive.org/md5/C5B09CE8256BCC61162F0F46EF01CFDE
译者:飞龙
前言
Cocos2d-x 是最常使用的开源游戏框架。它得到了微软对其移动和桌面平台官方支持,其小巧的核心运行速度比其他框架快,使得它能在低端 Android 设备上表现出色。目前,它由一个活跃的开源开发社区维护,该社区由原始 Cocos2d for iPhone 的作者和触控科技领导。
这本入门书籍将指导你从零开始创建一个简单的二维 Android 游戏。在这个过程中,你将学习 Cocos2d-x C++跨平台游戏框架的基础知识,如何处理精灵,为游戏添加物理效果,播放声音,显示文本,使用粒子系统生成逼真的爆炸效果,以及如何使用 Java Native Interface (JNI)添加原生 Android 功能。
这本书涵盖的内容
第一章,配置你的开发环境,逐步指导你配置 Cocos2d-x 及其所有先决条件。
第二章,图形,介绍了如何处理背景、精灵,以及如何使用精灵表提升性能来动画化它们。
第三章,理解游戏物理,展示了基于 Chipmunk 的新 Cocos2d-x 物理引擎的基础知识,该引擎在 Cocos2d-x 3.0 版本中引入。我们将创建基于物理的物体,为它们添加重力,并检测碰撞。
第四章,用户输入,我们在这里为游戏添加交互功能,使其能够通过触摸监听器和加速度计与用户互动。
第五章,处理文本和字体,证明了处理文本对于游戏开发至关重要。无论你的游戏复杂性如何,你都有可能显示信息,有时需要使用外文字符集。这一章展示了如何使用简单的 TrueType 字体和更具风格的位图字体,使你的游戏看起来更专业。
第六章,音频,说明了玩游戏时的情感部分来自于音乐和音效。在这一章中,你将学习如何使用 CocosDenshion 音频引擎为你的游戏添加背景音乐和音效,该音频引擎自原始 Cocos2d iPhone 游戏引擎以来一直存在。这一章还涵盖了如何使用新的音频引擎播放媒体,并突出了它们之间的主要区别。
第七章,创建粒子系统,说明了如何使用内置的粒子系统引擎创建逼真的爆炸、火焰、雪和雨效果。这一章还展示了当你需要定制效果时,如何创建自己的粒子系统,使用最受欢迎的工具。
第八章,添加原生 Java 代码,在你需要为 Cocos2d-x 游戏活动内部创建和调用 Android 特定行为时为你提供帮助。我们通过使用 Android 平台可用的 Java 原生接口(JNI)机制来实现这一点。
你需要这本书的内容
为了跟随本书的叙述并能够重现所有步骤,你需要一台装有 Windows 7 或更高版本操作系统的 PC,任何 Linux 发行版或运行 OS X 10.10 Yosemite 的 Mac。我们将在书中使用的许多工具都是可以免费下载的。我们解释了如何下载和安装它们。
本书适合的读者
这本书是为那些在游戏编程方面几乎没有经验,但具备 C++编程语言知识,并且愿意以非常全面的方式创建他们的第一款 Android 游戏的人编写的。
约定
在这本书中,你会发现多种文本样式,这些样式区分了不同类型的信息。以下是一些样式示例及其含义的解释。
文本中的代码字、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入如下所示:"为了向我们的游戏添加加速度计支持,我们首先将在HelloWorldScene.h
头文件中添加以下方法声明。"
代码块设置如下:
void HelloWorld::movePlayerByTouch(Touch* touch, Event* event)
{
Vec2 touchLocation = touch->getLocation();
if(_sprPlayer->getBoundingBox().containsPoint(touchLocation)){
movePlayerIfPossible(touchLocation.x);
}
}
当我们希望引起你对代码块中某个特定部分的注意时,相关的行或项目会以粗体显示:
Size screenSize = glview->getFrameSize();
Size designSize(768, 1280);
std::vector<std::string> searchPaths;
searchPaths.push_back("sounds");
任何命令行输入或输出都如下编写:
cocos new MyGame -p com.your_company.mygame -l cpp -d NEW_PROJECTS_DIR
新术语和重要词汇以粗体显示。你在屏幕上看到的内容,例如菜单或对话框中的单词,在文本中如下所示:"点击下一步按钮,你会进入下一个屏幕。"
注意
警告或重要注意事项会像这样出现在一个框中。
提示
技巧和诀窍会像这样出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的内容。读者的反馈对我们来说很重要,它帮助我们开发出你真正能从中获得最大收益的图书。
要向我们发送一般反馈,只需发送电子邮件至<[email protected]>
,并在邮件的主题中提及书名。
如果你在一个主题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请查看我们在www.packtpub.com/authors上的作者指南。
客户支持
既然你现在拥有了 Packt 的一本书,我们有一系列的事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你在www.packtpub.com
的账户下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support
注册,我们会直接将文件通过电子邮件发送给你。
错误更正
尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——我们会非常感激你能向我们报告。这样做,你可以让其他读者免受挫折,并帮助我们在后续版本中改进这本书。如果你发现任何错误更正,请通过访问www.packtpub.com/submit-errata
,选择你的书,点击错误更正提交表单链接,并输入你的更正详情。一旦你的更正被验证,你的提交将被接受,并且更正将在我们网站的相应标题下的错误更正部分上传或添加到现有的错误更正列表中。任何现有的错误更正可以通过在www.packtpub.com/support
选择你的标题来查看。
盗版
网络上的版权材料盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上以任何形式遇到我们作品非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如发现疑似盗版材料,请通过<[email protected]>
联系我们,并提供相关链接。
我们感谢你帮助保护我们的作者,以及我们为你提供有价值内容的能力。
问题
如果你在这本书的任何方面遇到问题,可以通过<[email protected]>
联系我们,我们将尽力解决。
第一章:设置你的开发环境
在本章中,我们将解释如何下载并设置所有需要的工具,以便开始为 Android 平台构建游戏的环境。尽管 Mac OS 和 Windows 开发环境之间有很大的相似性,但我们将涵盖这两个操作系统安装的所有细节。
本章节将涵盖以下主题:
-
Cocos2d-x 概述
-
安装 Java
-
安装 Android SDK
-
安装 Android 原生开发工具包(NDK)
-
安装 Apache Ant
-
安装 Python
-
安装 Cocos2d-x
-
安装 Eclipse IDE
-
模板代码演练
Cocos2d-x 概述
Cocos2d-x 是流行的 iOS 游戏框架 Cocos2d 的 C++跨平台移植版本。它最初于 2010 年 11 月发布,并在 2011 年被北京一家移动游戏公司触控科技收购。尽管如此,它仍然由一个超过 40 万开发者的活跃社区维护,包括原始 Cocos2d iPhone 引擎的创造者 Ricardo Quesada。
这个框架封装了所有游戏细节,如声音、音乐、物理、用户输入、精灵、场景和过渡等,因此开发者只需关注游戏逻辑,而无需重新发明轮子。
安装 Java
Android 平台技术栈基于 Java 技术;因此,首先要下载的是 Java 开发工具包(JDK)。尽管在撰写本书时 Java JDK 8 是最新版本,但它并不被所有 Android 版本官方支持,因此我们将下载 JDK 6,所有由 Cocos2d-x 生成的模板 Java 代码都可以用这个版本成功编译。
注意
Java 运行环境(JRE)对于构建 Android 应用程序来说是不够的,因为它只包含了运行 Java 应用程序所需的文件,但它不包括构建 Java 应用程序所需的工具。
你可以从 Oracle 的www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase6-419409.html
下载 JDK 6,无论你的开发环境是什么。
如果你的当前环境是 Windows,那么在安装 JDK 之后,你需要将二进制文件所在路径添加到 PATH 环境变量中。这个路径看起来像这样:C:\Program Files\Java\jdk1.6.0_45\bin
。
打开一个新的系统控制台,输入 javac –version
,如果显示了 Java 编译器的版本号,那么你已经成功在你的系统中安装了 JDK。
注意
JDK 7 是用于构建针对 Android 5.0 及以上版本应用程序所需的。如果你针对的是最新 Android 版本,应该下载这个版本。但是,如果你想你的游戏兼容低于 4.4 的 Android 版本,那么你应该选择 JDK 6。
安装 Android SDK
Android SDK 包含构建 Android 应用所需的所有命令行工具。它有适用于 Windows、Mac 和 GNU/Linux 操作系统的版本。
Android Studio 现在是唯一官方支持的 IDE;尽管如此,Cocos2d-x 3.4 只提供对 Eclipse 的即开即用支持,Eclipse 是之前的官方 Android 开发 IDE。它不再可供下载,因为它已经不再积极开发,但你可以手动下载 Eclipse 并按照以下步骤安装 Android Development Tools (ADT)。
下载 Android SDK
你可以从链接 developer.android.com/sdk
下载 Android SDK。在页面底部,在 Other Download Options 下,你会找到下载 SDK 工具的选项。选择与你的操作系统相匹配的版本。
在撰写本书时,SDK 的最新版本是 24.0.2。
运行 Android SDK 安装程序并在你的计算机上安装 Android SDK。
安装完 Android SDK 后,它还不能立即用来构建 Android 应用。因此,在安装向导的最后一屏,勾选 Start SDK Manager 的复选框,以便你可以下载构建游戏所需的组件,如下面的截图所示:
当 Android SDK 管理器启动后,从 Tools
文件夹中选择 Android SDK Platform-tools 和 Android SDK Build-tools。然后选择你所需 API 级别中的 SDK Platform,如下面的截图所示:
下载 Eclipse
从 www.eclipse.org/downloads
下载 Eclipse IDE for Java Developers 的最新版本。它会推荐与你的当前操作系统兼容的下载版本,选择最适合你的操作系统平台的版本,可以是 32 位或 64 位。
在撰写本书时,Eclipse Luna (4.4.1) 是最新版本。
设置 Eclipse ADT 插件
打开 Eclipse,导航到 Help | Install new Software 并添加 Eclipse ADT 下载位置,即 https://dl-ssl.google.com/android/eclipse/
,如下面的截图所示:
点击 OK,然后勾选 Developer Tools 复选框,点击 Next 以完成 ADT 安装向导。
设置 Android 原生开发工具包
我们已经下载了允许你使用 Java 技术创建 Android 应用的 Android SDK;尽管如此,Cocos2d-x 框架是用 C++ 编写的,因此你需要 Android 原生开发工具包(NDK)以便为 Android 平台构建 C++ 代码。
注意
Android 的官方文档明确指出,你应该在特定情况下使用这个本地工具包,但不要仅仅因为熟悉 C++ 语言或希望应用程序运行更快而使用它。制造商提出这个建议是因为 Android 核心 API 只对 Java 语言可用。
下载最新的 NDK 版本。在本书编写时,最新的版本是 10d。这个版本的 NDK 将允许你为所有 Android 平台构建,包括最新的。
你可以从以下链接下载适用于所有平台的最新版本 Android NDK:
developer.android.com/tools/sdk/ndk
下载后,运行可执行文件。它将在当前路径解压 Android NDK 目录;你需要记住这个路径,因为你稍后需要用到。
设置 Apache Ant
Apache Ant 是一个广泛用于自动化 Java 项目构建过程的构建管理工具。从 Cocos2d-x 3.0 开始引入,用于为 Android 平台构建框架。它简化了 Android 的构建过程,并增强了跨平台构建。在 Cocos2d-x 2.x 时代,在 Windows 操作系统内构建 Android 应用需要通过使用 Cygwin 模拟 UNIX 环境。这需要一些小的修改才能成功构建代码,其中许多修改在官方 Cocos2d-x 网站上仍然没有记录。
这个工具可以从以下链接下载:www.apache.org/dist/ant/binaries/
在编写本书时,最新的版本是 1.9.4。这个工具是一个跨平台工具,所以一个下载文件可以在任何支持 Java 技术的操作系统上工作。
为了安装这个工具,只需解压文件。记住这个路径,因为你在 Cocos2d-x 设置过程中需要用到。
设置 Python
所有 Cocos2d-x 配置文件都是用 Python 编写的。如果你使用的是 Mac OS 或任何 Linux 发行版,你的操作系统已经预装了 Python。因此,你可以跳过这一部分。
如果你使用的是 Windows 系统,你需要从以下链接下载 Python 2:www.python.org/ftp/python/2.7.8/python-2.7.8.msi
。
请考虑 Python 和 Cocos2d-x 同时支持版本 2 和版本 3。Cocos2d-x 只支持 Python 2。在编写本书时,2.x 分支的最新版本是 2.7.8。
安装程序设置完成后,你应该手动将 Python 安装路径添加到 PATH 环境变量中。默认的安装路径是C:\Python27
。
打开一个新的系统控制台并输入python
,如果出现如下截图所示的 Python 控制台,那么意味着 Python 已经正确安装:
注意
在 Windows 上设置环境变量,点击开始按钮并输入:编辑系统环境
变量,点击它然后点击环境变量按钮,接着将显示环境变量配置对话框。
设置 Cocos2d-x
既然你已经拥有了构建 Android 平台上的第一款 Cocos2d-x 游戏所需的所有前提条件,你将需要下载 Cocos2d-x 3.4 框架,并按照以下步骤进行设置:
-
你可以从
www.cocos2d-x.org/download
下载源代码。请注意,此页面还提供了下载 Cocos2d-x 分支 2 的链接,本书不涉及此分支,而且制造商已正式宣布新特性仅在分支 3 中提供。 -
下载压缩的 Cocos2d-x 源代码后,将其解压到你想要的位置。
-
为了配置 Cocos2d-x,打开你的系统终端,定位到你解压它的路径,并输入
setup.py
。这将需要你指定ANDROID_NDK_PATH
,在这里你需要指定之前在前面章节解压的 NDK 的根目录。其次,它将需要你指定ANDROID_SDK_ROOT
,这里你需要指定在安装过程中你选择安装 Android SDK 的目录路径。然后,它将需要你设置ANT_ROOT
,在这里你需要指定 ant 安装的根目录。最后,关闭终端,并打开一个新的终端,以便更改生效。
创建你的第一个项目
现在,Cocos2d-x 已经设置好了,可以开始创建你的第一个项目了。你可以通过输入以下命令来完成:
cocos new MyGame -p com.your_company.mygame -l cpp -d NEW_PROJECTS_DIR
此脚本为你创建了一个 Android 模板代码,你的游戏将运行在所有包含 Android API 9 或更高版本的 Android 设备上,即 Android 2.3(姜饼)及更高版本。
需要注意的是,包名应该恰好包含两个点,如示例所示,如果少于或多于两个点,项目创建脚本将无法工作。–l cpp
参数意味着新项目将使用 C++作为编程语言,这是本书唯一涵盖的语言。
与 2.x 分支相反,Cocos2d-x 3.x 允许你在框架目录结构之外创建你的项目。因此,你可以在任何位置创建你的项目,而不仅仅是像之前版本那样在projects
目录内。
这将需要一些时间,因为它会将所有框架文件复制到你的新项目路径中。完成后,将你的 Android 设备连接到电脑上,然后你可以在新项目路径中通过输入以下命令轻松运行模板HelloWorld
代码:
cocos run -p android
或者,无论你当前在终端的路径如何,都可以运行以下命令:
cocos run -p android /path/to/project
注意
要为 Windows 构建和运行 Cocos2d-x 3.4,您需要 Microsoft Visual Studio 2012 或 2013。
现在,您应该能够看到 Cocos2d-x 的标志和显示Hello World的文字,如下面的图片所示:
设置 Eclipse IDE
Cocos2d-x 3 分支显著改进了安卓构建过程。
在 2 分支中,需要在 IDE 中手动配置许多环境变量,导入许多核心项目,并处理依赖关系。即使完成所有步骤后,Cygwin Windows UNIX 端口与 Eclipse 的集成也从未完善,因此需要一些小的修改。
在 Eclipse 中构建 Cocos2d-x 3.4 就像导入项目并点击运行按钮一样简单。为了实现这一点,在 ADT 中,转到文件 | 导入 | 通用 | 将现有项目导入工作空间,选择 Cocos2d-x 在上一个部分创建新项目的路径。然后点击完成。
提示
Cocos2d-x 安卓模板项目是使用 API 级别 10 作为目标平台创建的。如果您系统上没有安装这个版本,您应该通过从包浏览器中右键点击项目,点击属性,并从项目构建目标框中选择您喜欢的已安装的安卓 API 版本来进行更改。
现在,在包浏览器中右键点击项目名称,点击作为运行,最后点击安卓应用程序。将会显示以下弹出窗口,要求您指定要启动 Cocos2d-x 游戏的安卓设备:
选择您的安卓设备后,您将看到我们在前一部分运行运行命令时所显示的 HelloWorld 游戏场景。
模板代码演练
在这一部分,我们将解释由项目创建脚本在上一个部分生成的 Cocos2d-x 模板代码的主要部分。
Java 类
我们的项目中现在有一个名为AppActivity
的 Java 类,它没有成员并从核心库中的Cocos2dxActivity
类继承。我们还可以看到项目中引用了核心库中的 22 个 Java 类。这段代码旨在使我们的 C++代码工作,我们完全不需要修改它。
安卓应用程序配置
生成的AndroidManifest.xml
是 Android 配置文件,它需要android.permission.INTERNET
权限,该权限允许你的 Android 应用程序使用设备上的互联网连接;然而,由于我们的简单游戏代码没有互联网交互,所以并不需要这个权限。因此,如果你愿意,可以删除AndroidManifest.xml
文件中的这一行。你的游戏默认会以横屏显示,但如果你希望创建一个在竖屏模式下运行的游戏,那么你应该将android:screenOrientation
的值从landscape
更改为portrait
。
为了更改 Android 应用程序名称,你可以修改位于strings.xml
文件中的app_name
值;这将影响启动器图标上的文字和 Android 系统内的应用程序标识符。
当你创建自己的游戏时,你将不得不创建自己的类,这些类通常会比脚本创建的两个类多。每次你创建一个新类时,都需要将其名称添加到新项目目录结构中jni
文件夹内的Android.mk
制作文件的LOCAL_SRC_FILES
属性中。这样,当你的cpp
代码由 C++ 构建工具构建时,它会知道应该编译哪些文件。
C++ 类
已经创建了两个 C++ 类:AppDelegate
和HelloWorldScene
。第一个负责启动 Cocos2d-x 框架并将控制权传递给开发者。框架加载过程发生在这个类中。如果 Cocos2d-x 核心框架在目标设备上成功启动,它将运行applicationDidFinishLaunching
方法,这是要运行的首个游戏特定功能。
代码非常直观,并且有详细的文档,以便你可以轻松理解其逻辑。我们对代码的第一次小改动将是隐藏默认显示在示例游戏中的调试信息。你可以猜测,为了实现这一点,你只需为director
单例实例中的setDisplayStats
方法调用发送false
作为参数,如下面的代码清单所示:
bool AppDelegate::applicationDidFinishLaunching() {
// initialize director
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
if(!glview) {
glview = GLViewImpl::create("My Game");
director->setOpenGLView(glview);
}
// turn on display FPS
director->setDisplayStats(false);
// set FPS. the default value is 1.0/60 if you don't call this
director->setAnimationInterval(1.0 / 60);
// create a scene. it's an autorelease object
auto scene = HelloWorld::createScene();
// run
director->runWithScene(scene);
return true;
}
提示
下载示例代码
你可以从你在www.packtpub.com
的账户中下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support
注册,我们会直接将文件通过电子邮件发送给你。
场景
在本书后续章节中,我们将介绍 Cocos2d-x 如何处理场景概念,就像电影一样;电影由场景组成,Cocos2d-x 游戏也是如此。我们可以将加载、主菜单、世界选择、游戏关卡、结束字幕等不同的屏幕可视化为不同的场景。每个场景都有一个定义其行为的类。模板代码只有一个名为HelloWorld
的场景,该场景在AppDelegate
类内部初始化并启动。正如我们在之前的代码中所见,场景流程由游戏导演管理。Director
类拥有驱动游戏的所有基本特性,就像电影中的导演一样。有一个导演类的单一共享实例在整个应用程序范围内被使用。
HelloWorldScene
包含了代表我们运行 HelloWorld 应用程序时出现的所有可见区域的层,即,hello world 标签,Cocos2d-x 标志和显示退出选项的菜单。
在init
方法中,我们实例化视觉元素,并使用从Node
核心类继承的addChild
方法将其添加到场景中。
总结
在本章中,我们介绍了 Cocos2d-x 3.4 游戏框架,并解释了如何下载和安装它。我们还解释了所有它的先决条件。我们配置了工作环境,将我们的第一个 Android 应用程序部署到实际设备上,并通过脚本生成的模板代码快速概览了其主要方面。
在下一章中,我们将介绍如何创建和操作所有的游戏图形,例如主角、敌人、障碍物、背景等。
第二章:图形
在本章中,我们将介绍如何创建和处理所有游戏图形。我们将创建场景,使用游戏导演处理这些场景之间的过渡,创建精灵,将它们定位到所需的位置,使用动作移动它们,以及使用动画为角色赋予生命。
本章将涵盖以下主题:
-
创建场景
-
理解节点
-
理解精灵
-
理解动作
-
动画精灵
-
添加游戏菜单
-
处理多种屏幕分辨率
创建场景
场景概念在 Cocos2d-x 游戏引擎中非常重要,因为游戏中所有显示的屏幕都被视为场景。如果将 Cocos2d-x 与 Android 原生 Java 开发进行类比,我们可以说 Cocos2d-x 的场景相当于 Android 所称的活动。
在上一章中,我们介绍了AppDelegate
类,并解释了它有责任在设备上加载框架,然后执行游戏特定的代码。这个类包含了ApplicationDidFinishLaunching
方法,这是我们代码的入口点。在这个方法中,我们实例化了将在游戏中首次显示的场景,然后请求director
加载它,如下面的代码清单所示:
bool AppDelegate::applicationDidFinishLaunching() {
auto director = Director::getInstance();
// OpenGL initialization done by cocos project creation script
auto glview = director->getOpenGLView();
auto scene = HelloWorld::createScene();
director->runWithScene(scene);
return true;
}
注意
所有 C++代码都在一个单一的 Android 活动中运行;尽管如此,我们仍然可以向游戏中添加原生活动。
理解图层
场景本身不是一个对象容器,因此它应该至少包含一个Layer
类的实例,这样我们才能向其中添加对象。这个图层创建过程在框架宏CREATE_FUNC
中被封装。你只需调用宏并将类名作为参数传递,它就会生成图层创建代码。
在框架的前一个版本中,图层操作与事件处理有关多种用途;然而,在 3.0 版本中,事件分发引擎被完全重写。Cocos2d-x 3.4 中仍然存在图层概念的唯一原因是兼容性。框架创建者官方宣布,他们可能会在后续版本中移除图层概念。
使用导演
场景由 Cocos2d-x 导演控制,这是一个处理游戏流程的类。它应用了单例设计模式,确保类只有一个实例。它通过场景堆栈控制应该呈现的场景类型,类似于 Android 处理场景的方式。
这意味着最后一个推送到堆栈的场景是即将呈现给用户的那一个。当场景被移除时,用户将能够看到之前可见的场景。
当我们在单个函数中使用单一导演实例不止一次时,我们可以将它的引用存储在局部变量中,如下所示:
auto director = Director::getInstance();
我们也可以将其存储在类属性中,以便在类的各个部分都可以访问。这样做可以让我们少写一些代码,同时也代表了性能的提升,因为我们每次想要访问单例实例时,不需要多次调用getInstance
静态方法。
Director 实例还可以为我们提供有用的信息,比如屏幕尺寸和调试信息,在我们的 Cocos 项目中默认是启用的。
暂停游戏
让我们开始创建我们的游戏。我们要添加的第一个功能是暂停和恢复游戏的功能。让我们开始构建——首先设置当我们暂停游戏时将显示的屏幕。
我们将通过向场景堆栈中添加一个新的暂停场景来实现这一点。当这个屏幕从堆栈中移除时,HelloWorld 场景将显示出来,因为它是在暂停场景推入场景堆栈之前显示的屏幕。以下代码清单展示了我们如何轻松地暂停游戏:
组织我们的资源文件
当我们创建 Cocos2d-x 项目时,一些资源,比如图片和字体,默认被添加到我们项目的Resources
文件夹中。我们将组织它们,以便更容易处理。为此,我们将在Resources
目录中创建一个Image
文件夹。在这个新文件夹中,我们将放置所有的图片。在本章稍后,我们将解释如何根据 Android 设备屏幕分辨率来组织每个图片的不同版本。
在本章附带资源中,我们为你提供了构建本章代码所需的图片。
创建我们的暂停场景头文件
首先,让我们创建我们的暂停场景头文件。我们是参考HelloWorld.h
头文件创建它的:
#ifndef __Pause_SCENE_H__
#define __Pause_SCENE_H__
#include "cocos2d.h"
class Pause : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
void exitPause(cocos2d::Ref* pSender);
CREATE_FUNC(Pause);
private:
cocos2d::Director *_director;
cocos2d::Size _visibleSize;
};
#endif // __Pause_SCENE_H__
提示
你可以通过输入using namespace cocos2d
来避免每次引用cocos2d
命名空间中的 Cocos2d-x 类时输入cocos2d
,然而,在头文件中使用它被认为是一个坏习惯,因为当包含的命名空间中有重复的字段名时,代码可能无法编译。
创建暂停场景实现文件
现在,让我们创建我们的暂停场景实现文件。类似于前一部分的做法,我们将基于项目创建脚本生成的HelloWorld.cpp
文件来创建这个文件。
在以下代码中,你会发现 Cocos2d-x 模板项目中捆绑的菜单创建代码。我们将在本章的后续部分解释如何创建游戏菜单,你还将学习字体创建,这将在第五章《处理文本和字体》中详细解释。
#include "PauseScene.h"
USING_NS_CC;
Scene* Pause::createScene()
{
auto scene = Scene::create();
auto layer = Pause::create();
scene->addChild(layer);
return scene;
}
bool Pause::init()
{
if ( !Layer::init() )
{
return false;
}
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
Vec2 origin = _director->getVisibleOrigin();
auto pauseItem = MenuItemImage::create("play.png", "play_pressed.png", CC_CALLBACK_1(Pause::exitPause, this));
pauseItem->setPosition(Vec2(origin.x + _visibleSize.width -pauseItem->getContentSize().width / 2, origin.y + pauseItem->getContentSize().height / 2));
auto menu = Menu::create(pauseItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
auto label = Label::createWithTTF("PAUSE", "fonts/Marker Felt.ttf", 96);
label->setPosition(origin.x + _visibleSize.width/2, origin.y + _visibleSize.height /2);
this->addChild(label, 1);
return true;
}
void Pause::exitPause(cocos2d::Ref* pSender){
/*Pop the pause scene from the Scene stack.
This will remove current scene.*/
Director::getInstance()->popScene();
}
在生成的HelloWorldScene.h
场景中,我们现在在menuCloseCallback
方法定义后添加以下代码行:
void pauseCallback(cocos2d::Ref* pSender);
现在,让我们在HelloWorldScene.cpp
实现文件中为pauseCallBack
方法创建实现:
void HelloWorld::pauseCallback(cocos2d::Ref* pSender){
_director->pushScene(Pause::createScene());
}
最后,通过使closeItem
调用pauseCallBack
方法而不是menuCloseCallBack
方法来修改其创建,这样这行代码将看起来像这样:
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
现在,我们已经创建了一个简单的暂停场景,当按下关闭按钮时,它会被推送到场景堆栈中,当从暂停场景中按下蓝色按钮时,它会被关闭。
现在,我们将PauseScene.cpp
文件添加到 eclipse 项目中jni
文件夹下的名为Android.mk
的 Android makefile 中,位于LOCAL_SRC_FILES
部分的HelloWorldScene.cpp
上方。
过渡
导演还负责在切换场景时播放过渡效果,Cocos2d-x 3.4 目前提供超过 35 种不同的场景过渡效果,例如渐变、翻转、翻页、分裂和缩放等。
Transition 是Scene
类的子类,这意味着你可以将过渡实例传递给任何接收场景对象的方法,如director
类的runWithScene
、replaceScene
或pushScene
方法。
当从游戏场景切换到暂停场景时,让我们使用一个简单的过渡效果。我们只需通过创建TransitionFlipX
类的新实例并将其传递给导演的pushScene
方法来实现这一点:
void HelloWorld::pauseCallback(cocos2d::Ref* pSender){
_director->pushScene(TransitionFlipX::create(1.0, Pause::createScene()));
}
理解节点
Node 表示屏幕上所有的可见对象,实际上它是所有场景元素的超类,包括场景本身。它是基础框架类,具有处理图形特性的基本方法,如位置和深度。
理解精灵
在我们的游戏中,精灵代表我们场景的图像,就像背景、敌人和我们的玩家。
在第四章《用户输入》中,我们将向场景添加事件监听器,使其能够与用户交互。
创建精灵
Cocos2d-x 的核心类实例化非常简单。我们已经看到scene
类有一个create
方法;同样,sprite
类也有一个同名静态方法,如下面的代码片段所示:
auto sprBomb = Sprite::create("bomb.png");
Cocos2d-x 目前支持 PNG、JPG 和 TIF 图像格式的精灵;然而,我们强烈建议使用 PNG 图像,因为它具有透明度能力,而 JPG 或 TIF 格式没有,同时也因为这种格式在合理的文件大小下提供的图像质量。这就是为什么你会看到所有 Cocos2d-x 生成的模板和示例都使用这种图像格式。
定位精灵
创建我们自己的精灵后,我们可以通过使用setPosition
方法轻松地在屏幕上定位它,但在这样做之前,我们将解释锚点的概念。
设置锚点
所有精灵都有一个称为锚点的参考点。当我们使用setPosition
方法定位一个精灵时,框架实际所做的是将指定的二维位置设置到锚点,从而影响整个图像。默认情况下,锚点被设置为精灵的中心,正如我们在以下图片中看到的:
理解 Cocos2d-x 坐标系统
与大多数计算机图形引擎不同,Cocos2d-x 的坐标系统在屏幕左下角有原点(0,0),正如我们在以下图片中看到的:
因此,如果我们想将精灵定位在原点(0,0),我们可以通过调用精灵类中的setPosition
方法来实现。它是重载的,所以它可以接收两个表示 x 和 y 位置的浮点数,一个Point
类实例,或者一个Vec2
实例。尽管生成的代码中使用Vec2
实例,但官方 Cocos2d-x 文档指出,传递浮点数最多可以快 10 倍。
sprBomb -> setPosition(0,0);
执行此代码后,我们可以看到只有精灵的右上区域可见,这仅占其大小的 25%,正如我们在以下图片中所示:
如果你希望精灵显示在原点,有多种方法可以选择,比如将精灵定位在对应精灵高度一半和宽度一半的点,这可以通过使用精灵方法getContentSize
来确定,它返回一个包含精灵高度和宽度属性的大小对象。另一个可能更简单的方法是将精灵的锚点重置为(0,0),这样当精灵在屏幕原点定位时,它完全可见并且位于屏幕左下角区域。《setAnchorPoint》方法接收一个Vec2
实例作为参数。在以下代码清单中,我们传递了一个指向原点(0,0)的Vec2
实例:
sprBomb -> setAnchorPoint(Vec2(0,0));
sprBomb -> setPosition(0,0);
注意
Vec2
类有一个不接受参数的构造函数,它会创建一个初始值为 0,0 的Vec2
对象。
当我们执行代码时,得到以下结果:
提示
默认锚点位于精灵中心的原因是,这样更容易将其定位在屏幕中心。
将精灵添加到场景中
在创建并定位了我们的精灵对象之后,我们需要使用addChild
方法将其添加到场景中,该方法包含两个参数:要添加到场景中的节点的指针和一个表示其在z轴位置的整数。z值最高的节点将显示在那些值较低的节点之上:
this->addChild(sprBomb,1);
现在让我们向HelloWorld
场景添加背景图像:我们将在init
方法中将炸弹定位在屏幕左下区域时所用的相同步骤来完成它:
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
我们已经在z位置为-1 的地方添加了背景,因此任何位置为 0 或更高的节点都将显示在背景之上,如下面的图片所示:
在可见区域外定位精灵
现在我们有一个位于屏幕底部的炸弹,它不会移动。我们现在将其定位在屏幕顶部中心区域,位于可见区域之外,这样当我们让这个精灵移动时,它看起来就像是下炸弹雨。
正如我们之前提到的,让我们将炸弹定位在可见区域内,然后在下一节中我们将使用动作功能使其向地面移动:
sprBomb->setPosition(_visibleSize.width / 2, _visibleSize.height + sprBomb->getContentSize().height/2);
我们移除了setAnchorPoint
语句;现在,炸弹拥有默认的锚点,并且我们修改了setPosition
语句,现在将其正好定位在可见区域内。
定位玩家精灵
现在让我们创建并定位我们的玩家精灵。
auto player = Sprite::create("player.png");
player->setPosition(_visibleSize.width / 2, _visibleSize.height* 0.23);
this->addChild(player, 0);
在之前的代码中,我们创建了一个玩家精灵。我们使用了默认的锚点,它直接指向图像的中心,并通过将其定位在屏幕宽度的一半和屏幕高度的 23%,使其水平居中,因为本章提供的背景图像是在这些比例下绘制的。我们以 z 值为 0 添加它,这意味着它将被显示在背景中。
现在让我们来处理炸弹,将其放置在可见区域内,然后在下一节中,我们将使用动作功能使其向地面移动:
sprBomb->setPosition(_visibleSize.width / 2, _visibleSize.height + sprBomb->getContentSize().height/2);
我们移除了setAnchorPoint
语句;现在,炸弹拥有默认的锚点,并且我们修改了setPosition
语句,现在将其放置在可见区域内。
在本章中,我们使用了许多图像,正如我们之前提到的,这些图像存储在我们的 Cocos2d-x 项目的Resources
文件夹中。你可以创建子文件夹来组织你的文件。
理解动作
我们可以轻松地让精灵执行具体的动作,如跳跃、移动、倾斜等。只需要几行代码就能让我们的精灵执行所需的动作。
移动精灵
我们可以通过创建一个MoveTo
动作,使精灵移动到屏幕的特定区域,然后让精灵执行该动作。
在下面的代码清单中,我们通过简单地编写以下代码行,使炸弹掉落到屏幕底部:
auto moveTo = MoveTo::create(2, Vec2(sprBomb->getPositionX(), 0 - sprBomb->getContentSize().height/2));
sprBomb->runAction(moveTo);
我们创建了一个moveTo
节点,它将把炸弹精灵移动到当前的横向位置,同时也会把它移动到屏幕底部直到不可见。为了实现这一点,我们让它移动到精灵高度负一半的 y 位置。由于锚点被设置为精灵的中心点,将其移动到其高度的负一半就足以让它移动到屏幕可见区域之外。
如你所见,它与我们的玩家精灵相撞,但炸弹只是继续向下移动,因为它仍然没有检测到碰撞。在下一章中,我们将为游戏添加碰撞处理。
注意
Cocos2d-x 3.4 拥有自己的物理引擎,其中包括一个易于检测精灵之间碰撞的机制。
如果我们想将精灵移动到相对于其当前位置的位置,我们可以使用MoveBy
类,它接收我们想要精灵在水平和垂直方向上移动多少的参数:
auto moveBy = MoveBy::create(2, Vec2(0, 250));
sprBomb->runAction(moveBy);
注意
你可以使用reverse
方法使精灵向相反方向移动。
创建序列
有时我们有一个预定义的动作序列,我们希望在代码的多个部分执行它,这可以通过序列来处理。顾名思义,它由一系列按预定义顺序执行的动作组成,如有必要可以反向执行。
在使用动作时经常使用序列,因此在序列中我们添加了moveTo
节点,然后是一个函数调用,该调用在移动完成后执行一个方法,这样它将允许我们从内存中删除精灵,重新定位它,或者在视频游戏中执行任何其他常见任务。
在以下代码中,我们创建了一个序列,首先要求炸弹移动到地面,然后请求执行moveFinished
方法:
//actions
auto moveFinished = CallFuncN::create(CC_CALLBACK_1(HelloWorld::moveFinished, this));
auto moveTo = MoveTo::create(2, Vec2(sprBomb->getPositionX(), 0 - sprBomb->getContentSize().height/2));
auto sequence = Sequence::create(moveTo, moveFinished, nullptr);
sprBomb->runAction(sequence);
请注意,在序列的末尾我们传递了一个nullptr
参数,所以当 Cocos2d-x 看到这个值时,它会停止执行序列中的项目;如果你不指定它,这可能会导致你的游戏崩溃。
注意
自从 3.0 版本以来,Cocos2d-x 建议使用nullptr
关键字来引用空指针,而不是使用传统的 NULL 宏,后者仍然有效,但不是在 C++中认为的最佳实践。
制作精灵动画
为了使我们的游戏看起来更加专业,我们可以使精灵具有动画效果,这样就不会一直显示静态图像,而是显示动画角色、敌人和障碍物。Cocos2d-x 提供了一种简单机制,可以将这类动画添加到我们的精灵中,如下面的代码清单所示:
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
sprPlayer->runAction(RepeatForever::create(animate));
使用精灵表提高性能
尽管我们可以基于位于多个文件中的图像创建精灵动画,就像我们在之前的代码中所做的那样,但加载大量文件将非常低效。这就是为什么我们更愿意加载包含多个图像的单个文件。为了实现这一点,一个带有plist
扩展名的纯文本文件指出了文件中每个图像的确切位置,Cocos2d-x 能够读取这个纯文本文件,并从一个单一的精灵表文件中提取所有图像。有许多工具可以让你创建自己的精灵表,最受欢迎的是纹理打包器,你可以从www.codeandweb.com/texturepacker
下载并在 Windows 或 Mac OS 上免费试用。
在本章中,我们包含的资源有:一个名为bunny.plist
的plist
文件和用纹理打包器创建的bunny_ss.png
精灵表。你可以使用以下代码加载此表的任何帧:
SpriteFrameCache* cache = SpriteFrameCache::getInstance();
cache->addSpriteFramesWithFile("bunny.plist");
auto sprBunny = Sprite::createWithSpriteFrameName("player3.png");
sprBunny -> setAnchorPoint(Vec2());
游戏菜单
在我们游戏的一部分中拥有菜单是很常见的,比如主屏幕和配置屏幕。这个框架为我们提供了一种简单的方法将菜单添加到游戏中。
下面的代码清单显示了菜单创建过程:
auto closeItem = MenuItemImage::create("pause.png", "CloseSelected.png", CC_CALLBACK_1(HelloWorld::pause_pressed, this));
closeItem->setPosition(Vec2(_visibleSize.width – closeItem->getContentSize().width/2 , closeItem-> getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
从前面的列表中我们可以看到,我们首先通过实例化MenuItemImage
类并传递三个参数给create
方法来创建一个菜单项:第一个参数表示菜单项应该显示的图像,第二个是选中图像时应该显示的图像,第三个参数指定当选择菜单项时应调用的方法。
注意
Cocos2d-x 分支 3 现在允许程序员使用 lambda 表达式来处理菜单项。
处理多屏幕分辨率
在创建游戏时,你需要决定打算支持哪些屏幕分辨率,然后创建所有图像的大小,使其在高分辨率屏幕上不会显得像素化,在低性能设备上加载时也不会影响性能。所有这些版本的图像应该有相同的名称,但它们应该存储在Resources
文件夹中的不同目录里。
在这个例子中,我们有三个目录:第一个包含高分辨率的图像,第二个包含中等分辨率的图像,第三个包含低分辨率的图像。
在准备好适合所有分辨率需求的所有图像大小之后,我们必须编写根据设备屏幕分辨率选择正确图像集的代码。正如我们之前提到的,AppDelegate
类包含applicationDidFinishLaunching
,该函数在 Cocos2d-x 框架在设备上加载后立即启动。在这个方法中,我们将编写多屏幕分辨率的代码,如下面的代码清单所示:
bool AppDelegate::applicationDidFinishLaunching() {
auto director = Director::getInstance();
// OpenGL initialization done by cocos project creation script
auto glview = director->getOpenGLView();
Size screenSize = glview->getFrameSize();
Size designSize = CCSizeMake(768, 1280);
std::vector<std::string> searchPaths;
if (screenSize.height > 800){
//High Resolution
searchPaths.push_back("images/high");
director->setContentScaleFactor(1280.0f / designSize.height);
}
else if (screenSize.height > 600){
//Mid resolution
searchPaths.push_back("images/mid");
director->setContentScaleFactor(800.0f / designSize.height);
}
else{
//Low resolution
searchPaths.push_back("images/low");
director->setContentScaleFactor(320.0f / designSize.height);
}
FileUtils::getInstance()->setSearchPaths(searchPaths);
glview->setDesignResolutionSize(designSize.width, designSize.height, ResolutionPolicy::NO_BORDER );
auto scene = HelloWorld::createScene();
director->runWithScene(scene);
return true;
}
通过将AndroidManifest.xml
文件中的android:screenOrientation
值设置为portrait
来进行修改。
将所有内容整合到一起
这是HelloWorldScene.cpp
实现文件的完整代码,我们在其中创建并定位了背景、动画玩家和移动的炸弹:
#include "HelloWorldScene.h"
#include "PauseScene.h"
USING_NS_CC;
Scene* HelloWorld::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = HelloWorld::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
接下来在init
函数中,我们将实例化和初始化我们的精灵:
bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(_visibleSize.width - closeItem->getContentSize().width/2 ,
closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
auto sprBomb = Sprite::create("bomb.png");
sprBomb->setPosition(_visibleSize.width / 2, _visibleSize.height + sprBomb->getContentSize().height/2);
this->addChild(sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2::Zero);
bg->setPosition(0,0);
this->addChild(bg, -1);
auto sprPlayer = Sprite::create("player.png");
sprPlayer->setPosition(_visibleSize.width / 2, _visibleSize.height * 0.23);
this->addChild(sprPlayer, 0);
接下来,我们将使用以下代码添加动画:
Vector<SpriteFrame*> frames;
Size playerSize = sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
sprPlayer->runAction(RepeatForever::create(animate));
在这里,我们将创建一个序列,该序列将把炸弹从屏幕顶部移动到底部。移动完成后,我们将指定调用moveFinished
方法。我们只是出于测试目的使用它来打印一条日志信息:
//actions
auto moveFinished = CallFuncN::create(CC_CALLBACK_1(HelloWorld::moveFinished, this));
auto moveTo = MoveTo::create(2, Vec2(sprBomb->getPositionX(), 0 - sprBomb->getContentSize().height/2));
auto sequence = Sequence::create(moveTo, moveFinished, nullptr);
sprBomb->runAction(sequence);
return true;
}
void HelloWorld::moveFinished(Node* sender){
CCLOG("Move finished");
}
void HelloWorld::pauseCallback(cocos2d::Ref* pSender){
_director->pushScene(TransitionFlipX::create(1.0, Pause::createScene()));
}
下图展示了在本章中完成所有代码后,我们的游戏看起来是什么样子:
总结
在本章中,我们了解了如何创建游戏场景,以及如何向其中添加精灵和菜单。我们还学会了如何轻松地动画化精灵并在屏幕上移动它们。
在下一章中,我们将学习如何使用内置的物理引擎以更真实的方式移动我们的精灵;通过它,我们将轻松配置运动并为游戏添加碰撞检测。
第三章:理解游戏物理
在本章中,我们将介绍如何通过使用基于流行的 Chipmunk 框架的 Cocos2d-x 内置引擎,向游戏中添加物理效果。我们将解释以下主题:
-
设置物理世界
-
检测碰撞
-
处理重力
-
处理物理属性
有关 Chipmunk 物理引擎的更多信息,你可以访问chipmunk-physics.net
。
物理引擎封装了与给场景真实运动相关的所有复杂性,例如给物体添加重力使其被吸引到屏幕底部,或检测实体之间的碰撞等等。
在处理物理时,我们应该记住我们在场景中处理的是一个物理世界,所有参与世界的物理元素都被称为物理实体。这些实体具有质量、位置和旋转等属性。这些属性可以更改以自定义实体。一个物理实体可以通过关节定义附着在另一个实体上。
需要注意的是,从物理学的角度来看,物理实体并不知道物理世界外的精灵和其他对象,但我们将在这章中看到如何将精灵与物理实体连接起来。
视频游戏最常见的特征之一是碰撞检测;我们经常需要知道物体何时与其他物体发生碰撞。这可以通过定义代表每个实体碰撞区域的形状轻松完成,然后指定一个碰撞监听器,我们将在本章后面展示如何操作。
最后,我们将介绍 Box2D 物理引擎,这是一个完全独立的物理引擎,与 Chipmunk 无关。Box2D 是用 C++编写的,而 Chipmunk 是用 C 编写的。
设置物理世界
为了在游戏中启用物理,我们需要向我们的HelloWorldScene.h
头文件中添加以下几行:
cocos2d::Sprite* _sprBomb;
void initPhysics();
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
在这里,我们为_sprBomb
变量创建了一个实例变量,这样它就可以被所有实例方法访问。在这种情况下,我们希望能够在每次检测到物理实体之间的碰撞时调用的onCollision
方法中访问炸弹实例,这样我们只需将它的可见属性设置为 false,就可以让炸弹消失。
现在,让我们转到我们的HelloWorld.cpp
实现文件,并进行一些更改以设置我们的物理世界。
首先,让我们修改我们的createScene
方法,现在它看起来应如下所示:
Scene* HelloWorld::createScene()
{
auto scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setGravity(Vect(0,0));
auto layer = HelloWorld::create();
//enable debug draw
scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
scene->addChild(layer);
return scene;
}
注意
在 Cocos2d-x 3 分支的早期版本中,你需要指定当前场景层将要使用的物理世界。但在 3.4 版本中,这不再必要,Layer
类中移除了setPhysicsWorld
方法。
在这里,我们可以看到我们现在是通过Scene
类中的createWithPhysics
静态方法创建场景实例,而不是使用简单的创建方法。
我们接下来要进行的第二步是将重力设置为(0,0),这样物理世界的重力就不会将我们的精灵吸引到屏幕底部。
然后,我们将启用物理引擎的调试绘制功能,这样我们就能看到所有的物理实体。这个选项将有助于我们在开发阶段,我们将使用 COCOS2D_DEBUG 宏,使其仅在调试模式下运行时显示调试绘制,如下所示:
#if COCOS2D_DEBUG
scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
#endif
在以下屏幕截图中,我们可以看到围绕炸弹和玩家精灵的红色圆形。这表示附加到每个玩家精灵的物理实体:
现在,让我们实现我们的setPhysicsBody
方法,它接收一个指向我将添加物理实体的精灵对象的指针作为参数。此方法将创建一个表示物理实体和碰撞区域的圆形。该圆形的半径将是精灵宽度的一半,以尽可能覆盖精灵的面积。
void HelloWorld::setPhysicsBody(cocos2d::Sprite* sprite){
auto body = PhysicsBody::createCircle(sprite->getContentSize().width/2);
body->setContactTestBitmask(true);
body->setDynamic(true);
sprite -> setPhysicsBody(body);
}
注意
圆形通常用于检测碰撞,因为它们在每个帧中检测碰撞所需的 CPU 努力较小;然而,在某些情况下,它们的精度可能无法接受。
现在,让我们在init
方法中为我们的玩家和炸弹精灵添加物理实体。为此,我们将在每个精灵初始化后调用我们的实例方法 setPhysicsBody。
碰撞检测
首先,让我们实现我们的onCollision
实例方法。每次检测到两个物理实体之间的碰撞时,都会调用它。正如在下面的代码中我们可以看到,当炸弹物理实体与我们的玩家碰撞时,它使炸弹变得不可见:
bool HelloWorld::onCollision(PhysicsContact& contact){
_sprBomb->setVisible(false);
return false;
}
注意
在开发过程中,这里是一个放置一些日志的好地方,以了解何时检测到碰撞。在 Cocos2d-x 3.4 中,你可以使用CCLOG
宏打印日志消息。通过以下方式定义宏COCOS2D_DEBUG
可以开启它:#define COCOS2D_DEBUG 1
。
如我们所见,这个方法返回一个布尔值。它表示这两个实体是否可以再次碰撞。在这个特定的情况下,我们将返回 false,表示一旦这两个物理实体碰撞,它们就不应该继续碰撞。如果我们返回 true,那么这两个对象将继续碰撞,这将导致我们的玩家精灵移动,从而给我们的游戏带来不希望出现的视觉效果。
现在,让我们使我们的游戏能够在炸弹与玩家碰撞时检测到。为此,我们将创建一个EventListenerPhysicsContact
实例,我们将设置它,以便当两个物理体开始碰撞时,它应该调用我们的onCollision
实例方法。然后,我们将事件监听器添加到事件分发器中。我们将在initPhysics
实例方法中创建这三个简单步骤。所以,我们的代码将如下所示:
void HelloWorld::initPhysics()
{
auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onCollision,this);
getEventDispatcher() ->addEventListenerWithSceneGraphPriority(contactListener,this);
}
我们的init
方法的代码将如下所示:
bool HelloWorld::init() {
if( !Layer::init() ){
return false;
}
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(_visibleSize .width - closeItem->getContentSize().width/2, closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(_visibleSize .width/2, _visibleSize .height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
auto sprPlayer = Sprite::create("player.png");
sprPlayer->setPosition(_visibleSize .width / 2, _visibleSize .height * 0.23);
setPhysicsBody(sprPlayer);
this->addChild(sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
return true;
}
处理重力
既然我们已经成功使用内置物理引擎来检测碰撞,那么让我们来玩弄一下重力。转到createScene
方法,并修改我们发送到构造函数的参数。在我们的游戏中,我们使用了(0,0)
值,因为我们不希望我们的世界有任何在x或y轴上移动我们物体的重力。
现在,尝试一下,将值改为正数或负数。当我们在x轴上使用负值时,它会将物体吸引向左,而在y轴上使用负值时,它会将物体吸引向下。
注意
改变这些值并理解添加到我们游戏中的物理可能会为你的下一个游戏提供一些想法。
处理物理属性
既然我们已经创建了对应于物理世界的场景,我们现在有能力改变物理属性,比如每个物体的速度、线性阻尼、力、冲量和扭矩。
应用速度
在上一章中,我们设法使用MoveTo
动作将炸弹从屏幕顶部移动到底部。现在我们使用了内置的物理引擎,只需为炸弹设置速度就能实现同样的效果。这可以通过简单地调用炸弹精灵物理体的setVelocity
方法来完成。速度是一个矢量量;因此,提到的方法接收一个Vect
实例作为参数。x的值表示其水平分量;在这个轴上,正值意味着物体将向右移动,负值意味着物体将向左移动。y值影响垂直运动。正值将物体向屏幕顶部移动,负值将物体向屏幕底部移动。
我们在HelloWorld.cpp
实现文件的init
方法中,在返回语句之前添加了以下行:
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
记得删除请求炸弹精灵执行MoveTo
动作的代码行,以便确认炸弹现在是因为其速度参数而移动。
现在让我们转到onCollision
方法,当炸弹与我们的玩家精灵发生碰撞时,我们将把炸弹的速度设置为零。
_sprBomb -> getPhysicsBody()->setVelocity(Vect());
注意
与Vec2
类相似,空构造函数会将所有向量值初始化为零。
线性阻尼
我们可以降低物理体的速度,以产生摩擦效果。实现这一目标的方法之一是调用linearDamping
方法,并指定身体速度的变化率。该值应该是一个介于0.0
和1.0
之间的浮点数。
你可以通过将炸弹物理体的值设置为0.1f
来测试线性阻尼,并观察炸弹速度如何降低。
_sprBomb->getPhysicsBody()->setLinearDamping(0.1f);
测试线性阻尼后,记得记录或删除这行代码,以防止游戏出现预期之外的行为。
应用力
我们可以通过简单地调用想要施加力的物理体的applyForce
方法,来立即对物体施加力。与前面章节中解释的方法类似,它接收一个向量作为参数,这意味着力有垂直和水平分量。
我们可以通过在onCollision
方法中给炸弹施加一个力来测试这个方法,使它在与玩家精灵碰撞后立即向右移动。
_sprBomb->getPhysicsBody()->applyForce(Vect(1000,0));
应用冲量
在上一节中,我们给物理体添加了一个即时力,现在我们可以通过调用applyImpulse
方法对其施加一个连续力。
在onCollision
方法中对物理体施加即时力之后,添加以下代码行:
_sprBomb->getPhysicsBody()->applyImpulse(Vect(10000,0));
现在运行游戏,你将看到炸弹向右移动。
删除在onCollision
方法中给炸弹添加力和冲量的代码行。
应用扭矩
最后,让我们在炸弹与玩家精灵碰撞后,使炸弹旋转。我们可以通过使用applyTorque
方法给炸弹的物理体施加一个扭矩力,该方法接收一个浮点数;如果是正数,它将使物理体逆时针旋转。
让我们在onCollision
方法中的返回语句之前,添加一个任意的正扭矩:
auto body = _sprBomb -> getPhysicsBody();
body->applyTorque(100000);
现在给applyTorque
方法添加一个负值,你将看到物理体如何顺时针旋转。
把所有东西放在一起
经过所有修改后,我们的onCollision
方法看起来像这样:
bool HelloWorld::onCollision(PhysicsContact& contact){
auto body = _sprBomb -> getPhysicsBody();
body->setVelocity(Vect());
body->applyTorque(100900.5f);
return false;
}
我们现在的init
方法看起来像这样:
bool HelloWorld::init()
{
if( !Layer::init() ){
return false;
}
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("CloseNormal.png", "CloseSelected.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(_visibleSize .width - closeItem->getContentSize().width/2, closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(_visibleSize .width/2, _visibleSize .height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
auto sprPlayer = Sprite::create("player.png");
sprPlayer->setPosition(_visibleSize .width/2, _visibleSize .height * 0.23);
setPhysicsBody(sprPlayer);
this->addChild(sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
return true;
}
下面的截图展示了我们对游戏进行修改后的样子:
提示
Box2D 物理引擎
到目前为止,我们使用了框架提供的内置物理引擎,该引擎基于 chipmunk C 物理库;然而,Cocos2d-x 也在其 API 中集成了 Box2D 物理引擎。
为了创建一个 Box2D 世界,我们实例化b2World
类,然后向其构造函数传递一个表示世界重力的b2Vec
对象。世界实例有一个用于创建b2Bodies
的实例方法。Sprite 类有一个名为setB2Body
的方法,它允许我们将 Box2D 物理体关联到任何给定的精灵。这比框架的第二个分支中的处理要平滑得多;之前需要更多的代码才能将b2Body
与精灵绑定。
尽管 Box2D 集成使用起来很方便,但我强烈建议使用内置的物理引擎,因为 Box2D 集成已不再积极开发中。
总结
我们通过创建物理世界和代表炸弹及玩家精灵的物理体,向游戏中添加了物理效果,并且在很少的步骤内使用了内置物理引擎提供的碰撞检测机制。我们还展示了如何更改重力参数,以便物理体根据重力力量移动。我们可以轻松地更改物体的物理属性,例如速度、摩擦力、力、冲量和扭矩,每个属性只需一行代码。到目前为止,我们的玩家忽略了用户事件。在下一章中,我们将介绍如何向游戏中添加用户交互。
第四章:用户输入
到目前为止,我们已经添加了在屏幕上移动并相互碰撞的图形,但这还不够有趣,因为玩家无法控制我们的主角,除非用户能够与之互动,否则它就不能算是一个游戏。在本章中,我们将向游戏中添加用户交互。本章将涵盖以下主题:
-
理解事件分发机制
-
处理触摸事件
-
处理加速计事件
-
保持屏幕活跃
-
处理 Android 返回键按下事件
理解事件分发机制
从 Cocos2d-x 的先前版本(版本 2)开始,事件处理现在有所不同。从 3.0 版本开始,我们现在有一个统一的事件分发机制,称为事件分发器,它处理游戏中可能发生的各种用户输入事件。
我们可以处理多种类型的用户输入事件,例如触摸、键盘按键按下、加速度和鼠标移动。在以下各节中,我们将介绍如何处理与移动游戏相关的用户输入事件,例如触摸和加速计。
有许多类允许我们监听之前提到的事件;一旦我们实例化了这些类中的任何一个,我们需要将其添加到事件分发器中,以便在触发用户事件时,它会调用相应监听器定义的方法。
你可以通过从Node
类继承的_eventDispatcher
实例属性访问事件分发器,或者调用位于 Cocos2d-x API Director
类中的getEventDispatcher
静态方法。
注意
Cocos2d-x 的事件分发机制使用了观察者设计模式,这是用于处理 Android 原生应用程序上用户输入事件的模式。
处理触摸事件
在游戏和用户之间创建交互的最常见方式是通过触摸事件。在 Cocos2d-x 中处理触摸事件非常直接。
在本节中,我们将允许用户通过触摸并移动到所需位置来移动我们的玩家精灵。
我们首先要做的是在HelloWorldScene.h
类头文件中创建initTouch
、movePlayerByTouch
和movePlayerIfPossible
方法,正如我们以下代码清单中看到的那样:
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
现在让我们将初始化代码添加到实现文件HelloWorldScene.cpp
中的initTouch
方法中。在这个简单的游戏中,我们将使用单一触摸来四处移动我们的兔子角色,不需要处理多点触控。
为了处理单一触摸,我们将创建EventListenerTouchOneByOne
类的新实例,然后我们将指定当触摸事件开始、移动和结束时游戏应该做什么。在以下代码清单中,实例化EventListenerTouchOneByOne
类之后,我们将指定当触发onTouchBegan
、onTouchMoved
和onTouchEnded
事件时应调用的方法。出于当前游戏的目的,我们只使用onTouchMoved
事件。为此,我们将创建一个回调到我们的方法movePlayerByTouch
,对于另外两个方法,我们将通过 lambda 函数创建空的结构。你可以通过链接en.cppreference.com/w/cpp/language/lambda
了解更多关于 C++ lambda 函数的信息。
void HelloWorld::initTouch() {
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = [](Touch* touch, Event* event){return true;
}
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::movePlayerByTouch,this);
listener->onTouchEnded = ={};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
注意
按照约定,所有的 C++成员变量都使用下划线前缀命名。
既然我们已经将所有触摸监听器初始化代码封装到一个方法中,让我们在方法的末尾添加以下行来调用它,我们称之为init
方法:
initTouch();
我们现在将创建movePlayerIfPossible
方法。这个方法只会在水平轴上的新请求位置没有超出屏幕限制时移动玩家精灵,正如我们可以在插图中看到的那样。这个方法将用于通过触摸输入事件移动我们的玩家精灵,并且在下一节中我们也将使用它通过加速度计移动我们的玩家精灵。
void HelloWorld::movePlayerIfPossible(float newX){
float sprHalfWidth = _sprPlayer->getBoundingBox().size.width/2;
if(newX >= sprHalfWidth && newX < visibleSize.width - sprHalfWidth){
_sprPlayer->setPositionX(newX);
}
}
注意
在这个方法中,我们采用了“告诉,不要询问”的设计原则,通过在验证玩家是否超出屏幕的方法中进行验证。这使我们避免了在触摸和加速度事件处理方法中重复验证玩家精灵是否超出屏幕的逻辑。
最后,我们现在将创建movePlayerByTouch
方法,该方法将在触发触摸事件后立即由事件调度器调用。在这个方法中,我们将评估屏幕上的位置,以及用户触摸屏幕的地方是否与精灵的边界矩形相交:
void HelloWorld::movePlayerByTouch(Touch* touch, Event* event){
auto touchLocation = touch->getLocation();
if(_sprPlayer->getBoundingBox().containsPoint(touchLocation)){
movePlayerIfPossible(touchLocation.x);
}
}
处理多点触控事件
在前面的部分中,我们启用了这个游戏所需的触摸事件,这是一个单一触摸;然而,Cocos2d-x 也处理多点触控功能,我们将在本节中介绍。
尽管我们的游戏不需要多点触控功能,但我们将创建一个测试代码,以便我们可以同时移动我们的玩家精灵和炸弹。为了做到这一点,我们将在HelloWorldScene.h
头文件的末尾添加initMultiTouch
和moveByMultitouch
方法,如下所示:
void initMultiTouch();
void moveByMultiTouch(const std::vector<cocos2d::Touch*>& touches, cocos2d::Event* event);
现在,让我们将实现添加到HelloWorldScene.cpp
实现文件中。我们将从initMultiTouch
初始化方法开始:
void HelloWorld::initMultiTouch() {
auto listener = EventListenerTouchAllAtOnce::create();
listener->onTouchesBegan = [](const std::vector<Touch*>& touches, Event* event){};
listener->onTouchesMoved = CC_CALLBACK_2(HelloWorld::moveByMultiTouch,this);
listener->onTouchesEnded = [](const std::vector<Touch*>& touches, Event* event){};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
在这里,我们可以找到与之前单点触控初始化方法的相似之处,但有许多不同之处,最显著的是我们现在实例化的是EventListenerTouchAllAtOnce
类,而不是像之前那样实例化EventListenerTouchOneByOne
类。尽管它的事件属性与单点触控版本命名相似,但您可能注意到它们现在是用复数形式书写的,因此现在指的是 touches 而不是 touch,例如onTouchesBegan
。现在,它也将期待一组不同的参数,因为我们将处理多点触控,事件方法将接收一个std::vector
参数,其中包含同时发生的触控集合。
如之前的代码所示,每当玩家移动触控时,我们将调用我们的moveByMultiTouch
方法,因此我们现在展示此方法的实现代码:
void HelloWorld::moveByMultiTouch(const std::vector<Touch*>& touches, Event* event){
for(Touch* touch: touches){
Vec2 touchLocation = touch->getLocation();
if(_sprPlayer->getBoundingBox().containsPoint(touchLocation)){
movePlayerIfPossible(touchLocation.x);
}else if(_sprBomb->getBoundingBox().containsPoint(touchLocation)){
_sprBomb->setPosition(touchLocation);
}
}
}
如您在前面的代码中所见,我们现在正在处理多点触控,在moveByMultiTouch
方法中,我们正在遍历所有的触控,并对每一个触控进行验证,看它是否触摸到我们的炸弹或兔子玩家精灵,如果是,那么它将把被触摸的精灵移动到触摸位置。
最后,让我们在init
方法的末尾调用initMultiTouch
初始化方法,如下所示:
initMultiTouch();
如前所述,本节的目的是向您展示处理多点触控事件是多么简单;然而,由于我们的游戏中不会使用它,一旦您完成多点触控功能的测试,就可以从我们的init
方法中删除对initMultiTouch
方法的调用。
处理加速度计事件
游戏与玩家之间互动的另一种常见方式是加速度计,它允许我们通过移动手机来移动我们的角色,以达到游戏的目标,从而获得数小时的乐趣。
为了将加速度计支持添加到我们的游戏中,我们首先将在HelloWorldScene.h
头文件中添加以下方法声明:
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
现在,让我们为对应的加速度计初始化创建HelloWorld.cpp
实现文件中的代码。我们首先要做的是通过调用Device
类上的静态方法setAccelerometerEnabled
来启用设备上的加速度计传感器,然后我们将创建一个事件监听器来监听加速度计的事件,最后,我们将它添加到事件分发器中,如下面的代码所示:
void HelloWorld::initAccelerometer(){
Device::setAccelerometerEnabled(true);
auto listener = EventListenerAcceleration::create(CC_CALLBACK_2(HelloWorld::movePlayerByAccelerometer, this));
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
注意
向分发器添加事件监听器的最常见方式是通过addEventListenerWithSceneGraphPriority
方法,该方法将把作为第二个参数传递的节点的z顺序作为其优先级。当我们有多个同时被触发的监听器,而想要指定哪个代码应该首先运行时,这非常有用。
在此阶段,我们已经初始化了加速度计,并且在上一节中创建了movePlayerIfPossible
方法,该方法将移动玩家精灵,并确保其不会超出屏幕限制。现在我们将要为movePlayerByAccelerometer
方法编写实现代码,该方法将在加速度计事件触发后立即被调用。由于我们获得的加速度值非常低,因此我们将其乘以十,以便我们的玩家精灵移动得更快。
void HelloWorld::movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event){
int accelerationMult = 10;
movePlayerIfPossible(_sprPlayer->getPositionX() + (acceleration->x * accelerationMult));
}
最后,让我们在HelloWorldScene.cpp
实现文件的init
方法末尾调用我们的加速度计初始化代码,如下所示:
initAccelerometer();
保持屏幕常亮
在上一节中,我们向游戏中添加了加速度计交互,这意味着我们的玩家是通过移动手机而不是触摸屏幕来控制主角的,这将导致许多 Android 设备在一段时间不活动(不触摸屏幕)后关闭屏幕。当然,没有人希望我们的 Android 设备的屏幕突然变黑;为了防止这种情况发生,我们将调用setKeepScreenOnJni
方法,这是在框架的前一个版本 3.3 中引入的。在此版本之前,这种恼人的情况被认为是框架的缺陷,现在终于得到了修复。
首先,我们需要在HelloWorldScene.cpp
头文件中包含助手,如下所示:
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"
然后,我们将在HelloWorldScene.cpp
实现文件的init
方法末尾添加以下行:
setKeepScreenOnJni(true);
处理 Android 后退键按下事件
我在很多使用 Cocos2d-x 开发的游戏中看到的一个常见错误是,当按下后退按钮时游戏不做任何反应。Android 用户习惯于在想要返回上一个活动时按下后退按钮。如果应用程序在按下后退按钮时没有任何反应,那么它会让用户感到困惑,因为这不符合预期行为,经验不足的用户甚至可能很难退出游戏。
我们可以通过向事件分发器中添加EventListenerKeyboard
方法,轻松地在用户按下后退按钮时触发自定义代码。
首先,我们将在HelloWorldScene.h
头文件中添加initBackButtonListener
和onKeyPressed
方法的声明,如下代码清单所示:
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
现在,让我们在HelloWorldScene.cpp
实现文件中添加initBackButtonListener
的实现代码。我们首先实例化EventListenerKeyboard
类,然后需要指定在onKeyPressed
事件和onKeyReleased
事件发生时要调用的方法,否则我们将遇到运行时错误。我们将创建一个空的方法实现,并通过 C++11 的 lambda 表达式将其分配给onKeyPressed
属性,然后我们将为监听器的onKeyReleased
属性添加一个回调到我们的onKeyPressed
方法。然后,像之前所做的那样,我们将这个监听器添加到事件分发机制中:
void HelloWorld::initBackButtonListener(){
auto listener = EventListenerKeyboard::create();
listener->onKeyPressed = ={};
listener->onKeyReleased = CC_CALLBACK_2(HelloWorld::onKeyPressed, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
我们现在将实现onKeyPressed
方法的代码。这将告诉Director
,如果按下的键是后退按钮键,则结束游戏:
void HelloWorld::onKeyPressed(EventKeyboard::KeyCode keyCode, Event* event){
if(keyCode == EventKeyboard::KeyCode::KEY_BACK){
Director::getInstance()->end();
}
}
最后,我们将在init
方法的末尾调用initBackButtonListener
方法,如下所示:
initBackButtonListener();
注意
请注意,你应该在每个想要捕获后退按钮按下事件的场景中,将EventListenerKeyboard
监听器添加到事件分发器中。
将所有内容整合到一起
在本章中添加了所有代码之后,现在我们的HelloWorldScene.h
头文件将如下所示:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
class HelloWorld : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
void pauseCallback(cocos2d::Ref* pSender);
CREATE_FUNC(HelloWorld);
private:
cocos2d::Director *_director;
cocos2d::Size visibleSize;
cocos2d::Sprite* _sprBomb;
cocos2d::Sprite* _sprPlayer;
void initPhysics();
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
};
#endif // __HELLOWORLD_SCENE_H__
最终的HelloWorldScene.cpp
实现文件将如下所示:
#include "HelloWorldScene.h"
#include "PauseScene.h"
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"
USING_NS_CC;
Scene* HelloWorld::createScene()
{
auto scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setGravity(Vect(0,0));
auto layer = HelloWorld::create();
//enable debug draw
//scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
scene->addChild(layer);
return scene;
}
接下来,我们将尝试移动玩家,使其不会移出屏幕外:
void HelloWorld::movePlayerIfPossible(float newX){
float sprHalfWidth = _sprPlayer->getBoundingBox().size.width/2;
if(newX >= sprHalfWidth && newX < visibleSize.width - sprHalfWidth){
_sprPlayer->setPositionX(newX);
}
}
void HelloWorld::movePlayerByTouch(Touch* touch, Event* event)
{
Vec2 touchLocation = touch->getLocation();
if(_sprPlayer->getBoundingBox().containsPoint(touchLocation)){
movePlayerIfPossible(touchLocation.x);
}
}
如你所见,在以下两个方法initTouch
和initAccelerometer
中,我们为每个初始化任务创建了函数。这将使我们能够简化代码,使其更容易阅读:
void HelloWorld::initTouch()
{
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = [](Touch* touch, Event* event){return true;};
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::movePlayerByTouch,this);
listener->onTouchEnded = ={};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
void HelloWorld::initAccelerometer()
{
Device::setAccelerometerEnabled(true);
auto listener = EventListenerAcceleration::create(CC_CALLBACK_2(HelloWorld::movePlayerByAccelerometer, this));
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
void HelloWorld::movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event)
{
movePlayerIfPossible(_sprPlayer->getPositionX() + (acceleration->x * 10));
}
现在,我们将初始化物理引擎。为此,我们将在init()
方法中调用initPhysics()
方法:
bool HelloWorld::init()
{
if( !Layer::init() ){
return false;
}
_director = Director::getInstance();
visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(visibleSize.width - closeItem->getContentSize().width/2, closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(visibleSize.width/2, visibleSize.height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
_sprPlayer = Sprite::create("player.png");
_sprPlayer->setPosition(visibleSize.width/2, visibleSize.height * 0.23);
setPhysicsBody(_sprPlayer);
this->addChild(_sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = _sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
_sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
initTouch();
initAccelerometer();
setKeepScreenOnJni(true);
initBackButtonListener();
return true;
}
void HelloWorld::pauseCallback(cocos2d::Ref* pSender){
_director->pushScene(TransitionFlipX::create(1.0, Pause::createScene()));
}
void HelloWorld::initBackButtonListener(){
auto listener = EventListenerKeyboard::create();
listener->onKeyPressed = ={};
listener->onKeyReleased = CC_CALLBACK_2(HelloWorld::onKeyPressed, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
void HelloWorld::onKeyPressed(EventKeyboard::KeyCode keyCode, Event* event){
if(keyCode == EventKeyboard::KeyCode::KEY_BACK){
Director::getInstance()->end();
}
}
如下图所示,我们最终让兔子在屏幕上移动并离开了它的初始中心位置。
注意
请注意,在之前的代码列表中,我们省略了与本章不相关的代码部分。如果你想了解到目前为止完整的代码列表是什么样子,你可以查看随书附带的资源材料。
总结
在本章中,我们允许用户通过两种不同的输入机制来控制游戏,即触摸屏幕和移动手机以使用加速度传感器;同时,我们还实现了按下后退按钮时游戏暂停的功能。
在下一章,我们将介绍向游戏中添加文本的不同方法。
第五章:处理文本和字体
在我们的游戏中,添加文本以向玩家显示信息是非常常见的。这可以通过使用 TrueType 字体或位图字体来完成,这将给我们带来更大的灵活性,实际上,这是专业游戏中使用最广泛的字体类型,因为它允许我们为游戏定制外观。本章将涵盖以下主题:
-
创建 TrueType 字体标签
-
添加标签效果
-
创建系统字体
-
创建位图字体标签
创建 TrueType 字体标签
使用 TrueType 字体添加文本非常简单。打开我们在第二章图形中创建的PauseScene.cpp
实现文件。在init
方法中,你会看到我们通过调用静态方法createWithTTF
创建了一个Label
类的实例。这个方法接收三个参数,第一个是我们想要绘制的字符串,第二个是表示你想要使用的字体文件的字符串,包括它在Resources
文件夹中的路径,第三个是字体大小。
注意
Label
类在 Cocos2d-x 3.x 版本中引入。它将 TrueType 字体和位图字体处理合并到一个单一类中。然而,尽管已弃用,为了兼容性,之前的标签处理类仍然在 API 中可用。
现在,让我们将createWithTTF
方法中的第三个参数值从 24 更改为 96,使字体变得更大:
auto label = Label::createWithTTF("PAUSE", "fonts/Marker Felt.ttf", 96);
注意
cocos new
命令生成的模板 Cocos2d-x 项目中包含了 Marker Felt 字体。
创建我们的 GameOverScene
现在是创建游戏结束场景的时候了,一旦炸弹与我们的bunny
精灵相撞,就会显示这个场景。
我们将通过复制Classes
目录中的PauseScene.cpp
和PauseScene.h
文件,并将它们分别重命名为GameOverScene.cpp
和GameOverScene.h
来完成这一操作。
提示
请记住,每次你向 Cocos2d-x 文件夹添加新的源文件时,都需要将类添加到jni
文件夹中的Android.mk
文件中,这样在下次构建时就会编译这个新的源文件。
现在,在GameOverScene.h
和GameOverScene.cpp
文件中,对这两个文件执行查找和替换操作,将单词Pause
替换为单词GameOver
。
最后,将GameOverScene.cpp
实现文件中的前几行代码替换为以下内容:
#include "GameOverScene.h"
#include "HelloWorldScene.h"
在GameOverScene.cpp
实现文件中的exitPause
方法体内,我们将用以下这行代码替换这个方法中的唯一一行:
Director::getInstance()->replaceScene(TransitionFlipX:: create(1.0, HelloWorld::createScene()));;
当玩家失败时调用我们的 GameOverScene
我们已经创建了游戏结束场景;现在让我们在炸弹与我们的player
精灵相撞时立即显示它。为了实现这一点,我们将在HelloWorld
类中的onCollision
方法中添加以下代码行。
_director->replaceScene(TransitionFlipX::create(1.0, GameOver::createScene()));
现在,通过在HelloWorldScene.h
头文件的开始处添加以下行,将游戏结束场景头文件包含到我们的gameplay
类中:
#include "GameOverScene.h"
自定义GameOverScene
现在,我们不希望有黑色背景,所以我们将添加我们在第二章图形中在游戏玩法中使用的相同背景:
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
现在,我们将更改从PauseScene
复制的 TrueType 字体标签,它现在将显示为Game Over
。在下一节中,我们将给这个标签添加一些效果。
auto label = Label::createWithTTF("Game Over", "fonts/Marker Felt. ttf", 96);
添加标签效果
现在,我们将添加仅适用于 TrueType 字体的效果。
让我们为我们的字体启用轮廓。Label
类的enableOutline
方法接收两个参数,一个Color4B
实例和一个整数,表示轮廓大小——数字越大,轮廓越粗:
label->enableOutline(Color4B(255, 0, 0, 100),6);
现在,让我们给字体添加一些发光效果:
label->enableGlow(Color4B(255, 0, 0, 255));
最后,让我们给标签添加阴影效果,目前所有三种标签类型都支持这一效果。
label->enableShadow();
你会从以下屏幕截图中注意到,效果相互重叠,所以请决定哪个效果看起来更好:
Color4B
构造方法接收四个参数。前三个是红、绿、蓝(RGB)分量,第四个是alpha
分量。这将允许我们添加一些透明度效果,其值可以从 0 到 255。标签实例不支持自定义效果,例如给文本中的每个单词着不同的颜色,为单个文本使用不同的字体,或者在标签中嵌入图像。
提示
如果你有兴趣在你的游戏中添加这些字体效果,你可以使用 Luma Stubma 创建的CCRichLabelTTF
类。这可以在github.com/stubma/cocos2dx-better
找到。
创建系统字体
你可以创建使用宿主操作系统的字体的标签;因此,不需要提供字体文件。建议只将这种标签用于测试目的,因为它会降低框架的灵活性,因为选定的字体可能不在用户的 Android 操作系统版本上可用。
为了测试,在我们当前文本下方,我们将在GameOverScene.cpp
实现文件的init
方法中添加以下标签:
auto label2 = Label::createWithSystemFont("Your score is", "Arial", 48);
label2->setPosition(origin.x + visibleSize.width/2,origin.y + visibleSize.height /2.5);
this->addChild(label2, 1);
这段代码产生了以下结果:
创建位图字体标签
到目前为止,我们已经看到了如何通过使用 TrueType 和系统字体轻松创建标签,现在我们将执行一些额外步骤,以使我们的标签具有更专业的风格。如前所述,位图字体是专业游戏中最常用的标签类型。
如其名称所示,位图字体是由代表每个字符的图像生成的,这将允许我们绘制任何我们想要的字体,但它将具有位图的所有缺点,例如标签可能被像素化的风险,处理不同尺寸时的灵活性不足,以及处理这类字体所需的磁盘和 RAM 额外空间。
有多种应用程序可用于创建位图字体。最常见的是Glyph Designer,你可以在71squared.com
获取它。这个应用程序最初是为 Mac OS 发布的,但在 2015 年初,也为 Windows 发布了Glyph Designer X。你还可以使用免费的在线应用程序Littera来创建自己的位图字体。它可以在kvazars.com/littera
找到。为了本书的需要,我们在章节中包含了位图字体的代码。我们将使用这个位图字体代码在游戏结束场景中显示玩家的总分。
向我们的游戏中添加更多炸弹
考虑到现在我们有一个游戏结束场景,让我们通过添加更多炸弹使这个游戏变得稍微困难一些。我们将使用 Cocos2d-x 调度器机制,它将允许我们在每个给定的时间段内调用一个方法。我们将addBombs
方法添加到HelloWorldScene
类中,并在前述类的init
方法内调度它,使其每八秒被调用一次:
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addBombs), 8.0f);
我们将向场景中添加三个随机位置的炸弹,每次调用addBombs
方法时都会发生这种情况:
void HelloWorld::addBombs(float dt)
{
Sprite* bomb = nullptr;
for(int i = 0 ; i < 3 ; i++)
{
bomb = Sprite::create("bomb.png");
bomb->setPosition(CCRANDOM_0_1() * visibleSize.width, visibleSize.height + bomb->getContentSize().height/2);
this->addChild(bomb,1);
setPhysicsBody(bomb);
bomb->getPhysicsBody()->setVelocity(Vect(0, ( (CCRANDOM_0_1() + 0.2f) * -250) ));
}
}
这段代码产生了以下结果:
注意
使用CC_SCHEDULE_SELECTOR
宏,我们创建了一个自定义选择器,在这种情况下称为自定义时间间隔。所选函数应该接收一个float
参数,代表自上次调用和当前调用之间经过的时间,以便你可以独立于硬件处理速度计算统一的游戏节奏。如果你没有将第二个float
参数传递给调度函数,那么它将在每个帧中执行所选函数。
在场景中,我们还将向调度器添加另一个方法,该方法每三秒调用一次,并将为玩家的分数增加 10 分。因此,玩家能够避免被炸弹击中的时间越长,他的得分就越高。
现在我们有超过两个物理体,这意味着我们必须修改我们的onCollision
方法,使其只有在player
精灵参与碰撞时才切换到gameOverScene
。为此,我们将在方法开始处添加以下代码行:
auto playerShape = _sprPlayer->getPhysicsBody()->getFirstShape();
if(playerShape != contact.getShapeA() && playerShape != contact.getShapeB())
{
return false;
}
如果该方法没有返回,这意味着玩家精灵确实参与了碰撞。因此,我们将使用 Cocos2d-x 内置的存储机制来写入存储在成员变量_score
中的玩家分数:
UserDefault::getInstance()->setIntegerForKey("score",_score);
注意
UserDefault
类使我们能够访问 Cocos2d-x 的数据存储机制。它可以存储bool
、int
、float
、double
和string
类型的值。通过使用此类存储的数据可以通过调用flush
方法来持久化,该方法将数据存储在 XML 文件中。
我们可以像创建 TrueType 字体和系统字体那样创建我们的位图字体。我们将在GameOverScene.cpp
实现文件的init
方法中添加以下代码行:
char scoreText[32];
int score = UserDefault::getInstance()->getIntegerForKey("score",0);
sprintf(scoreText, "%d", score);
auto label3 = Label::createWithBMFont("font.fnt", scoreText);
label3->setPosition(origin.x + visibleSize.width/2,origin.y + visibleSize.height /3.5);
this->addChild(label3, 1);
上述代码将产生以下结果:
把所有内容放在一起
在我们所有的修改之后,这就是我们的HelloWorldScene.h
头文件的样子:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "PauseScene.h"
#include "GameOverScene.h"
在本章中,我们对这个头文件唯一做的更改是包含了GameOverScene.h
:
class HelloWorld : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
void pauseCallback(cocos2d::Ref* pSender);
CREATE_FUNC(HelloWorld);
private:
cocos2d::Director *_director;
cocos2d::Size visibleSize;
cocos2d::Sprite* _sprBomb;
cocos2d::Sprite* _sprPlayer;
int _score;
void initPhysics();
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
void updateScore(float dt);
void addBombs(float dt);
};
#endif // __HELLOWORLD_SCENE_H__
现在,我们的HelloWorldScene.cpp
实现文件看起来像这样:
#include "HelloWorldScene.h"
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"
USING_NS_CC;
Scene* HelloWorld::createScene()
{
auto scene = Scene::createWithPhysics();
scene->getPhysicsWorld()->setGravity(Vect(0,0));
auto layer = HelloWorld::create();
//enable debug draw
//scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDR AW_ALL);
scene->addChild(layer);
return scene;
}
我们现在将添加事件和物理的代码:
void HelloWorld::updateScore(float dt)
{
_score += 10;
}
void HelloWorld::addBombs(float dt)
{
Sprite* bomb = nullptr;
for(int i = 0 ; i < 3 ; i++)
{
bomb = Sprite::create("bomb.png");
bomb->setPosition(CCRANDOM_0_1() * visibleSize.width, visibleSize.height + bomb->getContentSize().height/2);
this->addChild(bomb,1);
setPhysicsBody(bomb);
bomb->getPhysicsBody()->setVelocity(Vect(0, ( (CCRANDOM_0_1() + 0.2f) * -250) ));
}
}
}
bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
_score = 0;
_director = Director::getInstance();
visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(visibleSize.width - closeItem- >getContentSize().width/2, closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(visibleSize.width / 2, visibleSize.height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
_sprPlayer = Sprite::create("player.png");
_sprPlayer->setPosition(visibleSize.width / 2, visibleSize.height * 0.23);
setPhysicsBody(_sprPlayer);
this->addChild(_sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = _sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
_sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
initTouch();
initAccelerometer();
setKeepScreenOnJni(true);
initBackButtonListener();
schedule(CC_SCHEDULE_SELECTOR (HelloWorld::updateScore), 3.0f);
schedule(CC_SCHEDULE_SELECTOR (HelloWorld::addBombs), 8.0f);
return true;
}
void HelloWorld::pauseCallback(cocos2d::Ref* pSender){
_director->pushScene(TransitionFlipX::create(1.0, Pause::createScene()));
}
我们的GameOverScene.h
头文件现在看起来像这样:
#ifndef __GameOver_SCENE_H__
#define __GameOver_SCENE_H__
#include "cocos2d.h"
#include "HelloWorldScene.h"
class GameOver : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
void exitPause(cocos2d::Ref* pSender);
CREATE_FUNC(GameOver);
private:
cocos2d::Sprite* sprLogo;
cocos2d::Director *director;
cocos2d::Size visibleSize;
};
#endif // __Pause_SCENE_H__
最后,我们的GameOverScene.cpp
实现文件将看起来像这样:
#include "GameOverScene.h"
USING_NS_CC;
Scene* GameOver::createScene()
{
auto scene = Scene::create();
auto layer = GameOver::create();
scene->addChild(layer);
return scene;
}
bool GameOver::init()
{
if ( !Layer::init() )
{
return false;
}
director = Director::getInstance();
visibleSize = director->getVisibleSize();
Vec2 origin = director->getVisibleOrigin();
auto pauseItem = MenuItemImage::create("play.png", "play_pressed.png", CC_CALLBACK_1(GameOver::exitPause, this));
pauseItem->setPosition(Vec2(origin.x + visibleSize.width - pauseItem->getContentSize().width / 2, origin.y + pauseItem- >getContentSize().height / 2));
auto menu = Menu::create(pauseItem, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
在以下代码行中,我们创建了在本章中介绍的三种字体类型:
auto label = Label::createWithTTF("Game Over", "fonts/Marker Felt.ttf", 96);
label->enableOutline(Color4B(255, 0, 0, 100),6);
label->enableGlow(Color4B(255, 0, 0, 255));
label->enableShadow();
label->setPosition(origin.x + visibleSize.width/2, origin.y + visibleSize.height /2);
this->addChild(label, 1);
auto label2 = Label::createWithSystemFont("Your score is", "Arial", 48);
label2->setPosition(origin.x + visibleSize.width/2,origin.y + visibleSize.height/2.5);
this->addChild(label2, 1);
char scoreText[32];
int score = UserDefault::getInstance()- >getIntegerForKey("score",0);
sprintf(scoreText, "%d", score);
auto label3 = Label::createWithBMFont("font.fnt", scoreText);
label3->setPosition(origin.x + visibleSize.width/2,origin.y + visibleSize.height /3.5);
this->addChild(label3, 1);
return true;
}
void GameOver::exitPause(cocos2d::Ref* pSender){
Director::getInstance()- >replaceScene(TransitionFlipX::create(1.0, HelloWorld::createScene()));
}
总结
在本章中,我们了解了如何使用 TrueType 字体、系统字体和位图字体向游戏中添加文本,以及如何为这些文本添加效果。标签创建非常简单;您只需要调用其创建的静态方法,并将其添加到场景中后,就可以像在屏幕上定位精灵一样在屏幕上定位它们。
在下一章中,我们将介绍在版本 3 中从头开始编写的新音频引擎,以替代自其前身cocos2d
for iPhone 以来与引擎捆绑的传统CocosDenshion
音频引擎。
第六章:音频
Cocos2d-x 框架带有一个名为CocosDenshion
的音频引擎,它从 Cocos2d for iPhone 继承而来。这个引擎封装了播放声音效果和背景音乐的所有复杂性。现在,Cocos2d-x 有一个从零开始构建的新的音频引擎,旨在提供比CocosDenshion
库更多的灵活性。请注意,没有计划从 Cocos2d-x 框架中消除CocosDenshion
音频引擎,现在在 Cocos2d-x 中通常会有冗余的组件,以便程序员可以选择更适合他们需求的部分。
本章将涵盖以下主题:
-
播放背景音乐和声音效果
-
修改音频属性
-
离开游戏时处理音频
-
新的音频引擎
播放背景音乐和声音效果
为了通过使用CocosDenshion
音频引擎向我们的游戏中添加背景音乐,第一步是在我们的HelloWorldScene.cpp
实现文件中添加以下文件包含:
#include "SimpleAudioEngine.h"
在这个头文件中,我们将在私有成员部分也添加我们新的initAudio
方法的声明,该方法将用于启动背景音乐以及预加载每次player
精灵被炸弹撞击时要播放的音效:
void initAudio();
现在,在HelloWorld.cpp
实现文件中,我们将使用CocosDenshion
命名空间,这样我们在每次访问音频引擎单例实例时就不必隐式引用这个命名空间:
using namespace CocosDenshion;
现在,在同一个实现文件中,我们将编写initAudio
方法的主体,正如我们之前提到的,它将开始播放循环的背景音乐。我们提供了这一章的源代码,并将预加载每次我们的玩家失败时要播放的音效。playBackgroundMusic
方法的第二个参数是一个布尔值,它决定了我们是否希望背景音乐永远重复。
void HelloWorld::initAudio()
{
SimpleAudioEngine::getInstance()->playBackgroundMusic("music.mp3", true);
SimpleAudioEngine::getInstance()->preloadEffect("uh.wav");
}
让我们在Resources
目录中创建一个名为sounds
的文件夹,这样我们可以把所有的声音文件以有组织的方式放在那里。完成此操作后,我们需要在AppDelegate.cpp
实现文件中实例化searchPaths std::vector
之后添加以下行,将sounds
目录添加到搜索路径中,以便音频引擎可以找到我们的文件:
searchPaths.push_back("sounds");
注意
我们鼓励您组织您的Resources
文件夹,为音频和音乐创建一个声音文件夹以及子文件夹,这样我们就不必把所有内容都放在根目录下。
让我们转到每次两个物理体碰撞时调用的onCollision
方法。如果玩家的精灵物理体涉及到碰撞,那么我们将在添加以下代码行之后停止背景音乐并播放uh.wav
音效,然后切换到游戏结束场景:
SimpleAudioEngine::getInstance()->stopBackgroundMusic();
SimpleAudioEngine::getInstance()->playEffect("uh.wav");
最后,我们将在HelloWorld.cpp
实现文件中的init
方法末尾添加对我们initAudio
方法的调用:
initAudio();
修改音频属性
您可以通过调用setBackgroundMusicVolume
方法和setEffectsVolume
方法轻松修改背景音乐和声音效果的基本音频属性。两者都接收一个float
类型的参数,其中0.0
表示静音,1.0
表示最大音量,如下面的代码清单所示:
SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0.5f);
SimpleAudioEngine::getInstance()->setEffectsVolume(1.0f);
处理离开游戏时的音频
当游戏活动不再处于活动状态时,背景音乐和声音效果不会自动停止,应该通过从AppDelegate
类的applicationDidEnterBackgound
方法中移除以下注释块来手动停止:
// if you use SimpleAudioEngine, it must be pause
SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
为了让这行新代码工作,我们需要在HelloWorld.cpp
实现文件中添加相同的行,以便使用CocosDenshion
命名空间:
using namespace CocosDenshion;
当用户切换到另一个应用程序时,您的游戏将停止所有当前的声音。用户一回到我们的游戏,我们就需要恢复音乐。我们可以像以前一样做,但现在,我们将从AppDelegate
类的applicationWillEnterForeground
方法中移除以下注释块:
// if you use SimpleAudioEngine, it must resume here
SimpleAudioEngine::getInstance()->resumeBackgroundMusic();
新的音频引擎
在 Cocos2d-x 3.4 的实验阶段,从头开始构建了一个新的音频引擎,以便添加更多功能和灵活性。现在 Cocos2d-x 的新音频引擎可用于 Android、iOS、Mac OS 和 win-32 平台。它能够在 Android 平台上同时播放多达 24 个声音;这个数字可能会根据平台的不同而改变。
如果您运行与 Cocos2d-x 框架捆绑的测试,那么您可以测试两个音频引擎。在运行时,它们可能听起来没有明显差异,但它们在内部是非常不同的。
与CocosDenshion
引擎不同,这个新引擎中声音效果和背景音乐没有区别。因此,与CocosDenshion
的两个方法—setBackgroundMusicVolume
和setEffectsVolume
相比,框架只有一个setVolume
方法。在本节后面,我们将向您展示如何调整每个播放音频的音量,无论它是声音效果还是背景音乐。
让我们在HelloWorldScene.h
头文件中添加一个新的方法声明,名为initAudioNewEngine
,顾名思义,它将初始化我们游戏的音频功能,但现在它将使用新的音频引擎来完成同样的任务。
我们需要在我们的HelloWorldScene.h
文件中包含新的引擎头文件,如下所示:
#include "audio/include/AudioEngine.h"
让我们在HelloWorld.cpp
实现文件中包含以下代码行,以便我们可以直接调用AudioEngine
类,而无需每次使用时都引用其命名空间:
using namespace cocos2d::experimental;
现在,让我们按照以下方式在我们的实现文件中编写initAudioNewEngine
方法的代码:
void HelloWorld::initAudioNewEngine()
{
if(AudioEngine::lazyInit())
{
auto musicId = AudioEngine::play2d("music.mp3");
AudioEngine::setVolume(musicId, 0.25f);
CCLOG("Audio initialized successfully");
}else
{
log("Error while initializing new audio engine");
}
}
与使用单例实例的CocosDenshion
不同,新音频引擎的所有方法都是静态声明的。
从前面的代码清单中我们可以看到,在调用play2d
方法之前,我们调用了lazyInit
方法。尽管play2d
内部调用了lazyInit
方法,但我们希望尽快知道我们的 Android 系统是否能够播放音频并采取行动。请注意,当play2d
方法返回AudioEngine::INVALID_AUDIO_ID
值时,您还需要找出音频初始化是否出现了问题。
每次我们通过调用play2d
方法播放任意声音时,它都会返回一个唯一的递增的基于零的audioID
索引,我们将保存它,这样每次我们想要对该特定音频实例执行特定操作时,比如更改音量、移动到特定位置、停止、暂停或恢复,我们都可以引用它。
新音频引擎的一个缺点是它仍然支持有限的音频格式。它目前不支持.wav
文件。因此,为了播放uh.wav
声音,我们将它转换为 mp3,然后在onCollision
方法中通过如下调用play2d
来播放:
AudioEngine::stopAll();
AudioEngine::play2d("uh.mp3");
我们在本章提供的代码资源存档中包含了新的uh.mp3
音频文件。
对于我们的游戏,我们将实施两种方案;传统的CocosDenshion
引擎,这是最成熟的音频引擎,为我们提供了所需的基本功能,比如播放音效和背景音乐;以及新引擎中的相同音频功能。
新音频引擎中包含的新功能
play2d
方法被重载,以便我们可以指定是否希望声音循环播放、初始音量以及我们希望应用的声音配置文件。AudioProfile
类是 Cocos2d-x 框架的一部分,它只有三个属性:name
,不能为空;maxInstances
,将定义将同时播放多少个声音;以及minDelay
,它是一个double
数据类型,将指定声音之间的最小延迟。
新音频引擎具有的另一个功能是,通过调用setCurrentTime
方法并传递audioID
方法和以秒为单位的自定义位置(由float
表示)来从自定义位置播放音频。
在新音频引擎中,您可以指定在给定音频实例播放完成时您希望调用的函数。这可以通过调用setFinishCallback
方法来实现。
每次播放音频时,它都会被缓存,因此无需再次从文件系统中读取。如果我们想要释放一些资源,可以调用uncacheAll
方法来移除音频引擎内部用于回放音频的所有缓冲区,或者可以通过调用uncache
方法并指定要移除的文件系统中的文件路径来从缓存中移除任何特定的音频。
本节的目的是让您了解另一个处于实验阶段的音频引擎,如果CocosDenshion
没有您想要添加到游戏中的任何音频功能,那么您应该检查另一个音频引擎,看看它是否具备您所需的功能。
注意
新的音频引擎可以在 Mac OS、iOS 和 win-32 平台上同时播放多达 32 个声音,但在 Android 上只能同时播放多达 24 个声音。
向我们的游戏添加静音按钮
在本章结束之前,我们将在游戏中添加一个静音按钮,这样我们就可以通过一次触摸将音效和背景音乐音量设置为零。
为了实现这一点,我们将在HelloWorld
类中添加两个方法;一个用于初始化按钮,另一个用于实际静音所有声音。
为了实现这一点,我们将在HelloWorldScene.h
头文件的私有部分添加以下几行:
int _musicId;
cocos2d::MenuItemImage* _muteItem;
cocos2d::MenuItemImage* _unmuteItem;
void initMuteButton();
void muteCallback(cocos2d::Ref* pSender);
现在,我们将以下initMuteButton
实现代码添加到HelloWorldScene.cpp
文件中:
void HelloWorld::initMuteButton()
{
_muteItem = MenuItemImage::create("mute.png", "mute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_muteItem->setPosition(Vec2(_visibleSize.width - _muteItem- >getContentSize().width/2 ,
_visibleSize.height - _muteItem->getContentSize(). height / 2));
_unmuteItem = MenuItemImage::create("unmute.png", "unmute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_unmuteItem->setPosition(Vec2(_visibleSize.width - _unmuteItem- >getContentSize().width/2 , _visibleSize.height - _unmuteItem->getContentSize().height /2));
_unmuteItem -> setVisible(false);
auto menu = Menu::create(_muteItem, _unmuteItem , nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
}
如您所见,我们刚刚创建了一个新的菜单,我们在其中添加了两个按钮,一个用于静音游戏,另一个不可见用于取消静音。我们将这些分别存储在成员变量中,这样我们就可以通过在以下代码清单中声明的muteCallback
方法访问它们:
void HelloWorld::muteCallback(cocos2d::Ref* pSender)
{
if(_muteItem -> isVisible())
{
//CocosDenshion
//SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0);
AudioEngine::setVolume(_musicId, 0);
}else
{
//SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1);
AudioEngine::setVolume(_musicId, 1);
}
_muteItem->setVisible(!_muteItem->isVisible());
_unmuteItem->setVisible(!_muteItem->isVisible());
}
在这里,我们基本上只是判断_muteItem
菜单项是否可见。如果可见,则通过使用新的音频引擎CocosDenshion
将音量设置为零,否则将音量设置为最大值,即一。在任何一种情况下,都要改变静音和取消静音菜单项的实际可见值。
我们可以在以下屏幕截图中看到最终结果:
把所有内容放在一起
在我们添加了将sounds
文件夹包含在resources
路径中的行之后,我们的AppDelegate.cpp
实现文件中的applicationDidFinishLaunching
方法如下所示:
bool AppDelegate::applicationDidFinishLaunching() {
auto director = Director::getInstance();
// OpenGL initialization done by cocos project creation script
auto glview = director->getOpenGLView();
if(!glview) {
glview = GLViewImpl::create("Happy Bunny");
glview->setFrameSize(480, 800);
director->setOpenGLView(glview);
}
Size screenSize = glview->getFrameSize();
Size designSize(768, 1280);
std::vector<std::string> searchPaths;
searchPaths.push_back("sounds");
if (screenSize.height > 800){
//High Resolution
searchPaths.push_back("images/high");
director->setContentScaleFactor(1280.0f / designSize.height);
}
else if (screenSize.height > 600){
//Mid resolution
searchPaths.push_back("images/mid");
director->setContentScaleFactor(800.0f / designSize.height);
}
else{
//Low resolution
searchPaths.push_back("images/low");
director->setContentScaleFactor(320.0f / designSize.height);
}
FileUtils::getInstance()->setSearchPaths(searchPaths);
glview->setDesignResolutionSize(designSize.width, designSize. height, ResolutionPolicy::EXACT_FIT);
auto scene = HelloWorld::createScene();
director->runWithScene(scene);
return true;
}
下面的代码清单显示了我们在本章中进行更改后HelloWorldScene.h
头文件的样子:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "PauseScene.h"
#include "GameOverScene.h"
class HelloWorld : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
CREATE_FUNC(HelloWorld);
private:
cocos2d::Director *_director;
cocos2d::Size _visibleSize;
cocos2d::Sprite* _sprBomb;
cocos2d::Sprite* _sprPlayer;
cocos2d::MenuItemImage* _muteItem;
cocos2d::MenuItemImage* _unmuteItem;
int _score;
int _musicId;
void initPhysics();
void pauseCallback(cocos2d::Ref* pSender);
void muteCallback(cocos2d::Ref* pSender);
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
void updateScore(float dt);
void addBombs(float dt);
void initAudio();
void initAudioNewEngine();
void initMuteButton();
};
#endif // __HELLOWORLD_SCENE_H__
最后,在添加了音频管理代码之后,我们的HelloWorldScene.cpp
实现文件如下所示:
#include "HelloWorldScene.h"#include "SimpleAudioEngine.h"
#include "audio/include/AudioEngine.h"
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"
USING_NS_CC;
using namespace CocosDenshion;
using namespace cocos2d::experimental;
Scene* HelloWorld::createScene()
{
//no changes here
}
// physics code …
// event handling code …
在以下方法中,我们将通过使用新的音频引擎来初始化音频。注意,我们会将音频实例的背景音乐的 ID 存储在_musicId
整型成员变量中:
void HelloWorld::initAudioNewEngine()
{
if(AudioEngine::lazyInit())
{
_musicId = AudioEngine::play2d("music.mp3");
AudioEngine::setVolume(_musicId, 1);
AudioEngine::setLoop(_musicId,true);
CCLOG("Audio initialized successfully");
}else
{
CCLOG("Error while initializing new audio engine");
}
}
在这里,我们执行了与上一个方法中相同的初始化工作,但现在我们是使用CocosDenshion
音频引擎来完成:
void HelloWorld::initAudio()
{
SimpleAudioEngine::getInstance()->playBackgroundMusic("music. mp3",true);
SimpleAudioEngine::getInstance()->preloadEffect("uh.wav");
SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1.0f);
}
在以下方法中,我们创建了一个简单的菜单,以展示静音和取消静音游戏的选项。这里我们将静音和取消静音的精灵存储在对应的成员变量中,以便我们可以在muteCallback
方法中稍后访问它们,并操作它们的visibility
属性:
void HelloWorld::initMuteButton()
{
_sprMute = Sprite::create("mute.png");
_sprUnmute = Sprite::create("unmute.png");
_muteItem = MenuItemImage::create("mute.png", "mute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_muteItem->setPosition(Vec2(_visibleSize.width - _muteItem- >getContentSize().width/2 ,
_visibleSize.height - _muteItem->getContentSize().height / 2));
_unmuteItem = MenuItemImage::create("unmute.png", "unmute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_unmuteItem->setPosition(Vec2(_visibleSize.width - _unmuteItem->getContentSize().width/2 ,
_visibleSize.height - _unmuteItem->getContentSize(). height /2));
_unmuteItem -> setVisible(false);
auto menu = Menu::create(_muteItem, _unmuteItem , nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
}
以下方法将在每次按下静音或取消静音菜单项时被调用,在这个方法中,我们只需将音量设置为 0,并根据触摸的选项显示静音或取消静音按钮:
void HelloWorld::muteCallback(cocos2d::Ref* pSender)
{
if(_muteItem -> isVisible())
{
//CocosDenshion
//SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0);
AudioEngine::setVolume(_musicId, 0);
}else
{
//SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(1);
AudioEngine::setVolume(_musicId, 1);
}
_muteItem->setVisible(!_muteItem->isVisible());
_unmuteItem->setVisible(!_muteItem->isVisible());
}
我们对init
方法做的唯一修改是在其最后添加了对initMuteButton();
方法的调用:
bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
_score = 0;
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(_visibleSize.width - closeItem->getContentSize().width/2, closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(_visibleSize.width / 2, _visibleSize.height +_sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
_sprPlayer = Sprite::create("player.png");
_sprPlayer->setPosition(_visibleSize.width / 2, _visibleSize.height* 0.23);
setPhysicsBody(_sprPlayer);
this->addChild(_sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = _sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
_sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
initTouch();
initAccelerometer();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
setKeepScreenOnJni(true);
#endif
initBackButtonListener();
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::updateScore), 3.0f);
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addBombs), 8.0f);
initAudioNewEngine();
initMuteButton();
return true;
}
如你所见,尽管我们使用了新的音频引擎来播放声音,但我们展示了使用传统CocosDenshion
音频引擎所需的所有代码。为了启用CocosDenshion
实现,你只需在HelloWorld.cpp
文件的init
方法的底部调用initAudio
方法,而不是调用initAudioNewEngine
方法,最后,你还需要在onCollision
方法中移除CocosDenshion
实现代码的注释斜杠,并注释掉新的音频引擎播放代码。
总结
在本章中,我们通过使用 Cocos2d-x 框架捆绑的两个音频引擎,以非常简单的方式为我们的游戏添加了背景音乐和音效。
在下一章中,我们将介绍如何将粒子系统添加到我们的游戏中,以模拟每次炸弹击中player
精灵时的更真实的爆炸效果。
第七章:创建粒子系统
通过使用 Cocos2d-x 框架内置的粒子系统,您可以轻松模拟火、烟、爆炸、雪和雨。本章将教您如何创建这里提到的效果,并教您如何自定义它们。
本章将涵盖以下主题:
-
创建 Cocos2d-x 对象的集合
-
将粒子系统添加到我们的游戏中
-
配置粒子系统
-
创建自定义粒子系统
创建 Cocos2d-x 对象的集合
我们将向游戏中添加一个粒子系统,以模拟每次玩家触摸炸弹时的爆炸效果。为了做到这一点,我们将使用 Cocos2d-x 框架中的Vector
类来创建游戏中创建的所有炸弹对象的集合,这样当玩家触摸屏幕时,我们将遍历这个集合以验证玩家是否触摸到了任何炸弹。
如果玩家触摸到任何炸弹,我们将要:
-
在炸弹精灵所在位置显示爆炸效果
-
使炸弹不可见
-
使用继承的
removeChild
方法从屏幕上移除炸弹,最后 -
从集合中移除炸弹对象,这样下次我们遍历向量时,就会忽略它
为此,我们将炸弹集合按照以下方式添加到我们的HelloWorldScene.h
定义文件中:
cocos2d::Vector<cocos2d::Sprite*> _bombs;
请注意,我们指定要使用cocos2d
命名空间中捆绑的Vector
类,这样编译器可以清楚地知道我们是指向框架内置的集合类,而不是std
命名空间中的Vector
类。尽管可以使用std
命名空间中的Vector
类,但位于框架中的类是针对在 Cocos2d-x 对象集合中使用而优化的。
注意
Cocos2d-x 3.0 中引入的Vector
类使用 C++标准来表示对象集合,与使用 Objective-C 容器类来建模 Cocos2d-x 对象集合的已弃用的CCArray
类相对。这个新类处理 Cocos2d-x 中用于内存管理的引用计数机制,它还添加了std::vector
中不存在的功能,如random
、contains
和equals
方法。
只有在需要将实例作为参数传递给预期数据类型的 Cocos2d-x API 类函数时,才应使用std::vector
实例,例如FileUtils
类中的setSearchPaths
方法。
现在,让我们转到位于HelloWorldScene.cpp
实现文件中的init
方法,在声明持有第一个炸弹精灵引用的_sprBomb
变量旁边,我们将按照以下方式将此引用添加到我们的新_bombs
集合中:
_bombs.pushBack(_sprBomb);
现在,让我们回到在我们之前章节中创建的 addBombs
方法,以向我们的游戏中添加更多炸弹。在这个方法中,我们将把游戏中场景中生成的每个炸弹添加到 _bombs
集合中,如下所示:
void HelloWorld::addBombs(float dt)
{
Sprite* bomb = nullptr;
for(int i = 0; i < 3; i++){
bomb = Sprite::create("bomb.png");
bomb->setPosition(CCRANDOM_0_1() * visibleSize.width, visibleSize.height + bomb->getContentSize().height/2);
this->addChild(bomb,1);
setPhysicsBody(bomb);
bomb->getPhysicsBody()->setVelocity(Vect(0, ( (CCRANDOM_0_1() + 0.2f) * -250) ));
_bombs.pushBack(bomb);
}
}
爆炸的炸弹
我们希望当我们触摸炸弹时它们能爆炸。为了实现这一点,我们将创建我们的 explodeBombs
方法。在 HelloWorldScene.h
头文件中,我们将按以下方式编写声明:
bool explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event);
现在,我们将在 HelloWorldScene.cpp
实现文件中编写方法体;如前所述,每次玩家触摸屏幕时,我们可以验证触摸的位置并与每个炸弹的位置进行比较。如果发现任何交集,那么被触摸的炸弹将会消失。目前,我们还不打算添加任何粒子系统,我们将在后面的章节中做这件事:
bool HelloWorld::explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event)
{
Vec2 touchLocation = touch->getLocation();
cocos2d::Vector<cocos2d::Sprite*> toErase;
for(auto bomb : _bombs){
if(bomb->getBoundingBox().containsPoint(touchLocation)){
bomb->setVisible(false);
this->removeChild(bomb);
toErase.pushBack(bomb);
}
}
for(auto bomb : toErase){
_bombs.eraseObject(bomb);
}
return true;
}
请注意,我们创建了一个另一个向量,用于添加所有被用户触摸的炸弹,然后在另一个循环中将它们从 _bombs
集合中移除。我们这样做而不是直接从第一个循环中移除对象的原因是,这将会导致运行时错误。这是因为我们不能在遍历集合的同时对单一集合进行并发修改,即我们不能在遍历集合时从中移除一个项目。如果我们这样做,那么我们将得到一个运行时错误。
注意
Vector
类是在 Cocos2d-x 3.0 中引入的。它替代了在 Cocos2d-x 2.x 中使用的 CCArray
类。我们可以使用 C++11 的 for each 特性遍历 Vector
实例;因此,在 Cocos2d-x 2.x 中用于遍历 Cocos2d-x 对象的 CCARRAY_FOREACH
宏不再需要。
现在,我们将在 HelloWorldScene.cpp
实现文件中的 initTouch
方法中通过以下更改向我们的触摸监听器添加一个回调到 onTouchBegan
属性:
void HelloWorld::initTouch()
{
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::explodeBombs,this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::movePlayerByTouch,this);
listener->onTouchEnded = ={};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
这样就完成了,现在当你触摸炸弹时,它们将会消失。在下一节中,我们将添加一个爆炸效果,以增强我们游戏的外观。
向我们的游戏中添加粒子系统
Cocos2d-x 有内置的类,允许你通过显示大量称为粒子的微小图形对象来渲染最常见的视觉效果,如爆炸、火焰、烟花、烟雾和雨等。
实现起来非常简单。让我们通过简单地向我们的 explodeBombs
方法中添加以下行来添加一个默认的爆炸效果:
bool HelloWorld::explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event){
Vec2 touchLocation = touch->getLocation();
cocos2d::Vector<cocos2d::Sprite*> toErase;
for(auto bomb : _bombs){
if(bomb->getBoundingBox().containsPoint(touchLocation)){
auto explosion = ParticleExplosion::create();
explosion->setPosition(bomb->getPosition());
this->addChild(explosion);
bomb->setVisible(false);
this->removeChild(bomb);
toErase.pushBack(bomb);
}
}
for(auto bomb : toErase){
_bombs.eraseObject(bomb);
}
return true;
}
你可以通过更改前一段代码中突出显示的第一行中的粒子类名称,尝试引擎中嵌入的其他粒子系统,可以使用以下类名称:ParticleFireworks
、ParticleFire
、ParticleRain
、ParticleSnow
、ParticleSmoke
、ParticleSpiral
、ParticleMeteor
和 ParticleGalaxy
。
配置粒子系统
在上一节中,我们仅通过添加三行代码就创建了一个逼真的爆炸效果。我们可以自定义粒子系统的许多参数。例如,我们可以通过修改生命属性来调整我们希望粒子系统扩展的程度。
我们还可以通过设置startSize
属性和endSize
属性来调整粒子系统在开始时的大小以及我们希望它在结束时的大小。例如,如果我们想模拟火箭的涡轮,那么我们可以配置发射器从小尺寸开始,到大尺寸结束。
我们可以通过修改角度属性来调整粒子的移动角度。你可以为你的粒子系统分配随机角度,使其看起来更加真实。
粒子系统可以有两种模式,半径模式和重力模式。最常见的粒子系统使用重力模式,我们可以参数化重力、速度、径向和切向加速度。这意味着发射器创建的粒子会受到一个称为重力的力的吸引,我们可以自定义它们的水平和垂直分量。半径模式具有径向运动和旋转,因此这种模式的粒子系统将以螺旋形旋转。
通过totalParticles
属性也可以改变粒子的总数。粒子数量越多,粒子系统看起来越浓密,但要注意,渲染的粒子数量也会影响运行性能。举个例子,默认的爆炸粒子系统有 700 个粒子,而烟雾效果有 200 个粒子。
注意事项
你可以通过调用发射器实例中的set<属性名>
方法来修改本节中提到的属性。例如,如果你想修改系统的总粒子数,那么就调用setTotalParticles
方法。
在下面的代码列表中,我们将修改粒子系统的总粒子数、速度和生命周期:
bool HelloWorld::explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event){
Vec2 touchLocation = touch->getLocation();
cocos2d::Vector<cocos2d::Sprite*> toErase;
for(auto bomb : _bombs){
if(bomb->getBoundingBox().containsPoint(touchLocation)){
auto explosion = ParticleExplosion::create();
explosion->setDuration(0.25f);
AudioEngine::play2d("bomb.mp3");
explosion->setPosition(bomb->getPosition());
this->addChild(explosion);
explosion->setTotalParticles(800);
explosion->setSpeed(3.5f);
explosion->setLife(300.0f);
bomb->setVisible(false);
this->removeChild(bomb);
toErase.pushBack(bomb);
}
}
for(auto bomb : toErase){
_bombs.eraseObject(bomb);
}
return true;
}
创建自定义粒子系统
到目前为止,我们已经尝试了 Cocos2d-x 框架中捆绑的所有粒子系统,但在我们作为游戏开发者的旅程中,将有很多情况需要我们创建自己的粒子系统。
有一些工具允许我们以非常图形化的方式创建和调整粒子系统的属性。这使我们能够创建所见即所得(WYSIWYG)类型的粒子系统。
创建粒子系统最常用的应用程序,在 Cocos2d-x 官方文档中多次提到,名为 Particle Designer。目前它仅适用于 Mac OS,并且你需要购买许可证才能将粒子系统导出为 plist 文件。你可以从以下链接免费下载并试用:71squared.com/particledesigner
。Particle Designer 如下截图所示:
你也可以通过使用以下免费提供的网页应用程序,以图形化的方式创建你的粒子系统:www.particle2dx.com/
。
你还可以使用 V-Play 粒子编辑器,它可以在 Windows、Android、iOS 和 Mac 平台上免费下载和使用。这些工具可以从以下链接获得:games.v-play.net/particleeditor
。
使用前面提到的任何工具,你可以调整粒子系统的属性,比如最大粒子数、持续时间、生命周期、发射速率和角度等,并将其保存为 plist 文件。
我们创建了自己的粒子系统,并将其导出为 plist 文件。这个 plist 文件包含在本章源代码的代码归档中。我们将这个 plist 文件放置在一个新建的文件夹中,该文件夹位于Resources
目录下的particles
目录。
由于我们的 plist 文件不在Resources
目录的根目录下,我们需要在AppDelegate
类的applicationDidFinishLaunching
方法中添加particles
目录到搜索路径,只需在添加sounds
目录到searchPaths
之后加入以下代码行:
searchPaths.push_back("particles");
以下代码展示了如何使用ParticleSystemQuad
类显示我们的自定义粒子系统,并通过其create
静态方法传递由工具生成的 plist 文件的名称作为参数:
bool HelloWorld::explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event){
Vec2 touchLocation = touch->getLocation();
cocos2d::Vector<cocos2d::Sprite*> toErase;
for(auto bomb : _bombs){
if(bomb->getBoundingBox().containsPoint(touchLocation)){
AudioEngine::play2d("bomb.mp3");
auto explosion = ParticleSystemQuad::create("explosion.plist");
explosion->setPosition(bomb->getPosition());
this->addChild(explosion);
bomb->setVisible(false);
this->removeChild(bomb);
toErase.pushBack(bomb);
}
}
for(auto bomb : toErase){
_bombs.eraseObject(bomb);
}
return true;
}
如你所见,我们还添加了一行代码,以便每次炸弹接触到玩家精灵时播放音效,从而增加更真实的效果。这个 MP3 文件已包含在本章提供的代码中。
将所有内容整合到一起
在本章中,我们为游戏添加了粒子系统,使得玩家每次触碰炸弹都能产生逼真的爆炸效果。为了实现这一目标,我们对HelloWorldScene.h
头文件和HelloWorldScene.cpp
实现文件进行了修改。
在本章修改后,我们的HelloWorldScene.h
头文件如下所示:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "PauseScene.h"
#include "GameOverScene.h"
class HelloWorld : public cocos2d::Layer{
public:
static cocos2d::Scene* createScene();
virtual bool init();
CREATE_FUNC(HelloWorld);
private:
cocos2d::Director *_director;
cocos2d::Size _visibleSize;
cocos2d::Sprite* _sprBomb;
cocos2d::Sprite* _sprPlayer;
cocos2d::Vector<cocos2d::Sprite*> _bombs;
cocos2d::MenuItemImage* _muteItem;
cocos2d::MenuItemImage* _unmuteItem;
int _score;
int _musicId;
void initPhysics();
void pauseCallback(cocos2d::Ref* pSender);
void muteCallback(cocos2d::Ref* pSender);
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
bool explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
void updateScore(float dt);
void addBombs(float dt);
void initAudio();
void initAudioNewEngine();
void initMuteButton();
};
#endif // __HELLOWORLD_SCENE_H__
最后,以下代码展示了在本章中我们修改后的HelloWorldScene.cpp
实现文件的样子:
#include "HelloWorldScene.h"
#include "SimpleAudioEngine.h"
#include "audio/include/AudioEngine.h"
#include "../cocos2d/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxHelper.h"
USING_NS_CC;
using namespace CocosDenshion;
using namespace cocos2d::experimental;
//Create scene code …
//User input event handling code
在以下方法中,我们首先验证用户是否触摸到了炸弹,如果用户触摸到了,那么将在触摸时刻炸弹所在位置渲染一个爆炸粒子系统。
bool HelloWorld::explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event){
Vec2 touchLocation = touch->getLocation();
cocos2d::Vector<cocos2d::Sprite*> toErase;
for(auto bomb : _bombs){
if(bomb->getBoundingBox().containsPoint(touchLocation)){
AudioEngine::play2d("bomb.mp3");
auto explosion = ParticleSystemQuad::create("explosion.plist");
explosion->setPosition(bomb->getPosition());
this->addChild(explosion);
bomb->setVisible(false);
this->removeChild(bomb);
toErase.pushBack(bomb);
}
}
for(auto bomb : toErase){
_bombs.eraseObject(bomb);
}
return true;
}
在以下方法中,我们添加了一个事件监听器,每次用户触摸屏幕时都会触发,以验证是否触摸到了炸弹:
void HelloWorld::initTouch(){
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::explodeBombs,this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::movePlayerByTouch,this);
listener->onTouchEnded = ={};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
}
在以下方法中,我们通过使用其pushBack
方法,将新产生的炸弹添加到我们的新的cocos2d:Vector
集合中:
void HelloWorld::addBombs(float dt)
{
Sprite* bomb = nullptr;
for(int i = 0; i < 3; i++){
bomb = Sprite::create("bomb.png");
bomb->setPosition(CCRANDOM_0_1() * visibleSize.width, visibleSize.height + bomb->getContentSize().height/2);
this->addChild(bomb,1);
setPhysicsBody(bomb);
bomb->getPhysicsBody()->setVelocity(Vect(0, ( (CCRANDOM_0_1() + 0.2f) * -250) ));
_bombs.pushBack(bomb);
}
}
现在我们来看看在本章修改后,我们的init
方法长什么样子。注意,我们已经将初始化阶段创建的第一个炸弹添加到了新的cocos2d:Vector _bombs
集合中。
bool HelloWorld::init()
{
if ( !Layer::init() ){
return false;
}
_score = 0;
_director = Director::getInstance();
_visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("pause.png", "pause_pressed.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(_visibleSize.width - closeItem->getContentSize().width/2 , closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(_visibleSize.width/2, _visibleSize.height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
_sprPlayer = Sprite::create("player.png");
_sprPlayer->setPosition(_visibleSize.width/2, _visibleSize.height * 0.23);
setPhysicsBody(_sprPlayer);
this->addChild(_sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = _sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
_sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
initTouch();
initAccelerometer();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
setKeepScreenOnJni(true);
#endif
initBackButtonListener();
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::updateScore), 3.0f);
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addBombs), 8.0f);
initAudioNewEngine();
initMuteButton();
_bombs.pushBack(_sprBomb);
return true;
}
总结
在本章中,我们学习了如何在游戏中使用粒子系统模拟真实的火焰、爆炸、雨雪,如何自定义它们,以及如何从零开始创建它们。我们还学习了如何使用 Cocos2d-x API 中捆绑的Vector
类来创建 Cocos2d-x 对象的集合。
在下一章,我们将向您展示如何使用 Java Native Interface (JNI)向我们的游戏中添加 Android 原生代码。
第八章:添加原生 Java 代码
到目前为止,我们一直在使用 Cocos2d-x 游戏框架编写的编程语言(C++)来创建我们的游戏;然而,由 Google 编写的 Android API 仅在应用程序的 Java 层可用。在本章中,你将学习如何使用Java Native Interface (JNI)的能力,将我们的原生 C++代码与高端的 Java 核心进行通信。
本章节将涵盖以下主题:
-
理解 Cocos2d-x 在 Android 平台的架构
-
理解 JNI 的能力
-
向 Cocos2d-x 游戏中添加 Java 代码
-
通过插入 Java 代码向游戏中添加广告
理解 Cocos2d-x 在 Android 平台的架构(再次注意原文重复,不重复翻译)
在第一章,设置你的开发环境中,我们在安装构建 Cocos2d-x 框架所需的所有组件时,告诉你要下载并安装 Android 原生开发工具包 (NDK),它允许我们使用 C++语言而非主流的 Java 技术核心来构建 Android 应用程序,Android API 就是用这种技术核心编写的。
当一个 Android 应用程序启动时,它会查看其AndroidManisfest.xml
文件,寻找带有意图过滤器android.intent.action.MAIN
的活动定义,然后运行 Java 类。以下列表展示了由 Cocos 新脚本生成的AndroidManifest.xml
文件片段,其中指定了当 Android 应用程序启动时要启动的活动:
<activity
android:name="org.cocos2dx.cpp.AppActivity"
android:configChanges="orientation"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Cocos2d-x 项目创建脚本已经创建了一个名为AppActivity
的 Java 类,它位于proj.android
目录下的src
文件夹中的org.cocos2dx.cpp
Java 包名中。这个类没有主体,并继承自Cocos2dxActivity
类,正如我们可以在以下代码列表中欣赏到的那样:
package org.cocos2dx.cpp;
import org.cocos2dx.lib.Cocos2dxActivity;
public class AppActivity extends Cocos2dxActivity {
}
Cocos2dxActivity
类在其onCreate
方法中加载原生 C++框架核心。
理解 JNI 的能力(请注意,这里原文有重复,根据注意事项,我不会重复翻译)
JNI 提供了 C++代码和 Java 代码之间的桥梁。Cocos2d-x 框架为我们提供了 JNI 助手,这使得集成 C++代码和 Java 代码变得更加容易。
JniHelper
C++类有一个名为getStaticMethodInfo
的方法。这个方法接收以下参数:一个JniMethodInfo
对象来存储调用相应 Java 代码所需的所有数据,静态方法所在的类名,方法名以及它的签名。
为了找出 JNI 的方法签名,你可以使用javap
命令:例如,如果我们想知道AppActivity
类中包含的方法的签名,那么我们只需要打开一个控制台窗口,前往你的proj.android\bin\classes
目录,并输入以下命令:
SET CLASSPATH=.
javap -s org.cocos2dx.cpp.AppActivity
在这个特定情况下,你将收到如下自动为类创建的null
构造函数的签名:
Compiled from "AppActivity.java"
public class org.cocos2dx.cpp.AppActivity extends org.cocos2dx.lib.Cocos2dxActivity {
public org.cocos2dx.cpp.AppActivity();
Signature: ()V
}
然后,通过JniMethodInfo
实例附加的env
属性,我们可以调用一系列以Call…
开头的对象方法来调用 Java 方法。在下一节我们将编写的代码中,我们将使用CallStaticVoid
方法来调用一个不返回任何值的静态方法,顾名思义。请注意,如果你想传递一个 Java 字符串作为参数,那么你需要调用env
属性的NewStringUTF
方法,传递const char*
,它将返回一个jstring
实例,你可以用它来传递给一个接收字符串的 Java 方法,如下面的代码清单所示:
JniMethodInfo method;
JniHelper::getStaticMethodInfo(method, CLASS_NAME,"showToast","(Ljava/lang/String;)V");
jstring stringMessage = method.env->NewStringUTF(message);
method.env->CallStaticVoidMethod(method.classID, method.methodID, stringMessage);
最后,如果你在 C++代码中创建了jstring
或其他任何 Java 抽象类的实例,那么在将值传递给 Java 核心之后,请确保删除这些实例,这样我们就不必在内存中保留不必要的引用。可以通过调用JniMethodInfo
实例的env
属性中的DeleteLocalRef
方法,并传递你想移除的 Java 抽象引用来实现这一点:
method.env->DeleteLocalRef(stringMessage);
本节介绍的概念将应用于下一节的代码清单。
将 Java 代码添加到 Cocos2d-x 游戏
现在,我们将创建一个简单的集成,将这两项技术结合起来,使我们的 Cocos2d-x C++游戏能够使用 Android Java API 显示提示框消息。
注意
安卓中的提示框(Toast)是一种弹出的消息,它会显示一段指定的时间,在这段时间内无法被隐藏。本节的最后附有提示框消息的截图,以供参考。
Cocos2d-x 运行在一个 Java 活动中,为了显示原生的 Android 提示框消息,我们将创建一个 Java 类,它将有一个名为showToast
的静态方法。这个方法将接收一个字符串,并在提示框中显示它。为了访问 Cocos2d-x 游戏活动,我们将在该类中添加一个类型为Activity
的静态属性,并在重写的onCreate
方法中初始化它。然后,我们将创建一个公共的静态方法,这将允许我们从 Java 代码的任何地方访问这个实例。在这些修改之后,我们的AppActivity
Java 类代码将如下所示:
package org.cocos2dx.cpp;
import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;
import android.os.Bundle;
public class AppActivity extends Cocos2dxActivity {
private static Activity instance;
@Override
protected void onCreate(final Bundle savedInstanceState) {
instance = this;
super.onCreate(savedInstanceState);
}
public static Activity getInstance(){
return instance;
}
}
现在,让我们在com.packtpub.jni
包内创建所提到的JniFacade
Java 类,该类体内将只有一个接收字符串作为参数的静态 void 方法,然后如下所示在 UI 线程中以接收到的消息显示提示框:
package com.packtpub.jni;
import org.cocos2dx.cpp.AppActivity;
import android.app.Activity;
import android.widget.Toast;
public class JniFacade {
private static Activity activity = AppActivity.getInstance();
public static void showToast(final String message) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity.getBaseContext(), message, Toast. LENGTH_SHORT).show();
}
});
}
}
既然我们已经有了 Java 端的代码,让我们将JniBridge
C++类添加到我们的classes
文件夹中。
在JniBridge.h
头文件中,我们将编写以下内容:
#ifndef __JNI_BRIDGE_H__
#define __JNI_BRIDGE_H__
#include "cocos2d.h"
class JniBridge
{
public:
static void showToast(const char* message);
};
#endif
现在让我们创建实现文件JniBridge.cpp
,在这里我们将调用名为showToast
的静态 Java 方法,该方法接收一个字符串作为参数:
#include "JniBridge.h"
#define CLASS_NAME "com/packtpub/jni/JniFacade"
#define METHOD_NAME "showToast"
#define PARAM_CODE "(Ljava/lang/String;)V"
USING_NS_CC;
void JniBridge::showToast(const char* message)
{
JniMethodInfo method;
JniHelper::getStaticMethodInfo(method, CLASS_NAME, METHOD_NAME, PARAM_CODE);
jstring stringMessage = method.env->NewStringUTF(message);
method.env->CallStaticVoidMethod(method.classID, method.methodID, stringMessage);
method.env->DeleteLocalRef(stringMessage);
}
如我们所见,这里我们使用了 Cocos2d-x 框架中捆绑的JniMethodInfo
结构和JniHelper
类,以调用showToast
方法,并向它发送 C++代码中的 c 字符串,该字符串被转换成了 Java 字符串。
现在让我们在我们的HelloWorldScene.cpp
实现文件中包含JniBridge.h
头文件,这样我们就可以从主场景类内部访问到 Java 代码的桥梁:
#include "JniBridge.h"
现在在位于HelloWorld.cpp
实现文件中的init
方法末尾,我们将调用showToast
静态方法,以便使用 Android Java API 显示一个原生提示消息,显示从我们的 C++代码发送的文本,如下所示:
JniBridge::showToast("Hello Java");
这将产生以下结果:
正如我们从之前的截图中可以看出的,我们已经实现了从 C++游戏逻辑代码中显示原生 Java 提示消息的目标。
通过插入 Java 代码将广告添加到游戏中
在上一节中,我们通过使用 JNI,在我们的 C++游戏逻辑代码和 Android 应用的 Java 层之间创建了一个交互。在本节中,我们将修改我们的 Android 特定代码,以便在 Android 游戏中显示谷歌AdMob横幅。
注意
AdMob 是谷歌的一个平台,通过展示广告,它可以让你的应用实现盈利,同时它还具备分析工具和应用程序内购买的工具。
配置环境
为了显示谷歌 AdMob 横幅,我们需要将Google Play Services
库添加到我们的项目中。为此,我们首先需要通过使用 Android SDK 管理器下载它及其依赖项,即 Android 支持库:
成功下载Google Play Services及其依赖项后,你需要将 Android.support.v4 添加到你的项目中,因为 Google Play Services 库需要它。为此,我们将复制位于以下路径的android-support-v4.jar
文件:<ADT PATH>\sdk\extras\android\support\v4
到 Android 项目中的libs
文件夹,然后我们通过在 Eclipse 的包资源管理器中右键点击项目,选择构建路径,然后点击配置构建路径,将其添加到我们的构建路径中。Java 构建路径配置窗口将出现,点击添加 JARS…按钮并在libs
文件夹中添加android-support-v4.jar
文件。
现在,我们将复制我们刚刚下载的 Google Play Services 代码。该代码现在位于<ADT PATH>\sdk\extras\google\google_play_services
到我们的工作空间路径。您可以通过右键点击您的 Eclipse Java 项目,然后点击属性,最后选择左侧的资源选项来找出您的工作空间路径;在那里您将看到位置信息,如下面的截图所示:
我们已经设置了依赖项,现在让我们通过导航到文件 | 导入 | Android | 将现有 Android 代码导入工作空间 | 浏览…来添加 Google Play Services 库。然后,浏览到您在上一步中复制 Google Play Services 的位置。取消选择除google-play-services_lib
之外的所有项目,并点击完成:
既然我们的工作空间中已经有了google-play-services_lib
项目,让我们将其配置为 Cocos2d-x 游戏项目的库。为此,我们再次在包资源管理器中右键点击我们的项目,点击属性,在左侧窗格中选择Android部分,然后在屏幕底部的下方,我们将点击添加…按钮,以便将google-play-services_lib
库添加到我们的 Eclipse 项目中,如下面的截图所示:
现在我们已经准备就绪,可以进入下一部分,我们将使用刚刚添加的库来显示 Google AdMob 广告。
既然我们的 AdMob 横幅将显示在屏幕顶部,我们现在将把静音按钮移动到底部,这样就不会被横幅覆盖。我们将通过更改静音和取消静音按钮的位置来实现这一点。不再将屏幕高度减去静音精灵高度的一半作为其垂直位置,我们现在将其y组件设置为屏幕高度减去静音按钮高度的两倍,如下面的代码行所示,在initMuteButton
方法中:
_muteItem->setPosition(Vec2(_visibleSize.width - _muteItem->getContentSize().width/2 ,_visibleSize.height - _muteItem->getContentSize().height * 2));
修改 Android 清单
在本节中,我们将修改 Android 清单,以便插入使用 Google Play Services 库所需的配置。
我们只需要添加两个代码片段,其中之一将紧邻打开的应用程序标签,指示正在使用的 Google Play Services 版本,如下面的代码列表所示:
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
我们将要添加的第二个代码片段是AdActivity
声明,它将紧邻我们游戏活动的声明添加,以便我们的游戏能够识别 Google Play Services 库中的这个内置活动:
<activity
android:name="com.google.android.gms.ads.AdActivity"
android:configChanges="keyboard|keyboardHidden|orientation| screenLayout|uiMode|screenSize|smallestScreenSize" />
添加 Java 代码
既然我们已经配置了库并且修改了 Android 清单,广告库就可以使用了。我们将在AppActivity
类中添加一个广告初始化方法,并在调用其超类的实现之后调用它。
为了以下示例,我们将使用一个示例 AdMob ID,您可以将其替换为自己的 ID。您可以在www.google.com/admob
找到有关如何创建自己的 AdMob ID 的更多信息。
private void initAdMob() {
final String ADMOB_ID = "ca-app-pub-7870675803288590/4907722461";
final AdView adView;
final FrameLayout adViewLayout;
FrameLayout.LayoutParams adParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
adParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
AdRequest adRequest = new AdRequest.Builder().
addTestDevice(AdRequest.DEVICE_ID_EMULATOR).
addTestDevice("E8B4B73DC4CAD78DFCB44AF69E7B9EC4").build();
adView = new AdView(this);
adView.setAdSize(AdSize.SMART_BANNER);
adView.setAdUnitId(ADMOB_ID);
adView.setLayoutParams(adParams);
adView.loadAd(adRequest);
adViewLayout = new FrameLayout(this);
adViewLayout.setLayoutParams(adParams);
adView.setAdListener(new AdListener() {
@Override
public void onAdLoaded() {
adViewLayout.addView(adView);
}
});
this.addContentView(adViewLayout, adParams);
}
与上一节相比,我们不使用 JNI,因为我们根本不与 C++代码交互;相反,我们修改了由cocos
命令创建的 Android 活动,以便添加更多图形元素以查看在模板中定义的 OpenGL E 视图的另一侧。
我们只是以编程方式创建了一个帧布局,并向其中添加了一个adView
实例;最后,我们将这个帧布局作为内容视图添加到游戏活动中,然后通过使用重力布局参数指定其期望的位置,这样我们就能够在屏幕顶部显示 Google 广告。请注意,您可以修改广告的位置,即您希望它显示的位置,只需修改布局参数即可。
请注意,在广告成功加载后,我们将adView
添加到了我们的帧布局中。使用AdListener
,如果您在广告完成启动之前添加adView
实例,那么它将不会显示。
在将所有内容整合之后,这是我们的 Google AdMob 的样子:
将所有内容整合在一起
我们已经实现了将核心 Java 代码嵌入到我们的 Cocos2d-x 游戏中的目标。现在我们将展示本章中所有修改过的游戏部分。
在这里,我们展示了从零开始创建的 C++ JNI 桥(JniBridge.h
)的头文件:
#ifndef __JNI_BRIDGE_H__
#define __JNI_BRIDGE_H__
#include "cocos2d.h"
class JniBridge
{
public:
static void showToast(const char* message);
};
#endif
既然我们已经定义了我们的JniBridge
的头文件,让我们编写实现文件(JniBridge.cpp
):
#include "JniBridge.h"
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "com/packtpub/jni/JniFacade"
#define METHOD_NAME "showToast"
#define PARAM_CODE "(Ljava/lang/String;)V"
USING_NS_CC;
void JniBridge::showToast(const char* message)
{
JniMethodInfo method;
JniHelper::getStaticMethodInfo(method, CLASS_NAME, METHOD_ NAME,PARAM_CODE);
jstring stringMessage = method.env->NewStringUTF(message);
method.env->CallStaticVoidMethod(method.classID, method.methodID, stringMessage);
method.env->DeleteLocalRef(stringMessage);
}
现在让我们看看在包含了我们的JniBridge
之后,我们的游戏玩法类头文件(HelloWorldScene.h
)的样子:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "PauseScene.h"
#include "GameOverScene.h"
#include "JniBridge.h"
class HelloWorld : public cocos2d::Layer
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
void pauseCallback(cocos2d::Ref* pSender);
CREATE_FUNC(HelloWorld);
private:
cocos2d::Director *_director;
cocos2d::Size visibleSize;
cocos2d::Sprite* _sprBomb;
cocos2d::Sprite* _sprPlayer;
cocos2d::Vector<cocos2d::Sprite*> _bombs;
cocos2d::MenuItemImage* _muteItem;
cocos2d::MenuItemImage* _unmuteItem;
int _score;
int _musicId;
void initPhysics();
bool onCollision(cocos2d::PhysicsContact& contact);
void setPhysicsBody(cocos2d::Sprite* sprite);
void initTouch();
void movePlayerByTouch(cocos2d::Touch* touch, cocos2d::Event* event);
bool explodeBombs(cocos2d::Touch* touch, cocos2d::Event* event);
void movePlayerIfPossible(float newX);
void movePlayerByAccelerometer(cocos2d::Acceleration* acceleration, cocos2d::Event* event);
void initAccelerometer();
void initBackButtonListener();
void onKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
void updateScore(float dt);
void addBombs(float dt);
void initAudio();
void initAudioNewEngine();
void initMuteButton();
};
#endif // __HELLOWORLD_SCENE_H__
现在我们将向您展示在本书的最后一章末尾,HelloWorldScene.cpp
方法的样子:
#include "HelloWorldScene.h"
USING_NS_CC;
using namespace CocosDenshion;
using namespace cocos2d::experimental;
// User input handling code …
void HelloWorld::initMuteButton()
{
_muteItem = MenuItemImage::create("mute.png", "mute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_muteItem->setPosition(Vec2(_visibleSize.width - _muteItem->getContentSize().width/2 ,
_visibleSize.height - _muteItem->getContentSize().height * 2));
我们在代码中更改了静音按钮的位置,使其不被广告覆盖:
_unmuteItem = MenuItemImage::create("unmute.png", "unmute.png", CC_CALLBACK_1(HelloWorld::muteCallback, this));
_unmuteItem->setPosition(Vec2(_visibleSize.width - _unmuteItem->getContentSize().width/2 ,
_visibleSize.height - _unmuteItem->getContentSize().height *2));
_unmuteItem -> setVisible(false);
auto menu = Menu::create(_muteItem, _unmuteItem , nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 2);
}
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
_score = 0;
_director = Director::getInstance();
visibleSize = _director->getVisibleSize();
auto origin = _director->getVisibleOrigin();
auto closeItem = MenuItemImage::create("CloseNormal.png", "CloseSelected.png", CC_CALLBACK_1(HelloWorld::pauseCallback, this));
closeItem->setPosition(Vec2(visibleSize.width - closeItem->getContentSize().width/2 , closeItem->getContentSize().height/2));
auto menu = Menu::create(closeItem, nullptr);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);
_sprBomb = Sprite::create("bomb.png");
_sprBomb->setPosition(visibleSize.width / 2, visibleSize.height + _sprBomb->getContentSize().height/2);
this->addChild(_sprBomb,1);
auto bg = Sprite::create("background.png");
bg->setAnchorPoint(Vec2());
bg->setPosition(0,0);
this->addChild(bg, -1);
_sprPlayer = Sprite::create("player.png");
_sprPlayer->setPosition(visibleSize.width / 2, visibleSize.height * 0.23);
setPhysicsBody(_sprPlayer);
this->addChild(_sprPlayer, 0);
//Animations
Vector<SpriteFrame*> frames;
Size playerSize = _sprPlayer->getContentSize();
frames.pushBack(SpriteFrame::create("player.png", Rect(0, 0, playerSize.width, playerSize.height)));
frames.pushBack(SpriteFrame::create("player2.png", Rect(0, 0, playerSize.width, playerSize.height)));
auto animation = Animation::createWithSpriteFrames(frames,0.2f);
auto animate = Animate::create(animation);
_sprPlayer->runAction(RepeatForever::create(animate));
setPhysicsBody(_sprBomb);
initPhysics();
_sprBomb->getPhysicsBody()->setVelocity(Vect(0,-100));
initTouch();
initAccelerometer();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
setKeepScreenOnJni(true);
#endif
initBackButtonListener();
schedule(schedule_selector(HelloWorld::updateScore), 3.0f);
schedule(schedule_selector(HelloWorld::addBombs), 8.0f);
initAudioNewEngine();
initMuteButton();
_bombs.pushBack(_sprBomb);
JniBridge::showToast("Hello Java");
return true;
}
在我们所有的修改之后,这是我们的AppActivity.java
类的样子:
package org.cocos2dx.cpp;
import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.FrameLayout;
import com.google.android.gms.ads.AdListener;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdSize;
import com.google.android.gms.ads.AdView;
public class AppActivity extends Cocos2dxActivity {
private static Activity instance;
private void initAdMob() {
final String ADMOB_ID = "ca-app-pub-7870675803288590/4907722461";
final AdView adView;
final FrameLayout adViewLayout;
FrameLayout.LayoutParams adParams = new FrameLayout. LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.WRAP_CONTENT);
adParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
AdRequest adRequest = new AdRequest.Builder().
addTestDevice(AdRequest.DEVICE_ID_EMULATOR).
addTestDevice("E8B4B73DC4CAD78DFCB44AF69E7B9EC4").build();
adView = new AdView(this);
adView.setAdSize(AdSize.SMART_BANNER);
adView.setAdUnitId(ADMOB_ID);
adView.setLayoutParams(adParams);
adView.loadAd(adRequest);
adViewLayout = new FrameLayout(this);
adViewLayout.setLayoutParams(adParams);
adView.setAdListener(new AdListener() {
@Override
public void onAdLoaded() {
adViewLayout.addView(adView);
}
});
this.addContentView(adViewLayout, adParams);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
instance = this;
super.onCreate(savedInstanceState);
initAdMob();
}
public static Activity getInstance() {
return instance;
}
}
这是我们本章末尾的JniFacade.java
类文件的样子:包com.packtpub.jni
:
import org.cocos2dx.cpp.AppActivity;
import android.app.Activity;
import android.widget.Toast;
public class JniFacade {
private static Activity activity = AppActivity.getInstance();
public static void showToast(final String message) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity.getBaseContext(), message, Toast. LENGTH_SHORT).show();
}
}
}
}
在本章中添加了我们的JniBridge.cpp
文件后,这是我们位于proj.android\jni
的Android.mk
文件的样子:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
$(call import-add-path,$(LOCAL_PATH)/../../cocos2d)
$(call import-add-path,$(LOCAL_PATH)/../../cocos2d/external)
$(call import-add-path,$(LOCAL_PATH)/../../cocos2d/cocos)
LOCAL_MODULE := cocos2dcpp_shared
LOCAL_MODULE_FILENAME := libcocos2dcpp
LOCAL_SRC_FILES := hellocpp/main.cpp \
../../Classes/JniBridge.cpp \
../../Classes/AppDelegate.cpp \
../../Classes/PauseScene.cpp \
../../Classes/GameOverScene.cpp \
../../Classes/HelloWorldScene.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes
LOCAL_STATIC_LIBRARIES := cocos2dx_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module,.)
最后,这是本书末尾的AndroidManifest.xml
文件的样子:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.packt.happybunny"
android:installLocation="auto"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="9" />
<uses-feature android:glEsVersion="0x00020000" />
<application
android:icon="@drawable/icon"
android:label="@string/app_name" >
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<!-- Tell Cocos2dxActivity the name of our .so -->
<meta-data
android:name="android.app.lib_name"
android:value="cocos2dcpp" />
<activity
android:name="org.cocos2dx.cpp.AppActivity"
android:configChanges="orientation"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.google.android.gms.ads.AdActivity"
android:configChanges="keyboard|keyboardHidden|orientation| screenLayout|uiMode|screenSize|smallestScreenSize" />
</application>
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
概括
在本章中,我们学习了如何通过使用 JNI,在 C++游戏逻辑代码与 Android 的核心 Java 层之间添加交互,我们还通过直接修改在执行cocos
命令时创建的 Java Activity
类代码,在游戏中展示了 Google AdMob 横幅广告。