Cocos2dx-秘籍-全-

Cocos2dx 秘籍(全)

原文:zh.annas-archive.org/md5/dffde412782476e8d2c8a68a14d9ff20

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Cocos2d-x 是一套开源的跨*台游戏开发工具,被全球数千名开发者使用。Cocos2d-x 是用 C++编写的游戏框架,具有薄的*台相关层。完全用 C++编写的核心引擎具有最小的占用空间和最快的速度,并且优化以在各种设备上运行。

通过这本书,我们旨在为你提供从零开始使用 Cocos2d-x 创建 2D 游戏的详细指南。你将学习从基础阶段到高级水*的一切,我们将帮助你成功使用 Cocos2d-x 创建游戏。

这本书涵盖了哪些内容

第一章, Cocos2d-x 入门,涵盖了 Cocos2d-x 的安装过程,也教你如何创建项目,并讨论了如何为多*台构建项目。

第二章, 创建精灵,教你如何创建精灵、动画和动作。

第三章, 与标签一起工作,展示了如何显示字符串,并创建标签。

第四章, 构建场景和层,教你如何创建场景和层,以及如何更改场景。

第五章, 创建 GUIs,讨论了创建游戏必不可少的 GUI 部分,如按钮和开关。

第六章, 播放声音,提供了关于播放背景音乐和音效的信息。

第七章, 与资源文件一起工作,教你如何管理资源文件,也讨论了如何使用数据库。

第八章, 与硬件一起工作,指导你如何访问原生功能。

第九章, 控制物理,告诉你如何在精灵上使用物理。

第十章, 使用额外功能改进游戏,教你如何在 Cocos2d-x 中使用额外功能,以及使用各种工具。

第十一章, 利用优势,讨论了在游戏中使用实用技巧,以及如何改进游戏。

你需要这本书的哪些东西

你将需要一个运行 OS X 10.10 Yosemite 的 Mac。我们将在本书中使用的多数工具都可以免费下载和试用。我们已经解释了如何下载和安装它们。

这本书面向的对象是谁

如果你是一名游戏开发者,并且想学习更多关于 Cocos2d-x 的跨*台游戏开发,那么这本书适合你。了解 C++、Xcode、Eclipse 以及如何在终端中使用命令是这本书的先决条件。

部分

在本书中,您会发现几个频繁出现的标题(准备工作、如何操作、工作原理、更多内容、参见)。

为了清楚地说明如何完成食谱,我们使用以下部分:

准备工作

本节将告诉你可以在食谱中期待什么,并描述如何设置任何软件或食谱所需的任何初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包含对上一节发生情况的详细解释。

更多内容…

本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。

参见

本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。

惯例

在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"如果您需要编译除.cpp扩展名以外的文件。"

代码块设置如下:

CPP_FILES := $(shell find $(LOCAL_PATH)/../../Classes -name *.cpp)
LOCAL_SRC_FILES := hellocpp/main.cpp
LOCAL_SRC_FILES += $(CPP_FILES:$(LOCAL_PATH)/%=%)

LOCAL_C_INCLUDES := $(shell find $(LOCAL_PATH)/../../Classes -type d)

任何命令行输入或输出都应如下书写:

$ ./build_native.py

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中如下所示:"您选择Android 应用程序并点击确定。"

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小技巧和技巧以如下形式出现。

读者反馈

读者的反馈总是受欢迎的。让我们知道您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从您的账户中下载示例代码文件,这些文件适用于您购买的所有 Packt Publishing 书籍。www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从 www.packtpub.com/sites/default/files/downloads/B00561_Graphics.pdf 下载此文件。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章. Cocos2d-x 入门

在本章中,我们将安装 Cocos2d-x 并设置开发环境。本章将涵盖以下主题:

  • 设置我们的 Android 开发环境

  • 安装 Cocos2d-x

  • 使用 Cocos 命令

  • 使用 Xcode 构建项目

  • 使用 Eclipse 构建项目

  • 实现多分辨率支持

  • 准备你的原始游戏

简介

Cocos2d-x 是一个开源的跨*台游戏引擎,它是免费且成熟的。它可以发布适用于移动设备和桌面设备(包括 iPhone、iPad、Android、Kindle、Windows 和 Mac)的游戏。Cocos2d-x 是用 C++编写的,因此可以在任何*台上构建。Cocos2d-x 是用 C++编写的开源项目,因此我们可以自由地阅读游戏框架。Cocos2d-x 不是一个黑盒,这在我们使用它时证明是一个很大的优势。支持 C++11 的 Cocos2d-x 版本 3 最*才发布。它还支持 3D,并具有改进的渲染性能。本书专注于使用 3.4 版本,这是本书写作时可用的最新版本的 Cocos2d-x。本书还专注于 iOS 和 Android 开发,我们将使用 Mac,因为我们需要它来开发 iOS 应用程序。本章解释了如何设置 Cocos2d-x。

设置我们的 Android 开发环境

准备工作

我们首先设置我们的 Android 开发环境。如果你只想在 iOS 上构建,你可以跳过此步骤。要遵循此食谱,你需要一些文件。

以下列表提供了设置 Android 所需下载的先决条件:

  • Eclipse ADT(Android 开发者工具)与 Android SDK:

    dl.google.com/android/adt/adt-bundle-mac-x86_64-20140702.zip

    Eclipse ADT(Android 开发者工具)包括 Android SDK 和 Eclipse IDE。这是用于开发 Android 应用程序的 Android 开发工具。Android Studio 是一个 Android 开发 IDE,但不支持构建 NDK。官方网站表示,将很快发布支持 NDK 的 Android Studio 版本。这就是为什么我们在本书中使用 Eclipse 的原因。

  • Android NDK(本地开发工具包):

    dl.google.com/android/ndk/android-ndk-r10c-darwin-x86_64.bin

    构建 Android 应用程序需要 NDK(Native Development Kit)。你必须使用 NDK r10c。这是因为使用 NDK r9 或更早版本时可能会出现编译和链接错误。

  • Apache ANT:

    你可以从ant.apache.org/bindownload.cgi下载 Apache ANT

    这是一个帮助构建软件的 Java 库。在本书编写时,版本 1.9.4 是可用的最新稳定版本。

如何做到这一点...

  1. 您首先使用 Android SDK 安装 Eclipse ADT,然后继续解压缩 zip 文件到您所知的任何工作目录。我建议您将其解压缩到 Documents 文件夹 (~/adt-bundle-mac-x86_64-20140702)。ADT 包括 Android SDK 和 Eclipse。SDK 和 Eclipse 文件夹位于 ADT 文件夹下。我们将位于 ADT 文件夹下的 SDK 文件夹路径称为 ANDROID_SDK_ROOT。您必须记住它,因为您将在下一个菜谱中使用它。现在,您可以从 ~/adt-bundle-mac-x86_64-20140702/eclipse/Eclipse.app 启动 Eclipse。

  2. 下一步是更新 Android SDK:

    • 从 ADT 中位于 eclipse 文件夹的 Eclipse 启动。

    • 前往 窗口 | Android SDK 管理器

    • 在打开 Android SDK 管理器 后,检查 工具 和最新的 Android SDK (API21),Android 2.3.3(API10),以及如果需要,任何其他 SDK,如图所示:如何操作...

    • 点击 安装包...

    • 选择每个许可并点击 接受,如图所示:如何操作...

    • 接受所有许可后,您将看到安装按钮已启用。点击它。

    • 您需要等待很长时间才能更新和安装 SDK。

  3. 安装 NDK:

    打开终端窗口,将目录更改为您下载包的路径。更改下载包的权限并执行包。例如:

    $ chmod 700 android-ndk-r10c-darwin-x86_64.bin
    $ ./android-ndk-r10c-darwin-x86_64.bin
    
    

    最后,您将 NDK 文件夹移动到 Documents 文件夹。我们称 NDK 的安装路径为 NDK_ROOTNDK_ROOT 是包含文件的文件夹的地址,它帮助 Cocos2dx 引擎定位 Android 的本地文件。您必须记住 NDK_ROOT,因为您将在下一个菜谱中使用它。

  4. 安装 Apache ANT:

    将文件解压缩到 Documents 文件夹。这就完成了。我们将 ANT 的安装路径称为 ANT_ROOT。您必须记住 ANT_ROOT,因为我们将在下一个菜谱中使用它。

  5. 安装 Java:

    通过在终端中输入以下命令,您可以自动安装 Java(如果您之前尚未安装):

    $ java --version
    
    

    安装完成后,您可以通过再次输入命令来检查它是否已成功安装。

它是如何工作的...

让我们看看在整个过程中我们做了什么:

  • 安装 Eclipse:您可以将 Eclipse 用作 Cocos2d-x 的编辑器

  • 安装 ADT:您可以在 Eclipse 上开发 Android 应用程序

  • 安装 NDK:您可以为 Java 构建一个 C++ 源代码

  • 安装 ANT:您可以使用 Cocos2d-x 的命令行工具

现在您已经完成了 Android 开发环境的设置。此时,您知道如何安装它们及其路径。在下一个菜谱中,您将使用它们来构建和执行 Android 应用程序。当您想要调试 Android 应用程序时,这将非常有用。

安装 Cocos2d-x

准备中

要遵循此食谱,你需要从 Cocos2d-x 的官方网站下载 zip 文件(www.cocos2d-x.org/download)。

在撰写这本书的时候,版本 3.4 是当时可用的最新稳定版本。这本书将使用这个版本。

如何操作...

  1. 将你的文件解压到任何文件夹中。这次,我们将安装到用户的家目录。例如,如果用户名是 syuhari,那么安装路径是 /Users/syuhari/cocos2d-x-3.4。在这本书中,我们称之为 COCOS_ROOT

  2. 以下步骤将指导你完成设置 Cocos2d-x 的过程:

    • 打开终端

    • 在终端中更改目录到 COCOS_ROOT,使用以下命令:

      $ cd ~/cocos2d-x-v3.4
      
      
    • 使用以下命令运行 setup.py

      $ ./setup.py
      
      
    • 终端会要求你输入 NDK_ROOT。输入到 NDK_ROOT 路径。

    • 终端会要求你输入 ANDROID_SDK_ROOT。输入 ANDROID_SDK_ROOT 路径。

    • 最后,终端会要求你输入 ANT_ROOT。输入 ANT_ROOT 路径。

    • 执行 setup.py 命令后,你需要执行以下命令来添加系统变量:

      $ source ~/.bash_profile
      
      

      小贴士

      打开 .bash_profile 文件,你会看到 setup.py 展示了如何在系统中设置每个路径。你可以使用 cat 命令查看 .bash_profile 文件:

      $ cat ~/.bash_profile
      
      
  3. 我们现在验证 Cocos2d-x 是否可以安装:

    • 打开终端并运行不带参数的 cocos 命令:

      $ cocos
      
      
    • 如果你看到一个像以下截图所示的窗口,说明你已经成功完成了 Cocos2d-x 的安装过程:

    如何操作...

它是如何工作的...

让我们看看在上面的食谱中我们做了什么。你可以通过解压来安装 Cocos2d-x。你知道 setup.py 只是在环境中设置 cocos 命令和 Android 构建路径。安装 Cocos2d-x 非常简单和直接。如果你想安装 Cocos2d-x 的不同版本,你也可以这样做。要做到这一点,你需要遵循这个食谱中给出的相同步骤,但它们将适用于不同的版本。

更多内容...

设置 Android 环境有点困难。如果你最*开始开发 Cocos2d-x,你可以跳过 Android 的设置部分,并在 Android 上运行时进行设置。在这种情况下,你不需要安装 Android SDK、NDK 和 Apache ANT。此外,当你运行 setup.py 时,你只需按 Enter 键,无需为每个问题输入路径。

使用 Cocos 命令

下一步是使用 cocos 命令。这是一个跨*台工具,你可以用它来创建新项目、构建、运行和部署。cocos 命令适用于所有 Cocos2d-x 支持的*台,如果你不想使用 IDE,你也不需要使用它。在这个食谱中,我们将查看这个命令并解释如何使用它。

如何操作...

  1. 你可以通过执行带有 --help 参数的 cocos 命令来使用 cocos 命令的帮助,如下所示:

    $ cocos --help
    
    
  2. 我们接下来将生成我们的新项目:

    首先,我们使用cocos new命令创建一个新的 Cocos2d-x 项目,如下所示:

    $ cocos new MyGame -p com.example.mygame -l cpp -d ~/Documents/
    
    

    此命令的结果如下截图所示:

    如何操作...

    new参数后面是项目名称。提到的其他参数表示以下内容:

    • MyGame是项目的名称。

    • -p是 Android 的包名。这是 Google Play 商店中的应用程序 ID。因此,你应该使用反向域名作为唯一名称。

    • -l是项目使用的编程语言。你应该使用cpp,因为我们将在本书中使用 C++。

    • -d是生成新项目的位置。这次,我们在用户的文档目录中生成。

    你可以使用以下命令查找这些变量:

    $ cocos new —help
    
    

    恭喜你,你可以生成你的新项目。下一步是使用cocos命令构建和运行。

  3. 编译项目:

    如果你想为 iOS 构建和运行,你需要执行以下命令:

    $ cocos run -s ~/Documents/MyGame -p ios
    
    

    提到的参数解释如下:

    • -s是项目目录。这可以是绝对路径或相对路径。

    • -p表示要运行的*台。如果你想运行在 Android 上,使用-p android。可用选项有 IOS、Android、Win32、Mac 和 Linux。

    • 你可以通过运行cocos run –help来获取更详细的信息。

    此命令的结果如下截图所示:

    如何操作...

  4. 现在,你可以在 cocos2d-x 上构建和运行 iOS 应用程序。然而,如果你是第一次构建 iOS 应用程序,你可能需要等待很长时间。构建 Cocos2d-x 库需要很长时间,这取决于是否是干净构建或首次构建。如何操作...

它是如何工作的...

cocos命令可以创建新项目并构建它。如果你想创建新项目,你应该使用cocos命令。当然,你也可以使用 Xcode 或 Eclipse 来构建。你可以使用这些工具轻松开发和调试。

还有更多...

cocos run命令还有其他参数。它们如下所示:

  • --portrait会将项目设置为横幅模式。此命令没有参数。

  • --ios-bundleid将为 iOS 项目设置包 ID。然而,稍后设置它并不困难。

cocos命令还包括其他一些命令,如下所示:

  • compile命令:此命令用于构建项目。以下模式是有用的参数。如果你执行cocos compile [–h]命令,你可以看到所有参数和选项:

    cocos compile [-h] [-s SRC_DIR] [-q] [-p PLATFORM] [-m MODE]
    
    
  • deploy命令:此命令仅在目标*台为 Android 时生效。它将重新安装指定的项目到 Android 设备或模拟器:

    cocos deploy [-h] [-s SRC_DIR] [-q] [-p PLATFORM] [-m MODE]
    
    

    小贴士

    run命令继续编译和部署命令。

使用 Xcode 构建项目

准备工作

在使用 Xcode 构建项目之前,您需要一个带有 iOS 开发者账户的 Xcode 来在物理设备上进行测试。然而,您也可以在 iOS 模拟器上进行测试。如果您尚未安装 Xcode,您可以从 Mac App Store 获取它。一旦安装,请激活它。

如何操作...

  1. 从 Xcode 打开您的项目:

    您可以通过双击放置在以下位置的文件来打开您的项目:~/Documents/MyGame/proj.ios_mac/MyGame.xcodeproj

    如何操作...

  2. 使用 Xcode 进行构建和运行:

    您应该选择一个 iOS 模拟器或真实设备,您想在上面运行您的项目。

它是如何工作的...

如果这是您第一次构建,它将花费较长时间,但请有信心继续构建,因为这是第一次。如果您使用 Xcode 而不是 Eclipse 进行开发和调试,您可以更快地开发您的游戏。

使用 Eclipse 构建项目

准备工作

在开始此步骤之前,您必须完成第一个菜谱。如果您还没有完成,您将需要安装 Eclipse。

如何操作...

  1. 设置 NDK_ROOT

    • 打开 Eclipse 的首选项

    • 打开 C++ | Build | Environment如何操作...

  2. 点击 添加 并设置新变量,名称为 NDK_ROOT,其值为 NDK_ROOT 路径:如何操作...

  3. 将您的项目导入到 Eclipse 中:

    • 打开文件并点击 导入

    • 前往 Android | Existing Android Code into Workspace

    • 点击 下一步

    如何操作...

  4. ~/Documents/MyGame/proj.android 处将项目导入到 Eclipse:如何操作...

  5. 将 Cocos2d-x 库导入到 Eclipse 中:

    • 从步骤 3 到步骤 4 执行相同的步骤。

    • 使用以下命令在 ~/Documents/MyGame/cocos2d/cocos/platform/android/java 处导入项目 cocos2d lib

      importing cocos2d lib
      
      

    如何操作...

  6. 构建和运行:

    • 点击 Run 图标

    • 第一次运行时,Eclipse 将会要求您选择运行应用程序的方式。选择 Android Application 并点击 确定,如图所示:如何操作...

    • 如果您已将 Android 设备连接到您的 Mac,您可以在真实设备或模拟器上运行您的游戏。以下截图显示它在 Nexus5 上运行:如何操作...

  7. 如果您将 cpp 文件添加到您的项目中,您必须修改 ~/Documents/MyGame/proj.android/jni/Android.mk 中的 Android.mk 文件。此文件是构建 NDK 所必需的。为了添加文件,需要进行此修复。

  8. 原始的 Android.mk 可能如下所示:

    LOCAL_SRC_FILES := hellocpp/main.cpp \
                       ../../Classes/AppDelegate.cpp \
                       ../../Classes/HelloWorldScene.cpp
    
  9. 如果您添加了 TitleScene.cpp 文件,您必须按照以下代码进行修改:

    LOCAL_SRC_FILES := hellocpp/main.cpp \
                       ../../Classes/AppDelegate.cpp \
                       ../../Classes/HelloWorldScene.cpp \
                       ../../Classes/TitleScene.cpp 
    

上述示例显示了添加 TitleScene.cpp 文件的一个实例。然而,如果您还添加了其他文件,您需要添加所有添加的文件。

它是如何工作的...

当你将项目导入 Eclipse 时,你会遇到很多错误,但不要慌张。在导入 Cocos2d-x 库后,错误很快就会消失。这允许我们设置 NDK 的路径,Eclipse 可以编译 C++。在你修改了 C++ 代码后,在 Eclipse 中运行你的项目。Eclipse 会自动编译 C++ 代码、Java 代码,然后运行。

再次修复 Android.mk 以添加 C++ 文件是一项繁琐的任务。以下代码是原始的 Android.mk

LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes

以下代码是自动添加 C++ 文件的定制 Android.mk

CPP_FILES := $(shell find $(LOCAL_PATH)/../../Classes -name *.cpp)
LOCAL_SRC_FILES := hellocpp/main.cpp
LOCAL_SRC_FILES += $(CPP_FILES:$(LOCAL_PATH)/%=%)

LOCAL_C_INCLUDES := $(shell find $(LOCAL_PATH)/../../Classes -type d)

代码的第一行将 C++ 文件移动到 Classes 目录,并将它们添加到 CPP_FILES 变量中。第二行和第三行将 C++ 文件添加到 LOCAL_C_INCLUDES 变量中。这样做后,C++ 文件将自动在 NDK 中编译。如果你需要编译扩展名不是 .cpp 的文件,你需要手动添加它。

还有更多...

如果你想在 NDK 中手动构建 C++,可以使用以下命令:

$ ./build_native.py

这个脚本位于 ~/Documents/MyGame/proj.android。它使用了 ANDROID_SDK_ROOTNDK_ROOT。如果你想查看它的选项,运行 ./build_native.py –help

实现多分辨率支持

你可能会在不同设备上注意到屏幕外观的差异。在一些之前的菜谱中,有一个 iOS 的屏幕截图和一个 Nexus 5 的屏幕截图。它们显示了不同的图像大小。这张图片是位于 MyGame/ResourcesHelloWorld.png,分辨率为 480 x 320 像素。在这个菜谱中,我们解释了如何保持相同的大小,无论屏幕大小如何。

如何做到这一点…

通过 Xcode 打开 AppDelegate.cpp,并在 director->setAnimationInterval(1.0/60.0); 行之后添加代码,如下所示:

director->setAnimationInterval(1.0 / 60);
glview->setDesignResolutionSize(640, 960, ResolutionPolicy::NO_BORDER);

在这本书中,我们设计游戏时使用了 iPhone 3.5 英寸屏幕的屏幕尺寸。因此,我们使用 setDesignResolutionSize 方法将这个屏幕尺寸设置为设计分辨率大小。以下截图是在实现多分辨率后的 Nexus 5 的截图:

如何做到这一点…

以下截图是 iPhone 5 模拟器的截图。你现在知道这两个截图的外观是相同的:

如何做到这一点…

它是如何工作的…

分辨率策略有 EXACT_FITNO_BORDERSHOW_ALLFIXED_HEIGHTFIXED_WIDTH。以下是对它们的解释:

  • EXACT_FIT:整个应用程序在指定区域内可见,不尝试保留原始宽高比。

  • NO_BORDER:整个应用程序填充指定区域,无扭曲但可能有些裁剪,同时保持应用程序的原始宽高比。

  • SHOW_ALL:整个应用程序在指定区域内可见,无扭曲,同时保持应用程序的内部宽高比。应用程序的两侧可能会出现边框。

  • FIXED_HEIGHT: 应用程序采用设计分辨率大小的高度,并修改内部画布的宽度,以便适应设备的纵横比。不会发生扭曲,但是你必须确保你的应用程序可以在不同的纵横比上运行。

  • FIXED_WIDTH: 应用程序采用设计分辨率大小的宽度,并修改内部画布的高度,以便适应设备的纵横比。不会发生扭曲,但是你必须确保你的应用程序可以在不同的纵横比上运行。

通过实现多分辨率,无论屏幕大小如何,你都将保持屏幕上的图像。

准备你的原始游戏

在下一章中,我们将开始原始游戏。你知道HelloWorldScene.cppHelloWorldScene.h文件中有很多注释和代码。这就是为什么我们将从模板项目中删除不必要的代码,并立即开始原始游戏。

如何操作…

  1. 打开HelloWorldScene.h并删除menuCloseCallback方法和不必要的注释。现在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();
        CREATE_FUNC(HelloWorld);
    };
    #endif // __HELLOWORLD_SCENE_H__
    
  2. 下一步是打开HelloWorldScene.cpp并删除不必要的注释、代码和方法。现在HelloWorldScene.cpp应该看起来像以下代码:

    #include "HelloWorldScene.h"
    USING_NS_CC; 
    Scene* HelloWorld::createScene()
    {
        auto scene = Scene::create();
        auto layer = HelloWorld::create();
        scene->addChild(layer);
        return scene;
    } 
    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
        return true;
    }
    
  3. 下一步是删除resources中的不必要图像。从 Xcode 中的Resources文件夹中删除CloseNormal.pngCloseSelected.pngHelloWorld.png如何操作…

  4. 最后,如果你只开发 iOS 和 Android 应用程序,你不需要为 Linux、Windows 和 Windows Phone 等其他*台准备文件。你应该删除这些文件。

    在删除*台文件之前,它应该看起来像以下截图:

    如何操作…

    删除*台文件后,它应该看起来像以下截图:

    如何操作…

它是如何工作的…

使用这个配方,你可以在删除不必要的注释、代码和方法之前准备好最简单的项目。删除不必要的*台代码和资源对于减小应用程序的大小很重要。如果你从头开始构建你的原始游戏,你需要遵循这个配方,否则,如果你构建并运行这个项目,可能会得到一个黑屏。在下一章中,你可以在这个简单项目中开始编码。

第二章。创建精灵

在本章中,我们将创建精灵、动画和动作。本章将涵盖以下主题:

  • 创建精灵

  • 获取精灵的位置和大小

  • 操作精灵

  • 创建动画

  • 创建动作

  • 控制动作

  • 调用带动作的函数

  • 缓动动作

  • 使用纹理图集

  • 使用批节点

  • 使用 3D 模型

  • 检测碰撞

  • 绘制形状

简介

精灵是 2D 图像。我们可以通过改变它们的属性来动画化和变换它们。精灵基本上是项目中的项目,没有它们您的游戏就不完整。精灵不仅被显示,还可以被变换或移动。在本章中,您将学习如何使用 Cocos2d-x 中的 3D 模型创建精灵,然后我们将探讨精灵的优势。

创建精灵

精灵是游戏中最重要的东西。它们是在屏幕上显示的图像。在本食谱中,您将学习如何创建精灵并显示它。

准备工作

您可以通过以下步骤将上一章制作的图像添加到项目中:

  1. 将图像复制到Resource文件夹MyGame/Resources/res中。

  2. 在 Xcode 中打开您的项目。

  3. 从 Xcode 菜单转到产品 | 清理

当您将新图像添加到resource文件夹时,您必须进行清理和构建。如果您在添加新图像后没有进行清理,那么 Xcode 将无法识别它们。最后,在您将run_01.png添加到项目中后,您的项目看起来将如下截图所示:

准备工作

如何操作...

我们从以下代码中修改HelloWorld::init方法开始:

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    Size size = Director::getInstance()->getWinSize();
    auto sprite = Sprite::create("res/run_01.png");
   sprite->setPosition(Vec2(size.width/2, size.height/2));
    this->addChild(sprite);
    return true;
}

然后,在我们构建并运行项目之后,我们可以看到以下内容:

如何操作...

它是如何工作的...

您可以从Director::getWinSize方法获取屏幕大小。Director类是一个单例类。您可以使用getInstance方法获取其实例。因此,您可以通过Director::getInstance->getWinSize()获取屏幕大小。

小贴士

请注意,您可以使用getInstance方法在 Cocos2d-x 中获取单例类的实例。

精灵是由图像组成的。您可以通过指定图像来创建精灵。在这种情况下,您通过res文件夹中的run_01.png创建精灵。

接下来,您需要指定精灵的坐标。在这种情况下,您将精灵设置在屏幕中心。Size类具有宽度和高度属性。您可以使用setPosition方法指定精灵的位置。setPosition方法的参数是Vec2Vec2有两个属性,作为浮点向量,x轴坐标和y轴坐标。

最后一步是将精灵添加到图层上。图层就像屏幕上的透明纸。您将在第四章中了解图层,构建场景和图层

屏幕上显示的所有对象都是节点。精灵和层是节点的类型。如果您在其他节点中没有添加它,该节点将不会显示在屏幕上。您可以通过addChild方法在其他节点中添加节点。

更多内容...

您可以使用静态坐标设置精灵。在以下情况下,我们看到精灵位置是(100, 200)

sprite->setPosition(Vec2(100, 200));

此外,您可以使用 C++运算符重载将精灵设置在屏幕中心。

sprite->setPosition(size/2);

如果您想从层中移除精灵,可以使用以下代码移除它:

sprite->removeFromParent();

相关内容

Sprite类有很多属性。您可以操作它们并更改精灵的外观。您还将了解更多关于层和场景的内容,这些内容将在第四章 构建场景和层 中解释。

获取精灵的位置和大小

精灵有一个特定的尺寸和位置。在本菜谱中,我们解释了如何查看精灵的尺寸和位置。

如何操作...

要获取精灵位置,请使用以下代码:

Vec2 point = sprite->getPosition();
float x = point.x;
float y = point.y;

要获取精灵大小,请使用以下代码:

Size size = sprite->getContentSize();
float width = size.width;
float height = size.height;

工作原理...

默认情况下,精灵位置是(0,0)。您可以使用setPosition方法更改精灵位置,并使用getPosition方法获取它。您可以使用getContentSize方法获取精灵大小。但是,您不能通过setContentSize方法更改精灵大小。contentsize是一个常量值。如果您想更改精灵大小,您必须更改精灵的缩放。您将在下一个菜谱中了解缩放。

更多内容...

设置锚点

锚点是一个您设置的点,用作指定在设置位置时将使用精灵的哪个部分。锚点使用左下角坐标系。默认情况下,所有 Node 对象的锚点为(0.5, 0.5)。这意味着默认锚点是中心。

要获取精灵中心的锚点,我们使用以下代码:

sprite->setAnchorPoint(Vec2(0.5, 0.5));

要获取精灵左下角的锚点,我们使用以下代码:

sprite->setAnchorPoint(Vec2(0.0, 0.0));

要获取精灵左上角的锚点,我们使用以下代码:

sprite->setAnchorPoint(Vec2(1.0, 0.0));

要获取精灵右下角的锚点,我们使用以下代码:

sprite->setAnchorPoint(Vec2(0.0, 1.0));

要获取精灵右上角的锚点,我们使用以下代码:

sprite->setAnchorPoint(Vec2(1.0, 1.0));

以下图像显示了锚点的各种位置:

设置锚点

矩形

要获取精灵矩形,请使用以下代码:

Rect rect = sprite->getBoundingBox();
Size size = rect.size;
Vec2 point = rect.origin;

Rect是具有SizeVec2等属性的精灵矩形。如果缩放不等于一,则Rect中的Size将不等于size,使用getContentSize方法。getContentSizeSize是原始图像大小。另一方面,Rect中使用getBoundingBoxSize是外观的大小。例如,当您将精灵设置为半缩放时,Rect中使用getBoundingBoxSize是大小的一半,而使用getContentSizeSize是原始大小。精灵的位置和大小在您需要指定屏幕上的精灵时是一个非常重要的点。

相关内容

  • 检测碰撞配方,您可以使用rect检测碰撞。

操作精灵

精灵是一个可以通过改变其属性(包括旋转、位置、缩放、颜色等)进行动画或变换的 2D 图像。创建精灵后,您可以访问其各种属性,并进行操作。

如何做...

旋转

您可以将精灵的旋转更改为正或负度数。

sprite->setRotation(30.0f);

您可以使用getRotation方法获取旋转值。

float rotation = sprite->getRotation();

正值将其顺时针旋转,负值将其逆时针旋转。默认值是零。以下代码将精灵旋转 30 度顺时针,如下截图所示:

旋转

缩放

您可以更改精灵的缩放。默认值是1.0f,原始大小。以下代码将缩放到一半大小。

sprite->setScale(0.5f);

您还可以分别更改宽度和高度。以下代码只会将宽度缩放到一半。

sprite->setScaleX(0.5f);

以下代码只会将高度缩放到一半。

sprite->setScaleY(0.5f);

以下代码将宽度缩放到两倍,高度缩放到一半。

sprite->setScale(2.0f, 0.5f);

缩放

倾斜

您可以通过XY或同时针对XY均匀地更改精灵的倾斜。对于XY,默认值都是零。

以下代码将X倾斜调整为20.0

sprite->setSkewX(20.0f);

以下代码将Y倾斜调整为20.0

sprite->setSkewY(20.0f);

倾斜

颜色

您可以通过传递Color3B对象来更改精灵的颜色。Color3B具有 RGB 值。

sprite->setColor(Color3b(255, 0, 0));

颜色

透明度

您可以更改精灵的不透明度。不透明度属性设置为 0 到 255 之间的值。

sprite->setOpacity(100);

当精灵设置为 255 时,它是完全不透明的,当设置为零时,它是完全透明的。默认值始终是 255。

透明度

可见性

您可以通过传递布尔值来更改精灵的可见性。如果它是false,则精灵不可见;如果它是true,则精灵可见。默认值始终是true

sprite->setVisible(false);

小贴士

如果您想检查精灵的可见性,请使用isVisible方法而不是getVisible方法。精灵类没有getVisible方法。

if (sprite->isVisible()) {
    // visible
} else {
    // invisible
}

它是如何工作的...

精灵有许多属性。您可以使用settergetter方法操作精灵。

RGB 颜色是从 0 到 255 的 3 字节值。Cocos2d-x 提供了预定义的颜色。

Color3B::WHITE
Color3B::YELLOW
Color3B::BLUE
Color3B::GREEN
Color3B::RED
Color3B::MAGENTA
Color3B::BLACK
Color3B::ORANGE
Color3B::GRAY

您可以通过查看 Cocos2d-x 中的ccType.h文件来找到它们。

创建动画

当游戏中的角色开始移动时,游戏将变得生动。有许多方法可以制作动画角色。在这个菜谱中,我们将通过使用多个图像来动画化一个角色。

准备工作

您可以从以下一系列图像文件创建动画:

准备工作

您需要将跑步女孩的动画图像文件添加到您的项目中,并清理您的项目。

小贴士

请查看本章节的第一个菜谱创建精灵,了解如何将图像添加到您的项目中。

如何做...

您可以使用一系列图像创建动画。以下代码创建了跑步女孩的动画。

auto animation = Animation::create();
for (int i=1; i<=8; i++) {  // from run_01.png to run_08.png
    std::string name = StringUtils::format("res/run_%02d.png", i);
    animation->addSpriteFrameWithFile(name.c_str());
}
animation->setDelayPerUnit(0.1f);
animation->setRestoreOriginalFrame(true);
animation->setLoops(10);
auto action = Animate::create(animation);
sprite->runAction(action);

工作原理...

您可以使用Animation类和Animate类创建动画。它们以固定的时间间隔更改多个图像。系列图像文件的名称有序列号,我们在 for 循环中为Animation类添加了文件名。我们可以使用 Cocos2d-x 中的StringUtils类创建格式化的字符串。

小贴士

StringUtils是一个非常有用的类。StringUtils::toString方法可以从各种值生成std::string值。

int i = 100;
std::string int_string = StringUtils::toString(i);
CCLOG("%s ", int_string.c_str());
float j = 123.4f;
std::string float_string = StringUtils::toString(j);
CCLOG("%s", float_string.c_str());

StringUtils::format方法可以使用printf格式生成std::string值。

您可以通过使用 CCLOG 宏来查看日志。CCLOG 非常有用。您可以在游戏执行期间检查日志中的变量值。CCLOG 具有与sprintf函数相同的参数。

我们将使用addSpriteFrameWithFile方法将文件名添加到Animation实例中。它使用setDelayPerunit方法设置帧所占用的时间单位。使用setRestoreOriginalFrame方法设置为动画结束时恢复原始帧。真值是恢复原始帧。它设置为动画将要循环的次数。然后,通过传递您之前创建的Animation实例来创建Animate实例。最后,通过传递Animate实例来运行runAction方法。

如果您想永远运行动画,请使用setLoops方法设置-1

animation->setLoops(-1);

还有更多...

在前面的代码中,您无法控制每个动画帧。在这种情况下,您可以使用AnimationFrame类。这个类可以控制每个动画帧。您可以使用AnimationFrame::create方法的第二个参数设置帧所占用的时间单位。

auto rect = Rect::ZERO;
rect.size = sprite->getContentSize();
Vector<AnimationFrame*> frames;
for (int i=1; i<=8; i++) {
    std::string name = StringUtils::format("res/run_%02d.png", i);
    auto frame = SpriteFrame::create(name.c_str(), rect);
    ValueMap info;
    auto animationFrame = AnimationFrame::create(frame, i, info);
    frames.pushBack(animationFrame);
}
auto animation = Animation::create(frames, 0.1f);
animation->setDelayPerUnit(0.1f);
animation->setRestoreOriginalFrame(true);
animation->setLoops(-1);
auto action = Animate::create(animation);
sprite->runAction(action);

相关内容

  • 使用纹理图集创建动画的菜谱

创建动作

Cocos2d-x 有很多动作,例如移动、跳跃、旋转等。我们在游戏中经常使用这些动作。这类似于动画,当游戏中的角色开始动作时,游戏将变得生动。在这个菜谱中,您将学习如何使用许多动作。

如何做...

动作是游戏中非常重要的效果。Cocos2d-x 允许你使用各种动作。

移动

要在两秒内通过指定点移动精灵,你可以使用以下命令:

auto move = MoveBy::create(2.0f, Vec2(100, 100));
sprite->runAction(move);

要在两秒内将精灵移动到指定点,你可以使用以下命令:

auto move = MoveTo::create(2.0f, Vec2(100, 100));
sprite->runAction(move);

缩放

要在两秒内以 3x 的比例均匀缩放精灵,请使用以下命令:

auto scale = ScaleBy::create(2.0f, 3.0f);
sprite->runAction(scale);

要在两秒内将 X 轴缩放 5x 和 Y 轴缩放 3x,请使用以下命令:

auto scale = ScaleBy::create(2.0f, 5.0f, 3.0f);
sprite->runAction(scale);

要在两秒内以 3x 的比例均匀缩放精灵,请使用以下命令:

auto scale = ScaleTo::create(2.0f, 3.0f);
sprite->runAction(scale);

要在两秒内将 X 轴缩放 5x 和 Y 轴缩放 3x,请使用以下命令:

auto scale = ScaleTo::create(2.0f, 5.0f, 3.0f);
sprite->runAction(scale);

跳跃

要使精灵在两秒内跳过指定点三次,请使用以下命令:

auto jump = JumpBy::create(2.0f, Vec2(20, 20), 20.0f, 3);
sprite->runAction(jump);

要使精灵在两秒内跳到指定点三次,请使用以下命令:

auto jump = JumpTo::create(2.0f, Vec2(20, 20), 20.0f, 3);
sprite->runAction(jump);

旋转

要在两秒内顺时针旋转精灵 40 度,请使用以下命令:

auto rotate = RotateBy::create(2.0f, 40.0f);
sprite->runAction(rotate);

要在两秒内逆时针旋转精灵 40 度,请使用以下命令:

auto rotate = RotateTo::create(2.0f, -40.0f);
sprite->runAction(rotate);

闪烁

要使精灵在两秒内闪烁五次,请使用以下命令:

auto blink = RotateTo::create(2.0f, -40.0f);
sprite->runAction(blink);

淡入

要在两秒内淡入精灵,请使用以下命令:

auto fadein = FadeIn::create(2.0f);
sprite->runAction(fadein);

要在两秒内淡出精灵,请使用以下命令:

auto fadeout = FadeOut::create(2.0f);
sprite->runAction(fadeout);

扭曲

以下代码在两秒内将精灵的 X 轴扭曲 45 度和 Y 轴扭曲 30 度:

auto skew = SkewBy::create(2.0f, 45.0f, 30.0f);
sprite->runAction(skew);

以下代码在两秒内将精灵的 X 轴扭曲到 45 度和 Y 轴扭曲到 30 度。

auto skew = SkewTo::create(2.0f, 45.0f, 30.0f);
sprite->runAction(skew);

着色

以下代码通过指定的 RGB 值着色精灵:

auto tint = TintBy::create(2.0f, 100.0f, 100.0f, 100.0f);
sprite->runAction(tint);

以下代码将精灵着色到指定的 RGB 值:

auto tint = TintTo:: create(2.0f, 100.0f, 100.0f, 100.0f);
sprite->runAction(tint);

它是如何工作的...

Action 对象使精灵改变其属性。MoveToMoveByScaleToScaleBy 等都是 Action 对象。你可以使用 MoveToMoveBy 将精灵从一个位置移动到另一个位置。

你会注意到每个 Action 都有一个 ByTo 后缀。这就是它们有不同的行为的原因。带有 By 后缀的方法是相对于精灵的当前状态。带有 To 后缀的方法是相对于精灵的绝对状态。你知道 Cocos2d-x 中的所有动作都有 ByTo 后缀,并且所有动作都有与其后缀相同的规则。

还有更多...

当你想执行精灵动作时,你创建一个动作并通过传递 action 实例来执行 runAction 方法。如果你想在精灵运行动作时停止动作,请执行 stopAllActions 方法或通过传递 runAction 方法的返回值作为 action 实例的 stopAction

auto moveTo = MoveTo::create(2.0f, Vec2(100, 100));
auto action = sprite->runAction(moveTo);
sprite->stopAction(action);

如果你运行 stopAllActions,精灵正在运行的所有动作都将停止。如果你通过传递 action 实例来运行 stopAction,则将停止该特定动作。

控制动作

在之前的菜谱中,你学习了一些基本动作。然而,你可能想要使用更复杂的行为;例如,在移动时旋转角色,或者在跳跃后移动角色。在这个菜谱中,你将学习如何控制动作。

如何做到...

按顺序执行动作

Sequence 是一系列要按顺序执行的动作。这可以是一系列任意数量的动作。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f);
auto action = Sequence::create(move, rotate, nullptr);
sprite->runAction(action);

以下命令将按顺序执行以下动作:

  • 在两秒内将精灵向右移动 100 像素

  • 在两秒内顺时针旋转精灵 360 度

执行这些命令总共需要四秒。

生成动作

SpawnSequence 非常相似,不同之处在于所有动作将同时运行。你可以同时指定任意数量的动作。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f);
auto action = Spawn::create(move, rotate, nullptr);
sprite->runAction(action);

它将同时执行以下动作:

  • 在两秒内将精灵向右移动 100 像素

  • 顺时针旋转精灵 360 度

执行它们总共需要两秒。

重复动作

Repeat 对象是为了重复指定次数的动作。

auto rotate = RotateBy::create(2.0f, 360.0f);
auto action = Repeat::create(rotate, 5);
sprite->runAction(action);

以下命令将执行一个 rotate 动作五次。

如果你想要无限重复,可以使用 RepeatForever 动作。

auto rotate = RotateBy::create(2.0f, 360.0f);
auto action = RepeatForever::create(rotate);
sprite->runAction(action);

反转动作

如果你生成一个 action 实例,你可以调用一个 reverse 方法以相反的动作运行。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto action = Sequence::create(move, move->reverse(), nullptr);
sprite->runAction(action);

以下代码将按顺序执行以下动作:

  • 在两秒内将精灵向右移动 100 像素。

  • 在两秒内将精灵向左移动 100 像素。

此外,如果你生成一个序列动作,你可以调用一个 reverse 方法以相反的顺序运行它。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f);
auto sequence = Sequence::create(move, rotate, nullptr);
auto action = Sequence::create(sequence, sequence->reverse(), nullptr);
sprite->runAction(action);

以下代码将按顺序执行以下动作:

  • 在两秒内将精灵向右移动 100 像素。

  • 在两秒内顺时针旋转精灵 360 度

  • 在两秒内逆时针旋转精灵 360 度

  • 在两秒内将精灵向左移动 100 像素。

DelayTime

DelayTime 是在指定秒数内延迟的动作。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto delay = DelayTime::create(2.0f);
auto rotate = RotateBy::create(2.0f, 360.0f);
auto action = Sequence::create(move, delay, rotate, nullptr);
sprite->runAction(action);

以下命令将按顺序执行以下动作:

  • 在两秒内将精灵向右移动 100 像素

  • 延迟下一个动作两秒

  • 在两秒内顺时针旋转精灵 360 度

执行它总共需要六秒。

它是如何工作的...

Sequence 动作按顺序执行动作。你可以通过按顺序添加动作来生成一个 Sequence 实例。此外,你还需要指定 nullptr 作为最后一个参数。如果你没有指定 nullptr,你的游戏将会崩溃。

如何工作...

Spawn 动作同时运行动作。你可以像 Sequence 动作一样,通过添加动作和 nullptr 来生成一个 Spawn 实例。

如何工作...

RepeatRepeatForever 动作可以运行,重复执行相同的动作。Repeat 动作有两个参数,即重复的动作和重复的次数。RepeatForever 动作有一个参数,即重复的动作,这就是为什么它会无限运行。

大多数动作,包括 SequenceSpawnRepeat,都有 reverse 方法。但像带有后缀 ToMoveTo 方法一样,它没有 reverse 方法;这就是为什么它不能运行反向动作。Reverse 方法生成其反向动作。以下代码使用了 MoveBy::reverse 方法。

MoveBy* MoveBy::reverse() const
{
    return MoveBy::create(_duration, -_positionDelta);
}

DelayTime 动作可以在此之后延迟一个动作。DelayTime 动作的好处是您可以将其放入 Sequence 动作中。结合 DelayTimeSequence 是一个非常强大的功能。

更多内容...

Spawn 产生的结果与运行多个连续的 runAction 语句相同。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f); 
sprite->runAction(move);
sprite->runAction(rotate);

然而,Spawn 的好处是您可以将其放入 Sequence 动作中。结合 SpawnSequence 是一个非常强大的功能。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f); 
auto fadeout = FadeOut::create(2.0f);
auto spawn = Spawn::create(rotate, fadeout, nullptr);
auto fadein = FadeIn::create(2.0f);
auto action = Sequence::create(move, spawn, fadein, nullptr);
sprite->runAction(action);

更多内容...

使用动作调用函数

您可能希望通过触发某些动作来调用一个函数。例如,您正在控制序列动作、跳跃和移动,并且想要为跳跃动作使用一个声音。在这种情况下,您可以通过触发这个跳跃动作来调用一个函数。在这个菜谱中,您将学习如何通过动作调用一个函数。

如何操作...

Cocos2d-x 有一个 CallFunc 对象,允许您创建一个函数并将其传递到您的 Sequence 中运行。这允许您向您的 Sequence 动作添加自己的功能。

auto move = MoveBy::create(2.0f, Vec2(100, 0));
auto rotate = RotateBy::create(2.0f, 360.0f);
auto func = CallFunc::create([](){
    CCLOG("finished actions");
});
auto action = Sequence::create(move, rotate, func, nullptr);
sprite->runAction(action);

前面的命令将按顺序执行以下动作:

  • 将精灵向右移动 100 像素,持续两秒

  • 将精灵顺时针旋转 360 度,持续两秒

  • 执行 CCLOG

工作原理...

CallFunc 动作通常用作回调函数。例如,如果您想在 move 动作完成后执行不同的过程。使用 CallFunc,您可以在任何时间调用方法。您可以使用 lambda 表达式作为回调函数。

如果您收到带有参数的回调,其代码如下:

auto func = CallFuncN::create([](Ref* sender){
    CCLOG("callback");
    Sprite* sprite = dynamic_cast<Sprite*>(sender);
});

此参数的实例是运行动作的精灵。您可以通过将其转换为 Sprite 类来获取精灵实例。

然后,您还可以指定一个回调方法。CallFuncCC_CALLBACK_0 宏作为参数。CC_CALLBACK_0 是一个用于调用不带参数的方法的宏。如果您想调用一个带有一个参数的方法,您需要使用 CallFuncN 动作和 CC_CALLBACK_1 宏。CC_CALLBACK_1 是一个用于调用一个参数的方法的宏。由 CallFuncN 调用的方法的参数是 Ref 类。

您可以使用以下代码调用一个方法:

bool HelloWorld::init() {
    …
    auto func = 
CallFunc::create(CC_CALLBACK_0(HelloWorld::finishedAction, this));
    …
}

void HelloWorld::finishedAction()
{
    CCLOG("finished action");
}

要使用参数调用一个方法,您可以使用以下代码:

bool HelloWorld::init() {
    …
    auto func = CallFuncN::create(CC_CALLBACK_1(HelloWorld::callback, this));
    …
}

void HelloWorld::callback(Ref* sender)
{
    CCLOG("callback");
}

更多内容...

要组合 CallFuncNReverse 动作,请使用以下代码:

auto move = MoveBy::create(2.0f, Vec2(100, 0)); 
auto rotate = RotateBy::create(2.0f, 360.0f); 
auto func = CallFuncN::create(={
    Sprite* sprite = dynamic_cast<Sprite*>(sender);
    sprite->runAction(move->reverse());
});
auto action = Sequence::create(move, rotate, func, nullptr);
sprite->runAction(action);

前面的命令将按顺序执行以下动作:

  • 将精灵向右移动 100 像素,持续两秒

  • 将精灵顺时针旋转 360 度,持续两秒

  • 将精灵向左移动 100 像素,持续两秒

缓动动作

缓动是通过指定加速度来动画化,以使动画*滑。缓动动作是模拟游戏中的物理效果的好方法。如果您在动画中使用缓动动作,您的游戏看起来将更加自然,动画也更加*滑。

如何做到...

让我们使用加速度和减速度将一个Sprite对象从(200,200)移动到(500,200)

auto sprite = Sprite::create("res/run_01.png");
sprite->setPosition(Vec2(200, 200));
this->addChild(sprite);

auto move = MoveTo::create(3.0f, Vec2(500, 200));
auto ease = EaseInOut::create(move, 2.0f);
sprite->runAction(ease);

接下来,让我们从屏幕顶部掉下一个Sprite对象,并使其弹跳。

auto sprite = Sprite::create("res/run_01.png");
sprite->setPosition(Vec2(size.width/2, size.height));
sprite->setAnchorPoint(Vec2(0.5f, 0.0f));
this->addChild(sprite);

auto drop = MoveTo::create(3.0f, Vec2(size.width/2, 0));
auto ease = EaseBounceOut::create(drop);
sprite->runAction(ease);

工作原理...

动画的持续时间与您是否使用缓动无关。EaseInEaseOutEaseInOut有两个参数——第一个参数是缓动动作,第二个参数是缓动速率。如果您将此参数指定为1.0f,则此缓动动作与不使用缓动相同。任何大于1.0f的值,表示缓动速度快,小于1.0f,缓动速度将变慢。

下表列出了典型的缓动类型。

类名 描述
EaseIn 在加速的同时移动。
EaseOut 在减速的同时移动。
EaseInOut 在加速的同时开始移动,在减速时停止。
EaseExponentialIn 它与EaseIn类似,但意味着以指数曲线的速度加速。它也像EaseIn一样与OutInOut一起使用。
EaseSineIn 它与EaseIn类似,但意味着以正弦曲线的速度加速。它也像EaseIn一样与OutInOut一起使用。
EaseElasticIn 在慢慢摇动后移动,逐渐移动。它也像EaseIn一样与OutInOut一起使用。
EaseBounceIn 在弹跳后移动。它也像EaseIn一样与OutInOut一起使用。
EaseBackIn 在相反方向移动后开始移动。它也像EaseIn一样与OutInOut一起使用。

这是一个显示典型缓动函数的图表:

工作原理...

使用纹理图集

纹理图集是一个包含每个精灵集合的大图像。我们通常使用纹理图集而不是单个图像。在本教程中,您将学习如何使用纹理图集。

准备工作

您必须将纹理图集文件添加到您的项目中,并清理您的项目。

  • running.plist

  • running.png

如何做到...

让我们尝试读取纹理图集文件,并从中创建一个精灵。

auto cache = SpriteFrameCache::getInstance();
cache->addSpriteFramesWithFile("res/running.plist");
auto sprite = Sprite::createWithSpriteFrameName("run_01.png");
sprite->setPosition(size/2);
this->addChild(sprite);

工作原理...

首先,我们加载了纹理图集文件,当SpritFrameCache类缓存了其中包含的所有图像。其次,您生成了一个精灵。不要使用Sprite::create方法来生成它,而是使用Sprite::createWithSpriteFrameName方法。然后,您可以像处理普通精灵一样处理该精灵。

纹理图集是一个包含多个图像的大图像。它由一个 plist 文件和一个 texture 文件组成。您可以使用工具创建纹理图集。您将在第十章 使用额外功能改进游戏 中学习如何使用工具创建纹理图集,使用额外功能改进游戏plist 文件定义为图像的原始文件名,它位于 texture 文件中。它还定义了纹理图集将使用的图像。纹理图集的 plist 文件是 xml 格式,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>frames</key>
        <dict>
            <key>run_01.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,2},{356,474}}</string>
                <key>offset</key>
                <string>{-62,-26}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{60,89},{356,474}}</string>
                <key>sourceSize</key>
                <string>{600,600}</string>
            </dict>
            <key>run_02.png</key>
            <dict>
                <key>frame</key>
                <string>{{360,2},{272,466}}</string>
                <key>offset</key>
                <string>{-30,-33}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{134,100},{272,466}}</string>
                <key>sourceSize</key>
                <string>{600,600}</string>
            </dict>

      omit

        </dict>
        <key>metadata</key>
        <dict>
            <key>format</key>
            <integer>2</integer>
            <key>realTextureFileName</key>
            <string>running.png</string>
            <key>size</key>
            <string>{2048,1024}</string>
            <key>smartupdate</key>
            <string>$TexturePacker:SmartUpdate :e4468ff02abe538ce50e3e1448059f78:1/1$</string>
            <key>textureFileName</key>
            <string>running.png</string>
        </dict>
    </dict>
</plist>

它是如何工作的...

为什么我们会使用纹理图集?因为高效使用内存是好的。当计算机将图像加载到内存中时,需要双倍的内存大小。例如,有十个大小为 100x100 的图像。我们将使用九个图像,但一个图像需要 128x128 大小的内存。另一方面,纹理图集是一个包含九个图像的图像,其大小为 1000x1000。它需要 1024x1024 的内存大小。这就是为什么使用纹理图集来节省不必要的内存使用。

还有更多...

纹理图集的大小在使用中会根据设备而变化。您可以在以下代码中检查设备的最大纹理大小:

int max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
CCLOG("texture size = %d", max);

您可以使用纹理图集和 plist 文件生成动画。首先,您需要将 run_animation.plist 文件添加到您的项目中。该文件如下截图所示:

还有更多...

这个 plist 定义了一个帧动画。在这种情况下,我们使用从 run_01.pngrun_08.png 的图像定义了一个名为 run 的动画。如果您将 -1 指定给 loop 键的值,动画将无限循环。纹理图集指定为 running.plist

其次,您需要使用 plist 文件生成动画。

auto cache = AnimationCache::getInstance(); cache->addAnimationsWithFile("res/run_animation.plist"); auto animation = cache->getAnimation("run"); auto action = Animate::create(animation); sprite->runAction(action);

您还需要使用 AnimationCache::addAnimationWithFile 方法缓存动画数据,使用动画 plist。接下来,您将通过指定在 plist 中定义为动画名称的 run 来生成一个 Animation 实例。然后,您从动画中生成一个动作。之后,您可以使用动作实例通过 runAction 方法进行动画处理。

相关内容

手动创建纹理图集非常困难。您最好使用像 TexturePacker 这样的工具,您将在第十一章 利用优势 中了解它。

使用批处理节点

如果屏幕上有许多精灵,渲染速度会变慢。然而,射击游戏需要很多图像,如子弹等。在这种情况下,如果渲染速度慢,游戏会得到不好的评价。在本章中,您将学习如何控制许多精灵。

如何做到这一点...

让我们尝试使用 SpriteBatchNode 显示许多精灵。

auto batchNode = SpriteBatchNode::create("res/run_01.png");
this->addChild(batchNode);
for (int i=0; i<300; i++) {
 auto sprite = Sprite::createWithTexture(batchNode->getTexture());
 float x = CCRANDOM_0_1() * size.width;
 float y = CCRANDOM_0_1() * size.height;
 sprite->setPosition(Vec2(x,y));
 batchNode->addChild(sprite);
}

它是如何工作的...

SpriteBatchNode 实例可用于以下操作:

  • 使用纹理生成 SpriteBatchNode 实例

  • 在层上添加实例

  • 使用 SpriteBatchNode 实例中的纹理生成精灵

  • SpriteBatchNode 实例上添加这些精灵

SpriteBatchNode 只能引用一个纹理(一个图像文件或一个纹理图集)。只有包含在该纹理中的精灵可以添加到 SpriteBatchNode 中。所有添加到 SpriteBatchNode 的精灵都会在一个 OpenGL ES 绘制调用中绘制。如果精灵没有添加到 SpriteBatchNode,则需要为每个精灵进行一个 OpenGL ES 绘制调用,这效率较低。

更多...

以下截图是执行屏幕图像。您可以在左下角看到关于 Cocos2d-x 的三条信息。最上面一行是多边形的顶点数。中间一行是 OpenGL ES 绘制调用的次数。您理解到许多精灵可以通过一个 OpenGL ES 绘制调用来绘制。最下面一行是每帧帧数和每帧秒数。

更多...

小贴士

如果您想隐藏此调试信息,应将 Director::setDisplayStats 方法的值设置为 false。您可以在项目的 AppDelegate.cpp 中找到它。

director->setDisplayStats(false);

自 Cocos2d-x 版本 3 以来,绘制调用的 auto batch 功能已被添加,Cocos2d-x 可以通过一个 OpenGL ES 绘制调用绘制许多精灵,无需 SpriteBatchNode。然而,它有以下条件:

  • 相同的纹理

  • 相同的 BlendFunc

使用 3D 模态

Cocos2d-x 版本 3 支持一个令人兴奋的新功能,称为 3D 模态。我们可以在 Cocos2d-x 中使用和显示 3D 模态。在本教程中,您将学习如何使用 3D 模态。

准备工作

您必须将 3D 对象数据添加到您的项目中,并清理您的项目。在 COCOS_ROOT/test/cpp-tests/Resources/Sprite3DTest 文件夹中存在的资源文件是—body.pnggirl.c3b

如何做...

让我们尝试显示一个 3D 模型并移动它。

auto size = Director::getInstance()->getWinSize();

// create 3D modal
auto sprite3d = Sprite3D::create("res/girl.c3b");
sprite3d->setPosition(Vec2(size.width/2, 100));
this->addChild(sprite3d);

// action 3D modal
auto animation3d = Animation3D::create("res/girl.c3b");
auto animate3d = Animate3D::create(animation3d);
auto repeat = RepeatForever::create(animate3d);
sprite3d->runAction(repeat);

如何做...

它是如何工作的...

您可以像创建 2D 精灵并显示它一样,从 3D 模型创建 3D 精灵。Placement 方法和 action 方法与 2D 精灵中看到的方法完全相同。您可以从 3D 模型中定义的动画数据创建 Animation3D 实例。

更多...

最后,您将尝试将 3D 精灵向左或向右移动。当您运行以下代码时,您会注意到 3D 精灵的外观取决于它们在屏幕上的位置:

Sprite3d->setPositionX(size.width);
// move fro right to left
auto move1 = MoveBy::create(5.0f, Vec2(-size.width, 0));
auto move2 = MoveBy::create(5.0f, Vec2(size.width, 0));
auto seq = Sequence::create(move1, move2, NULL);
auto loop = RepeatForever::create(seq);
sprite3d->runAction(loop);

参见

您可以使用 obj、c3b 和 c3t 等三维数据格式。其中“c3t”代表 Cocos 3d 二进制。您可以通过转换 fbx 文件来获取这种格式的数据。

检测碰撞

在动作游戏中,一个非常重要的技术是检测每个精灵之间的碰撞。然而,检测 rectrectrectpoint 之间的碰撞相当复杂。在本教程中,您将学习如何轻松地检测碰撞。

如何做...

有两种检测碰撞的方法。第一种方法检查一个点是否包含在精灵的矩形内。

Rect rect = sprite->getBoundingBox();
if (rect.containsPoint(Vec2())) {
 CCLOG("the point bumped rectangle");
}

第二种方法检查两个精灵的矩形是否重叠。

if (rect.intersectsRect(Rect(0, 0, 100, 100))) {
 CCLOG("two rectangles bumped");}

它是如何工作的...

Rect类有两个属性——sizeoriginsize属性是精灵的大小。origin属性是精灵的左下角坐标。首先,你使用getBoundingBox方法获取精灵的rect

它是如何工作的...

通过指定坐标使用Rect::containsPoint方法,可以检测它是否包含矩形。如果包含,该方法返回true。通过指定另一个矩形使用Rect::intersectsRect方法,可以检测它们是否重叠。如果重叠,该方法返回true

下图显示了rectpointrectrect之间的碰撞:

它是如何工作的...

还有更多...

Rect类有更多方法,包括getMinXgetMidXgetMaxXgetMinYgetMidYgetMaxYunionWithRect。你可以使用这些方法中的每一个来获取以下图中的值。

还有更多...

相关内容

  • 如果你使用了物理引擎,你可以以不同的方式检测碰撞。查看第九章,控制物理

  • 如果你想要考虑图像的透明部分来检测碰撞,请查看第十一章,利用优势

绘制形状

在 Cocos2d-x 中,使用DrawNode类绘制形状可以变得简单。如果你可以使用DrawNode绘制各种形状,你将需要为这些形状准备纹理。在本节中,你将学习如何不使用纹理来绘制形状。

如何操作...

首先,你创建了一个DrawNode实例,如下面的代码所示。你也得到了窗口大小。

auto size = Director::getInstance()->getWinSize();
auto draw = DrawNode::create();
this->addChild(draw);

绘制点

你可以通过指定点、半径和颜色来绘制点。

draw->drawDot(Vec2(size/2), 10.0f, Color4F::WHITE);

绘制点

绘制线条

你可以通过指定起点、终点和颜色来绘制线条。使用drawLine方法时,将绘制一个1px粗的线条。如果你想绘制更粗的线条,请使用带有给定半径的drawSegment方法。

draw->drawLine(Vec2(300, 200), Vec2(600, 200), Color4F::WHITE);
draw->drawSegment(Vec2(300, 100), Vec2(600, 100), 10.0f,
Color4F::WHITE);

绘制线条

绘制圆形

你可以像以下代码所示绘制圆形。参数的指定如下:

  • 中心位置

  • 半径

  • 角度

  • 段数

  • 是否绘制到中心

  • 缩放 X 轴

  • 缩放 Y 轴

  • 颜色

draw->drawCircle(Vec2(300, size.height/2), 50.0f, 1.0f, 10, true,
1.0f, 1.0f, Color4F::WHITE);
draw->drawCircle(Vec2(450, size.height/2), 50.0f, 1.0f, 100, false,
1.0f, 1.0f, Color4F::WHITE);
draw->drawSolidCircle(Vec2(600, size.height/2), 50.0f, 1.0f, 100,
1.0f, 1.0f, Color4F::WHITE);

绘制圆形

段数是多边形的顶点数。正如你所知,圆是一个具有许多顶点的多边形。增加顶点数接*于*滑的圆,但过程负载会增加。顺便说一句,如果你想得到一个实心圆,你应该使用drawSolidCircle方法。

绘制三角形

你可以使用以下代码,通过三个顶点和颜色绘制一个三角形。

draw->drawTriangle(Vec2(380,100), Vec2(480, 200), Vec2(580, 100),
Color4F::WHITE);

绘制三角形

绘制矩形

你可以使用以下代码,通过左下角点、右上角点和颜色绘制矩形。如果你使用drawSolidRect方法,你可以绘制填充颜色。

draw->drawRect(Vec2(240, 100), Vec2(340,200), Color4F::WHITE);
draw->drawSolidRect(Vec2(480, 100), Vec2(580, 200), Color4F::WHITE);

绘制矩形

绘制多边形

你可以使用以下代码,通过给定的顶点、顶点数量、填充颜色、边框宽度和边框颜色绘制一个多边形。

std::vector<Vec2>verts;
verts.push_back(Vec2(380,100));
verts.push_back(Vec2(380,200));
verts.push_back(Vec2(480,250));
verts.push_back(Vec2(580,200));
verts.push_back(Vec2(580,100));
verts.push_back(Vec2(480,50));
draw->drawPolygon(&verts[0], verts.size(), Color4F::RED, 5.0f,
Color4F::GREEN);

绘制多边形

绘制贝塞尔曲线

你可以使用以下代码绘制一个贝塞尔曲线,如所示。使用drawQuadBezier方法,你可以绘制一个二次贝塞尔曲线,使用drawCubicBezier方法你可以绘制一个三次贝塞尔曲线。drawQuadBezier方法的第三个参数和drawCubicBezier方法的第四个参数与圆一样,表示顶点数量。

draw->drawQuadBezier(Vec2(240, 200), Vec2(480, 320), Vec2(720, 200), 24, Color4F::WHITE); draw->drawCubicBezier(Vec2(240, 100), Vec2(240, 200), Vec2(720, 200), Vec2(720, 100), 24, Color4F::WHITE);

绘制贝塞尔曲线

它是如何工作的...

DrawNode就像一个机制,使得 Cocos2d-x 能够以高速处理,通过一次性绘制所有形状而不是分别或逐个绘制。当你绘制多个形状时,你应该使用一个DrawNode实例,而不是多个DrawNode实例并在其中添加多个形状。此外,DrawNode没有深度概念。Cocos2d-x 将按照添加到DrawNode中的形状顺序进行绘制。

第三章. 使用标签

在本章中,我们将创建标签。要在屏幕上显示标签,您可以使用带有系统字体、真型字体和位图字体的 Label 类。本章将涵盖以下主题:

  • 创建系统字体标签

  • 创建真型字体标签

  • 创建位图字体标签

  • 创建丰富文本

创建系统字体标签

首先,我们将解释如何使用系统字体创建标签。系统字体是已安装在您的设备上的字体。由于它们已经安装,因此无需经过安装过程。因此,我们将跳过此配方中系统字体的安装说明,并直接进入创建标签。

如何做到...

这是如何通过指定系统字体创建标签的方法。您可以使用以下代码创建单行标签:

auto label = Label::createWithSystemFont("Cocos2d-x", "Arial", 40);
label->setPosition(size/2);
this->addChild(label);

如何做到...

它是如何工作的...

您应该使用 Label 类通过指定一个字符串、系统字体和字体大小来显示字符串。Label 类将显示一个转换为图像的字符串。在创建 Label 实例后,您可以像使用 Sprite 一样使用它。因为 Label 也是一个节点,我们可以使用动作、缩放和透明度函数等属性来操作标签。

换行

您还可以在字符串中的任何位置添加新行,只需将换行符代码放入字符串中即可:

auto label = Label::createWithSystemFont("Hello\nCocos2d-x", "Arial", 40);
label->setPosition(size/2);
this->addChild(label);

换行

文本对齐

您还可以在水*和垂直方向上指定文本对齐。

文本对齐类型 描述
TextHAlignment::LEFT 将文本水*对齐到左侧。这是水*对齐的默认值。
TextHAlignment::CENTER 将文本水*对齐到中心。
TextHAlignment::RIGHT 将文本水*对齐到右侧。
TextVAlignment::TOP 将文本垂直对齐到顶部。这是垂直对齐的默认值。
TextVAlignment::CENTER 将文本垂直对齐到中心。
TextVAlignment::BOTTOM 将文本垂直对齐到底部。

以下代码用于将文本水*对齐到中心:

label-> setHorizontalAlignment(TextHAlignment::CENTER);

文本对齐

还有更多...

您也可以在创建标签后更新字符串。如果您想每秒更新一次字符串,可以通过设置以下计时器来实现:

首先,按照以下方式编辑 HelloWorld.h

class HelloWorld : public cocos2d::Layer
{
private:
    int sec;
public:
    …
;

接下来,按照以下方式编辑 HelloWorld.cpp

sec = 0;
std::string secString = StringUtils::toString(sec);
auto label = Label::createWithSystemFont(secString, "Arial", 40);
label->setPosition(size/2);
this->addChild(label);

this->schedule(= {
	sec++;
	std::string secString = StringUtils::toString(sec);
	label->setString(secString);
}, 1.0f, "myCallbackKey");

首先,您必须在头文件中定义一个整型变量。其次,您需要创建一个标签并将其添加到层上。然后,您需要设置调度器每秒执行函数。然后您可以通过使用 setString 方法来更新字符串。

小贴士

您可以使用 StringUtils::toString 方法将整型或浮点值转换为字符串值。

调度器可以在指定的时间间隔执行方法。我们将在第四章中解释调度器的工作原理,构建场景和层。请参阅它以获取有关调度器的更多详细信息。

创建真型字体标签

在这个食谱中,我们将解释如何使用真型字体创建标签。真型字体是可以安装到项目中的字体。Cocos2d-x 的项目已经包含了两个真型字体,即arial.ttfMarker Felt.ttf,它们位于Resources/fonts文件夹中。

如何操作...

下面是如何通过指定真型字体来创建标签的方法。以下代码可以用来创建一个单行标签,使用真型字体:

auto label = Label:: createWithTTF("True Type Font", "fonts/Marker
Felt.ttf", 40.0f);
label->setPosition(size/2);
this->addChild(label);

如何操作...

工作原理...

你可以通过指定标签字符串、真型字体的路径和字体大小来创建一个具有真型字体的Label。真型字体位于Resources文件夹的font文件夹中。Cocos2d-x 有两个真型字体,即arial.ttfMarker Felt.ttf,它们位于Resources/fonts文件夹中。你可以从一个真型字体文件中生成不同字号的Label对象。如果你想要添加真型字体,如果你将其添加到font文件夹中,你可以使用原始的真型字体。然而,与位图字体相比,在渲染方面,真型字体较慢,并且更改字体样式和大小等属性是一个昂贵的操作。你必须小心不要频繁更新它。

更多内容...

如果你想要创建很多具有相同属性的Label对象,你可以通过指定TTFConfig来创建它们。TTFConfig具有真型字体所需的属性。你可以使用以下方式通过TTFConfig创建标签:

TTFConfig config;
config.fontFilePath = "fonts/Marker Felt.ttf";
config.fontSize = 40.0f;
config.glyphs = GlyphCollection::DYNAMIC;
config.outlineSize = 0;
config.customGlyphs = nullptr;
config.distanceFieldEnabled = false;

auto label = Label::createWithTTF(config, "True Type Font");
label->setPosition(size/2);
this->addChild(label);

TTFConfig对象允许你设置一些具有相同属性的标签。

如果你想要改变Label的颜色,你可以改变它的颜色属性。例如,使用以下代码,你可以将颜色改为RED

label->setColor(Color3B::RED);

相关内容

  • 你可以为标签设置效果。请查看本章的最后一个食谱。

创建位图字体标签

最后,我们将解释如何创建位图类型的标签。位图字体也是你可以安装到项目中的字体。位图字体本质上是一个包含大量字符和控制文件的图像文件,该控制文件详细说明了图像中每个字符的大小和位置。如果你在游戏中使用位图字体,你会看到位图字体在所有设备上大小相同。

准备工作

您必须准备一个位图字体。您可以使用GlyphDesigner等工具创建它。我们将在第十章使用额外功能改进游戏之后解释这个工具。现在,我们将使用 Cocos2d-x 中的位图字体。它位于COCOS_ROOT/tests/cpp-tests/Resources/fonts文件夹中。首先,您必须将以下文件添加到您的项目中Resources/fonts文件夹中。

  • future-48.fnt

  • future-48.png

如何操作...

如此通过指定位图字体创建标签。以下代码可以用于使用位图字体创建单行标签:

auto label = Label:: createWithBMFont("fonts/futura-48.fnt",
"Bitmap Font");
label->setPosition(size/2);
this->addChild(label);

如何操作...

工作原理...

您可以通过指定label字符串、真型字体路径和字体大小来创建具有位图字体的Label。位图字体中的字符由点阵组成。这种字体渲染速度很快,但不可缩放。这就是为什么它在生成时具有固定字体大小。位图字体需要以下两个文件:一个.fnt 文件和一个.png文件。

更多...

Label中的每个字符都是一个Sprite。这意味着每个字符都可以旋转或缩放,并且具有其他可更改的属性:

auto sprite1 = label->getLetter(0);
sprite1->setRotation(30.0f);

auto sprite2 = label->getLetter(1);
sprite2->setScale(0.5f);

更多...

创建富文本

在屏幕上创建Label对象后,您可以在它们上轻松创建一些效果,如阴影和轮廓,而无需创建自己的自定义类。Label类可以用于将这些效果应用于这些对象。但是请注意,并非所有标签类型都支持所有效果。

如何操作...

阴影

如此创建具有阴影效果的标签

auto layer = LayerColor::create(Color4B::GRAY);
this->addChild(layer);
auto label = Label::createWithTTF("Drop Shadow", "fonts/Marker
Felt.ttf", 40);
label->setPosition(size/2);
this->addChild(label);
// shadow effect
label->enableShadow();

阴影

轮廓

如此创建具有轮廓效果的标签

auto label = Label::createWithTTF("Outline", "fonts/Marker
Felt.ttf", 40);
label->setPosition(size/2);
this->addChild(label);
// outline effect
label->enableOutline(Color4B::RED, 5);

轮廓

发光

如此创建具有发光效果的标签

auto label = Label::createWithTTF("Glow", "fonts/Marker Felt.ttf", 40);
label->setPosition(size/2);
this->addChild(label);
// glow effect label->enableGlow(Color4B::RED);

发光

工作原理...

首先,我们生成一个灰色图层并将背景颜色改为灰色,因为否则我们无法看到阴影效果。将效果添加到标签中非常简单。您需要生成一个Label实例并执行一个效果方法,例如enableShadow()。这可以无参数执行。enableOutline()有两个参数,即轮廓颜色和轮廓大小。轮廓大小有一个默认值-1。如果它有负值,则轮廓不会显示。接下来,您必须设置第二个参数。enableGlow方法只有一个参数,即发光颜色。

并非所有标签类型都支持所有效果,但所有标签类型都支持阴影效果。OutlineGlow效果仅适用于真型字体。在之前的版本中,如果我们想在标签上应用效果,我们必须创建自己的自定义字体类。然而,当前版本的 Cocos2d-x,版本 3,支持标签效果,如阴影、轮廓和发光。

更多...

您还可以更改阴影颜色和偏移量。第一个参数是阴影颜色,第二个参数是偏移量,第三个参数是模糊半径。然而,不幸的是,在 Cocos2d-x 版本 3.4 中不支持更改模糊半径。

auto label = Label::createWithTTF("Shadow", "fonts/Marker
Felt.ttf", 40);
label->setPosition(Vec2(size.width/2, size.height/3*2));
this->addChild(label);
label->enableShadow(Color4B::RED, Size(5,5), 0);

同时设置两个或更多这些效果也是可能的。以下代码可以用于设置标签的阴影和轮廓效果:

auto label2 = Label::createWithTTF("Shadow & Outline", "fonts/Marker Felt.ttf", 40); label2->setPosition(Vec2(size.width/2, size.height/3)); this->addChild(label2); label2->enableShadow(Color4B::RED, Size(10,-10), 0);
label2->enableOutline(Color4B::BLACK, 5);

还有更多...

第四章:构建场景和层

本章将涵盖以下主题:

  • 创建场景

  • 场景间的切换

  • 使用效果切换场景

  • 为替换场景制作原创过渡效果

  • 为弹出场景制作原创过渡效果

  • 创建层

  • 创建模态层

简介

一屏有一个场景。场景是一个容器,它包含精灵、标签和其他对象。例如,场景可以是标题场景、游戏场景或选项菜单场景。每个场景都有多个层。层是一个类似于 Photoshop 层的透明纸。添加到层中的对象将在屏幕上显示。在本章中,我们将解释如何使用Scene类和Layer类以及如何在场景间进行切换。最后,在本章结束时,你将能够创建原创的场景和层。

创建场景

在 Cocos2d-x 中,你的游戏应该有一个或多个场景。场景基本上是一个节点。在本食谱中,我们将解释如何创建和使用Scene类。

如何做...

在本食谱中,我们将使用在第一章中创建的项目,即Cocos2d-x 入门

  1. 首先,在Finder中复制HelloWorldScene.cppHelloWorldScene.h文件,并将它们重命名为TitleScene.cppTitleScene.h。其次,将它们添加到 Xcode 项目中。结果如下所示:如何做...

  2. 接下来,我们必须将HelloWorldScene更改为TitleScene,并将搜索和替换方法放在提示部分。

    小贴士

    如何搜索和替换类名?

    在此情况下,选择TitleScene.h,然后在 Xcode 中选择查找 | 查找和替换…菜单。然后,在字符串匹配区域输入HelloWorld,在替换字符串区域输入TitleScene。执行所有替换。对TitleScene.cpp执行相同的操作。结果如下所示:

    TitleScene.h的结果如下:

    #ifndef __TitleScene_SCENE_H__
    #define __TitleScene_SCENE_H__
    
    #include "cocos2d.h"
    class TitleScene : public cocos2d::Layer
    {
    public:
        static cocos2d::Scene* createScene();
        virtual bool init();
        CREATE_FUNC(TitleScene);
    };
    
    #endif // __TitleScene_SCENE_H__
    

    接下来,TitleScene.cpp的结果如下:

    #include "TitleScene.h"
    
    USING_NS_CC;
    
    Scene* TitleScene::createScene()
    {
        auto scene = Scene::create();
        auto layer = TitleScene::create();
        scene->addChild(layer);
        return scene;
    }
    
    // on "init" you need to initialize your instance
    bool TitleScene::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
    
        return true;
    }
    
  3. 接下来,在TitleSceneHelloWorldScene之间的差异处添加一个标签。在TitleScene::init方法的返回行之前添加如下:

    bool TitleScene::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
    
        auto size = Director::getInstance()->getWinSize();
        auto label =
        Label::createWithSystemFont("TitleScene", "Arial",
        40);
        label->setPosition(size/2);
        this->addChild(label);
    
        return true;
    }
    
  4. 类似地,在HelloWorld::init方法中添加标签。

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
    
        auto size = Director::getInstance()->getWinSize();
        auto label = Label::createWithSystemFont("HelloWorld",
        "Arial", 40);
        label->setPosition(size/2);
        this->addChild(label);
    
        return true;
    }
    
  5. 接下来,为了显示TitleScene类,按照以下方式修改AppDelegate.cpp

    #include "TitleScene.h"
    
    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(true);
    
        // set FPS. the default value is 1.0/60 if you don't
        call this director->setAnimationInterval(1.0 / 60);
        glview->setDesignResolutionSize(640, 960,
        ResolutionPolicy::NO_BORDER); 
        // create a scene. it's an autorelease object
        auto scene = TitleScene::createScene();
    
        // run
        director->runWithScene(scene);
    
        return true;
    }
    

结果如下所示:

如何做...

它是如何工作的...

首先,你需要通过复制HelloWorldScene类文件来创建TitleScene。从空白文件创建一个原创的Scene类相当困难。然而,Scene的基本类是模式化的。因此,你可以通过复制和修改HelloWorldScene类文件来轻松创建它。当你开发游戏时,当你需要新的场景时,你需要执行此步骤。

最后,我们更改 AppDelegate.cpp 文件。AppDelegate 类是 Cocos2d-x 中首先执行的一个类。当应用程序准备好运行时,会执行 AppDelegate::applicationDidFinishLaunching 方法。此方法将准备 Cocos2d-x 的执行。然后,它将创建第一个场景并运行它。

auto scene = TitleScene::createScene();
// run
director->runWithScene(scene);

TitleScene::createScene 方法用于创建一个标题场景,而 runWithScene 方法用于运行它。

场景之间的转换

您的游戏需要在场景之间进行转换。例如,在启动游戏后,会显示标题场景。然后,它会转换到关卡选择场景、游戏场景等等。在这个菜谱中,我们将解释如何促进场景之间的转换,这将提高游戏玩法和游戏流程。

如何做到这一点...

一个游戏有很多场景。因此,您可能需要在游戏中在不同场景之间移动。也许,当游戏开始时,会显示一个标题场景。然后,在下一个标题场景中会出现一个游戏场景。有两种方法可以转换到场景。

  1. 一种方法是使用 Director::replaceScene 方法。此方法直接替换场景。

    auto scene = HelloWorld::createScene();
    Director::getInstance()->replaceScene(scene);
    
  2. 另一种方法是使用 Director::pushScene 方法。此方法挂起正在运行的场景,并在挂起场景的堆栈上推入一个新的场景。

    auto scene = HelloWorld::createScene();
    Director::getInstance()->pushScene(scene);
    

在这种情况下,旧场景被挂起。您可以回到旧场景以弹出新的场景。

auto Director::getInstance()->popScene();

它是如何工作的...

可以通过使用 addChild 方法来显示层、精灵和其他节点。然而,场景不能通过 addChild 方法来显示;它可以通过使用 Director::replaceSceneDirector::pushScene 方法来显示。这就是为什么场景在同一时间只能在一个屏幕上可见。SceneLayer 类似,但存在显著差异。

通常,当您从标题场景切换到游戏场景时,您会使用 replaceScene 方法。进一步,您可以使用 pushScene 方法显示模态场景,例如在游戏暂停期间。在这种情况下,挂起游戏场景的一个简单方法是暂停游戏。

小贴士

当在游戏中替换场景时,应用程序将释放旧场景使用的内存。然而,如果游戏推入场景,它们将不会释放旧场景使用的内存,因为它们将挂起它。此外,当弹出新场景时,游戏将恢复。如果您使用 pushScene 方法添加了大量的场景,设备内存将不再足够。

带效果的场景转换

流行游戏在场景转换时显示一些效果。这些效果可以是自然的、戏剧性的等等。Cocos2d-x 有很多转换效果。在这个菜谱中,我们将解释如何使用转换效果及其产生的效果。

如何做到这一点...

您可以使用 Transition 类为场景过渡添加视觉效果。Cocos2d-x 有许多种 Transition 类。然而,使用它们的模式只有一种。

auto nextScene = HelloWorld::createScene();
auto transition = TransitionFade::create(1.0f, nextScene);
Director::getInstance()->replaceScene(transition);

这可以在场景被推入时使用。

auto nextScene = HelloWorld::createScene();
auto transition = TransitionFade::create(1.0f, nextScene);
Director::getInstance()->pushScene(transition);

它是如何工作的...

首先,您需要创建 nextscene 对象。然后,您需要创建一个具有设定持续时间和即将进入的场景对象的 transition 对象。最后,您需要使用 transition 对象运行 Director::pushScene。此配方将过渡场景的持续时间和淡入淡出动作设置为 1 秒。以下表格列出了主要的 Transition 类:

| Transition Class | 描述 |
| --- |
| TransitionRotoZoom | 旋转并缩放即将离开的场景,然后旋转并缩放进入即将进入的场景。 |
| TransitionJumpZoom | 缩放并跳转到即将离开的场景,然后跳转并缩放进入即将进入的场景。 |
| TransitionMoveInL | 从右向左移动场景。 |
| TransitionSlideInL | 从左侧滑入即将进入的场景。 |
| TransitionShrinkGrow | 在缩小即将离开的场景的同时放大即将进入的场景。 |
| TransitionFlipX | 水*翻转屏幕。 |
| TransitionZoomFlipX | 通过缩放和*移水*翻转屏幕。正面显示的是即将离开的场景,背面显示的是即将进入的场景。 |
| TransitionFlipAngular | 将屏幕水*翻转一半,垂直翻转一半。 |
| TransitionZoomFlipAngular | 通过稍微缩放和*移水*翻转一半,垂直翻转一半。 |
| TransitionFade | 从即将离开的场景淡出,然后淡入即将进入的场景。 |
| TransitionCrossFade | 使用 RenderTexture 对象交叉淡入两个场景。 |
| TransitionTurnOffTiles | 以随机顺序关闭即将离开的场景的瓦片。 |
| TransitionSplitCols | 奇数列向上移动,偶数列向下移动。 |
| TransitionSplitRows | 奇数行向左移动,偶数行向右移动。 |
| TransitionFadeTR | 从左下角到右上角渐变即将离开的场景的瓦片。 |
| TransitionFadeUp | 从底部到顶部渐变即将离开的场景的瓦片。 |
| TransitionPageTurn | 将场景的右下角翻起,以过渡到下面的场景,从而模拟翻页效果。 |
| TransitionProgressRadialCW | 逆时针径向过渡到下一个场景。 |

还有更多...

您还可以通过使用 onEnterTransitionDidFinish 方法和 onExitTransitionDidStart 方法来学习过渡场景的开始和结束。当您的游戏完全显示新场景时,onEnterTransitionDidFinish 方法会被调用。当旧场景开始消失时,onExitTransitionDidStart 方法会被调用。如果您想在场景出现或消失期间做些事情,您将需要使用这些方法。

现在我们来看一个使用 onEnterTransitionDidFinishonExitTransitionDidStart 方法的例子。HelloWorldScene.h 包含以下代码:

class HelloWorld : public cocos2d::Layer
{
public:
  static cocos2d::Scene* createScene();
  virtual bool init();
  CREATE_FUNC(HelloWorld);

  virtual void onEnterTransitionDidFinish();
  virtual void onExitTransitionDidStart();
};

HelloWorldScene.cpp has the following code:
void HelloWorld::onEnterTransitionDidFinish()
{
  CCLOG("finished enter transition");
}

void HelloWorld::onExitTransitionDidStart()
{
  CCLOG("started exit transition");
}

为替换场景制作原创过渡

你知道 Cocos2d-x 有很多过渡效果。然而,如果你需要的过渡效果它没有,那么创建一个原创的过渡效果是困难的。但是,如果你有基本的过渡效果知识,你仍然可以创建它。在这个菜谱中,我们将向你展示如何创建原创的过渡效果。

如何做...

尽管 Cocos2d-x 有很多不同类型的 Transition 类,但你可能找不到你需要的过渡效果。在这个菜谱中,你可以创建一个如开门这样的原创过渡效果。当场景替换开始时,上一个场景被分割成两部分并向左或向右打开。

你必须创建名为 "TransactionDoor.h" 和 "TransactionDoor.cpp" 的新文件,并将它们添加到你的项目中。

TransactionDoor.h 包含以下代码:

#ifndef __TRANSITIONDOOR_H__
#define __TRANSITIONDOOR_H__

#include "cocos2d.h"

NS_CC_BEGIN

class CC_DLL TransitionDoor : public TransitionScene , public TransitionEaseScene
{
public:
  static TransitionDoor* create(float t, Scene* scene);

  virtual ActionInterval* action();
  virtual void onEnter() override;
  virtual ActionInterval * easeActionWithAction(ActionInterval * action) override;
  virtual void onExit() override;
  virtual void draw(Renderer *renderer, const Mat4 &transform,
  uint32_t flags) override;
  CC_CONSTRUCTOR_ACCESS:
  TransitionDoor();
  virtual ~TransitionDoor();

protected:
  NodeGrid* _gridProxy;
  private:
  CC_DISALLOW_COPY_AND_ASSIGN(TransitionDoor);
};

class CC_DLL SplitDoor : public TiledGrid3DAction
{
public:
  /**
   * creates the action with the number of columns to split and
   the duration
   * @param duration in seconds
   */
  static SplitDoor* create(float duration, unsigned int cols);

  // Overrides
  virtual SplitDoor* clone() const override;
  /**
   * @param time in seconds
   */ 
  virtual void update(float time) override;
  virtual void startWithTarget(Node *target) override;

CC_CONSTRUCTOR_ACCESS:
  SplitDoor() {}
  virtual ~SplitDoor() {}

  /** initializes the action with the number of columns to split
and the duration */
  bool initWithDuration(float duration, unsigned int cols);

protected:
  unsigned int _cols;
  Size _winSize;

private:
  CC_DISALLOW_COPY_AND_ASSIGN(SplitDoor);
};

NS_CC_END

#endif /* defined(__TRANSITIONDOOR_H__) */

使用以下代码为 TransactionDoor.cpp

#include "TransitionDoor.h"

NS_CC_BEGIN

TransitionDoor::TransitionDoor()
{
  _gridProxy = NodeGrid::create();
  _gridProxy->retain();
}
TransitionDoor::~TransitionDoor()
{
  CC_SAFE_RELEASE(_gridProxy);
}

TransitionDoor* TransitionDoor::create(float t, Scene* scene)
{
  TransitionDoor* newScene = new (std::nothrow) TransitionDoor();
  if(newScene && newScene->initWithDuration(t, scene))
  {
    newScene->autorelease();
    return newScene;
  }
  CC_SAFE_DELETE(newScene); 
  return nullptr;
}

void TransitionDoor::onEnter()
{
  TransitionScene::onEnter();

  _inScene->setVisible(true);

  _gridProxy->setTarget(_outScene);
  _gridProxy->onEnter();

  ActionInterval* split = action();
  ActionInterval* seq = (ActionInterval*)Sequence::create
  (
   split,
   CallFunc::create(CC_CALLBACK_0(TransitionScene::finish,this)),
   StopGrid::create(),
   nullptr
   );

  _gridProxy->runAction(seq);
}

void TransitionDoor::draw(Renderer *renderer, const Mat4
&transform, uint32_t flags)
{
  Scene::draw(renderer, transform, flags);
  _inScene->visit();
  _gridProxy->visit(renderer, transform, flags);
}

void TransitionDoor::onExit()
{
  _gridProxy->setTarget(nullptr);
  _gridProxy->onExit();
  TransitionScene::onExit();
}

ActionInterval* TransitionDoor:: action()
{
  return SplitDoor::create(_duration, 3);
}

ActionInterval*
TransitionDoor::easeActionWithAction(ActionInterval * action)
{
  return EaseInOut::create(action, 3.0f);
}

SplitDoor* SplitDoor::create(float duration, unsigned int cols)
{
  SplitDoor *action = new (std::nothrow) SplitDoor();

  if (action)
  {
    if (action->initWithDuration(duration, cols))
    {
      action->autorelease();
    }
    else
    {
      CC_SAFE_RELEASE_NULL(action);
    }
  }

  return action;
}

bool SplitDoor::initWithDuration(float duration, unsigned int cols)
{
  _cols = cols;
  return TiledGrid3DAction::initWithDuration(duration, Size(cols,
1));
}

SplitDoor* SplitDoor::clone() const 
{
  // no copy constructor
  auto a = new (std::nothrow) SplitDoor();
  a->initWithDuration(_duration, _cols);
  a->autorelease();
  return a;
}

void SplitDoor::startWithTarget(Node *target)
{
  TiledGrid3DAction::startWithTarget(target);
  _winSize = Director::getInstance()->getWinSizeInPixels();
}

void SplitDoor::update(float time)
{
  for (unsigned int i = 0; i < _gridSize.width; ++i) 
  {
    Quad3 coords = getOriginalTile(Vec2(i, 0));
    float  direction = 1;

    if ( (i % 2 ) == 0 )
    {
      direction = -1;
    }

    coords.bl.x += direction * _winSize.width/2 * time;
    coords.br.x += direction * _winSize.width/2 * time;
    coords.tl.x += direction * _winSize.width/2 * time;
    coords.tr.x += direction * _winSize.width/2 * time;|

    setTile(Vec2(i, 0), coords);
  }
}
NS_CC_END

以下代码将允许我们使用 TransitionDoor 效果:

auto trans = TransitionDoor::create(1.0f,
HelloWorld::createScene());
Director::getInstance()->replaceScene(trans);

它是如何工作的...

所有类型的过渡都以 TransitionScene 作为 SuperClassTransitionScene 是一个基本类,具有基本的过渡过程。如果你想以更简单的方式创建原创的过渡效果,你会在 Cocos2d-x 中寻找类似的过渡效果。然后,你可以从类似的类创建你的类。TransitionDoor 类是从 TransitionSplitCol 类创建的。然后,根据需要添加和修改它们。然而,为了修复它们,你需要对这些有基本的知识。

Transition 类的一些重要属性如下:

属性 描述
_inScene 指向下一个场景的指针。
_outScene 出场景的指针。
_duration 过渡的持续时间,由创建方法指定的浮点值。
_isInSceneOnTop 布尔值;如果为真,则下一个场景是场景图的顶部。

transition 类的一些重要属性如下:

属性 描述
onEnter 开始过渡效果。
Action 创建效果动作。
onExit 完成过渡效果并进行清理。

TransitionDoor 类的情况下,下一个场景在 onEnter 方法中被设置为可见,上一个场景在分割成两个网格。然后,开始一个如开门这样的效果。在动作方法中,通过使用 SplitDoor 类创建 Action 类的实例。SplitDoor 类基于 Cocos2d-x 中的 SplitCol 类。SplitDoor 类将上一个场景的两个网格向左或向右移动。

更多...

除了上述描述的方法之外,还有一些必要的方法。这些方法在 Node 类中定义。

属性 描述
onEnter 节点开始出现在屏幕上
onExit 节点从屏幕消失
onEnterTransitionDidFinish 节点在屏幕上出现后完成过渡效果
onExitTransitionDidStart 节点在从屏幕消失前开始过渡效果

如果您想在场景出现在屏幕上时播放背景音乐,您可以通过使用 onEnter 方法来播放它。如果您想在过渡效果完成之前播放它,请使用 onEnterTransitionDidFinish 方法。除此之外,onEnter 方法中的初始过程在 onEnterTransitionDidFinish 方法中开始动画,在 onExit 方法中清理过程,等等。

为弹出场景创建原创过渡效果

Cocos2d-x 为推入场景提供了过渡效果。但由于某种原因,它没有为弹出场景提供过渡效果。我们希望在推入场景后使用效果弹出场景。在本教程中,我们将解释如何为弹出场景创建一个原创的过渡效果。

准备工作

在本教程中,您将了解如何使用效果弹出过渡场景。您需要创建一个新的类,因此您必须创建名为 DirectorEx.hDirectorEx.cpp 的新类文件,并将它们添加到您的项目中。

如何操作...

Cocos2d-x 为推入场景提供了带有效果的过渡场景。然而,它没有为弹出场景提供过渡效果。因此,我们创建了一个名为 DirectorEx 的原创类来为弹出场景创建过渡效果。接下来的代码片段提供了代码示例。

DirectorEx.h 包含以下代码:

class DirectorEx : public Director
{
public:
  Scene* previousScene(void);
  void popScene(Scene* trans);
};

DirectorEx.cpp 包含以下代码:

#include "DirectorEx.h"

Scene* DirectorEx::previousScene()
{
  ssize_t sceneCount = _scenesStack.size();
  if (sceneCount <= 1) {
    return nullptr;
  }
  return _scenesStack.at(sceneCount-2);
}

void DirectorEx::popScene(Scene* trans)
{
  _scenesStack.popBack();
  ssize_t sceneCount = _scenesStack.size();
  if (sceneCount==0) {
    end();
  } else {
    _sendCleanupToScene = true;
    _nextScene = trans;
  }
}

此类可以使用如下:

DirectorEx* directorEx = static_cast<DirectorEx*>(Director::getInstance());
Scene* prevScene = directorEx->previousScene();
Scene* pScene = TransitionFlipX::create(duration, prevScene);
directorEx->popScene(pScene);

它是如何工作的...

如果我们自定义了 Cocos2d-x 中的 Director 类,它可以使用弹出场景的效果进行过渡。然而,这并不是一个好主意。因此,我们创建了一个名为 DirectorExDirector 类的子类,并使用此类如下:

  1. 首先,您可以通过获取 DirectorEx 类的实例来将 Director 类的实例进行转换。

    DirectorEx* directorEx = static_cast<DirectorEx*>(Director::getInstance());
    
  2. 此外,您还需要获取前一个场景的实例。

    Scene* prevScene = directorEx->previousScene();
    
  3. 接下来,您必须创建一个过渡效果。

    Scene* pScene = TransitionFlipX::create(duration, prevScene);
    
  4. 最后,您可以使用 DirectorEx::popScene 方法使用此效果弹出场景。

    directorEx->popScene(pScene);
    

创建层

层是一个可以在 Scene 上使用的对象。它类似于 Photoshop 中的图层的一个透明层。所有对象都添加到 Layer 中,以便在屏幕上显示。此外,一个场景可以有多个层。层还负责接受输入、绘制和触摸。例如,在游戏中,一个场景有一个背景层、HUD 层和一个玩家的层。在本教程中,我们将解释如何使用 Layer

如何操作...

以下代码展示了如何创建一个层并将其添加到场景中:

auto layer = Layer::create();
this->addChild(layer);

这很简单。如果您有一个颜色层,您就可以做到。

auto layer = LayerColor::create(Color4B::WHITE);
this->addChild(layer);

它是如何工作的...

场景类是显示在屏幕上的,但 Layer 类可以堆叠在多个层中。场景有一个或多个层,精灵必须位于一个层上。Layer 类是一个透明的纸。此外,一个透明的节点需要更多的 CPU 资源。因此,你需要小心不要堆叠太多的层。

创建模态层

在用户界面设计中,模态层是一个重要的层。模态层就像一个子窗口。当模态层显示时,玩家不能触摸模态层外的任何其他按钮。他们只能触摸模态层上的按钮。当我们需要与玩家确认某些事情时,我们需要模态层。在这个菜谱中,我们将解释如何创建模态层。

如何操作...

首先,你需要创建两个名为 ModalLayer.hModalLayer.cpp 的新文件。它们应该包含以下代码:

ModalLayer.h 应该包含以下代码:

#include "cocos2d.h"

USING_NS_CC;

class ModalLayer : public Layer
{
public:
  ModalLayer();
  ~ModalLayer();
  bool init();
  CREATE_FUNC(ModalLayer);
  void close(Ref* sender=nullptr);
};

ModalLayer.cpp should have the following code:
#include "ModalLayer.h"
USING_NS_CC;

ModalLayer::ModalLayer()
{
}

ModalLayer::~ModalLayer()
{
}

bool ModalLayer::init()
{
  if (!Layer::init())
  {
    return false;
  }

  auto listener = EventListenerTouchOneByOne::create();
  listener->setSwallowTouches(true);
  listener->onTouchBegan = [](Touch *touch,Event*event)->bool{
  return true; };
  this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

  return true;
}

void ModalLayer::close(Ref* sender)
{
  this->removeFromParentAndCleanup(true);
}

你应该从 ModalLayer 类创建一个子类,并添加一个菜单按钮或你需要的设计。然后,你必须创建它的一个实例并将其添加到正在运行的场景中。然后,它应该启用模态层上的按钮,但禁用模态层底部的按钮。

// add modal layer
auto modal = ModalLayer::create();
this->addChild(modal);

// close modal layer
modal->close();

它是如何工作的...

在 Cocos2d-x 版本 3 中创建模态层很容易。在版本 3 中,触摸事件从层的顶部发生。所以,如果模态层捕捉到所有触摸事件,那么位于模态层下的节点会收到这些事件的通知。模态层正在捕捉所有的事件。请参考以下代码:

listener->onTouchBegan = [](Touch *touch,Event*event)->bool{ return true; };

小贴士

这个模态层可以捕捉所有触摸事件。然而,Android 有像返回键这样的按键事件。当玩家在模态层显示时触摸返回键,你必须决定如何处理。在一种情况下,模态层会被关闭,在另一种情况下,返回键会被忽略。

第五章:创建 GUI

在本章中,我们将创建各种 UI 组件。本章将涵盖以下主题:

  • 创建菜单

  • 创建按钮

  • 创建复选框

  • 创建加载条

  • 创建滑块

  • 创建文本字段

  • 创建滚动视图

  • 创建页面视图

  • 创建列表视图

简介

游戏有很多 GUI 组件,例如,有菜单、按钮、复选框、加载条等等。没有这些组件我们无法制作游戏。此外,这些与之前讨论的节点略有不同。在本章中,我们将了解如何为游戏创建各种 GUI 组件,如菜单、滑块、文本字段等。

创建菜单

在这个菜谱中,我们将创建一个菜单。菜单有各种按钮,例如开始按钮和暂停按钮。菜单是任何游戏都非常重要的组件,并且它们也非常有用。使用菜单的步骤稍微复杂一些。在这个菜谱中,我们将简要了解创建菜单的过程,以了解其复杂性并熟悉它们。

准备中

我们准备了一张作为按钮图像的图片,并将其添加到项目中的Resources/res文件夹。我们将使用以下按钮图片作为菜单:

准备中

如何做...

首先,我们将创建一个简单的菜单,其中有一个按钮项。我们将使用item1.png文件作为按钮图像。通过以下代码创建菜单。

auto normalItem = Sprite::create("res/item1.png");
auto selectedItem = Sprite::create("res/item1.png");
selectedItem->setColor(Color3B::GRAY);
auto item = MenuItemSprite::create(normalItem, selectedItem,
[](Ref* sender){
    CCLOG("tapped item");
});
auto size = Director::getInstance()->getVisibleSize();
item->setPosition(size/2);
auto menu = Menu::create(item, nullptr);
menu->setPosition(Vec2());
this->addChild(menu);

以下图像显示了此代码的执行结果:

如何做...

此外,你可以在点击菜单项后在日志中看到tapped item文本。你会注意到当你点击按钮时,按钮会变得稍微暗一些。

它是如何工作的...

  1. 创建一个表示未操作时按钮的正常状态的精灵。

  2. 在按钮被按下时创建一个表示选中状态的精灵。在这种情况下,我们使用了正常状态和选中状态相同的图像,但玩家在点击按钮时无法理解状态的变化。这就是为什么我们通过使用setColor方法将选中图像改为稍微暗一些的图像。

  3. 通过使用这两个精灵创建MenuItemSprite类的实例。第三个参数指定了当按钮被按下时需要处理的 lambda 表达式。

这次,我们在菜单中只创建了一个按钮,但我们可以添加更多按钮。要做到这一点,我们可以在Menu::create方法中枚举几个项目,并在末尾指定nullptr。要添加多个按钮到菜单中,请使用以下代码:

auto menu = Menu::create(item1, item2, item3, nullptr);

此外,还可以通过使用菜单实例的addChild方法添加项目。

menu->addChild(item);

如果按钮被按下,当你创建MenuItemSprite实例时指定的 lambda 表达式开始运行。参数传递了一个被按下的MenuItemSprite实例。

还有更多...

还可以自动对齐多个按钮。我们在Resources/res文件夹中创建了三个项目。这些项目的名称分别为item1.pngitem2.pngitem3.png。您可以创建三个按钮,并使用以下代码将这些按钮垂直对齐在屏幕中央:

Vector<MenuItem*> menuItems;
for (int i=1; i<=3; i++) {
    std::string name = StringUtils::format("res/item%d.png", i);
    auto normalItem = Sprite::create(name);
    auto selectedItem = Sprite::create(name);
    selectedItem->setColor(Color3B::GRAY);
    auto item = MenuItemSprite::create(normalItem, selectedItem,
[](Ref* sender){
        auto node = dynamic_cast<Node*>(sender);
        if (node!=nullptr) {
            CCLOG("tapped item %d", node->getTag());
        }
    });
    item->setTag(i);
    menuItems.pushBack(item);
}
auto size = Director::getInstance()->getVisibleSize();
auto menu = Menu::createWithArray(menuItems);
menu->setPosition(size/2);
menu->alignItemsVertically();
this->addChild(menu);

还有更多...

如果您想水*对齐这些项目,可以使用以下代码:

menu->alignItemsHorizontally();

到目前为止,间隔的对齐已经自动调整;然而,如果您想指定填充,可以使用另一种方法。

以下代码将以垂直方式指定并排的间隔:

menu->alignItemsVerticallyWithPadding(20.0f);

以下代码将以水*方式指定并排的间隔:

menu->alignItemsHorizontallyWithPadding(20.0f);

创建按钮

在这个菜谱中,我们将解释如何创建按钮。在Button类发布之前,我们通过使用之前菜谱中介绍的Menu类创建按钮。由于Button类的出现,现在可以精细控制按钮的按下。

准备工作

要使用本章中提到的Button类和其他 GUI 组件,您必须包含CocosGUI.h文件。让我们在HelloWorldScene.cpp中添加以下代码行:

#include "ui/CocosGUI.h"

如何做...

让我们使用Button类创建一个按钮。首先,您将使用之前菜谱中使用的item1.png图像生成按钮实例。我们还将通过使用addEventListener方法在按钮按下时指定回调函数作为 lambda 表达式。您可以使用以下代码创建按钮:

auto size = Director::getInstance()->getVisibleSize();
auto button = ui::Button::create("res/item1.png");
button->setPosition(size/2);
this->addChild(button);
button-> addTouchEventListener(
    [](Ref* sender, ui::Widget::TouchEventType type){
        switch (type) {
            case ui::Widget::TouchEventType::BEGAN:
                CCLOG("touch began");
                break;
            case ui::Widget::TouchEventType::MOVED:
                CCLOG("touch moved");
                break;
            case ui::Widget::TouchEventType::ENDED:
                CCLOG("touch ended");
                break;
            case ui::Widget::TouchEventType::CANCELED:
                CCLOG("touch canceled");
                break;

            default:
                break;
        }
    });

工作原理...

您现在可以运行此项目并按下按钮。进一步,您可以移动触摸位置并释放手指。这样,您将看到按钮的触摸状态将在日志中改变。让我们一步一步地看看。

当您使用本章中提到的Button类和其他 GUI 组件时,您必须包含CocosGUI.h文件,因为这个文件定义了必要的类。此外,请注意,这些类有自己的命名空间,例如"cocos2d::ui"。

创建Button类的实例很容易。您只需指定精灵文件名。此外,您可以通过使用addTouchEventListener方法创建一个回调函数作为 lambda 表达式。此函数有两个参数。第一个参数是按下的按钮实例。第二个参数是触摸状态。触摸状态有四种类型。TouchEventType::BEGAN是在按钮按下时的状态。TouchEventType::MOVE是在按下后移动手指时发生的事件类型。TouchEventType::ENDED是在您从屏幕上释放手指时的状态。TouchEventType::CANCELED是在您在按钮外部释放手指时发生的事件。

还有更多...

可以通过指定选择状态图片和禁用状态图片来创建按钮实例。使用以下代码创建此按钮。

auto button = ui::Button::create(
    "res/normal.png",
    "res/selected.png",
    "res/disabled.png");

MenuItemSprite类不同,您无法通过更改使用setColor方法设置的普通图片颜色来指定选择状态。您必须准备选择图片和禁用图片。

创建复选框

在这个菜谱中,我们将创建一个复选框。在 Cocos2d-x 版本 2 中,复选框是通过使用MenuItemToggle类创建的。然而,这样做相当繁琐。在 Cocos2d-x 版本 3 中,我们可以通过使用 Cocos Studio 中的Checkbox类来创建复选框。

准备中

因此,在您开始之前,让我们准备复选框的图片。在这里,我们已经准备了所需的最低OnOff状态图片。请将这些图片添加到Resouces/res文件夹中。

关闭状态的图片看起来可能如下所示:

准备中

开启状态的图片看起来可能如下所示:

准备中

如何操作...

让我们使用Checkbox类创建一个复选框。首先,您将使用check_box_normal.png图片和check_box_active.png图片生成复选框实例。您还将通过使用addEventListener方法指定当复选框状态改变时的回调函数,作为 lambda 表达式。使用以下代码创建复选框:

auto size = Director::getInstance()->getVisibleSize();
auto checkbox = ui::CheckBox::create(
    "res/check_box_normal.png",
    "res/check_box_active.png");
checkbox->setPosition(size/2);
this->addChild(checkbox);
checkbox->addEventListener([](Ref* sender, ui::CheckBox::EventType type){
    switch (type) {
        case ui::CheckBox::EventType::SELECTED:
            CCLOG("selected checkbox");
            break;
        case ui::CheckBox::EventType::UNSELECTED:
            CCLOG("unselected checkbox");
            break;
        default:
            break;
    }
});

以下图示显示了运行前面的代码后复选框被选中。

如何操作...

它是如何工作的...

它通过指定OnOff图片生成复选框实例。此外,回调函数的指定方式与之前菜谱中Button类的方式相同。复选框有两个EventType选项,即ui::Checkbox::EventType::SELECTEDui::Checkbox::EventType::UNSELECTED

您也可以使用isSelected方法获取复选框的状态。

If (checkbox->isSelected()) {
    CCLOG("selected checkbox");
} else {
  CCLOG("unselected checkbox");
}

您也可以使用setSelected方法更改复选框的状态。

checkbox->setSelected(true);

更多内容...

此外,还可以进一步指定更详细复选框状态的图片。Checkbox::create方法有五个参数。这些参数如下:

  • 未选择图片

  • 未选择且按下图片

  • 选择图片

  • 未选择且禁用图片

  • 选择且禁用图片

这里是如何指定这五种状态图片的:

auto checkbox = ui::CheckBox::create(
    "res/check_box_normal.png",
    "res/check_box_normal_press.png",
    "res/check_box_active.png",
    "res/check_box_normal_disable.png",
    "res/check_box_active_disable.png");

要禁用复选框,请使用以下代码:

checkbox->setEnabled(false);

创建加载条

当您正在处理一个进程或下载某些内容时,您可以通过向用户展示其进度来表明它没有冻结。为了显示这样的进度,Cocos2d-x 有一个LoadingBar类。在这个菜谱中,您将学习如何创建和显示加载条。

准备中

首先,我们必须为进度条准备一个图片。这个图片被称为loadingbar.png。您需要将这个图片添加到Resouces/res文件夹中。

准备中

如何做...

通过指定加载条的图像生成加载条的实例。进一步地,使用setPercent方法将其设置为 0%。最后,为了使条从 0%到 100%以每 0.1 秒 1%的速度前进,我们将使用以下schedule方法:

auto loadingbar = ui::LoadingBar::create("res/loadingbar.png");
loadingbar->setPosition(size/2);
loadingbar->setPercent(0);
this->addChild(loadingbar);
this->schedule(={
    float percent = loadingbar->getPercent();
    percent++;
    loadingbar->setPercent(percent);
    if (percent>=100.0f) {
        this->unschedule("updateLoadingBar");
    }
}, 0.1f, "updateLoadingBar");

以下图是加载条在 100%时的图像。

如何做...

工作原理...

您必须指定一个图像作为加载条图像以创建LoadingBar类的实例。您可以通过使用setPercent方法设置加载条的百分比。此外,您可以通过使用getPercent方法获取其百分比。

还有更多...

默认情况下,加载条将向右移动。您可以通过使用setDirection方法更改此方向。

loadingbar->setDirection(ui::LoadingBar::Direction::RIGHT);

当您设置ui::LoadingBar::Direction::RIGHT值时,加载条的开始位置是右边缘。然后,加载条将向左方向移动。

创建滑块

在本配方中,我们将解释滑块。滑块将用于更改声音或音乐的音量等任务。Cocos2d-x 有一个Slider类用于此目的。如果我们使用此类,我们可以轻松创建滑块。

准备工作

因此,在我们开始之前,让我们准备滑块的图像。请在Resouces/res文件夹中添加这些图像。

  • sliderTrack.png:滑块的背景准备工作

  • sliderThumb.png:用于移动滑块的图像准备工作

如何做...

让我们使用Slider类创建一个滑块。首先,您将使用sliderTrack.png图像和sliderThumb.png图像生成滑块实例。您还将通过使用addEventListener方法指定当滑块值改变时的回调函数,作为 lambda 表达式。

auto slider = ui::Slider::create("res/sliderTrack.png",
"res/sliderThumb.png");
slider->setPosition(size/2);
this->addChild(slider);
slider->addEventListener([](Ref* sender, ui::Slider::EventType
type){
    auto slider = dynamic_cast<ui::Slider*>(sender);
    if (type==ui::Slider::EventType::ON_PERCENTAGE_CHANGED) {
        CCLOG("percentage = %d", slider->getPercent());
    }
});

以下图显示了前面代码的结果。

如何做...

工作原理...

您必须指定两个图像作为滑块的条形图像和滑块的拇指图像以创建Slider类的实例。回调函数的指定方式与之前配方中的Button类相同。滑块只有一个EventType,即ui::Slider::EventType::ON_PERCENTAGE_CHANGED。这就是为什么状态是唯一变化值的原因。您可以通过使用getPercent方法获取滑条上显示的百分比。

还有更多...

如果您想在滑块上看到进度,您可以使用loadProgressBarTexture方法。我们需要一个进度条的图像。以下图像显示了进度条图像。让我们将其添加到Resources/res文件夹中。

还有更多...

然后,我们通过指定此图像使用loadProgressbarTexture方法。

slider->loadProgressBarTexture("res/sliderProgress.png");

让我们运行到目前为止已修改的代码。您将看到如下截图所示的条左侧的颜色:

还有更多...

创建文本字段

你可能想在游戏中设置一个昵称。要设置昵称或获取玩家的输入文本,你可以使用TextField类。在本教程中,我们将学习一个简单的TextField示例以及如何在游戏中添加文本框。

如何做...

你将通过指定占位符文本、字体名称和字体大小来创建一个文本字段。然后,你通过使用addEventListener设置一个回调函数。在回调函数中,你可以获取玩家在textField中输入的文本。通过以下代码创建textField

auto textField = ui::TextField::create("Enter your name", "Arial", 30);
textField->setPosition(Vec2(size.width/2, size.height*0.75f));
this->addChild(textField);
textField->addEventListener([](Ref* sender,
ui::TextField::EventType type){
    auto textField = dynamic_cast<ui::TextField*>(sender);
    switch (type) {
        case ui::TextField::EventType::ATTACH_WITH_IME:
                CCLOG("displayed keyboard");
                break;
        case ui::TextField::EventType::DETACH_WITH_IME:
                CCLOG("dismissed keyboard");
                break;
          case ui::TextField::EventType::INSERT_TEXT:
                CCLOG("inserted text : %s",
                textField->getString().c_str());
                break;
          case ui::TextField::EventType::DELETE_BACKWARD:
                CCLOG("deleted backward");
                break;
          default:
                break;
    }
});

让我们运行这段代码。你将在占位符文本中看到它,并且它将自动显示键盘,如下面的截图所示:

如何做...

它是如何工作的...

  1. 你创建了一个TextField类的实例。第一个参数是占位符字符串。第二个参数是字体名称。你可以指定仅真型字体。第三个参数是字体大小。

  2. 你可以通过使用addEventListener方法获取事件。以下列表提供了事件名称及其描述:

事件名称 描述
ATTACH_WITH_IME 键盘将出现。
DETACH_WITH_IME 键盘将消失。
INSERT_TEXT 文本已输入。你可以通过使用getString方法获取字符串。
DELETE_BACKWARD 文本被删除。

还有更多...

当玩家输入密码时,你必须使用setPasswordEnable方法来隐藏它。

textField->setPasswordEnabled(true);

让我们运行到目前为止修改过的代码。你将看到如何隐藏你输入的密码,如下面的截图所示:

还有更多...

创建滚动视图

当你在游戏中显示一个巨大的地图时,需要一个滚动视图。它可以通过滑动来滚动,并在区域边缘弹跳。在本教程中,我们解释了 Cocos2d-x 的ScrollView类。

如何做...

让我们立即实现它。在这种情况下,我们将HelloWorld.png的大小加倍。此外,我们尝试在ScrollView中显示这个巨大的图像。通过以下代码创建滚动视图:

auto scrollView = ui::ScrollView::create();
scrollView->setPosition(Vec2());
scrollView->setDirection(ui::ScrollView::Direction::BOTH);
scrollView->setBounceEnabled(true);
this->addChild(scrollView);

auto sprite = Sprite::create("res/HelloWorld.png");
sprite->setScale(2.0f);
sprite->setPosition(sprite->getBoundingBox().size/2);
scrollView->addChild(sprite);
scrollView->setInnerContainerSize(sprite->getBoundingBox().size);
scrollView->setContentSize(sprite->getContentSize());

让我们运行这段代码。你将看到巨大的HelloWorld.png图像。此外,你将看到你可以通过滑动来滚动它。

如何做...

它是如何工作的...

  1. 你可以通过不带参数的create方法创建一个ScrollView类的实例。

  2. 你可以通过使用setDirection方法设置滚动视图的方向。在这种情况下,我们想要上下左右滚动,所以你应该设置ui::ScrollView::Direction::BOTH。这意味着我们可以垂直和水*滚动。如果你想只上下滚动,你设置ui::ScrollView::Direction::VERTICAL。如果你想只左右滚动,你设置ui::ScrollView::Direction::HORIZONTAL

  3. 如果你希望在区域边缘滚动时产生弹跳效果,你应该使用setBounceEnabled方法将其设置为true

  4. 你将提供要在滚动视图中显示的内容。在这里,我们使用了放大两倍的HelloWorld.png

  5. 你必须使用setInnerContainerSize方法指定滚动视图中的内容大小。在这种情况下,我们在setInnerContainerSize方法中指定了HelloWorld.png的两倍大小。

  6. 你必须使用setContentSize方法指定滚动视图的大小。在这种情况下,我们使用setContentSize方法指定了HelloWorld.png的原始大小。

创建页面视图

页面视图类似于滚动视图,但它将按页面滚动。PageView也是 Cocos2d-x 中的一个类。在这个菜谱中,我们将解释如何使用PageView类。

如何实现...

让我们立即实现它。在这里,我们将三个HelloWorld.png图像并排排列在页面视图中。通过以下代码创建页面视图:

auto pageView = ui::PageView::create();
pageView->setPosition(Vec2());
pageView->setContentSize(size);
this->addChild(pageView);

for (int i=0; i<3; i++) {
    auto page = ui::Layout::create();
    page->setContentSize(pageView->getContentSize());

    auto sprite = Sprite::create("res/HelloWorld.png");
    sprite->setPosition(sprite->getContentSize()/2);
    page->addChild(sprite);
    pageView->insertPage(page, i);
}

pageView->addEventListener([](Ref* sender, ui::PageView::EventType type){
    if (type==ui::PageView::EventType::TURNING) {
        auto pageView = dynamic_cast<ui::PageView*>(sender);
        CCLOG("current page no =%zd",
        pageView->getCurPageIndex());
    }
});

当你运行此代码时,你会看到一个HelloWorld.png。你会看到你可以通过滑动动作移动到下一页。

如何工作...

使用不带参数的create方法创建PageView类的实例。在这里,我们将其设置为与屏幕相同的大小。

并排显示三个HelloWorld.png图像。你必须使用Layout类在PageView中设置页面布局。

使用addChild方法设置页面大小并添加图片。

使用insertPage方法将Layout类的实例插入到页面视图中。此时,你指定页面编号作为第二个参数。

获取页面变化的事件,你使用addEventListener方法。PageView只有一个事件,即PageView::EventType::TURNING。你可以通过使用getCurPageIndex方法获取当前页面编号。

创建列表视图

ListView是 Cocos2d-x 中的一个类,类似于 iOS 中的UITableView或 Android 中的List ViewListView在设置场景时需要创建大量按钮时非常有用。在这个菜谱中,我们将解释如何使用ListView类。

如何实现...

在这里,我们尝试显示包含 20 个按钮的ListView。每个按钮都有一个像"list item 10."这样的编号。此外,当你点击任何按钮时,你会在日志上显示你选择的按钮编号。通过以下代码创建列表视图:

auto listView = ui::ListView::create();
listView->setPosition(Vec2(size.width/2 - 200, 0.0f));
listView->setDirection(ui::ListView::Direction::VERTICAL);
listView->setBounceEnabled(true);
listView->setContentSize(size);
this->addChild(listView);

for (int i=0; i<20; i++) {
    auto layout = ui::Layout::create();
    layout->setContentSize(Size(400, 50));
    layout->setBackGroundColorType(ui::Layout::BackGroundColorType::SOLID);
    layout->setBackGroundColor(Color3B::WHITE);

    auto button = ui::Button::create();
    button->setPosition(layout->getContentSize()/2);
    std::string name = StringUtils::format("list item %d", i); 
    button->setTitleText(name);
    button->setTitleFontSize(30);
    button->setTitleColor(Color3B::BLACK);
    layout->addChild(button);
    listView->addChild(layout);
}

listView->addEventListener([](Ref* sender, ui::ListView::EventType 
type){
    auto listView = dynamic_cast<ui::ListView*>(sender);
    switch (type) {
        case ui::ListView::EventType::ON_SELECTED_ITEM_START:
            CCLOG("select item started");
            break;
        case ui::ListView::EventType::ON_SELECTED_ITEM_END:
            CCLOG("selected item : %zd", listView->getCurSelectedIndex());
            break;
        default: 
            break; 
    } 
});

当你运行此代码时,你会看到一些按钮。你会看到你可以通过滑动来滚动它,并且你可以获取你点击的按钮编号。

如何实现...

如何工作...

  1. 创建ListView类的实例。你可以像ScrollView一样指定滚动方向。由于我们只想在垂直方向上滚动,你指定ui::ListView::Direction::VERTICAL。此外,你可以通过使用setBounceEnabled方法来指定区域边缘的弹跳效果。

  2. 在列表视图中创建 20 个按钮。您必须使用Layout类来显示列表视图中的内容,就像在PageView的情况下一样。您需要将Button类的一个实例添加到Layout类的实例中。

  3. 通过使用addEventListener方法来获取事件。ListView有两个事件,即ON_SELECTED_ITEM_STARTON_SELECTED_ITEM_END。当您触摸列表视图时,会触发ON_SELECTED_ITEM_START。当您不移动手指就释放时,会触发ON_SELECTED_ITEM_END。如果您移动手指,则不会触发ON_SELECTED_ITEM_END,这将是一个滚动过程。您可以通过使用getCurSelectedIndex方法来获取按钮编号。

第六章. 播放声音

没有声音的游戏将会无聊且缺乏活力。适合视觉的背景音乐和音效可以使游戏更加生动。最初,我们使用了一个非常著名的音频引擎,名为SimpleAudioEngine,但现在 Cocos2d-x 3.3 版本已经推出了全新的AudioEngine。在本章中,我们将讨论SimpleAudioEngineAudioEngine。本章将涵盖以下主题:

  • 播放背景音乐

  • 播放一个音效

  • 控制音量、音调和*衡

  • 暂停和恢复背景音乐

  • 暂停和恢复音效

  • 使用 AudioEngine 播放背景音乐和音效

  • 播放电影

播放背景音乐

通过使用SimpleAudioEngine,我们可以非常容易地播放背景音乐。SimpleAudioEngine是一个共享的单例对象,可以在代码的任何地方调用。在SimpleAudioEngine中,我们只能播放一个背景音乐。

准备工作

我们必须包含SimpleAudioEngine的头文件才能使用它。因此,您需要添加以下代码:

#include "SimpleAudioEngine.h"

如何操作...

以下代码用于播放名为background.mp3的背景音乐。

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
audio->preloadBackgroundMusic("background.mp3");

// play the background music and continuously play it.
audio->playBackgroundMusic("background.mp3", true);

它是如何工作的...

SimpleAudioEngine有一个名为CocosDenshion的命名空间。对于SimpleAudioEngine,您只需使用getInstance方法获取一个实例。您可以在不预加载的情况下播放背景音乐,但这可能会导致播放延迟。这就是为什么您应该在播放之前预加载音乐。如果您希望播放连续,则需要将 true 值作为第二个参数设置。

更多内容...

SimpleAudioEngine支持多种格式,包括 MP3 和 Core Audio 格式。它可以播放以下格式:

格式 iOS (BGM) iOS (SE) Android (BGM) Android (SE)
IMA (.caf)
Vorbis (.ogg)
MP3 (.mp3)
WAVE (.wav)

如果您想在 iOS 和 Android 上播放不同格式的声音,您可以使用以下宏代码来播放:

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) 
#defie MUSIC_FILE        "background.ogg" 
#else
#define MUSIC_FILE        "background.caf" 
#endif

audio->playBackgroundMusic(MUSIC_FILE, true);

在此代码中,如果设备是 Android,它将播放.ogg文件。如果设备是 iOS,它将播放.caf文件。

播放一个音效

通过使用SimpleAudioEngine,我们可以播放音效;要播放它们,我们只需要执行两个步骤,即预加载和播放。音效不是背景音乐;请注意,我们可以同时播放多个音效,但只能同时播放一个背景音乐。在本食谱中,我们将解释如何播放音效。

准备工作

就像播放背景音乐一样,你必须包含SimpleAudioEngine的头文件。

#include "SimpleAudioEngine.h"

如何操作...

让我们立即尝试播放一个音效。音频格式根据操作系统通过在播放背景音乐时引入的宏来改变。播放音效的代码如下:

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) 
#define EFFECT_FILE        "effect.ogg" 
#else 
#define EFFECT_FILE        "effect.caf" 
#endif

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
audio->preloadEffect( EFFECT_FILE ); 
audio->playEffect(EFFECT_FILE); 

它是如何工作的...

整体流程与播放背景音乐相同。在播放之前,您需要预加载一个音效文件。音效文件比背景音乐文件小。因此,在播放之前,您可以预加载很多音效。

更多内容...

在 Android 上,我们可以同时播放的音效数量少于 iOS。因此,我们现在将解释如何增加 Android 上的这个数量。最大同时播放次数在 Cocos2dxSound.java 中定义。

Cocos2dxSound.java 的路径是 cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib。然后,在第 66 行,定义了最大同时播放次数。

private static final int MAX_SIMULTANEOUS_STREAMS_DEFAULT = 5;

如果我们将这个值改为 10,我们就可以同时播放 10 个音效。

控制音量、音调和*衡

您可以控制音效的音量、音调和*衡,这三个因素的恰当组合可以使游戏听起来更加有趣。

如何操作...

让我们尝试通过控制音量、音调和*衡立即播放一个音效。以下是一个代码片段来完成此操作:

auto audio = CocosDenshion::SimpleAudioEngine::getInstance(); 
// set volume 
audio->setEffectsVolume(0.5); 

// set pitch, pan, gain with playing a sound effect.
float pitch = 1.0f;
float pan = 1.0f;
float gain = 1.0f;
audio->playEffect(EFFECT_FILE, true, pitch, pan, gain);

它是如何工作的...

您可以使用 setEffectsVolume 方法控制音效的音量。音量的最大值是 1.0,最小值是 0.0。如果您将音量设置为 0.0,音效将被静音。音量的默认值是 1.0。

您可以同时播放多个音效,但不能单独设置这些音效的音量。要更改音效的主音量,请使用 setEffectsVolume 方法设置音量。如果您想单独更改音量,应使用 gain 值;我们将在后面解释。

playEffect 方法中的第二个参数是连续播放音效的标志。对于第三个及以后的参数,请参考以下表格:

参数 描述 最小值 最大值
第三个参数 (pitch) 播放速度 0.0 2.0
第四个参数 (pan) 左右*衡 -1.0 1.0
第五个参数 (gain) 与音源的距离 0.0 1.0

pitch 是一个允许我们将声音分类为相对高音或低音的品质。通过使用这个 pitch,我们可以控制第三个参数中的播放速度。如果您将 pitch 设置为小于 1.0,音效将缓慢播放。如果设置为大于 1.0,音效将快速播放。如果设置为 1.0,音效将以原始速度播放。pitch 的最大值是 2.0。然而,在 iOS 中,您可以设置 pitch 大于 2.0。另一方面,Android 中 pitch 的最大值是 2.0。因此,我们采用了最大值作为下限。

你可以通过更改第四个参数中的pan来改变左右扬声器的*衡。如果你将其设置为-1.0,你只能从左扬声器听到声音。如果你将其设置为 1.0,你只能从右扬声器听到声音。默认值为 0.0;你可以从左右扬声器以相同的音量听到声音。不幸的是,你将无法在设备的扬声器中听到太多差异。如果你使用耳机,你可以听到这种差异。

你可以通过更改第五个参数中的gain来改变每个音效的音量。你可以使用setEffectVolume方法设置主音量,通过改变增益值来设置每个音效的音量。如果你将其设置为 0.0,则音量为静音。如果设置为 1.0,则音量为最大。音效的最终音量将是增益值和setEffectsVolume方法中指定的值的组合。

暂停和恢复背景音乐

这个配方将帮助你更好地理解暂停和恢复背景音乐的概念。

如何操作...

停止或暂停背景音乐非常简单。使用这些方法时,你不需要指定参数。停止背景音乐的代码如下:

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
// stop the background music
audio->stopBackgroundMusic();

暂停代码:

// pause the background music 
audio->pauseBackgroundMusic();

恢复暂停的背景音乐代码:

// resume the background music 
audio->resumeBackgroundMusic();

它是如何工作的...

你可以使用stopBackgroundMusic方法停止正在播放的背景音乐。或者,你可以使用pauseBackgroundMusic方法暂停背景音乐。一旦停止,你可以使用playBackgroundMusic方法再次播放。此外,如果你暂停了它,你可以使用resumeBackgroundMusic方法恢复播放音乐。

更多内容...

你可以使用isBackgroundMusicPlaying方法来确定背景音乐是否正在播放。以下代码可以用来实现这一点:

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
if (audio->isBackgroundMusicPlaying()) { 
    // background music is playing 
} else { // background music is not playing 
}

小贴士

然而,在使用此方法时,你需要小心。此方法总是返回一个表示 iOS 模拟器中播放状态的 true 值。在 Cocos2d-x 中的audio/ios/CDAudioManager.m的第 201 行,如果设备是 iOS 模拟器,SimpleAudioEngine将音量设置为零并连续播放。这就是为什么在 iOS 模拟器中存在问题。然而,我们在注释掉此过程之前测试了最新的 iOS 模拟器,发现没有问题。如果你想使用此方法,你应该注释掉此过程。

暂停和恢复音效

你可能还想停止音效。此外,你可能想先暂停它们,然后再恢复。

如何操作...

停止或暂停音效非常简单。以下是将它停止的代码:

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
unsigned int _soundId; 
// get the sound id as playing the sound effect
_soundId = audio->playEffect(EFFECT_FILE); 
// stop the sound effect by specifying the sound id
audio->stopEffect(_soundId);

以下是将它暂停的代码:

// pause the sound effect
audio->pauseEffect(_soundId);

你可以这样恢复暂停的代码:

// resume the sound effect
audio->resumeEffect(_soundId);

它是如何工作的...

SimpleAudioEngine 可以播放多个音效。因此,如果你想单独停止或暂停某个音效,你必须指定该音效。当你播放音效时,你可以得到音效 ID。你可以使用这个 ID 停止、暂停或恢复特定的音效。

还有更多...

你可以停止、暂停或恢复所有正在播放的音效。执行此操作的代码如下:

auto audio = CocosDenshion::SimpleAudioEngine::getInstance();
// stop all sound effects audio->stopAllEffects();
// pause all sound effects
audio->pauseAllEffects();
// resume all sound effects
audio->resumeAllEffects();

使用 AudioEngine 播放背景音乐和音效

AudioEngine 是从 Cocos2d-x 版本 3.3 中引入的新类。SimpleAudioEngine 无法播放多个背景音乐,但 AudioEngine 可以。此外,AudioEngine 在播放背景音乐完成后可以调用回调函数。另外,我们可以通过回调函数获取播放时间。在这个菜谱中,我们将学习全新的 AudioEngine

准备工作

我们必须包含 AudioEngine 的头文件才能使用它。此外,AudioEngine 有一个名为 experimental 的命名空间。为了包含头文件,你需要添加以下代码:

#include "audio/include/AudioEngine.h" USING_NS_CC; 
using namespace experimental;

如何操作...

AudioEngine 比起 SimpleAudioEngine 来说更容易使用。它的 API 非常简单。以下代码可以用来播放、停止、暂停和恢复背景音乐。

// play the background music 
int id = AudioEngine::play2d("sample_bgm.mp3"); 
// set continuously play
AudioEngine::setLoop(id, true); 
// change the volume, the value is from 0.0 to 1.0 
AudioEngine::setVolume(id, 0.5f); 
// pause it
AudioEngine::pause(id); 
// resume it that was pausing 
AudioEngine::resume(id); 
// stop it
AudioEngine::stop(id); 
// seek it by specifying the time 
AudioEngine::setCurrentTime(int id, 12.3f); 
// set the callback when it finished playing it
AudioEngine::setFinishCallback(int id, [](int audioId, std::string filePath){ 
    // this is the process when the background music was finished.
});

它是如何工作的...

AudioEngine 不再需要预加载方法。进一步来说,AudioEngine 不区分背景音乐和音效。你可以使用相同的方法播放两者。当你播放时,你可以得到一个作为返回值的音效 ID。当你改变音量、停止、暂停等操作时,你必须指定这个音效 ID。

还有更多...

如果你想要从内存中卸载音频文件,你可以通过使用 AudioEngine::uncache 方法或 AudioEngine::uncacheAll 方法来 uncache。在使用 uncache 方法的情况下,你必须指定你想要卸载的路径。在使用 uncacheAll 方法的情况下,所有音频数据都会从内存中卸载。在卸载文件时,你必须停止相关的音乐和音效。

播放电影

你可能想在游戏中播放电影以丰富表现。Cocos2d-x 提供了一个 VideoPlayer 类来实现这个目的。这个类使得播放电影变得简单;然而,它仍然是一个 experimental 类。所以,在使用它时你必须非常小心。

准备工作

在使用 VideoPlayer 类之前,你必须做一些准备工作。

  1. 你必须将电影文件添加到 Resources/res 文件夹中。在这个例子中,我们添加了名为 splash.mp4 的视频。

  2. 接下来,你必须包含一个头文件。执行此操作的代码如下:

    #include "ui/CocosGUI.h"
    USING_NS_CC;
    using namespace experimental::ui;
    
  3. 然后,你必须向 proj.android/jni/Android.mk 文件添加以下代码以构建 Android 应用程序。

    LOCAL_WHOLE_STATIC_LIBRARIES += cocos_ui_static
    $(call import-module,ui)
    
  4. 在 Xcode 中,你必须为 iOS 添加 MediaPlayer.framework,如下面的图片所示:

准备工作

如何操作...

让我们尝试在你的游戏中播放视频。这里就是:

auto visibleSize = Director::getInstance()->getVisibleSize(); auto videoPlayer = VideoPlayer::create(); 

videoPlayer->setContentSize(visibleSize); 
videoPlayer->setPosition(visibleSize/2); 
videoPlayer->setKeepAspectRatioEnabled(true); 
this->addChild(videoPlayer);

videoPlayer->addEventListener([](Ref *sender, 
VideoPlayer::EventType eventType) { 
    switch (eventType) { 
        case VideoPlayer::EventType::PLAYING: 
            CCLOG("PLAYING");
            break;
        case VideoPlayer::EventType::PAUSED: 
            CCLOG("PAUSED"); 
            break;
        case VideoPlayer::EventType::STOPPED: 
            CCLOG("STOPPED"); 
            break;
        case VideoPlayer::EventType::COMPLETED: 
            CCLOG("COMPLETED"); 
            break; 
        default:
            break; 
    }
});

videoPlayer->setFileName("res/splash.mp4"); 
videoPlayer->play();

它是如何工作的...

基本上,VideoPlayer 类与其他节点相同。首先,你创建一个实例,指定其位置,然后将其添加到图层上。接下来,通过使用 setContentSize 方法设置内容大小。如果你通过使用 setKeepAspectRatioEnabled 方法设置一个假值,视频播放器的尺寸将等于你通过 setContentSize 方法指定的内容大小。相反,如果你设置一个真值,视频播放器将保留电影的宽高比。

你可以通过添加事件监听器来获取播放状态的事件。VideoPlayer::EventType 有四种类型的事件,分别是 PLAYINGPAUSEDSTOPPEDCOMPLETED

最后,你通过使用 setFileName 方法设置电影文件,并通过使用 play 方法播放它。

小贴士

视频格式有很多。然而,你可以在 iOS 和 Android 上播放的视频格式是 mp4。这就是为什么你应该使用 mp4 格式在你的游戏中播放视频。

第七章. 与资源文件一起工作

游戏有很多资源,如图片和音频文件。Cocos2d-x 有一个资源管理系统。本章将涵盖以下主题:

  • 选择资源文件

  • 管理资源文件

  • 使用 SQLite

  • 使用 .xml 文件

  • 使用 .plist 文件

  • 使用 .json 文件

选择资源文件

你的游戏有每个分辨率的图片以支持多分辨率适配。如果你决定为每个分辨率找到一个图片,你的应用程序逻辑将非常复杂。Cocos2d-x 有一个搜索路径机制来解决这个问题。在这个菜谱中,我们将解释这个搜索路径机制。

准备工作

如果你想在不同的分辨率之间共享一些资源,那么你可以将所有共享资源放在 Resources 文件夹中,并将指定分辨率的资源放在不同的文件夹中,如下面的图片所示。

准备工作

CloseNormal.pngCloseSelected.png 是不同分辨率之间的共享资源。然而,HelloWorld.png 是指定分辨率的资源。

如何操作...

你可以按照以下方式设置 Cocos2d-x 搜索资源的优先级:

std::vector<std::string> searchPaths; 
searchPaths.push_back("ipad"); 
FileUtils::setSearchPaths(searchPaths); 
Sprite *sprite = Sprite::create("HelloWorld.png");
Sprite *close  = Sprite::create("CloseNormal.png");

它是如何工作的...

Cocos2d-x 将在 Resources/ipad 中找到 HelloWorld.png。Cocos2d-x 将使用此路径中的 HelloWorld.png;这就是为什么它可以在 Resources/ipad 中找到这个资源。然而,Cocos2d-x 不能在 Resources/ipad 中找到 CloseNormal.png。它将找到下一个顺序路径的 Resources 文件夹。系统可以在 Resources 文件夹中找到它并使用它。

你应该在创建第一个场景之前,在 AppDelegate::applicationDidFinishLaunching 方法中添加此代码。然后,第一个场景就可以使用这个搜索路径设置了。

参见

  • 下一个菜谱中称为 管理资源文件 的搜索路径机制。

管理资源文件

Cocos2d-x 有一个管理资源的扩展,它被称为 AssetsManagerExtension。这个扩展是为了资源如图片和音频文件的热更新而设计的。你可以通过这个扩展更新游戏中的资源新版本,而无需更新你的应用程序。

准备工作

在使用 AssetsManagerExtension 之前,你应该了解它。这个扩展有许多有用的功能来帮助你进行热更新。以下是一些这些功能:

  • 支持多线程下载

  • 两级进度支持——文件级和字节级进度

  • 支持压缩的 ZIP 文件

  • 恢复下载

  • 详细进度信息和错误信息

  • 重试失败资源的可能性

你必须准备一个网络服务器,因此,你的应用程序将下载资源。

如何操作...

你需要上传资源和清单文件。在这种情况下,我们将更新 HelloWorld.png 和一个名为 test.zip.zip 文件。这个 .zip 文件包含一些新的图片。AssetsManagerExtension 将根据清单文件下载资源。清单文件是 version.manifestproject.manifest

version.manifest 文件包含以下代码:

{
    "packageUrl" : "http://example.com/assets_manager/", 
    "remoteVersionUrl" : 
    "http://example.com/assets_manager/version.manifest", 
    "remoteManifestUrl" : 
    "http://example.com/assets_manager/project.manifest", 
    "version" : "1.0.1",}

project.manifest 文件包含以下代码:

{
    "packageUrl" : "http://example.com/assets_manager/", 
    "remoteVersionUrl" : "http://example.com/assets_manager/version.manifest", 
    "remoteManifestUrl" : "http://example.com/assets_manager/project.manifest", 
    "version" : "1.0.1",
    "assets" : {
        "HelloWorld.png" : {
            "md5" : "b7892dc221c840550847eaffa1c0b0aa" 
        }, 
        "test.zip" : {
            "md5" : "c7615739e7a9bcd1b66e0018aff07517", 
            "compressed" : true
        }
    }
}

然后,你必须上传这些清单文件和新资源。

接下来,你必须为热更新准备你的应用程序。你必须在你的项目中创建 local.manifest 文件。本地清单文件应包含以下代码:

{
    "packageUrl" : "http://example.com/assets_manager/", 
    "remoteVersionUrl" : 
"http://example.com/assets_manager/version.manifest", 
    "remoteManifestUrl" : 
"http://example.com/assets_manager/project.manifest", 
    "version" : "1.0.0", 
}

你应该在项目中创建一个管理 AssetsManagerExtension 的类。在这里,我们创建了一个名为 ResourceManager 的类。首先,你将创建 ResourceManager 的头文件。它被称为 ResourceManager.h。此文件包含以下代码:

#include "cocos2d.h"
#include "extensions/cocos-ext.h"

class ResourceManager {
private:
    ResourceManager();
    static ResourceManager* instance;

    cocos2d::extension::AssetsManagerEx* _am; 
    cocos2d::extension::EventListenerAssetsManagerEx* _amListener; 

public: 
    // custom event name 
    static const char* EVENT_PROGRESS; 
    static const char* EVENT_FINISHED; 

    virtual ~ResourceManager(); 
    static ResourceManager* getInstance(); 

    void updateAssets(std::string manifestPath); 
};

下一步是创建一个 ResourceManager.cpp 文件。此文件包含以下代码:

#include "ResourceManager.h"

USING_NS_CC;
USING_NS_CC_EXT;

// custom event name
const char* ResourceManager::EVENT_PROGRESS = "__cc_Resource_Event_Progress"; 
const char* ResourceManager::EVENT_FINISHED = "__cc_Resource_Event_Finished"; 

ResourceManager* ResourceManager::instance = nullptr; 

ResourceManager::~ResourceManager() {
    CC_SAFE_RELEASE_NULL(_am);
}

ResourceManager::ResourceManager()
:_am(nullptr)
,_amListener(nullptr)
{

}

ResourceManager* ResourceManager::getInstance() { 
    if (instance==nullptr) { 
        instance = new ResourceManager(); 
    } 
     return instance; 
}

void ResourceManager::updateAssets(std::string manifestPath) 
{
    std::string storagePath = FileUtils::getInstance()- 
    >getWritablePath(); 
    CCLOG("storage path = %s", storagePath.c_str()); 

    if (_am!=nullptr) { 
        CC_SAFE_RELEASE_NULL(_am); 
    } 
    _am = AssetsManagerEx::create(manifestPath, storagePath); 
    _am->retain(); 

    if (!_am->getLocalManifest()->isLoaded()) { 
        CCLOG("Fail to update assets, step skipped."); 
    } else { 
        _amListener = EventListenerAssetsManagerEx::create(_am, 
this{ 
            static int failCount = 0; 
            switch (event->getEventCode()) 
            {
                case 
EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST: 
                { 
                    CCLOG("No local manifest file found, skip 
                    assets update.");
                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::UPDATE_PROGRESSION: 
                { 
                    std::string assetId = event->getAssetId(); 
                    float percent = event->getPercent(); 
                    std::string str; 
                    if (assetId == AssetsManagerEx::VERSION_ID) { 
                        // progress for version file
                    } else if (assetId == 
AssetsManagerEx::MANIFEST_ID) {
                        // progress for manifest file 
                    } else { 
                        // dispatch progress event 
                        CCLOG("%.2f Percent", percent); 
                        auto event = 
EventCustom(ResourceManager::EVENT_PROGRESS); 
                        auto data = Value(percent); 
                        event.setUserData(&data); 
                        Director::getInstance()->getEventDispatcher()->dispatchEvent(&event); 
                    } 

                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::ERROR_DOWNLOAD_MANIFEST: 
                case 
EventAssetsManagerEx::EventCode::ERROR_PARSE_MANIFEST: 

                { 
                    CCLOG("Fail to download manifest file, update 
skipped."); 
                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::ALREADY_UP_TO_DATE: 
                case 
EventAssetsManagerEx::EventCode::UPDATE_FINISHED: 
                { 
                    CCLOG("Update finished. %s", 
                    event->getMessage().c_str()); 
                    CC_SAFE_RELEASE_NULL(_am); 
                    // dispatch finished updating event 
                    Director::getInstance()->getEventDispatcher()- 
>dispatchCustomEvent(ResourceManager::EVENT_FINISHED); 
                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::UPDATE_FAILED: 
                { 
                    CCLOG("Update failed. %s", event- 
>getMessage().c_str()); 

                    // retry 5 times, if error occurred 
                    failCount ++; 
                    if (failCount < 5) { 
                        _am->downloadFailedAssets(); 
                    } else { 
                        CCLOG("Reach maximum fail count, exit 
update process"); 
                        failCount = 0; 
                    } 
                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::ERROR_UPDATING: 
                { 
                    CCLOG("Asset %s : %s", event- 
>getAssetId().c_str(), event->getMessage().c_str()); 
                    break; 
                } 
                case 
EventAssetsManagerEx::EventCode::ERROR_DECOMPRESS: 
                { 
                    CCLOG("%s", event->getMessage().c_str()); 
                    break;
                } 
                default: 
                    break;
            } 
        }); 

        // execute updating resources 
        Director::getInstance()->getEventDispatcher()- 
>addEventListenerWithFixedPriority(_amListener, 1); 
        _am->update(); 
    } 
}

最后,要开始更新资源,请使用以下代码:

// label for progress 
auto size = Director::getInstance()->getWinSize(); 
TTFConfig config("fonts/arial.ttf", 30); 
_progress = Label::createWithTTF(config, "0%", 
TextHAlignment::CENTER); 
_progress->setPosition( Vec2(size.width/2, 50) ); 
this->addChild(_progress); 

// progress event 
getEventDispatcher()- 
>addCustomEventListener(ResourceManager::EVENT_PROGRESS, 
this{ 
    auto data = (Value*)event->getUserData(); 
    float percent = data->asFloat(); 
    std::string str = StringUtils::format("%.2f", percent) + "%"; 
    CCLOG("%.2f Percent", percent); 
    if (this->_progress != nullptr) { 
        this->_progress->setString(str); 
    }
}); 

// fnished updating event 
getEventDispatcher()- 
>addCustomEventListener(ResourceManager::EVENT_FINISHED, 
this{ 
    // clear cache 
    Director::getInstance()->getTextureCache()- 
>removeAllTextures(); 
    // reload scene 
    auto scene = HelloWorld::createScene(); 
    Director::getInstance()->replaceScene(scene); 
});

// update resources 
ResourceManager::getInstance()- 
>updateAssets("res/local.manifest");

它是如何工作的...

首先,我们将解释清单文件和 AssetsManagerExtension 的机制。清单文件是 JSON 格式。本地清单和版本清单包含以下数据:

描述
packageUrl 资源管理器将尝试请求和下载所有资产的 URL。
remoteVersionUrl 允许检查远程版本的远程版本清单文件 URL,以确定是否已将新版本上传到服务器。
remoteManifestUrl 包含所有资产信息的远程清单文件 URL。
version 此清单文件的版本。

此外,远程清单中在名为 assets 的键中还有以下数据。

描述
key 每个键代表资产的相对路径。
Md5 md5 字段表示资产的版本信息。
compressed 当压缩字段为 true 时,下载的文件将自动解压;此键是可选的。

AssetsManagerExtension 将按照以下步骤执行热更新:

  1. 在应用程序中读取本地清单。

  2. 根据本地清单中的远程版本 URL 下载版本清单。

  3. 将本地清单中的版本与版本清单中的版本进行比较。

  4. 如果两个版本不匹配,AssetsManagerExtension 将根据本地清单中的远程清单 URL 下载项目清单。

  5. 将远程清单中的 md5 值与应用程序中资产的 md5 进行比较。

  6. 如果两个 md5 值不匹配,AssetsManagerExtension 将下载此资产。

  7. 下次,AssetsManagerExtension 将使用下载的版本清单而不是本地清单。

接下来,我们将解释 ResourceManager 类。你可以按照以下方式执行热更新:

ResourceManager::getInstance()->updateAssets("res/local.manifest");

您应该通过指定本地清单的路径来调用 ResourceManager::updateAssets 方法。ResourceManager::updateAssets 将通过指定本地清单的路径和应用程序中存储的路径来创建一个 AssetsManagerEx 的实例,这是 AssetsManagerExtension 类的名称。

它将创建一个 EventListenerAssetsManagerEx 的实例以监听热更新的进度。

如果压缩值为真,AssetsManagerExtension 将在下载后解压它。

您可以通过调用 AssetsManagerEx::update 方法来更新资产。在更新过程中,您可以获取以下事件:

事件 描述
ERROR_NO_LOCAL_MANIFEST 无法找到本地清单。
UPDATE_PROGRESSION 获取更新的进度。
ERROR_DOWNLOAD_MANIFEST 下载清单文件失败。
ERROR_PARSE_MANIFEST 解析清单文件时出错。
ALREADY_UP_TO_DATE 已在更新资产(本地清单中的版本和版本清单中的版本相等)。
UPDATE_FINISHED 资产更新完成。
UPDATE_FAILED 更新资产时发生错误。在这种情况下,错误的原因可能是连接。您应该尝试再次更新。
ERROR_UPDATING 更新失败。
ERROR_DECOMPRESS 解压时发生错误。

ResourceManager 捕获到名为 UPDATE_PROGRESSION 的事件时,它会分发名为 EVENT_PROGRESS 的事件。如果您捕获到 EVENT_PROGRESS,您应该更新进度标签。

此外,如果它捕获到名为 UPDATE_FINISHED 的事件,它还会分发名为 EVENT_FINISHED 的事件。如果您捕获到 EVENT_FINISHED,您应该刷新所有纹理。这就是为什么我们要移除所有纹理缓存并重新加载场景。

// clear cache Director::getInstance()->getTextureCache()->removeAllTextures();
// reload sceneauto scene = HelloWorld::createScene(); 
Director::getInstance()->replaceScene(scene);

使用 SQLite

您可以通过使用游戏中的数据库轻松地保存和加载游戏数据。在智能手机应用程序中,通常使用名为 SQLite 的数据库。SQLite 易于使用。然而,在使用它之前,您必须设置一些事情。在本菜谱中,我们将解释如何在 Cocos2d-x 中设置和使用 SQLite。

准备就绪

Cocos2d-x 没有 SQLite 库。您必须将 SQLite 的源代码添加到 Cocos2d-x 中。

您需要从网站 sqlite.org/download.html 下载源代码。本书撰写时的最新版本是版本 3.8.10。您可以下载此版本的 .zip 文件并将其展开。然后,您可以将生成的文件添加到您的项目中,如下面的图像所示:

准备就绪

在本菜谱中,我们将创建一个名为 SQLiteManager 的原始类。因此,您必须将 SQLiteManager.hSQLiteManager.cpp 文件添加到您的项目中。

然后,如果您为 Android 构建,您必须按照以下方式编辑 proj.android/jni/Android.mk

LOCAL_SRC_FILES := hellocpp/main.cpp \ 
                   ../../Classes/AppDelegate.cpp \ 
                   ../../Classes/HelloWorldScene.cpp \ 
                   ../../Classes/SQLiteManager.cpp \ 
                   ../../Classes/sqlite/sqlite3.c

如何做到这一点...

首先,您必须按照以下方式编辑 SQLiteManager.h 文件:

#include "cocos2d.h" #include "sqlite/sqlite3.h"

class SQLiteManager {
private:
    SQLiteManager();
    static SQLiteManager* instance;
    sqlite3 *_db;
    bool open();
    void close();
public:
    virtual ~SQLiteManager();
    static SQLiteManager* getInstance();
    void insert(std::string key, std::string value);
    std::string select(std::string key);
};

接下来,你必须编辑SQLiteManager.cpp文件。这段代码有点长。所以,我们将一步一步地解释它。

  1. 为单例类添加以下代码:

    SQLiteManager* SQLiteManager::instance = nullptr; 
    SQLiteManager::~SQLiteManager() { 
    }
    
    SQLiteManager::SQLiteManager()
    {
        if (this->open()) {
            sqlite3_stmt* stmt;
            // create table 
            std::string sql = "CREATE TABLE IF NOT EXISTS 
    data(key TEXT PRIMARY KEY,value TEXT);"; 
            if (sqlite3_prepare_v2(_db, sql.c_str(), -1, &stmt, 
    nullptr) == SQLITE_OK) { 
                if (sqlite3_step(stmt)!=SQLITE_DONE) { 
                    CCLOG("Error in CREATE TABLE"); 
                }
      } 
            sqlite3_reset(stmt); 
            sqlite3_finalize(stmt); 
            this->close(); 
       } 
    }
    
    SQLiteManager* SQLiteManager::getInstance() { 
        if (instance==nullptr) { 
            instance = new SQLiteManager(); 
        }
        return instance;
    }
    
  2. 添加打开和关闭数据库的方法:

    bool SQLiteManager::open() 
    {
        std::string path = FileUtils::getInstance()- 
    >getWritablePath()+"test.sqlite"; 
        return sqlite3_open(path.c_str(), &_db)==SQLITE_OK; 
    }
    
    void SQLiteManager::close() 
    { 
        sqlite3_close(_db); 
    }
    
  3. 添加向数据库插入数据的方法:

    void SQLiteManager::insert(std::string key, std::string value)
    {
        this->open();
        // insert data
        sqlite3_stmt* stmt;
        std::string sql = "INSERT INTO data (key, value) VALUES(?, ?)";
        if (sqlite3_prepare_v2(_db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
            sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT);
            if (sqlite3_step(stmt)!=SQLITE_DONE) { 
                CCLOG("Error in INSERT 1, %s", 
                sqlite3_errmsg(_db)); 
            }
        }
        sqlite3_reset(stmt);
        sqlite3_finalize(stmt);
        this->close();
    }
    
  4. 添加从数据库选择数据的方法:

    std::string SQLiteManager::select(std::string key){
        this->open(); 
    
        // select data 
        std::string value; 
        sqlite3_stmt* stmt; 
        std::string sql = "SELECT VALUE from data where key=?"; 
        if (sqlite3_prepare_v2(_db, sql.c_str(), -1, &stmt, 
        NULL) == SQLITE_OK) { 
            sqlite3_bind_text(stmt, 1, key.c_str(), -1, 
            SQLITE_TRANSIENT); 
            if (sqlite3_step(stmt) == SQLITE_ROW) { 
                const unsigned char* val = 
                sqlite3_column_text(stmt, 0); 
                value = std::string((char*)val); 
                CCLOG("key=%s, value=%s", key.c_str(), val); 
            } else {
                CCLOG("Error in SELECT, %s", 
                sqlite3_errmsg(_db));
            }
        } else {
            CCLOG("Error in SELECT, %s", sqlite3_errmsg(_db)); 
        } 
        sqlite3_reset(stmt); 
        sqlite3_finalize(stmt); 
        this->close(); 
        return value; 
    }
    
  5. 最后,这是如何使用这个类的方法。要插入数据,请使用以下代码:

    SQLiteManager::getInstance()->insert("foo", "value1");
    

    要选择数据,请使用以下代码:

    std::string value = SQLiteManager::getInstance()- 
    >select("foo");
    

工作原理...

首先,在SQLiteManager类的构造方法中,如果该类不存在,则创建一个名为 data 的表。数据表按以下 SQL 创建:

CREATE TABLE IF NOT EXISTS data(key TEXT PRIMARY KEY,value TEXT);

为了使用 SQLite,你必须包含sqlite3.h并使用 sqlite3 API。这个 API 是用 C 语言编写的。如果你想学习它,你应该查看网站sqlite.org/cintro.html

我们在应用程序的沙盒区域创建了名为test.sqlite的数据库。如果你想更改位置或名称,你应该编辑open方法。

std::string path = FileUtils::getInstance()->getWritablePath()+"test.sqlite";

你可以通过使用insert方法指定键和值来插入数据。

SQLiteManager::getInstance()->insert("foo", "value1");

此外,你可以通过使用select方法指定键来选择值。

std::string value = SQLiteManager::getInstance()->select("foo");

还有更多...

在本教程中,我们创建了insert方法和select方法。然而,你也可以执行其他 SQL 方法,如deletereplace。此外,你可以使数据库与你的游戏匹配。因此,你可能需要为此类编辑代码。

使用.xml 文件

XML 通常用作 API 的返回值。Cocos2d-x 拥有 TinyXML2 库,可以解析 XML 文件。在本教程中,我们将解释如何使用这个库来解析 XML 文件。

准备工作

首先,你需要创建一个 XML 文件,并将其保存为test.xml,位于项目中的Resources/res文件夹。在这种情况下,我们将使用以下代码:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <key>key text</key>
    <array>
        <name>foo</name>
        <name>bar</name>
        <name>hoge</name>
    </array>
</root>

要使用 TinyXML-2 库,你必须包含它并使用命名空间如下:

#include "tinyxml2/tinyxml2.h" 
using namespace tinyxml2;

如何操作...

你可以使用 TinyXML2 库来解析 XML 文件。在以下代码中,我们解析test.xml并记录其中的每个元素。

std::string path = util->fullPathForFilename("res/test.xml"); 
XMLDocument *doc = new XMLDocument();
XMLError error = doc->LoadFile(path.c_str());
if (error == 0) { 
    XMLElement *root = doc->RootElement(); 
    XMLElement *key = root->FirstChildElement("key"); 
    if (key) { 
        CCLOG("key element = %s", key->GetText()); 
    }
    XMLElement *array = key->NextSiblingElement();
    XMLElement *child = array->FirstChildElement();
    while ( child ) {
        CCLOG("child element= %s", child->GetText());
        child = child->NextSiblingElement();
    }
    delete doc;
}

这个结果是以下日志:

key element = key text
child element= foo
child element= bar
child element= hoge

工作原理...

首先,你必须创建XMLDocument的一个实例,然后使用XMLDocument::LoadFile方法解析.xml文件。要获取根元素,你必须使用XMLDocument::RootElement方法。基本上,你可以使用FirstChildElement方法获取元素。如果它是连续的元素,你可以使用NextSiblingElement方法获取下一个元素。如果没有更多元素,NextSiblingElement的返回值将是 null。

最后,你不应该忘记删除XMLDocument的实例。这就是为什么你使用 new 操作创建它的原因。

使用.plist 文件

在 OS X 和 iOS 中使用的 PLIST 是一个属性列表。文件扩展名是.plist,但实际上,PLIST 格式是一个 XML 格式。我们经常使用.plist文件来存储游戏设置等。Cocos2d-x 有一个类,通过它可以轻松地使用.plist文件。

准备工作

首先,您需要创建一个.plist文件,并将其保存为test.plist到您项目中的Resources/res文件夹。在这种情况下,它有两个键,即foobarfoo键有一个整数值1bar键有一个字符串值This is string。请参考以下代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>foo</key>
  <integer>1</integer>
  <key>bar</key>
  <string>This is string</string>
</dict>
</plist>

如何操作...

您可以使用FileUtils::getValueMapFromFile方法来解析.plist文件。在以下代码中,我们解析test.plist并记录其中的键值。

FileUtils* util = FileUtils::getInstance();
std::string path = util->fullPathForFilename("res/test.plist");
ValueMap map = util->getValueMapFromFile(path); 
for (auto element : map) { 
    std::string key = element.first; 
    Value value = element.second; 
    switch (value.getType()) { 
        case Value::Type::BOOLEAN: 
            CCLOG("%s, %s", key.c_str(), 
            value.asBool()?"true":"false");
            break; 
        case Value::Type::INTEGER: 
            CCLOG("%s, %d", key.c_str(), value.asInt()); 
            break; 
        case Value::Type::FLOAT: 
            CCLOG("%s, %f", key.c_str(), value.asFloat()); 
            break; 
        case Value::Type::DOUBLE: 
            CCLOG("%s, %f", key.c_str(), value.asDouble()); 
            break; 
        case Value::Type::STRING: 
            CCLOG("%s, %s", key.c_str(), 
            value.asString().c_str()); 
            break;        default: 
            break; 
    }
}

它是如何工作的...

您可以通过将.plist文件的路径指定给FileUtils::getValueMapFromFile方法来解析.plist文件。这样做之后,您将得到.plist文件中的数据作为ValueMap值。ValueMap类是一个基于std::unordered_map的包装类。PLIST 的数据容器是ArrayDictionary。解析.plist文件后,Arraystd::vector<Value>,而Dictionarystd::unordered_map<std::string, Value>。此外,您可以使用Value::getType方法来区分值的类型。然后,您可以使用Value::asIntasFloatasDoubleasBoolasString方法来获取值。

更多...

您可以从ValueMap保存.plist文件。这样做,您可以将游戏数据保存到.plist文件中。要保存.plist文件,请使用以下代码:

ValueMap map; for (int i=0; i<10; i++) { 
    std::string key = StringUtils::format("key_%d", i); 
    Value val = Value(i); 
    map.insert(std::make_pair(key, val));
}
std::string fullpath = util->getWritablePath() + "/test.xml"; 
FileUtils::getInstance()->writeToFile(map, fullpath);

首先,您需要在ValueMap中设置键值。在这种情况下,值都是整数类型,但您也可以设置混合类型的值。最后,您需要使用FileUtils::writeToFile方法将文件保存为.plist文件。

使用.json 文件

我们可以将 JSON 格式像 XML 格式一样用于保存/加载游戏相关数据。JSON 比 XML 格式简单。它比 XML 文件格式表示相同数据所需的空间更少。此外,今天,它被用作 Web API 的值。Cocos2d-x 有一个名为RapidJSON的 JSON 解析库。在这个菜谱中,我们将解释如何使用 RapidJSON。

准备工作

RapidJSON 通常包含在 Cocos2d-x 中。然而,您需要按照以下方式包含头文件:

#include "json/rapidjson.h"
#include "json/document.h"

如何操作...

首先,我们将按照以下方式解析一个 JSON 字符串:

std::string str = "{\"hello\" : \"word\"}";

您可以使用rapidjson::Document来解析 JSON,如下所示:

rapidjson::Document d;
d.Parse<0>(str.c_str());
if (d.HasParseError()) { 
    CCLOG("GetParseError %s\n",d.GetParseError()); 
} else if (d.IsObject() && d.HasMember("hello")) { 
    CCLOG("%s\n", d["hello"].GetString()); 
}

它是如何工作的...

您可以通过使用Document::Parse方法并指定 JSON 字符串来解析 JSON。当您使用Document::HasParseError方法时可能会得到解析错误;您可以通过使用Document::GetParseError方法来获取这个错误的描述。此外,您可以通过指定哈希键并使用Document::GetString方法来获取一个元素。

更多...

在实际应用中,你可以从一个文件中获取一个 JSON 字符串。现在我们将解释如何从文件中获取这个字符串。首先,你需要在项目的 Resources/res 文件夹中添加一个名为 test.json 的文件,并按照以下方式保存:

[{"name":"Tanaka","age":25}, {"name":"Ichiro","age":40}]

接下来,你可以按照以下方式从一个文件中获取一个 JSON 字符串:

std::string jsonData = FileUtils::getInstance()- 
>getStringFromFile("res/test.json"); 
CCLOG("%s\n", jsonData.c_str()); 
rapidjson::Document d; 
d.Parse<0>(jsonData.c_str()); 
if (d.HasParseError()) { 
    CCLOG("GetParseError %s\n",d.GetParseError()); 
} else { 
    if (d.IsArray()) { 
        for (rapidjson::SizeType i = 0; i < d.Size(); ++i) { 
            auto name = d[i]["name"].GetString(); 
            auto age = d[i]["age"].GetInt(); 
            CCLOG("name-%s, age=%d", name, age); 
        }
    }
}

你可以通过使用 FileUtils::getStringFromFile 方法从文件中获取字符串。之后,你可以以相同的方式解析。此外,这个 JSON 字符串可能是一个数组。你可以使用 Document::IsArray 方法检查格式是否为数组。然后,你可以使用 for 循环遍历数组中的 JSON 对象。

第八章. 与硬件协同工作

本章将涵盖以下主题:

  • 使用原生代码

  • 使用*台更改处理

  • 使用加速度传感器

  • 保持屏幕开启

  • 获取 dpi

  • 获取最大纹理大小

简介

Cocos2d-x 有很多 API。然而,我们没有需要的 API,例如,内购、推送通知等。在这种情况下,我们必须创建原始 API,并且需要为 iOS 编写 Objective-C 代码或为 Android 编写 Java 代码。此外,我们希望获取正在运行的设备信息。当我们想要为每个设备进行调整时,我们必须获取设备信息,例如运行的应用程序版本、设备名称、设备上的 dpi 等。然而,这样做非常困难且令人困惑。在本章中,你可以为 iOS 或 Android 编写原生代码并获取设备信息。

使用原生代码

在 Cocos2d-x 中,你可以为跨*台编写一个源文件。然而,你必须为依赖过程(如购买或推送通知)编写 Objective-C 函数或 Java 函数。如果你想从 C++ 调用 Java(Android),你必须使用 JNIJava 原生接口)。特别是,JNI 非常复杂。要从 C++ 调用 Java,你必须使用 JNI。在本食谱中,我们将解释如何从 C++ 调用 Objective-C 函数或 Java 函数。

准备就绪

在这种情况下,我们将创建一个新的类,名为 Platform。你可以通过这个类来获取应用程序的版本。在编写代码之前,你需要在你的项目中创建三个文件,分别命名为 Platform.hPlatform.mmPlatform.cpp

准备就绪

在 Xcode 中,你很重要的一点是不要将 Platform.cpp 添加到 编译源文件 中。这就是为什么 Platform.cpp 是为 Android 目标而设计的,并且不需要为 iOS 构建的原因。如果你将其添加到 编译源文件 中,你必须从那里将其删除。

准备就绪

如何做到这一点...

  1. 首先,你必须使用以下代码创建一个头文件,名为 Platform.h

    class Platform
    {
    public:
        static const char* getAppVersion();
    };
    
  2. 你必须为 iOS 创建一个名为 Platform.mm 的执行文件。此代码是用 Objective-C 编写的。

    #include "Platform.h"
    
    const char* Platform::getAppVersion()
    {
        NSDictionary* info = [[NSBundle mainBundle] 
        infoDictionary]; 
        NSString* version = [info 
        objectForKey:(NSString*)kCFBundleVersionKey]; 
        if (version) { 
            return [version UTF8String]; 
        }
        return nullptr;
    }
    
  3. 你必须为 Android 创建一个名为 Platform.cpp 的执行文件。以下代码是用 C++ 编写的,并通过 JNI 使用 Java:

    #include "Platform.h"
    #include "platform/android/jni/JniHelper.h"
    #define CLASS_NAME "org/cocos2dx/cpp/AppActivity"
    
    USING_NS_CC;
    
    const char* Platform::getAppVersion()
    {
        JniMethodInfo t;
        const char* ret = NULL;
        if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, 
        "getAppVersionInJava", "()Ljava/lang/String;")) {
            jstring jstr = (jstring)t.env- 
    >CallStaticObjectMethod(t.classID,t.methodID); 
            std::string sstr = JniHelper::jstring2string(jstr); 
            t.env->DeleteLocalRef(t.classID); 
            t.env->DeleteLocalRef(jstr); 
            ret = sstr.c_str(); 
        }
        return ret;
    }
    
  4. 当你在项目中添加新的类文件时,你必须编辑 proj.android/jni/Android.mk 以构建 Android。

    LOCAL_SRC_FILES := hellocpp/main.cpp \ 
                       ../../Classes/AppDelegate.cpp \ 
                       ../../Classes/HelloWorldScene.cpp \
                       ../../Classes/Platform.cpp
    
  5. 接下来,你必须编写 AppActivity.java 中的 Java 代码。此文件名为 pro.android/src/org/cocos2dx/cpp/AppActivity.java

    public class AppActivity extends Cocos2dxActivity { 
        public static String appVersion = "";
    
        @Override 
        protected void onCreate(Bundle savedInstanceState) { 
            super.onCreate(savedInstanceState); 
    
            try { 
                PackageInfo packageInfo = 
    getPackageManager().getPackageInfo(getPackageName(), 
    PackageManager.GET_META_DATA); 
                appVersion = packageInfo.versionName; 
            } catch (NameNotFoundException e) { 
            }
        }
    
        public static String getAppVersionInJava() { return appVersion; 
        }
    }
    
  6. 最后,你可以通过以下代码获取你游戏的版本:

    #include "Platform.h" 
    
    const char* version = Platform::getAppVersion(); 
    CCLOG("application version = %s", version);
    

它是如何工作的...

  1. 首先,我们将从 iOS 开始。你将能够通过在 Platform.mm 中使用 Objective-C 来获取你游戏的版本。你可以在 .mm 文件中编写 C++ 和 Objective-C。

  2. 接下来,我们将寻找 Android。当您在 Android 设备上调用 Platform::getAppversion 时,将执行 Platform.cpp 中的方法。在这个方法中,您可以通过 JNI 调用 AppActivity.java 中的 getAppVersionInJava 方法。C++ 可以通过 JNI 连接到 Java。这就是为什么您只能通过 Java 来获取应用程序版本的原因。

  3. 在 Java 中,您可以通过使用 onCreate 方法来获取您应用程序的版本。您可以将其设置为静态变量,然后从 Java 中的 getAppVersionInJava 方法中获取它。

还有更多...

您可以通过在 Cocos2d-x 中使用 JniHelper 类轻松地使用 JNI。这个类如何从 C++ 中管理错误并创建 C++ 和 Java 之间的桥梁已经解释过了。您可以通过以下代码使用 JniHelper 类:

JniMethodInfo t; 
JniHelper::getStaticMethodInfo(t, CLASS_NAME, 
"getAppVersionInJava", 
"()Ljava/lang/String;")

您可以使用 JniHelper::getStaticMethodInfo 来获取 Java 方法的信息。第一个参数是 JniMethodInfo 类型的变量。第二个参数是包含您要调用的方法的类的名称。第三个参数是方法名称。最后一个参数是此方法的参数。此参数由返回值和参数决定。括号中的字符是 Java 方法的参数。在这种情况下,此方法没有参数。括号后面的字符是返回值。Ljava/lang/String 表示返回值是一个字符串。如果您可以轻松地获取此参数,则应使用名为 javap 的命令。使用此命令将生成以下结果。

$ cd /path/to/project/pro.android/bin/classes 
$ javap -s org.cocos2dx.cpp.AppActivity 
Compiled from "AppActivity.java" 
public class org.cocos2dx.cpp.AppActivity extends 
org.cocos2dx.lib.Cocos2dxActivity { 
  public static java.lang.String appVersion; 
    descriptor: Ljava/lang/String; 
  public org.cocos2dx.cpp.AppActivity(); 
    descriptor: ()V 

  protected void onCreate(android.os.Bundle); 
    descriptor: (Landroid/os/Bundle;)V 

  public static java.lang.String getAppVersionInJava(); 
    descriptor: ()Ljava/lang/String; 

  static {}; 
    descriptor: ()V 
}

从上述结果中,您可以看到 getAppVersionInJava 方法的参数为 ()Ljava/lang/String;

如前所述,您可以将 Java 方法的信息作为 t 变量获取。因此,您可以通过这个变量和以下代码来调用 Java 方法:

jstring jstr = (jstring)t.env- >CallStaticObjectMethod(t.classID,t.methodID);

使用*台更改处理方式

您可以使程序在每种操作系统的源代码的特定部分运行。例如,您将根据*台更改文件名、文件路径或图像缩放。在这个菜谱中,我们将介绍在出现问题时根据所选*台进行分支代码的情况。

如何做到这一点...

您可以通过以下方式使用预处理器来更改处理方式:

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) 
    CCLOG("this platform is Android"); 
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) 
    CCLOG("this platform is iOS"); 
#else 
    CCLOG("this platfomr is others");
#endif

它是如何工作的...

Cocos2d-x 在 CCPlatformConfig.h 中定义了 CC_TARGET_PLATFORM 值。如果您的游戏是为 Android 设备编译的,则 CC_TARGET_PLATFORM 等于 CC_PLATFORM_ANDROID。如果它是为 iOS 设备编译的,则 CC_TARGET_PLATFORM 等于 CC_PLATFORM_IOS。不用说,除了 Android 和 iOS 之外,还有其他值。请检查 CCPlatformConfig.h

还有更多...

在预处理器中使用的代码在编辑器上难以阅读。此外,在编译您的代码之前,您可能无法注意到错误。您应该定义一个可以由预处理器更改的常量值,但您应该尽可能多地使用代码来更改处理方式。您可以使用 Cocos2d-x 中的 Application 类来检查*台,如下所示:

switch (Application::getInstance()->getTargetPlatform()) { 
        case Application::Platform::OS_ANDROID: 
            CCLOG("this device is Android"); 
            break;
        case Application::Platform::OS_IPHONE: 
            CCLOG("this device is iPhone"); 
            break;
        case Application::Platform::OS_IPAD: 
            CCLOG("this device is iPad"); 
            break;
        default: 
            break;
}

你可以使用Application::getTargetPlatform方法获取*台值。你将能够检查,不仅仅是 iPhone 或 iPad,还可以通过此方法检查 IOS。

使用加速度传感器

通过在设备上使用加速度传感器,我们可以通过使用摇动和倾斜设备等操作使游戏更加吸引人。例如,通过倾斜屏幕移动球,瞄准目标的迷宫游戏,以及试图减肥的瘦熊猫,在这些游戏中玩家需要摇动设备来玩游戏。你可以通过加速度传感器获取设备的倾斜值和移动速度。如果你能使用它,你的游戏就会更加独特。在这个菜谱中,我们学习如何使用加速度传感器。

如何操作...

你可以通过以下代码从加速度传感器获取 x、y 和 z 轴的值:

Device::setAccelerometerEnabled(true);
auto listener = EventListenerAcceleration::create([](Acceleration* 
acc, Event* event){ 
    CCLOG("x=%f, y=%f, z=%f", acc->x, acc->y, acc->z); 
}); 
this->getEventDispatcher()- 
>addEventListenerWithSceneGraphPriority(listener, this);

它是如何工作的...

  1. 首先,你通过使用Device::setAccelerometerEnable方法启用加速度传感器。Device类中的方法是静态方法。因此,你可以直接调用方法,而不需要实例,如下所示:

    Device::setAccelerometerEnable(true);
    
  2. 你为获取加速度传感器的值设置事件监听器。在这种情况下,你可以通过 lambda 函数获取这些值。

  3. 最后,你在事件分发器中设置事件监听器。

  4. 如果你在这台真实设备上运行此代码,你可以从加速度传感器获取 x、y 和 z 轴的值。x 轴是斜坡的左右。y 轴是斜坡的前后。z 轴是垂直运动。

还有更多...

加速度传感器会消耗更多的电量。当你使用它时,你为事件发生设置一个适当的间隔。以下代码将间隔设置为 1 秒。

Device::setAccelerometerInterval(1.0f);

小贴士

如果间隔较高,我们可能会错过一些倾斜输入。然而,如果我们使用较低的间隔,我们会消耗大量的电量。

保持屏幕开启

在玩游戏时,你必须确保设备不会进入睡眠模式。例如,在你的游戏中,玩家可以通过使用加速度传感器来控制游戏并保持游戏进行。问题是,如果玩家在用加速度传感器玩游戏时不触摸屏幕,设备就会进入睡眠模式并进入后台模式。在这个菜谱中,你可以轻松地保持屏幕开启。

如何操作...

如果你使用Device::setKeepScreenOn方法将其设置为true,你可以保持屏幕开启:

Device::setKeepScreenOn(true);

它是如何工作的...

每个*台都有不同的方法来防止设备进入睡眠模式。然而,Cocos2d-x 可以为每个*台做到这一点。你可以使用这种方法而不需要执行*台。在 iOS *台上,setKeepScreenOn方法如下:

void Device::setKeepScreenOn(bool value) 
{
    [[UIApplication sharedApplication] 
setIdleTimerDisabled:(BOOL)value]; 
}

在 Android *台上,方法如下:

public void setKeepScreenOn(boolean value) { 
    final boolean newValue = value; 
    runOnUiThread(new Runnable() { 
        @Override
        public void run() { 
            mGLSurfaceView.setKeepScreenOn(newValue); 
        }
    });
}

获取 dpi

每个设备都有许多dpi(每英寸点数)的变化。您可以通过分辨率准备几种不同类型的图像。您可能想要根据设备上的 dpi 更改图像。在这个菜谱中,如果您想获取游戏正在运行的 dpi,您需要使用 Cocos2d-x 函数。

如何操作...

您可以通过使用Device::getDPI方法来获取设备正在执行游戏时的 dpi(每英寸点数),如下所示:

int dpi = Device::getDPI(); 
CCLOG("dpi = %d", dpi);

工作原理...

实际上,我们检查了一些设备的 dpi。为了使用 dpi 信息,您可以进一步调整多屏幕分辨率。

设备 Dpi
iPhone 6 Plus 489
iPhone 6 326
iPhone 5s 326
iPhone 4s 326
iPad Air 264
iPad 2 132
Nexus 5 480

获取最大纹理大小

可使用的最大纹理大小因设备而异。特别是,当您使用纹理图集时,应该小心。这就是为什么包含大量图像的纹理图集体积会变得很大。您不能使用超过最大尺寸的纹理。如果您使用它,您的游戏将会崩溃。在这个菜谱中,您可以获取最大纹理大小。

如何操作...

您可以通过以下代码轻松获取最大纹理大小:

auto config = Configuration::getInstance();
int texutureSize = config->getMaxTextureSize();
CCLOG("max texture size = %d", texutureSize);

工作原理...

Configuration类是一个单例类。这个类有一些 OpenGL 变量。OpenGL 是一个用于渲染 2D 和 3D 矢量图形的多*台 API。它使用起来相当困难。Cocos2d-x 将其封装起来,使其易于使用。OpenGL 有很多关于图形的信息。最大纹理大小是提供这些信息的一个变量。您可以得到您应用程序正在运行的设备的最大纹理大小。

还有更多...

您可以获取其他 OpenGL 变量。如果您想检查Configuration拥有的变量,您将使用Configuration::getInfo方法。

auto config = Configuration::getInstance(); 
std::string info = config->getInfo(); 
CCLOG("%s", info.c_str());

iPhone 6 Plus 上的日志结果:

{
  gl.supports_vertex_array_object: true  cocos2d.x.version: 
  cocos2d-x 3.5 
  gl.vendor: Apple Inc. 
  gl.supports_PVRTC: true 
  gl.renderer: Apple A8 GPU 
  cocos2d.x.compiled_with_profiler: false 
  gl.max_texture_size: 4096 
  gl.supports_ETC1: false 
  gl.supports_BGRA8888: false 
  cocos2d.x.build_type: RELEASE 
  gl.supports_discard_framebuffer: true 
  gl.supports_NPOT: true 
  gl.supports_ATITC: false 
  gl.max_samples_allowed: 4 
  gl.max_texture_units: 8 
  cocos2d.x.compiled_with_gl_state_cache: true 
  gl.supports_S3TC: false 
  gl.version: OpenGL ES 2.0 Apple A8 GPU - 53.13 
}

如果您获取每个变量,并检查Configuration类的源代码,您可以轻松理解它们。

第九章:控制物理

本章将涵盖以下主题:

  • 使用物理引擎

  • 检测碰撞

  • 使用关节

  • 使用加速度传感器改变重力

简介

物理对于游戏来说非常重要。玩家需要模拟现实世界的情况。你可以通过使用物理引擎来给你的游戏增加物理真实感。正如你所知,有两个著名的物理引擎:Box2D 和 Chipmunk。在 Cocos2d-x 2.x 版本中,你必须使用这些物理引擎。然而,使用它们相当困难。自从 Cocos2d-x 3.x 版本以来,Cocos2d-x 已经包含了一个封装在 Chipmunk 中的有用物理引擎。因此,物理引擎不再是我们的担忧,因为它可扩展且 CPU 友好。在本章中,你将学习如何在游戏中轻松使用物理引擎。

使用物理引擎

当你意识到你的游戏需要模拟现实世界的情况时,你应该怎么做?你知道答案是使用物理引擎。当你开始使用物理引擎时,你必须使用一些新的类和方法。在本菜谱中,你将学习如何在 Cocos2d-x 中使用基本的物理引擎。

如何做到这一点...

  1. 首先,你必须在场景中创建一个物理世界。你可以通过以下代码来创建它:

    Scene* HelloWorld::createScene()
    {
        auto scene = Scene::createWithPhysics();
        auto layer = HelloWorld::create();
        scene->addChild(layer);
    	return scene;
    }
    
  2. 接下来,你必须向物理世界中添加物理体。物理体是不可见的。它是一个物理形状,如正方形、圆形或更复杂的形状。在这里,让我们创建一个正方形形状。你必须创建它并将其设置为精灵以使其可见。

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
        Size visibleSize = Director::getInstance()->getVisibleSize();
        Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
        auto wall = Node::create();
        auto wallBody = PhysicsBody::createEdgeBox(visibleSize, 
    PhysicsMaterial(0.1f, 1.0f, 0.0f)); 
        wall->setPhysicsBody(wallBody);
        wall->setPosition(Vec2(visibleSize.width/2+origin.x, 
    VisibleSize.height/2+origin.y));
        addChild(wall);
    
        auto sprite = Sprite::create("CloseNormal.png");
        sprite->setPosition(visibleSize/2);
        auto physicsBody = PhysicsBody::createCircle(sprite-
    >getContentSize().width/2);
        physicsBody->setDynamic(true); 
        sprite->setPhysicsBody(physicsBody);
        this->addChild(sprite);
    
        return true;
    }
    
  3. 最后,你必须运行前面的代码。然后你可以看到精灵在地面上下落和弹跳。

它是如何工作的...

  1. 首先,你必须使用Scene::createWithPhysics方法在场景中创建物理世界。这样,你就可以在游戏中使用物理引擎。

  2. 接下来,你必须创建一个倒置的墙,从屏幕边缘的左侧到右侧。如果你移除这个墙并运行代码,精灵对象将永远下落。你可以通过使用PhysicsBody::createEdgeBox方法以及这个大小和材料设置来创建一个边缘框。在这种情况下,墙的大小将与屏幕相同。材料设置指定为PhysicsMaterial(0.1f, 1.0f, 0.0f)。这意味着密度是1.0f,恢复系数是1.0f,摩擦系数是0.0f。让我们尝试改变这个参数,并在给定的情况下检查它。

  3. 最后,你可以使用精灵创建物理体。在这种情况下,精灵是圆形的。因此,你需要使用PhysicsBody::createCircle方法来创建圆形物理体。然后,使用Sprite::setPhysicsBody方法将物理体添加到精灵中。

  4. Cocos2d-x 有很多物理体形状,如下表所示:

形状 描述
PhysicsShapeCircle 实心圆形形状
PhysicsShapePolygon 实心多边形形状
PhysicsShapeBox 实心矩形形状
PhysicsShapeEdgeSegment 段形
PhysicsShapeEdgePolygon 空心多边形形状
PhysicsShapeEdgeBox 空心矩形形状
PhysicsShapeEdgeChain 用于连接多个边缘

还有更多…

然后,你可以通过使用 Scene::getPhysicsWorld 方法来获取一个 PhysicsWorld 实例。在这个示例中,我们将 PhysicsWorld::DEBUGDRAW_ALL 设置为物理世界。这就是为什么你可以看到所有物理对象边缘都是红色线条。当你发布你的游戏时,你必须移除这个设置。

Scene* HelloWorld::createScene()
{
    auto scene = Scene::createWithPhysics();
    auto layer = HelloWorld::create();
    scene->addChild(layer);

    PhysicsWorld* world = scene->getPhysicsWorld();
    world->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);

    return scene;
}

还有更多…

此外,你可以将原始重力值设置为 PhysicsWorld。例如,你可以在设备倾斜时更改重力。以下是如何更改重力的代码:

PhysicsWorld* world = scene->getPhysicsWorld();
auto gravity = Vec2(0, 98.0f);
world->setGravity(gravity);

上述代码与地球重力相反。默认重力值是 Vec2(0, -98.0f)

检测碰撞

当物理对象之间发生碰撞时,你想要对物理体采取行动,例如显示爆炸和显示粒子。在这个示例中,你将学习如何检测物理世界中的碰撞。

如何实现...

  1. 首先,你必须在 init 方法中创建事件监听器,如下所示:

    auto contactListener = 
    EventListenerPhysicsContact::create();
    contactListener->onContactBegin = [](PhysicsContact& contact){
        CCLOG("contact begin");
        auto shapeA = contact.getShapeA();
        auto bodyA = shapeA->getBody();
    
        auto shapeB = contact.getShapeB();
        auto bodyB = shapeB->getBody();
        return true;
    };
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(contactListener, this);
    
  2. 接下来,你必须设置你想要检查碰撞的物理体的接触测试位掩码。在这个示例中,你设置了墙壁体和精灵体,如下所示:

    auto wallBody = PhysicsBody::createEdgeBox(visibleSize, PhysicsMaterial(0.1f, 1.0f, 0.0f)); 
    wallBody->setContactTestBitmask(1);
    
    auto physicsBody = PhysicsBody::createCircle(sprite->getContentSize().width/2);
    physicsBody->setContactTestBitmask(1);
    

它是如何工作的...

你可以通过使用 EventListenerPhysicsContact 类在物理世界中检测碰撞。它将接收物理世界中所有的接触回调。如果你在这个监听器中设置了 onContactBegin 方法,你可以捕获物理体的碰撞。你可以在 onContactBegin 方法中使用 getShapeAgetShapeBgetBody 方法从参数的 PhysicsContact 实例中获取两个物理形状,如下所示:

contactListener->onContactBegin = [](PhysicsContact& contact){
    CCLOG("contact begin");
    auto shapeA = contact.getShapeA();
    auto bodyA = shapeA->getBody();

    auto shapeB = contact.getShapeB();
    auto bodyB = shapeA->getBody();
    return true;
};

onContactBegin 方法返回 true 或 false。如果返回 true,两个物理体将发生碰撞。如果返回 false,则不会有碰撞响应。所以,你可以决定以任何方式检查两个体的碰撞类型。

setContactTestBitmask 方法有一个参数用于接触测试位掩码。这个掩码定义了哪些类型的物体与这个物理体发生交叉通知。当两个物体共享相同的空间时,每个物体的类别掩码通过执行逻辑与操作与另一个物体的接触掩码进行比较。如果任一比较结果不为零,则创建 PhysicsContact 对象并将其传递给物理世界的代理。为了获得最佳性能,只为需要的交互设置接触掩码中的位。位掩码是一个整数。默认值是 0x00000000(所有位清除)。

PhysicsContact 有一些其他事件,如下表所示:

事件 描述
onContactBegin 当两个形状开始接触时调用
onContactPreSolve 两个形状正在接触
onContactPostSolve 已处理两个形状的碰撞响应
onContactSeparate 当两个形状分离时调用

使用关节

关节用于将两个物理体连接在一起。然后,你可以创建一个复杂形状来连接一些形状。此外,你可以创建齿轮或电机等对象来使用关节。Cocos2d-x 有很多不同类型的关节。在这个菜谱中,我们解释了一种典型的关节类型。

准备工作

你将创建一个创建物理对象的方法。这就是为什么你必须创建多个物理对象的原因。这个方法被称为 makeSprite。你必须在 HelloWorld.h 中添加以下代码:

cocos2d::Sprite* makeSprite();

你必须在 HelloWorld.cpp 中添加以下代码:

Sprite* HelloWorld::makeSprite()
{
    auto sprite = Sprite::create("CloseNormal.png");
    auto physicsBody = PhysicsBody::createCircle(sprite->getContentSize().width/2);
    physicsBody->setDynamic(true);
    physicsBody->setContactTestBitmask(true);
    sprite->setPhysicsBody(physicsBody);
    return sprite;
}

如何做到...

在这个菜谱中,我们解释了 PhysicsJointGear。这个关节的作用是保持一对物体的角速度比。

  1. 首先,你必须在 HelloWorld.h 中添加以下代码:

    void onEnter();
    cocos2d::DrawNode* _drawNode;
    cocos2d::PhysicsWorld* _world;
    
  2. 其次,你必须在 HelloWorld.cpp 中添加 onEnter 方法,通过使用两个物理对象和 PhysicsJointGear 类来创建齿轮关节:

    void HelloWorld::onEnter()
    {
        Layer::onEnter();
    
        Size visibleSize = Director::getInstance()->getVisibleSize();
        Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
        _world = Director::getInstance()->getRunningScene()->getPhysicsWorld();
    
        // wall
        auto wall = Node::create();
        auto wallBody = PhysicsBody::createEdgeBox(visibleSize, PhysicsMaterial(0.1f, 1.0f, 0.0f));
        wallBody->setContactTestBitmask(true);
        wall->setPhysicsBody(wallBody);
        wall->setPosition(Vec2(visibleSize.width/2+origin.x, visibleSize.height/2+origin.y));
        addChild(wall);
    
        // gear object 1
        auto sp1 = this->makeSprite();
        sp1->setPosition(visibleSize/2);
        this->addChild(sp1);
        // gear object 2
       auto sp2 = this->makeSprite();
        sp2->setPosition(Vec2(visibleSize.width/2+2, visibleSize.height));
        this->addChild(sp2);
    
        // joint: gear
        auto body1 = sp1->getPhysicsBody();
        auto body2 = sp2->getPhysicsBody();
        auto pin1 = PhysicsJointPin::construct(body1, wallBody, sp1->getPosition());  
        _world->addJoint(pin1);
        auto pin2 = PhysicsJointPin::construct(body2, wallBody, sp2->getPosition());
        _world->addJoint(pin2);
        auto joint = PhysicsJointGear::construct(body1, body2, 0.0f, 2.0f);
        _world->addJoint(joint);
    }
    
  3. 接下来,你必须能够触摸物理对象。在 HellowWorld.h 中添加以下代码:

    bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
    void onTouchMoved(cocos2d::Touch* touch, cocos2d::Event* event);
    void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event);
    cocos2d::Node* _touchNode;
    

    然后,在 HelloWorld.cpp 中的 HelloWorld::onEnter 方法中添加以下代码:

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
    touchListener->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchMoved, this);
    touchListener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
    
  4. 接下来,你按照以下方式在三个触摸方法中编写执行代码:

    bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
    {
        auto location = touch->getLocation();
        auto shapes = _world->getShapes(location);
        if (shapes.size()<=0) {
            return false;
    	}
        PhysicsShape* shape = shapes.front();
        PhysicsBody* body = shape->getBody();
        if (body != nullptr) {
            _touchNode = Node::create();
            auto touchBody = PhysicsBody::create(PHYSICS_INFINITY, PHYSICS_INFINITY);
            _touchNode->setPhysicsBody(touchBody);
            _touchNode->getPhysicsBody()->setDynamic(false);
            _touchNode->setPosition(location);
            this->addChild(_touchNode);
            PhysicsJointPin* joint = PhysicsJointPin::construct(touchBody, body, location);
            joint->setMaxForce(5000.0f * body->getMass());
            _world->addJoint(joint);
            return true;
        }
        return false;
    }
    
    void HelloWorld::onTouchMoved(Touch* touch, Event* event)
    {
        if (_touchNode!=nullptr) {
            _touchNode->setPosition(touch->getLocation());
        }
    }
    
    void HelloWorld::onTouchEnded(Touch* touch, Event* event)
    {
        if (_touchNode!=nullptr) {
            _touchNode->removeFromParent();
            _touchNode = nullptr;
        }
    }
    
  5. 最后,你将通过触摸物理对象来运行和测试齿轮关节。

它是如何工作的...

  1. 首先,你必须将齿轮对象固定在墙上,因为如果它们没有被固定,齿轮对象会掉到地板上。为了固定它们,你使用 PhysicsJointPin 类。

    auto pin1 = PhysicsJointPin::construct(body1, wallBody, sp1->getPosition());
    
  2. 然后,你使用 PhysicsJointGear 类创建齿轮关节。在 PhysicsJointGear::construct 方法中,你指定两个物理体,即相位值和比值。相位值是两个物体的初始角偏移量。比值是齿轮比。如果比值是 2.0f,一个轴将旋转两次,另一个轴将旋转一次。

    auto joint = PhysicsJointGear::construct(body1, body2, 0.0f, 2.0f);
    _world->addJoint(joint);
    
  3. 你在第二步中能够创建齿轮关节。然而,你不能移动这个齿轮。这就是为什么你启用屏幕触摸和物理对象的移动。在 onTouchBegan 方法中,我们检查触摸区域中的物理对象。如果对象在触摸位置不存在,它返回 false

    auto location = touch->getLocation();
    auto shapes = _world->getShapes(location);
    if (shapes.size()<=0) {
        return false;
    }
    
  4. 如果对象存在于触摸位置,从物理形状中获取物理体。然后,在触摸位置创建一个节点,并将物理体添加到这个节点上。这个节点用于 onTouchMoved 方法。

    PhysicsShape* shape = shapes.front();
    PhysicsBody* body = shape->getBody();
    if (body != nullptr) {
        _touchNode = Node::create();
        auto touchBody = PhysicsBody::create(PHYSICS_INFINITY, PHYSICS_INFINITY);
        _touchNode->setPhysicsBody(touchBody);
        _touchNode->getPhysicsBody()->setDynamic(false);
        _touchNode->setPosition(location);
        this->addChild(_touchNode);
    
  5. 要给这个对象添加力,使用 touchBody 和触摸 location 添加 PhysicsJointPin。然后,使用 setMaxForce 方法设置力。

    PhysicsJointPin* joint = PhysicsJointPin::construct(touchBody, body, location);
    joint->setMaxForce(5000.0f * body->getMass());
    _world->addJoint(joint);
    
  6. onTouchMoved 方法中,按照以下方式移动触摸节点:

    void HelloWorld::onTouchMoved(Touch* touch, Event* event)
    {
        if (_touchNode!=nullptr) {
            _touchNode->setPosition(touch->getLocation());
        }
    }
    
  7. onTouchEnded 方法中,按照以下方式移除触摸节点:

    void HelloWorld::onTouchEnded(Touch* touch, Event* event)
    {
        if (_touchNode!=nullptr) {
            _touchNode->removeFromParent();
            _touchNode = nullptr;
        }}
    

还有更多…

Cocos2d-x 有很多关节。每个关节都有不同的任务,如下表所示:

关节 描述
PhysicsJointFixed 固定关节在参考点上连接两个物体。固定关节对于创建可以稍后拆分的复杂形状非常有用。
PhysicsJointLimit 极限关节在两个物体之间施加最大距离限制。
PhysicsJointPin 允许两个物体独立绕销轴旋转
PhysicsJointDistance 使用固定距离连接两个物体
PhysicsJointSpring 使用弹簧连接两个物体
PhysicsJointRotarySpring 类似于可以旋转的弹簧关节
PhysicsJointRotaryLimit 类似于可以旋转的极限关节
PhysicsJointRatchet 类似于扳手
PhysicsJointGear 保持一对物体的角速度比
PhysicsJointMotor 保持一对物体的相对角速度

这很难用文字解释。所以,你应该检查 Cocos2d-x 提供的 cpp-tests 应用程序。你运行 cpp-tests 应用程序并从菜单中选择 Node::Physics。你可以检查以下图片:

还有更多…

然后,你可以触摸或拖动这些物理对象,因此,你可以看到每个关节的工作情况。

通过使用加速度传感器改变重力

带有物理引擎的游戏通常会通过倾斜设备来改变重力的方向。这样做可以在游戏中增加现实感。在这个菜谱中,你可以通过使用加速度传感器来改变重力的方向。

准备工作

为了避免屏幕旋转,你必须更改一些代码和设置。首先,你应该将 设备方向 设置为仅 横向右,如图所示:

准备工作

其次,你需要在 RootViewController.mm 中将 shouldAutorotate 方法的返回值更改为 false。

- (BOOL) shouldAutorotate {
    return NO;
}

如何做到...

你可以在 HelloWorld.cpp 中检查加速度传感器值,如下所示:

Device::setAccelerometerEnabled(true);
auto listener = EventListenerAcceleration::create(={
    auto gravity = Vec2(acc->x*100.0f, acc->y*100.0f);
    world->setGravity(gravity);
});
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

它是如何工作的...

如果你倾斜设备,你可以获取变化的加速度 xy 值。此时,x 轴和 y 轴的值是 100 倍。这就是为什么加速度的值对于使用重力来说非常小。

auto gravity = Vec2(acc->x*100.0f, acc->y*100.0f);

在旋转设备时,如果主页按钮在右侧,那么它就是主页位置。此时,加速度 y 值是负的。在旋转时,如果主页按钮在左侧;加速度 y 值是正的。在旋转时,如果处于纵向位置,那么加速度 x 值是正的。或者,在旋转时,如果颠倒,那么加速度 x 值是负的。然后,通过使用加速度传感器值来改变重力,你可以在游戏中实现真实重力。

第十章. 使用额外功能改进游戏

本章将涵盖以下主题:

  • 使用 Texture Packer

  • 使用 Tiled 地图编辑器

  • 获取瓦片图中对象的属性

  • 使用物理编辑器

  • 使用字形设计器

简介

很长时间以来,有很多工具可供您使用,这些工具可以帮助您进行游戏开发。其中一些工具可以在 Cocos2d-x 中使用。使用这些工具,您可以快速高效地开发您的游戏。例如,您可以使用原始字体创建精灵表,创建像角色扮演游戏一样的地图,复杂的物理对象等等。在本章中,您将学习如何在您的游戏开发中使用这些额外工具。

使用 Texture Packer

Texture Packer是一个可以拖放图像并发布的工具。使用这个工具,我们不仅可以创建精灵表,还可以导出多精灵表。如果有很多精灵,那么在创建精灵表时,我们需要使用命令行工具,加密它们等等。在本食谱中,您可以使用 Texture Packer。

准备工作

Texture Packer 是一个付费应用程序。然而,您可以使用免费试用版。如果您没有,您可以通过访问www.codeandweb.com/texturepacker来下载它。

如何操作...

  1. 您需要启动 Texture Packer,之后您将看到一个空白窗口出现。如何操作...

  2. 在本食谱中,我们将使用以下截图所示的精灵:如何操作...

  3. 您只需将图像拖入 Texture Packer 窗口,它将自动读取所有文件并将它们排列好。如何操作...

  4. 就这样。那么,让我们发布精灵表图像和plist,点击发布按钮。这就是您如何获取精灵表图像和plist

它是如何工作的...

您可以获取精灵表图像和plist文件。在本部分,我们解释了如何一键发布适用于所有设备的精灵表。

  1. 点击带有齿轮图标的AutoSD按钮,您将看到一个额外的窗口出现,如图所示:它是如何工作的...

  2. 选择cocos2d-x HDR/HD/SD并点击应用按钮。点击后,设置默认的缩放、扩展、大小等,如图所示:它是如何工作的...

  3. 接下来,您必须点击发布按钮,您将看到一个选择数据文件名的窗口。重要的是要选择名为HDR的文件夹,如图所示:它是如何工作的...

  4. 最后,您将自动获得三个尺寸的精灵表,如图所示:它是如何工作的...

HDR文件夹中的精灵表是最大尺寸的。被拖放进来的图像是 HDR 图像。这些图像适合调整成高清或标清图像。

还有更多…

您可以使用如下命令使用 Texture Packer:

texturepacker foo_*.png --format cocos2d --data hoge.plist --sheet hoge.png

前面的命令是使用名为 foo_*.png 的图像创建名为 hoge.plisthoge.png 的精灵图集。例如,如果文件夹中有 foo_1.pngfoo_10.png,则精灵图集将从这 10 张图像创建。

此外,该命令还有其他选项,如下表所示:

选项 描述
--help 显示帮助文本
--version 打印版本信息
--max-size 设置最大纹理大小
--format cocos2d 要写入的格式,默认为 cocos2d
--data 要写入的数据文件名称
--sheet 要写入的图集名称

除了这些选项之外,您还可以通过以下命令查看其他选项:

texturepacker --help

使用 Tiled 地图编辑器

瓦片地图是由单元格组成的网格,单元格中的值表示该位置应该放置的内容。例如,(0,0) 是一条道路,(0,1) 是草地,(0,2) 是河流等等。瓦片地图非常有用,但手动创建它们相当困难。Tiled 是一个可以用来创建瓦片地图的工具。Tiled 是一个免费的应用程序。然而,这个应用程序是一个非常强大、有用且流行的工具。Tiled 地图有多种类型,例如,2D 地图如《龙之谷》,水*滚动游戏地图如《超级马里奥》等等。在这个菜谱中,您基本上可以使用纹理打包器。

准备工作

如果您没有 Tiled 地图编辑器,您可以从 www.mapeditor.org/ 下载它。

然后,下载后,您将安装应用程序并将 dmg 文件中的 example 文件夹复制到您的计算机的工作空间中。

Tiled 地图编辑器是一个免费的应用程序。然而,如果您喜欢,可以向这个软件捐赠。

如何操作...

在这部分,我们解释如何使用 Tiled 工具从头开始创建一个新的地图。

  1. 启动 Tiled 并在菜单中选择文件 | 新建。打开如下所示的附加窗口:如何操作...

  2. 瓦片层格式中选择 XML,并在地图大小中更改宽度高度为 50 个瓦片。最后,点击确定。这样您就可以看到 Tiled 的窗口,如下所示:如何操作...

  3. 在菜单中选择地图 | 新建瓦片集…。您可以选择瓦片集窗口。通过点击窗口中间的浏览…按钮选择瓦片集图像。在这种情况下,您将选择 Tiled 的 example 文件夹中的 tmw_desert_spacing.png 文件。这个瓦片集包含宽度为 32px、高度为 32px、边距和间距为 1px 的瓦片。因此,您必须将这些值更改为如下截图所示:如何操作...

  4. 最后,点击确定按钮,您将看到如下截图所示的新编辑器窗口:如何操作...

  5. 接下来,让我们尝试使用您选择的瓦片来绘制地面层。从右侧和下方的面板中选择瓦片,然后在工具栏中选择桶形图标。然后,点击地图,您将看到用相同瓦片绘制的地面。如何操作...

  6. 您可以在地图上排列瓦片。在右下角的面板中选择瓦片,然后在工具栏中选择印章图标。然后,点击地图。这样您就可以将瓦片放置在地图上。如何操作...

  7. 在您完成地图排列后,需要将其保存为新的文件。在菜单中转到 文件 | 另存为… 并保存您创建的新文件。要使用 Cococs2d-x,您必须将 tmx 文件和瓦片集图像文件添加到项目中的 Resources/res 文件夹。在这个菜谱中,我们在 Tiled 的 example 文件夹中添加了 desert.tmxtmw_desert_spacing.png如何操作...

  8. 从现在开始,您必须在 Xcode 中工作。按照以下代码编辑 HelloWorld::init 方法:

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
        Vec2 origin = Director::getInstance()- >getVisibleOrigin();
        _map = TMXTiledMap::create("res/desert.tmx");
        _map->setPosition(Vec2()+origin);
        this->addChild(_map);
    
        return true;
    }
    
  9. 构建并运行后,您可以在模拟器或设备上看到以下图像:

如何操作...

工作原理...

Tiled 地图所需文件是 tmx 文件和瓦片集图像文件。这就是为什么您必须将这些文件添加到您的项目中。您可以使用 TMXTiledMap 类查看 Tiled 地图对象。您必须将 tmx 文件路径指定给 TMXTiledMap::create 方法。TMXTiledMap 对象是节点。只有当您使用 addChild 方法添加 TMXTiledMap 对象时,您才能看到瓦片地图。

_map = TMXTiledMap::create("res/desert.tmx");
_map->setPosition(Vec2()+origin);
this->addChild(_map);

小贴士

TMXTileMap 对象的锚点位置是 Vec2(0,0)。正常节点的锚点位置是 Vec2(0.5f, 0.5f)

更多内容…

瓦片地图非常大。因此,我们尝试通过滚动来移动地图。在这种情况下,您触摸屏幕,并通过触摸点到屏幕中心的距离来滚动地图。

  1. HelloWorld::init 方法中添加以下代码:

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
    touchListener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
    _eventDispatcher- >addEventListenerWithSceneGraphPriority(touchListener, this);
    
  2. HelloWorldScene.h 中定义 touch 方法和一些属性,如下面的代码所示:

    bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
    void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event);
    void update(float dt);
    cocos2d::Vec2 _location;
    cocos2d::TMXTiledMap* _map;
    
  3. HelloWorldScene.cpp 中添加 touch 方法,如下面的代码所示:

    bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
    {
        return true;
    }
    
    void HelloWorld::onTouchEnded(Touch* touch, Event* event)
    {
        auto size = Director::getInstance()->getVisibleSize();
        auto origin = Director::getInstance()- >getVisibleOrigin();
        auto center = Vec2(size/2)+origin;
        _location = touch->getLocation() - center;
        _location.x = floorf(_location.x);
        _location.y = floorf(_location.y);
        this->scheduleUpdate();
    }
    
  4. 最后,在 HelloWorldScene.cpp 中添加 update 方法,如下面的代码所示:

    void HelloWorld::update(float dt)
    {
        auto mapSize = _map->getContentSize();
        auto winSize = Director::getInstance()- >getVisibleSize();
        auto origin = Director::getInstance()- >getVisibleOrigin();
    
        auto currentLocation = _map->getPosition();
        if (_location.x > 0) {
            currentLocation.x--;
            _location.x--;
        } else if (_location.x < 0) {
            currentLocation.x++;
            _location.x++;
        }
        if (_location.y > 0) {
            currentLocation.y--;
            _location.y--;
        } else if (_location.y < 0) {
            currentLocation.y++;
            _location.y++;
        }
    
        if (currentLocation.x > origin.x) {
            currentLocation.x = origin.x;
        } else if (currentLocation.x < winSize.width + origin.x - mapSize.width) {
            currentLocation.x = winSize.width + origin.x - mapSize.width;}
        if (currentLocation.y > origin.y) {
            currentLocation.y = origin.y;
        } else if (currentLocation.y < winSize.height + origin.y - mapSize.height) {currentLocation.y = winSize.height + origin.y - mapSize.height;}
    
        _map->setPosition(currentLocation);
        if (fabsf(_location.x)<1.0f && fabsf(_location.y)<1.0f) {
            this->unscheduleUpdate();
        }
    }
    

之后,运行此项目并触摸屏幕。这样您就可以在您滑动方向上移动地图。

获取瓦片地图中对象的属性

现在,您可以移动 Tiled 地图。然而,您可能会注意到地图上的对象。例如,如果移动方向上有木材或墙壁,您就不能超过该对象移动。在这个菜谱中,您将通过获取其属性来注意到地图上的对象。

准备工作

在这个菜谱中,您将为树对象创建一个新的属性并为其设置一个值。

  1. 启动 Tiled 应用程序并重新打开 desert.tmx 文件。

  2. Tilesets 窗口中选择树对象。

  3. 属性 窗口的左下角点击加号图标添加一个新属性。然后,将弹出一个窗口指定属性的名称。在文本区域中输入 isTree

  4. 在你命名了新属性后,它将显示在属性列表中。然而,你会发现它的值是空的。所以,你必须设置新的值。在这种情况下,你需要设置一个真值,如下面的图像所示:准备中

  5. 保存它并更新项目中的 desert.tmx

如何做到这一点...

在这个菜谱中,你将获取你触摸的对象的属性。

  1. 编辑 HelloWorld::init 方法以显示瓦片图并添加触摸事件监听器。

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
        Vec2 origin = Director::getInstance()->getVisibleOrigin();
        _map = TMXTiledMap::create("res/desert.tmx");
        _map->setPosition(Vec2()+origin);
        this->addChild(_map);
    
        auto touchListener = EventListenerTouchOneByOne::create();
        touchListener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
        _eventDispatcher- >addEventListenerWithSceneGraphPriority(touchListener, this);
    
        return true;
    }
    
  2. 添加 HelloWorld::getTilePosition 方法。如果你通过指定触摸位置调用此方法,你可以获取瓦片的网格行/列位置。

    Vec2 HelloWorld::getTilePosition(Vec2 point)
    {
        auto mapContentSize = _map->getContentSize();
        auto tilePoint = point - _map->getPosition();
        auto tileSize = _map->getTileSize();
        auto mapRowCol = _map->getMapSize();
        auto scale = mapContentSize.width / (mapRowCol.width * tileSize.width);
        tilePoint.x = floorf(tilePoint.x / (tileSize.width * scale));
        tilePoint.y = floorf((mapContentSize.height - tilePoint.y)/(tileSize.height*scale));
        return tilePoint;
    }
    
  3. 最后,你可以获取你触摸的对象的属性。添加如下所示的 HelloWorld::onTouchBegan 方法:

    bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
    {
        auto touchPoint = touch->getLocation();
        auto tilePoint = this->getTilePosition(touchPoint);
        TMXLayer* groundLayer = _map->getLayer("Ground");
        int gid = groundLayer->getTileGIDAt(tilePoint);
        if (gid!=0) {
            auto properties = _map- >getPropertiesForGID(gid).asValueMap();
            if (properties.find("isTree")!=properties.end()) {
                if(properties.at("isTree").asBool()) {
                    CCLOG("it's tree!");
                }
            }
        }
        return true;
    }
    

让我们构建并运行这个项目。如果你触摸了设置了新 isTree 属性的树,你可以在日志中看到 它是树!

它是如何工作的...

在这个菜谱中有两个要点。第一个要点是在瓦片图中获取瓦片的行/列位置。第二个要点是在瓦片图中获取对象的属性。

首先,让我们解释一下如何在瓦片图中获取瓦片的行/列位置。

  1. 使用 TMXTiledMap::getContentSize 方法获取地图大小。

    auto mapContentSize = _map->getContentSize();
    
  2. 从触摸点和地图位置计算地图上的 point

    auto tilePoint = point - _map->getPosition();
    
  3. 使用 TMXTiledMap::getTileSize 方法获取瓦片大小。

    auto tileSize = _map->getTileSize();
    
  4. 使用 TMXTiledMap::getMapSize 方法在地图中获取瓦片的行/列。

    auto mapRowCol = _map->getMapSize();
    
  5. 使用原始大小 mapContentSize 和通过列宽和瓦片宽计算出的实际大小来获取放大显示。

    auto scale = mapContentSize.width / (mapRowCol.width * tileSize.width);
    
  6. 瓦片坐标的原点位于左上角。这就是为什么你触摸的瓦片的行/列位置是使用瓦片的大小、行和放大显示来计算的,如下面的代码所示:

    tilePoint.x = floorf(tilePoint.x / (tileSize.width * scale));
    tilePoint.y = floorf((mapContentSize.height - tilePoint.y)/(tileSize.height*scale));
    

    tilePoint.x 是列位置,tilePoint.y 是行位置。

接下来,让我们看看如何获取 Tiled 地图中对象的属性。

  1. 使用触摸点获取你触摸的瓦片的行/列位置。

    auto touchPoint = touch->getLocation();
    auto tilePoint = this->getTilePosition(touchPoint);
    
  2. 从瓦片图中获取名为 "Ground" 的层。

    TMXLayer* groundLayer = _map->getLayer("Ground");
    
  3. 在这个层上有一个名为 Ground 的对象。使用瓦片的行/列从该层获取 TileGID

    int gid = groundLayer->getTileGIDAt(tilePoint);
    
  4. 最后,使用 TMXTiledMap::getPropertiesForGID 方法从地图中获取属性作为 ValueMap。然后,从其中获取 isTree 属性的值,如下面的代码所示:

    auto properties = _map->getPropertiesForGID(gid).asValueMap();
    if (properties.find("isTree")!=properties.end()) {
        if(properties.at("isTree").asBool()) {
            CCLOG("it's tree!");
        }
    }
    

在这个菜谱中,我们只展示了日志。然而,在你的实际游戏中,你将需要将分数添加到对象、爆炸等。

使用物理编辑器

在第九章中,控制物理,你学习了关于物理引擎的内容。我们可以创建物理体来使用 Cocos2d-x API。然而,我们只能创建圆形或方形形状。实际上,在真实游戏中,你必须使用复杂形状。在这个菜谱中,你将学习如何使用物理编辑器创建许多形状。

准备工作

物理编辑器是由创建纹理打包器的同一家公司开发的。物理编辑器是一个付费应用程序。但你可以使用免费试用版。如果你还没有,你可以通过访问www.codeandweb.com/physicseditor来下载它。

在这里,你准备使用此工具的图像。这里,我们将使用以下类似齿轮的图像。此图像的名称是gear.png

准备中

如何操作...

首先,你需要创建一个物理文件来使用物理编辑器。

  1. 启动物理编辑器。然后,将图像gear.png拖到左侧面板。如何操作...

  2. 点击工具栏中从左数第三个的形状追踪器图标。形状追踪器图标如下所示:如何操作...

  3. 在此之后,你可以看到如下所示的弹出窗口:如何操作...

    你可以更改容差值。如果顶点值太大,渲染器会变慢。因此,你需要设置合适的顶点值来更改容差值。最后,点击确定按钮。你将看到以下内容:

    如何操作...

  4. 导出器中选择Cocos2d-x。在这个工具中,锚点的默认值是Vec2(0,0)。在 Cocos2d-x 中,锚点的默认值是Vec2(0.5f, 0.5f)。因此,你应该将锚点更改为中心,如下面的截图所示:如何操作...

  5. 选择类别碰撞接触的复选框。你需要向下滚动才能在右侧面板中看到此窗口。你可以勾选所有复选框并点击右侧面板底部的所有按钮。

  6. plist文件发布到 Cocos2d-x 中使用此形状。点击发布按钮并保存为之前的名称。

  7. 你可以在导出器选择器下看到下载加载器代码链接。点击链接。之后,打开浏览器并浏览到 github 页面。Cocos2d-x 无法加载物理编辑器的plist。然而,加载器代码在 github 上提供。因此,你必须克隆此项目并将代码添加到项目中的Cocos2d-x文件夹。如何操作...

接下来,你将编写代码,通过使用物理编辑器数据来创建物理体。在这种情况下,齿轮对象将出现在接触点。

  1. 包含文件PhysicsShapeCache.h

    #include "PhysicsShapeCache.h"
    
  2. 创建一个具有如下代码所示物理世界的场景:

    Scene* HelloWorld::createScene()
    {
        auto scene = Scene::createWithPhysics();
        auto layer = HelloWorld::create();
        PhysicsWorld* world = scene->getPhysicsWorld();
        world->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
        scene->addChild(layer);
        return scene;
    }
    
  3. 在场景中创建一个与屏幕大小相同的墙壁,并添加触摸事件监听器。然后,按照以下代码加载 Physics Editor 的数据:

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
    
        Size visibleSize = Director::getInstance()->getVisibleSize();
        Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
        auto wall = Node::create();
        auto wallBody = PhysicsBody::createEdgeBox(visibleSize, PhysicsMaterial(0.1f, 1.0f, 0.0f));
        wallBody->setContactTestBitmask(true);
        wall->setPhysicsBody(wallBody);
        wall->setPosition(Vec2(visibleSize/2)+origin);
        this->addChild(wall);
    
        auto touchListener = EventListenerTouchOneByOne::create();
        touchListener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
        _eventDispatcher- >addEventListenerWithSceneGraphPriority(touchListener, this);
    
        PhysicsShapeCache::getInstance()- >addShapesWithFile("res/gear.plist");
    
        return true;
    }
    
  4. 使齿轮对象在触摸屏幕时执行以下代码所示的操作:

    bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
    {
        auto touchPoint = touch->getLocation();
        auto body = PhysicsShapeCache::getInstance()- >createBodyWithName("gear");
        auto sprite = Sprite::create("res/gear.png");
        sprite->setPhysicsBody(body);
        sprite->setPosition(touchPoint);
        this->addChild(sprite);
        return true;
    }
    
  5. 然后,构建并运行此项目。触摸屏幕后,齿轮对象将出现在触摸点。如何操作...

工作原理...

  1. 首先,您必须添加两个文件,plist 和图像。物理体在您使用 Physics Editor 发布的 plist 文件中定义。然而,您使用齿轮图像来创建精灵。因此,您必须将 plist 文件和 gear.png 添加到您的项目中。

  2. Cocos2d-x 无法读取 Physics Editor 的数据。因此,您必须添加在 github 上提供的加载器类。

  3. 要使用物理引擎,您必须创建一个带有物理世界的场景,并且应该将调试绘制模式设置为简单,以便更好地理解物理体。

    auto scene = Scene::createWithPhysics();
    auto layer = HelloWorld::create();
    PhysicsWorld* world = scene->getPhysicsWorld();
    world->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
    
  4. 没有边框或墙壁,物理对象会从屏幕上掉落。因此,您必须设置一个与屏幕大小相同的墙壁。

    auto wall = Node::create();
    auto wallBody = PhysicsBody::createEdgeBox(visibleSize, PhysicsMaterial(0.1f, 1.0f, 0.0f));
    wallBody->setContactTestBitmask(true);
    wall->setPhysicsBody(wallBody);
    wall->setPosition(Vec2(visibleSize/2)+origin);
    this->addChild(wall);
    
  5. 加载由 Physics Editor 创建的物理数据的 plistPhysicsShapeCache 将一次性加载 plist。之后,物理数据将缓存在 PhysicsShapeCache 类中。

    PhysicsShapeCache::getInstance()- >addShapesWithFile("res/gear.plist");
    
  6. HelloWorld::onTouchBegan 方法中,在触摸点创建齿轮对象。您可以使用 PhysicsShapeCache::createBodyWithName 方法以及物理对象数据来创建物理体。

    auto body = PhysicsShapeCache::getInstance()- >createBodyWithName("gear");
    

使用 Glyph Designer

在游戏中,您必须经常使用文本。在这种情况下,如果您使用系统字体来显示文本,您将遇到一些问题。这就是为什么每个设备都有不同的字体。位图字体比 TTF 字体渲染更快。因此,Cocos2d-x 使用位图字体在屏幕左下角显示 fps 信息。因此,您应该将位图字体添加到您的游戏中以显示文本。在本教程中,您将学习如何使用 Glyph Designer,这是制作原始位图字体的工具,以及如何在 Cocos2d-x 中使用位图字体。

准备工作

Glyph Designer 是一款付费应用程序。但您可以使用免费试用版。如果您没有它,您可以通过访问以下网址下载它:

71squared.com/glyphdesigner

接下来,我们将找到一个适合您游戏氛围的免费字体。在这种情况下,我们将使用 dafont 网站上的 Arcade 字体(www.dafont.com/arcade-ya.font)。下载后,您需要将其安装到您的计算机上。

在 dafont 网站上有很多字体。但是,每个字体的许可协议都不同。如果您使用了该字体,您需要检查其许可协议。

如何操作...

在本节中,您将学习如何使用 Glyph Designer。

  1. 启动 Glyph Designer。在左侧面板中,有你在计算机上安装的所有字体。你可以从那里选择你想要在游戏中使用的字体。这里我们将使用你不久前下载的Arcade字体。如果你还没有安装它,你可以加载它。要加载字体,你必须点击工具栏中的加载字体按钮。如何操作...

  2. 选择或加载字体后,它将在中心面板中显示。如果你的游戏使用了字体的一部分,你必须保留所需的字符以节省内存和应用程序容量。要选择字符,你可以使用右面板中的包含字形窗口。你需要向下滚动才能在右面板中看到这个窗口。如何操作...

  3. 对于其他设置,你可以指定大小、颜色和阴影。在字体颜色选项中,你可以设置渐变。

  4. 最后,你可以通过点击工具栏右侧的导出图标来创建一个原始字体。

  5. 导出后,你将拥有两个扩展名为 .fnt.png 的文件。

工作原理...

位图字体有两个文件,.fnt.png。这些文件是成对使用的,用于位图字体。现在,你将学习如何在 Cocos2d-x 中使用位图字体。

  1. 你必须将 Glyph Designer 中创建的字体添加到项目中的Resources/font文件夹。

  2. 将以下代码添加到你的游戏中以显示"Cocos2d-x"。

    auto label = Label::createWithBMFont("fonts/arcade.fnt", "Cocos2d-x");
    label->setPosition(Vec2(visibleSize/2)+origin);
    this->addChild(label);
    
  3. 构建并运行你的项目后,你将看到以下内容:工作原理...

还有更多...

有些字体不是等宽的。真型字体在文字处理程序中使用已经足够好。然而,等宽字体更吸引人。例如,点字符需要使用等宽字体。当你想要将等宽字体转换为非等宽字体时,你可以按照以下步骤进行:

  1. 在右面板的纹理图集中勾选名为固定宽度的复选框。

  2. 预览你的字体,并点击工具栏中的预览图标。然后,你可以在文本框中检查你想要检查的字符。

  3. 如果你想要更改字符间距,那么你需要更改固定宽度复选框旁边的数字。还有更多…

第十一章。利用优势

本章将涵盖以下主题:

  • 使用加密的精灵图集

  • 使用加密的 zip 文件

  • 使用加密的 SQLite 文件

  • 创建观察者模式

  • 使用 HTTP 进行网络连接

简介

到目前为止,我们已经解释了 Cocos2d-x 中的基本技术信息。它支持在智能手机上开发游戏。实际上,您可以使用 Cocos2d-x 的基本功能创建您自己的游戏。然而,如果您的游戏成为热门,作弊者可能会尝试破解代码。因此,在某些情况下,加密是必要的,以防止未经授权访问您的游戏数据。加密是游戏开发中的一个重要方面,因为它可以帮助您保护代码,防止人们破坏游戏的整体体验,并且还可以防止游戏被非法破解。在本章中,您将学习如何加密您的游戏资源。

使用加密的精灵图集

对于黑客来说,从应用程序中提取资源文件非常容易。这对版权来说是一个巨大的担忧。精灵图集可以使用TexturePacker非常容易地加密。在本食谱中,您将学习如何加密您的精灵以保护它们免受黑客和作弊者的侵害。

如何操作...

要使用TexturePacker加密精灵图集,您需要在TexturePacker的左侧面板上设置它。然后,您需要按照这里写的步骤操作,以成功加密您的精灵。

  1. 将纹理格式更改为zlib compr. PVR(.pvr.ccz, Ver.2)

  2. 点击内容保护图标,您将看到一个额外的窗口,用于设置密码。

  3. 在以下屏幕截图所示的文本输入区域中输入加密密钥。您可以输入您喜欢的密钥。然而,输入 32 个十六进制数字很难,因此,您只需点击创建新密钥按钮。点击后,您会发现它会自动输入加密密钥如何操作...

  4. 记下这个加密密钥。这是您将需要用来解密加密文件的密钥。

  5. 最后,您可以发布加密的精灵图集。

它是如何工作的...

现在,让我们看看如何使用这些加密的精灵图集。

  1. 按照以下图像所示将加密的精灵图集添加到您的项目中:如何工作...

  2. HelloWorld.cpp中包含ZipUtils类以解密。

    #include "ZipUtils.h"
    
  3. 设置用于加密的TexturePacker的加密密钥。

    ZipUtils::setPvrEncryptionKey (0x5f2c492e, 0x635eaaf8, 0xe5a4ee49, 0x32ffe0cf);
    
  4. 最后,使用加密的精灵图集创建精灵。

    Size visibleSize = Director::getInstance()- >getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
    
    SpriteFrameCache::getInstance()- >addSpriteFramesWithFile("res/encrypted.plist");
    auto sprite = Sprite::createWithSpriteFrameName("run_01.png");
    sprite->setPosition(Vec2(visibleSize/2)+origin);
    this->addChild(sprite);
    

更多内容…

应用程序通常有很多精灵图集。您可以为每个精灵图集使用不同的加密密钥。但这可能会造成一些混淆。您需要在应用程序中的所有精灵图集中使用相同的密钥。第一次,您需要单击创建新密钥按钮来创建加密密钥。然后,您需要单击另存为全局密钥按钮将加密密钥保存为全局密钥。下次,当您创建新的加密精灵图集时,您可以通过单击使用全局密钥按钮将此加密密钥设置为全局密钥。

现在,我们将继续了解如何检查加密的精灵图集。加密的精灵图集的扩展名是 .ccz

  1. 双击具有 .ccz 扩展名的加密文件。

  2. 启动 Texture Packer,您将看到需要输入解密密钥的窗口,如图所示:还有更多…

  3. 输入解密密钥或单击使用全局密钥按钮。如果您已将密钥保存为全局密钥,则单击确定按钮。

  4. 如果密钥是正确的密钥,您将看到如图所示的精灵图集:

使用加密的 zip 文件

在智能手机中,游戏经常从服务器下载 zip 文件以更新资源。这些资源通常是黑客的主要目标。他们可以解密这些资源以操纵游戏系统中的信息。因此,这些资源的安全性非常重要。在这种情况下,zip 文件被加密以防止作弊者。在本教程中,您将学习如何使用密码解压加密的 zip 文件。

准备工作

Cocos2d-x 有一个解压库。然而,在这个库中加密/解密是禁用的。这就是为什么我们必须在 unzip.cpp 中启用 crypt 选项。此文件的路径是 cocos2d/external/unzip/unzip.cpp。您将不得不注释掉 unzip.cpp 中的第 71 行以启用 crypt 选项。

//#ifndef NOUNCRYPT
//        #define NOUNCRYPT
//#endif

当我们尝试在 Cocos2d-x 版本 3.7 中构建时,unzip.h 中的第 46 行出现了错误,如下所示:

#include "CCPlatformDefine.h"

您必须编辑以下代码以移除此错误,如下所示:

#include "platform/CCPlatformDefine.h"

如何操作...

首先,在 HelloWorld.cpp 中包含 unzip.h 文件以使用解压库,如下所示:

#include "external/unzip/unzip.h"

接下来,让我们尝试使用密码解压加密的 zip 文件。这可以通过在 HelloWorld.cpp 中添加以下代码来完成:

#define BUFFER_SIZE    8192
#define MAX_FILENAME   512

bool HelloWorld::uncompress(const char* password)
{
    // Open the zip file
    std::string outFileName = FileUtils::getInstance()- 
    >fullPathForFilename("encrypt.zip"); 
    unzFile zipfile = unzOpen(outFileName.c_str()); 
    int ret = unzOpenCurrentFilePassword(zipfile, password); 
    if (ret!=UNZ_OK) { CCLOG("can not open zip file %s", outFileName.c_str()); 
        return false;
    }

    // Get info about the zip file
    unz_global_info global_info;
    if (unzGetGlobalInfo(zipfile, &global_info) != UNZ_OK) {
        CCLOG("can not read file global info of %s", 
        outFileName.c_str());
        unzClose(zipfile);
        return false;
    }

    CCLOG("start uncompressing");

    // Loop to extract all files.
    uLong i;
    for (i = 0; i < global_info.number_entry; ++i) {
        // Get info about current file.
        unz_file_info fileInfo;
        char fileName[MAX_FILENAME];
        if (unzGetCurrentFileInfo(zipfile, &fileInfo, fileName, 
        MAX_FILENAME, nullptr, 0, nullptr,  0) != UNZ_OK) {
            CCLOG("can not read file info");
            unzClose(zipfile);
            return false;
        }

        CCLOG("filename = %s", fileName);

        unzCloseCurrentFile(zipfile);

        // Goto next entry listed in the zip file.
        if ((i+1) < global_info.number_entry) {
            if (unzGoToNextFile(zipfile) != UNZ_OK) {
                CCLOG("can not read next file");
                unzClose(zipfile);
                return false;
            }
        }
    }

    CCLOG("end uncompressing");
    unzClose(zipfile);

    return true;
}

最后,您可以通过指定密码来解压加密的 zip 文件以使用此方法。如果密码是 cocos2d-x,您可以使用以下代码解压:

this->uncompress("cocos2d-x");

它是如何工作的...

  1. 使用 unzOpen 函数打开加密的 zip 文件,如下所示:

    unzFile zipfile = unzOpen(outFileName.c_str());
    
  2. 在使用 unzOpen 函数打开它之后,再次使用 unzOpenCurrentFilePassword 函数打开,如下所示:

    int ret = unzOpenCurrentFilePassword(zipfile, password);
    if (ret!=UNZ_OK) {
        CCLOG("can not open zip file %s", outFileName.c_str());
        return false;
    }
    
  3. 之后,您可以继续使用与解压未加密的 zip 文件相同的方式。

使用加密的 SQLite 文件

我们经常使用 SQLite 来保存用户数据或游戏数据。SQLite 是一个强大且有用的数据库。然而,在你的游戏沙盒中有一个数据库文件。作弊者会从你的游戏中获取它,并对其进行编辑以作弊。在这个菜谱中,你将学习如何加密你的 SQLite 并防止作弊者编辑它。

准备中

我们将使用 wxSqlite 库来加密 SQLite。这是一个免费软件。首先,你需要在 Cocos2d-x 中安装 wxSqlite 并编辑一些代码,并在 Cocos2d-x 中设置文件。

  1. 下载 wxSqlite3 项目的 zip 文件。访问以下网址:sourceforge.net/projects/wxcode/files/Components/wxSQLite3/wxsqlite3-3.1.1.zip/download

  2. 解压 zip 文件。

  3. cocos2d/external 下创建一个名为 wxsqlite 的新文件夹。

  4. 在展开文件夹后,将 sqlite3/secure/src 复制到 cocos2d/external/wxsqlite,如下截图所示:准备中

  5. 将在第 4 步中添加到 wxsqlite/srcsqlite3.hsqlite3secure.c 添加到你的项目中,如下截图所示:准备中

  6. 在 Xcode 的 构建设置 中的 其他 C 标志 中添加 -DSQLITE_HAS_CODEC,如下截图所示:准备中

  7. cocos2d/external/wxsqlite 中创建一个名为 Android.mk 的新文件,如下代码所示:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := wxsqlite3_static
    LOCAL_MODULE_FILENAME := libwxsqlite3
    LOCAL_CFLAGS += -DSQLITE_HAS_CODEC
    LOCAL_SRC_FILES := src/sqlite3secure.c
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/src
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/src
    include $(BUILD_STATIC_LIBRARY)
    
  8. cocos2d/cocos/storage/local-storage 中编辑 Android.mk,如下代码所示:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := cocos_localstorage_static
    
    LOCAL_MODULE_FILENAME := liblocalstorage
    
    LOCAL_SRC_FILES := LocalStorage.cpp
    
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/..
    
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../..
    
    LOCAL_CFLAGS += -Wno-psabi
    LOCAL_CFLAGS += -DSQLITE_HAS_CODEC
    LOCAL_EXPORT_CFLAGS += -Wno-psabi
    
    LOCAL_WHOLE_STATIC_LIBRARIES := cocos2dx_internal_static
    LOCAL_WHOLE_STATIC_LIBRARIES += wxsqlite3_static
    
    include $(BUILD_STATIC_LIBRARY)
    
    $(call import-module,.)
    
  9. cocos2d/cocos/storage/local-storage 中编辑 LocalStorage.cpp。注释掉第 33 行和第 180 行,如下代码所示。

    LocalStorage.cpp 第 33 行:

    //#if (CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID)
    

    LocalStorage.cpp 第 180 行:

    //#endif // #if (CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID)
    
  10. proj.andorid/jni 中编辑 Android.mk,如下代码所示:

    LOCAL_SRC_FILES := hellocpp/main.cpp \
                       ../../Classes/AppDelegate.cpp \
                       ../../Classes/HelloWorldScene.cpp \
                       ../../cocos2d/external/wxsqlite/src/sqlite3secure.c
    
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/../../cocos2d/external/wxsqlite/src/
    LOCAL_CFLAGS += -DSQLITE_HAS_CODEC
    

在此之后,SQLite 被加密,可以在你的项目中使用。

如何操作...

  1. 你必须包含 sqlite3.h 以使用 SQLite API。

    #include "sqlite3.h"
    
  2. 创建加密的数据库,如下代码所示:

    std::string dbname = "data.db";
    std::string path = FileUtils::getInstance()->getWritablePath() + dbname;
    CCLOG("%s", path.c_str());
    
    sqlite3 *database = nullptr;
    if ((sqlite3_open(path.c_str(), &database) != SQLITE_OK)) {
        sqlite3_close(database);
        CCLOG("open error");
    } else {
        const char* key = "pass_phrase";
        sqlite3_key(database, key, (int)strlen(key));
    
        // sql: create table
        char create_sql[] = "CREATE TABLE sample ( "
        "               id     INTEGER PRIMARY KEY, "
        "               key    TEXT    NOT NULL,    "
        "               value  INTEGER NOT NULL     "
        "             )                             ";
    
        // create table
        sqlite3_exec(database, create_sql, 0, 0, NULL);
    
        // insert data
        char insert_sql[] = "INSERT INTO sample ( id, key, value )"
        "            values (%d, '%s', '%d')     ";
    
        char insert_record[3][256];
        sprintf(insert_record[0],insert_sql,0,"test",300);
        sprintf(insert_record[1],insert_sql,1,"hoge",100);
        sprintf(insert_record[2],insert_sql,2,"foo",200);
    
        for(int i = 0; i < 3; i++ ) {
            sqlite3_exec(database, insert_record[i], 0, 0, NULL);
        }
    
        sqlite3_reset(stmt);
        sqlite3_finalize(stmt);
        sqlite3_close(database);
    }
    
  3. 从加密的数据库中选择数据,如下代码所示:

    std::string dbname = "data.db";
    std::string path = FileUtils::getInstance()->getWritablePath() + dbname;
    CCLOG("%s", path.c_str());
    
    sqlite3 *database = nullptr;
    if ((sqlite3_open(path.c_str(), &database) != SQLITE_OK)) {
        sqlite3_close(database);
        CCLOG("open error");
    } else {
        const char* key = "pass_phrase";
        sqlite3_key(database, key, (int)strlen(key));
    
        // select data
        sqlite3_stmt *stmt = nullptr;
    
        std::string sql = "SELECT value FROM sample WHERE key='test'";
        if (sqlite3_prepare_v2(database, sql.c_str(), -1, &stmt, NULL) == SQLITE_OK) {
            if (sqlite3_step(stmt) == SQLITE_ROW) {
                int value = sqlite3_column_int(stmt, 0);
                CCLOG("value = %d", value);
            } else {
                CCLOG("error , error=%s", sqlite3_errmsg(database));
            }
        }
    
        sqlite3_reset(stmt);
        sqlite3_finalize(stmt);
        sqlite3_close(database);
    }
    

它是如何工作的...

首先,你必须使用 pass 语句创建加密的数据库。要创建它,请按照以下三个步骤进行:

  1. 正常打开数据库。

  2. 接下来,使用 sqlite3_key 函数设置密码短语。

    const char* key = "pass_phrase";
    sqlite3_key(database, key, (int)strlen(key));
    
  3. 最后,执行 SQL 语句来创建表。

在此之后,你将需要在应用程序中使用加密的数据库文件。你可以从 CCLOG 打印的路径中获取它。

要从中选择数据,使用相同的方法。在打开数据库后,你可以使用相同的密码短语从加密的数据库中获取数据。

还有更多...

你可能想知道这个数据库是否真的被加密了。那么让我们检查一下。使用命令行打开数据库并执行以下命令:

$ sqlite3 data.db 
SQLite version 3.8.4.3 2014-04-03 16:53:12
Enter ".help" for usage hints.
sqlite> .schema
Error: file is encrypted or is not a database
sqlite>

如果数据库被加密,你将无法打开它,并且会弹出错误消息,如下所示:

"file is encrypted or is not a database".

创建观察者模式

事件调度器是一种响应触摸屏幕、键盘事件和自定义事件等事件的机制。你可以使用事件调度器来获取事件。此外,你可以在设计模式中使用它来创建观察者模式。在本教程中,你将学习如何使用事件调度器以及如何在 Cocos2d-x 中创建观察者模式。

准备工作

首先,我们将详细介绍观察者模式。观察者模式是一种设计模式。当事件发生时,观察者通知观察者中注册的主题。它主要用于实现分布式事件处理。观察者模式也是 MVC 架构中的关键部分。

准备工作

如何操作...

在本教程中,我们将每秒创建一个计数标签。当触摸屏幕时,计数标签将在此位置创建,然后使用观察者模式每秒进行计数。

  1. 创建一个扩展Label类的Count类,如下面的代码所示:

    Count.h
    class Count : public cocos2d::Label
    {
    private:
        int _count;
        void countUp(float dt);
    public:
        ~Count();
        virtual bool init();
        CREATE_FUNC(Count);
    };
    Count.cpp
    Count::~Count()
    {
        this->getEventDispatcher()- >removeCustomEventListeners("TimeCount");
    }
    
    bool Count::init()
    {
        if (!Label::init()) {
            return false;
        }
    
        _count = 0;
    
        this->setString("0");
        this->setFontScale(2.0f);
    
        this->getEventDispatcher()- >addCustomEventListener("TimeCount", = { this->countUp(0); });
    
        return true;
    }
    
    void Count::countUp(float dt)
    {
        _count++;
        this->setString(StringUtils::format("%d", _count));
    }
    
  2. 接下来,当触摸屏幕时,此标签将在触摸位置创建,并使用调度器每秒调用HelloWorld::countUp方法,如以下HelloWorld.cpp中的代码所示:

    bool HelloWorld::init()
    {
        if ( !Layer::init() )
        {
            return false;
        }
    
        auto listener = EventListenerTouchOneByOne::create();
        listener->setSwallowTouches(_swallowsTouches);
        listener->onTouchBegan = C_CALLBACK_2(HelloWorld::onTouchBegan, this);
        this->getEventDispatcher()- >addEventListenerWithSceneGraphPriority(listener, this);
    
        this->schedule(schedule_selector(HelloWorld::countUp), 1.0f);
    
        return true;
    }
    
    bool HelloWorld::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *unused_event) {
        auto countLabel = Count::create(); this->addChild(countLabel); countLabel->setPosition(touch->getLocation()); 
        return true; }
    
    void HelloWorld::countUp(float dt)
    {
        this->getEventDispatcher()- >dispatchCustomEvent("TimeCount"); }
    
  3. 构建并运行此项目后,当你触摸屏幕时,它将在触摸位置创建一个计数标签,然后你会看到标签同时每秒进行计数。

它是如何工作的...

  1. 添加名为TimeCount的自定义事件。如果TimeCount事件发生,则调用Count::countUp方法。

    this->getEventDispatcher()- >addCustomEventListener("TimeCount", = {
        this->countUp(0);
    });
    
  2. 不要忘记,当移除Count类的实例时,需要从EventDispatcher中移除自定义事件。如果你忘记这样做,那么当事件发生时,EventDispatcher将调用zombie实例,你的游戏将会崩溃。

    this->getEventDispatcher()- >removeCustomEventListeners("TimeCount");
    
  3. HelloWorld.cpp中,使用调度器调用HelloWorld::countUp方法。HelloWorld::countUp方法调用名为TimeOut的自定义事件。

    this->getEventDispatcher()- >dispatchCustomEvent("TimeCount");
    

    然后,EventDispatcher将通知列出的主题。在这种情况下,调用Count::countUp方法。

    void Count::countUp(float dt){
        _count++;
        this->setString(StringUtils::format("%d", _count));
    }
    

还有更多...

使用EventDispatcher,标签同时计数。如果你使用调度器而不是EventDispatcher,你会注意到一些不同之处。

按照以下代码更改Count::init方法:

bool Count::init()
{
    if (!Label::init()) {
        return false;
    }

    _count = 0;

    this->setString("0");
    this->setFontScale(2.0f);
    this->schedule(schedule_selector(Count::countUp), 1.0f); 
    return true;
}

在此代码中,通过每秒调用Count::countUp方法使用调度器。你可以看到,标签不是同时计数的。每个标签都是每秒进行计数,然而不是同时。使用观察者模式,可以同时调用许多主题。

使用 HTTP 进行网络连接

在最*的智能手机游戏中,我们通常使用互联网网络来更新数据、下载资源等。没有网络的游戏是不存在的。在本教程中,你将学习如何使用网络下载资源。

准备工作

要使用网络,必须包含network/HttpClient的头文件。

 #include "network/HttpClient.h"

如果你要在 Android 设备上运行它,你需要编辑proj.android/AndroidManifest.xml

<user-permission android:name="android.permission.INTERNET" />

如何操作...

在下面的代码中,我们将从google.com/获取响应,然后,将响应数据作为日志打印出来。

auto request = new network::HttpRequest();
request->setUrl("http://google.com/ ");
request->setRequestType(network::HttpRequest::Type::GET);
request->setResponseCallback([](network::HttpClient* sender, network::HttpResponse* response){
    if (!response->isSucceed()) {
        CCLOG("error");
        return;
    }

    std::vector<char>* buffer = response->getResponseData();
    for (unsigned int i = 0; i <buffer-> size (); i ++) {
        printf("%c", (* buffer)[i]);
    }
    printf("\n");
});

network::HttpClient::getInstance()->send(request);
request->release();

它是如何工作的...

  1. 首先,创建一个HttpRequest实例。HttpRequest类没有create方法。这就是为什么你使用new来创建实例。

    auto request = new network::HttpRequest();
    
  2. 指定 URL 和请求类型。在这种情况下,将google.com/设置为请求 URL,并将 GET 设置为请求类型。

    request->setUrl("http://google.com/ "); request->setRequestType(network::HttpRequest::Type::GET);
    
  3. 设置回调函数以接收来自服务器的数据。你可以使用HttpResponse::isSucceed方法检查其成功。然后,你可以使用HttpResponse::getResponseData方法获取响应数据。

    request->setResponseCallback([](network::HttpClient* 
    sender, network::HttpResponse* response){ 
        if (!response->isSucceed()) { 
            CCLOG("error"); 
            return;
        }
    
        std::vector<char>* buffer = response- >getResponseData(); 
        for (unsigned int i = 0; i <buffer-> size (); i ++) { 
            printf("%c", (* buffer)[i]); 
        }
        printf("\n");
    });
    
  4. 你可以通过调用指定HttpRequest类实例的HttpClient::send方法来请求网络。如果你通过网络获取响应,那么调用第 3 步中提到的回调函数。

    network::HttpClient::getInstance()->send(request);
    
  5. 最后,你必须释放HttpRequest实例。这就是你使用new创建它的原因。

    request->release();
    

还有更多...

在本节中,你将学习如何使用HttpRequest类从网络获取资源。在下面的代码中,从网络获取谷歌日志并显示。

auto request = new network::HttpRequest();
request- >setUrl("https://www.google.co.jp/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); 
request->setRequestType(network::HttpRequest::Type::GET); 
request->setResponseCallback(&{ 
    if (!response->isSucceed()) { 
        CCLOG("error");
        return;
    }

    std::vector<char>* buffer = response->getResponseData(); 
    std::string path = FileUtils::getInstance()->getWritablePath() 
+ "image.png"; 
    FILE* fp = fopen(path.c_str(), "wb");
    fwrite(buffer->data(), 1, buffer->size(), fp);
    fclose(fp);

    auto size = Director::getInstance()->getWinSize();
    auto sprite = Sprite::create(path);
    sprite->setPosition(size/2);
    this->addChild(sprite);
});

network::HttpClient::getInstance()->send(request);
request->release();

构建并运行此代码后,你可以看到以下窗口。

还有更多…

小贴士

你必须将原始数据保存在沙盒中。你可以使用FileUtils::getWritablePath方法获取沙盒的路径。

posted @ 2025-10-02 09:35  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报