Unity2022-移动游戏开发-全-
Unity2022 移动游戏开发(全)
原文:
zh.annas-archive.org/md5/1632eb0b2399b90ebe5e9eb246847cbc译者:飞龙
前言
作为游戏开发者,您的目标是让您的客户在他们的位置上接触到您,随着每年越来越多的人购买移动设备,移动平台是一个需要考虑的关键平台。幸运的是,Unity 提供了跨平台功能,允许您一次编写游戏,然后通过最小修改将其移植到其他游戏机。然而,为移动设备开发也需要特定的考虑和功能,这正是Unity 2022 移动游戏开发所涉及的。
在本书中,我们将引导您通过使用 Unity 创建和部署移动游戏到 iOS 和 Android 的过程。我们将涵盖添加移动设备输入、设计适应各种屏幕尺寸的用户界面以及探索使用 Unity 的内购(IAP)和广告系统来盈利等基本主题。我们还将讨论使用通知来保留用户以及使用 Twitter 和 Facebook 的 SDK 与全世界分享您游戏的重要性。
此外,我们将深入研究 Unity 的分析系统以优化您游戏的表现并提供关于用户行为的见解。您还将学习在将游戏发布到 Google Play 和 iOS 应用商店之前,以各种方式对您的游戏进行润色。
最后,我们将介绍 Unity 的 AR Foundation 框架的使用,该框架使您能够创建未来兼容且与多台设备兼容的增强现实(AR)应用。
在本书结束时,您将深入理解如何使用 Unity 进行移动游戏开发,包括移动设备特有的关键功能。
本书面向的对象
如果您是一位对为 iOS 和 Android 开发移动游戏感兴趣的 Unity 游戏开发者,那么这本书是您理想的资源。尽管具备 C#的相关知识会有帮助,但并非必需。无论您是经验丰富的开发者还是刚开始,本书提供的逐步指导将帮助您理解使用 Unity 进行移动游戏开发所需的独特功能和考虑因素。
本书涵盖的内容
第一章, 构建您的游戏,通过创建一个将在整本书中修改以融入移动特定功能的基础项目,介绍了 Unity 游戏开发的基础。
第二章, Android 和 iOS 开发的项目设置,解释了如何配置您的开发环境以便将游戏部署到 Android 和 iOS 移动设备上。
第三章, 移动输入/触摸控制,向您介绍移动输入的基本原理,包括触摸和手势识别、使用加速度计以及通过Touch类访问设备信息。
第四章, 分辨率无关 UI,专注于如何构建分辨率无关的 UI 元素,这对于使用不同宽高比和分辨率的全部游戏项目都很有用。
第五章,高级移动 UI,基于前一章的知识,扩展到包括在 UI 上工作的移动特定方面,如需要屏幕控制,以及调整 UI 以适应有刘海的设备。
第六章,实现应用内购买,解释了如何将 Unity 的 IAP 系统集成到我们的项目中,包括创建可消耗和非可消耗的 IAP。
第七章,使用 Unity Ads 进行广告宣传,涵盖了将 Unity 的广告框架集成到我们的项目中,并探讨了创建简单和复杂广告的方法。
第八章,将社交媒体集成到我们的项目中,展示了如何通过整合分享到 Twitter 的高分等功能将社交媒体集成到您的游戏中,并使用 Facebook SDK 进行登录并显示玩家的姓名和头像。
第九章,通过通知保持玩家参与度,展示了如何将通知集成到您的游戏中,包括它们的设置、创建基本通知以及自定义它们呈现的方式。
第十章,使用 Unity Analytics,涵盖了将 Unity 的分析工具集成到您的游戏中,包括跟踪自定义事件和使用远程设置修改游戏玩法,而无需玩家重新下载游戏。
第十一章,远程配置,将展示如何轻松设置 Unity 的远程配置系统,以及我们如何通过改变玩家移动速度来改变游戏难度,通过简单示例利用它。
第十二章,提升游戏感觉,介绍了游戏设计中的“游戏感觉”概念,并探讨了如何集成缓动动画、材质、后处理效果和粒子效果来增强玩家体验。
第十三章,构建游戏发布版本,指导您如何为 iOS 和 Android 设备构建游戏发布版本所需的步骤。
第十四章,将游戏提交到应用商店,提供了将您的游戏提交到 Google Play 和 iOS 应用商店的技巧和窍门。
第十五章,增强现实,涵盖了将 AR 添加到您的游戏中的过程,包括 ARCore、ARKit 和 AR Foundation 的设置、安装和配置,检测现实世界中的表面,以及通过生成对象与环境交互。
为了充分利用这本书
在本书中,我们将使用 Unity 3D 游戏引擎进行开发,您可以从unity.com/download下载。项目是使用 Unity 2022.1.0b16 创建的,但如果您使用的是该引擎的后续版本,则可能只需要进行最小更改。如果有一个新版本发布,并且您想下载本书中使用的确切版本,您可以在 Unity 的下载存档unity3d.com/get-unity/download/archive中找到。您还可以在 Unity 编辑器的系统需求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署您的项目,您需要一个 Android 或 iOS 设备。
为了简化,我们假设您在为 Android 开发时使用的是 Windows 操作系统,在为 iOS 开发时使用的是 Macintosh 计算机。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Unity 2022.1.0b16 | Windows, macOS, 或 Linux |
| Unity Hub 3.3.1 | Windows, macOS, 或 Linux |
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还从我们丰富的图书和视频目录中提供了其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/6M4wR。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个示例:“这为我们提供了所需的代码 – 特别是,GameNotificationManager类 – 以添加到我们的脚本中。”
代码块设置如下:
public void ShowNotification(string title, string body,
DateTime deliveryTime)
{
IGameNotification notification =
notificationsManager.CreateNotification();
if (notification != null)
{
notification.Title = title;
notification.Body = body;
notification.DeliveryTime = deliveryTime;
notification.SmallIcon = "icon_0";
notification.LargeIcon = "icon_1";
notificationsManager.ScheduleNotification(notification);
}
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
ShowNotification("Endless Runner", notifText, notifTime);
// Example of cancelling a notification
var id = ShowNotification("Test", "Should Not Happen",
notifTime);
if(id.HasValue)
{
notificationsManager.CancelNotification(id.Value);
}
/* Cannot be added again until the user quits game */
addedReminder = true;
}
}
任何命令行输入或输出都按照以下方式编写:
$ mkdir css
$ cd css
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个示例:“通过编辑 | 项目设置进入项目设置菜单。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
copyright@packt.com 并附上材料链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读了《Unity 2022 移动游戏开发》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走?
您选择的设备是否不兼容电子书购买?
别担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取好处:
- 扫描二维码或访问以下链接

packt.link/free-ebook/9781804613726
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他好处发送到您的电子邮件。
第一部分:游戏玩法/开发设置
在本书的这一部分,我们将探讨 Unity 游戏开发的基础元素,特别是专注于创建移动游戏的准备工作。本部分中的章节将为您提供设置开发环境所需的知识和技能,并指导您构建游戏项目并将其部署到移动设备上。
到本部分结束时,你将拥有关于 Unity 游戏开发的知识基础,并准备好进入书中后续部分更高级的主题。
本部分包含以下章节:
-
第一章, 构建你的游戏
-
第二章, Android 和 iOS 开发的项目设置
第一章:构建你的游戏
当我们开始使用 Unity 游戏引擎构建移动游戏时,在深入探讨移动平台构建细节之前,熟悉引擎本身是很重要的。虽然有可能你已经构建了一个游戏并希望将其迁移到移动平台,但也会有那些之前从未接触过 Unity 或许很久没有使用过它的人。本章将为新来者提供一个介绍,并为回归者提供一个复习,同时为已经熟悉 Unity 的人提供一些最佳实践。虽然如果你已经熟悉 Unity,可以跳过这一章,但我认为了解项目背后的思考过程也很重要,这样你就可以在未来的作品中记住这些思考,以便为你的未来作品做好准备。
在本章中,我们将构建一个类似于 *Imangi Studios LLC 的 Temple Run 系列的 3D 无尽跑酷游戏。在我们的案例中,玩家将连续向某个方向奔跑并躲避挡在他们路上的障碍物。我们还可以轻松地为游戏添加额外功能,因为游戏将不断添加新内容。
本章将分为几个主题。它将包含简单的、逐步的过程供你遵循。以下是我们的任务概述:
-
设置项目
-
创建玩家
-
使用 C# 脚本移动玩家
-
使用属性和 XML 注释改进脚本
-
Update 函数与 FixedUpdate 函数
-
让摄像机跟随我们的玩家
-
创建基本瓦片
-
使游戏无限
-
创建障碍物
技术要求
本书使用 Unity 2022.1.0b14 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需做最小改动即可。如果你想要下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档 unity3d.com/get-unity/download/archive。
你也可以在 Unity 编辑器系统 要求 部分的 docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html 找到 Unity 的系统要求。
你可以在 GitHub 上找到本章的代码文件,地址为 github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter01。
设置项目
现在我们已经明确了目标,让我们开始构建我们的项目:
-
要开始,请在你的电脑上打开 Unity Hub。
-
从启动开始,我们将通过点击 新建 按钮来创建一个新的项目。
-
接下来,在
MobileDev下,并在 模板 中,确保 3D 已被选中。之后,点击 创建 并等待 Unity 加载:

图 1.1 – 创建 3D 项目
- 完成后,你将看到 Unity 编辑器首次弹出:

图 1.2 – Unity 编辑器
- 如果你的布局看起来与前面的截图不同,请转到工具栏的右上角部分,并选择那里的下拉菜单,该菜单显示为 Layout。从那里,从提供的选项中选择 Default:

图 1.3 – 布局按钮
现在我们已经首次打开 Unity,并显示了默认布局!
小贴士
如果这是你第一次使用 Unity,那么我强烈建议你阅读 Unity 用户手册 中的 Unity 的界面 部分,你可以通过 docs.unity3d.com/Manual/UsingTheEditor.html 访问。
现在我们已经打开了 Unity,我们实际上可以开始构建我们的项目了。
创建玩家
要开始,我们将构建一个始终向前移动的玩家。现在让我们开始吧:
-
要开始,我们将为玩家创建一些可以行走的地面。为此,转到顶部菜单并选择 GameObject | 3D Object | Cube。
-
从那里,我们将移动到
Floor。然后,对于0,0,0)。这可以通过输入值或右键单击 Transform 组件然后选择 Reset Position 选项来完成。 -
然后,我们将设置
7,0.1,10):

图 1.4 – 创建地面
在 Unity 中,默认情况下,1 单位空间代表现实生活中的 1 米。因此,我们的 Scale 值将使地面比它宽(X 和 Z)更长,我们在地面上有一些大小(Y),所以玩家会与之碰撞并落在上面,因为我们默认附加了一个 Box Collider 组件。
注意
当创建 Cube 对象时,会自动添加 Box Collider 组件,并且需要将其附加到对象上以进行碰撞。有关 Box Collider 组件的更多信息,请参阅 docs.unity3d.com/Manual/class-BoxCollider.html。
-
接下来,我们将创建我们的玩家,它将是一个球体。为此,我们将转到 GameObject | 3D Object | Sphere。
-
将球体重命名为
Player并设置0,1,-4):

图 1.5 – 定位玩家
这将球体略微抬离地面并将其移回到接近起点。注意,相机对象(见相机图标)默认指向球体,因为它位于 (0, 1, -10)的位置。
- 我们希望球体移动,因此需要通知物理引擎我们希望这个物体对力做出反应,所以我们需要添加一个Rigidbody组件。要做到这一点,在Player对象被选中时,进入菜单并选择Component | Physics | Rigidbody。为了查看现在会发生什么,让我们点击中间第一个工具栏上的Play按钮:

图 1.6 – 游戏当前状态
如前一张截图所示,当我们在游戏中播放时,应该看到球体掉到地面上。
小贴士
您可以通过单击顶部的Maximize On Play按钮或右键单击Game标签并选择Maximize来启用/禁用在播放时占用整个屏幕的Game标签。
- 再次点击Play按钮以关闭游戏并返回到Scene标签,如果它没有自动发生。
现在我们已经在游戏中有了地板和玩家的对象,并且已经告诉玩家要响应物理!接下来,我们将通过代码的使用为玩家添加交互性。
通过 C#脚本移动玩家
我们希望玩家能够移动,因此为了实现这一点,我们将在脚本中创建我们自己的功能,实际上在这个过程中创建我们自己的自定义组件:
- 要创建一个脚本,我们将进入Project窗口,并单击菜单左上角的Create按钮,点击+图标,然后选择Folder:

图 1.7 – + 图标的定位
小贴士
您也可以通过在Project窗口的右侧单击右键来访问Create菜单。使用此方法,您可以右键单击并选择Create | Folder。
- 从那里,我们将这个文件夹命名为
Scripts。始终组织我们的项目是个好主意,这样有助于项目组织。
小贴士
如果你在Project窗口中不小心拼错了某个项目的名称,你可以通过右键单击并选择Rename选项,或者选择对象然后单击名称来重命名它。
- 双击文件夹进入,在
PlayerBehaviour(没有空格)处创建一个脚本。
注意
我之所以使用behaviour而不是behavior,是因为 Unity 中的所有组件都是另一个名为MonoBehaviour的类的子类,我在这方面遵循 Unity 的指导。
-
双击脚本以打开您选择的脚本编辑器(IDE),并向其中添加以下代码:
using UnityEngine; public class PlayerBehaviour : MonoBehaviour { // A reference to the Rigidbody component private Rigidbody rb; // How fast the ball moves left/right public float dodgeSpeed = 5; // How fast the ball moves forward automatically public float rollSpeed = 5; // Start is called before the first frame update void Start() { // Get access to our Rigidbody component rb = GetComponent<Rigidbody>(); } // Update is called once per frame void Update() { // Check if we're moving to the side var horizontalSpeed = Input.GetAxis("Horizontal") * dodgeSpeed; rb.AddForce(horizontalSpeed, 0, rollSpeed); } }
在前面的代码中,我们有一些将要使用的变量。rb变量是对我们之前添加的 GameObject 的Rigidbody组件的引用。它使我们能够使对象移动,我们将在Update函数中使用它。我们还有两个变量,dodgeSpeed和rollSpeed,分别决定了玩家在左右移动或向前移动时移动的速度。
由于我们的对象只有一个Rigidbody组件,我们在Start函数中将其分配一次,该函数在游戏开始时将 GameObject 加载到场景中时被调用。
然后,我们使用Update函数来检查我们的玩家是否按下了按键以根据 Unity 的Input.GetAxis函数返回的负值来移动左右。如果我们按下“A”键或左箭头,它将返回-1。如果我们按下右箭头或“D”键,我们将得到一个返回的正值,直到1,如果没有按键,输入将移动到0。然后我们将其乘以dodgeSpeed以增加速度,以便更容易看到对象的移动。
注意
有关输入管理器的更多信息,请参阅docs.unity3d.com/Manual/class-InputManager.html。
最后,一旦我们有了这个值,我们将对球在 X 轴上的horizontalSpeed单位以及 Z 轴上的rollSpeed应用一个力。
-
保存你的脚本并返回到 Unity 编辑器。
-
现在,我们需要通过从
Player对象中选择PlayerBehaviour脚本中的Player对象来将此脚本分配给我们的玩家。
注意
注意,当编写脚本时,如果我们将一个变量声明为public,它将在我们希望设计师为了游戏玩法目的调整值时显示在public中,但它也允许其他脚本在代码中访问该属性。默认情况下,变量和方法是private的,这意味着它们只能在类内部使用。有关访问修饰符的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/access-modifiers。
如果一切顺利,我们应该在我们的对象上看到脚本,如下所示:

图 1.8 – 添加的 PlayerBehaviour 组件
- 通过转到文件 | 保存来保存你的场景。之后,玩游戏并使用左右箭头查看玩家根据你的输入移动,但无论如何,默认情况下向前移动:

图 1.9 – 游戏当前状态
现在,你可以看到球会自动移动,并且我们的输入被正确接收!
使用属性和 XML 注释改进我们的脚本
我们可以停止使用 PlayerBehaviour 类脚本,但我想要提及一些我们可以用来提高代码质量和风格的事情。当你开始以团队形式构建项目时,这变得特别有用。因为你将与其他人一起工作,其中一些人将与你一起编写代码。然后,将会有设计师和艺术家,他们不会与你一起编写代码,但仍然需要使用你编写的程序。
在编写脚本时,我们希望它们尽可能没有错误。将 rb 变量设置为 private 开始了这个过程,因为现在用户将无法在这个类之外修改它。我们希望我们的队友修改 dodgeSpeed 和 rollSpeed,但我们可能希望给他们一些关于它是什么以及/或如何使用的建议。要在 检查器 窗口中这样做,我们可以使用一种称为 属性 的东西。
使用属性
属性是我们可以在变量、类或函数声明开头添加的东西,它允许我们附加额外的功能。Unity 中存在许多这样的属性,你也可以编写你自己的属性,但现在是时候谈谈我使用最频繁的属性了。
工具提示属性
如果你已经使用 Unity 一段时间了,你可能已经注意到 Rigidbody 中的一些组件有一个很好的功能——如果你将鼠标移到变量名上,你会看到变量是什么以及如何使用它们的描述。你首先会学到的是,我们可以通过使用 Tooltip 属性在我们的组件中实现相同的效果。如果我们对 dodgeSpeed 和 rollSpeed 变量这样做,它看起来可能就像这样:
[Tooltip("How fast the ball moves left/right")]
public float dodgeSpeed = 5;
[Tooltip("How fast the ball moves forward automatically")]
public float rollSpeed = 5;
保存前面的脚本并返回到编辑器:

图 1.10 – 工具提示属性示例
现在,当我们使用鼠标突出显示变量并保持在那里时,我们将显示放置的文本。这是一个非常好的习惯,因为你的队友可以始终知道你的变量被用于什么,而无需实际查看脚本本身。
注意
更多关于 Tooltip 属性的信息,请查看 docs.unity3d.com/ScriptReference/TooltipAttribute.html。
范围属性
我们还可以用来保护我们的代码的是 Range 属性。这将允许我们为变量指定一个最小值和最大值。由于我们希望玩家始终向前移动,我们可能希望限制玩家向后移动。为此,我们可以添加以下突出显示的代码行:
[Tooltip("How fast the ball moves forward automatically")]
[Range(0, 10)]
public float rollSpeed = 5;
保存你的脚本,并返回到编辑器:

图 1.11 – 范围属性示例
我们现在在我们的值旁边添加了一个滑动条,我们可以拖动它来调整我们的最小值和最大值之间。这不仅保护我们的变量不被改变为无效状态,还使得我们的设计师可以通过简单地拖动它们来轻松调整事物。
RequireComponent 属性
目前,我们正在使用 Rigidbody 组件来创建我们的脚本。当作为团队成员工作时,其他人可能不会阅读你的脚本,但仍然期望他们在创建游戏玩法时使用它们。不幸的是,这意味着他们可能会做一些意想不到的事情,例如移除 Rigidbody 组件,这将在我们的脚本运行时导致错误。幸运的是,我们还有 RequireComponent 属性,我们可以用它来解决这个问题。
看起来像这样:
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class PlayerBehaviour : MonoBehaviour
通过添加此属性,我们声明,当我们将此组件包含在 GameObject 中且其 GameObject 没有附加 Rigidbody 组件时,组件将被自动添加。这也使得如果我们尝试从该对象中移除 Rigidbody 组件,编辑器将警告我们无法这样做,除非我们首先移除 PlayerBehaviour 组件。请注意,这适用于从 MonoBehaviour 扩展的任何类;只需将 Rigidbody 替换为你希望保留的内容即可。
现在,如果我们进入 Unity 编辑器,并尝试通过在 检查器 中右键单击它并选择 移除组件 来移除 Rigidbody 组件,将会看到以下消息:

图 1.12 – 无法移除组件窗口
这正是我们想要的,这确保了组件将存在,使我们不必每次想要使用组件时都包含 if 检查。
注意,之前我们没有在私有变量 rb 上使用 Tooltip 属性。由于它不会在编辑器中显示,所以实际上并不需要。然而,我们也可以通过使用 XML 注释来增强这一点:使用 XML 注释。
XML 注释
我们可以通过 XML 注释实现一些我们之前使用传统注释无法实现的好事情。当我们在 Visual Studio 中使用变量/函数而不是代码时,现在我们将看到关于它的注释。这将帮助团队中的其他开发者通过额外的信息和细节确保他们正确地使用你的代码。
XML 注释看起来像这样:
/// <summary>
/// A reference to the Rigidbody component
/// </summary>
private Rigidbody rb;
可能看起来需要更多的写作才能使用这种格式,但实际上我并没有全部打出来。XML 注释是 C# 的一个相当标准的特性,所以如果你使用 MonoDevelop 或 Visual Studio 并输入 ///,动作将自动为你生成摘要块(以及如果需要参数,如函数所需的 param 标签)。
那么,我们为什么要这样做呢?好吧,如果你在 IntelliSense 中选择变量,它将显示以下信息给我们:

图 1.13 – XML 注释中的工具提示示例
当其他人试图使用你的代码时,这是一个很大的帮助,这也是 Unity 员工编写代码的方式。我们还可以将此扩展到函数和类,以确保我们的代码更具自文档化。
不幸的是,XML 注释在检查器中不会显示,并且 Tooltip 属性不能用于项目的一些方面,如函数。考虑到这一点,我使用 Tooltip 用于公共说明和/或将在 检查器 窗口中显示的内容,而将 XML 注释用于其他所有内容。
注意
如果你想要更深入地了解 XML 注释,请随时查看 msdn.microsoft.com/en-us/library/b2s063f7.aspx。
现在我们已经探讨了改进代码格式的几种方法;让我们看看如何通过查看 Unity 提供的一些不同的 Update 函数来提高性能。
Update 函数与 FixedUpdate 函数
接下来要查看的是我们的移动代码。你可能已经注意到,我们目前正在使用 Update 函数来移动我们的玩家。正如上面的注释所述,Update 函数在游戏运行时每帧被调用一次。需要考虑的一点是,Update 被调用的频率是可变的,这意味着它可能会随时间变化。这取决于许多因素,包括所使用的硬件。这意味着 Update 函数被调用得越多,计算机的性能就越好。我们希望所有玩家都能获得一致的经验,而我们可以通过使用 FixedUpdate 函数来实现这一点。
FixedUpdate 与 Update 类似,但有一些关键区别。首先,它是在固定时间步长被调用,这意味着调用之间的时间相同。还重要的是要注意,在调用 FixedUpdate 之后才会进行物理计算。这意味着通常应该在 FixedUpdate 函数中执行基于物理的对象代码修改,除了像跳跃这样的单次事件:
/// <summary>
/// FixedUpdate is a prime place to put physics
/// calculations happening over a period of time.
/// </summary>
void FixedUpdate()
{
// Check if we're moving to the side
var horizontalSpeed = Input.GetAxis("Horizontal") *
dodgeSpeed;
rb.AddForce(horizontalSpeed, 0, rollSpeed);
}
通过调整代码以使用 FixedUpdate,球的运动速度应该更加一致。
注意
更多关于 FixedUpdate 的信息,请查看 docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html。
将所有内容整合在一起
在讨论了所有这些内容之后,我们现在可以拥有脚本的最终版本,其外观如下:
using UnityEngine;
/// <summary>
/// Responsible for moving the player automatically and
/// receiving input.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class PlayerBehaviour : MonoBehaviour
{
/// <summary>
/// A reference to the Rigidbody component
/// </summary>
private Rigidbody rb;
[Tooltip("How fast the ball moves left/right")]
public float dodgeSpeed = 5;
[Tooltip("How fast the ball moves
forward automatically")]
[Range(0, 10)]
public float rollSpeed = 5;
// Start is called before the first frame update
public void Start()
{
// Get access to our Rigidbody component
rb = GetComponent<Rigidbody>();
}
/// <summary>
/// FixedUpdate is a prime place to put physics
/// calculations happening over a period of time.
/// </summary>
void FixedUpdate()
{
// Check if we're moving to the side
var horizontalSpeed = Input.GetAxis("Horizontal") *
dodgeSpeed;
rb.AddForce(horizontalSpeed, 0, rollSpeed);
}
}
我希望你们也同意,这样做可以使代码更容易理解,并且更容易使用。现在,我们可以继续探讨游戏中的其他功能!
让摄像机跟随我们的玩家
目前,我们的摄像机在游戏进行时保持同一位置。这对这个游戏来说效果不佳,因为玩家在游戏进行时会移动。我们可以通过两种主要方式移动我们的摄像机。我们可以直接移动摄像机并使其成为玩家的子对象,但这不会起作用,因为摄像机将具有与球相同的旋转,这会导致摄像机不断旋转,可能会让玩家感到头晕目眩。因此,我们可能希望使用脚本来移动它。幸运的是,我们可以相当容易地修改摄像机观察事物的方式,所以让我们继续修复它:
-
前往项目窗口并创建一个新的 C# 脚本,命名为
CameraBehaviour。从那里,使用以下代码:using UnityEngine; /// <summary> /// Will adjust the camera to follow and face a target /// </summary> public class CameraBehaviour : MonoBehaviour { [Tooltip("What object should the camera be looking at")] public Transform target; [Tooltip("How offset will the camera be to the target")] public Vector3 offset = new Vector3(0, 3, -6); /// <summary> /// Update is called once per frame /// </summary> private void Update() { // Check if target is a valid object if (target != null) { // Set our position to an offset of our // target transform.position = target.position + offset; // Change the rotation to face target transform.LookAt(target); } } }
此脚本将设置附加到其上的对象的位置为目标位置加上偏移量。之后,它将改变对象的方向以面对目标。这两个参数都被标记为 public,因此可以在 检查器 窗口中进行调整。
-
保存脚本并返回 Unity 编辑器。选择
CameraBehaviour组件。您可以通过将脚本从 项目 窗口拖放到 GameObject 上,或者通过在 检查器 窗口的底部点击 添加组件 按钮来实现,输入我们组件的名称,然后点击 Enter 确认。 -
之后,将
Player对象从 层次结构 窗口拖放到 检查器 窗口中脚本的 目标 属性:

图 1.14 – CameraBehaviour 组件设置
- 保存场景并播放游戏:

图 1.15 – 游戏当前状态
现在摄像机跟随玩家移动。您可以随意调整变量,看看它如何影响摄像机的视觉效果,以获得您最喜欢的项目感觉。在此之后,我们可以为球设置一个移动的目标,我们将在下一节中介绍。
创建基本瓦片
我们希望我们的游戏是无限的,但为了实现这一点,我们需要有可以生成来构建我们环境的部件;让我们现在就做吧:
-
要开始,我们首先需要为我们的跑步游戏创建一个可重复的单个部件。为此,我们将在已有的地板上添加一些墙壁。从
Left Wall。 -
修改
1、2、10)。从那里,通过点击工具覆盖层上的带有箭头的按钮或按 W 键来选择 移动 工具。
注意
Unity 最近的添加是 叠加层 的概念,它已经取代了原始的工具栏。有关它们的信息以及如何使用它们,请查看 docs.unity3d.com/2022.1/Documentation/Manual/overlays.html。
关于 Unity 内置快捷键的更多信息,请查看docs.unity3d.com/Manual/UnityHotkeys.html。
-
我们希望这堵墙与地板匹配,所以按住 V 键进入 顶点吸附 模式。在顶点吸附模式下,我们可以选择网格上的任何顶点,并将其移动到另一个对象上的另一个顶点的相同位置。这对于确保物体之间没有空洞非常有用。
-
在开启顶点吸附模式的情况下,选择内边缘并拖动,直到它碰到地板的边缘。或者,你可以设置(
3,0.95,0):

图 1.16 – 左墙设置
注意
关于在场景中移动对象的信息,包括更多关于顶点吸附模式的细节,请查看docs.unity3d.com/Manual/PositioningGameObjects.html。
- 然后,复制这个墙壁,并将另一个对象放在另一边(
-3,0.95,0),命名为Right Wall:

图 1.17 – 右墙设置
如前一张截图所示,我们现在保护玩家不会从游戏区域的左右边缘掉落。由于墙壁的设置方式,如果我们移动Floor对象,墙壁也会移动。
注意
关于移动 Unity 相机或导航到场景视图的信息,请查看docs.unity3d.com/Manual/SceneViewNavigation.html。
游戏设计的方式是,球滚过单个瓦片后,我们就不再需要它在那里了。如果我们只是让它留在那里,由于游戏环境中使用了这么多需要内存的东西,游戏的速度会随着时间的推移而变慢,所以移除不再使用的资源是个好主意。我们还需要有某种方法来确定何时生成新的瓦片以继续玩家可以走的路径。
-
现在,我们还想知道这个部分在哪里结束,所以我们将添加一个带有触发碰撞器的对象。选择
Tile End。 -
然后,我们将添加一个(
7,2,1)以适应玩家可以行走的区域的大小。注意,有一个绿色框围绕该空间,显示碰撞可以发生的位置。将(0,1,10)设置为超过瓦片的末端。最后,检查触发器属性,以便碰撞引擎将碰撞器转换为触发器,这样当它被击中时可以运行代码事件,但不会阻止玩家通过它:

图 1.18 – 标题
如我之前简要提到的,这个触发器将被用来告诉游戏我们的玩家已经走过了这个方块。由于我们希望在玩家通过摄像机所能看到的范围之前仍然能看到方块,所以这个触发器被放置在方块之后。我们将告诉引擎从游戏中移除这个方块,但关于这一点我们将在本章后面详细讨论。
-
现在我们已经创建了所有对象,我们希望将我们的对象组合在一起作为一个可以创建副本的整体。为此,让我们通过访问
Basic Tile创建一个空的 GameObject 实例。设置0、0、0)。 -
然后,转到层次结构窗口,将地板、方块末端、左墙和右墙对象拖放到其上方,使它们成为基本 方块对象的子对象。
-
目前,摄像机可以看到方块的开始,为了解决这个问题,让我们将
0、0、-5)设置为。如您在下面的屏幕截图中所见,现在整个方块将移回:

图 1.19 – 将方块移回
- 最后,我们需要知道应该在什么位置生成下一个部件,因此通过访问
Next Spawn Point创建另一个空的 GameObject,并设置其0、0、5)。
注意
注意,当我们修改一个有父对象的对象时,位置是相对于父对象,而不是其世界位置。
如您所见,生成点位置现在将在我们当前标题的边缘:

图 1.20 – 下一个生成点位置
- 现在我们有一个完全完成的单个方块。而不是手动多次复制它,我们将利用 Unity 的预制件概念。预制件,或预制对象,是 GameObject 和组件的蓝图,我们可以将其转换为文件,从而可以复制。预制件还有其他一些有趣的功能,但我们将随着使用它们来讨论它们。
从预制件。然后,将基本方块对象从层次结构窗口拖放到预制件文件夹内的项目窗口中。如果基本方块名称在层次结构窗口中的文本变为蓝色,我们将知道它已经被正确创建:

图 1.21 – 基本方块预制件创建
我们现在有一个可以创建副本以扩展我们的环境的方块预制件。
使其无限
现在我们已经打下基础,让我们这样设置,以便我们可以继续运行而不是在短时间内停止,通过在彼此前面生成这个基本方块的副本:
-
首先,我们有我们的预制件,因此我们可以通过选择它并按删除键来删除基本方块在层次结构窗口中的原始基本方块。
-
我们需要一个地方来创建所有这些地砖,并可能管理游戏的信息,例如玩家的分数。在 Unity 中,这通常被称为
GameManager。从GameManager。 -
在你的 IDE 中打开脚本并使用以下代码:
using UnityEngine; /// <summary> /// Manages the main gameplay of the game /// </summary> public class GameManager : MonoBehaviour { [Tooltip("A reference to the tile we want to spawn")] public Transform tile; [Tooltip("Where the first tile should be placed at")] public Vector3 startPoint = new Vector3(0, 0, -5); [Tooltip("How many tiles should we create in advance")] [Range(1, 15)] public int initSpawnNum = 10; /// <summary> /// Where the next tile should be spawned at. /// </summary> private Vector3 nextTileLocation; /// <summary> /// How should the next tile be rotated? /// </summary> private Quaternion nextTileRotation; /// <summary> /// Start is called before the first frame update /// </summary> private void Start() { // Set our starting point nextTileLocation = startPoint; nextTileRotation = Quaternion.identity; for (int i = 0; i < initSpawnNum; ++i) { SpawnNextTile(); } } /// <summary> /// Will spawn a tile at a certain location and /// setup the next position /// </summary> public void SpawnNextTile() { var newTile = Instantiate(tile, nextTileLocation, nextTileRotation); // Figure out where and at what rotation we /// should spawn the next item var nextTile = newTile.Find("Next Spawn Point"); nextTileLocation = nextTile.position; nextTileRotation = nextTile.rotation; } }
此脚本将根据 tile 和 initSpawnNum 属性生成一系列地砖。
- 保存你的脚本并回到 Unity。从那里,创建一个新的空 GameObject 并命名为
Game Controller,如果需要的话,为了组织目的可以重置位置。将其拖放到 Hierarchy 窗口的顶部。为了清晰起见,如果你想的话,可以重置位置。然后,将 Game Manager 脚本附加到对象上,并通过拖放从 Project 窗口中的 Basic Tile 预制件到 Tile 槽来设置 Tile 属性:

图 1.22 – 分配 Tile 属性
- 保存你的场景并运行项目:

图 1.23 – 游戏的当前状态
太好了,但现在我们将在这些之后创建新的对象,我们不想一次性创建大量的这些对象。一旦我们到达地砖的尽头,最好是创建一个新的地砖并移除它。我们稍后会进一步优化这一点,但这样,在任何给定时间内,游戏中总是有大约相同数量的地砖。
-
进入
TileEndBehaviour,使用以下代码:using UnityEngine; /// <summary> /// Handles spawning a new tile and destroying this /// one upon the player reaching the end /// </summary> public class TileEndBehaviour : MonoBehaviour { [Tooltip("How much time to wait before destroying " + "the tile after reaching the end")] public float destroyTime = 1.5f; private void OnTriggerEnter(Collider other) { // First check if we collided with the player if(other.gameObject.GetComponent <PlayerBehaviour>()) { // If we did, spawn a new tile var gm = GameObject.FindObjectOfType <GameManager>(); gm.SpawnNextTile(); // And destroy this entire tile after a // short delay Destroy(transform.parent.gameObject, destroyTime); } } } -
现在,为了将其分配给预制件,我们可以转到 Project 窗口,然后进入 Prefabs 文件夹。从那里,双击 Basic Tile 对象以打开其编辑器。从 Hierarchy 选项卡,选择 Tile End 对象,然后向其添加一个 Tile End Behaviour 组件:

图 1.24 – 添加 Tile End Behaviour
- 点击预制件名称旁边的左箭头以返回基本场景:

图 1.25 – 左箭头位置
提示
你也可以通过从 Project 窗口选择预制件对象,转到 Inspector 选项卡,并点击 Open Prefab 按钮来打开预制件编辑器。
- 保存你的场景并播放。现在你会注意到,随着玩家继续移动,新的地砖会随着你的移动而生成;如果你在播放时切换到 Scene 选项卡,你会看到球通过地砖时,它们会自行销毁:

图 1.26 – 地砖自动被销毁
这样可以确保玩家面前有地砖可以访问!但当然,这只是一个无休止的直线。在下一节中,我们将看到如何使游戏变得更加有趣。
创建障碍物
我们有一些基本的瓦片是很好的,但给玩家一些事情做,或者在我们的案例中,一些需要避免的事情是个好主意。这将给玩家提供某种挑战和基本的游戏目标,即避免这里的障碍物。在本节中,你将学习如何自定义你的瓦片,为玩家添加需要避免的障碍物。所以,让我们看看步骤:
-
就像我们为基本瓦片创建预制体一样,我们将通过代码创建一个单独的障碍物。我想让它容易看到障碍物在世界上看起来是什么样子,并确保它不是太大,所以我将再次将基本瓦片预制体拖回到世界中。
-
接下来,我们将通过访问
Obstacle来创建一个立方体。将2改为1,并将其放置在平台上方(0,1,0.25):

图 1.27 – 添加障碍物
- 然后,我们可以玩游戏来查看它的工作方式:

图 1.28 – 障碍物阻止玩家
-
如前一个屏幕截图所示,玩家被阻止,但并没有真正发生什么。在这种情况下,我们希望玩家在碰到这个障碍物时失去游戏,然后重新开始游戏;为此,我们需要编写一个脚本。从
ObstacleBehaviour中,我们将使用以下代码:using UnityEngine; using UnityEngine.SceneManagement; // LoadScene public class ObstacleBehaviour : MonoBehaviour { [Tooltip("How long to wait before restarting the game")] public float waitTime = 2.0f; private void OnCollisionEnter(Collision collision) { // First check if we collided with the player if (collision.gameObject.GetComponent <PlayerBehaviour>()) { // Destroy the player Destroy(collision.gameObject); // Call the function ResetGame after // waitTime has passed Invoke("ResetGame", waitTime); } } /// <summary> /// Will restart the currently loaded level /// </summary> private void ResetGame() { // Get the current level's name string sceneName = SceneManager.GetActiveScene().name; // Restarts the current level SceneManager.LoadScene(sceneName); } } -
保存脚本并返回到编辑器,将脚本附加到我们刚刚创建的
Obstacle游戏对象。 -
保存你的场景并尝试游戏:

图 1.29 – 障碍物摧毁玩家
如前一个屏幕截图所示,一旦我们碰到障碍物,玩家就会被摧毁,然后几秒钟后,游戏重新开始。你将学习如何使用粒子系统和其他东西来完善它,但到目前为止,它已经可以正常工作,这正是我们想要的。
- 现在我们知道它工作得很好,我们可以将其制作成一个预制体。就像我们最初创建瓦片一样,将障碍物对象从层次结构拖放到项目选项卡中的预制体文件夹:

图 1.30 – 创建障碍物预制体
-
接下来,我们将移除障碍物对象,因为我们将在创建瓦片时生成它。为此,在层次结构窗口中选择障碍物对象,然后按删除键。
-
我们将制作标记来指示我们可能希望生成障碍物的地方。展开
Next Spawn Point(0,1,4)。然后我们将对象重命名为Center。 -
之后,为了帮助在场景窗口中看到对象,转到检查器窗口,点击灰色立方体图标,然后在选择图标菜单中,选择你想要的任何颜色选项(我选择了蓝色)。完成此操作后,你会发现如果我们靠近对象,我们可以在编辑器中看到文本(但默认情况下它不会在游戏选项卡中显示):

图 1.31 – 创建中心标记
- 我们希望有一种方法来获取我们将来可能需要的所有潜在生成点,以防我们决定扩展项目,因此我们将分配一个标签作为引用,以便更容易找到这些对象。为此,在
ObstacleSpawn的顶部:

图 1.32 – 创建障碍物生成标签
- 返回并选择中心对象,将标签属性分配给障碍物生成:

图 1.33 – 为中心对象分配标签
注意
有关标签及其使用原因的更多信息,请参阅docs.unity3d.com/Manual/Tags.html。
- 继续复制两次,并将其他名称分别改为
Left和Right,将它们移动到中心左侧和右侧两个单位,以成为其他可能的障碍物点:

图 1.34 – 创建左右标记
- 注意,默认情况下,这些更改不会影响原始预制件;这就是为什么对象目前显示为黑色文本。要实现这一点,请选择基本瓦片,然后在检查器窗口下的预制件部分,点击覆盖并选择应用全部:

图 1.35 – 应用预制件更改
-
现在预制件设置正确后,我们可以通过在层次窗口中选择它并按下Delete键来继续移除它。
-
然后,我们需要进入
GameManager脚本并做一些修改。首先,我们需要引入一些新变量:/// <summary> /// Manages the main gameplay of the game /// </summary> public class GameManager : MonoBehaviour { [Tooltip("A reference to the tile we want to spawn")] public Transform tile; [Tooltip("A reference to the obstacle we want to spawn")] public Transform obstacle; [Tooltip("Where the first tile should be placed at")] public Vector3 startPoint = new Vector3(0, 0, -5); [Tooltip("How many tiles should we create in advance")] [Range(1, 15)] public int initSpawnNum = 10; [Tooltip("How many tiles to spawn with no obstacles")] public int initNoObstacles = 4;
这些变量中的第一个是我们将要创建副本的障碍物的引用。第二个是生成障碍物前应该生成多少个瓦片的参数。这是为了确保玩家在需要躲避之前可以看到障碍物。
-
然后,我们需要修改
SpawnNextTile函数以生成障碍物:/// <summary> /// Will spawn a tile at a certain location and setup /// the next position /// </summary> /// <param name="spawnObstacles">If we should spawn an /// obstacle</param> public void SpawnNextTile(bool spawnObstacles = true) { var newTile = Instantiate(tile, nextTileLocation, nextTileRotation); // Figure out where and at what rotation we should // spawn the next item var nextTile = newTile.Find("Next Spawn Point"); nextTileLocation = nextTile.position; nextTileRotation = nextTile.rotation; if (spawnObstacles) { SpawnObstacle(newTile); } }
注意,我们修改了SpawnNextTile函数,现在它有一个默认参数设置为true,这将告诉我们是否想要生成障碍物。在游戏开始时,我们可能不希望玩家立即开始躲避,但我们可以调整这个值来增加或减少我们使用的数量。因为它有一个默认值true,所以在Start函数中调用它的原始版本仍然可以正常工作而不会出错,但稍后我们将对其进行修改。
-
在这里,我们询问值是否为
true以调用名为SpawnObstacle的函数,但这个函数还没有编写。我们将在下一部分添加它,但首先,我们将使用List类,并确保编译器知道我们指的是哪个List类,因此需要在文件顶部添加一个using语句:using UnityEngine; using System.Collections.Generic; // List -
现在我们可以编写
SpawnObstacle函数。将以下函数添加到脚本中:private void SpawnObstacle(Transform newTile) { // Now we need to get all of the possible places // to spawn the obstacle var obstacleSpawnPoints = new List<GameObject>(); // Go through each of the child game objects in // our tile foreach (Transform child in newTile) { // If it has the ObstacleSpawn tag if (child.CompareTag("ObstacleSpawn")) { // We add it as a possibility obstacleSpawnPoints.Add(child.gameObject); } } // Make sure there is at least one if (obstacleSpawnPoints.Count > 0) { // Get a random spawn point from the ones we // have int index = Random.Range(0, obstacleSpawnPoints.Count); var spawnPoint = obstacleSpawnPoints[index]; // Store its position for us to use var spawnPos = spawnPoint.transform.position; // Create our obstacle var newObstacle = Instantiate(obstacle, spawnPos, Quaternion.identity); // Have it parented to the tile newObstacle.SetParent(spawnPoint.transform); } } -
最后,让我们更新
Start函数:/// <summary> /// Start is called before the first frame update /// </summary> private void Start() { // Set our starting point nextTileLocation = startPoint; nextTileRotation = Quaternion.identity; for (int i = 0; i < initSpawnNum; ++i) { SpawnNextTile(i >= initNoObstacles); } }
现在,只要i小于initNoObstacles变量的值,就不会生成变量,实际上为我们提供了一个可以调整四个格子的缓冲区,通过更改initNoObstacles变量来调整。
- 保存脚本并返回 Unity 编辑器。然后,在检查器窗口中将游戏管理器(脚本)组件的
Obstacle变量分配为我们之前创建的障碍物预制件:

图 1.36 – 分配障碍物属性
-
由于默认的光照设置,目前很难看清东西,所以让我们转到层次结构窗口并选择方向光对象。方向光的作用类似于地球上的太阳,从某个位置向四面八方发光。
-
使用默认设置时,灯光有点太亮,阴影默认太暗,所以先调整到
0.5,然后再次调整到0.5:

图 1.37 – 调整方向光
- 保存你的场景并玩游戏:

图 1.38 – 游戏当前状态
如前一个截图所示,我们现在有了一些玩家需要避免的障碍物!
注意
关于方向光和 Unity 拥有的其他光照类型的信息,请查看unity3d.com/learn/tutorials/topics/graphics/light-types?playlist=17102。
摘要
那就是了!一个坚实的基础——但仅仅是基础。然而,话虽如此,我们在本章中讨论了大量的内容。我们讨论了如何在 Unity 中创建新项目,并构建了一个可以持续移动并接受水平移动输入的玩家。然后,我们讨论了如何使用 Unity 的属性和 XML 注释来提高我们的代码质量并帮助我们团队合作。我们还介绍了如何拥有移动的摄像头。我们创建了一个基于瓦片的关卡设计系统,在游戏进行中创建新的瓦片,随机生成玩家需要避免的障碍物。
在整本书中,我们将探讨更多可以用来改进这个项目并使其更加完善的方法,同时将其改为在移动平台上提供最佳体验。然而,在我们到达那里之前,我们实际上需要弄清楚如何部署我们的项目,这就是我们在下一章将要工作的内容。
第二章:Android 和 iOS 开发项目设置
我们现在有一个项目可以开始,但当前,它是针对在 PC 上玩而构建的。然而,由于本书是关于移动开发的,在我们进一步深入之前,让游戏在设备本身上运行非常重要。
在本章中,我们将介绍我们需要执行的所有设置,以便将项目部署到我们的移动设备。在撰写本书时,移动开发通常是针对 Android 或 iOS 进行的,因此我们将涵盖这两个平台。
本章将分为几个主题。整个章节本身将是一个从开始到结束的简单步骤过程。以下是我们将执行的任务列表:
-
介绍构建 设置菜单
-
为 PC 构建项目
-
将您的项目导出到 Android
-
使用模拟器运行 Android APK
-
将项目放在您的 Android 设备上
-
Unity iOS 安装和 Xcode 设置
-
为 iOS 构建项目
-
通过 iOS 模拟器运行项目
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需做最小改动即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您也可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
如果您想部署到 Android 设备,您可以使用 macOS、Linux 或 Windows,并且根据您希望使用的功能,您可以将游戏以某种方式导出,以便在 Android 5.1 Lollipop 及以上版本上运行应用程序。
注意
更多关于支持的不同类型 Android 版本的信息,请查看docs.unity3d.com/ScriptReference/AndroidSdkVersions.html。
要为 iOS 设备开发,除了设备本身运行 iOS 12 或更高版本外,您还需要在运行 OS X 10.13 High Sierra 或更高版本的 Intel Macintosh 和运行 Apple 硅的 Macintosh 上做一些工作。我将使用 12.3.1 macOS Monterey。如果您没有,您可以使用 Windows 开发您的游戏,当您想发布游戏时,将您的项目带到 Macintosh 上进行最终导出。
注意
使用 Windows 构建 iOS 应用还有一些其他潜在的方法,但它们不在这个书籍的范围内。一个可能的选择是使用 Unity 的 CI/CD 云构建自动化与部署工具服务,该服务会自动创建你游戏的不同版本。有关更多信息,请查看unity.com/solutions/ci-cd.
另一个潜在的选择可能是通过云服务租用一台 Macintosh 电脑来自行构建。有关该选项和其他潜在选项的更多信息,请查看mindster.com/how-develop-ios-apps-windows/.
你可以在 GitHub 上找到本章的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter02.
介绍构建设置菜单
在开发过程中,有时你可能想看看你的游戏在编辑器外部的样子。这可以给你一种成就感。我知道我第一次将构建版本推送到控制台开发工具包时就有这种感觉。无论是 PC、Macintosh、Linux、网络玩家、移动设备还是控制台,我们都必须通过相同的菜单——构建设置菜单:
- 首先,打开我们在第一章“构建你的游戏”中创建的项目。此外,打开我们创建的场景(
SampleScene.unity,位于Scenes文件夹中):

图 2.1 – SampleScene 文件
-
由于场景是我们的游戏玩法,让我们首先在
SampleScene对象中的Scenes文件夹中打开文件,选择Gameplay,然后按Enter键提交更改。Unity 将询问你是否想要重新加载场景。通过点击重新加载来完成此操作。 -
从这里,我们将通过选择文件 | 构建设置来打开构建设置菜单:
小贴士
你也可以通过按Ctrl + Shift + B或Command + Shift + B来打开菜单。

图 2.2 – 构建设置菜单
在前面的屏幕截图中,你会注意到构建设置菜单已经出现。此菜单包含三个部分:
-
构建中的场景(顶部):此窗口包含我们希望在构建项目时包含的场景。这确保了除非指定,否则测试级别等事物不会被包含。
-
平台(左下角):这是一个你可以导出游戏的平台列表。Unity 标志出现在你正在编译的当前平台上。要更改平台,你需要从列表中选择它,然后点击列表下方的切换平台按钮。
-
选项(右下角):在 平台 部分的右侧,你会看到一些可以根据你想要构建的方式进行调整的设置,某些选项会根据你将要工作的平台而改变。
- 默认情况下,我们的构建中没有场景,所以让我们继续更改它。点击列表中索引为 0 的
Scenes/Gameplay级别,这意味着当你的游戏运行时,这个级别将是第一个加载的:

图 2.3 – 将游戏玩法场景添加到构建中的场景
现在我们已经了解了构建设置的工作原理,让我们看看如何为 PC 构建项目,以便在继续构建我们的移动游戏之前了解一般情况。
注意
你还可以通过从 项目 窗口拖放场景到 构建中的场景 部分添加场景。你也可以拖动场景以按你希望的顺序重新排列它们。
为 PC 构建项目
默认情况下,我们的平台设置为 Windows, Mac, Linux。为了验证一切是否正常工作,让我们先在我们的平台上运行游戏,然后再转向移动设备:
-
要开始,我们将选择 构建 选项。在我的情况下,我将把项目导出到 Windows,但这个过程对于 macOS 和 Linux 也是类似的。
-
完成此操作后,将弹出一个窗口,要求输入游戏名称和放置游戏的位置。我将创建一个新
Export文件夹,位于包含Assets和Library的同一文件夹中,因此它不会显示在 项目 窗口中,但将位于我的项目文件夹中:

图 2.4 – 导出文件夹
- 点击 选择文件夹 并等待其完成。一旦完成,应该会出现以下窗口:

图 2.5 – 创建的文件夹
我们有可执行文件,但还有一个包含我们应用程序所有资源的数据文件夹(目前称为 MobileDev_Data)。你必须包含数据文件夹以及与你的游戏一起创建的其他文件,否则它将无法运行。
如果你为 Mac 构建项目,它将捆绑应用程序和数据,所以一旦你导出游戏,你只需要提供应用程序即可。
- 如果你双击
.exe文件来运行游戏,你将被带到正确的游戏屏幕,如下面的截图所示:

图 2.6 – 运行游戏
这样一来,我们应该能够像平时一样控制和玩游戏。这真是太好了!
小贴士
你将需要使用 Alt + F4(在 Macintosh 上为 Command + Q)来退出游戏,并且你可以通过按 Alt + Enter 切换到窗口模式。
现在我们已经讨论了构建项目的通用方法,让我们深入了解不同平台的具体情况。在下一节中,我们将讨论如何将我们的项目上传到 Android 设备。
为 Android 导出项目
现在我们已经完成了所有设置,我们可以用我们的项目打开 Unity 并将其导出为 Android 设备。在本节中,我们首先检查是否已安装 Android 构建支持,然后我们将更新构建和玩家设置以导出我们的项目。那么,让我们开始吧。
为 Unity 安装 Android 构建支持
首先,如果你还没有这样做,你需要在安装 Unity 时选择将Android 构建支持作为选项。如果你已经安装了它,你可以跳过这一部分。如果你在初始安装时没有安装它,我们将在以下步骤中介绍安装过程:
-
关闭 Unity 编辑器,打开 Unity Hub 并选择安装部分。
-
从那里,点击你当前 Unity 版本右侧的齿轮图标,并选择添加模块选项:

图 2.7 – 选择添加模块选项
- 选择Android 构建支持选项,这也应该选中Android SDK 和 NDK 工具和OpenJDK选项。之后,点击继续按钮:

图 2.8 – 检查 Android 构建支持选项
- 你将被带到许可条款页面。阅读它,如果你同意,勾选协议框并点击安装按钮:

图 2.9 – 来自谷歌的 Android SDK 和 NDK 许可条款
等待安装完成。一旦完成,你应该会在安装底部的位置看到 Android 标志:

图 2.10 – 新增的平台
这意味着 Android 构建支持现在已添加到我们的 Unity 版本中,我们可以在那里构建项目。接下来,我们将了解如何为 Android 构建项目以及所需的设置。
更新 Android 项目的构建和玩家设置
现在我们有了 Android 支持,让我们再次打开我们的项目并更改我们正在开发的平台:
-
在这一点上,我们将深入 Unity,并通过转到文件 | 构建设置来再次进入我们的构建设置菜单。
-
从平台列表中选择Android选项,然后点击切换平台按钮进行更改:

图 2.11 – 切换平台按钮的位置
注意,这将使 Unity 重新导入我们游戏中的所有资产,因此在开始构建较大的项目时可能会耗时较长。一旦完成,你应该会注意到 Unity 标志现在位于Android选项旁边,表示这是要构建的平台:

图 2.12 – 切换到 Android 平台
- 现在,为了能够构建我们的项目,我们必须为我们的游戏设置捆绑标识符,这是一个标识应用的字符串。它写法类似于反向的 URL,例如,
com.yourCompanyName.yourGameName。要修改这个,我们需要打开玩家设置菜单,我们可以通过点击构建设置菜单左下角的玩家设置…按钮或通过进入编辑 | 项目设置 | 玩家来访问它。你会注意到菜单作为一个新窗口出现:

图 2.13 – 玩家设置菜单
现在我们处于Android模式(注意 Unity 编辑器标题栏上的文本),我们可以更改这些属性:

图 2.14 – Android 模式
-
我们将在后面的章节中讨论更多这些内容,但现在,将你的
JohnPDoran更改。 -
然后向下滚动,直到你到达其他设置选项。从那里,你会看到包名属性正在使用我们为公司名称和产品名称属性设置的任何内容,但如果我们想通过勾选覆盖默认包名属性来自定义它,我们必须确保它与默认值不同。
此外,还有一个最小 API 级别选项;确保你的选项设置为与你的手机相同的版本或更早的版本,具体取决于你想要支持什么。注意,你越早使用,可访问的功能就越少,但你的项目将能够支持更多的手机:

图 2.15 – 玩家设置调整
- 关闭我们之前创建的
Export文件夹。它将询问你希望文件具有的名称。我将使用MobileDev,因为我们之前这样做过,因为它将创建一个.apk文件而不是.exe文件:

图 2.16 – 创建的 Android APK
稍等片刻,一旦完成,你应该会在文件夹中找到一个新的.apk文件。当然,如果我们不能将其放在我们的实际手机上,那么仅仅拥有 APK 文件并没有什么作用,所以在下一个小节中,我们将使我们的手机能够在我们设备上测试游戏。
使用模拟器运行 Android APK
虽然测试 Android 游戏的最佳方式是在实际的 Android 设备上,但也可以在模拟器上测试游戏,这是一种我们可以让计算机运行为 Android 设备创建的软件的中介。在撰写本文时,有几个 Android 模拟器适用于 Windows,但我最有成功经验的是名为LDPlayer的一个轻量级且快速的 Android 模拟器;然而,由于它是免费的,所以包含广告。对于使用英特尔处理器的 Macintosh 用户,您可以使用BlueStacks (www.bluestacks.com/download.html),但在撰写本文时,在由 Apple 硅芯片驱动的 Macintosh 上没有可用的 Android 模拟器。本节完全可选;如果您更愿意在实际设备上测试,请跳到将项目放在您的 Android 设备上或Unity for iOS 设置和 Xcode 安装部分。
要在模拟器上测试游戏,请按照以下步骤操作:
- 从您的网页浏览器,访问
www.ldplayer.net/。从那里,点击下载 LDPlayer 9 按钮:

图 2.17 – LDPlayer 网站
- 下载程序后,打开安装程序,完成标准安装过程,并在安装完成后点击立即尝试按钮:

图 2.18 – 欢迎使用 LDPlayer 菜单
-
从欢迎屏幕,点击欢迎使用 LDPlayer菜单右上角的X。
-
将您的
Export文件夹中的MobileDev.apk文件拖放到 LDPlayer 菜单。如果一切顺利,您应该在屏幕上看到MobileDev图标:

图 2.19 – 游戏已添加到主屏幕
- 从那里,您可以点击图标开始游戏!

图 2.20 – 游戏正在模拟设备上运行
如您所见,游戏在模拟设备上运行得非常完美!我们可以在完成游戏后,从MobileDev标签页点击X来退出。现在我们已经看到了在模拟器上运行游戏是多么简单,让我们看看如何在实际设备上操作!
将项目放在您的 Android 设备上
以下步骤可能因您的 Android 版本和具体手机而异,但这里有一套通用的步骤,以便能够将我们的 Android 应用到设备上:
- 在您的 Android 设备上,您需要进入您的设置应用。从那里选择应用部分:

图 2.21 – 设置 | 应用位置
- 从那里,向下滚动直到到达特殊应用访问部分或类似部分,然后点击它进入菜单:

图 2.22 – 特殊应用访问选项
- 在里面,你会看到一个名为 安装未知应用 的部分。选择此选项:

图 2.23 – 安装未知应用选项
- 我们将把我们的应用到设备的文件中上传,因此我们希望从 文件 应用中启用 安装未知应用。选择它,然后从菜单中启用它:

图 2.24 – 启用安装未知应用选项
启用此功能后,你的设备现在可以安装 .apk 文件,但你现在需要将你的游戏移动到设备上以安装它。最简单的方法是通过 USB 将其传输到设备;我们现在就这样做。
小贴士
对于那些不愿意使用 USB 的用户,我建议使用云存储应用,如 Dropbox,上传 .apk 文件,然后从应用中下载并以此方式安装。还有一个名为 ADB 的工具,可以通过 USB 或 Wi-Fi 将文件发送到你的手机。有关该工具以及 Android 构建过程的更多信息,请参阅 docs.unity3d.com/Manual/android-BuildProcess.html。
- 通过 USB 将你的手机连接到电脑。连接后,你的手机将显示一个通知,表明它通过 USB 连接进行充电。点击该通知,将选项更改为 文件传输:

图 2. 25 – USB 设置
- 之后,回到你的电脑,进入 Windows 资源管理器/Finder,然后转到 设备和驱动器 部分;你应该在那里看到你的设备:

图 2.26 – 选择我们的 Android 设备
- 双击你的设备并访问内部共享存储部分。然后,将我们之前制作的
.apk文件拖入此文件夹:

图 2.27 – 将 APK 文件放置在 Android 设备上
- 现在,回到你的手机,打开 文件/文件资源管理器 应用。从那里,向下滚动到菜单底部并选择 内部 存储 选项:

图 2.28 – 内部存储位置
- 从那里,从列出的文件中选择你的
.apk文件:

图 2.29 – 选择应用程序
- 你将需要确认安装。点击 安装 按钮:

图 2.30 – 安装应用
- 您可能会看到一个弹出窗口,提示 Play Protect 无法识别开发者。我们将在第十四章“提交游戏到 App Store”中介绍如何解决这个问题,但现在,请点击安装即可并等待安装完成:

图 2.31 – 安装即可
您的手机可能还会询问您是否希望通过 Play Protect 扫描应用。由于我们在阅读本书的过程中将创建多个项目版本,因此这很可能不是必需的,所以我将选择不发送。
小贴士
当然,我无法为所有设备列出步骤,因为一些设备需要不同的驱动程序或需要执行额外的步骤才能在设备上打开文件。如果这些步骤不起作用,并且您不知道如何将文件传输到您的设备上以及如何访问和添加新文件,请使用您选择的互联网搜索引擎搜索手机名称文件传输,将手机名称替换为您的手机名称。
- 完成后,请点击打开按钮来打开我们的游戏:

图 2.32 – 在 Android 设备上运行的游戏
如您所见,游戏已经安装并正在运行。当然,您目前还不能控制它,还有很多新功能您还不能使用,但这表明您已正确设置了 Android 设备。当然,现在您的游戏已经安装到 Android 设备上,您现在需要将其在 iOS 上运行,我们将在下一节中介绍。
iOS Unity 设置和 Xcode 安装
使用 Android 系统,需要做大量的设置工作,但将游戏构建并安装到设备上则相对轻松,而使用 iOS 系统,设置工作较少,但需要更多精力将游戏实际安装到设备上。
以前,您需要付费的 Apple 开发者许可证才能将游戏安装到 iOS 设备上。尽管将游戏上传到 App Store 仍然需要这个许可证,但您不再需要它来进行测试。请注意,免费选项并不提供所有功能,最显著的是应用内购买(IAPs)和游戏中心;然而,游戏应该在您的设备上运行得很好。当我们在第十三章“构建游戏发布版本”中介绍将项目上传到 App Store 时,我们将介绍如何调整您的项目以反映在 Apple 开发者门户中。
要为 iOS 设备开发,除了设备本身运行 iOS 12 或更高版本外,您还需要在运行 OS X 10.13 High Sierra 或更高版本的 Macintosh 计算机上做一些工作,对于使用 Apple 硅的 Macintosh,则需要 Big Sur 11.0。我将使用 12.3.1 macOS Monterey。就像与 Android 一样,我们还需要在真正进行导出之前做一些设置。现在我们就开始吧:
-
首先,如果你还没有这样做,在安装 Unity 时,你需要将 iOS 构建支持 (*) 作为选项添加。如果你在初始安装时没有安装它,你可以打开 Unity Hub 并选择 安装 部分。
-
从那里,点击你当前 Unity 版本右侧的三个点,并选择 添加 模块 选项:


- 从弹出的菜单中,勾选 iOS 构建 支持 选项:


- 点击 安装 按钮,等待安装完成。一旦完成,你应该会在平台列表中看到一些关于 iOS 支持的文本:


此模块允许你能够导出你的项目到 iOS。由于我主要使用我的 Windows 机器,所以我只添加了 iOS 支持,但你也可以从你的 Macintosh 电脑导出 iOS 和 Android 应用。
- 你还需要安装
Xcode并按 Enter。


-
从那里,你将在页面右上角看到 Xcode 程序。点击它,然后点击 安装/更新 按钮(在我的情况下,是一个云图标)并完成安装过程:
-
你可能需要输入你的 Apple ID 信息;请继续并完成输入,然后等待其完成。
注意
如果你没有 Apple ID,你可以从 appleid.apple.com/ 获取一个。
-
一旦安装了 Xcode,请打开它。你将看到 Xcode 和 iOS SDK 的许可协议;请继续点击 同意。然后它将开始安装它运行所需的组件。
-
当你打开 Xcode 时,你将被带到欢迎屏幕,但我们首先需要做一些设置。从顶部菜单栏,请选择 Xcode | 首选项(或按 Command + ,)。从那里,点击 账户 按钮。这将显示你想要在 Xcode 中使用的所有 Apple ID:

图 2.37 – 账户窗口
- 点击屏幕左下角的 + 图标,然后在询问创建哪种账户时选择 Apple ID:

图 2.38 – 添加 Apple ID 账户
- 从弹出的菜单中,添加你的 Apple ID 信息,你应该会在屏幕上看到它。
如果你选择名称,你将在右侧看到额外的信息,例如你所在的团队。如果你没有加入 Apple 开发者计划,它将只是一个个人团队,但如果你为其付费,你应该也会看到那里有额外的团队。
现在我们已经完成了 iOS 和 Xcode 的设置和安装,让我们继续构建我们的项目。
构建 iOS 项目
虽然与 Android 的工作有一些相似之处,但也有一些非常重要的差异需要注意,所以在阅读本节时请记住。让我们按照以下步骤为 iOS 设备构建项目:
-
在这一点上,我们将进入 Unity(如果需要,将 目标 切换到 MacStandalone)并再次通过转到 文件 | 构建 设置 来进入 构建设置 菜单。
-
从 平台 列表中选择 iOS 选项,然后点击 切换平台 按钮进行更改:

图 2.39 – 从平台列表中选择 iOS 选项
注意,这将使 Unity 重新导入我们游戏中的所有资产,随着项目的不断增大,这可能会消耗更多时间。这也意味着当我们构建项目时,它将创建一个 Xcode 项目而不是一个简单的应用,构建完成后我们需要打开并与之工作。
-
如果我们在为 Android 构建时没有这样做,我们必须修改修改游戏捆绑标识符所需的属性。为此,我们需要打开 玩家设置 菜单,我们可以通过点击 构建设置 菜单中的 玩家设置... 按钮,或者通过转到 编辑 | 项目设置 | 玩家 来访问它。
-
从菜单顶部更改
JohnPDoran。然后向下滚动,直到找到 其他设置 选项,然后您会看到 包名 属性正在使用我们为 公司名称 和 产品名称 属性设置的值,但如果我们想自定义它,可以通过勾选 覆盖默认包名 属性来实现。
注意
如果您在为 Android 构建时已经更改了此属性,它已经完成;无需再次执行此操作。
- 现在,我们可以通过点击我们之前创建的
Export文件夹来尝试构建项目——在这种情况下,我在其中创建了一个新的文件夹,并将其命名为MobileDev_iOS:

图 2.40 – 选择构建位置
小贴士
您可以按向下箭头按钮在弹出的查找器窗口中搜索文件夹。
- 一旦项目构建完成,我们将被带到创建项目的 Finder 窗口位置。从那里,我们可以双击
.xcodeproj文件,在 Xcode 中打开项目:

图 2.41 – 构建项目的 Xcode 项目位置
-
在 Xcode 中,等待所有内容加载完成后,您会在顶部中央控制台中注意到一个带有 ! 的黄色三角形。如果您点击它,您会在左侧看到一些信息。
-
双击屏幕左侧的更新到推荐设置选项,然后在弹出的窗口中点击执行更改按钮:

图 2.42 – 执行更改
- 然后,转到窗口的中间部分,在TARGETS下选择Unity-iPhone选项:

图 2.43 – 选择 Unity-iPhone 选项
-
之后,在签名与能力部分,勾选自动管理签名选项,当弹出窗口出现时点击启用自动。然后,在弹出的窗口中将你的团队分配到你的配置文件中。
-
完成所有前面的步骤后,通过 USB 连接你的手机,当你解锁它时,可能会询问你是否信任此设备;点击信任按钮。在加载所有需要的符号(等待直到顶部中间部分显示就绪)后,在右上角,将通用 iOS 设备更改为你已连接的设备。你可能需要在安装符号后拔掉并重新插入你的手机,以便它能够识别设备。
-
当你点击播放按钮时,会出现一个窗口,显示它将要构建到的设备。一旦你确认所有细节都正确无误,点击运行按钮:

图 2.44 – 播放设置窗口
- 你的手机可能正忙,所以在你能够构建到设备之前可能需要等待一会儿。你可能会得到一个窗口要求你访问你的钥匙链中的密钥访问权限。请点击允许,如果需要的话,输入你的设备密码。你还需要在某个时候解锁你的手机,以便它能够进行安装。
如果一切顺利,你应该会看到你的应用自动开始在设备上播放:

图 2.45 – 在 iOS 设备上播放我们的游戏
这样,我们也在 iOS 端运行了游戏。
- 如果你转到主屏幕,你应该会注意到应用现在已经在你的 iOS 设备上了,如下面的截图所示:

图 2.46 – iOS 应用的定位
注意
以以下方式构建时,应用只能运行有限的时间,可能最多一周,如果没有付费许可。如果你的游戏立即崩溃,而之前运行正确,这很可能是原因。在修改你的实际项目之前,重新部署设备以检查是否是这个问题。
通过 iOS 模拟器运行项目
就像我们能够在 PC 上使用 Android 模拟器来播放我们游戏项目的模拟版本一样,通过使用 iOS 模拟器,在 iOS 上也可以做同样的事情:
- 在 Unity 编辑器中,转到玩家设置。从那里,转到其他设置部分,将目标 SDK属性从设备 SDK更改为模拟器 SDK:

图 2.47 – 设置目标 SDK 为模拟器 SDK
- 重新构建项目。这次我创建了一个新文件夹来指定这次构建是为了 iOS 模拟器:

图 2.48 – 选择构建文件夹
- 打开新项目,然后从那里,在上面的部分,你现在会看到一个模拟器设备被选中,而不是之前构建中给出的任何 iOS 设备选项。你还可以点击选项以打开一个下拉列表,你可以从中选择你想要构建的特定设备:

图 2.49 – 选择模拟器设备
- 一旦你选择了设备,按下播放按钮,等待项目构建完成。如果一切顺利,你应该能看到模拟器打开,然后你可以玩游戏:

图 2.50 – 在模拟器上玩游戏
有了这些,我们已经看到了在需要时在模拟器中构建我们的项目是多么容易;只需确保在为实际设备构建时将目标 SDK属性切换回设备 SDK。
摘要
我们现在已经在 Android 和 iOS 设备上运行我们的游戏,并且我们已经学会了每次我们想要在这些设备上部署我们的游戏时需要采取的步骤。
虽然我将在我们到达第十三章**,构建我们的游戏发布副本之前不再写关于将内容导出到这两种设备的内容,但了解我们将要做的更改如何与这两个平台一起工作,并在每个平台上继续测试,以确保你的项目正确运行,并且帧率是你可接受的,这是一个好主意。
这一点特别需要注意,因为通过编辑器或模拟器在你的 PC 上运行项目并不总是能准确地代表游戏在不同设备上的运行情况。因此,你可能会发现,在移动设备上运行良好的游戏在你的电脑上可能会出现卡顿。问题是,除非你总是检查设备上的游戏,否则你不会知道,所以我强烈建议你这样做。
我们的游戏现在在移动设备上运行,但由于我们编写的输入代码,它不会对我们在设备上所做的任何事情做出反应。在下一章中,我们将探讨我们如何向我们的项目添加输入,以及我们需要考虑的不同形式的输入如何改变我们的游戏的设计。
第二部分:移动特定功能
本书本部分将专注于在 Unity 项目中开发特定于移动的功能。到本部分结束时,你将拥有创建一个功能丰富、用户界面精美、集成了货币化和社交媒体功能的移动游戏的全部知识。
本部分包含以下章节:
-
第三章, 移动输入/触摸控制
-
第四章, 分辨率无关的用户界面
-
第五章, 高级移动用户界面
-
第六章, 实现应用内购买
-
第七章, 使用 Unity Ads 进行广告投放
-
第八章, 将社交媒体整合到我们的项目中
第三章:移动输入/触摸控制
玩家如何与你的项目互动可能是其中最重要的正确做法之一。虽然所有项目都会添加玩家输入,无论你使用什么平台,但这是一个可以使你的移动游戏成功或失败的区域。
如果实施的控制不适合你正在制作的游戏,或者如果控制感觉笨拙,玩家不会长时间地玩你的游戏。虽然许多人认为 Rockstar 的《侠盗猎车手》系列游戏在游戏机和 PC 上表现良好,但在移动设备上玩游戏由于屏幕上的所有按钮以及摇杆被虚拟版本取代(这些虚拟版本在提供触觉反馈方面与其他平台不同),因此会提供更大的入门障碍。
通常表现良好的移动和桌面游戏通常具有简单易用的控制,尽可能多地找到简化游戏玩法的方法。许多流行的游戏只需要单一输入,例如 Dong Nguyen 的Flappy Bird和 Ketchapp 的Ballz。
与传统游戏(如手势和捏合)相比,游戏与移动设备交互的方式有很多不同,我们将在本章中探讨其中的一些。
在本章中,我们将介绍移动设备上输入的不同工作方式。我们将从已经内置到我们项目中的输入开始,使用鼠标,然后转向触摸事件、手势、使用加速度计以及通过Touch类访问信息。
本章将分为多个主题。它将包含从开始到结束的简单、分步的过程。以下是我们的任务大纲:
-
使用鼠标输入
-
通过触摸移动
-
使用 Unity Remote
-
实现手势
-
使用捏合缩放玩家
-
使用加速度计
-
检测游戏对象上的触摸
技术要求
本书使用Unity 2022.1.0b16和Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小改动即可工作。如果你想要下载本书中使用的确切版本(并且有新版本发布),你可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。
你也可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
你可以在 GitHub 上找到本章中存在的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter03。
使用鼠标输入
在我们深入探讨仅适用于移动设备的解决方案之前,我想指出,通过使用鼠标控制,可以编写既适用于移动设备又适用于 PC 的输入。移动设备支持使用鼠标点击作为屏幕上的轻触,轻触/点击的位置是手指按下的地方。这种输入形式仅提供触摸发生的位置,并指示发生了按下;它不提供移动设备独有选项的所有功能。我们将在本章后面讨论使用移动特定输入时你可以使用的所有功能,但我认为重要的是要注意在桌面上也有点击事件。我个人经常使用桌面进行测试,以便在 PC 和我的设备上轻松测试,因此我无需将每个项目中的每个更改部署到移动设备上进行测试。
要使用基于桌面的鼠标点击事件来移动玩家,首先,在 Unity 中,打开你的PlayerBehaviour脚本,并将FixedUpdate函数更新为以下内容:
/// <summary>
/// FixedUpdate is a prime place to put physics
/// calculations happening over a period of time.
/// </summary>
void FixedUpdate()
{
// Check if we're moving to the side
var horizontalSpeed = Input.GetAxis("Horizontal") *
dodgeSpeed;
/* If the mouse is held down (or the screen is pressed
* on Mobile) */
if (Input.GetMouseButton(0))
{
/* Get a reference to the camera for converting
* between spaces */
var cam = Camera.main;
/* Converts mouse position to a 0 to 1 range */
var screenPos = Input.mousePosition;
var viewPos = cam.ScreenToViewportPoint(screenPos);
float xMove = 0;
/* If we press the right side of the screen */
if (viewPos.x < 0.5f)
{
xMove = -1;
}
else
{
/* Otherwise we're on the left */
xMove = 1;
}
/* Replace horizontalSpeed with our own value */
horizontalSpeed = xMove * dodgeSpeed;
}
rb.AddForce(horizontalSpeed, 0, rollSpeed);
}
我们在先前的脚本中添加了一些内容。首先,我们通过使用Input.GetMouseButton函数来检查鼠标按钮是否被按下。如果鼠标被按下,该函数将返回true,如果没有,则返回false。该函数接受一个参数,用于指定我们想要检查哪个鼠标按钮,0代表左键,1代表右键,2代表中键。然而,对于移动设备,只有0会被识别为点击。
注意
关于Input.GetMouseButton函数的更多信息,请参阅docs.unity3d.com/ScriptReference/Input.GetMouseButton.html。
我们可以通过使用Input.mousePosition属性来获取鼠标所在的位置。然而,这个值是以屏幕空间给出的。什么是屏幕空间?好吧,让我们先谈谈我们如何通过使用世界空间来在 Unity 中传统地处理位置。
屏幕空间与世界空间
当通过检查器窗口在 Unity 中处理位置时,我们有一个位于游戏世界中心的(0,0,0)点,我们称之为原点,然后我们根据从这个点开始的偏移来引用其他所有内容。我们通常将这种定位方法称为世界空间。假设我们的摄像机指向原点,世界空间看起来是这样的:

图 3.1 – 世界空间的一个示例
这些线代表我们世界的x、y和z轴。如果我要将一个物体向右或向左移动,它将沿着x轴正方向或负方向移动。在学校时,你可能已经学过使用图表和点,而世界空间的工作方式与此非常相似。
注意
在 检查器 窗口中,父对象的子对象使用不同的系统,即它们被给予相对于其父对象的位置。这个系统被称为 局部空间。
当使用鼠标输入时,Unity 以另一个空间提供这些信息,屏幕空间。在这个空间中,位置基于相机位置,并不涉及实际的游戏世界。这个空间也是二维的,所以只有 x 和 y 位置,z 总是固定在 0:

图 3.2 – 屏幕空间示例
在前一个案例中,屏幕的左下角是 (0,0),右上角是 (Screen.width, Screen.height)。Screen.width 和 Screen.height 是 Unity 中的值,将给出屏幕窗口的屏幕大小(以像素为单位)。
我们可以使用这些值作为提供的内容,然后比较玩家按下的屏幕哪一侧,但在我看来,将位置转换为更容易处理的空间会更好。这样一个空间就是 视口空间,它从 (0,0) 到 (1,1):

图 3.3 – 视口空间示例
而不是搜索我们的 x 位置是否小于屏幕宽度的一半,我可以选择直接检查 viewPos.x 的值是否小于 0.5,这正是我们在前面的代码中所做的。如果值小于 0.5,它就在屏幕的左侧,所以我们返回 -1;否则,它就在右侧,所以我们返回 1。
注意
注意,Unity 的一些函数将使用 Vector3 而不是 Vector2 来处理三维空间。
一旦我们知道这一点,我们就可以将水平速度变量设置为根据我们的移动向左或向右移动。
保存脚本并返回 Unity,你将看到以下内容:

图 3.4 – 游戏当前状态
如前一个屏幕截图所示,我们现在可以使用鼠标(通过 Input.GetMouseButton 函数和 Input.mousePosition 变量)或我们的键盘(通过 GetAxis 函数),如前所述,来移动我们的玩家。
这种输入方式对我们目前所做的事情来说已经足够好了,但我假设你将想知道如何使用移动设备自己的移动方式,因此我们将继续学习如何使用触摸来复制相同的功能。
使用触摸控制移动
Unity 的输入引擎有一个名为 Input.touches 的属性,它是一个 Touch 对象的数组。Touch 结构包含有关发生的触摸的信息,例如触摸的压力量和屏幕点击的次数。它还包含位置属性,如 Input.mousePosition,它将告诉你在像素中触摸发生的位置。
注意
关于 Touch 结构体的更多信息,请查看 docs.unity3d.com/ScriptReference/Touch.html。
让我们看看使用触摸代替鼠标输入的步骤:
-
调整我们前面的代码,使其看起来像以下这样:
/// <summary> /// FixedUpdate is a prime place to put physics /// calculations happening over a period of time. /// </summary> void FixedUpdate() { // Check if we're moving to the side var horizontalSpeed = Input.GetAxis("Horizontal") * dodgeSpeed; /* Check if Input has registered more than 0 touches */ if (Input.touchCount > 0) { /* Get a reference to the camera for converting between spaces */ var cam = Camera.main; /* Store the first touch detected */ var firstTouch = Input.touches[0]; /* Converts mouse position to a 0 to 1 range */ var screenPos = firstTouch.position; var viewPos = cam.ScreenToViewportPoint(screenPos); float xMove = 0; /* If we press the right side of the screen */ if (viewPos.x < 0.5f) { xMove = -1; } else { /* Otherwise we're on the left */ xMove = 1; } /* Replace horizontalSpeed with our own value */ horizontalSpeed = xMove * dodgeSpeed; } rb.AddForce(horizontalSpeed, 0, rollSpeed); }
现在,请注意,这段代码看起来与我们前面章节中编写的代码非常相似。考虑到这一点,我们不需要复制和粘贴适当的代码两次并做出更改,就像许多初学者程序员会做的那样,我们可以利用相似之处来创建一个函数。对于差异,我们可以使用参数来更改值。
-
考虑到这一点,让我们将以下函数添加到
PlayerBehaviour类中:/// <summary> /// Will figure out where to move the player /// horizontally /// </summary> /// <param name="screenPos">The position the player /// has touched/clicked on in screen space</param> /// <returns>The direction to move in the x axis</returns> private float CalculateMovement(Vector3 screenPos) { /* Get a reference to the camera for converting * between spaces */ var cam = Camera.main; /* Converts mouse position to a 0 to 1 range */ var viewPos = cam.ScreenToViewportPoint(screenPos); float xMove = 0; /* If we press the right side of the screen */ if (viewPos.x < 0.5f) { xMove = -1; } else { /* Otherwise we're on the left */ xMove = 1; } /* Replace horizontalSpeed with our own value */ return xMove * dodgeSpeed; }
在前面的代码中,我们不是使用 Input.mousePosition 或触摸位置,而是使用函数的参数。而且,与之前编写的函数不同,这个函数将实际使用返回值;在这种情况下,它将给我们一个浮点数值。我们将在 Update 函数中使用这个值,当调用此函数时将 horiztonalSpeed 设置为新值。现在函数已经存在,我们可以在适当的时候调用它。
-
现在,更新
Update函数,如下所示:/// <summary> /// FixedUpdate is a prime place to put physics /// calculations happening over a period of time. /// </summary> void FixedUpdate() { // Check if we're moving to the side var horizontalSpeed = Input.GetAxis("Horizontal") * dodgeSpeed; /* Check if we are running either in the Unity editor or in a * standalone build.*/ #if UNITY_STANDALONE || UNITY_WEBPLAYER || UNITY_EDITOR /* If the mouse is held down (or the screen is tapped * on Mobile */ if (Input.GetMouseButton(0)) { var screenPos = Input.mousePosition; horizontalSpeed = CalculateMovement(screenPos); } /* Check if we are running on a mobile device */ #elif UNITY_IOS || UNITY_ANDROID // Check if Input has registered more than // zero touches if (Input.touchCount > 0) { /* Store the first touch detected */ var firstTouch = Input.touches[0]; var screenPos = firstTouch.position; horizontalSpeed = CalculateMovement(screenPos); } #endif rb.AddForce(horizontalSpeed, 0, rollSpeed); }
在前面的示例中,我使用了一个基于所选平台的 #if 指令。Unity 会根据我们部署的平台自动创建 #define。这个 #if 指令,连同 #elif 和 #endif,允许我们根据这些指令在我们的项目中包含或排除代码。
在 Visual Studio 中,请注意,如果您正在为 iOS 或 Android 构建项目,UNITY_IOS || UNITY_ANDROID 部分的代码会被灰色显示,这意味着当前不会调用这部分代码,因为我们正在使用 Unity 编辑器 运行游戏。然而,当我们导出代码到我们的平台时,将使用适当的代码。
注意
要查看所有其他平台相关的 #define 指令,请查看 docs.unity3d.com/Manual/PlatformDependentCompilation.html。
利用上述指令,我们可以指定我们项目不同版本的代码,这对于处理多平台开发至关重要。
注意
除了 Unity 内置的 #define 指令外,您可以通过转到 编辑 | 项目设置 | 玩家,在 检查器 窗口中向下滚动到 其他设置,并更改 脚本 定义符号 来创建自己的。
这对于针对特定设备或显示某些调试信息非常有用,还有其他许多用途。
- 保存脚本并返回 Unity。
当我们将游戏导出到您的 Android 设备上时,请注意,现在控制台使用我们新创建的触摸代码可以正常工作。这使我们能够在移动设备和 PC 上都使用相同的功能。
我们已经知道我们可以将我们的游戏导出到 Android 设备,但还有一种方法可以在不进行完整导出的情况下测试我们的游戏。这可以通过下载一个特殊的应用程序来完成,该应用程序将允许我们从计算机流式传输我们的游戏到我们的移动设备,这是我们将在下一节讨论的内容。
使用 Unity Remote
另一种通过移动设备检查我们的游戏如何工作的方法是使用 Unity 创建的应用程序,称为Unity Remote。该应用程序是用 Unity 5 创建的,自从应用程序更新以来已经有一段时间了,但它仍然与当前版本的 Unity 兼容;然而,它要求我们做一些额外的操作和设置。
Unity Remote 的 Android 设置
为了设置手机使用 Unity Remote,我们需要下载应用程序并学习如何启用调试模式,因此在本节中,我们将看到如何进行操作:
- 首先,打开Google Play应用程序,然后在搜索栏中输入
unity remote:

图 3.5 – 搜索 Unity Remote 应用程序
- 选择Unity Remote 5应用程序。之后,您将被带到安装界面,因此点击安装按钮并等待其下载完成。

图 3.6 – Google Play 上的 Unity Remote 5 应用程序页面
- 完成后,您应该会在手机上看到它显示出来,准备打开。

图 3.7 – 设备上的应用程序
- 点击应用程序,您将被带到询问应用程序是否允许拍照和录制视频的屏幕。这是由于您的游戏可能使用的功能,因此请随意选择您的游戏是否可以使用这些功能。

图 3.8 – 权限屏幕
- 之后,您应该会看到一个类似这样的屏幕:

图 3.9 – 运行 Unity Remote 5 应用程序
这是您在玩游戏之前应该看到的设置屏幕,但如果您现在尝试运行您的项目,什么都不会发生;因此,我们还需要做一些准备工作。
启用开发者模式和调试
首先,我们需要将我们的手机设置为开发者模式,以便我们可以启用调试:
- 首先,在 Unity 编辑器中,转到项目设置菜单并打开编辑器部分。从那里,在Unity Remote选项下,将设备从无更改为任何****Android 设备。

图 3.10 – 设置 Unity Remote 设备
- 如果您还没有这样做,请通过 USB 将手机连接到您的电脑。从设置菜单中,您需要选择通知中的USB 已连接的附件选项。

图 3.11 – 将手机插入电脑时的通知窗口
- 从弹出的菜单中选择USB 网络共享选项。

图 3.12 – 将模式更改为 USB 网络共享
如果您在设备上没有看到这个选项,您可以在手机的移动热点和网络共享部分查找,或者使用您选择的互联网搜索引擎查找如何为您的设备启用 USB 网络共享。
-
然后,在设备上,转到设置 | 关于手机。
-
点击构建号属性七次以启用开发者模式。

图 3.13 – 我手机上的“关于手机”菜单
- 从那里,转到开发者选项部分。我使用了设置窗口的搜索功能来找到确切的位置。

图 3.14 – 开发者选项菜单
- 从那里,向下滚动并选择USB 调试选项。您可能会看到一个窗口解释 USB 调试的作用。点击确定按钮。

图 3.15 – 启用 USB 调试
-
接下来,您将弹出一个窗口,提示允许 USB 调试?点击允许按钮。
-
现在,您应该能够返回到Unity Remote 5应用程序。回到您的电脑上,点击播放按钮,您可能会看到电脑似乎冻结了一段时间,但经过短暂的时间,您应该看到手机上的画面反映了 PC 上的游戏:

图 3.16 – Unity Remote 游戏画面
当您使用 Unity Remote 在设备上玩游戏时,可能会看到一些模糊或图形问题。这是因为 Unity 正在将游戏的外观图像发送到设备以进行交互;游戏实际上并没有在设备上运行。画质远不如实际设备上的游戏,但它确实允许我们检查实际设备上游戏的当前状态!
启用开发者模式和调试还有好处,允许我们通过构建菜单直接将游戏部署到我们的设备上,而无需手动安装。要这样做,请转到构建设置菜单,并在运行设备选项下选择您的手机。

图 3.17 – 设置运行设备
现在,如果游戏结束后选择构建和运行,您应该看到的是手机打开时直接运行的游戏:

图 3.18 – 直接在设备上安装的游戏
现在我们已经学会了如何在 Android 设备上使用 Unity Remote,我们现在可以看看如何在 iOS 设备上设置 Unity Remote。
iOS 的 Unity Remote 设置
在 iOS 上设置 Unity Remote 可以在装有iTunes的 Mac 电脑或 Windows 电脑上完成。然而,步骤将与 Android 相似,除了不需要启用调试。为了本节的目的,我将使用 Mac:
- 首先,在 Unity 编辑器中,转到项目设置菜单并打开编辑器部分。从那里,在Unity Remote选项下,将设备从无更改为任何****iOS 设备。

图 3.19 – 选择“任何 iOS 设备”选项
- 然后,从您的 iOS 设备上打开 App Store 并搜索
unity remote。

图 3.20 – 搜索 Unity Remote 应用程序
- 选择 Unity Remote 5 应用程序并在您的设备上安装它。一旦安装完成,它应该位于您的手机上:

图 3.21 – 安装好的 Unity Remote
- 打开应用程序,您应该会看到以下类似内容,询问您是否想信任此计算机。请点击信任按钮:

图 3.22 – “信任此计算机”选项
- 从您的电脑上,开始玩游戏。可能需要一点时间,但您应该会看到游戏正在流式传输到您的设备,您可以像往常一样玩游戏:

图 3.23 – Unity Remote 流式传输我们的游戏画面
如同我们在构建 Android 版本时提到的,您可能会在 Unity Remote 上玩游戏时看到一些模糊或图形问题。这是因为 Unity 正在向设备发送游戏外观的图像以进行交互;游戏实际上并不在设备上。质量远不及实际设备上的游戏,但它确实允许我们检查实际设备上的游戏当前状态!
有了这些,我们现在可以在各自的设备上玩游戏,而无需进行构建!这可以是一个快速检查设备上是否正确工作的好方法,而无需每次都进行构建。现在,让我们看看一些我们可以用来解释输入的移动特定方法。
实现手势
在移动游戏中,你还会发现另一种输入类型,即滑动,例如在 Kiloo 的 Subway Surfers 中。这允许我们使用触摸的一般移动来为我们指定移动的方向。这通常用于让我们的玩家 跳跃 到另一个位置或快速移动到某个方向。因此,我们将继续使用以下步骤来实现,而不是我们之前的移动系统:
-
在
PlayerBehaviour脚本中,继续添加一些新的变量供我们使用:[Header("Swipe Properties")] [Tooltip("How far will the player move upon swiping")] public float swipeMove = 2f; [Tooltip("How far must the player swipe before we will execute the action (in inches)")] public float minSwipeDistance = 0.25f; /// <summary> /// Used to hold the value that converts /// minSwipeDistance to pixels /// </summary> private float minSwipeDistancePixels; /// <summary> /// Stores the starting position of mobile touch /// events /// </summary> private Vector2 touchStart;
为了确定我们是否在滑动,我们首先需要检查我们的移动的起始和结束位置。我们将起始位置存储在 touchStart、swipeMove、minSwipeDistance 变量中,这将确保在玩家实际跳跃之前,玩家已经在 x 轴上移动了一小段距离——在这种情况下,我们希望用户至少移动四分之一英寸,以便输入被计为一次滑动。
还要注意,Header 属性已被添加到第一个变量的顶部。这将向 Inspector 选项卡添加标题,使拆分脚本的不同部分更容易。如果你保存脚本并进入 Unity,你应该会在选择玩家时看到这个新属性已被添加:

图 3.24 – 在“滑动属性”下新添加的移动输入/触摸控制
我们下一步是将 MinSwipeDistance 值从英寸转换为像素等效值,这可以用来查看用户的滑动动作移动了玩家角色的多远。
-
返回到
PlayerBehaviour的Start函数,添加以下突出显示的代码:// Start is called before the first frame update public void Start() { // Get access to our Rigidbody component rb = GetComponent<Rigidbody>(); minSwipeDistancePixels = minSwipeDistance * Screen.dpi; }
通过 Screen.dpi 的 minSwipeDistance,我们知道移动在像素上的长度需要多长才能被计为一次滑动。
注意
关于 Screen.dpi 变量的更多信息,请参阅 docs.unity3d.com/ScriptReference/Screen-dpi.html。
现在我们知道了滑动长度,我们需要添加触发一次滑动的能力。正如我们之前提到的,我们一直在使用 FixedUpdate,它通常比 Update 函数调用得少。
我们使用 Input.GetAxis 和 Input.GetMouseButton 函数,这些函数在每个按钮被按下的帧返回 true,并且会在 FixedUpdate 期间继续响应,而 FixedUpdate 可能会错过输入事件发生的起始和结束帧,这对于滑动事件和某些动作(如跳跃)在游戏中是必需的。如果你想在输入开始或结束时立即发生某些事情,你可能会想利用 Update 函数,这正是我们将要对我们手势所做的事情。
-
现在,回到
PlayerBehaviour脚本中,为项目添加以下函数:/// <summary> /// Update is called once per frame /// </summary> private void Update() { /* Check if we are running on a mobile device */ #if UNITY_IOS || UNITY_ANDROID /* Check if Input has registered more than zero touches */ if (Input.touchCount > 0) { /* Store the first touch detected */ Touch touch = Input.touches[0]; SwipeTeleport(touch); } #endif }
在前面的代码中,我们添加了一个名为 SwipeTeleport 的新行为,并使用其属性在发生滑动时移动玩家。
然后,我们将创建一个函数来处理这种新的滑动行为,如下所示:
/// <summary>
/// Will teleport the player if swiped to the left or
/// right
/// </summary>
/// <param name="touch">Current touch event</param>
private void SwipeTeleport(Touch touch)
{
/* Check if the touch just started */
if (touch.phase == TouchPhase.Began)
{
/* If so, set touchStart */
touchStart = touch.position;
}
/* If the touch has ended */
else if (touch.phase == TouchPhase.Ended)
{
/* Get the position the touch ended at */
Vector2 touchEnd = touch.position;
/* Calculate the difference between the
beginning and end of the touch on the x
axis. */
float x = touchEnd.x - touchStart.x;
/* If not moving far enough, don't do the
teleport */
if (Mathf.Abs(x) < minSwipeDistancePixels)
{
return;
}
Vector3 moveDirection;
/* If moved negatively in the x axis, move
left */
if (x < 0)
{
moveDirection = Vector3.left;
}
else
{
/* Otherwise player is on the right */
moveDirection = Vector3.right;
}
RaycastHit hit;
/* Only move if player wouldn't hit something
*/
if (!rb.SweepTest(moveDirection, out hit,
swipeMove))
{
/* Move the player */
var movement = moveDirection * swipeMove;
var newPos = rb.position + movement;
rb.MovePosition(newPos);
}
}
}
在这个函数中,我们不是只使用当前的触摸位置,而是存储触摸开始时的起始位置。当玩家抬起手指时,我们也会获取位置。然后我们获取该移动的方向,并将其应用到球体上,在实际上造成移动之前检查我们是否会与某物发生碰撞。
- 保存你的脚本并回到 Unity 中,将你的项目导出到你的移动设备或模拟器。

图 3.25 – 执行滑动操作后的游戏视觉
现在,无论我们向左或向右滑动,玩家都会相应地移动。让我们在下一节学习在玩游戏时可以使用的一种其他动作。
使用捏合缩放玩家
使用触摸事件在游戏中修改事物的概念也可以应用于其他触摸交互方法,例如使用手指捏合来缩放。为了了解如何做到这一点,让我们调整 PlayerBehaviour 脚本,以便我们可以通过捏合或拉伸视图来改变玩家的缩放比例:
-
打开
PlayerBehaviour脚本并添加以下属性:[Header("Scaling Properties")] [Tooltip("The minimum size (in Unity units) that the player should be")] public float minScale = 0.5f; [Tooltip("The maximum size (in Unity units) that the player should be")] public float maxScale = 3.0f; /// <summary> /// The current scale of the player /// </summary> private float currentScale = 1; -
接下来,添加以下函数:
/// <summary> /// Will change the player's scale via pinching and /// stretching two touch events /// </summary> private void ScalePlayer() { /* We must have two touches to check if we are * scaling the object */ if (Input.touchCount != 2) { return; } else { /* Store the touches detected. */ Touch touch0 = Input.touches[0]; Touch touch1 = Input.touches[1]; Vector2 t0Pos = touch0.position; Vector2 t0Delta = touch0.deltaPosition; Vector2 t1Pos = touch1.position; Vector2 t1Delta = touch1.deltaPosition; /* Find the previous frame position of each touch. */ Vector2 t0Prev = t0Pos - t0Delta; Vector2 t1Prev = t1Pos - t1Delta; /* Find the the distance (or magnitude) between the * touches in each frame. */ float prevTDeltaMag = (t0Prev - t1Prev).magnitude; float tDeltaMag = (t0Pos - t1Pos).magnitude; /* Find the difference in the distances * between each frame. */ float deltaMagDiff = prevTDeltaMag - tDeltaMag; /* Keep the change consistent no matter what * the framerate is */ float newScale = currentScale; newScale -= (deltaMagDiff * Time.deltaTime); // Ensure that the new value is valid newScale = Mathf.Clamp(newScale, minScale, maxScale); /* Update the player's scale */ transform.localScale = Vector3.one * newScale; /* Set our current scale for the next frame */ currentScale = newScale; } }
与使用单个触摸事件不同,在这个例子中,我们使用了两个。使用这两个触摸事件,我们可以看到触摸在上一帧(delta)中的变化。然后我们使用这个差异来修改玩家的缩放比例。为了确保球体始终有一个有效的值,我们使用 Mathf.Clamp 函数来保持值在 minScale 和 maxScale 中设置的范围之间。
-
接下来,我们需要通过更新
Update函数来调用该函数:/// <summary> /// Update is called once per frame /// </summary> private void Update() { /* Check if we are running on a mobile device */ #if UNITY_IOS || UNITY_ANDROID /* Check if Input has registered more than zero touches */ if (Input.touchCount > 0) { /* Store the first touch detected */ Touch touch = Input.touches[0]; SwipeTeleport(touch); ScalePlayer(); } #endif } -
保存你的脚本并返回到 Unity 编辑器。导出你的游戏,你应该能够看到玩家缩放的效果——通过分开两根手指,你会看到球体膨胀,反之亦然:

图 3.26 – 游戏当前状态下捏合手势的结果
注意
对于使用 LDPlayer 的用户,可以通过按下 Ctrl 然后滚动鼠标滚轮来复制捏合/缩放效果。
希望这展示了能够使用多触控和利用触摸事件的一些优势所赋予的力量,而不仅仅是单次鼠标点击。接下来,我们将探索另一种 PC 所没有的输入方法。
使用加速度计
移动设备有的一种输入类型,而 PC 没有,就是加速度计。这允许你通过倾斜手机的物理位置在游戏中移动。最流行的例子可能是 Lima Sky 的Doodle Jump和 Gameloft 的Asphalt系列游戏中玩家的移动。为了做类似的事情,我们可以使用Input.acceleration属性获取我们设备的加速度,并使用它来移动玩家。让我们看看完成这一点的步骤:
-
我们可能希望允许我们的设计师设置他们是否想要使用
加速度计模式或ScreenTouch,这是我们之前使用的。考虑到这一点,让我们在滑动****属性标题上方创建一个新的enum类型PlayerBehaviour脚本:[Tooltip("How fast the ball moves forwards automatically")] [Range(0, 10)] public float rollSpeed = 5; public enum MobileHorizMovement { Accelerometer, ScreenTouch } [Tooltip("What horizontal movement type should be used")] public MobileHorizMovement horizMovement = MobileHorizMovement.Accelerometer; [Header("Swipe Properties")] [Tooltip("How far will the player move upon swiping")] public float swipeMove = 2f;
之前的脚本使用enum定义了一个名为MobileHorizMovement的自定义类型,它可以有两个值之一,Accelerometer或ScreenTouch。然后我们创建了一个名为horizMovement的新变量。
现在,如果你保存PlayerBehaviour脚本并返回到检查器标签页,你会看到我们可以选择这两个选项之一(加速度计或屏幕触摸)。通过使用这个下拉菜单,项目中的游戏设计师可以轻松选择我们想要使用哪个选项,并且如果将来想要的话,我们还可以扩展更多(我们将在下一章中这样做):

图 3.27 – 从检查器调整水平移动属性
-
接下来,让我们更新
Update#elif UNITY_IOS || UNITY_ANDROID`代码块:/* Check if we are running on a mobile device */ #elif UNITY_IOS || UNITY_ANDROID switch (horizMovement) { case MobileHorizMovement.Accelerometer: /* Move player based on accelerometer direction */ horizontalSpeed = Input.acceleration.x * dodgeSpeed; break; case MobileHorizMovement.ScreenTouch: /* Check if Input registered more than zero touches */ if (Input.touchCount > 0) { /* Store the first touch detected */ var firstTouch = Input.touches[0]; var screenPos = firstTouch.position; horizontalSpeed = CalculateMovement(screenPos); } break; } #endif // Check if we are running on a mobile device #elif UNITY_IOS || UNITY_ANDROID if(horizMovement == MobileHorizMovement.Accelerometer) { // Move player based on direction of the accelerometer horizontalSpeed = Input.acceleration.x * dodgeSpeed; } //Check if Input has registered more than zero touches if (Input.touchCount > 0) { if (horizMovement == MobileHorizMovement.ScreenTouch) { //Store the first touch detected. Touch touch = Input.touches[0]; horizontalSpeed = CalculateMovement(touch.position); } } #endif
如果将horizMovement变量设置为加速度计,这段新代码将使用我们设备的加速度,而不是屏幕上检测到的触摸。
- 保存你的脚本并导出项目。

图 3.28 – 通过加速度计移动玩家
有了这个,你会注意到我们现在可以倾斜屏幕向右或向左,玩家将向相应的方向移动。
在 Unity 中,加速度以重力加速度值来衡量,1 代表 1 g 的力。如果你将设备竖直握在面前(主按钮在底部),则x轴沿右侧为正,y轴向上为正,z轴指向你时为正。
注意
更多关于加速度计的信息,请查看docs.unity3d.com/Manual/MobileInput.html。
知道我们的常规输入正在工作是很不错的,但你可能还想检查场景中的游戏对象是否被触摸,以便游戏可以对其做出反应。让我们接下来做这件事。
检测游戏对象上的触摸
为了让我们的玩家做些其他事情,以及展示一些额外的输入功能,我们将确保如果玩家点击障碍物,它将被销毁。我们将使用以下步骤修改我们现有的代码以添加此新功能,利用射线投射的概念:
-
在
PlayerBehaviour脚本中,添加以下新函数:/// <summary> /// Will determine if we are touching a game object /// and if so call events for it /// </summary> /// <param name="screenPos">The position of the touch /// in screen space</param> private static void TouchObjects(Vector2 screenPos) { /* Convert the position into a ray */ Ray touchRay = Camera.main.ScreenPointToRay(screenPos); RaycastHit hit; /* Create a LayerMask that will collide with all * possible channels */ int layerMask = ~0; /* Are we touching an object with a collider? */ if (Physics.Raycast(touchRay, out hit, Mathf.Infinity, layerMask, QueryTriggerInteraction.Ignore)) { /* Call the PlayerTouch function if it exists * on a component attached to this object */ hit.transform.SendMessage("PlayerTouch", SendMessageOptions.DontRequireReceiver); } } /// <summary> /// Will determine if we are touching a game object /// and if so call events for it /// </summary> /// <param name="touch">Our touch event</param> private static void TouchObjects(Touch touch) { // Convert the position into a ray Ray touchRay = Camera.main.ScreenPointToRay(touch.position); RaycastHit hit; // Create a LayerMask that will collide with all // possible channels int layerMask = ~0; // Are we touching an object with a collider? if (Physics.Raycast(touchRay, out hit, Mathf.Infinity, layerMask, QueryTriggerInteraction.Ignore)) { // Call the PlayerTouch function if it exists on a // component attached to this object hit.transform.SendMessage("PlayerTouch", SendMessageOptions.DontRequireReceiver); } }
在这里,我们使用不同的版本来确定碰撞——一个raycast。这基本上是一个指向给定方向的不可见向量,我们将用它来检查它是否与场景中的任何对象发生碰撞。这通常用于游戏,如第一人称射击游戏,以确定玩家是否击中了敌人,而无需生成并移动一个项目。
我们在这里使用的Physics.Raycast版本接受五个参数:
-
第一个指定了要使用哪个射线。
-
第二个是
hit,它包含有关是否发生碰撞的信息。 -
第三个参数指定了检查碰撞的距离。
-
第四个是层掩码,它决定了你可以与哪些对象发生碰撞。在我们的例子中,我们希望与所有碰撞体发生碰撞,因此我们使用位运算符(
~)将0转换为通过翻转创建该数字所使用的所有位得到的数字。 -
最后,我们有一个名为
QueryTriggerInteraction的枚举,我们将其设置为Ignore。这意味着我们创建的带有触发器的Tile End对象不会阻止我们的触摸事件,这在默认情况下会发生,即使我们看不到它们。
注意
关于位运算符(~)的更多信息,请查看docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators#bitwise-complement-operator-。
关于射线投射的更多信息,请查看docs.unity3d.com/ScriptReference/Physics.Raycast.html。
如果我们击中了某个东西,我们将在我们与之碰撞的对象上调用名为SendMessage的函数。如果该函数存在于游戏对象的任何组件上,该函数将尝试调用具有相同名称的第一个参数的函数。第二个参数让我们知道如果它不存在,是否应该显示错误。
注意
关于SendMessage函数的更多信息,请查看docs.unity3d.com/ScriptReference/GameObject.SendMessage.html。
-
现在,在
UpdateTouchObjects函数中调整代码,以便我们可以在 Unity 编辑器中测试功能:/// <summary> /// Update is called once per frame /// </summary> private void Update() { /* Check if we are running either in the Unity editor or in a * standalone build.*/ #if UNITY_STANDALONE || UNITY_WEBPLAYER || UNITY_EDITOR /* If the mouse is tapped */ if (Input.GetMouseButtonDown(0)) { Vector2 screenPos = new Vector2( Input.mousePosition.x, Input.mousePosition.y); TouchObjects(screenPos); } /* Check if we are running on a mobile device */ #elif UNITY_IOS || UNITY_ANDROID /* Check if Input has registered more than zero touches */ if (Input.touchCount > 0) { /* Store the first touch detected */ Touch touch = Input.touches[0]; TouchObjects(touch.position); SwipeTeleport(touch); ScalePlayer(); } #endif } -
在此点保存
PlayerBehaviour脚本。 -
最后,我们调用
PlayerTouchObstacleBehaviour脚本并添加以下代码:[Tooltip("Explosion effect to play when tapped")] public GameObject explosion; /// <summary> /// If the object is tapped, we spawn an explosion and /// destroy this object /// </summary> private void PlayerTouch() { if (explosion != null) { var particles = Instantiate(explosion, transform.position, Quaternion.identity); Destroy(particles, 1.0f); } Destroy(this.gameObject); } public GameObject explosion; /// <summary> /// If the object is tapped, we spawn an explosion and /// destroy this object /// </summary> private void PlayerTouch() { if (explosion != null) { var particles = Instantiate(explosion, transform.position, Quaternion.identity); Destroy(particles, 1.0f); } Destroy(this.gameObject); }
此功能基本上将销毁它附加到的游戏对象,并在 1 秒后创建一个也会销毁自己的爆炸。
注意
通过使用 Unity 的OnMouseDown函数,我们可以得到类似我们正在编写的结果。正如我们已经讨论过的,在为移动设备开发时可以使用鼠标事件。然而,请注意,该函数的计算成本比我在这里建议的方法要高。
这是因为当你轻触屏幕时,每个具有OnMouseDown方法的对象都会执行raycast和 100 次,因此在处理移动设备开发时,保持性能很重要。有关更多信息,请参阅answers.unity3d.com/questions/1064394/onmousedown-and-mobile.html。
- 保存脚本并返回 Unity。
我们还没有创建爆炸粒子效果。为了创建这种效果,我们将使用一个粒子系统。我们将在第十二章“改进游戏感觉”中更深入地探讨粒子系统,但现在,我们可以将粒子系统视为一个尽可能简单的游戏对象,这样我们就可以同时将许多它们放在屏幕上,而不会使游戏速度减慢太多。这主要用于像烟雾或火焰这样的东西,但在这个例子中,我们的障碍物将会爆炸。
使用以下步骤创建爆炸粒子效果:
-
通过进入
Explosion并按Enter键来创建一个粒子系统。 -
在层次结构窗口中选择游戏对象,然后在检查器选项卡中打开粒子系统组件。在那里,点击渲染器部分以展开它,并通过点击名称旁边的圆圈并将菜单中选择默认材质来将RenderMode更改为网格和材质为默认材质:

图 3.29 – 选择默认材质
这将使粒子看起来像我们已经创建的障碍物,即一个带有默认材质的盒子。

图 3.30 – 盒子的视觉效果
-
接下来,在顶部的粒子系统部分,将重力修改器属性更改为1。这确保了对象将逐渐下落,就像具有刚体的正常对象一样,但计算量更少。
-
然后,在开始速度下,移动到右侧并点击向下箭头,从该菜单中选择随机介于 两个常量:

图 3.31 – 使用两个常量之间的随机值
-
这将使单个窗口变为两个,表示可以用于此属性的最低和最高值。从那里,将两个值设置为
0``8。这使得生成的对象以0到8之间的速度开始。 -
然后,将开始大小更改为0到0.25之间的某个值。这将确保我们创建的一堆立方体比我们计划替换的那个要小。
-
将
1改为取消选中循环选项。这确保粒子系统只会持续1秒,取消选中循环意味着粒子系统默认只激活一次。
注意
您可以通过在场景窗口的右下角菜单中点击播放按钮,并选择粒子系统对象来查看所做的每个更改的效果。
-
最后,将开始生命周期属性更改为1,以确保所有粒子在游戏对象被销毁之前都死亡。
-
在发射部分,将随时间变化速率更改为0。然后,在爆发部分,点击+按钮并将计数设置为50:

图 3.32 – 创建创建时的单个爆发
这意味着在粒子系统创建的最初时刻将生成 50 个粒子,就像爆炸一样。
- 然后,检查生命周期大小并点击复选框旁边的文本以显示更多详细信息。从那里,通过选择一个逐渐减少的曲线来更改大小属性,以便在最后,它们都将变为0.0。这可以通过首先选择曲线本身,然后转到检查器窗口底部的粒子系统曲线部分来完成。如果您看不到以下截图所示的內容,您可以点击并拖动名称向上使其突出显示。从那里,您可以点击具有向下曲线的选项:

图 3.33 – 设置生命周期大小曲线
这会使粒子逐渐变小,并且它们只有在变得不可见(缩放为0.0)后才会自我销毁。
-
最后,检查碰撞属性并打开它,将类型属性设置为世界。这将导致粒子击中地面。
-
然后,通过将对象从层次结构选项卡拖放到资产 | 预制体文件夹中的项目选项卡来使您的对象成为一个预制体。一旦预制体创建完成(您应该在层次结构中看到对象的文本变为蓝色),通过选择它并按删除键来从场景中删除原始对象。
-
接下来,在障碍物预制体中分配障碍物行为(脚本)的爆炸属性:

图 3.34 – 在障碍物预制体中分配爆炸属性
- 保存您的项目并运行游戏:

图 3.35 – 触摸时爆炸的障碍物
现在,当我们点击其中一个障碍物时,我们可以看到物体被摧毁,并且播放了爆炸效果!
如果你将游戏导出到你的移动设备上,你应该看到相同的功能:

图 3.36 – 游戏的当前状态
从现在开始,无论何时我们在移动设备上点击障碍物,它们都会被摧毁。
摘要
在本章中,我们学习了在移动设备上工作时控制游戏的主要方式。我们还学习了如何使用鼠标输入、触摸事件、手势和加速度计来允许玩家与我们的游戏互动。
在下一章中,我们将通过深入用户界面世界并创建可享受的菜单,来探索玩家与游戏互动的另一种主要方式,无论用户在什么设备上玩游戏。
第四章:分辨率无关的 UI
当在移动设备上工作时,你需要花费相当一部分时间处理的事情之一是用户界面(UI)。与为 PC 开发项目不同,那时你只需要关心一个分辨率或纵横比,但在为移动设备构建时,有许多不同分辨率和纵横比的不同设备。例如,我们有一些可以放入我们口袋中的手机,还有平板电脑,它们非常大。不仅如此,移动游戏还可以水平或垂直玩游戏。一些新的手机甚至允许你折叠它们,以动态地增加或减少屏幕大小。
图形用户界面(GUI)是玩家与游戏互动的方式。实际上,你在所有前面的章节(Unity 编辑器)以及与你的操作系统交互时都使用了 GUI。如果没有某种形式的 GUI,你与计算机的唯一交互方式将是命令行界面(CLI)——即在 Windows 中的命令提示符和在 Linux 和 macOS 中的终端。
当处理图形用户界面(GUIs)时,我们希望它们只包含对玩家在任何给定时间都重要的信息,同时也要简单直观。有些人主要的工作是编程和/或设计用户界面(UIs),而且这个领域也有大学学位。因此,虽然我们不会讨论与使用 GUIs 相关的所有内容,但我确实想谈谈在将来进行自己的项目时应该相当有帮助的方面。
当为移动设备构建时,设计你的 UI 以实现分辨率无关性——也就是说,确保 UI 将缩放并调整自身以适应给定的任何屏幕大小——这一点非常重要。如果你游戏的 UI 是分辨率无关的或响应式的,作为游戏开发者,你将能够针对大量设备。
本章将分为几个主题。本章是一个从头到尾的简单步骤过程。以下是我们任务的概述:
-
创建标题屏幕
-
向屏幕添加 UI 元素
-
添加屏幕控制
-
实现暂停菜单
-
暂停游戏
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小更改即可。如果你想下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。你还可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
您可以在 GitHub 上找到本章中提供的代码文件,链接为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter04。
创建标题屏幕
在我们开始向游戏添加 UI 元素之前,让我们首先通过创建我们无论如何都需要的东西来设置一些基础和基础知识 – 一个标题屏幕:
- 首先,我们将创建一个新的场景供我们使用,方法是转到文件 | 新建场景。会出现一个窗口询问应使用哪个模板。在这种情况下,我们将选择基本(内置),然后点击创建按钮。

图 4.1 – 创建基本场景
在处理 UI 时,我们通常会想要看到屏幕上将要绘制的视觉表示,因此我们将想要使用 2D 模式以更好地表示我们的 UI 在游戏最终版本中的外观。
- 要做到这一点,请转到场景视图标签 – 您将看到带有下面带有2D按钮的控制栏菜单。点击它,您应该会看到摄像机自动移动到类似于以下截图的视图:

图 4.2 – 选择 2D 模式
由于唯一的选择是垂直于XY平面(x轴指向右侧,y轴向上)查看,并且我们的摄像机已更改为正交视图,因此Gizmos消失了。
-
我们需要创建一个名为我们游戏的Text对象。转到菜单并选择游戏对象 | UI | Text – Text Mesh Pro。
-
如果这是您第一次使用TextMeshPro,可能会弹出一个TMP 导入器窗口。如果是这样,请点击导入 TMP Essentials按钮。

图 4.3 – TMP 导入器窗口
小贴士
注意,虽然这本书使用TextMesh Pro来绘制文本,但以下脚本部分的步骤也适用于传统的 Unity UI 系统和Text对象,并且本章中的所有概念在这两种系统中都是相同的。
-
您将在层次视图中看到三个新对象,Canvas、Text (TMP)和EventSystem:
- 画布:这是所有 UI 元素将驻留的区域,如果您尝试创建一个没有已经存在的 UI 元素,Unity 会像刚才那样为您创建一个。从场景视图标签,它将围绕自身绘制一个白色矩形以显示其大小,并根据游戏视图的大小进行调整:

图 4.4 – 缩放以显示画布
注意
如果您在层次结构窗口中双击一个对象,摄像机将自动移动和缩放,以便您可以在场景窗口中看到该对象。GameObject 包含一个Canvas组件,它允许您指定图像的渲染方式(以及一个Canvas Scaler组件,根据游戏运行设备的分辨率调整您的艺术作品大小,以及一个Graphic Raycaster组件,它确定Canvas上的任何对象是否被击中。我们将在本节稍后深入了解Canvas Scaler组件)。
重要提示
想要了解更多关于Canvas对象的信息,请查看docs.unity3d.com/Manual/UICanvas.html。特别是关于渲染模式的讨论对于理解 UI 元素如何渲染到屏幕上的方式非常有用。
- Text (TMP):此对象是我们实际文本对象,它具有所有允许我们在画布对象上的任何位置定位对象以及更改将显示的文本、颜色、大小等属性。
注意
想要了解更多关于 TextMesh Pro 的信息,请查看docs.unity3d.com/Packages/com.unity.textmeshpro@3.0/manual/index.html。
- EventSystem:此对象允许用户根据各种输入类型(如键盘按键、触摸事件或游戏手柄)向游戏中的对象发送事件。此对象中存在一些属性,允许您指定用户如何与您的 UI 交互,如果您尝试创建一个没有存在的 UI 元素,Unity 将为您创建一个,就像这里所做的那样。如果您想在您的关卡中使用 Unity 的 UI 系统创建任何类型的交互式材料,例如按钮、滑块等,您必须在关卡中附加带有EventSystem组件的对象,否则事件将不会触发。
注意
想要了解更多关于EventSystem对象的信息,请查看docs.unity3d.com/Manual/EventSystem.html。
- 默认情况下,您可能看不到我们的文本框创建的位置。如果您看不到它,可以转到层次结构窗口,然后双击Text (TMP)对象。如果一切顺利,我们应该会有如下所示的内容:

图 4.5 – 放大查看 Text (TMP) 对象
- 下一步我们将做的是让识别这个对象变得更加容易。因此,考虑到这一点,滚动到
标题文本的顶部。为了使其更易于查看,在选中对象后,转到检查器选项卡,并向下滚动到TextMeshPro – 文本 (UI)组件,然后将顶点颜色属性更改为黑色。
我们可以通过查看对象是否在为画布创建的白色框内来判断对象是否将在游戏中可见。需要注意的是,与迄今为止我们看到的所有游戏对象使用的默认变换组件不同,我们的文本对象在相同位置有一个矩形变换组件。
矩形变换组件
矩形 变换与常规变换不同,因为变换组件代表一个点或对象的中心,而矩形变换代表一个矩形,其中 UI 元素将驻留。如果一个具有矩形变换的对象有一个父对象,该父对象也有矩形变换,那么子对象将指定对象相对于父对象的位置。
注意
有关对象定位和矩形变换的信息,请参阅docs.unity3d.com/Manual/UIBasicLayout.html。
为了更好地了解0的属性,这些属性将围绕对象的锚点中心化我们的对象;然后你可以双击层次结构标签页中的对象,将相机定位到其新位置,并使用鼠标滚轮进行缩放/缩小:

图 4.6 – 画布上居中的标题文本
如果你选择了标题文本对象(双击以在屏幕上居中对象),我们的对象的锚点可以通过场景标签页中的四个小矩形看到,在场景标签页的中心形成一个 X 形状。
重要提示
如我之前所述,请注意,这里显示的用于画布的白色框根据你在游戏标签页视图(我的设置为自由宽高比,因此根据此比例填充空间)中设置的宽高比可能看起来与你的屏幕不同。如果你转到游戏标签页,你可以从左侧的下拉菜单中选择它们。
接下来,我们将查看在矩形变换组件中工作方式不同的两个主要元素:锚点和轴点。
锚点
在0中找到的 UI 元素会粘附到其父元素的左侧边缘。
锚点之上的属性是相对于已设置的锚点的位置。这在支持多个分辨率而不缩放创建的艺术资源时非常有用。在我们的例子中,我们希望标题相对于相机的顶部定位。让我们看看使用锚点时的步骤:
- 点击锚点预设菜单(位于矩形变换组件的左上角,即X 轴位置和宽度值左侧的框)。从那里,它显示了一些在游戏中常用的最常见锚点位置,以便于选择。在我们的例子中,我们将想要选择顶部居中选项:

图 4.7 – 在锚点预设菜单中选择顶部居中选项
- 注意在选中后,
-290)。这意味着我们的对象位置为0,对象将沿着 y 轴 的锚点居中,这将使对象的一半位于屏幕之外,这并不好,如以下示例所示:

图 4.8 – 将 Pos Y 更改为 0
我放置了标签以更容易看到问题;您可以通过将标签拖放到屏幕边缘来完成此操作。
小贴士
要重置任何布局更改,您可以在屏幕右上角选择 布局 菜单并选择 默认。
如果我们将对象的值更改为 -25(减去其一半值),它将被正确放置。然而,如果我们决定稍后更改此值,硬编码此值将是一个问题,因为我们必须记得再次调整它。如果我们可以使地图相对于我们的高度边缘在 0 处,那就好多了,幸运的是,我们有这样的属性来解决这个问题。
- 接下来,更改
1,然后如果之前更改了0,则更改0:

图 4.9 – 调整枢轴和 Pos Y 值
如您所见,由于枢轴设置已更改,文本现在紧贴顶部。
枢轴
0、0.5 和 1,并注意旋转的不同之处。
重要提示
注意,如果您在单击对象时按住 Alt + Shift 键,可以通过我之前提到的 锚点预设 菜单设置对象的 枢轴、位置 和 锚点 设置。这样,我们讨论的所有步骤都会同时发生,但在直接使用快捷键之前,了解每一步的含义是一个好主意。
现在我们已经基本了解了如何在 m 空间中工作,让我们开始最终确定我们的 标题 文本 对象。
调整和调整标题文本大小
现在我们已经正确放置了对象,让我们使用以下步骤给标题文本添加一些视觉效果:
- 选择
Endless Roller并设置40。注意现在文本显示为两行,并且不在我们定义的 矩形 变换 组件的大小范围内。

图 4.10 – 用于 TextMeshPro - 文本 (UI) 组件的设置
- 考虑到这一点,向上滚动到
300``50。我们还想让它从世界顶部偏移,所以让我们将-30更改为给它一点偏移:

图 4.11 – 调整标题文本的偏移
对于这个分辨率来说,这看起来很棒;然而,如果我们以更高的分辨率玩游戏,它可能看起来像这样:

图 4.12 – 标题屏幕当前状态
如果您尝试调整屏幕如何根据我们提供的分辨率变化(组件以调整屏幕变化)的 UI,那么 UI 不缩放可能是个好主意。
- 从层次结构组件中选择画布对象,然后从检查器窗口中转到画布缩放器组件。从那里,将UI 缩放模式更改为与屏幕大小缩放。
这里关键的性质是参考分辨率。这是我们想要基于我们的菜单的分辨率——如果分辨率变大,它将放大;如果它变小,它将缩小。您可能已经根据您的原型或您制作的图像文件有了分辨率的概念;然而,为了参考,以下是在撰写本书时的一些最常见的屏幕分辨率。
这里有一些示例 Apple 设备分辨率:
| 设备名称 | 分辨率 |
|---|---|
| iPhone 12 Pro Max/13 Pro Max | 2778 x 1284 |
| iPhone 12/12 Pro/13 Pro | 2532 x 1170 |
| iPhone 12/13 | 2532 x 1170 |
| iPhone 12 mini/13 mini | 2340 x 1080 |
| iPhone 11 Pro Max | 2688 x 1242 |
| iPhone 11 Pro | 2436 x 1125 |
| iPhone 11 | 1792 x 828 |
| iPhone 14 Pro Max | 2796 x 1290 |
| iPhone 14 Pro | 2556 x 1179 |
| iPhone 14 | 2532 x 1170 (与 12/13 相同) |
| iPhone 14 Plus | 2778 x 1284 (与 Phone 12 Pro Max/13 Pro Max 相同) |
| iPhone SE (2020) | 1334 x 750 |
| iPhone XS Max | 1242 x 2688 |
| iPhone XS | 1125 x 2436 |
| iPhone XR | 828 x 1792 |
| iPhone X | 2436 x 1125 |
| iPhone 7 Plus/8 Plus | 1080 x 1920 |
| iPhone 7/8 | 750 x 1334 |
| iPhone 6S Plus | 1080 x 1920 |
| iPhone 6S | 750 x 1334 |
| iPad Pro (第 1-5 代 12.9 英寸) | 2048 x 2732 |
| iPad 第 9 代 | 2160 x 1620 |
| iPad Air 第 4 代 | 2388 x 1668 |
| iPad Mini (第 6 代) | 2266 x 1488 |
这里是一些示例 Android 设备分辨率:
| 设备名称 | 分辨率 |
|---|---|
| 三星 Galaxy S22 Ultra | 3080 x 1440 |
| 三星 Galaxy S22 | 2340 x 1080 |
| 三星 Galaxy Z Fold3 | 2208 x 1768 |
| 三星 Galaxy S20 Ultra/S21 Ultra | 3200 x 1440 |
| 三星 Galaxy S20/S21 | 2400 x 1080 |
| 三星 Note 10+ | 2280 x1080 |
| Google Pixel 5 XL | 2960 x 1440 |
| Google Pixel 5 | 2340 x 1080 |
| Google Pixel 4 XL | 1440 x 2960 |
| Google Pixel 4 | 2280 x 1080 |
| 三星 Galaxy S10/S10+ | 3040 x 1440 |
| Google Pixel 3 XL | 2960 x 1440 |
| Google Pixel 3/3a XL | 2160 x 1080 |
| Google Pixel 3a | 2220 x 1080 |
| 三星 Galaxy S8/S8+ | 2960 x 1440 |
| Google Pixel 2 XL | 2560 x 1312 |
| Nexus 6P | 1440 x 2560 |
| Nexus 5X | 1080 x 1920 |
| Google Pixel/Pixel 2 | 1080 x 1920 |
| Google Pixel XL/Pixel 2 XL | 1440 x 2560 |
| 三星 Galaxy Note 5 | 1440 x 2560 |
| LG G5 | 1440 x 2560 |
| 一加 3 | 1080 x 1920 |
| 三星 Galaxy S7 | 1440 x 2560 |
| 三星 Galaxy S7 Edge | 1440 x 2560 |
| Nexus 7 (2013) | 1200 x 1920 |
| Nexus 9 | 1536 x 2048 |
| 三星 Galaxy Tab 10 | 800 x 1280 |
| Chromebook Pixel | 2560 x 1700 |
注意
要查看流行的手机屏幕分辨率列表,请查看screensiz.es/phone或www.ios-resolution.com/。
我使用的是 Google Pixel 3a XL,分辨率为 2160 x 1080,以及 iPhone 13 Pro Max,分辨率为 2778 x 1284,因此我认为这是一个不错的起点。然而,如果你正在创建艺术资产,最好在计划支持的最高分辨率上创建 UI,然后从那里构建其他分辨率。
Unity 内置了一些最常见的分辨率,这些分辨率可以从之前提到的窗口视图中下拉菜单中查看/更改。
-
如果在
1920 x 1080中还没有的话。 -
接下来,在匹配下,将其全部移动到高度。这将确保当屏幕的高度发生变化时,我们将修改 UI 的缩放比例。
-
接下来,让我们将文本放大一些。选择
1000和200,然后更改130:

图 4.13 – 调整标题文本以放大
- 现在,如果我们以更高的分辨率玩游戏,它将很好地显示我们的标题,并放大以适应更大的尺寸:

图 4.14 – 调整标题屏幕
- 前往游戏视图控制栏并选择一个较小的分辨率,例如800x480 横向(800x480),你会注意到文本会缩小以很好地适应:

图 4.15 – 较小分辨率的标题屏幕
如你所见,画布缩放器组件会根据设备的分辨率调整文本的大小。接下来,我们将看到如何快速测试不同的分辨率。
注意
更多关于画布缩放器组件的信息,请查看docs.unity3d.com/Manual/script-CanvasScaler.html。
选择不同的宽高比
如我之前提到的,在游戏视图中,如果我们进入控制栏并选择第一个选项,会出现一个下拉菜单,我们可以从中选择不同的分辨率来测试我们的游戏,这样我们就可以在导出到我们的设备之前找到潜在的问题:

图 4.16 – 不同分辨率选项
默认情况下,已经为我们内置了多种分辨率,但我们也可以使用底部的+按钮创建自己的分辨率。我建议您为您的手机创建两个新的选择,用于横屏模式和竖屏模式,如果您默认情况下没有包含(在我的情况下,1920 x 1080,1080 x 1920,2778 x 1284,和 1284 x 2778):
因此,到目前为止,我们可以看到在横屏比例下,它工作得相当好,但让我们尝试一个竖屏的:

图 4.17 – 当前竖屏视图
哎呀!目前,文本已经溢出了屏幕的边界。看起来我们需要修复这个问题:
-
选择
200。 -
现在,转到
0.25和0.75:

图 4.18 – 设置锚点值
注意到和值。它们现在已经被和属性所取代,这些属性目前设置为-338和-338。这意味着这个区域距离我们的锚点 25%处-338个单位,并且距离我们的最大锚点 75%处-338个单位。我们希望屏幕调整大小以适应这些锚点,因此我们将两个和值都更改为0。
- 将我们的场景保存为
Scenes文件夹中的新文件,命名为MainMenu,然后玩游戏:

图 4.19 – 标题文本自动调整以适应屏幕
如您在前面的截图中所见,文本现在更适合。您也会注意到,无论我们使用什么分辨率,这段文本所占的空间都适合游戏的标题。现在我们已经正确显示了文本,让我们添加从主菜单正确进入游戏的功能。
与按钮一起工作
与我们的标题不同,对于我们希望玩家触摸的东西,使按钮在每个设备中具有相同的大小是个好主意,因为无论我们使用什么设备,我们的手指大小都是一样的。为了展示一个可能的解决方案,我们将使用不同的缩放技术创建一个新的 Canvas:
-
如果游戏正在运行,请停止游戏。我们首先将当前的
Canvas - Scale w/Screen重命名。这样,我们可以很容易地判断我们是否正在使用正确的 Canvas。 -
现在我们已经准备好了这个,我们可以创建新的一个。转到顶部菜单栏,然后选择
Canvas - Scale Physical。然后,在Canvas Scaler组件下,将UI Scale Mode更改为Constant Physical Size:

图 4.20 – 创建物理 Canvas
使用这种方法,Unity 将尝试调整 Canvas 的大小,以便每个元素都具有相同的物理大小,而不管分辨率如何。由于我们打算制作可以用手指按下的按钮,这很有意义。
- 现在,在层次结构视图中选择此 Canvas(Canvas - Scale Physical),然后转到菜单并选择GameObject | UI | Button - TextMeshPro以在此 Canvas 内创建一个新按钮。
注意
你也可以通过在层次结构窗口中右键单击Canvas - Scale Physical对象并选择UI | Button - TextMeshPro来做到这一点。
在这一点上,你将看到一个名为 child 的新子对象:

图 4.21 – 展示 Text (TMP)对象
下一个问题是什么尺寸的按钮才合适?谷歌在其为 Android 提供的材料指南中建议至少为 48 x 48(短),而苹果在其(WWDC)上推荐至少 44 x 44 dp。无论如何,这大约是 8mm x 8mm,或者 0.3 英寸 x 0.3 英寸。
注意
要阅读材料指南,请查看material.io/design/layout/spacing-methods.html#touch-click-targets。或者,要查看 Material 3 的指南,请查看m3.material.io/components/buttons/specs#85e63496-f905-4978-ae35-69ab83b70536
如果你现在查看游戏并检查一些不同的分辨率选项,你可能会因为按钮的大小而有点害怕,这取决于分辨率:

图 4.22 – 小按钮
这是因为我们的按钮大小假设的是(值)在玩游戏时,我们将在设备上看到的东西更接近:

图 4.23 – 16:9 宽高比的游戏
- 如果游戏正在运行,请停止游戏。之后,从
Play。
注意
如果你感兴趣,想了解你的设备的 DPI 是多少,请查看dpi.lv/。
- 接下来,让我们对按钮对象本身做一些调整。
从检查器窗口顶部的Play Button,使其清楚对象是什么。
- 接下来,转到
0以将按钮居中于屏幕中间。之后,按钮的大小相当大,所以让我们将75``35更改为:

图 4.24 – 调整按钮的大小
我们现在有一个按钮,但它实际上还没有做任何事情。让我们现在修复它。
-
让我们创建一个脚本来包含我们想要的功能。从
Scripts``MainMenuBehaviour。 -
一旦你的 IDE 已经打开,使用以下代码:
using UnityEngine; using UnityEngine.SceneManagement; // LoadScene public class MainMenuBehaviour : MonoBehaviour { /// <summary> /// Will load a new scene upon being called /// </summary> /// <param name="levelName">The name of the level /// we want to go to</param> public void LoadLevel(string levelName) { SceneManager.LoadScene(levelName); } }
LoadLevel函数将根据我们提供的名称加载一个级别,利用 Unity 的场景管理器,我们通过在代码顶部添加一个语句来添加它,以便我们可以访问该命名空间。
-
保存脚本并返回到 Unity 编辑器。要从编辑器调用 Unity 的 UI 事件,我们需要有一个附加了
MainMenuBehaviour组件的游戏对象来调用此函数。我们可以使用当前现有的对象之一,但我们将创建一个新的对象,这样在未来更容易找到它。 -
考虑到这一点,创建一个空的游戏对象(
Main Menu),然后将其添加到MainMenuBehaviour脚本中。然后,将其拖放到 Hierarchy 选项卡顶部,以便将来更容易访问,并为了整洁重置其位置:

图 4.25 – 创建主菜单对象
-
从 Hierarchy 中选择你的 Play Button 对象,转到 Inspector 选项卡,并从那里向下滚动到 Button 组件。然后,在 On Click () 部分,点击 + 图标为我们的按钮添加一些操作。
-
然后,将 Hierarchy 选项卡中的 Main Menu 对象拖放到当前显示为 None (Object) 的区域,该区域已添加到列表中。
-
点击当前显示为
Gameplay的下拉菜单:

图 4.26 – 为按钮添加功能
- 通过访问 文件 | 保存 来保存你的场景。最后,像之前一样,通过访问 文件 | 构建设置 打开 构建设置,并将我们的 MainMenu 场景添加到索引 0 的列表中,通过选择 添加打开的场景 并将 MainMenu 级别拖到顶部,这样主菜单级别将成为我们开始游戏时启动的场景:

图 4.27 – 使用主菜单开始游戏
- 保存你的项目和场景,然后点击 播放 按钮:

图 4.28 – 主菜单当前状态
到目前为止,我们的主菜单运行良好,我们可以通过点击 Play 按钮来无问题地进入游戏:

图 4.29 – 游戏场景当前状态
现在我们已经对 UI 系统有了基础的了解,并且我们有标题屏幕,我们将继续构建大多数游戏都需要的东西:暂停菜单。
添加暂停菜单
在玩游戏时,尤其是移动游戏,可能会有需要随时停止游戏的时候。拥有一个暂停菜单将允许我们的玩家方便地决定何时停止当前游戏状态,并在对他们来说方便的时间重新开始游戏。这还将使我们能够深入探讨使用 Unity 的 UI 系统的一些额外概念,因此,考虑到这一点,让我们开始构建一个:
- 打开
Assets/Scenes文件夹,双击 Gameplay,如果你还没有这样做,保存 MainMenu 级别:

图 4.30 – 打开游戏玩法场景
在我们担心如何打开暂停菜单之前,让我们先创建我们将首先打开的暂停菜单。
-
我们首先要做的是在我们进入暂停菜单时降低屏幕亮度。一个简单的方法是让一个图像缩放以覆盖整个屏幕,这就是面板对象默认所做的。我们可以通过选择游戏对象 | UI | 面板来创建它。请注意,这除了创建面板对象外,还会创建一个Canvas对象和一个EventSystem对象,因为在这个场景中已经不存在面板对象了。
-
将
PauseMenu重命名。然后,从178选择对象:

图 4.31 – 设置面板的颜色
该组件的工作方式与 2D 游戏类似,包括要绘制的图像上的信息和用于它的颜色。
- 切换到游戏窗口以更好地查看面板对象对屏幕所做的工作。当前图像有一个细边框,在这种情况下我不太喜欢。如果你愿意,你可以保留它,但我会将其移除,并将源图像值更改为无(精灵)通过选择当前项并按Delete键:

图 4.32 – 创建背景
现在我们有了这个,我们需要用内容填充菜单。在这种情况下,我们将有一个对象表示游戏已暂停,以及一些按钮允许玩家继续、重新开始或返回主菜单。
- 让我们再创建一个面板来存放我们的暂停菜单内容。我们希望这个面板是暂停菜单对象的子对象,因此我们可以通过转到层次结构窗口,右键单击暂停菜单,然后选择UI | 面板来轻松完成此操作:

图 4.33 – 通过层次结构窗口创建子对象
现在,对于这个面板,我不想让它占据整个屏幕,所以我将使用另一个组件根据我们收到的分辨率来修改其大小。在这种情况下,我将使用纵横比 适配器组件。
-
在
Aspect中。从那里,选择纵横比适配器然后按Enter键。 -
之后,转到我们新添加的组件,并将值更改为
Fit In Parent以确保面板始终适合屏幕,并设置为0.5。这意味着面板的高度将是宽度的两倍(宽度超过高度,即½或0.5)。
如果你进入游戏窗口并切换纵横比,你会注意到菜单将保持相似的形状。
注意
有关纵横比适配器组件的更多信息,请参阅docs.unity3d.com/Manual/script-AspectRatioFitter.html。
-
这很好,但我不想让面板直接粘附到屏幕边缘,所以我们将通过点击 图像 组件旁边的勾选标记使此对象不可见。这将禁用组件并停止组件的功能。
-
然后,右键单击
暂停菜单内容并将10改为在屏幕周围添加边框。 -
我们将使用和上次一样的物理按钮,所以让我们转到 画布 对象,并在 画布缩放器 组件下,将 UI 缩放模式 更改为 常量 物理大小:

图 4.34 – 暂停菜单内容设置
我们可以像之前一样手动放置所有内容,但在这个情况下,我们可能想使用 Unity UI 系统的另一个功能:布局组。
将调整对象的子项大小,以便组件可以自动适应父项的区域。有几种不同的布局组,包括基于网格的、水平和垂直布局组。在我们的情况下,菜单可能将是垂直的。
重要提示
更多关于 Unity 自动创建布局的信息,请参阅 docs.unity3d.com/Manual/UIAutoLayout.html。
-
选择
垂直布局组并按 Enter 键选择 垂直布局组。 -
让我们创建一些子项以适应我们的菜单。从 层次结构 窗口,右键单击 暂停菜单内容 对象,并选择 UI | 按钮 - TextMeshPro。
-
这将创建一个按钮,但你会注意到它看起来几乎和普通按钮一样。让我们打开它的子项
Resume。然后,我们检查0。为了使所有按钮文本都在一行上,我们也可以更改5。 -
之后,选择 暂停菜单内容 对象,在 检查器 窗口中,转到 垂直布局组(脚本) 组件,并将 子项对齐 值更改为 居中。然后,将 子项控制大小 值更改为 宽度 开关。
-
然后,在
5:
这将在布局组的所有子项的每个方向添加五个像素的填充。

图 4.35 – 添加填充
-
现在,复制这个按钮两次,并将文本更改为
Restart和Main Menu。然后,为了容易区分它们,让我们将对象的名称更改为Resume Button、Restart Button和Main Menu Button。 -
接下来,右键单击
Paused030。注意子项在层次结构中的放置顺序会改变它们显示的顺序。考虑到这一点,将 文本 对象拖到顶部:

图 4.36 – 暂停菜单设置
这看起来不错,但这里也有很多间距。所以,如果我们想的话,我们可以将菜单的内容压缩到只适合我们那里。
- 要做到这一点,我们可以转到层次并选择暂停菜单内容对象,然后添加一个内容大小适配器组件。一旦添加,我们将更改垂直适配为首选大小。

图 4.37 – 预设大小选项的影响
- 这会将所有按钮挤在一起,因此我们可以更改
5并在按钮之间添加一些空间:

图 4.38 – 暂停菜单的当前视图
-
现在我们已经有了按钮本身,让我们实际上让它们做些事情。在
Scripts文件夹中创建一个新的 C# 脚本,命名为PauseScreenBehaviour,并双击它以打开您选择的 IDE。 -
一旦打开,使用以下代码:
using UnityEngine; using UnityEngine.SceneManagement; // SceneManager public class PauseScreenBehaviour : MainMenuBehaviour { /// <summary> /// If our game is currently paused /// </summary> public static bool paused; [Tooltip("Reference to the pause menu object to turn on/off")] public GameObject pauseMenu; /// <summary> /// Reloads our current level, effectively /// "restarting" the game /// </summary> public void Restart() { SceneManager.LoadScene(SceneManager .GetActiveScene().name); } /// <summary> /// Will turn our pause menu on or off /// </summary> /// <param name="isPaused"></param> public void SetPauseMenu(bool isPaused) { paused = isPaused; /* If the game is paused, timeScale is 0, otherwise 1 */ Time.timeScale = (paused) ? 0 : 1; pauseMenu.SetActive(paused); } void Start() { /* Must be reset in Start or else game will be paused upon restart */ paused = false; } }
在这个脚本中,我们首先使用一个static变量,它被称为paused。当我们声明一个static变量时,我们确保在这个类内部只有一个这样的变量,所有实例都将共享。这个优点之一是我们可以使用类名后跟一个点然后是属性名(在这种情况下,PauseScreenBehaviour.paused)来访问属性。我们将在稍后想要通过代码打开菜单时使用这个概念。
然后,我们有两个公共函数,我们将通过 UI 元素来调用它们。首先,我们有一个Restart函数,它将使用 Unity 的场景管理器带我们回到当前加载的水平,从而有效地重新启动游戏。需要注意的是,在 Unity 中重新启动时,static变量不会重置,这就是为什么我在Start函数中将paused设置为false,以确保当我们到达水平时,它是未暂停的。
最后,我们有一个SetPauseMenu函数,它将根据isPaused的值打开或关闭暂停菜单。它还设置Time.timeScale属性,其中0表示不会发生任何事情,而1表示正常时间。这个属性将修改Time.deltaTime变量,实际上取消我们在使用它时所有的移动。
-
保存您的脚本,然后回到 Unity。
-
然后,我们将在“暂停屏幕处理程序”中创建一个新的空游戏对象,并将其暂停屏幕行为(脚本)组件附加到它上面。
-
接下来,在“层次”选项卡中将暂停菜单变量分配给暂停菜单游戏对象:

图 4.39 – 分配暂停菜单属性
-
现在我们有了脚本,我们就可以更改按钮以实际执行某些操作了。选择Resume Button对象,转到检查器窗口,然后转到按钮组件的On Click ()部分,点击+按钮以添加要执行的操作。
-
由于未勾选,因此将
false拖放到此处,这应该对我们适用:

图 4.40 – 从继续按钮调用 SetPauseMenu 函数
-
同样,对
Restart函数也进行相同的操作。 -
接下来,对
LoadLevel也进行相同的操作,并将我们主菜单级别的名称放在字符串位置(在我的例子中是MainMenu)。
注意
PauseScreenHandler脚本已经包含了LoadLevel和MainMenuBehaviour类。
- 保存游戏并运行它:

图 4.41 – 游戏当前状态
如前一个屏幕截图所示,如果我们开始游戏,菜单会正确显示——我们可以点击主菜单按钮进入主菜单,继续会继续游戏。
到目前为止,我们有一些问题:一旦菜单消失,就没有办法再恢复它;游戏应该从未暂停的状态开始,实际上游戏应该暂停。让我们接下来解决这些问题。
暂停游戏
要正确暂停游戏,我们将使用以下步骤调整我们之前编写的一些脚本:
-
打开
PlayerBehaviour脚本,并将以下加粗代码添加到FixedUpdate函数中:/// <summary> /// FixedUpdate is a prime place to put physics /// calculations happening over a period of time. /// </summary> void FixedUpdate() { /* If the game is paused, don't do anything */ if (PauseScreenBehaviour.paused) { return; } // Check if we're moving to the side var horizontalSpeed = Input.GetAxis("Horizontal") * dodgeSpeed; // Rest of the FixedUpdate function...
添加的代码使得如果游戏被暂停,我们将在函数内不执行任何操作。
-
然后,我们还需要将相同的脚本添加到
Update函数的顶部:/// <summary> /// Update is called once per frame /// </summary> private void Update() { /* Using Keyboard/Controller to toggle pause menu */ if (Input.GetButtonDown("Cancel")) { // Get the pause menu var pauseBehaviour = GameObject.FindObjectOfType <PauseScreenBehaviour>(); // Toggle the value pauseBehaviour.SetPauseMenu (!PauseScreenBehaviour.paused); } /* If the game is paused, don't do anything */ if (PauseScreenBehaviour.paused) { return; } /* Check if we are running either in the Unity editor or in a standalone build.*/ #if UNITY_STANDALONE || UNITY_WEBPLAYER || UNITY_EDITOR // Rest of the Update function… -
保存你的脚本并返回到 Unity 编辑器。现在,游戏默认应该是未暂停的,所以让我们继续在层次结构视图中选择暂停菜单对象,然后在检查器视图中点击活动按钮来禁用它:

图 4.42 – 禁用暂停菜单
- 保存你的场景,然后继续玩游戏。在游戏过程中,按Esc键,你应该会看到暂停菜单出现,游戏被暂停!我们也可以使用继续或再次按Esc来恢复游戏。

图 4.43 – 游戏当前状态
这很好,但这只会在我们的玩家连接了键盘或控制器时才起作用。在下一章中,我们将看到如何有一个玩家可以点击以打开菜单的按钮。此外,我们可能还希望能够通过一些屏幕上的视觉 UI 菜单来移动玩家。这就是我们接下来要解决的问题。
摘要
这样,我们就为创建移动游戏的 UI 元素打下了良好的基础。我们首先介绍了如何创建标题屏幕,利用按钮和文本对象。然后,我们介绍了如何使用面板、按钮、文本和布局组来使你的菜单适应元素的大小。我们还简要介绍了布局组如何以令人愉悦的方式排列我们的对象。
在下一章中,我们将继续探索游戏用户界面(UIs),通过了解如何添加暂停屏幕按钮和屏幕上的摇杆,以及调整我们的 GUI 以适应刘海屏设备。
第五章:高级移动 UI
在上一章中,我们介绍了 Unity UI 系统以及如何构建分辨率无关的 UI 元素,这对于利用不同宽高比和分辨率的所有游戏项目都很有用。在本章中,我们将探讨一些与 UI 相关的特定于移动的方面,例如需要屏幕控制以及将我们的 UI 适配到带有刘海的设备。
本章将分为多个主题。本章是一个从开始到结束的简单步骤过程。以下是我们任务的概述:
-
添加暂停屏幕按钮
-
实现屏幕摇杆
-
适配带刘海的设备的 GUI
在本章的整个过程中,我们将把我们在上一章中实现的暂停屏幕适配到移动设备上。然后,我们将实现一个屏幕上的摇杆作为额外的移动选项,最后,让我们的 UI 自动适应带有刘海的移动设备。
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小更改即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以在 Unity 的下载存档中找到它,地址为unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
您可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter05。
添加暂停屏幕按钮
虽然许多移动游戏都支持通过蓝牙控制器,但其中大多数,如果不是全部,都允许用户仅通过设备来控制游戏。越来越多的移动游戏将包括屏幕按钮或模拟摇杆,玩家可以使用它们来控制他们的角色。在本节中,我们将看到如果我们希望,我们如何实现这一点。
首先,让我们构建一个暂停菜单按钮:
- 由于我们将要创建多种类型的屏幕控制,让我们创建一个面板来容纳它们。从“屏幕控制”中,从检查器视图中移除或禁用图像组件,因为我们不需要看到图像。
对于我们控制版本的这一版,我们将使用一些 2D 精灵来使识别各种 UI 元素更容易。如果您想使用与我使用的完全相同的精灵,这些精灵包含在本书的示例代码中。
- 从
Sprites中拖放图像文件到新添加的文件夹中。由于我们的项目是 3D 的,Unity 假设我们想要它们作为纹理,但我们想使用 Unity 的 UI 系统。考虑到这一点,选择所有三个精灵。从 Inspector 中,将 Texture Type 改为 Sprite (2D and UI),然后滚动到最底部,然后点击 Apply 按钮。
备注
这里使用的精灵来自 Kenney 的 Onscreen Controls 套件。您还可以使用其他七种可能的样式,这些样式可在kenney.nl/assets/onscreen-controls找到。
-
现在我们有了精灵,让我们构建我们的第一个 UI 元素,一个暂停按钮。从 Hierarchy 视图中,右键单击 On Screen Controls 对象,然后选择 UI | Button - TextMeshPro。
-
将新对象
Show Pause Button重命名,并使用 Anchor Presets 菜单将对象放置在屏幕的右下角(使用 Alt+ Shift 设置 Pivot 和 Position)。

图 5.1 – 右下角选项
-
然后,从 Image 组件中,将我们的 pauseButton 精灵拖放到 Source Image 属性中。您会注意到它被拉伸了,所以点击 Set Native Size 按钮让精灵自动调整大小。
-
我们实际上不需要包含文本对象,所以选择 Text (TMP) 对象并按 Delete 键。

图 5.2 – 创建暂停按钮
-
返回并选择
pause。 -
返回到 Resume Button 并为其按钮添加另一个事件,当离开时重新开启 Show Pause Menu 按钮。

图 5.3 – 创建恢复按钮点击动作
现在,我们希望在暂停游戏时移除我们的屏幕控制。这可以通过 Inspector 完成,但也可以通过代码完成。
-
打开
PauseScreenBehaviour脚本,并将以下属性添加到脚本中:[Tooltip("Reference to the on screen controls menu")] public GameObject onScreenControls; -
之后,更新 SetPauseMenu 函数以包含以下新行:
/// <summary> /// Will turn our pause menu on or off /// </summary> /// <param name="isPaused">is the game currently paused</param> public void SetPauseMenu(bool isPaused) { paused = isPaused; /* If the game is paused, timeScale is 0, otherwise 1 */ Time.timeScale = (paused) ? 0 : 1; pauseMenu.SetActive(paused); onScreenControls.SetActive(!paused); }
注意,我们使用的是 !paused 而不是上一行中的 paused。! 操作符会将 true 的值转换为 false,反之亦然。这将导致当游戏未暂停时 onScreenControls 窗口打开,当游戏暂停时关闭。
由于我们已经在代码编辑器中,我们也可以利用这个时间修复将来可能出现的问题:如前所述,一个现在不明显的问题(除非你重新启动关卡)是 static 变量会在每次重新加载游戏时保持其值。在我们的例子中,我们设置了 paused,它将 Time.timeScale 设置为 0。幸运的是,我们可以相当容易地修复这个问题。
-
打开
PauseScreenBehaviour脚本,并更新Start函数,替换原始行:void Start() { /* Must be reset in Start or else game will be paused upon * restart */ SetPauseMenu(false); } -
返回到 Unity 并转到 Pause Screen Handler 对象。从 Pause Screen Behaviour 脚本中,将 On Screen Controls 属性设置为我们的 On Screen Controls 对象。

图 5.4 – 更新暂停屏幕行为
- 保存你的脚本和场景,然后玩游戏:

图 5.5 – 游戏当前状态
暂停菜单现在工作正常。这是一种简单的方法将屏幕控制添加到屏幕上。更高级的版本将是一个我们可以用来控制玩家移动的模拟摇杆。让我们接下来处理这个问题。
实现屏幕摇杆
为了实现这个屏幕摇杆,我们将利用两个图像:一个背景图像,然后是一个放置在背景图像上的摇杆图像。然后我们将编写代码,允许玩家模拟他们正在物理移动摇杆。稍后,我们将学习如何让模拟摇杆正确地影响游戏:
-
右键点击
Joystick Background。 -
从
analogStickBackground精灵处点击设置原生 大小按钮。 -
从 Rect Transform 组件中,按住 Alt + Shift 并使用 Anchor Presets 菜单将 Joystick Background 移动到底部左选项。
-
接下来,右键点击
Joystick。 -
从
analogStick精灵处点击设置原生 大小按钮。

图 5.6 – 创建摇杆 UI
-
我们希望这个摇杆可以移动,因此为了做到这一点,我们将创建一个新的脚本。从
Scripts文件夹中创建一个新的 C# 脚本,命名为MobileJoystick。然后,将MobileJoystick脚本附加到Joystick对象上。 -
打开代码编辑器,定位到
MobileJoystick脚本,并在Start函数中添加以下属性及其初始化:/// <summary> /// A reference to this object's RectTransform /// component /// </summary> RectTransform rt; /// <summary> /// The original position of the stick used to /// calculate the offset of movement /// </summary> Vector2 originalAnchored; // Start is called before the first frame update void Start() { rt = GetComponent<RectTransform>(); originalAnchored = rt.anchoredPosition; } -
要在拖动摇杆时让摇杆执行某些操作,我们可以在脚本中添加一个接口,用于拖动和停止拖动时。为此,我们需要在脚本顶部添加以下
using语句:using UnityEngine.EventSystems; /* IDragHandler, IEndDragHandler */ -
之后,我们在类定义中添加以下加粗代码:
public class MobileJoystick : MonoBehaviour, IDragHandler, IEndDragHandler -
现在,我们会得到一些错误,因为我们还没有实际定义接口中给出的函数,让我们现在就定义它们:
/// <summary> /// Will allow the user to move the joystick /// </summary> /// <param name="eventData">Information about the /// movement, we are only /// using the position</param> public void OnDrag(PointerEventData eventData) { /* We use our parent's info since the joystick moves */ var parent = rt.parent.GetComponent<RectTransform>(); var parentSize = parent.rect.size; var parentPoint = eventData.position - parentSize; /* Calculate the point relative to the parent's local space */ Vector2 localPoint = parent.InverseTransformPoint(parentPoint); /* Calculates what the new anchor point should be */ Vector2 newAnchorPos = localPoint - originalAnchored; /* Prevent the analog stick from moving too far */ newAnchorPos = Vector2.ClampMagnitude( newAnchorPos, parentSize.x/2); rt.anchoredPosition = newAnchorPos; } /// <summary> /// Will be called when the player lets go of the /// stick /// </summary> /// <param name="eventData">Information about the /// movement, unused</param> public void OnEndDrag(PointerEventData eventData) { /* Reset the stick to it's original position */ rt.anchoredPosition = Vector3.zero; } -
保存你的脚本并返回到 Unity 编辑器。玩游戏并尝试点击并拖动模拟摇杆:

图 5.7 – 摇杆现在可以移动
-
现在它们功能上已经工作,让我们让它们真正影响游戏。我们需要有一种方式来传达
MobileJoystick的信息。为此,让我们添加一个新的属性:/// <summary> /// Gets the value of the joystick in a -1 to 1 /// manner in the same way that Input.GetAxis does /// </summary> public Vector2 axisValue; -
接下来,将以下行添加到
OnDrag函数中:// Update the axis value to the new position axisValue = newAnchorPos / (parentSize.x / 2); -
然后,将以下行添加到
OnEndDrag函数中:axisValue = Vector2.zero; -
现在我们需要进入
PlayerBehaviour脚本。从那里,我们将添加一个新变量来告诉我们是否有MobileJoystick:private MobileJoystick joystick; // Start is called before the first frame update public void Start() { // Get access to our Rigidbody component rb = GetComponent<Rigidbody>(); minSwipeDistancePixels = minSwipeDistance * Screen.dpi; joystick = GameObject.FindObjectOfType <MobileJoystick>(); } -
这样,如果玩家已经关闭了
MobileJoystick,我们仍然希望游戏能够运行。 -
接下来,我们需要更新
FixedUpdate函数,进行以下更改:/// <summary> /// FixedUpdate is a prime place to put physics /// calculations /// happening over a period of time. /// </summary> void FixedUpdate() { /* If the game is paused, don't do anything */ if (PauseScreenBehaviour.paused) { return; } // Check if we're moving to the side var horizontalSpeed = Input.GetAxis("Horizontal") * dodgeSpeed; /* If the joystick is active and the player is moving the joystick, override the value */ if (joystick && joystick.axisValue.x != 0) { horizontalSpeed = joystick.axisValue.x * dodgeSpeed; } /* Check if we are running either in the Unity editor or in a standalone build.*/ #if UNITY_STANDALONE || UNITY_WEBPLAYER || UNITY_EDITOR /* If the mouse is held down (or the screen is tapped on Mobile */ if (Input.GetMouseButton(0)) { if(!joystick) { var screenPos = Input.mousePosition; horizontalSpeed = CalculateMovement(screenPos); } } /* Check if we are running on a mobile device */ #elif UNITY_IOS || UNITY_ANDROID switch (horizMovement) { case MobileHorizMovement.Accelerometer: /* Move player based on accelerometer direction */ horizontalSpeed = Input.acceleration.x * dodgeSpeed; break; case MobileHorizMovement.ScreenTouch: /* Check if Input registered more than zero touches */ if (!joystick && Input.touchCount > 0) { /* Store the first touch detected */ var firstTouch = Input.touches[0]; var screenPos = firstTouch.position; horizontalSpeed = CalculateMovement(screenPos); } break; } #endif rb.AddForce(horizontalSpeed, 0, rollSpeed); } -
保存你的脚本并返回到 Unity 编辑器。保存你的场景然后玩游戏:

图 5.8 – 操纵杆移动玩家
这样,如果我们游戏开始时启用了操纵杆,游戏将使用它来移动玩家。或者,你也可以禁用操纵杆,你的游戏将和之前一样工作。
在这个阶段,我们的用户界面应该适用于绝大多数手机。然而,有一些手机含有“刘海”。我们将在下一节中看到如何调整我们的用户界面以适应这种情况。
适应刘海设备 GUI
自从这本书的第一版以来,已经有许多手机推出了传感器外壳,更常见的是“刘海”。随着 iPhone X 的流行,这已经成为现在许多手机的一部分。尽管有些人在线上声称全面屏显示是未来,但 iOS 设备、运行 9.0 及以上版本的 Android 设备和 Unity 都添加了对设备内置刘海的支持,我们可以在 Unity 中使用Screen.safeArea属性来确保所有内容都可见。
要开始,我们将首先进入主菜单来调整菜单文本:
-
前往
Scenes文件夹中的MainMenu场景。在添加暂停菜单部分,我们看到了如何使用Panel对象来包含我们想要显示的内容。我们将使用这个概念来处理安全区域。 -
在打开关卡后,转到层次视图,通过右键单击Canvas - Scale w/Screen对象并选择UI | Panel来为我们的标题屏幕创建一个子面板。
-
之后,通过拖放对象到新创建的Panel对象上,将Title Text设置为新创建面板的子项:

图 5.9 – 安全区域设置
-
从
Scripts``UISafeAreaHandler。双击它以打开你的代码编辑器并使用以下代码:using UnityEngine; public class UISafeAreaHandler : MonoBehaviour { RectTransform panel; // Start is called before the first frame update void Start() { panel = GetComponent<RectTransform>(); } // Update is called once per frame void Update() { Rect area = Screen.safeArea; /* Pixel size in screen space of the whole screen */ Vector2 screenSize = new Vector2(Screen.width, Screen.height); /* Set anchors to percentages of the screen used. */ panel.anchorMin = area.position / screenSize; panel.anchorMax = (area.position + area.size) / screenSize; } }
Screen.safeArea属性返回一个Rect类型的变量,它包含 X 和 Y 位置以及宽度和高度,就像Screen.safeArea将只返回Rect(0, 0, Screen.width, Screen.height),由于没有刘海,这将有效。
使用Screen.safeArea``Update函数进行修改。
我们之前看到锚点可以用来指定面板的大小。锚点在视口空间中工作,也就是说,值从(0, 0)到(1, 1)。由于Rect``Screen.safeArea在屏幕(像素)空间中,我们通过像素屏幕大小来除以转换为视口空间的点。
保存脚本并返回 Unity 编辑器。然后,将 UI 安全区域处理器 组件附加到我们刚刚创建的 Panel 对象上。
- 返回 Unity 编辑器并切换到
Screen.safeArea属性,你应该会注意到面板会相应地调整其大小以适应我们的屏幕:

图 5.10 – 调整缺口值
设备模拟器 是一个旨在让开发者看到他们的游戏在许多设备上看起来如何的工具。有关更多信息,请参阅 docs.unity3d.com/Manual/device-simulator-view.html。
在竖屏模式下,屏幕顶部被切断以适应缺口,底部被切断以适应主页按钮。我们还可以点击 Rotate 文字旁边的按钮,看看我们的游戏在另一侧面对的设备上的样子:

图 5.11 – 横屏模式
切换到横屏模式,我们失去了缺口左右两侧的空间,在 iOS 上,它还会切断另一侧。就像在竖屏模式下一样,顶部会被切断以适应主页按钮。
通过这种方式,我们可以看到菜单正确地调整了自己!然而,有可能 Play 按钮不再工作。这是因为我们的两个 Canvas 对象都绘制在相同的排序顺序中,这意味着它们可以相互覆盖,类似于如果你过去在 2D 游戏中工作过的 Z-fighting 概念。幸运的是,我们可以很容易地解决这个问题。
- 选择
-1。带有0的按钮将始终位于此 Canvas 内容的顶部。
虽然半透明的白色面板在说明概念时很有用,但我们不希望用户在游戏进行时看到它。考虑到这一点,让我们关闭图像。
- 选择 Panel 对象。从 Image 组件中,取消勾选组件名称左侧的复选框以禁用它。
小贴士
如果你仍然希望图像可见并且按钮仍然可以工作,你可以取消勾选 Raycast Target 属性。
现在第一个 Canvas 已经完成,我们也可以对另一个 Canvas 执行相同的操作:
- 前往 Canvas - Scale Physical 组件,并创建另一个带有 UI 安全区域处理器 组件的 Panel 对象,确保禁用 Image 组件。接下来,将 Play 按钮设置为它的子对象:

图 5.12 – 调整安全区域处理器
-
保存你的场景。现在我们已经完成了主菜单,我们也可以调整
Gameplay场景。 -
打开
Gameplay场景,选择 On Screen Controls 对象,然后只需将其 UI 安全区域处理器 组件添加到它上:

图 5.13 – 使屏幕控制使用 UI 安全区域
-
要调整暂停菜单,我们不想改变暂停菜单对象,因为我们希望在缺口区域也有黑色屏幕。我们之前创建了一个面板来存放暂停菜单的内容,但该对象正在使用宽高比适配器,这将会覆盖我们在代码中做出的任何锚点更改。为了保持这种功能以及子对象中的内容大小适配器,我们只需创建一个父面板来充当容器。
-
通过在层次结构窗口中选择它并然后在检查器窗口中点击其名称旁边的复选框来使暂停菜单对象再次激活。在面板对象上右键单击,并通过右键单击并选择UI | 面板来创建一个新的面板对象。在新面板中,添加UI 安全区域处理器组件并禁用图像组件:

图 5.14 – 调整暂停菜单以适应缺口
-
最后,由于我们不再与暂停菜单对象一起工作,请从层次结构窗口中选择暂停菜单对象,然后在检查器窗口中,取消选中该名称旁边的复选框以禁用对象。
-
保存您的场景并播放游戏:

图 5.15 – UI 现在正确响应缺口
如您所见,如果我们按住空格键,我们可以看到两个菜单都在正确工作!
摘要
在本章中,我们将暂停菜单整合到我们的游戏本身中,并使其与我们的项目中的所有内容协同工作。然后我们看到了如何创建屏幕控制,以便玩家可以使用移动设备以另一种方式与游戏互动。最后,我们看到了如何让我们的游戏自动适应以适应分配的安全区域,以处理手机上的缺口。我们将在后面的章节中更深入地探讨这些概念,所以请记住这些解释。
在下一章中,我们将深入探讨盈利模式,并看看我们如何将 Unity 广告添加到我们的项目中。
第六章:实现应用内购买
如第七章中所述,使用 Unity Ads 进行广告宣传,当涉及到在移动平台上销售您的游戏时,有许多选择。如果您决定采用免费游玩模式,除了显示广告外,还可以通过使用应用内购买(IAPs)来向人们销售额外的内容和/或优势。这可以是一种吸引您的游戏用户并将他们从免费玩家转变为付费客户的方式。
通常,这些可以是移除广告或向玩家提供主题等选项,但您也可以做一些事情,比如解锁新关卡和添加额外内容,这样沉迷于您游戏的人们就会争相给您更多的时间。或者,您也可以将您的 IAPs 视为玩家想要购买以增强他们的游戏体验的项目,例如增强和升级。
在本章中,我们将 Unity 的 IAP 系统集成到我们的项目中,并查看如何创建一个既适用于消耗性内容又可永久解锁功能的 IAP。到本章结束时,我们将看到如何设置 Unity 的 IAP 系统并创建我们的第一个可能的购买项目,然后我们将看到如何在某些设备上恢复购买,在查看各种应用商店的额外资源之前。
本章分为多个主题。它包含了一个从开始到结束的简单分步过程。以下是我们任务的概述:
-
设置 Unity IAP
-
创建我们的第一个购买
-
添加一个按钮以恢复购买
-
配置您选择的商店的购买
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小更改即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
您可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter06%20and%2007。
设置 Unity IAP
Unity IAP 是一项服务,允许我们在游戏项目中向玩家销售各种不同的物品,并且默认支持 iOS 应用商店、Mac 应用商店、Google Play、Windows 商店、Amazon Appstore 等。因此,使用这项服务,我们可以轻松地在许多不同的地方销售我们的物品。我们已经在 第七章 中设置了 Unity 服务,使用 Unity Ads 进行广告宣传,所以这将更容易开始。执行以下步骤以添加 Unity IAP:
-
通过访问 窗口 | 包管理器 来打开 服务 窗口,然后在顶部工具栏上点击 服务 按钮。
-
从那里,向下滚动到 应用内购买 项,然后点击 安装 按钮。

图 6.1:包管理器中的应用内购买选项
- 将会弹出一个窗口,询问你是否确认要激活 购买 服务包。点击 确定 按钮。

图 6.2:激活购买服务
- 完成后,您可以关闭 包管理器,然后通过访问 编辑 | 项目设置 并进入 服务 部分来进入 项目设置 菜单。如果一切顺利,您应该看到一个 应用内购买 菜单部分。选择它,您应该会注意到它已被切换为开启状态。
重要提示
IAP 包是从主引擎外部创建的,因为代码旨在非常灵活,可以更新以适应任何需要的政策。然后我们只需更新包,而不是更新到 Unity 的最新版本,这在处理大型项目时可能非常重要。

图 6.3:应用内购买已启用
现在我们已经将 IAP 系统引入到我们的项目中,我们可以利用它来为玩家创建第一个可购买对象。
创建我们的第一个购买
要创建我们的第一个应用内购买,我们将利用 Unity 项目中刚刚添加的一个功能,即无代码 IAP。它被称为无代码 IAP,因为您不需要为实际的 IAP 交易编写任何代码,只需编写定义用户在购买时获得什么的脚本。这是将 IAP 集成到 Unity 游戏中迄今为止最简单的方法,也是在我们项目中尝试 IAP 的好方法。
最常见的 IAP 之一是能够在移动游戏中禁用广告。使用以下步骤,让我们通过创建一个按钮来添加这个功能,当点击该按钮时,将禁用广告:
-
通过访问
Assets/Scenes文件夹,并双击MainMenu文件来打开主菜单层级。 -
从那里,如果尚未在场景窗口中,请返回,然后单击2D按钮进入 2D 模式,因为我们将处理 UI。
-
我们首先需要有一些东西可以出售,为此,我们将使用 IAP 目录,我们可以通过转到服务 | 内购 | IAP 目录…来访问它:

图 6.4:打开 IAP 目录
一旦到达菜单,它应该看起来像这样:

图 6.5:IAP 目录
现在,我们首先需要为我们的产品创建一个 ID,这是我们将在不同的应用商店中识别我们的产品的方式。在我们的例子中,让我们使用removeAds。然后,在类型下,将其更改为非消耗品:

图 6.6:创建 IAP
我们所说的非消耗品是指玩家只需购买一次,游戏会记住这一点以备后用。其他的是消耗品,这意味着它们用于可以重复购买的东西,例如特殊增强和订阅。这些提供了一定时间内的内容访问权限,可能直到用户取消为止。
-
接下来,我们可以通过单击窗口右上角的X来关闭IAP 目录。
-
在层次结构窗口中选择Canvas - Scale Physical对象。从那里,选择服务 | 内购 | 创建 IAP 按钮,我们应该在我们的场景中看到一个新按钮被创建:

图 6.7:创建 IAP 按钮
此按钮将用于执行内购以移除游戏中的广告。当前按钮使用 Unity 的旧版文本系统,但可以轻松调整以使用TextMeshPro。
为了确保开始和移除广告按钮都能正确显示在屏幕上,我们将创建一个可以容纳这两个按钮的菜单。这意味着我们需要为我们的安全区域面板创建另一个面板作为子面板。
从SafeAreaHolder。
-
之后,创建一个子
SafeAreaHolder,并让它像之前一样填充整个屏幕。添加一个10。 -
然后,添加一个内容大小适配器组件,并将垂直适配和水平适配字段设置为首选大小。
-
将新添加的按钮重命名为
Remove Ads Button,然后在RemoveAds处添加一个10。
小贴士
关于这些说明的含义以及每个步骤的作用的提醒,请参阅第四章,分辨率无关 UI。
- 最后,将两个按钮拖放到面板对象上,播放按钮在上半部分,移除广告按钮在其下方,如下所示:

图 6.8:将移除广告按钮添加到场景中
- 接下来,
IAP Button类有一个购买完成(产品)函数,它的工作方式与我们在过去使用Button组件时使用的On Click类似。考虑到这一点,我们需要创建一个当玩家按下按钮时我们希望调用的函数。
在第七章**,使用 Unity Ads 进行广告投放*中,我们在UnityAdController类内部创建了一个名为showAds的static变量。我们将使用这个变量来检查是否应该显示广告。
我们需要打开MainMenuBehaviour脚本,并将以下函数添加到类中:
public void DisableAds()
{
UnityAdController.showAds = false;
/* Used to store that we shouldn't show ads */
PlayerPrefs.SetInt("Show Ads", 0);
}
protected virtual void Start()
{
/* Initialize the showAds variable */
bool showAds = (PlayerPrefs.GetInt("Show Ads", 1)
== 1);
UnityAdController.showAds = showAds;
}
在这里,我们正在使用 Unity 的PlayerPrefs系统来保存是否应该向玩家显示广告。PlayerPrefs很酷,因为它可以在游戏的多次游玩之间保存信息,并且常用于如高分和玩家偏好设置(因此得名)。为了测试重置属性,你可以进入PlayerPrefs,如果应用被卸载或应用数据被清除,它可能会被移除,因此我们稍后会添加一个恢复购买按钮,允许玩家在允许的平台恢复他们的购买。对于不允许的平台,你需要向服务器发起 API 调用以检查当前用户是否已经购买了不可消耗的内购项目。更多细节将在本章的添加恢复购买按钮部分中介绍。
注意
关于PlayerPrefs的更多信息,请查看docs.unity3d.com/ScriptReference/PlayerPrefs.html。
注意,我将Start函数设置为virtual,这意味着继承的类也可以将其用作自己脚本的基石。我们还标记了该函数为protected,它与private函数的作用相同,但它也可以在子类中访问。
-
考虑到这一点,我们还需要更新
PauseScreenBehaviour的Start函数,如下所示:protected override void Start() { /* Initialize Ads if needed */ base.Start(); if (!UnityAdController.showAds) { /* If not showing ads, just start the game */ SetPauseMenu(false); } }
override关键字将替换Start的默认行为。然而,当我们调用base.Start()时,我们确保了MainMenuBehaviour中的先前内容将被调用——在这种情况下,我们确保UnityAdController设置了正确的值。
-
最后,我们需要调整
ObstacleBehaviour脚本以处理不播放广告的情况。更新ShowContinue函数如下:// Other code above... /* Come back after 1 second and check again */ yield return new WaitForSeconds(1f); } else if (!UnityAdController.showAds) { /* It's valid to click the button now */ contButton.interactable = true; /* If player clicks on button we want to just continue */ contButton.onClick.AddListener(Continue); UnityAdController.obstacle = this; /* Change text to allow continue */ btnText.text = "Free Continue"; /* We can now leave the coroutine */ break; } else { /* It's valid to click the button now */ contButton.interactable = true; // More code below... -
我们还需要对
ResetGame方法进行轻微调整,通过删除或注释以下行:/*If we find the button, we can use it */ if (continueButton) { //if (UnityAdController.showAds) //{ // If a player clicks on a button, we want to play an ad // and then continue StartCoroutine(ShowContinue(continueButton)); //} //else //{ /* If we can't play an ad, no need for the continue button */ // continueButton.gameObject.SetActive(false); //} } -
保存你的脚本并进入 Unity。
-
从
Main Menu对象到Runtime Only下拉菜单下的小框。然后,从右侧下拉菜单中选择Main Menu Behaviour | DisableAds:

图 6.9:将 DisableAds 函数调用添加到购买
- 现在,保存我们的场景并开始游戏:

图 6.10:购买菜单看起来工作正常
现在,如果我们点击移除广告按钮,它会询问我们是否想要进行购买。如果我们这样做,它将确保当我们进入游戏时,没有广告。同样,现在当我们死亡时,它将显示一个免费 继续按钮:

图 6.11:确保购买工作正常
有了这个,我们现在已经在 Unity 中创建了一个简单的购买。
注意
如果你想要了解更多关于无代码 IAP 的信息,请查看docs.unity3d.com/Manual/UnityIAPCodelessIAP.html。
有了这个,你现在可以构建你想要在游戏中拥有的任何产品。然而,某些平台对恢复先前购买的功能也有要求。在下一节中,我们将看到如何做到这一点。
添加一个恢复购买的按钮
在支持它的平台上(特别是 Google Play 和通用 Windows 应用程序),如果你购买了某个产品,卸载,然后使用 Unity IAP 重新安装游戏,它将自动恢复用户在重新安装后的第一次初始化期间拥有的任何产品。
对于 iOS 用户,由于苹果要求他们在之前重新验证密码,因此用户必须能够通过按钮恢复他们的购买。如果不这样做,将阻止我们的游戏在 iOS 应用商店被接受,所以如果我们希望在那里部署,包含这个功能是个好主意。让我们看看如何做到这一点:
-
前往层次结构窗口并选择移除广告按钮对象。一旦选中,通过按Ctrl + D来复制它。
-
通过选择它并在检查器窗口中将名称更改为
Restore Button来更改副本的名称。 -
从
恢复购买也是如此。 -
现在,选择恢复对象,然后在IAP 按钮组件中,转到按钮类型并选择恢复:

图 6.12:添加恢复按钮
你应该注意,IAP 按钮组件的属性已经更改,现在只能设置按钮类型,因为没有其他可以自定义的内容。
-
保存你的场景并跳入 Unity。
-
当你开始游戏并尝试点击恢复时,你会在控制台窗口看到一个警告,指出这不是一个受支持的平台:

图 6.13:尝试在 Windows 上恢复时的警告
因此,考虑到这一点,我们可以调整我们的游戏,使得按钮只有在当前运行在受支持的平台上时才会显示。
-
前往
脚本文件夹,创建一个名为RestoreAdsChecker的 C#脚本。一旦打开,使用以下脚本:using UnityEngine; /// <summary> /// Will show or remove a button depending on whether /// we can restore ads or not /// </summary> public class RestoreAdsChecker : MonoBehaviour { // Use this for initialization void Start() { bool canRestore = false; switch (Application.platform) { // Windows Store case RuntimePlatform.WSAPlayerX86: case RuntimePlatform.WSAPlayerX64: case RuntimePlatform.WSAPlayerARM: // iOS, OSX, tvOS case RuntimePlatform.IPhonePlayer: case RuntimePlatform.OSXPlayer: case RuntimePlatform.tvOS: canRestore = true; break; } gameObject.SetActive(canRestore); } }
此脚本遍历 Unity 的IAPButton类中列出的所有商店,如果它们是可以恢复的,我们将canRestore设置为true;否则,它将保持false。最后,如果我们无法恢复它,我们将删除对象,而无需为不同的构建创建特定的事物。
-
保存脚本并返回 Unity。
-
将我们新创建的
RestoreAdsChecker组件附加到恢复****按钮对象:

图 6.14:添加 Restore Ads Checker 组件
- 保存您的项目并启动游戏:

图 6.15:RestoreAdsChecker 组件的结果
现在,由于我们在游戏的 PC 构建中添加了RestoreAdsChecker组件,恢复按钮不会显示,但如果我们导出为 iOS,它将显示在我们的设备上!
注意
关于恢复交易和该功能如何工作的更多信息,请查看docs.unity3d.com/Manual/UnityIAPRestoringTransactions.html。
这样可以确保我们的游戏在每个支持该特性的不同平台上都有这个特定的功能。考虑到这一点,我们将接下来查看一些具体的商店和位置,您可能希望在游戏中添加对 IAP 的支持。
配置您选择的商店的购买
很遗憾,书中没有足够的空间一步一步地介绍每个商店的过程,但我有一些页面可以供您参考,以了解以下商店的整个流程:
-
苹果应用商店和 Mac 应用商店:
docs.unity3d.com/Manual/UnityIAPAppleConfiguration.html -
Google Play 商店:
docs.unity3d.com/Manual/UnityIAPGoogleConfiguration.html -
Windows 商店:
docs.unity3d.com/Manual/UnityIAPWindowsConfiguration.html -
亚马逊应用商店:
docs.unity3d.com/Manual/UnityIAPAmazonConfiguration.html
当尝试使用相同的构建发布到多个 Android IAP 商店(如三星和谷歌)时,可能会遇到一些潜在问题。您可以在docs.unity3d.com/Manual/UnityIAPCrossStoreInstallationIssues.html找到解决这些问题的信息。
摘要
在本章中,我们介绍了如何通过在项目中使用 Unity 来创建内购项目。我们首先讲解了如何设置 Unity 的内购系统,然后深入探讨了如何使用无代码内购轻松添加可购买项目到游戏中。接着,我们创建了在卸载和重新安装游戏时恢复购买的功能,并讨论了根据我们想要的目标商店,我们可以去哪里设置我们的购买。这些新技能使您能够从游戏中获得额外收入,同时允许您针对多个商店和平台,使更多的人能够看到它。
当然,拥有所有这些赚钱的方式,如果没有人玩我们的游戏,那也是无济于事的。在下一章中,我们将学习如何利用社交媒体分享我们的得分,并吸引其他玩家对我们游戏标题的兴趣。
进一步阅读
想要了解更多关于提升免费增值策略的小技巧,我建议您阅读 Pepe Agell 在www.chartboost.com/blog/inapp-purchases-for-indie-mobile-games-freemium-strategy上发表的文章。
第七章:使用 Unity Ads 进行广告
当您在制作移动游戏时,您需要考虑您将如何销售您的游戏。决定如何最好地销售游戏可能很困难。当然,您可以以价格出售您的游戏,并且有可能成功,但您将限制您的受众数量到一个非常低的水平。这可能对细分市场游戏很有用,但如果您试图制作一款具有广泛吸引力的游戏,您希望尽可能多的玩家玩您的游戏,您可能会遇到一些问题。
游戏定价可能成为吸引初始顾客的主要障碍,这些顾客将通过口碑分享游戏并帮助更多人玩您的游戏。为了解决这个潜在问题,您确实有选择将您的游戏免费化的选项。
之后,您可以在玩家玩游戏时给他们提供购买物品或展示广告的机会。
这并不是说在免费游戏中放很多广告就是最好的选择。广告过多,甚至是不合适的广告,可能会驱使用户离开,这可能会更糟。许多开发者都有自己的观点,关于是否使用广告是个好主意,但这不是本章的目的。在本章中,我们将探讨我们在游戏过程中可用的不同广告选项,并展示如何实现它们,如果您选择将此内容添加到您的游戏中。
本章分为几个主题。它包含一个从开始到结束的简单分步过程。以下是我们任务的概述:
-
设置 Unity Ads
-
创建一个简单的广告
-
添加内联回调方法
-
带有奖励的选择加入广告
-
集成冷却计时器
在本章中,我们将 Unity Ads 框架集成到我们的项目中,并学习如何创建简单和复杂的广告版本。这是通过首先设置 Unity 的 Ads 系统,然后创建一个简单的广告,在添加额外的回调选项之前完成。然后我们将看到如何通过利用选择加入奖励和添加冷却计时器来防止玩家观看过多的广告,从而为观看广告提供额外的激励。
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来编辑器版本中只需做最小改动即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您也可以在Unity 编辑器系统 要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。
您可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter06%20and%2007。
设置 Unity 广告
Unity Ads 是一个针对 iOS 和 Android 的视频广告网络,可以通过展示广告来货币化您现有的玩家基础。Unity Ads 提供可以以奖励或非奖励形式展示的视频广告。正如其名所示,奖励广告将给用户带来奖励或激励,这有助于他们在玩游戏时。在我们能够启用 Unity Ads 之前,我们必须首先启用 Unity 的服务套件。要激活 Unity 服务,您必须将项目链接到 Unity 服务项目 ID,这是 Unity 区分您创建的不同项目的方式。那么,让我们看看如何操作:
- 通过访问窗口 | 通用 | 服务或按Ctrl + 0打开服务窗口。这将打开 Unity 包管理器,并自动选择广告选项:

图 7.1 – Unity 包管理器中的广告选项
- 点击安装按钮并等待其完成安装。安装完成后,您应该看到一个看起来像以下窗口的窗口:

图 7.2 – 服务激活窗口
您也可以在安装包后通过访问服务 | 广告 | 配置来访问此菜单。
假设您之前没有使用过 Unity 服务,您将需要创建组织和项目名称详情。
- 点击下拉菜单,选择您的用户名,然后点击创建项目 ID按钮。项目名称在您首次创建项目时自动根据项目名称创建,但您可以在服务窗口的设置部分更改此名称。
重要提示
Unity 会自动使用您的账户用户名创建一个组织;然而,如果您需要创建另一个组织,您可以在id.unity.com/organizations进行操作。
- 您将被问到关于您游戏的问题。如果您的游戏不是针对儿童的,请从下拉菜单中选择否,然后点击保存按钮。否则,选择是,然后点击保存。
重要提示
当你表明你的游戏是为 13 岁以下的儿童设计的,广告将不会对你的游戏中的用户进行行为定位。行为定位可以通过展示与用户更相关的广告来提高每千次展示的有效成本(eCPM),但由于儿童在线隐私保护规则法案(COPPA)的规定,其使用在 13 岁以下的用户中是被禁止的。有关更多信息,请查看forum.unity.com/threads/age-designation.326930/。

图 7.3 – 回答 COPPA 合规性问题
-
然后,当切换到广告菜单时,点击右上角的切换按钮来开启它。此时应该已经开启了广告。
-
如果你向下滚动,你会看到一个名为游戏 ID的属性;记下这些值,因为我们将在游戏开始时初始化 Unity Ads 时需要它们。
-
为了开始,一个好的做法是将所有与广告相关的行为共享到一个脚本中,因此我们将通过访问
Assets/Scripts文件夹,并选择创建 | C# 脚本来创建一个新的类,名为UnityAd Controller。 -
打开你选择的 IDE 中的文件,并使用以下代码:
using UnityEngine; using UnityEngine.Advertisements; /* Advertisement class */ public class UnityAdController : MonoBehaviour { /// <summary> /// If we should show ads or not /// </summary> public static bool showAds = true; /// <summary> /// Replace with your actual gameId /// </summary> private string gameId = "1234567"; /// <summary> /// If the game is in test mode or not /// </summary> private bool testMode = true; /// <summary> /// Unity Ads must be initialized or else ads will /// not work properly /// </summary> private void Start() { /* No need to initialize if it already is done */ if (!Advertisement.isInitialized) { Advertisement.Initialize(gameId, testMode); } } }
之前的代码执行了多项操作。我们首先声明我们正在使用UnityEngine.Advertisments命名空间来获取对Advertisement类的访问权限。如果你只想实现视频、横幅和插屏广告来作为你的盈利策略,这是 Unity 建议使用的 API。除此之外,为了使用 Unity Ads,你必须调用Advertisement.Initialize函数,我在这个对象的Start函数中进行了调用。
- 从
GameObject(Unity Ad Controller)。一旦创建,将Unity Ad Controller脚本附加到它上:

图 7.4 – 创建 Unity Ad Controller 对象
- 由于此对象是在主菜单级别生成的,它会在游戏开始时加载,这对于我们打算使用的功能来说非常合适。
到目前为止,我们已经完成了利用 Unity Ads 所需的设置过程,通过启用 Unity Analytics 然后开启广告菜单。设置完成后,我们现在可以继续向我们的项目中添加一个简单的广告。
显示一个简单的广告
广告是玩家玩游戏时可能产生收入的一种方式。如前所述,Unity Ads 有两种不同的广告类型我们可以展示:简单广告和奖励广告。简单广告易于使用,因此得名,并允许用户拥有简单的全屏插屏广告。这在玩家在关卡之间移动或玩家想要重新开始游戏时非常有用。现在让我们看看我们如何实现这个功能。执行以下步骤:
-
要开始,我们需要向
UnityAdController类中添加一个新函数:/// <summary> /// Will get the appropriate Ad ID for the platform we /// are on /// </summary> /// <returns>A usable Ad ID</returns> private static string GetAdID() { string adID = "Interstitial_"; if (Application.platform == RuntimePlatform.IPhonePlayer) { adID += "iOS"; } else { adID += "Android"; } return adID; } /// <summary> /// Will load and display an ad on the screen /// </summary> public static void ShowAd() { // Load an Ad to play Advertisement.Load(GetAdID()); // Display it after it is loaded Advertisement.Show(GetAdID()); }
在这里,我们创建了一个名为ShowAd的静态方法。我们将其设置为静态,这样我们就可以在不实际创建这个类的实例的情况下调用该函数。该函数将广告加载到内存中,然后,当它准备好时,我们将调用Show()函数来在屏幕上显示它。我们还创建了一个名为GetAdID的辅助函数,以便根据我们部署的平台提供正确的广告类型。
-
保存你的脚本,然后打开
MainMenuBehaviour文件,并添加以下高亮代码:/// <summary> /// Will load a new scene upon being called /// </summary> /// <param name="levelName">The name of the level we /// want to go to</param> public void LoadLevel(string levelName) { if (UnityAdController.showAds) { /* Show an ad */ UnityAdController.ShowAd(); } SceneManager.LoadScene(levelName); }
如果支持,每次我们调用LoadLevel函数时,都会播放一条广告。我们还添加了一个具有默认值的新参数。这个功能的好处是我们可以选择性地决定何时显示广告。
例如,我们可能希望当游戏重新启动时,不要播放广告。
- 现在我们来看看实际操作。玩完游戏后,点击播放按钮:

图 7.5 – 显示的广告示例
如前一个截图所示,广告工作正常。这是在编辑器中玩游戏时显示的屏幕。它有按钮,允许我们测试玩家是否跳过或完整观看视频。当我们禁用测试模式时,我们将看到实时的视频广告。
如果你进入游戏后打开暂停菜单并点击主菜单按钮,你也会看到这种情况发生。
重要提示
如果这不起作用或没有显示,请检查你之前学到的玩家设置菜单,并确保你的当前平台设置为 Android 或 iOS。
这为我们提供了在游戏中显示广告的最简单方法,但为了确保我们的广告能够正常工作,我们还需要做很多事情,我们将在下一部分讨论。
重要提示
可以使用的另一种广告类型是横幅广告。这些广告与默认广告类似,但在调用GetAdID时,你会使用Banner而不是Interstitial。
更多信息请参阅docs.unity.com/monetization-dashboard/AdUnits.html。
利用广告回调方法
我们为LoadLevel函数编写的代码在进入游戏主菜单时运行得很好;然而,如果我们从主菜单直接进入游戏本身,游戏仍然会在后台运行,广告会阻止玩家玩游戏。
当你在实际移动设备上运行你的应用时,Unity 项目会在显示 Unity Ads 时暂停。然而,如果你在 Unity 编辑器中进行测试,游戏在显示占位符广告时不会暂停。但是,我们可以使用Advertisement.ShowOptions类来模拟这种行为。
当广告显示时,我们将暂停游戏,一旦广告结束,我们将恢复游戏。为此,请执行以下步骤:
-
让我们先打开
UnityAdController类,并添加以下变量,然后将Start函数更新为以下内容:/// <summary> /// A static reference to this object /// </summary> public static UnityAdController instance; /// <summary> /// Unity Ads must be initialized or else ads will not /// work properly /// </summary> private void Start() { /* No need to initialize if it already is done */ if (!Advertisement.isInitialized) { instance = this; // Use the functions provided by this to allow // custom Advertisement.Initialize(gameId, testMode); } }
instance变量将被用来给Advertisement.Show函数提供一个第二个参数来引用运行代码的对象。
-
更新
ShowAd函数以添加第二个参数到我们的函数中:/// <summary> /// Will load and display an ad on the screen /// </summary> public static void ShowAd() { // Load an Ad to play Advertisement.Load(GetAdID()); // Display it after it is loaded Advertisement.Show(GetAdID(), instance); }
对于第二个参数,Advertisement.Show函数接受一个IUnityAdsShowListener对象。名称开头的I表示这是一个接口。这是 C#中的一个关键字,表示一种合同,承诺提供给此函数的内容包含接口所需的功能。
-
现在更新类定义如下:
public class UnityAdController : MonoBehaviour, IUnityAdsShowListener
通过添加逗号然后IUnityAdsShowListener,我们声明我们将实现IUnityAdsShowListener接口提供的方法。
在 C#中,每当我们在类定义中添加一个接口时,我们都在承诺将包含接口内部声明的所有方法的实现,如果我们不这样做,我们的代码将无法编译。这是必要的,因为稍后我们将传递一个类型为IUnityAdsShowListener的对象给 Unity 的代码,它将在适当的时候使用这些方法。
要查看这些方法,从您的 IDE 中,您可能可以右键单击IUnityAdsShowListener选项并选择转到定义。从那里,您可能看到以下内容:
namespace UnityEngine.Advertisements
{
public interface IUnityAdsShowListener
{
void OnUnityAdsShowClick(string placementId);
void OnUnityAdsShowComplete(
string placementId,
UnityAdsShowCompletionState
showCompletionState);
void OnUnityAdsShowFailure(string placementId,
UnityAdsShowError error, string message);
void OnUnityAdsShowStart(string placementId);
}
}
我们需要在我们的类中创建四个方法,具有完全相同的名称、参数和返回类型。
重要注意事项
关于接口及其在 C#中的工作方式,请查看www.tutorialsteacher.com/csharp/csharp-interface。
-
完成此操作后,我们需要实现接口中使用的函数:
#region IUnityAdsShowListener Methods /// <summary> /// This callback method handles logic for the ad /// starting to play. /// </summary> /// <param name="placementId">The identifier for the Ad Unit showing the content.</param> public void OnUnityAdsShowStart(string placementId) { /* Pause game while ad is shown */ PauseScreenBehaviour.paused = true; Time.timeScale = 0f; } /// <summary> /// This callback method handles logic for the ad /// finishing. /// </summary> /// <param name="placementId">The identifier for the /// Ad Unit showing the content</param> /// <param name="showCompletionState">Indicates the /// final state of the ad (whether the ad was skipped /// or completed).</param> public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState showCompletionState) { /* Unpause game when ad is over */ PauseScreenBehaviour.paused = false; Time.timeScale = 1f; } /* This callback method handles logic for the user clicking on the ad. */ public void OnUnityAdsShowClick(string placementId) { } /* This callback method handles logic for the Ad Unit failing to show. */ public void OnUnityAdsShowFailure(string placementId, UnityAdsShowError error, string message) { } #endregion
在创建广告时,这四个函数中的每一个都会执行一些操作。值得注意的是OnUnityAdsShowStart方法,其中我们暂停游戏,然后是OnUnityAdsShowComplete方法,其中我们取消暂停。我们在这里使用区域是为了使代码的模块化更容易。
重要注意事项
关于#region块的更多信息,请查看docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-region。
-
接下来,我们将确保
PauseScreenBehaviour不会覆盖这个新的更改。因此,我们将用以下内容替换Start()函数:void Start() { if (!UnityAdController.showAds) { /* If not showing ads, just start the game */ SetPauseMenu(false); } }
前面的代码片段很重要,否则当我们在Start函数中告诉游戏暂停后,游戏将在关卡加载时立即关闭。这是游戏 PC 版本所需要的,因为没有其他方法可以取消暂停静态值。
- 保存我们的脚本并重新启动游戏:

图 7.6 – 游戏暂停,直到玩家结束广告
这样,当我们从主菜单过渡到游戏时,我们将暂停游戏,直到我们准备好加入。现在我们已经了解了如何处理基本且非可选的广告,让我们给玩家提供观看广告以获得某种利益的机会。
奖励式广告
根据 AdColony 的数据,58%的移动开发者推荐的最合适的移动游戏广告形式是奖励视频广告。也就是说,我们将广告设置为一种可选体验,玩家可以选择观看广告并为此获得某种形式的奖励。这样,用户会感觉到是否观看广告是他们自己的选择,而且他们会更有动力观看,因为他们会从中获得一些东西。
奖励广告的位置通常会产生更高的每千次展示有效成本(eCPM),因为它们通过允许用户在观看广告之前选择加入以换取一些游戏内奖励,从而提供了更多用户参与度。
注意
如果您想了解更多关于为什么推荐奖励广告的原因,请查看www-staging.adcolony.com/blog/2016/04/26/the-top-ads-recommended-by-mobile-game-developers/。
在我们的游戏中,我们可以添加重新开始游戏或观看广告以继续游戏的选项。这意味着我们需要创建某种类型的菜单,以便玩家可以选择是否观看广告,所以让我们添加它:
-
如果您还没有停止游戏,请先停止游戏,然后打开游戏玩法
-
场景。之后,让我们创建一个
Game Over菜单,然后关闭暂停菜单,以便我们可以清楚地看到游戏结束对象。为了更容易看到,您可以自由切换到我们之前在创建游戏 UI 元素时使用的 2D 模式。 -
接下来,展开
Game Over Contents并更改子GameOver。 -
现在,将
Continue (Play Ad)改为Continue Button并更改按钮对象的名称:

图 7.7 – 游戏结束菜单设置
-
我们首先需要更新
ObstacleBehaviour脚本以处理它;添加以下高亮代码:using UnityEngine; using UnityEngine.UI; // Button public class ObstacleBehaviour : MonoBehaviour { [Tooltip("How long to wait before restarting the game")] public float waitTime = 2.0f; public GameObject explosion; private GameObject player; private void OnCollisionEnter(Collision collision) { // First check if we collided with the player if (collision.gameObject.GetComponent <PlayerBehaviour>()) { // Destroy (Hide) the player collision.gameObject.SetActive(false); player = collision.gameObject; // Call the function ResetGame after waitTime // has passed Invoke("ResetGame", waitTime); } } /// <summary> /// Will restart the currently loaded level /// </summary> private void ResetGame() { //Bring up restart menu var go = GetGameOverMenu(); go.SetActive(true); // Get our continue button var buttons = go.transform.GetComponentsInChildren<Button> (); Button continueButton = null; foreach (var button in buttons) { if (button.gameObject.name == "Continue Button") { continueButton = button; break; } } // If we found the button we can use it if (continueButton) { if (UnityAdController.showAds) { // If player clicks on button we want to // play ad and then continue continueButton.onClick.AddListener (UnityAdController.ShowAd); UnityAdController.obstacle = this; } else { // If can't play an ad, no need for // continue button continueButton.gameObject.SetActive(false); } } } /// <summary> /// If the object is tapped, we spawn an explosion and /// destroy this object /// </summary> private void PlayerTouch() { if (explosion != null) { var particles = Instantiate(explosion, transform.position, Quaternion.identity); Destroy(particles, 1.0f); } Destroy(this.gameObject); } /// <summary> /// Retrieves the Game Over menu game object /// </summary> /// <returns>The Game Over menu object</returns> GameObject GetGameOverMenu() { var canvas = GameObject.Find("Canvas").transform; return canvas.Find("Game Over").gameObject; } /// <summary> /// Handles resetting the game if needed /// </summary> public void Continue() { var go = GetGameOverMenu(); go.SetActive(false); player.SetActive(true); // Explode this as well (So if we respawn player // can continue) PlayerTouch(); } } -
首先,添加以下变量并更新
OnCollisionEnter函数如下:/// <summary> /// A reference to the player object /// </summary> private GameObject player; private void OnCollisionEnter(Collision collision) { // First check if we collided with the player if (collision.gameObject.GetComponent <PlayerBehaviour>()) { // Destroy the player //Destroy(collision.gameObject); // Destroy (Hide) the player player = collision.gameObject; player.SetActive(false); // Call the function ResetGame after // waitTime has passed Invoke("ResetGame", waitTime); } }
在这种情况下,我们移除了摧毁player对象的代码,并将其隐藏起来。我们这样做的原因是,如果玩家决定播放广告,我们就可以将其取消隐藏并正常继续游戏。
-
接下来,我们将使用
Button类,所以我们需要在ObstacleBehaviour脚本的顶部添加以下内容:using UnityEngine.UI; -
完成这些后,我们将更新
ResetGame函数如下:/// <summary> /// Will restart the currently loaded level /// </summary> private void ResetGame() { //Bring up restart menu var go = GetGameOverMenu(); go.SetActive(true); // Get our continue button var buttons = go.transform.GetComponentsInChildren<Button>(); Button continueButton = null; foreach (var button in buttons) { if (button.gameObject.name == "Continue Button") { continueButton = button; break; } } /*If we found the button we can use it */ if (continueButton) { if (UnityAdController.showAds) { // If player clicks on button we want to // play ad and then continue continueButton.onClick.AddListener( UnityAdController.ShowAd); UnityAdController.obstacle = this; } else { /* If can't play an ad, no need for continue button */ continueButton.gameObject.SetActive(false); } } }
我们还会摧毁玩家所击中的物体。因此,如果我们重新启动游戏,那么玩家将能够从他们最初开始的地方重新开始。
-
接下来,添加以下两个辅助函数:
/// <summary> /// Retrieves the Game Over menu game object /// </summary> /// <returns>The Game Over menu object</returns> GameObject GetGameOverMenu() { var canvas = GameObject.Find("Canvas").transform; return canvas.Find("Game Over").gameObject; } /// <summary> /// Handles resetting the game if needed /// </summary> public void Continue() { var go = GetGameOverMenu(); go.SetActive(false); player.SetActive(true); /* Explode this as well (So if we respawn player can continue) */ PlayerTouch(); }
考虑到这一点,我们还创建了一个Continue函数,这样我们就可以在需要时设置游戏继续。
-
打开
UnityAdController脚本,并在文件顶部添加以下变量声明:/// <summary> /// For holding the obstacle for continuing the game /// </summary> public static ObstacleBehaviour obstacle; -
之后,继续在
UnityAdController脚本中,更新OnUnityAdsShowComplete函数如下:/// <summary> /// This callback method handles logic for the ad /// finishing. /// </summary> /// <param name="placementId">The identifier for the Ad Unit showing the content</param> /// <param name="showCompletionState">Indicates the final state of the ad (whether the ad was skipped or completed).</param> public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState showCompletionState) { /* If there is an obstacle, we can remove it to continue the game */ if (obstacle != null && showCompletionState == UnityAdsShowCompletionState.COMPLETED) { obstacle.Continue(); } /* Unpause game when ad is over */ PauseScreenBehaviour.paused = false; Time.timeScale = 1f; }
我们首先检查是否有玩家撞击的障碍物。如果有,我们接着检查由函数提供的showCompletionState变量的值。我们使用UnityAdsShowCompletionState枚举来验证玩家实际上完成了广告,并且没有点击跳过按钮。
-
我们想要确保 Unity 的广告系统在两个场景中都能正常工作,因此我们可以复制粘贴
GameManager脚本,并将以下高亮代码添加到Start函数中:/// <summary> /// Start is called before the first frame update /// </summary> private void Start() { /* If there is no UnityAdController, we can add it through code */ if (!GameObject.FindObjectOfType <UnityAdController>()) { var adController = new GameObject("Unity Ad Controller"); adController.AddComponent<UnityAdController>(); } // Set our starting point nextTileLocation = startPoint; nextTileRotation = Quaternion.identity; for (int i = 0; i < initSpawnNum; ++i) { SpawnNextTile(i >= initNoObstacles); } } -
保存你的脚本并返回到 Unity 编辑器。
-
点击游戏结束对象并禁用它,保存我们的场景,然后打开主菜单场景并进入游戏。
提示
如果你没有在那里看到广告,可能是因为 Unity Ads 没有被初始化。这是在主菜单场景中完成的,所以你需要在看到广告之前先去那里。
在这个阶段,当我们游戏中死亡时,我们会看到一个游戏结束屏幕:

图 7.8 – 游戏结束屏幕
如果我们点击继续(播放广告),将播放一个广告。如果玩家跳过它,则不会发生任何事情,但如果他们看完整个广告,应该会像什么都没发生一样带他们回到游戏中:

图 7.9 – 继续游戏
这样,我们的广告系统就正常工作了。我们现在已经看到了如何将广告的使用集成到我们的游戏玩法中,并为玩家提供观看此内容的原因。
添加冷却计时器
广告对开发者来说很棒;然而,根据 Unity 的货币化常见问题解答,每个用户每天只能观看 25 个广告。考虑到这一点,我们可能希望让玩家只能偶尔触发广告。这也具有让玩家在一段时间后想回到我们游戏中的好处。
重要提示
关于 Unity 的货币化常见问题解答,请查看docs.unity.com/ads/FAQ.html。
我们现在将实现一个功能,我们的继续选项将偶尔工作一次,并带有我们可以轻松定制的短延迟:
-
要开始,回到
UnityAdController脚本,并向其中添加以下新的变量,如高亮代码所示:using System; // DateTime using UnityEngine; using UnityEngine.Advertisements; /* Advertisement class */ public class UnityAdController : MonoBehaviour, IUnityAdsShowListener { /// <summary> /// A static reference to this object /// </summary> public static UnityAdController instance; /// <summary> /// If we should show ads or not /// </summary> public static bool showAds = true; // Nullable type public static DateTime? nextRewardTime = null; /// <summary> /// For holding the obstacle for continuing the /// game /// </summary> public static ObstacleBehaviour obstacle; // Rest of UnityAdController...
nextRewardTime变量是DateTime类型,我们之前没有讨论过。基本上,它是一个表示时间点的结构,我们可以将其与其他时间点进行比较,并且是.NET Framework 内置的。我们将使用它来存储玩家在需要的情况下能够再次播放广告之前需要过去的时间。请注意,DateTime是System命名空间的一部分。这就是为什么我们在前面的代码中也添加了using System;行。
重要提示
关于DateTime类的更多信息,请查看msdn.microsoft.com/en-us/library/system.datetime(v=vs.110).aspx。
你可能会注意到这个变量的类型旁边有一个?符号。当我们这样做时,我们创建了一个被称为可空类型的结构。使用它们的优点是它们可以除了具有正常值之外还可以是null。我们这样做是为了我们不必仅仅为了有一个默认值而填写一个默认值。
重要提示
关于可空类型的更多信息,请查看www.tutorialspoint.com/csharp/csharp_nullables.htm。
-
为了在广告之间添加时间延迟,我们将为此创建一个新的函数:
public static void ShowRewardAd() { nextRewardTime = DateTime.Now.AddSeconds(15); ShowAd(); }
现在我们展示奖励广告时,将nextRewardTime设置为从函数调用时的 15 秒。当然,我们也可以使用AddMinutes和AddHours函数将其设置为分钟或小时。
-
保存您的脚本,然后打开
ObstacleBehaviour脚本。在脚本顶部,添加以下新的using语句:using System; // DateTime using System.Collections; // IEnumerator -
之后,我们需要修改
ResetGame()函数的底部部分,以包含以下代码:// Rest of ResetGame above... /*If we found the button we can use it */ if (continueButton) { if (UnityAdController.showAds) { // If player clicks on button we want // to play ad and then continue StartCoroutine(ShowContinue( continueButton)); } else { /* If can't play an ad, no need for continue button */ continueButton.gameObject.SetActive( false); } } }
现在,我们不再只是为这个按钮添加一个监听器,而是用对StartCoroutine函数的调用替换了它,该函数接受我们尚未编写的函数。我认为在我们实际编写一个之前,先简单谈谈协程可能是个好主意。
协程就像一个具有暂停执行并在一段时间后继续执行的能力的函数。默认情况下,协程在开始使用yield之后的帧上恢复,但也可以使用WaitForSeconds函数引入时间延迟,以指定在再次调用之前需要等待多长时间。
-
接下来,使用以下脚本为
ShowContinue函数:public IEnumerator ShowContinue(Button contButton) { while (true) { var btnText = contButton.GetComponentInChildren < TMPPro.TMP_Text>(); /* Check if we haven't reached the next reward time yet (if one exists) */ var rewardTime = UnityAdController.nextRewardTime; bool validTime = rewardTime.HasValue; bool timePassed = true; if (validTime) { timePassed = DateTime.Now > rewardTime.Value; } if (!timePassed) { /* Unable to click on the button */ contButton.interactable = false; /* Get the time remaining until we get to the next reward time */ TimeSpan remaining = rewardTime.Value - DateTime.Now; /* Get the time left in the following format 99:99 */ var countdownText = string.Format("{0:D2}:{1:D2}", remaining.Minutes, remaining.Seconds); /* Set our button's text to reflect the new time */ btnText.text = countdownText; /* Come back after 1 second and check again */ yield return new WaitForSeconds(1f); } else { /* It's valid to click the button now */ contButton.interactable = true; /* If player clicks on button we want to play ad and then continue */ contButton.onClick.AddListener( UnityAdController.ShowRewardAd); UnityAdController.obstacle = this; /* Change text to its original version */ btnText.text = "Continue (Play Ad)"; /* We can now leave the coroutine */ break; } } }
这个协程将执行多项操作,首先是通过进入一个 while (true) 循环。现在,这通常是一个非常糟糕的事情,因为它会导致无限循环,但如果没有设置奖励时间或者已经超过了 nextRewardTime 变量中设置的时间,我们会跳出循环。如果没有,我们将计算出在时间到达之前还有多少时间,并将按钮的文本更改为显示它。然后我们使用 WaitForSeconds 函数暂停执行,并在 1 秒后返回。
重要提示
如果你感兴趣,想了解更多关于协程幕后工作原理的细节,Oliver Booth 在 blog.oliverbooth.dev/2021/04/27/how-do-unitys-coroutines-actually-work/ 上写了一篇不错的文章。
- 保存所有脚本并返回 Unity 中玩游戏:

图 7.10 – 延迟屏幕正常工作
重新启动游戏一次后,你会看到如果我们再次尝试这样做,我们会被带到延迟屏幕。当时间降到 0 时,玩家将再次能够继续。
重要提示
关于此类奖励广告的最佳实践等信息,请参阅 docs.unity.com/ads/MonetizationStrategy.html。
摘要
通过这些,我们已经建立了如何在游戏中添加广告的良好基础。希望你能看到实现起来有多简单,并能想到新的方法来吸引玩家,以获得最佳体验。在本章的整个过程中,我们发现了如何设置 Unity Ads。然后我们看到我们可以创建简单的广告,并学习了如何通过实现 IUnityAdsShowListener 接口来响应玩家的操作。之后,我们看到我们可以通过在游戏中使用可选广告为玩家添加奖励,并为系统添加了冷却时间,以使游戏对玩家来说不那么令人烦恼。凭借这些新获得的能力,你应该能够添加广告,并从你未来创建的游戏中获得额外的收入。
重要提示
默认情况下,广告应处于测试模式。向测试者分发实时广告违反了 Unity Ads 的服务条款。如果他们点击或安装任何广告中的游戏,他们的活动将被货币化,自动欺诈系统将标记该游戏为欺诈并禁用它。
要禁用测试模式,你可以通过访问 服务 | 广告 | 配置 并点击 转到仪表板 来进入分析货币化仪表板。(如果你需要,请选择 设置冥想伙伴 和 我只计划使用 Unity 广告。)
从你的项目中选择 设置。向下滚动到 测试模式 部分,并根据需要修改 Apple App Store 或 Google Play Store 属性。
虽然这是我们游戏盈利的有效方式,但我们将在下一章深入探讨另一种更受欢迎的游戏内盈利形式:应用内购买。
第八章:将社交媒体整合到我们的项目中
我们现在已经拥有了将我们的游戏推向世界所需的所有基础要素;它在机械上运作良好,并且我们已经设置了所有盈利方式。我们为项目添加的所有功能都很好,但如果没有人玩你的游戏,就没有理由保留它们。
口碑营销是让其他人尝试您的游戏最可靠的方式。为人们提供分享游戏的机会有助于其他人发现项目,这是我们真的应该尝试去做的事情,因为作为独立开发者,营销和推广您的游戏是最难做的事情之一。
在本章中,您将学习一些将社交媒体整合到您项目中的不同方法。我们将从添加一些可以分享的内容开始——一个分数。之后,我们将看到如何将分数分享到 Twitter。然后,我们将看到如何将我们的游戏与 Facebook 连接,并在游戏本身中使用 Facebook 的内容。
本章将分为多个主题。它将包含从开始到结束的简单分步过程。以下是我们任务的概述:
-
添加评分系统
-
通过 Twitter 分享高分
-
下载和安装 Facebook 的 SDK
-
通过 Facebook 登录我们的游戏
-
显示 Facebook 的名称和头像
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小改动即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署您的项目,您需要一个 Android 或 iOS 设备。
与前几章不同,使用 Facebook SDK 需要为您的 Unity 版本安装 iOS 和 Android 构建支持,所以在导入包之前,请确保两者都已添加,否则您将遇到错误。
您可以在 GitHub 上找到本章中提供的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter08。
添加评分系统
为了给玩家提供与他人分享我们游戏的动力,我们需要提供一个令人信服的理由。有些人非常具有竞争性,希望成为游戏中的最佳玩家,挑战他人做得更好。为了帮助这一点,我们可以允许玩家通过社交媒体分享得分值。然而,为了做到这一点,我们首先需要一个评分系统。幸运的是,这并不太难,所以让我们快速添加以下步骤:
-
首先,打开位于项目
Assets/Scenes文件夹中的Gameplay.scene文件。为了显示玩家的得分,我们需要一种方法在屏幕上显示它。在我们的例子中,最简单的方法是使用文本对象。 -
从层次结构窗口中选择On Screen Controls对象,它是Canvas对象的子对象。之后,右键单击On Screen Controls对象,选择UI | Text – Text Mesh Pro,如图下所示:

图 8.1– 在屏幕上添加文本对象
这将使Text对象成为Panel对象的子对象,从而在设备中如果有凹槽的情况下,它会自动调整大小以适应凹槽。
-
将此对象重命名为
Score Text,并使用顶部的锚点预设菜单,按住Shift + Alt来设置中心点和位置。 -
之后,让我们将
60设置为确保我们有空间容纳增加的大小。 -
接下来,在
0处设置45,以便更容易看到。 -
为了提高其可读性,将材质预设更改为LiberationSans SDF – Outline,并将顶点颜色更改为黑色。
之后,滚动到文本材质部分(1)。注意它似乎填充了文本的内外部。为了修复这个问题,在1。

图 8.2:调整文本轮廓的属性
-
接下来,打开
PlayerBehaviour脚本,并在文件顶部添加以下行:using TMPro; //TextMeshProUGUI -
接下来,在类内部添加以下代码:
[Header("Object References")] public TextMeshProUGUI scoreText; private float score = 0; public float Score { get { return score; } set { score = value; /* Check if scoreText has been assigned */ if (scoreText == null) { Debug.LogError("Score Text is not set. " + "Please go to the Inspector and assign it"); /* If not assigned, don't try to update it. */ return; } /* Update the text to display the whole number portion of the score */ scoreText.text = string.Format("{0:0}", score); } }
我们首先有一个对scoreText对象的引用,我们将在TextMeshProUGUI类中设置它,该类包含与对象上显示的文本相关的属性。
这利用了 C#的get/set函数,它们是隐式获取器和设置器。基本上,每次我们获取或设置Score变量时,都会执行{}之间所包含的内容。在我们的例子中,每次我们设置Score变量时,它都会自动更新我们的文本。
小贴士
想要了解更多关于 TextMeshPro 的信息,请查看docs.unity3d.com/Packages/com.unity.textmeshpro@3.0/manual/index.html#support--api-documentation。
这比我的许多学生所做的方法有优势,他们是在每一帧更新文本的值,这其实是不必要的。我们只需要在值改变时更新文本,这使得它非常适合我们在这个情况下使用。
注意
关于get/set访问器的更多信息,请查看docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-properties。
-
然后,更新
PlayerBehaviour类,使其具有以下突出显示的更改:// Start is called before the first frame update public void Start() { // Get access to our Rigidbody component rb = GetComponent<Rigidbody>(); minSwipeDistancePixels = minSwipeDistance * Screen.dpi; joystick = GameObject.FindObjectOfType<MobileJoystick>(); Score = 0; } /// <summary> /// FixedUpdate is a prime place to put physics calculations /// happening over a period of time. /// </summary> void FixedUpdate() { /* If the game is paused, don't do anything */ if (PauseScreenBehaviour.paused) { return; } Score += Time.deltaTime; // Rest of Update here...
我们在这里做的是在玩家创建时重置分数,在游戏未暂停时增加值。
-
保存脚本并返回 Unity。
-
选择Player对象,将我们的Score Text对象拖放到Player 组件上的Score Text变量:

图 8.3:分配分数文本属性
- 一旦变量已被分配,继续玩游戏。游戏界面如下截图所示:

图 8.4:游戏中添加的分数
现在,正如你所看到的,我们的游戏有了分数,并且在我们玩游戏时更新。这将使玩家能够轻松了解他们对游戏的掌握程度,并给他们一些可以与他人分享的信息。现在我们有了评分系统,让我们看看我们如何使用 Twitter 分享高分。
通过 Twitter 分享高分
Twitter 是一个在线新闻和社交网络服务,用户通过称为tweets的消息进行互动,这些消息限制在 280 个字符以内。许多独立游戏开发者使用 Twitter 作为吸引他人玩他们游戏的方式。
Twitter 是一个很好的起点,因为我们可以通过简单地打开一个特定的 URL 非常容易地将它添加到我们的项目中。让我们看看如何做到这一点:
-
打开
PauseScreenBehaviour脚本。一旦进入,我们将在PlayerScreenBehaviour类中添加以下代码:#region Share Score via Twitter /// <summary> /// Web address in order to create a tweet /// </summary> private const string tweetTextAddress = "http://twitter.com/intent/tweet?text="; /// <summary> /// Where we want players to visit /// </summary> private string appStoreLink = "http://johnpdoran.com/"; [Tooltip("Reference to the player for the score")] public PlayerBehaviour player; /// <summary> /// Will open Twitter with a prebuilt tweet. When called on iOS /// or Android will open up Twitter app if installed /// </summary> public void TweetScore() { /* Create contents of the tweet */ string tweet = "I got " + string.Format("{0:0}", player.Score) + " points in Endless Roller! Can you do better?"; /* Create the entire message */ string message = tweet + "\n" + appStoreLink; /* Ensures string is URL friendly */ string url = UnityEngine.Networking.UnityWebRequest .EscapeURL(message); /* Open the URL to create the tweet */ Application.OpenURL(tweetTextAddress + url); } #endregion
首先,我们将使用一些新事物。你会注意到前面的代码块以#region开始,以#endregion结束。这样做的作用是允许我们在 Visual Studio 中展开和折叠这部分代码。当我们引入较长的代码文件时,能够折叠或隐藏脚本中的某些部分,以便我们只关注正在工作的文件部分,这会非常方便。由于这部分代码与脚本的其他部分无关,这是一个我们使用它的好地方。
在 Unity 中打开 URL,我们需要使用Application.OpenURL函数和UnityWebRequest类。
注意
关于 Twitter 的 Web Intents 及其使用方式,请查看dev.twitter.com/web/intents。
UnityWebRequest类通常用于在运行时加载内容,但它也有EscapeURL函数,该函数将字符串转换为网络浏览器舒适的格式。例如,换行符将不会单独显示。
注意
关于EscapeURL函数的更多信息,请查看docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.EscapeURL.html。
保存脚本并返回 Unity。通过将层次结构窗口中的Player游戏对象拖放到检查器窗口中的Player属性上:

图 8.5:分配 Player 属性
-
现在,我们需要在我们的Game Over屏幕上有一个按钮,以便我们可以分享我们的分数。
-
打开画布对象,通过在检查器窗口中点击其名称旁边的勾选标记来切换Game Over对象为开启。
-
从那里,展开两个Tweet Score Button,并更新子对象中的文本以显示
Tweet Score。 -
之后,选择Tweet Score按钮对象,并滚动到按钮组件。从那里,将我们调用的函数更改为PauseScreenBehaviour | TweetScore函数:

图 8.6:调用 TweetScore 函数
-
在层次结构中选择Game Over对象,并再次禁用它。接下来,保存你的场景并开始游戏。
-
现在我们失败游戏时,我们可以点击Tweet Score按钮,我们的浏览器将在我们的 PC 上打开:

图 8.7:PC 上的结果
然而,在我们的移动设备上,如果已安装,它将打开 Twitter 应用:

图 8.8:通过 Twitter 移动应用发布我们的分数
通过这样,你学会了如何轻松地使用 Twitter 分享东西。
注意
对于那些对 Twitter 有更多兴趣的人来说,它确实有自己的 Unity API,这允许你让用户使用 Twitter 登录到你的游戏,如果你愿意这样做而不是 Facebook,我们稍后会这样做。如果你对此感兴趣,你可以在dev.twitter.com/twitterkit/unity/overview找到更多信息。
当然,还有其他社交网络存在,其中一些有自己的软件开发工具包(SDK),这允许你访问它们拥有的信息。在下一节中,我们将探讨如何利用这一点。
下载并安装 Facebook 的 SDK
在没有提到 Facebook 的情况下讨论社交网络章节是不可能的。Facebook 有自己的 SDK,可以与 Unity 一起使用。这可以让我们在我们的游戏体验中使用 Facebook 已经拥有的信息,包括用户的姓名和头像。让我们看看如何整合这些步骤:
- 打开你的网络浏览器并访问
developers.facebook.com/docs/unity/:

图 8.9:Unity 的 Facebook SDK 页面
- 点击
facebook-unity-sdk-15.1.0文件夹。然后,打开FacebookSDK文件夹,你会看到一个单独的文件,facebook-unity-sdk-15.1.0.unitypackage。
与前面的章节不同,使用 Facebook SDK 需要为安装的 Unity 版本添加 iOS 和 Android 构建支持,所以在导入包之前确保两者都已添加,否则你会遇到错误。要做到这一点,在 Unity Hub 中,转到 安装 部分,从齿轮图标中选择 添加模块 并添加之前未包含的相关项目(如果有)。
- 双击
unitypackage文件,你应该会弹出一个窗口,如图所示:

图 8.10:Unity 包导入对话框
如果这不起作用,你也可以转到 资产 | 导入包 | 自定义包,然后找到你解压文件的文件夹,并以此方式打开它。
-
点击 导入 按钮,等待它加载完成。从这里,你会看到一个弹出窗口提示项目可能包含过时的 API。继续点击 我已经备份,继续 按钮,等待它完成。
-
现在,为了使用 Facebook API,我们首先需要一个 Facebook App ID,所以让我们接下来做这件事。
-
返回你的网络浏览器并转到
developers.facebook.com/,点击屏幕右上角的 登录 按钮。一旦你登录到你的 Facebook 账户,你应该会看到以下截图类似的内容:

图 8.11:开发者 Meta 菜单
-
从前面的页面,点击屏幕右上角的 开始 按钮。从那里,你会被带到需要点击 下一步 的屏幕,然后你会被要求选择你的角色。点击 开发者,然后在下一个屏幕上点击 创建第一个 应用 按钮。
-
之后,添加
Endless Roller) 和你的 联系邮箱,然后选择 创建 应用 ID。 -
一旦你被带到你的应用页面,点击左侧默认游戏信息的 仪表板 选项。注意 App ID 并通过点击它或突出显示它然后按 Ctrl + C 来复制它。
-
如果你看到的是
Endless Roller) 和你的联系邮箱,然后选择 创建应用。

图 8.12:获取应用 ID
-
返回 Unity,您可能需要与 Google 分享一些信息。按您的意愿回答。然后,您将可以选择启用 Android 自动解析。我设置了启用并等待它解析 Android 依赖项。
-
之后,您会在顶部栏注意到一个新的 Facebook 选项。选择它,然后选择编辑设置。一旦到了那里,如果您需要,点击检查器,您将看到几个选项。将Facebook App Id设置为创建的应用的 ID,然后将名称设置为游戏名称:

图 8.13:将应用 ID 添加到 Facebook 设置
小贴士
有可能您在尝试将游戏导出到 Android 时可能会因为更改 SDK 位置而遇到错误。如果是这种情况,请关闭您的 Unity 项目,然后转到项目文件夹并删除Temp文件夹。重新启动项目后,错误应该会消失。
- 通过访问Facebook | 编辑设置...返回 Facebook 设置菜单。
现在,您会注意到在Android 构建 Facebook 设置下出现了一个新的错误,指出未找到 OpenSSL。
- 为了解决这个问题,我们首先需要通过访问
slproweb.com/products/Win32OpenSSL.html下载 OpenSSL。从那里,选择Win64 OpenSSL v1.1.1u选项下的EXE链接,如图所示:
重要提示
对于使用 Mac 的用户,您可以按照以下说明安装 OpenSSL 并将其添加到您的路径:developers.facebook.com/docs/facebook-login/android/advanced。

图 8.14:OpenSSL 下载链接
下载完成后,使用默认选项安装程序,如图所示:

图 8.15:安装 OpenSSL
-
安装完成后,您可以取消勾选捐赠选项,然后点击完成按钮。
-
我们接下来需要将 OpenSSL 的位置添加到路径中。为此,按下您的 Windows 键:
-
按下键盘并开始输入
env,然后选择编辑系统环境变量 -
选项,如图所示:

图 8.16:选择编辑系统环境变量选项
- 在弹出的窗口中,点击
C:\Program Files\OpenSSL-Win64\bin:

图 8.17:编辑路径的环境变量
-
然后,点击
C:\Program Files\Unity\Hub\Editor\2022.1.0b16\Editor\Data\PlaybackEngines\AndroidPlayer\OpenJDK\bin。 -
点击确定按钮,然后在环境变量窗口中的确定按钮。
-
一旦添加了这两个选项,请关闭您的 Unity 项目并重新启动计算机。Unity 重新打开后,您可能需要等待解决 Android 依赖项菜单完成,但一旦完成,您应该能够正确看到Facebook 设置(Facebook | 编辑设置)菜单,并在调试 Android 密钥 哈希 [?]下获得值:

图 8.18:调试 Android 密钥哈希
-
对于某些 API 的调用,也需要使用客户端令牌,因此我们也将获取一个。为此,请通过转到Facebook | 开发者页面返回您项目的仪表板。
-
一旦仪表板加载完成,转到设置 | 高级 | 安全 | 客户端令牌。复制客户端令牌值,然后返回 Unity 编辑器,将其粘贴到检查器中的Facebook 设置。
这意味着我们的 Facebook SDK 设置已经完成!
-
根据您希望部署的平台,访问以下网站并完成列出的任务:
现在我们已经设置了这些,我们可以通过首先允许我们的游戏通过 Facebook 登录来添加内容。
通过 Facebook 登录到我们的游戏
当使用 Facebook API 时,我们可以做的一件事是允许我们的用户使用他们的 Facebook 账户登录到游戏。然后,我们可以在项目中自动使用他们的姓名和图像。以下步骤展示了我们如何实现这一点:
-
让我们先打开
Assets/Scenes文件夹,然后双击MainMenu文件。 -
从那里,让我们点击2D按钮进入 2D 模式,如果您之前还没有这样做。我们将替换原始菜单,并为玩家提供一个通过 Facebook 登录或游戏开始时作为访客游玩的按钮。
-
前往菜单选项。
-
然后,选择
Facebook 登录对象。再次选择菜单选项游戏对象,然后通过转到检查器选项卡并点击其名称旁边的勾选标记来禁用它:

图 8.19:创建 Facebook 登录菜单
我们将使Facebook 登录对象在需要时打开菜单。
-
接下来,打开
225,然后右键单击按钮组件,并选择重置选项以移除其原始的On Click ()功能。 -
复制
Facebook 登录按钮和继续作为访客按钮。同时,更改Facebook 登录和继续作为访客:

图 8.20:Facebook 登录按钮设置
-
现在我们已经正确地使按钮工作,我们需要编写一个脚本,以便我们可以登录。转到
Scripts文件夹并打开我们的MainMenuBehaviour脚本。我们将使用List类来保存我们想要访问 Facebook 和 Facebook SDK 中FB类内容的权限。 -
因此,为了做到这一点,我们首先将在
MainMenuBehaviour脚本顶部添加以下内容:using UnityEngine; using UnityEngine.SceneManagement; // LoadScene using System.Collections.Generic; // List using Facebook.Unity; // FB -
然后,将以下变量添加到
MainMenuBehaviour类中:[Header("Object References")] public GameObject mainMenu; public GameObject facebookLogin; -
现在,在
MainMenuBehaviour类中添加以下代码:#region Facebook #endregion
在这个区域内,我们将添加几个不同的方法,首先是处理初始化 Facebook API 的一些方法:
public void Awake()
{
/* We only call FB Init once, so check if it
/* has been called already */
if (!FB.IsInitialized)
{
FB.Init(OnInitComplete, OnHideUnity);
}
}
在这种情况下,Awake方法调用FB.Init函数,它接受两个参数,这两个参数都是委托,或者是在初始化完成以及应用隐藏或不再当前焦点时调用的函数。这两个函数的定义如下:
/// <summary>
/// Once initialized, will inform if logged in on Facebook
/// </summary>
private void OnInitComplete()
{
if(FB.IsInitialized)
{
if (FB.IsLoggedIn)
{
print("Logged into Facebook");
/* Close Login and open Main Menu */
ShowMainMenu();
}
}
else
{
print("Failed to init Facebook SDK; open as
guest");
ShowMainMenu();
}
}
/// <summary>
/// Called whenever Unity loses focus
/// </summary>
/// <param name="active">If the game is currently
active</param>
private void OnHideUnity(bool active)
{
/* Set TimeScale based on if the game is
paused */
Time.timeScale = (active) ? 1 : 0;
}
在这种情况下,如果我们登录到 Facebook,我们将向屏幕打印一条消息,并显示主菜单。同样,如果我们失去 Unity 的焦点,我们将暂停游戏。
我们还需要添加一些其他最终函数来完成我们的最终实现,我们将在下面添加:
/// <summary>
/// Attempts to log in on Facebook
/// </summary>
public void FacebookLogin()
{
List<string> permissions = new List<string>();
/* Add permissions we want to have here */
permissions.Add("public_profile");
FB.LogInWithReadPermissions(permissions,
FacebookCallback);
}
/// <summary>
/// Called once facebook has logged in, or not
/// </summary>
/// <param name="result">The result of our login request</param>
private void FacebookCallback(IResult result)
{
if (result.Error == null)
{
OnInitComplete();
}
else
{
print(result.Error);
}
}
public void ShowMainMenu()
{
if (facebookLogin != null && mainMenu != null)
{
facebookLogin.SetActive(false);
mainMenu.SetActive(true);
}
}
在这种情况下,我们正在访问玩家的公共资料,其中包含诸如他们的姓名和他们的个人资料图片等信息。
注意
对于我们可以访问的所有属性,请查看developers.facebook.com/docs/facebook-login/permissions#reference-public_profile。
- 保存你的脚本,转到Facebook 登录按钮,并将按钮的OnClick()动作更改为现在通过点击+按钮并将主菜单对象拖放到其中,然后选择主菜单行为 | Facebook 登录来调用你的函数:

图 8.21:调用 FacebookLogin 函数
- 然后,在
MainMenuBehaviour.ShowMainMenu函数中。

图 8.22:设置“继续作为访客”按钮的界面
- 最后,我们需要设置我们创建的变量。在层次结构窗口中选择主菜单对象,然后设置主菜单和Facebook 登录属性:

图 8.23:设置主菜单行为属性
确保将Facebook 登录设置为包含两个按钮的面板对象。
- 保存你的场景,开始游戏,然后点击Facebook 登录按钮:

图 8.24:用户访问令牌请求屏幕
重要提示
为了在编辑器中正确查看所有内容,最好最大化游戏标签页,您可以通过右键点击游戏标签页并选择最大化,或者通过在工具栏上勾选播放时最大化选项来实现。
现在,您应该会看到一个要求用户访问令牌的菜单,这是一个每个资料都有的值,我们可以将其关联起来。我们需要去 Facebook 获取这个令牌,所以这就是我们接下来要做的。
- 点击查找访问令牌页面,浏览器将打开一个新页面:

图 8.25:访问令牌工具页面
- 然后,您需要点击需要授予权限链接,然后,在生成访问令牌中点击继续,您将在用户令牌下看到一个字符序列。复制该字符串,将其粘贴到 Unity 中的用户访问令牌属性中,然后点击发送****成功按钮。
注意
如果在授予权限时出现错误,提示此应用的未来 Facebook 活动已关闭,这意味着您的 Facebook 设置不允许您的 Facebook 资料在 Facebook 之外使用。为了使用 Facebook 进行登录,您的账户必须启用 Off-Facebook 跟踪。为此,您可以访问www.facebook.com/off_facebook_activity,并确保未来的 Off-Facebook 活动值设置为开启,以便能够登录。如果您不想被跟踪,我们将允许用户以访客身份登录。
现在,您会注意到控制台已经打印出我们已登录 Facebook,并且当我们发送密钥时菜单会关闭:

图 8.26:已登录 Facebook
注意
有关用户访问令牌的更多信息,请参阅developers.facebook.com/docs/facebook-login/access-tokens/#usertokens。
现在我们有了登录 Facebook 的能力,我们可以使用从 Facebook 获取的信息来定制我们的游戏,这就是我们接下来要做的。
显示 Facebook 名称和资料图片
做一件好事就是让我们的游戏适应我们的玩家。因此,一旦玩家登录,我们将欢迎他们,并按照以下步骤在屏幕上显示他们的图像:
-
再次前往
MainMenuBehaviour脚本。从那里,我们需要添加一个新的using语句来显示图像,并更改我们需要更改的文本,以便使用 Unity 的 UI 系统和 TextMeshPro:using UnityEngine.UI; // Image using TMPro; //TextMeshProUGUI -
然后,我们需要添加两个新的变量:
[Tooltip("Will display the user's Facebook profile pic")] public Image profilePic; [Tooltip("The text object used to display the greeting")] public TextMeshProUGUI greeting;
这些变量将保存我们从 Facebook 获取的信息,以便我们显示。
-
之后,我们将更新
ShowMainMenu函数并添加一些新函数来使用:public void ShowMainMenu() { if (facebookLogin != null && mainMenu != null) { facebookLogin.SetActive(false); mainMenu.SetActive(true); if (FB.IsLoggedIn) { /* Get information from Facebook profile */ FB.API("/me?fields=name", HttpMethod.GET, SetName); FB.API("/me/picture?width=256&height=256", HttpMethod.GET, SetProfilePic); } } }
FB.API函数调用 Facebook 的 Graph API 以获取数据或代表用户执行操作,并允许我们获取之前定义的我们有权限获取的信息。在我们的案例中,我们正在寻找用户的姓名和头像,并在获取到这些数据后分别调用SetName和SetProfilePic函数。
然而,我们目前没有SetName和SetProfilePic函数,所以我们将继续添加它们。
-
在脚本的
Facebook区域添加以下附加代码:private void SetName(IResult result) { if (result.Error != null) { print(result.Error); return; } string playerName = result.ResultDictionary["name"].ToString(); if (greeting != null) { greeting.text = "Hello, " + playerName + "!"; greeting.gameObject.SetActive(true); } } private void SetProfilePic(IGraphResult result) { if (result.Error != null) { print(result.Error); return; } // Variable setup int texWidth = result.Texture.width; int texHeight = result.Texture.height; Rect rect = new Rect(0, 0, texWidth, texHeight); Vector2 pivot = Vector2.zero; Texture2D texture = result.Texture; // Create the profile pic Sprite fbImage = Sprite.Create(texture, rect, pivot); if (profilePic != null) { profilePic.sprite = fbImage; profilePic.gameObject.SetActive(true); } }
获取数据后,我们将修改图像或字符串以显示我们检索的新数据。
重要提示
更多关于FB.API函数的信息,请查看developers.facebook.com/docs/unity/reference/current/FB.API。
- 现在,我们需要实际创建我们想要显示的文本和图像。打开
Safe Area Holder。然后,在Welcome Profile上右键点击:

图 8.27:创建欢迎配置文件
这将作为玩家所有信息的容器。
-
使用
10。从那里,将子对齐更改为底部居中,然后在控制子大小属性下检查宽度和高度。然后,添加一个内容大小适配器组件,并将水平适配和垂直适配大小更改为首选大小。最后,在锚点预设菜单中,按住Alt + Shift并选择底部居中。 -
现在,在层次结构选项卡中选择Welcome Profile对象,右键点击它,然后选择UI | Text - TextMeshPro。
-
重命名下一个
Greeting。 -
然后,调整
Welcome的大小到更大一些,例如50,将顶点颜色更改为黑色,然后调整对齐使其垂直和水平居中:

图 8.28:问候文本设置
- 同样,接下来我们右键点击
256。之后,也要检查256,因为 Facebook 可能会给我们提供比这更大的图片,这将保持图片的大小。布局元素(脚本)组件非常适合允许你覆盖布局组默认执行的操作,如果你没有从默认行为中获得你想要的结果,这可能很有用。
注意
更多关于布局元素(脚本)组件的信息,请查看developers.facebook.com/docs/unity/,或docs.unity3d.com/Manual/script-LayoutElement.html。
- 接下来,更改
Profile Pic的名称,然后重新排列它,使其在层次结构中位于Greeting对象之上:

图 8.29:头像设置
使用水平布局组重新排列对象会改变它们的放置顺序。
如果你将分辨率更改为一个非常小的尺寸,图像将绘制在我们的菜单上方。这是由于两个画布都被告知它们在绘制时的优先级相同,类似于 2D 游戏中的 Z 冲突工作方式。为了解决未来可能的问题,我们将把缩放画布作为背景元素。
-
要这样做,我们将选择
1。 -
现在,回到主菜单对象,并在MainMenuBehaviour组件中设置问候语和个人资料图片属性。
-
最后,由于我们不想在游戏开始时显示它们,让我们关闭问候语以及我们的个人资料图片对象。
-
保存我们的游戏,然后通过适当的登录信息再次启动它:

图 8.30:登录到 Facebook
如你所见,我们已经登录,你可以看到我的名字,但我有一个并非我实际个人资料的图片。这是因为它正在使用我的游戏资料。如果我们想使用我的实际 Facebook 个人资料图片,我们必须向我们的应用程序添加另一个权限:
- 在 Graph API 探索器中,回到权限部分,在添加权限下,点击下拉菜单并选择gaming_user_picture。

图 8.31:添加用户图片选项
- 从那里,再次点击生成访问令牌并获取一个新的访问令牌,该令牌会共享你的实际配置文件信息。请注意,用户必须选择是否与你共享此信息。

图 8.32:选择如何登录 Facebook
现在使用你的新访问令牌尝试运行游戏。

图 8.33:登录到实际 Facebook 账户
如前述截图所示,我登录后检索了我的实际 Facebook 信息。
Facebook 仍然是一个对游戏开发者来说极其有用的平台,可以帮助个性化用户的游戏体验。这可以很容易地扩展到利用 Facebook 拥有的其他数据,并与所有用户的亲朋好友分享内容。
对于那些只想拥有登录游戏以验证特定平台玩家的能力的人来说,还有其他几个可用的选项供你使用。有关详细信息,请参阅docs.unity.com/authentication/SettingUpExternalIdentityProviders.html。
摘要
在本章中,我们介绍了一些我们可以与他人分享我们的游戏的方法,以及个性化我们的游戏体验和利用社交媒体为我们提供的功能。我们首先添加了一个简单的评分系统,然后允许用户通过 Twitter 分享他们的分数。然后我们设置了 Facebook SDK,使得我们可以登录并玩游戏,同时检索有关我们用户的信息,我们可以使用这些信息来定制他们的游戏体验。
既然我们的游戏有人玩,我们就希望他们能够持续回来并长时间地玩。实现这一目标的最简单方法之一是通过使用通知,我们将在下一章中探讨这一点。
第三部分:游戏感觉/润色
在本书的这一部分,我们将专注于为您的游戏添加润色,以增强整体玩家体验。到本部分结束时,您将拥有所有必要的工具和知识,通过润色游戏和玩家体验,将您的游戏提升到下一个水平。
本部分包含以下章节:
-
第九章, 通过通知保持玩家参与度
-
第十章, 使用 Unity 分析工具
-
第十一章, 远程配置
-
第十二章, 提升游戏感觉
-
第十三章, 构建我们的游戏发布版本
-
第十四章, 将游戏提交到应用商店
-
第十五章, 增强现实
第九章:使用通知保持玩家参与
让用户不断回到你的游戏中的最佳方式之一是通过使用推送通知。这允许你在用户不使用你的游戏时与他们保持联系。如果使用得当,这可以让用户长时间玩你的游戏。过于频繁或不当使用通知会导致用户静音你的应用通知,这并不是一个理想的情况。
在本章中,我们将探讨如何为 Android 和 iOS 设备创建通知。然后我们将学习如何安排通知,以便玩家在之后返回游戏,以及我们可以自定义它们的方式。
本章分为多个主题。它包含从开始到结束的简单、分步的过程。以下是我们的任务大纲:
-
设置通知
-
提前安排通知
-
自定义通知展示
-
取消通知
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需做最小改动即可。如果你想要下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。你还可以在 Unity 编辑器系统要求 部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。为了部署你的项目,你需要一个 Android 或 iOS 设备。
你可以在 GitHub 上找到本章中存在的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter09。
设置通知
在我们开始向我们的项目添加通知之前,我们需要添加一个 Unity 提供的特殊预览包。按照这里给出的步骤操作:
-
从 Unity 编辑器中,转到 窗口 | 包管理器。
-
从那里,如果左上角部分没有显示 包:Unity 注册表,请从 包 菜单的工具栏中选择 项目 下拉菜单,并选择 Unity 注册表。
-
滚动到可用的选项,直到你到达移动通知并选择它。一旦到达那里,点击它旁边的箭头并选择查看所有版本,然后选择最新版本(在我的情况下,是版本 2.0.2)。从那里,点击安装按钮,你将看到以下截图:

图 9.1:安装移动通知包
注意
重要的是要注意,此包要求你的游戏使用 Android 5.1(API 级别 22)和 iOS 10 或更高版本才能正常工作。
我们还将使用由 Unity 编写的另一个跨平台包装器,以便快速实现通知功能并消除编写平台特定代码的需求。
- 然后,打开 Samples 部分,然后点击 Notification Samples 按钮旁边的 Import 按钮。

图 9.2:导入通知样本
这个由 Unity 创建的项目用于展示如何在一些实际示例中使用 Unity 的移动通知 API。我们正在使用它作为跨平台包装器,这将允许我们创建一次通知,它将在 Android 和 iOS 上工作,而无需我们进行任何额外的工作。
-
安装完成后,你可以关闭 Package Manager 窗口。为了确保我们可以导出我们的项目,我们需要确保我们的项目具有正确的最小 API 级别。
-
接下来,通过访问 Edit | Project Settings 菜单进入 Project Settings。从那里,转到 Player 选项,并在 Other Settings 下滚动到 Minimum API Level 并确认它设置为与移动通知包中指定的版本(在本例中为 Android 5.1 ‘Lollipop’(API 级别 22))或更高版本。之后,你可以关闭 Project Settings 窗口:

图 9.3:确保设置正确的最小 API 级别
你应该能看到有几个文件夹是项目的一部分。我们关心的文件位于 Assets\Samples\Mobile Notifications\2.0.2\Notification Samples\Scripts 文件夹中。请看以下截图:

图 9.4:通知样本脚本文件夹
这为我们提供了所需的代码——特别是 GameNotificationManager 类——可以添加到我们的脚本中。在这个时候,我们可以将这些脚本移动到我们的 Scripts 文件夹的子文件夹中,并删除其他文件,或者保持文件在当前位置。
要开始在屏幕上显示通知,我们首先需要在我们的级别中添加一个新的对象,该对象将包含游戏通知管理器:
-
如果还没有打开,请打开 MainMenu 场景。从那里,通过访问 GameObject | Create Empty 创建一个新的游戏对象。
-
从
Notifications Manager中,为了整洁,通过右键点击 Transform 组件并选择 Reset Position 选项来重置 Transform 组件的 Position 属性。 -
之后,附加
gamen,然后从列表中选择 Game Notifications Manager。它应该看起来像以下截图:

图 9.5:添加游戏通知管理器
在组件放置完毕后,我们可以进行创建第一个通知所需的设置。由于 GameNotificationsManager 类的实现,我们需要另一个脚本来发送通知,我们将称之为 NotificationsController。
-
从
Assets/Scripts文件夹创建一个新的 C# 脚本,命名为NotificationsController。双击新创建的文件以打开你选择的代码编辑器。 -
接下来,为该类添加以下代码:
using UnityEngine;
using NotificationSamples; /* GameNotificationManager */
public class NotificationsController : MonoBehaviour
{
private GameNotificationsManager notificationsManager;
// Start is called before the first frame update
private void Start()
{
/* Get access to the notifications manager */
notificationsManager =
GetComponent<GameNotificationsManager>();
/* Create a channel to use (required for Android)
*/
var channel = new
GameNotificationChannel("channel0",
"Default Channel",
"Generic Notifications");
/* Initialize the manager so it can be used. */
notificationsManager.Initialize(channel);
}
}
在前面的代码中,我们首先通过组件获取对 GameNotificationsManager 类的访问权限。由于我们将此脚本附加到包含此脚本的同一游戏对象上,我们可以使用 GetComponent 函数。之后,我们创建一个频道来发布我们的通知。最后,我们使用该频道初始化 GameNotificationsManager 组件。
- 保存你的脚本并返回到 Unity 编辑器。在 检查器 窗口中,将 通知控制器 脚本附加到 通知管理器 对象上,如图下所示:

图 9.6:添加通知管理器
现在我们已经完成了设置,让我们看看我们如何实际安排一个通知发生。
提前安排通知
创建通知的最常见形式之一是要求玩家在以后的时间回来玩游戏。这鼓励用户继续玩我们的游戏,并多次回来。我们可以通过以下步骤设置未来的交付时间来实现这一点:
-
打开
NotificationsController脚本,并向其中添加以下函数:public void ShowNotification(string title, string body, DateTime deliveryTime) { IGameNotification notification = notificationsManager.CreateNotification(); if (notification != null) { notification.Title = title; notification.Body = body; notification.DeliveryTime = deliveryTime; notificationsManager.ScheduleNotification( notification); } }
此函数接受三个参数——标题、正文和发送通知的时间。
-
此函数需要使用
System命名空间中的DateTime类,因此请在NotificationsController文件顶部添加以下行:using System; /* DateTime */ -
在测试中调用函数,以确保一切设置正确,让我们在
Start函数中调用该函数,添加以下突出显示的代码:// Start is called before the first frame update private void Start() { /* Get access to the notifications manager */ notificationsManager = GetComponent<GameNotificationsManager>(); /* Create a channel to use for it (required for Android) */ var channel = new GameNotificationChannel("channel0", "Default Channel", "Generic Notifications"); /* Initialize the manager so it can be used. */ notificationsManager.Initialize(channel); /* Create sample notification to happen in 5 seconds */ var notifText = "Come back and try to beat your score!!"; var notifTime = DateTime.Now.AddSeconds(5); ShowNotification("Endless Runner", notifText, notifTime); }
在此示例中,我们将 "Endless Runner" 作为标题,将 "Come back!" 作为正文,对于第三个参数,我们通过使用 DateTime.Now 获取当前时间,然后使用 AddSeconds 方法添加 5 秒,传递 5。
-
保存脚本并返回到 Unity 编辑器。
-
不幸的是,你无法在您的电脑上测试通知是否工作。我们必须导出游戏以查看它是否正确工作。
-
将你的游戏导出到你的设备上并开始游戏。正如你所看到的,我们的通知正在正常工作!

图 9.7:我们的默认通知正常工作
重要提示
默认情况下,游戏通知管理器组件的模式设置为队列清除和重新安排,这将导致如果你打开游戏,将无法看到通知。如果你希望始终看到通知,请将模式更改为无队列。
-
通常情况下,这类通知应该在玩家上次玩完游戏后的一天发送。我们可以通过修改函数来实现这一点:
// Start is called before the first frame update private void Start() { /* Get access to the notifications manager */ notificationsManager = GetComponent<GameNotificationsManager>(); /* Create a channel to use (required for Android) */ var channel = new GameNotificationChannel("channel0", "Default Channel", "Generic Notifications"); /* Initialize the manager so it can be used. */ notificationsManager.Initialize(channel); /* Create sample notification to happen in 5 seconds */ var notifText = "Come back and try to beat your score!!"; var notifTime = DateTime.Now.AddDays(1); ShowNotification("Endless Runner", notifText, notifTime); }
这样,当我们达到某个等级时,通知将在一天后显示,但每次我们进入主菜单时也会发生这种情况。为了防止这种情况,我们可以添加一个static bool变量,在添加通知时将其打开。在 Unity 中,当一个变量被标记为static时,它将在整个程序运行过程中保持一致。要添加此变量,请按照以下步骤操作:
-
更新脚本,添加以下加粗显示的代码:
private static bool addedReminder = false; // Start is called before the first frame update private void Start() { /* Get access to the notifications manager */ notificationsManager = GetComponent<GameNotificationsManager>(); /* Create a channel to use (required for Android) */ var channel = new GameNotificationChannel("channel0", "Default Channel", "Generic Notifications"); /* Initialize the manager so it can be used. */ notificationsManager.Initialize(channel); /* Check if the notification hasn't been added yet */ if (!addedReminder) { /* Create sample notification to happen later */ var notifText = "Come back and try to beat your score!!"; var notifTime = DateTime.Now.AddDays(1); ShowNotification("Endless Runner", notifText, notifTime); /* Cannot be added again until the user quits game */ addedReminder = true; } } -
保存你的脚本。现在,每次我们玩游戏时,我们只会看到一次通知!
这展示了我们如何在脚本中创建通知,但到目前为止,通知看起来有点简单。幸运的是,我们可以自定义通知,这是我们接下来要做的。
注意
如果你想要了解如何通过 Google Firebase 等工具从 Unity 外部发送通知到你的应用,请查看firebase.google.com/products/cloud-messaging。
自定义通知
Unity 包含一些默认的视觉效果,用于与通知一起使用,但通常,用我们自己的内容替换可以帮助我们的游戏脱颖而出,并使玩家更具视觉吸引力。为了为 Android 通知提供自定义图标,你需要有一个至少 48 x 48 像素的小图标,并且只有白色像素和透明的背景。大图标至少需要 192 x 192 像素,可以有任何我们想要的颜色。你可以创建自己的图像,或者使用示例代码中提供的Hi-ResIcon.png和Small-ResIcon.png图像,这些图像位于 GitHub 仓库的Chapter 08/Assets/Sprites文件夹中。按照以下步骤进行自定义:
-
从项目窗口中选择你计划用于小图标和大图标的图像。
-
在选择图像后,转到检查器窗口并检查Alpha Is Transparency属性。
-
最后,打开高级选项并检查读写属性。点击应用按钮以使更改生效。
你可以在以下截图中的检查器窗口中看到选项:

图 9.8:设置通知图标所需的属性
到目前为止,我们的图片已经准备好了,我们可以开始将它们放入通知中。为此,我们需要进入项目设置菜单:
-
通过访问编辑 | 项目设置来打开项目设置菜单。
-
从那里,转到移动通知设置选项。
-
从菜单中,您将看到两个选项——iOS 和 Android。我们希望使用 iOS 选项的默认属性,因此如果尚未选择,我们首先选择Android。
-
检查设备重启时重新安排通知选项。这将使得如果有人再次玩游戏,他们将不再收到我们之前创建的通知。这有助于用户不会因为频繁收到垃圾信息而感到烦恼。
-
接下来,在通知图标下,点击加号(+)图标。将小图标图像拖放到第一个2D 纹理选项中。然后,再次点击加号(+)图标,并将类型更改为大号。之后,将您的大图标分配到 2D 纹理位置:

图 9.9:设置移动通知图标
-
然后,返回到
NotificationsController脚本并更新ShowNotification函数以使用我们新的图标:public void ShowNotification(string title, string body, DateTime deliveryTime) { IGameNotification notification = notificationsManager.CreateNotification(); if (notification != null) { notification.Title = title; notification.Body = body; notification.DeliveryTime = deliveryTime; notification.SmallIcon = "icon_0"; notification.LargeIcon = "icon_1"; notificationsManager.ScheduleNotification( notification); } } -
保存您的脚本并返回到 Unity 编辑器。将您的游戏导出为 Android,您应该会看到图标已更新。现在,通知将显示工具栏中的小图标,如图下所示:

图 9.10:显示小通知图标
当访问通知本身时,它将使用两个图标!这可以在以下屏幕截图中看到:

图 9.11:从通知栏中查看带有两个图标的通知
您还可以通过使用如下类似的行来修改其他属性,例如 iOS 中使用的徽章数字:
notification.BadgeNumber = 5;
重要提示
想要了解更多关于如何自定义通知的信息,请查看docs.unity3d.com/Packages/com.unity.mobile.notifications@1.0/manual/index.html。
这允许我们的通知看起来就像我们想要的那样,具有我们希望拥有的尽可能多的光泽。现在我们已经将通知自定义成我们想要的样子,讨论如何取消我们不再希望发生的通知可能是有意义的。
取消通知
我们可能想要取消通知的几个原因。这可能是由于在游戏过程中,玩家做出了一个决定,导致某些内容不再相关。
对于这个示例,让我们创建一个示例通知,在我们有机会取消它之前取消它。但为了取消一个通知,我们必须有一种方法来知道哪个通知是哪个。幸运的是,通知管理器有一个名为Id的属性,每个通知都有一个。我们可以手动设置通知,或者 Unity 会为我们生成它。在我们的情况下,我们将使用自动生成的:
-
打开
NotificationsController脚本并转到ShowNotification函数。更新如下:public int? ShowNotification(string title, string body, DateTime deliveryTime) { IGameNotification notification = notificationsManager.CreateNotification(); if (notification != null) { notification.Title = title; notification.Body = body; notification.DeliveryTime = deliveryTime; notification.SmallIcon = "icon_0"; notification.LargeIcon = "icon_1"; var pendingNotif = notificationsManager.ScheduleNotification( notification); return pendingNotif.Notification.Id; } return null; }
注意,这个函数的返回类型已从void更改为int?,问号不是笔误。这被称为System.Nullable结构体,它基本上是一种特殊类型,允许我们有一个变量,它可以包含与其关联的类型的任何值,也可以被分配为null。这意味着这个值可以设置也可以不设置。一些平台,如 PC,没有对系统通知的支持,因此在这些情况下,他们选择使用null而不是类似负一的东西。
-
现在我们已经更新了脚本,让我们现在展示一个示例,说明我们如何取消一个通知。转到
Start函数并更新如下:// Start is called before the first frame update private void Start() { /* Get access to the notifications manager */ notificationsManager = GetComponent<GameNotificationsManager>(); /* Create a channel to use (required for Android) */ var channel = new GameNotificationChannel("channel0", "Default Channel", "Generic Notifications"); /* Initialize the manager so it can be used. */ notificationsManager.Initialize(channel); /* Check if the notification hasn't been added yet */ if (!addedReminder) { /* Create sample notification to happen in 5 seconds */ var notifText = "Come back and try to beat your score!!"; // After 5 seconds var notifTime = DateTime.Now.AddSeconds(5); // After 1 day //notifTime = DateTime.Now.AddDays(1); ShowNotification("Endless Runner", notifText, notifTime); // Example of canceling a notification var id = ShowNotification("Test", "Should Not Happen", notifTime); if(id.HasValue) { notificationsManager.CancelNotification( id.Value); } /* Cannot be added again until the user quits game */ addedReminder = true; } } -
保存你的脚本并构建你的游戏。如果你在你的 Android 或 IOS 设备上打开项目,你应该能够看到我们设置为不可取消的验证正常播放。但被取消的那个没有发生。
小贴士
你也可以通过简单地从GameNotificationManager类中调用CancelAllNotifications函数一次性取消所有通知。有关取消通知的更多信息,请参阅docs.unity3d.com/Packages/com.unity.mobile.notifications@1.0/manual/index.html。
摘要
到目前为止,我们已经看到了如何利用 Unity 的移动通知包为我们的玩家创建通知。我们学习了如何安排它们在未来发生,以及如何自定义这些通知以拥有我们独特的视觉风格!
我们现在已经为玩家提供了所有必要的条件来玩游戏并返回我们的游戏,但我们只依赖于我们创建的内容。除此之外,我们可能还想了解玩家在玩游戏时的行为。然后,我们可以利用这些信息来改进和/或调整我们的游戏。
在下一章中,我们将探讨如何使用 Unity Analytics 的工具来实现这一点。
第十章:使用 Unity 分析
制作游戏是一次美妙的体验,也是大量的辛勤工作,但在设计项目时,您必须依靠您的经验和直觉来使其尽可能出色。在游戏行业中,我们通常会使用测试游戏——一个选择的人玩游戏并给出反馈的过程,然后我们使用收到的反馈来改进项目。
这种测试游戏通常是在现场进行的;然而,通过为移动设备创建游戏,发布后会有很多人玩游戏,并且他们中的大多数都有互联网连接。有了这种玩游戏和在线的组合,我们可以将有关游戏如何被玩的数据发送给我们自己。这仍然允许我们进行针对大量不同人群的测试游戏。能够查看我们的数据将使我们能够检查做出改变游戏的选择是否正确,并且我们能够即时调整我们的游戏。
这些数据可能关于玩家在游戏中倾向于在哪里死亡这样简单的事情,或者像他们多久回来玩一次、每天平均玩游戏的时间、同时有多少用户、人们在停止游戏前玩了多久以及他们做出的选择这样的事情。在本章的整个过程中,我们将学习如何通过使用 Unity 的 Gaming Services 平台和内置分析系统来了解用户的行为。
本章将涵盖多个主题。本章本身是一个从开始到结束的简单步骤过程。以下是我们的任务概述:
-
设置 Unity 分析
-
跟踪自定义事件
-
使用漏斗分析器
-
使用远程配置调整属性
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来编辑器的版本中只需进行最小改动即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署您的项目,您需要一个 Android 或 iOS 设备。
您可以在 GitHub 上找到本章中提供的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter10。
设置分析
尽管我们已从 Unity 游戏服务中激活了 Analytics 选项,以便在 第七章 使用 Unity Ads 进行广告 中使用 Unity 的广告系统,但我们并没有真正深入到该系统本身。现在让我们按照以下步骤完成该设置的设置:
-
首先,我们需要在我们的项目中安装 Unity Analytics 包。我们可以通过返回 Unity 编辑器并打开包管理器来实现,方法是转到 窗口 | 包管理器。
-
这本书所使用的 Unity 版本存在一个错误,Analytics 包默认不会显示;相反,Unity 会显示遗留的 Analytics 库包。然而,可以通过点击
com.unity.services.analytics来添加新包。如果一切顺利,你应该能够通过检查 包 管理器 窗口来确认 Analytics 包已正确安装:

图 10.1:附加了 Analytics
下一步是初始化 SDK 并确保我们的用户同意记录他们引起的事件。为此,我们需要编写一些代码。
-
从
Assets/Scripts文件夹创建一个新的 C# 脚本,命名为AnalyticsManager。 -
双击新添加的脚本,使用您选择的代码编辑器打开它,并使用以下代码:
using System.Collections.Generic; /* List */ using Unity.Services.Analytics; /* AnalyticsService, ConsentCheckException */ using Unity.Services.Core; /* UnityServices */ using UnityEngine; public class AnalyticsManager : MonoBehaviour { // Start is called before the first frame update async void Start() { try { await UnityServices.InitializeAsync(); List<string> consentIdentifiers = await AnalyticsService.Instance .CheckForRequiredConsents(); } catch (ConsentCheckException) { /* Something went wrong */ } } } -
保存脚本并返回到 Unity 编辑器。
这是初始化 SDK 以及向 Unity 游戏服务发送事件所需的最少代码量。
注意,这里有几个关键词之前尚未使用过。例如,我们的 Start 函数在返回类型前有 async。同样,在两个函数调用之前也有 await。在这里,我们正在使用一种称为异步编程的东西。await 关键字允许我们在 async 方法返回值之前等待。因此,主应用程序线程在那里停止,直到它收到返回值。如果我们没有使用 await 关键字,下一个函数将立即调用,由于我们需要在继续之前计算出结果,这就是为什么我们使用它的原因。async 关键字启用了 await 关键字。因此,任何使用 await 的方法都必须标记为 async。
注意
有关 async 和 await 的更多信息,请查看官方 MSDN 文档,网址为 learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/。
此外,请注意我们有一个try和catch块。当我们使用try然后是一段代码块时,我们是在指出我们放在那里的代码有可能失败。例如,可能抛出一个异常。如果在执行try块期间捕获到类型为ConsentCheckException的异常,这意味着同意检查出了问题。
如果这种情况正常发生,Unity 会向控制台抛出错误。catch块中的代码处理这种异常情况。catch块是,而不是向控制台抛出错误并导致游戏中的问题,这是我们尝试解决问题的方法。在我们的情况下,我们希望处理不使用 Unity 服务运行游戏的方式,这可能会意味着我们根本不会记录事件。
注意
更多关于try和catch的信息,请查看官方 MSDN 文档:learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch。
当然,仅仅有这段代码是没有用的。我们必须实际运行它。所以为了做到这一点,我们需要将脚本添加到一个对象上。为了方便使用,我们将创建一个新的对象来附加这个脚本。
- 在 Unity 编辑器中,通过转到
Analytics Manager创建一个空的游戏对象,重置其位置,然后将 Analytics Manager 脚本添加到该对象上。

图 10.2:Analytics Manager 设置
只要Analytics被启用,当我们按下播放按钮开始游戏时,编辑器就会向 Analytics 服务发送 App Start 事件:
这个好处在于我们可以确保这个功能能够正确工作,而无需导出我们的游戏。然而,这个消息默认情况下不会显示给我们。我们可以启用编辑器控制台日志记录,通过启用脚本定义符号来允许我们在实验时获得额外的可见性。
- 前往
UNITY_ANALYTICS_EVENT_LOGS。完成后,点击应用按钮并等待它完成编译脚本。
提示
如果由于某种原因,你的脚本定义符号在 Android 平台上消失了,切换到 PC 设置并将值放入其中似乎可以纠正这个问题。
- 保存项目并运行游戏。如果一切顺利,你应该会在控制台窗口中看到事件出现:

图 10.3:控制台接收我们的默认事件
现在我们已经使这个功能工作,我们可以检查游戏侧是否发生了事件。
- 从 Unity 的顶部栏进入 服务 | 常规设置。从那里进入 分析 – 游戏服务,然后点击 转到仪表板。一旦进入,点击 事件浏览器 按钮,你应该能看到 Unity 收到的最新 100 个事件:

图 10.4:事件浏览器页面
现在我们已经设置了 Unity Analytics,让我们开始创建自己的自定义事件以进行跟踪!
跟踪自定义事件
Unity Analytics 自动执行多项不同操作以简化使用。然而,作为一名游戏设计师,你可能经常想要检查游戏的一些方面是否被使用,或者玩家是否到达了某些内容。为了跟踪这些信息,我们可以利用自定义 事件系统。
自定义事件是用户在玩游戏时发送到云端的片段数据。每个自定义事件都可以有自己的参数,这将允许我们在数据生成时过滤发送的数据。我们将讨论如何通过代码使用云传输信息。
发送基本的自定义事件
我们将要发送的第一种事件只是一个事件名称。这可以用于跟踪人们访问某个地方的次数,或者检查是否出现了无效的情况。为了便于触发和测试跟踪,我们将使每次游戏暂停时发生一个事件。让我们看看步骤:
-
打开
PauseScreenBehaviour脚本,并在脚本顶部添加以下using语句:using Unity.Services.Analytics; /* AnalyticsService */
此命名空间包含 Unity 分析系统使用的所有函数。
-
更新
SetPauseMenu函数以包含以下突出显示的代码:/// <summary> /// Will turn our pause menu on or off /// </summary> /// <param name="isPaused">is the game currently paused</param> public void SetPauseMenu(bool isPaused) { paused = isPaused; /* If the game is paused, timeScale is 0, otherwise 1 */ Time.timeScale = (paused) ? 0 : 1; pauseMenu.SetActive(paused); onScreenControls.SetActive(!paused); /* Send custom gamePaused event */ if (paused && (AnalyticsService.Instance != null)) { AnalyticsService.Instance.CustomData( "gamePaused"); AnalyticsService.Instance.Flush(); } }
当 pauseMenu 被激活时,此代码将调用 AnalyticsService.Instance.CustomData 函数。AnalyticsService.Instance.CustomData 的第一个参数是一个字符串,这是你希望事件拥有的名称。这个名称将在 Unity Analytics 中使用。如果可用网络连接,事件会自动每 60 秒发送一次。然而,如果你想立即上传所有记录的事件到服务器,也有 AnalyticsService.Instance.Flush 函数,它会在调用时立即上传事件。
- 保存脚本,然后返回到 Unity 编辑器。一旦进入,开始玩游戏然后暂停。正如你所见,事件已成功通过云端发送!

图 10.5:暂停事件成功上传
以前,Unity Analytics 会接受你发送的任何事件。但是,使用这个新系统,你必须提前在仪表板上定义事件,否则它将忽略这些信息。因此,在我们开始在事件管理器中看到事件触发之前,我们需要去仪表板定义这个事件。
如前所述,信息在 Unity 仪表板上显示可能需要一些时间,但在这个时候查看这些信息将来可以在哪里接收是个好主意。
- 从仪表板进入分析,然后选择事件管理器。
这是你可以看到从游戏中接收到的自定义事件和参数的地方。
- 从那里,选择添加新项,然后选择自定义事件。

图 10.6:添加新的自定义事件
- 将自定义事件命名为
gamePaused,与我们在这里做的完全一样,用于传递到SetPauseMenu函数中的CustomData函数的字符串。我们还将为事件添加一个事件描述,供我们未来参考。之后,点击创建按钮,事件应该就准备好供我们使用了。

图 10.7:设置自定义事件详细信息
- 最后,向下滚动到事件,并再次确认启用选项是开启的;否则,事件将只显示在事件浏览器的无效事件区域。

图 10.8:启用事件
-
返回 Unity 编辑器并再次播放你的游戏,触发
gamePaused事件,以确保它正确显示。 -
等待大约 15 分钟,返回事件浏览器,并点击那里的
gamePaused事件!为了更容易看到,你还可以点击gamePaused,然后点击应用按钮,以便仅将事件浏览器隔离出来以显示这些事件。
如果不是,事件浏览器中的事件更新可能需要一些时间,但它们最终会显示出来。

图 10.9:事件浏览器中的简单自定义事件
- 接下来,点击数据****探索器选项。
在gamePaused事件被调用时。
小贴士
如果你刚刚创建了事件,可能需要长达 12 小时才能接收到信息。如果那样的话,稍后再检查。过去,我不得不等待长达 48 小时才能得到数据,所以如果它需要一段时间才显示出来,请不要担心,尽管我在 Unity 游戏服务的最新版本中还没有遇到这个问题。
- 点击添加事件左侧的+按钮,将自定义事件添加到这个图表中。然后,选择添加事件下拉菜单,然后选择gamePaused。由于我们刚刚创建了事件,我们不会在分析中的先前日期看到它,但如果我们点击柱状图按钮来更改数据的显示方式,我们可以更容易地看到它:

图 10.10:添加事件
现在,当我们向下滚动时,我们可以看到gamePaused事件已经被调用了!
发送带属性的定制事件
我们还可能想要跟踪的是玩家在失败前能走多远。现在让我们看看如何做到这一点:
-
在仪表板上,首先,转到事件管理器。点击
gameOver,然后在事件描述字段中填写类似“记录玩家触发游戏结束事件时”的内容。之后,点击创建按钮。 -
从那里,你将被带到事件页面。点击分配参数按钮,然后从参数下拉菜单中选择添加****新参数。

图 10.11:添加新参数
- 对于参数名称,输入
score,然后为参数填写一个描述。对于参数类型,选择Float,然后点击创建按钮。

图 10.12:添加自定义参数
- 创建一个名为
gameOver的新事件和一个自定义参数,我们将称之为score,它将是一个浮点数。然后点击score,然后点击分配。

图 10.13:分配分数参数
- 最后,点击
gameOver事件将在发生时被保存。Unity 将询问你是否确定要启用此事件;点击启用按钮以确保它会发生。

图 10.14:启用事件
-
现在,返回 Unity 编辑器。首先,我们需要打开
ObstacleBehaviour脚本以修改游戏结束时发生的行为。 -
要使用参数利用 Unity Analytics,在文件顶部添加以下
using声明:using Unity.Services.Analytics; /* AnalyticsService */ using System.Collections.Generic; /* Dictionary */
顶部选项很明显,但我们还添加了System.Collections.Generic以便访问Dictionary类,我们将在下一部分代码中使用它。
-
接下来,我们将更新
OnCollisionEnter函数到以下内容:private void OnCollisionEnter(Collision collision) { var go = collision.gameObject; var playerBehaviour = go.GetComponent<PlayerBehaviour>(); // First check if we collided with the player if (playerBehaviour) { // Destroy the player //Destroy(collision.gameObject); // Destroy (Hide) the player player = go; player.SetActive(false); /* If Unity Analytics doesn't exist will throw an exception */ try { /* Define Custom Parameters */ var eventData = new Dictionary<string, object> { { "score", playerBehaviour.Score } }; AnalyticsService.Instance.CustomData("game Over", eventData); AnalyticsService.Instance.Flush(); } catch(ServicesInitializationException e) { /* Displays the exception but doesn't break * the game */ Debug.LogWarning(e.Message); } /* Call the function ResetGame after waitTime has passed */ Invoke("ResetGame", waitTime); } }
在这个脚本中我们做了很多事情。首先,我们重写了检查玩家使用组件作为变量的代码,这样我们就不必再次调用GetComponent了。除此之外,主要新增的是调用AnalyticsService.Instance.CustomData函数时带有第二个参数。第二个参数(这是可选的)是一个字典,我们还没有讨论过。
使用string类作为键类型,以便你可以引用其他数据类型。
注意
关于字典的更多信息,请查看csharp.net-informations.com/collection/dictionary.htm。
我们将此代码包裹在try和catch块中,因为如果调用AnalyticsService.Instance并且它是null,Unity 编写的代码将抛出异常。这将导致游戏崩溃并停止工作。如果从标题屏幕开始游戏,游戏目前运行良好,但如果从游戏场景开始游戏,它将不再正确工作,因为 Analytics 尚未初始化。这次,与上一个示例不同,我添加了一行来实际打印出错误给出的消息作为警告。让我们的代码尽可能健壮总是一个好主意,这就是为什么我们添加了这个检查。
-
保存脚本并返回到 Unity 编辑器。
-
玩游戏并输掉。注意在
gameOver事件中:

图 10.15:正在派发游戏结束事件
你也可以进入仪表板查看信息,但 Unity 表示可能需要 6 小时才能变得可见,尽管我通常在 15 分钟左右就能看到。你将立即在控制台窗口中看到消息,但由于它接收到的所有事件,它们在 Analytics 中不会立即填充,直到 Unity 端的后端计算完成。
-
在等待之后,转到
gameOver事件。 -
为了能够看到我们在最右侧创建的
score参数的值,点击看起来像<>的按钮来访问特定事件的 JSON 事件内容。请注意,我们可以在这里看到我们的分数值:

图 10.16:事件内容中的分数值
有了这些,我们现在知道如何访问事件了!
此信息以 JSON 文件的方式格式化。我们之前使用的Dictionary类。
事件浏览器的局限性之一是我们只能看到最近的 100 个事件。随着我们的项目越来越受欢迎,我们可能会遇到越来越多的活动。考虑到这一点,我们可能需要一种方法来解释这些信息。我们可以用来实现这一目标的方法之一是通过 SQL 数据探索器。这使我们能够访问创建的所有事件,但我们必须利用结构化查询语言(SQL),这是一种为管理数据库中存储的数据而创建的特定领域语言。
- 点击SQL 数据探索器按钮打开菜单。请注意,查询字段包含一些默认文本。

图 10.17:SQL 数据探索器
-
点击运行按钮以查看默认结果及其工作方式。一旦查询完成,你将能够使用从该查询中解释的数据创建图表。
-
在 图表设置 下,将 X 轴列 设置为 EVENT_DATE,将 Y 轴 1 列 设置为 COUNT(DISTINCT USER_ID),然后点击 应用 按钮。

图 10.18:图表设置
这将允许您以类似于我们在数据探索器中看到的方式创建图表,但如果我们要使用 SQL 的强大功能,这将给我们提供更精细的控制。
对于那些对进一步探索 SQL 感兴趣的人来说,市面上有关于 SQL 的整本书,但在这个案例中,我们想要从我们的自定义事件中提取分数值。因此,我们不想创建图表,而是想创建一个表格,这样我们除了编写一些自定义查询外,还可以看到如何导出数据。
-
在 查询 字段中,将代码替换为以下内容:
SELECT EVENT_JSON:score::FLOAT FROM EVENTS WHERE EVENT_NAME = 'gameOver'
此代码将遍历每个事件并检查事件名称是否为 gameOver。如果是,它将从中隔离 score 属性。这将允许我们创建整个项目中所有 score 属性的列表。
- 点击 运行 按钮并等待其执行。一旦完成,您将看到顶部的 共享 按钮变为蓝色,您可以按下它。一旦选择,点击 导出为 CSV 文件 选项。
CSV 代表 逗号分隔值。这是一种使用逗号分隔文件中指定值的文件类型,但如果您使用 Google Sheets 或 Excel 打开文件,程序将能够解析文件以类似于漂亮的表格的形式。

图 10.19:Microsoft Excel 中的分数值表
这为我们提供了一种查看玩家在玩游戏过程中所达到的所有不同分数值的方法。这可以帮助我们看到人们是否表现良好或真的很差,并允许我们根据需要调整游戏。
使用以下类似的片段:
SELECT EVENT_JSON
FROM EVENTS
WHERE EVENT_NAME = 'gameOver'
之前的代码将确保,而不是第一个事件只显示 6.5199995,我们会有每个槽位都包含我们之前讨论过的全部 JSON 数据——在这种情况下,以下内容:
"{
""clientVersion"": ""0.1"",
""collectInsertedTimestamp"": ""2022-12-23
05:36:22.379"",
""eventDate"": ""2022-12-23 00:00:00.000"",
""eventID"": 3072590187314819072,
""eventLevel"": 0,
""eventName"": ""gameOver"",
""eventTimestamp"": ""2022-12-23 14:36:19.072"",
""eventUUID"": ""0d924683-5667-4455-b899-722bfeae93cd"",
""gaUserAcquisitionChannel"": ""None"",
""gaUserAgeGroup"": ""UNKNOWN"",
""gaUserCountry"": ""KR"",
""gaUserGender"": ""UNKNOWN"",
""gaUserStartDate"": ""2022-07-28 00:00:00.000"",
""mainEventID"": 3072590187314819072,
""msSinceLastEvent"": 12124,
""platform"": ""PC_CLIENT"",
""score"": 6.51999473571777,
""sessionID"": ""0ce65df8-9696-40e8-ae44-924417e0d406"",
""timezoneOffset"": ""+0900"",
""userCountry"": ""KR"",
""userID"": ""21328f4689677b24baf59487ae951228""
}"
通过添加 :score::FLOAT,我们从 JSON 数据中获取 score 变量,并告诉计算机将其解释为浮点值。
对于那些对探索 SQL 更感兴趣的人,我建议查看 Unity 游戏服务的示例食谱 github.com/Unity-Technologies/UGS-SQL-Cookbook,其中包含了一组常见的 SQL 查询和与 Unity 分析一起使用的最佳实践。
当然,这只是一个简单的例子,所以我建议您为用户达到重要里程碑时创建自定义事件——例如,当用户升级或进行 内购(IAP)时。
现在我们知道了如何创建不同类型的事件,让我们看看我们如何实际上跟踪事件并利用漏斗分析器工具了解更多关于玩家在做什么。
与漏斗一起工作
我们想了解关于玩家们的许多事情之一是他们在实际玩游戏的方式——例如,用户是否跳过了我们的教程?为了跟踪玩家如何通过一系列事件,我们使用了漏斗。漏斗帮助我们确定玩家在游戏中的流失点。
如果你看到很多人没有到达某个步骤,你可以假设在前面步骤中发生的事情导致人们停止玩我们的游戏。
注意
关于漏斗如何工作以及为什么你想使用它们的信息,请查看data36.com/funnel-analysis/。
漏斗基于自定义事件的概念,我们在本章的使用属性发送自定义事件部分使用了它。我们可以使用漏斗工具(之前称为漏斗分析器)来查看通过这些漏斗发送的数据,然后我们可以使用这些数据来做出明智的决定,关于应该对游戏做出哪些更改。按照以下步骤添加工具:
- 从仪表板转到分析并选择漏斗:

图 10.20:漏斗页面
目前,还没有设置漏斗,因此我们应该创建一个。
- 点击+ 新建漏斗按钮并填写以下截图中的详细信息。然后,滚动到最底部并点击应用按钮:

图 10.21:漏斗设置
如果你已经玩过游戏,你可以看到已经触发的事件的结果:

图 10.22:事件触发结果
在我们的案例中,我们已经玩游戏并失败了,所以我们目前有 100%的完成率。
- 滚动到顶部页面并点击保存漏斗按钮。给漏斗起一个名字,描述你正在创建的内容,然后点击保存按钮。

图 10.23:漏斗设置
你应该能够选择该漏斗,并且它将提供有关它被调用的所有时间的信息。
这里使用的概念也可以很容易地以其他方式扩展,例如跟踪用户观看广告或进行游戏内购买的频率以及是什么原因导致他们这样做。
注意
当游戏运行正常时,漏斗分析非常出色,但如果游戏崩溃,也可以从 Unity Cloud Diagnostics 中获取相关的分析信息。有关该信息以及如何设置的信息,请参阅docs.unity.com/cloud-diagnostics/en/manual/CrashandExceptionReporting/SettingupCrashandExceptionReporting。
摘要
在本章中,我们探讨了多种利用 Unity 的 Analytics 工具来提升游戏的方法,从理解玩家行为到根据这些反馈调整游戏,而无需用户下载我们游戏的新版本。
具体来说,我们学习了如何设置 Unity 编辑器中的 Unity Analytics 部分,然后我们看到了如何利用代码创建事件并将其发送到云端供我们查看。有了这些数据,我们学习了如何利用漏斗和漏斗分析器来更深入地了解我们的玩家。
现在我们已经了解了如何创建事件,接下来让我们看看利用 Unity Gaming Services 的另一个主要好处——使用远程配置功能更改项目,我们将在下一章中了解更多相关信息。
第十一章:远程配置
获取您游戏的新构建版本并导出可能需要相当长的时间。实际上在 Unity 编辑器中进行更改需要时间,然后您必须导出游戏并在您针对的每个应用商店上上传新版本。然后,您必须花费时间等待它们批准应用,以及让每个人实际下载它。
我和学生讨论的其中一件事是创建可以轻松更改而无需打开 Unity 编辑器的项目。这可以通过使用数据驱动开发实践来实现,例如使用文本文件、AssetBundles 或 Unity 的远程配置(之前称为远程设置)菜单来构建关卡或遭遇,使我们能够即时修改已发布的游戏副本中的变量。
在本章中,我们将看到设置 Unity 的远程配置系统有多简单,以及我们如何通过改变玩家移动的速度来改变游戏难度来利用它进行一个简单的示例。
本章本身是一个从头到尾的简单步骤过程。以下是我们的任务概述:
-
远程配置设置
-
将游戏覆盖集成到游戏中
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小更改即可。如果您想下载本书中使用的确切版本,您可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署您的项目,您需要一个 Android 或 iOS 设备。
您可以在 GitHub 上找到本章中包含的代码文件,网址为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter11。
远程配置设置
为了让我们使用远程配置,我们首先需要做的是将远程配置包添加到我们的项目中。因此,让我们看看如何通过以下步骤来完成:
- 从 Unity 编辑器中,通过点击屏幕左上角的云按钮或转到窗口 | 通用 | 服务来打开包管理器的服务窗口(如图 11.1 所示)。

图 11.1:服务按钮的位置
如果一切顺利,你应该会看到以下截图中的内容:

图 11.2:包管理器 | 服务
- 从那里,向下滚动并点击远程配置包,然后点击右下角的安装按钮。如果一切顺利,您应该会看到以下截图类似的内容:

图 11.3:已安装的远程配置包
- 关闭包管理器,通过转到窗口 | 远程配置来打开远程配置窗口。这将打开一个独立的窗口,然后我会将其拖放到控制台旁边以便于使用。如果一切顺利,您的编辑器应该看起来类似于以下截图:

图 11.4:添加了远程配置窗口
并且,就这样,远程配置包被正确安装了!
就像在前一章中我们需要创建 Unity 游戏服务和我们的项目之间的连接以便使用 Unity 分析一样,我们还需要做同样的事情来调整远程配置值。
创建键值对
我们首先需要做的是创建我们想要更改的变量:
- 从 Unity 编辑器,如果远程配置窗口是打开的,点击在仪表板中查看选项。

图 11.5:远程配置页面
或者,对于在 Unity 仪表盘网站上的人,您可以点击LiveOps。一旦进入,在左侧,打开远程配置部分,然后点击位于其下的配置标签页。
这个部分是我们设置和修改值的位置。就像使用字典一样,设置是键值对,尽管目前只有一个配置production,但可以创建许多其他环境。通常,可以使用两种配置 - 来自构建设置窗口的True。
- 点击
RollSpeed。在选择类型下拉菜单中,选择浮点数。最后,在浮点数值字段中输入5。最后,点击添加按钮:

图 11.6:添加键
然后,让我们为DodgeSpeed变量(值为5)做同样的事情:

图 11.7:添加了 DodgeSpeed 和 RollSpeed 键
- 重要的是要注意,这实际上并没有做出任何改变。事实上,您会在值上方看到一个说明,指出有未发布的更改。注意那里有一个大蓝色按钮,上面写着发布。点击它以部署更改。它将弹出一个窗口询问您是否要确认更改:

图 11.8:确认我们的更改
-
现在我们有一些值可以获取,让我们看看我们实际上如何做到这一点。回到 Unity 编辑器。
-
在远程配置窗口中,点击拉取按钮。如果一切顺利,你应该能看到添加到我们项目中的值:

图 11.9:拉取后添加到我们的远程配置中的值
每次你准备从项目中更改远程配置时,拉取是一个好习惯,确保你总是拥有可能的最新的属性版本。并且,我们已经看到了如何创建不同的键值对添加到我们的项目中。
将游戏覆盖集成到游戏玩法中
现在我们已经看到了如何获取这些值以及系统是如何工作的,让我们看看我们如何将其集成到我们的项目中并使其影响游戏玩法:
-
如果游戏玩法场景尚未打开,请打开它,并通过转到
Remote Config Manager并重置其位置来创建一个新的 GameObject。 -
然后,从
Assets\Scripts文件夹中创建一个新的 C#脚本,命名为RemoteConfigManager。 -
将新创建的
RemoteConfigManager组件附加到我们在步骤 1中创建的RemoteConfigManager对象上。如果一切顺利,你的项目应该看起来类似于以下截图。

图 11.10:添加 RemoteConfigManager
-
在
RemoteConfigManager脚本中打开它,并用你选择的脚本编辑器替换其脚本,如下所示:using UnityEngine; using Unity.RemoteConfig; /* ConfigManager */ public class RemoteConfigManager : MonoBehaviour { public PlayerBehaviour playerBehaviour; public struct userAttributes { } public struct appAttributes { } private void Awake() { ConfigManager.FetchCompleted += ApplyRemoteSettings; ConfigManager.FetchConfigs<userAttributes, appAttributes>(new userAttributes(), new appAttributes()); } private void ApplyRemoteSettings(ConfigResponse configResponse) { /* Check if new settings have been loaded */ if (configResponse.requestOrigin == ConfigOrigin.Remote) { /* There are, so values should be updated */ playerBehaviour.UpdateRemoteConfigValues(); } } }
在Awake函数中,我们使用ConfigManager.FetchConfigs方法从远程服务器获取应用程序配置设置。在成功完成获取操作后,将触发ConfigManager.FetchCompleted事件。在这种情况下,我们添加了一个ApplyRemoteSettings函数,当该事件触发时也应该调用它,然后我们实现它。
该方法接受一个ConfigResponse结构体,它表示RemoteConfig获取的响应。值得注意的是requestOrigin属性,它是一个枚举,表示最后检索到的配置设置的来源点。它可以是三个选项之一:
-
缓存:我们当前会话中加载的配置设置是从之前的会话缓存的,因此没有加载新的配置设置 -
默认:当前会话中没有加载任何配置设置 -
远程:当前会话中从远程服务器加载了新的配置设置
在我们的情况下,只有当值是远程时,我们才需要做些事情。如果是这种情况,这意味着已经加载了新的设置,这意味着我们需要更新当前加载的值。
-
然后,我们需要转到
PlayerBehaviour脚本,并将以下内容添加到顶部部分,与其余的using语句一起:using Unity.RemoteConfig; /* ConfigManager */ -
之后,我们需要将
UpdateRemoteConfigValues函数添加到PlayerBehaviour类中,因为它目前不存在;否则,我们将得到编译器错误:/// <summary> /// Will update each value for this component we are using with /// Remote Config /// </summary> public void UpdateRemoteConfigValues() { /* Get the value from the cloud and set the value to use */ float newRollSpeed = ConfigManager.appConfig.GetFloat("RollSpeed"); Debug.Log("Update RollSpeed to: " + newRollSpeed); rollSpeed = newRollSpeed; /* Get the value from the cloud and set the value to use */ float newDodgeSpeed = ConfigManager.appConfig.GetFloat("DodgeSpeed"); Debug.Log("Update DodgeSpeed to: " + newRollSpeed); dodgeSpeed = newDodgeSpeed; }
在这种情况下,我们使用ConfigManager的appConfig属性,即RuntimeConfig对象,允许我们访问从云中获取的当前值,这些值是我们环境(s)当前设置的。然后,我们将rollSpeed和dodgeSpeed变量的当前值设置为从云中检索到的值。
关于ConfigManager类的更多信息,请查看以下内容:docs.unity3d.com/Packages/com.unity.remote-config@0.3/api/Unity.RemoteConfig.ConfigManager.htm
- 保存两个脚本并返回到 Unity 编辑器。然后,转到层次结构窗口,选择远程配置管理器,并在检查器窗口中,将玩家行为值分配给玩家对象。

图 11.11:添加玩家行为
- 在游戏开始后,你应该能在
Debug.Log语句中看到,它告诉我们值正在更新为我们在远程配置中放置的内容:

图 11.12:值更新正确
-
由于
dodgeSpeed和rollSpeed变量现在是通过UpdateRemoteConfigValues函数设置的,因此我们现在可以将它们从PlayerBehaviour脚本中隐藏起来,使类看起来如下所示:/// <summary> /// How fast the ball moves left/right /// </summary> [HideInInspector] public float dodgeSpeed = 5; /// <summary> /// How fast the ball moves forwards automatically /// </summary> [HideInInspector] public float rollSpeed = 5;
在这里,我们修改了两个属性,添加了[HideInInspector]标签,这将隐藏项目在检查器窗口中。我们还更改了变量,使用 XML 注释而不是工具提示,因为它们不再在检查器窗口中显示。
- 保存脚本并选择玩家对象。从那里,转到检查器窗口,注意属性在PlayerBehaviour组件中不再可见:

图 11.13:值在检查器窗口中隐藏
现在,值将通过远程配置组件设置,用户不会对为什么他们的值被玩家行为中的内容所替换感到困惑。
在游戏运行时调整这些值可以非常实用,并允许你分享游戏更改,而无需要求用户下载新版本!
摘要
在本章中,我们学习了如何使用远程配置来实时调整我们的游戏。
重要提示
你可以用远程设置做更多的事情。你可以了解更多关于远程配置以及如何使用它来处理非默认参数的信息,请参阅docs.unity3d.com/Manual/UnityAnalyticsRemoteSettingsComponent.html。
有了这些,我们游戏的所有实现细节都已经完成,但现在的游戏相当简陋。在下一章中,我们将探讨使用粒子系统、屏幕震动等特性来使我们的游戏更加精致的方法。
第十二章:改善游戏感觉
我们现在有一个基本的游戏,但仅仅是...基本。在本章中,你将学习游戏开发者的一些秘密,将他们游戏的基本原型转变为一个经过大量打磨、令人满意的游戏体验,这被称为改善项目的游戏感觉。
也被称为juiciness,或者让我们的游戏更有“汁感”,在游戏行业的某些角落,游戏感觉是一个总称,用于描述我们在游戏中做的所有事情,以使用户的交互体验更加愉悦。这是现在大多数移动游戏都会做的事情,缺乏这种交互性会让其他人认为我们的项目缺乏打磨。
在本章中,你将学习如何将一些这些功能集成到你的项目中的一些不同方法。我们将从学习如何使用动画开始。然后,我们将看到如何使用 Unity 的材料系统来为我们的对象添加视觉吸引力。接着,我们将通过使用后处理效果来提高我们游戏的整体视觉质量。最后,我们将使用游戏开发者工具箱中最强大的工具之一,粒子系统,来改善玩家在环境中移动时的反馈。
本章涵盖了多个主题。它从开始到结束提供了一个简单的逐步过程。以下是我们将涵盖的任务概述:
-
使用 LeanTween 进行动画
-
将缓动添加到暂停菜单
-
与材料一起工作
-
使用后处理效果
-
添加粒子效果
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小改动即可。如果你想下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。你还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。为了部署你的项目,你需要一个 Android 或 iOS 设备。
你可以在 GitHub 上找到本章中提供的代码文件github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter12。
使用 LeanTween 进行动画
目前,我们游戏的菜单完全静态。这虽然功能齐全,但并不能让玩家对我们的游戏产生兴趣。为了让游戏看起来更有活力,我们应该对菜单进行动画处理。能够使用 Unity 内置的动画系统是很好的,如果你想要同时修改许多不同的属性,它非常有用。如果你不需要精确控制,比如说,如果你只修改一个属性或者你想要通过代码纯动画化某个东西,你还可以使用一个插值库。
Tweening,简称“插值”,是动画和游戏开发中常用的一种技术,用于在指定的时间内,在两个状态或值之间创建平滑的过渡。它涉及将属性或一组属性从某个值逐渐过渡到另一个值。
如果给出了开始和结束点,库将负责中间的所有工作,以确保属性在指定的时间和速度内到达终点。通过使用 tween,开发者可以轻松地为他们的应用程序添加流畅且视觉上吸引人的动画,而无需手动处理插值计算和动画循环。插值库提供了方便的 API 和功能,以简单高效的方式创建和控制 tween。
我最喜欢的插值库之一是 Dented Pixel 的LeanTween,这是一个开源库,可以免费用于商业和非商业项目,针对移动设备进行了优化,并被许多游戏使用,包括《精灵宝可梦 GO》。在接下来的章节中,我们将首先安装和设置 LeanTween,然后看看我们如何使用它来动画化我们的标题屏幕 UI 菜单。
LeanTween 设置
LeanTween 允许我们以仅一行代码的方式对对象进行旋转、震动、打击、移动、淡入淡出和调整,以多种不同的方式。它还允许我们在动画的开始、中间和结束时触发自定义事件,使我们能够有效地完成任何想要创建动画的事情,一旦熟悉了它,这种方式将变得非常强大。
既然我们知道我们想要在我们的项目中添加 tween,那么让我们首先将 LeanTween 引擎添加到我们的项目中。执行以下步骤:
-
打开
LeanTween然后按Enter。 -
从那里,你将看到一个项目列表,第一个是LeanTween;选择它,你将被带到 LeanTween 的产品页面:

图 12.1:资产商店搜索
- 一旦进入项目页面,点击按钮,如果有的话,按钮上会写着添加到我的资产或在 Unity 中打开,这取决于你是否已经有了这个包。此时你可能需要登录你的 Unity 账户。一旦添加,从包管理器中,继续从资产页面点击导入按钮。

图 12.2:包管理器
重要提示
这里包管理器中的包可能看起来与你的不同,因为它们是我个人从资源商店购买的。
-
你应该在这里看到一个
Framework文件夹;然而,其他的可能对你也有用,所以请随意使用它们。 -
一旦你完成了选择,点击导入按钮:

图 12.3:导入 Unity 包对话框
- 我们不再需要包管理器了,所以请继续关闭它。你会注意到现在我们已经在我们的
Assets/LeanTween/Framework文件夹中选择了文件:

图 12.4:导入 LeanTween
有了这些,我们已经设置了 LeanTween。
重要提示
你可能还想考虑其他缓动库,例如 iTween 和 DOTween。有关更多信息以及它们的比较,请查看dotween.demigiant.com/#enginesComparison。
现在我们已经建立了一个缓动系统,让我们看看我们如何实际使用它!
创建一个简单的缓动
在过渡到游戏开发之前,缓动(或中间插值)在动画中变得流行,这个过程是给定起始值和结束值时,计算机将在两个状态之间生成中间帧,使得起始值看起来平滑地演变到第二个值。缓动是我们必须提供的信息,以便开始缓动过程。
现在我们已经将 LeanTween 包含到我们的项目中,我们可以在代码中使用它。为此,执行以下步骤:
-
从 Unity 编辑器中,通过转到项目窗口并双击MainMenu场景来打开MainMenu级别。
-
现在,转到
Scripts文件夹,通过双击它来打开MainMenuBehaviour。 -
我们将添加以下新函数,我们将使用它来使对象从屏幕左侧移动到中心:
/// <summary> /// Will move an object from the left side of the screen /// to the center /// </summary> /// <param name="obj">The UI element we would like to /// move</param> public void SlideMenuIn(GameObject obj) { obj.SetActive(true); var rt = obj.GetComponent<RectTransform>(); if (rt) { /* Set the object's position offscreen */ var pos = rt.position; pos.x = -Screen.width / 2; rt.position = pos; /* Move the object to the center of the screen (x of 0 is centered) */ LeanTween.moveX(rt, 0, 1.5f); } }
在我们使用 LeanTween 移动任何东西之前,我们首先将我们的对象(obj参数)的位置设置在屏幕之外,通过设置x位置。需要注意的是,在处理 Unity 中的 UI 元素时,默认情况下我们处理的是屏幕空间,正如你从第三章中回忆的,移动输入/触摸控制,这意味着我们是按像素移动的。
从这里,我们可以看到我们正在从 LeanTween 调用 moveX 函数。我们使用的版本接受三个参数,第一个是我们希望移动的 RectTransform 对象,第二个是我们希望移动到的 x 位置。根据我们设置的锚点和中心点,x 轴上的 0 位置实际上是居中的,所以我们传递 0。最后,我们有我们希望过渡发生的时间(以秒为单位)。
-
现在我们有了这个函数,让我们实际调用它。更改
MainMenuBehaviour脚本的Start函数,使其看起来如下:protected virtual void Start() { /* Initialize the showAds variable */ bool showAds = (PlayerPrefs.GetInt("Show Ads", 1) == 1); UnityAdController.showAds = showAds; /* Slide in the login menu if it exists */ if (facebookLogin != null) { SlideMenuIn(facebookLogin); } /* Unpause the game if needed */ Time.timeScale = 1; }
我们首先通过调用SlideMenuIn函数将 Facebook 登录菜单显示到屏幕上,该函数会将菜单移动到屏幕中央。LeanTween 默认使用游戏的Time.timeScale属性来缩放移动。当我们从暂停菜单离开游戏并返回主菜单时,游戏仍然处于暂停状态。这确保了在我们想要滑动这个菜单时,游戏不会被暂停。当我们开始构建暂停菜单时,我们将看到如何在游戏暂停时使我们的缓动效果仍然工作。
如果你现在玩游戏,你会注意到 Facebook 登录屏幕现在会从屏幕外移动到屏幕中央。
目前,对象以相当静态的方式移动。我们可以通过添加一些额外的功能,例如easeType,来给这个缓动效果增加活力。
-
将以下高亮代码添加到
SlideMenuIn函数中:/// <summary> /// Will move an object from the left side of the screen /// to the center /// </summary> /// <param name="obj">The UI element we would like to /// move</param> public void SlideMenuIn(GameObject obj) { obj.SetActive(true); var rt = obj.GetComponent<RectTransform>(); if (rt) { /* Set the object's position offscreen */ var pos = rt.position; pos.x = -Screen.width / 2; rt.position = pos; /* Move the object to the center of the screen (x of 0 is centered) */ LeanTween.moveX(rt, 0, 1.5f).setEase(LeanTweenType.easeInOutExpo); } }
这里发生的情况是,LeanTween.moveX函数返回一个LTDescr类型的对象,这实际上是创建的缓动的引用。我们可以通过在缓动上调用额外的函数来添加额外的参数。实际上,编写这个的另一种方式如下:
// Move the object to the center of the screen (x of 0 is
// centered)
var tween = LeanTween.moveX(rt, 0, 1.5f);
tween.setEase(LeanTweenType.easeInOutExpo);
然而,LeanTween 文档中的大多数示例都使用了前一种方法,即同时链式调用多个不同的事件发生。
重要提示
要查看除了easeType之外 LeanTween 中常用的其他方法,请参阅tedliou.com/archives/leantween-ui-animation/。
-
最后,我们将添加当前菜单在选中按钮跳转到另一个菜单时能够滑出屏幕的功能:
/// <summary> /// Will move an object to the right offscreen /// </summary> /// <param name="obj">The UI element we would like to /// move </param> public void SlideMenuOut(GameObject obj) { var rt = obj.GetComponent<RectTransform>(); if (rt) { var tween = LeanTween.moveX(rt, Screen.width / 2, 0.5f); tween.setEase(LeanTweenType.easeOutQuad); tween.setOnComplete(() => { obj.SetActive(false); }); } }
注意,这与之前编写的函数类似,但现在我们还在使用另一个名为setOnComplete的函数,它可以接受一个函数或表达式 lambda,它基本上是一个没有名称的函数,常用于obj中,我使用了 lambda。这将做的就是在对象离开屏幕后,它会自动关闭;但我们有做任何事的潜力。这可以非常强大,因为我们可以通过代码做任何我们通常能做的事情。
重要提示
关于 lambda 表达式的更多信息,请参阅docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions。
-
然后,我们需要更新
ShowMainMenu函数以实际显示菜单:public void ShowMainMenu() { if (facebookLogin != null && mainMenu != null) { SlideMenuIn(mainMenu); SlideMenuOut(facebookLogin); // No longer needed as menus will be animating //facebookLogin.SetActive(false); //mainMenu.SetActive(true); if (FB.IsLoggedIn) { /* Get information from Facebook profile */ FB.API("/me?fields=name", HttpMethod.GET, SetName); FB.API("/me/picture?width=256&height=256", HttpMethod.GET, SetProfilePic); } } } -
保存脚本并重新进入游戏:

图 12.5:菜单的滑动进出
如你所见,当在主菜单上时,菜单现在会飞入和飞出。
通过前面的示例,你应该能够看到将运动添加到我们的项目中是多么容易,以及它如何提高游戏的整体质量,使其更具互动性。
添加暂停菜单的补间动画
现在我们已经完成了主菜单,让我们继续为暂停菜单添加补间动画:
-
继续打开我们的
PauseScreenBehaviour脚本,以实现以下SetPauseMenu的实现:/// <summary> /// Will turn our pause menu on or off /// </summary> /// <param name="isPaused">is the game currently /// paused</param> public void SetPauseMenu(bool isPaused) { paused = isPaused; /* If the game is paused, timeScale is 0, otherwise 1 */ Time.timeScale = (paused) ? 0 : 1; // No longer needed //pauseMenu.SetActive(paused); if (paused) { SlideMenuIn(pauseMenu); } else { SlideMenuOut(pauseMenu); } onScreenControls.SetActive(!paused); /* Send custom gamePaused event */ if (paused && (AnalyticsService.Instance != null)) { AnalyticsService.Instance.CustomData( "game Paused"); AnalyticsService.Instance.Flush(); } }
注意,因为PauseMenuBehaviour继承自MainMenuBehaviour,它也可以调用SlideMenuIn和SlideMenuOut函数,只要它们被标记为protected或public。
现在,如果我们运行游戏,当我们点击暂停菜单时,看起来没有任何事情发生。这是因为——正如我之前提到的——补间动画是由Time.timeScale缩放的,我们刚刚改变了它。为了解决这个问题,我们可以使用另一个 LeanTween 函数,称为setIgnoreTimeScale,我们将它设置为true在我们在MainMenuBehaviour脚本中编写的两个函数中。回到MainMenuBehaviour脚本,并将以下高亮代码添加到SlideMenuIn方法中:
/// <summary>
/// Will move an object from the left side of the screen
/// to the center
/// </summary>
/// <param name="obj">The UI element we would like to
/// move</param>
public void SlideMenuIn(GameObject obj)
{
obj.SetActive(true);
var rt = obj.GetComponent<RectTransform>();
if (rt)
{
/* Set the object's position offscreen */
var pos = rt.position;
pos.x = -Screen.width / 2;
rt.position = pos;
/* Move the object to the center of the screen
(x of 0 is centered) */
var tween = LeanTween.moveX(rt, 0, 1.5f);
tween.setEase(LeanTweenType.easeInOutExpo);
tween.setIgnoreTimeScale(true);
}
}
-
将高亮代码添加到
SlideMenuOut方法中:/// <summary> /// Will move an object to the right offscreen /// </summary> /// <param name="obj">The UI element we would like to /// move </param> public void SlideMenuOut(GameObject obj) { var rt = obj.GetComponent<RectTransform>(); if (rt) { var tween = LeanTween.moveX(rt, Screen.width / 2, 0.5f); tween.setEase(LeanTweenType.easeOutQuad); tween.setIgnoreTimeScale(true); tween.setOnComplete(() => { obj.SetActive(false); }); } } -
保存这两个脚本,并进入编辑器尝试一下:

图 12.6:屏幕飞入
完美!我们现在已经实现了屏幕飞入的效果,就像我们想要的那样。
在前两个部分中,我们学习了如何创建补间事件以及如何将它们应用于不同的场景。在下一部分,我们将看到另一种方法,我们可以通过使用材质来改善我们项目的视觉效果。
与材质一起工作
之前,我们一直在我们的项目中使用默认材质。这对我们来说效果很好,但也许我们应该谈谈创建自定义材质来改善玩家视觉效果的想法。材质是在 Unity 中绘制 3D 对象的指令。它们由一个着色器和着色器使用的属性组成。着色器是一个脚本,它指导材质如何在对象上绘制东西。
着色器是一个很大的主题,已经有整本书被写出来,所以我们在这里不能深入探讨,但我们可以谈谈如何与 Unity 中包含的标准着色器一起工作。执行以下步骤:
- 首先,打开
Materials:

图 12.7:添加材质
- 打开我们刚刚创建的
Materials文件夹,然后一旦进入其中,通过在文件夹内右键单击并选择创建 | 材质来创建一个新的材质:

图 12.8:创建材质
-
将这个新材质命名为
Ball。在0.9和0.8。 -
现在,转到场景视图,并将球体材质拖放到我们的玩家对象上。

图 12.9:设置材质属性
阿尔贝多属性充当漫反射贴图设置对象的基础颜色,尽管您也可以应用纹理,使用图像文件来改变其外观。材质的金属度参数决定了表面有多像金属。表面越像金属,它反射的环境就越多。平滑度属性决定了表面的平滑程度;更高的平滑度会使光线均匀地反射,使反射更清晰。
重要提示
有关标准着色器和其参数的更多信息,请参阅docs.unity3d.com/Manual/StandardShaderMaterialParameters.html。
使用材质只是我们提高项目视觉质量的一种方式。实际上,我们可以通过使用后处理效果来大幅度修改项目的视觉效果,这是我们接下来将要探讨的。后处理效果是指在屏幕上显示之前,对相机将要绘制的图像(图像缓冲区)应用滤镜和其他效果的流程。
使用后处理效果
我们可以通过使用后处理效果(之前称为图像效果)以最小的努力提高游戏视觉质量。后处理是在屏幕上显示之前,对相机将要绘制的图像(图像缓冲区)应用滤镜和其他效果的流程。
Unity 在其免费提供的后处理堆栈中包含了许多效果。Unity 的后处理堆栈是一组可以应用于游戏或应用程序中渲染图像的视觉效果,以增强整体视觉质量。这些效果可能包括色彩分级、景深、运动模糊、环境遮挡等。通过使用后处理堆栈,开发者可以轻松地将这些效果添加到他们的游戏中,而无需从头开始创建。
术语“堆叠”用于强调这些效果旨在一起使用,以分层的方式实现所需的视觉风格或美学。通过提供作为堆叠的预构建效果集合,Unity 简化了开发者实现高级视觉效果的流程,使他们能够更多地关注项目创意方面。因此,让我们按照以下步骤继续添加:
- 通过前往窗口 | 包管理器再次打开包管理器。从那里,点击左上角的包下拉菜单并将其设置为Unity 注册表。之后,向下滚动直到您看到后处理选项并选择它:

图 12.10:后处理
-
选择后,点击安装按钮并等待其完成。
-
切换到
后处理。然后,将鼠标移至后处理层选择项上并点击,将脚本添加到您的项目中。
后处理层组件处理后处理体积的混合以及后处理应基于什么。
-
在后处理层组件下,将层更改为所有层。这将使得场景中的所有内容在体积混合方面都会被使用。
-
接下来,我们需要将后处理体积组件添加到我们的主摄像机游戏对象中。通过点击添加组件按钮,然后选择后处理 体积选项来完成此操作。

图 12.11:添加体积
注意,此组件需要一个配置文件。我们可以继续添加该配置文件。
- 我们可以通过在
Assets文件夹上右键单击,选择MobilePostProcessing来创建一个新的后处理配置文件:

图像 12.12:添加后处理配置文件
- 返回到主摄像机对象,并将此对象附加到后处理体积组件的配置文件属性。之后,转到后处理体积组件,并勾选全局属性框。
这将使得我们创建的体积无论玩家的摄像机在世界中的位置如何,都会始终显示在玩家的屏幕上。
- 由于
后处理配置文件是一个单独的文件,我们可以在玩游戏时对其进行更改,而不用担心丢失更改。考虑到这一点,开始游戏,一旦游戏开始,就暂停它。
现在,有大量可能的效果可以添加,以修改游戏的外观。
重要提示
注意,对于我们在配置文件中添加的每个元素,我们尝试运行游戏时设备的帧率将会降低。请使用这些选项测试您的设备,并注意其效果。
- 接下来,在
0.45下:

图 12.13:添加暗角
注意现在似乎游戏周围出现了一个变黑的边缘或边框。
小贴士
如果 UI 菜单消失,从场景视图切换回游戏视图似乎可以解决这个问题。
- 接下来,启用
0.35,通过点击该部分的右上角来使其变得更暗:

图 12.14:启用平滑度
暗角是指与中心相比,图像边缘变暗和/或去饱和的术语。当我想要让玩家专注于屏幕中心时,我喜欢使用这个效果。
- 再次点击添加效果...按钮,这次选择Unity | 颗粒。
检查并设置0.15,你会注意到屏幕已经变得模糊。虽然如果设置得太大不是什么好主意,但请注意,减小0.3并取消选中彩色将有助于改善事物的外观:

图 12.15:颗粒
如果你曾经去过仍然使用胶片的电影院,你可能会注意到胶片上在电影过程中可见的小颗粒。Unity 中的颗粒效果模拟了这种胶片颗粒,使效果在电影播放次数越多时越明显。这通常用于恐怖游戏,以模糊玩家的视线。
- 另一个要添加的属性是
10。从那里,将0.6设置为帮助使事物更亮:

图 12.16:Bloom 效果
Bloom效果试图模仿现实世界中相机的成像伪影,在光线较亮区域的物体沿着边缘发光,从而压倒相机。
- 最后,停止游戏,然后回到后处理层组件,在抗锯齿下,将模式更改为快速近似抗锯齿(FXAA),然后检查快速****模式:

图 12.17:抗锯齿
锯齿效应是屏幕上线条看起来参差不齐的效果。如果我们尝试在设备上玩游戏,而该设备的屏幕分辨率不足以正确显示,就会发生这种情况。
抗锯齿尝试通过将这些线条附近的颜色组合起来以减少其突出度,但这会使图像看起来更模糊。
重要提示
有关 Unity 中后处理的更多信息,请参阅docs.unity3d.com/Packages/com.unity.postprocessing@3.2/manual/index.html。
有许多其他属性需要考虑和适应,以使你的项目看起来正是你想要的。探索它们,找到适合你想要实现愿景的方法!
游戏本身目前可以运行,但可以做一些润色。我们可以增加游戏润色的一种方法就是使用粒子系统,这是我们接下来要看的。
添加粒子效果
通常用于自然或有机效果,如火焰、烟雾和火花,粒子系统创建的对象设计为尽可能低廉的成本,称为粒子。因此,我们可以一次生成许多粒子,而性能成本最小。最容易创建的粒子系统类型之一是追踪玩家的轨迹,所以现在让我们按照以下步骤添加一个:
- 在层次结构窗口中选择Player,然后右键单击并选择效果|粒子系统。
这样会使这个系统成为玩家的子系统,这对我们即将要做的事情是有好处的。
-
在
0和模拟空间到世界。然后,将起始颜色更改为一个突出的颜色,例如紫色。 -
打开
0(它将自动更改为0.0001)。
这是一个正确的步骤。紫色粒子现在正跟随玩家,如截图所示:

图 12.18:粒子轨迹
然而,我们仍然可以做很多事情来改进这一点。我们不仅可以改变颜色,还可以让它随机在两种颜色之间交替。
-
要这样做,请转到开始颜色的右侧,你会看到一个向下的小箭头。点击它,然后选择在两种颜色之间随机选择。然后,将颜色更改为两种紫色颜色之一,以增加一些随机性。
-
然后,在
0.5和1.2旁边。 -
使用这个设置,将
0设置为0.2。 -
然后,打开
100:

图 12.19:粒子设置
- 保存游戏并播放:

图 12.20:最终粒子轨迹
小贴士
如果你想要探索更多关于如何润色项目的细节,你可以查看我的另一本 Unity 书籍,Unity 5.x 游戏开发蓝图,也由 Packt 出版,它也深入探讨了游戏润色。
如你所见,粒子系统在我们的 PC 和移动设备上看起来都很棒。
当然,还有许多其他领域可以通过使用粒子系统来改进。也许每次玩家碰到墙壁时,我们可以显示一些火花;当我们滑动时,我们可以播放另一个效果;当玩家暂停游戏时,我们可以在屏幕上显示一些东西落下。可能性是无限的!
摘要
我们现在通过仅做几件简单的事情就大幅提高了我们的游戏。我们首先使用 LeanTween 的 tweens 通过几行代码动画化我们的菜单,并看到几行代码如何以多种方式提高我们 UI 的视觉质量。接下来,我们看到了如何创建材质来提高我们的球体视觉质量,然后使用一些后处理效果来润色屏幕内容。最后,我们讨论了如何使用粒子效果为玩家创建一条漂亮的轨迹。
通过这些概念,你现在拥有了大幅提高你的游戏项目感觉的技能,让玩家真正享受与你的游戏互动。
到目前为止,我们的游戏终于准备好进入主流市场。在下一章中,我们将探讨如何构建我们的游戏,以便将我们的游戏发布到苹果 App Store 和谷歌 Play。
第十三章:构建我们游戏的发布副本
构建我们游戏的发布副本是提交我们的游戏到应用商店过程中的关键步骤。这一步骤涉及创建一个稳定且准备向公众发布的游戏版本。
在本章中,我们将指导你完成为 iOS 和 Android 设备构建游戏发布副本所需的步骤。我们将涵盖一些必须按顺序执行的额外步骤,以确保我们的项目能够正常工作。我们还将提供一些技巧和最佳实践,以确保你的发布副本质量最高,且无错误和其他问题。因此,让我们开始创建游戏发布副本。本章将分为多个主题。它将包含从开始到结束的简单分步过程。以下是我们的任务大纲:
- 为应用商店生成发布版本
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小改动即可工作。如果你想要下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档 unity3d.com/get-unity/download/archive。你还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求 docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。为了部署你的项目,你需要一个 Android 或 iOS 设备。
你可以在 GitHub 上找到本章中存在的代码文件,链接为 github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter13%20and%2014。
为应用商店生成发布版本
我们在第二章,“Android 和 iOS 开发项目设置”中之前导出了我们游戏的副本,但发布游戏到应用商店之前,我们还需要执行一些额外的步骤:
-
第一步将是确认你目前是否已经准备好将你的项目部署到所选的移动平台。你可以通过导航到文件 | 构建设置来进入构建设置菜单进行检查。
-
从那里,你应该能看到根据你希望构建的平台,在Android或iOS选择项右侧的 Unity 标志。如果它不在正确的平台上,请选择你想要构建的平台,然后点击切换平台按钮,等待它完成项目资源的重新导入:

图 13.1 – 构建设置菜单
-
在确认我们是为 Android 或 iOS 构建后,通过点击菜单中的 Player Settings... 按钮,或通过转到 编辑 | 项目设置 | Player 来打开 Player 设置菜单。
-
如果你还没有这样做,请分别设置
JohnPDoran和EndlessRoller。 -
然后,你将看到来自
Assets文件夹的Hi-ResIcon图像,然后将其拖放到 默认图标 槽中。这将导致 Android 设置中的 图标 部分自动调整图像以适应你正在针对的任何设备:

图 13.2 – 设置默认图标图像
当然,你也可以使用你自己的图像,并且如果你愿意,可以使用透明度。
-
在 分辨率和展示 部分中,你可以根据需要启用或禁用不同的旋转和宽高比。我们调整了游戏以适应这些设置,但当你自己在项目上工作或希望限制用户体验时,了解这一点可能很有用。
-
如果你拥有 Unity Personal,可以使用 Splash Image 选项显示你的自有标志,除了 Unity 的标志。如果你拥有 Unity Pro 许可证,你可以通过取消选择 显示启动屏幕 选项 来完全禁用它。
-
在
com.CompanyName.GameName下进行确认。 -
接下来,打开 发布设置。这是我们将输入有关游戏发行者信息的地方(在这种情况下,我假设是你)。无论何时为 Android 构建游戏,你都需要一个 密钥库,它允许你签署游戏,批准构建过程。点击 密钥库管理器 按钮。从那里,你将被带到菜单。
-
从菜单中,点击 密钥库… 下拉菜单,然后选择 创建新 | 任何地方…,并选择此文件的位置。
-
请记住这个位置,因为你将来将使用它来创建游戏的新版本。
-
然后,你需要在 密码 字段中设置一个值,你将需要知道,因为你将反复使用它。之后,在 确认密码 文本框中,你应该输入之前输入的内容。
-
从那里,在 新密钥值 部分中,你需要添加之前相同的信息——密码及确认,然后是你的名字和其他信息。你可以在下面的屏幕截图中看到我填写的内容。完成后,点击 添加 密钥 按钮:

图 13.3 – 创建密钥库
- 你将看到一个弹出窗口询问你是否想将新的密钥库设置为你的 项目密钥库 和 项目密钥 值。点击 是。

图 13.4 – 确认窗口
你应该会看到这个屏幕:

图 13.5 – 分配的密钥库
到此为止,为 iOS 工作的人员应该已经完成,但对于希望针对 Google Play 的人来说,还有一些额外的任务需要完成。
Google Play 需要使用更新的版本其计费库,因此我们需要更新 Unity 的 In App Purchase (IAP)包。
-
从 Unity 编辑器,转到
Assets文件夹,右键单击,选择Packages文件夹,并在您选择的文本编辑器中打开manifest.json文件。接下来,找到以下行:"com.unity.purchasing": "4.1.5", -
更新为
"``com.unity.purchasing": "4.4.1",. -
保存文件并返回到 Unity 编辑器,它应该会更新包。然而,这也会引入一个到我们之前代码的漏洞,导致广告在我们的设备上无法正确显示。因此,考虑到这一点,打开
UnityAdController脚本并向类中添加以下代码:public bool rewardCalled = false; // To account for a bug in Unity Advertisements 4.0.1 // with Google // Play we have to add a way for UnityAdsShowComplete // to be // called by ourselves as well if it isn't called by // Unity IEnumerator RewardRoutine(string placementId) { rewardCalled = false; yield return new WaitForSecondsRealtime(0.25f); while (Advertisement.isShowing) { yield return null; } Debug.Log("Done"); // If reward wasn't called yet, call it if(!rewardCalled) { OnUnityAdsShowComplete(placementId, UnityAdsShowCompletionState.COMPLETED); } } -
然后,将
ShowAd函数更新为以下内容:/// <summary> /// Will load and display an ad on the screen /// </summary> public void ShowAd() { // Add fix for Unity Ads bug StartCoroutine(RewardRoutine(GetAdID())); // Display it after it is loaded Advertisement.Show(GetAdID(), instance); } -
我们现在还必须提前加载我们的广告,因此更新
Start函数为以下内容:/// <summary> /// Unity Ads must be initialized, or else ads will /// not work properly /// </summary> private void Start() { /* No need to initialize if it already is done */ if (!Advertisement.isInitialized || instance == null) { instance = this; // Use the functions provided by this to allow // custom Advertisement.Initialize(gameId, testMode); // Load an Ad to play Advertisement.Load(GetAdID()); } } -
按照同样的思路,
OnUnityAdsShowComplete现在需要只发生一次,因此我们添加了一个检查以查看奖励是否已经发放。广告完成后,我们加载一个新的广告:/// <summary> /// This callback method handles logic for the ad /// finishing. /// </summary> /// <param name="placementId">The identifier for /// the Ad Unit showing the content</param> /// <param name="showCompletionState">Indicates /// the final state of the ad (whether the ad was /// skipped or completed).</param> public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState showCompletionState) { if(!rewardCalled) { if (obstacle != null && showCompletionState == UnityAdsShowCompletionState.COMPLETED) { obstacle.Continue(); } /* Unpause game when ad is over */ PauseScreenBehaviour.paused = false; Time.timeScale = 1f; rewardCalled = true; // Load an Ad to play Advertisement.Load(GetAdID()); } }
Google Play 还规定,广告不能在关卡开始时播放,因此我们需要调整我们的脚本以适应这一点。打开 PauseScreenBehaviour.cs 文件并调整 Start 函数:
protected override void Start()
{
/* Initialize Ads if needed */
base.Start();
//if (!UnityAdController.showAds)
//{
// /* If not showing ads, just start the game
// */
// SetPauseMenu(false);
//}
// Can no longer show ads at the Start of the game
SetPauseMenu(false);
}
-
接着,转到
MainMenuBehaviour并更新LoadLevel脚本:/// <summary> /// Will load a new scene upon being called /// </summary> /// <param name="levelName">The name of the level we /// want to go to</param> public void LoadLevel(string levelName) { /* Can no longer show an ad upon starting gameplay */ try { if (UnityAdController.showAds && levelName != "Gameplay") { /* Show an ad */ UnityAdController.instance.ShowAd(); } else { Time.timeScale = 1f; } } catch { } SceneManager.LoadScene(levelName); } -
保存我们已工作的所有脚本并返回到 Unity 编辑器。
在编辑器中,返回到 Gameplay 级别并选择 Resume 按钮。从那里,转到 On Click 事件并确保 Show Pause 按钮有一个 Set Active 为 true 的事件。这样,当我们点击 Resume 按钮时,我们就能再次暂停游戏:

图 13.6 – 打开暂停菜单
- 我们还希望确保 Unity 游戏服务始终初始化。一种方法是通过转到 服务 | 内联购买 | IAP 目录 并检查 自动初始化 Unity 游戏服务 属性。否则,我们可能会遇到我们之前添加的无代码 IAP 的错误:

图 13.7 – 自动初始化 Unity 游戏服务
Google Play 商店现在还要求新应用必须是 Android App Bundle (AAB)而不是 Android 应用程序包 (APK),因此我们需要将应用程序配置为 AAB。
注意
关于为什么进行此更改的更多信息,请查看 [android-developers.googleblog.com/2021/06/the-future-of-android-app-bundles-is.html.](https://android-developers.googleblog.com/2021/06/the-future-of-android-app-bundles-is.html
)
-
在 发布设置 下,启用 分割 应用程序二进制。
-
我还将
API 级别 31设置为当前 Google Play 要求应用程序的目标,尽管未来可能是一个更大的数字。这可能会导致在构建时 Unity 要求您下载 Android 软件开发工具包(SDK)的更新。 -
此外,Google Play 要求我们支持 ARM64,因此为了实现这一点,在 配置 下,将 脚本后端 改为 IL2CPP 以启用选项,之后检查 ARM64 选项。
-
接下来,转到 文件 | 构建设置 并启用 构建应用包(Google Play)。同时,如果已勾选,请确保取消勾选 开发构建;目前 Google Play 不支持开发构建:

图 13.8 – 构建应用包
- 最后,我们还需要从
AndroidManifest文件中禁用调试。从 Unity 编辑器中,转到Plugins\Android文件夹,并打开AndroidManifest.xml文件。从那里,搜索android:debuggable="true"行,将其更改为"false"并保存文件。
应用程序清单是每个 Android 项目都必须拥有的东西,它告诉 Android 应用程序的所有不同组件以及应用程序需要拥有的所有权限,以便项目能够工作。出于安全原因,在应用程序可以发布到 Google Play 之前,您需要禁用调试。
-
返回到 构建设置 菜单,然后点击 构建 按钮,并为您的项目命名。
-
如果一切顺利,你应该会看到文件以
.aab文件的形式创建,然后可以将其上传到 Google Play 商店!
摘要
看到所有辛勤工作汇聚成成品,准备好与世界分享,这令人兴奋。构建游戏发布副本和更新构建设置的过程是使您的游戏准备好分发的重要步骤。
然而,任何游戏开发项目的最终目标是将游戏发布给公众,让尽可能多的人玩到它。这就是将您的游戏提交到应用商店的过程发挥作用的地方。在下一章中,我们将深入探讨如何成功将您的游戏提交到 Google Play 商店和 iOS 应用商店的细节。
第十四章:将游戏提交到应用商店
在本书的整个过程中,我们已经讨论了为移动设备构建游戏的多方面内容。我们游戏开发之旅的最后一步实际上是发布游戏,让人们真正地玩它。所有那些长时间的努力工作现在都汇聚成了一种大众都能享受的东西。
在进行这一操作时,有许多需要注意的事项,这正是我们接下来将要讨论的内容。
在本章中,我们将介绍将您的游戏提交到 Google Play Store 或 iOS App Store 的过程,并提供一些技巧和建议,以帮助使这个过程更加顺利。到本章结束时,您将确切了解如何为两家商店创建开发者账户,以及如何将您的游戏提交到相应的商店。
本章将分为多个主题。它将包含从开始到结束的简单分步过程。以下是我们的任务大纲:
-
将您的游戏发布到 Google Play Store
-
将您的游戏发布到 Apple iOS App Store
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小改动即可。如果您想下载本书中使用的确切版本,并且有新版本发布,您可以在 Unity 的下载存档中找到它,地址为unity3d.com/get-unity/download/archive。您还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求,地址为docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署您的项目,您需要一个 Android 或 iOS 设备。
您可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter13%20and%2014。
将您的游戏发布到 Google Play Store
现在您的游戏已经制作完成,您需要将其实际发布到 Google 的 Play Store。要将游戏发布到 Google Play Store,您需要支付一次性 25 美元的费用。这可能或可能不是一大笔钱,但它比 iOS App Store 便宜得多,并且是一次性费用,因此对于那些预算较为紧张的人来说,您可能希望先在 Google 上尝试并赚取一些利润,然后再转向 Apple 的商店。我们首先将查看 Google Play Console,然后再填写提交游戏所需的所有详细信息。我们还将讨论如何将我们的游戏标记为测试版,以便在最终提交之前从他人那里获取反馈。
设置 Google Play Console
第一步是获取访问 Google Play 控制台的权利。这允许您在 Google Play 上发布 Android 应用程序,如果您愿意,还可以发布 Google Play 游戏服务。请执行以下步骤:
- 打开您的网络浏览器并访问
play.google.com/console/about/。这是Google Play 控制台页面,允许您将应用程序添加到 Google Play 商店。

图 14.1 –– Google Play 控制台页面
- 如果您尚未登录到您的 Google 账户,您需要登录;否则,您需要作为开发者注册:

14.2 – 创建 Play 控制台开发者账户
- 您需要选择一个账户类型,然后需要填写与您的开发者账户相关的信息,以及您将要发布的应用程序相关的信息。

图 14.3 – 添加有关您的应用程序的信息
- 接下来将出现一个页面,您需要接受条款并点击创建账户并支付。之后,您需要输入您的信用卡或借记卡信息以完成购买。

图 14.4 – 完成开发者注册费用的支付
- 如果一切顺利,您将被带到 Google Play 控制台。
一旦您有了账户,现在您可以开始将游戏发布到 Google Play 商店的过程。要开始这个过程,我们需要将一个项目添加到我们的账户中,这就是我们接下来要做的。
在 Google Play 上发布应用程序
将应用程序发布到 Google Play 的过程涉及填写多个不同的字段,包括有关您游戏的信息,以及截图的艺术资源。为此,请执行以下步骤:
- 从所有应用程序页面,点击创建****应用程序按钮。

图 14.5 – 所有应用程序页面
- 您将被带到一页,您需要选择您游戏的应用名称和默认语言。然后您需要选择它是应用程序还是游戏。我们将选择游戏。

图 14.6 – 创建应用程序菜单
- 滚动到页面底部,您将看到三个声明,您需要阅读并检查它们是否相关,然后点击创建****应用程序按钮:

图 14.7 – 应用程序仪表板
仪表板页面显示了为了使您的应用程序上线所需的所有最新步骤的详细信息。第一部分包括构建游戏的测试版本,以便在最终发布之前其他人可以给您提供反馈。
滚动到立即开始测试部分。从那里,选择选择****测试人员选项。

图 14.8 – 现在开始测试
- 这将打开内部测试页面,您也可以通过仪表板的发布部分进入测试 | 内部测试来访问。从那里向下滚动,在测试者下,您将看到一个选项,通过点击创建电子邮件 列表按钮为您游戏创建测试者电子邮件列表。

图 14.9 – 内部测试菜单
这将弹出一个窗口,您可以在其中填写一个列表名称,然后通过手动输入值(以逗号分隔)或使用我们之前讨论的 CSV 文件来添加您想要测试游戏的人的电子邮件地址。

图 14.10 – 创建电子邮件列表
-
完成后,点击保存更改按钮,您将需要确认保存的更改。请注意,此电子邮件列表将在您的开发者账户中的所有应用中使用。点击创建,然后应该返回到上一个部分。
-
从那里,对于反馈 URL 或电子邮件地址,填写您的电子邮件地址或网站。之后,点击保存更改按钮。请注意,测试者如何加入您的测试部分当前复制链接按钮为灰色。这是因为我们目前尚未发布应用;然而,一旦我们创建发布,这将是我们的下一步行动。
-
在右上角,将
.aab文件拖入拖放应用包以上传区域。 -
在下面,您将能够提供发布名称和关于这个特定版本游戏的发布说明。完成这些细节后,点击保存为草稿按钮,然后点击下一步。

图 14.11 – 填写发布详情
您将看到一些信息和关于您项目的任何错误、警告和消息。如有需要,请阅读它们,假设没有错误,您应该能够点击保存按钮,允许其他人尝试您的项目!

图 14.12 – 审查发布
- 通过点击仪表板按钮返回仪表板。返回主页面后,您将看到下一个部分是设置 您的应用:

图 14.13 – 设置您的应用
我们需要逐个完成这些部分,以便设置商店列表。
- 首先,点击设置隐私政策选项,并提供您想要使用的隐私政策的链接。过去,人们使用 Unity 自己为使用 Unity Analytics 和广告的用户推荐的链接(
unity3d.com/legal/privacy-policy),但谷歌最近更改了其政策,现在您的组织或应用程序的名称需要包含在政策中。
最安全的选项是雇佣一名律师为您的应用程序起草隐私政策,但如果您希望自行创建隐私政策,本网站可以帮助您创建相关材料:letsmakeagame.net/game-privacy-policy-generator/。在我的情况下,我检查了包含在“第三方服务”部分隐私政策的链接中的Facebook和Unity选项,然后填写了所需的数据。完成之后,点击生成按钮,您将获得用于在您的网页上托管的自定义隐私政策所需的模板数据。
-
接下来,转到应用程序访问,然后选择所有功能均无需特殊访问即可使用,并按保存。
-
在广告下,选择是,我的应用程序包含广告,并选择保存。
-
前往内容评级,选择开始新问卷,并根据您的游戏回答问题。

图 14.14 – 内容评级
- 接下来,您将回答有关您的目标受众和内容的问题。请注意,如果您表示您的受众包含 13 岁及以下的儿童,您将需要回答几个其他问题。之前我们说游戏不是,所以我选择了其他年龄。
在下一部分,您将确认您的游戏不是新闻应用程序或 COVID-19 接触者追踪或状态应用程序。
- 接下来,在数据安全部分,您需要逐页浏览并回答给出的问题。
对于 Unity 游戏服务不同区域的数据安全要求,您可以使用以下链接:docs.unity.com/ugs-overview/GoogleDataSafety.html。在我们的项目中,我们使用应用内购买、广告变现、远程配置和分析。我们需要逐一检查这三个选项,并确保我们为每个选项选择正确的选项。请注意,尽管某个产品以一种方式共享某些内容,但这并不意味着另一个产品将以另一种方式共享。
-
在数据收集和安全部分,我回答了是、是、我的应用程序不允许用户创建账户和否。
-
在数据类型下,选择近似 位置选项。
-
在个人信息下,选择用户 ID。分析可以跟踪更多,但我们不会启用它们。
-
在财务信息下,选择购买历史。
-
最后,在设备或其他 ID下,选择设备或其他 ID。然后,点击下一步按钮。
-
对于这些值中的每一个,我们需要点击箭头来回答有关它们的问题。对于用户 ID,标记属性为已收集和已共享,它不会被临时处理,需要数据收集,并且收集和共享的目的用于应用功能。然后,点击保存按钮。
-
对于购买历史,请确保它是已收集和已共享,它不会被临时处理,需要数据收集,并且收集和共享的目的用于应用功能。然后,点击保存按钮。
-
对于大致位置,请确保它是已收集和已共享,它不会被临时处理,需要数据收集,并且收集和共享的目的用于应用功能。然后点击保存按钮。
-
对于设备或其他 ID,请确保它是已收集,选择是,此收集的数据会被临时处理和需要数据收集(用户无法关闭此数据收集)选项,并确认数据是为了应用功能而收集的。之后,点击保存按钮。然后,您应该能够点击下一步按钮。

图 14.15 – 数据安全
-
如果一切看起来都很好,请继续点击保存按钮,然后您就可以返回仪表板。
-
接下来,选择政府应用部分,回答否,然后点击保存。
-
然后,在管理应用的组织和展示方式部分,点击选择应用类别并提供联系 详细信息选项。
-
从这里,您可以选择游戏是应用还是游戏;
Arcade)。然后,您可以使用标签来帮助描述您游戏的内容。这是帮助人们定义您的应用的一种方式,也是帮助人们通过某些关键词发现项目的一种方式,所以请考虑使您的游戏独特的事物,并确保将它们包含在这里。在每个标签下,都有一个您可以点击以获取有关它们的信息的问号,这可以帮助您在不确定它们是什么时。最后,我选择了Arcade, Casual, Hyper-casual, Runner。当您完成时,点击应用。

图 14.16 – 管理标签
-
然后,填写您的商店列表联系详细信息。请注意,这些信息将显示在 Google Play 上,所以我不建议添加您的个人电话号码。完成后,点击保存按钮并返回仪表板仪表板。
-
最后,点击设置您的商店列表选项。向下滚动,您将被带到需要填写有关您的游戏信息的部分,从简短描述开始,然后是更详细的完整描述:

图 14.17 – 创建主要商店列表
- 然后,您需要提供用于显示游戏的图形资产。您至少需要两张截图,以及一些额外的图标和图形:

图 14.18 – 添加手机截图
- 您还需要为图标和其他特色图形添加更多图片。带有星号(****)的是必需的。您可以在本书的示例代码中找到一些预先创建的,但我建议您在将此游戏定制到您喜欢的样子后创建自己的:

图 14.19 – 完成主要商店列表
-
当您完成时,点击 Save 按钮,然后返回仪表板。
-
到目前为止,您已经通过了 Google Play 开发流程的大部分障碍。现在您可以选择进入 发布您的应用 部分,跳转到 在 Google Play 上发布您的应用 部分,创建一个新的构建版本并将发布推向世界,或者首先通过项目的 alpha 和 beta 阶段来获取宝贵的反馈,并迭代您的项目以留下更好的第一印象。无论如何,过程将与您之前所做的一样,除了选择您希望项目可用的国家和地区以及您希望为游戏收费的内容。
完成这些步骤后,我们现在可以开始发布到苹果的 iOS App Store 的过程,这是我们接下来要做的。
将您的游戏发布到苹果 iOS App Store
就像 Google Play 商店一样,将您的游戏发布到 App Store 需要额外付费。与 Google Play 商店不同,费用是每年 99 美元加税。然而,很多人认为在 iOS 设备上拥有他们的标题是值得额外费用的。在本节中,我们将介绍将我们的游戏发布到 App Store 的过程。我们将首先设置您的苹果开发者账户并创建一个配置文件。之后,我们将利用 iTunes Connect 工具将应用添加到商店,并使用 Xcode 创建一个存档,这样我们就可以将项目上传到 App Store,以便最终进行提交审查。
苹果开发者设置和创建配置文件
为了将应用部署到 iOS 设备上,您需要使用 Mac 计算机,但在我们继续到 iTunes 商店之前,我们首先需要提前弄清楚所有的证书和权限。
实施以下步骤:
- 在 Mac 计算机上,访问 developer.apple.com:

图 14.20 – 苹果开发者网站
- 从那里,点击屏幕右上角的 Account 按钮,填写您的 Apple ID 和密码,然后按 Enter 登录:

图 14.21 – 登录苹果开发者
如果你设置了双因素身份验证方法,你可能需要验证你确实是你自己。
- 从那里,点击账户。现在,在这个时候,你需要支付 99 美元的年度费用。这个过程应该是相当直接的,一旦完成,你将到达一个类似于以下页面的页面:

图 14.22 – 苹果开发者账户页面
- 在证书、标识符和配置文件部分,选择证书部分以开始创建应用的流程。如果你刚刚支付了 99 美元的费用,你可能会看到一个错误,表明所选团队没有符合 此功能 的会员资格。
别担心——这仅仅意味着支付尚未在苹果端处理。大约 30 分钟到 1 小时后再次尝试,屏幕应该可以正常工作。
- 我们需要设置一些证书,以便我们可以导出到 iOS 应用商店。从证书页面,点击证书右侧的+按钮:

图 14.23 – 证书页面
- 当页面询问我们需要哪种证书时,在软件部分下选择苹果分发选项,然后点击继续:

图 14.24 – 选择证书类型
你可能会注意到这里还有一种类型,也声称有 iOS 分发类型。你可能想知道为什么我们不选择那个。截至 Xcode 11,这是苹果希望开发者用于旨在进入应用商店的设备的证书的新类型。
在撰写本文时,如果你只计划为 iOS 开发,你可以选择iOS 分发。然而,由于苹果未来可能会停止提供它,我选择苹果分发。
-
接下来,我们需要在我们的 Mac 上创建一个
Applications\Utilities文件夹,并打开钥匙串访问程序。 -
从那里,转到钥匙串访问 | 证书助理 | 从证书颁发机构请求证书...:

图 14.25 – 请求证书
- 一旦进入,请在用户电子邮件地址属性中填写您的电子邮件地址。然后,对于通用名称,输入一个名称,并保留CA 电子邮件地址字段为空。然后,对于请求是选项,选择保存到磁盘:

图 14.26 – 填写我们的证书信息
- 然后,点击继续按钮并选择保存证书的位置。我个人使用了我的桌面,但你可以使用任何你喜欢的位置,只要你能记住稍后在哪里:

图 14.27 – 保存证书
-
之后,屏幕将显示请求已在磁盘上创建。请继续点击完成,然后返回您的网页浏览器。
-
滚动到页面底部然后点击继续按钮。从那里,您将被带到生成您的证书页面。点击选择文件按钮然后选择我们刚刚创建的文件。然后,点击继续按钮:

图 14.28 – 上传证书
- 然后,您将被带到显示您的证书已准备好的屏幕。请继续点击下载按钮,并将证书保存到您的磁盘。如果您使用 Safari,可能会被询问是否允许下载;请点击允许:

图 14.29 – 下载您的证书
- 之后,双击
.cer文件以将数据访问权限赋予密钥链。在密钥链下,将值更改为login。您将被询问是否要添加证书;请继续点击添加:

图 14.30 – 向密钥链添加证书
- 下一步是创建一个 App ID。为此,转到左侧边栏并点击
Endless Roller项目,我们可以通过点击编辑按钮来自定义它。然而,如果您之前没有这样做并且拥有不同的捆绑 ID,让我们接下来详细了解一下:

图 14.31 – 标识符列表
-
我们可以通过点击屏幕右上角的+按钮来创建一个新的 ID。
-
从那里,选择
Endless Roller。然后在com.JohnPDoran.EndlessRoller下。在应用服务下,您可以选择想要使用的选项,但在这个案例中,我们不需要担心这些,所以我们可以直接滚动到页面底部然后点击继续按钮。
在这种情况下,我之前提到的捆绑包将不会工作,因为已经存在具有此特定捆绑包的 ID,并且您需要具有唯一的 ID。考虑到这一点,我只是将原始 App ID 重命名为 Endless Roller,然后完成了这一部分。
我们在这里需要设置的最后一个方面是配置文件。苹果公司将配置文件定义为“一组数字实体,将开发者和设备唯一地绑定到授权的 iPhone 开发团队,并允许设备用于测试。”这意味着它是设备与开发者账户之间的链接,该账户使项目得以实现。
重要提示
想了解更多关于配置文件的信息,请查看medium.com/@alexi.schreier/wtf-is-a-provisioning-profile-on-ios-a9b65d79221f。
- 要这样做,点击菜单左侧的配置文件部分。从那里,点击配置文件右侧的蓝色+图标。在分发下选择App Store,然后点击继续:

图 14.32 – 为 App Store 创建配置文件
- 从那里,您需要选择您的 App ID。
Endless Roller可能已经选中;否则,在下拉列表中搜索它并选择它,然后点击继续。

图 14.33 – 选择 App ID
-
然后,选择您的证书并点击继续。
-
然后,选择您想要使用的证书并点击
Endless Roller– 然后点击生成:

图 14.34 – 生成配置文件
- 按钮将变为下载。继续下载配置文件,并妥善保管,因为我们稍后会用到它:

图 14.35 – 配置文件已准备好
这样,我们的配置文件就准备好了。
将应用程序添加到 App Store Connect
现在我们有了配置文件,我们实际上可以将我们的应用程序放到商店。为此,执行以下步骤:
- 在您的网络浏览器中,访问
appstoreconnect.apple.com并点击我的应用程序按钮:

图 14.36 – App Store Connect
如果您打算销售应用程序,您还必须前往协议、税务和银行部分并输入您的银行信息。
- 从应用程序页面,前往左上角,点击+图标,通过选择新应用程序来将新应用程序添加到我们的配置文件中:

图 14.37 – 应用程序页面
- 在此菜单中,再次选择
Endless Roller。在EndlessRoller)下,在用户访问下选择完全访问。然后,点击创建按钮:

图 14.38 – 创建新应用程序
- 然后,您将被带到1.0 准备提交屏幕。开始填写标题信息。首先,在描述文本框中填写您之前为 Google Play 使用的相关信息。然后,在关键词下输入人们可能搜索以找到您的游戏的可能术语,在版权下输入您的名字。

图 14.39 – 填写版本信息
- 最后,您需要提供一些游戏截图以供使用。如果您点击iOS 截图属性页面,您将看到有关如何创建截图的详细信息(特别是图像的大小)。本章中使用的图像是为 6.5”的 iPhone 显示屏设计的,但您也可以选择可选的 6.7”版本以支持 iPhone 14 Pro:

图 14.40 – 提交应用截图
-
注意,在构建部分,它指出您需要使用几种工具之一提交您的构建。我们将在完成此处剩余步骤后进行。
-
从构建部分进入应用信息屏幕。从那里,将类别更改为游戏,然后在子类别下选择休闲。然后,点击保存:

图 14.41 – 设置应用类别
- 前往定价和可用性部分并选择一个价格。在我的情况下,我将使用$0.00(免费),但像往常一样,您可以挑选您想要的。完成此处选项后,点击保存选项:

图 14.42 – 设置定价和可用性
-
接下来,进入隐私政策部分。正如在 Google Play 部分所述,过去人们使用 Unity 本身推荐的链接为使用 Unity Analytics 和 Ads 的人提供链接(
unity3d.com/legal/privacy-policy),但最安全的选项是雇佣一名律师为您的应用起草隐私政策。如果您想自己创建隐私政策,本站可以帮助您创建相关材料:letsmakeagame.net/game-privacy-policy-generator/。在我的情况下,我在第三方服务部分的隐私政策链接中选择了Facebook和Unity选项,并填写了必要的数据。完成后,点击生成按钮,您将获得用于自己隐私政策的模板数据,然后进行托管。 -
填写隐私政策 URL部分,然后点击开始按钮。

图 14.43 – 设置应用隐私
-
根据您的应用使用的内容类型填写适当的答案。在我们的案例中,由于我们使用 Unity Ads、Analytics 和 IAPs(应用内购买),我们需要浏览它们的每个页面并确保我们使用了它们的所有信息。以下是我用来填写细节的信息:
documentation.cloud.unity3d.com/en/collections/2654776-apple-privacy-surveys#engage-nutritional-labels。 -
完成这些信息后,您的 产品页面预览 屏幕应该看起来像这样:

图 14.44 – 应用隐私完成
-
完成后,点击 发布 按钮。
-
一旦所有信息都填写完毕,请再次打开 Xcode 并您的导出项目(遵循 第二章,Android 和 iOS 开发项目设置)中的相同步骤。
-
Facebook SDK 的添加为我们项目添加了一些必须添加的内容,以便在 iOS 上编译。在切换回 iOS 平台后,我得到了一个错误,说 CocoaPods 无法正确安装。考虑到这一点,我必须对项目进行以下更改。
-
切换回 PC、Mac 和 Linux 独立构建。从您的 Mac 桌面打开一个终端窗口,并输入以下代码 –
sudo gem install cocoapods -v 1.10.2。这将安装一个与这个版本的 Unity 兼容的稳定版本的 CocoaPods。之后,回到 Unity 中,将平台切换回 iOS,你应该会看到错误消失。 -
此外,我还通过访问
github.com/googlesamples/unity-jar-resolver并下载最新版本来更新了 Unity JAR 解析器,在我这个案例中,是external-dependency-manager-1.2.175.unitypackage文件。

图 14.45 – 更新的 unity-jar-resolver
- 打开文件并导入文件后,Unity 会询问我是否想要替换过时的库,我通过点击 应用 按钮接受了。

图 14.46 – 更新文件
- 更新完成后,转到 Assets | 外部依赖管理器 | iOS 解析器 | 设置。在 Cocaopods 集成 下,将值更改为 Xcode 项目。

图 14.47 – 更改 Cocoapods 集成
使用 Facebook SDK,我们将无法再使用 Unity 为我们提供的基项目打开我们的项目,因此我们需要使用终端为我们生成一个工作空间文件。
-
然后,在构建项目后,在项目位置打开一个终端窗口。然后,根据您的处理器,您可能需要输入不同的命令。
-
如果您的 Mac 处理器使用 ARM 处理器(如 M1),请输入以下命令 –
sudo arch -x86_64 gem install ffi。

图 14.48 – 执行 gem 安装
- 此命令只需使用一次。任务完成后,输入以下命令 –
arch -x86_64 pod install。此命令只需在您创建项目的新构建时执行一次。

图 14.49 – 完成的宝石安装
-
如果你的 Mac 处理器使用 x86 处理器(如 Intel),请输入以下命令代替 –
pod install。 -
一旦 Pod 安装成功,双击项目目录中新建的
Unity-iPhone.xcworkspace以在 Xcode 中打开工作区。

图 14.50 – 新工作区文件
如果你现在尝试运行游戏,它将给出关于目标之间依赖循环的错误。幸运的是,一旦我们进入 Xcode,我们就可以解决这个问题。
- 进入 Xcode 后,在左侧菜单的最远端,选择 Unity-iPhone 项目,然后在右侧的相应部分下,在 TARGETS 下选择 UnityFramework 选项。从屏幕顶部显示的选项卡中,点击 构建阶段。从那里,你应该会看到一系列选项。将 头文件 部分拖到 编译源文件 部分之上。如果一切顺利,它应该看起来像这样。

图 14.51 – 调整头文件顺序
- 苹果公司还禁止包含框架文件,因此我们还需要阻止它们被嵌入。要做到这一点,请转到 构建阶段,然后在 构建选项 下,将 始终嵌入 Swift 标准库 选项更改为 否。

图 14.52 – 构建选项
- 一旦项目在 Xcode 中打开并成功导出,请转到 产品 | 归档 并等待其完成:

图 14.53 – 创建归档
-
这通常需要一段时间,所以请等待其完成。你可能需要使用访问密钥。点击 允许 按钮。
-
完成后,你应该会被带到以下菜单。点击 分发应用 按钮,将项目上传到 App Store:

图 14.54 – 分发应用归档
- 你将被要求选择一些选项。通常,使用默认选项,之后,它将显示已上传到商店的
.ipa文件。在上传之前,它将给你一个最后查看项目每个方面的信息的机会。点击 上传 按钮,并等待其完成:

图 14.55 – 查看 ipa 内容
当你的应用上传后,你会看到以下屏幕:

图 14.56 – 归档上传完成
然而,这不会立即显示在 App Store Connect 上;你可能需要等待片刻(或几个小时)才能更新。然而,一旦准备好,你将在我们之前提到的构建部分看到它。
-
上传完成后,您应该在提交应用之前能够点击选择构建按钮。
-
从那里,选择我们创建的构建,然后点击完成按钮:

图 14.57 – 添加构建
- 注意有一个部分说缺少合规信息。点击
https调用。我使用了标准的加密算法,并表示不会在法国提供应用。
关于 Unity 对此立场的更多信息,您可以查看 forum.unity.com/threads/unity-iap-and-export-compliance.742898/ 和 forum.unity.com/threads/how-to-answer-apples-app-store-new-export-compliance-information-dialogue.1363785/.
- 然后,点击保存按钮。完成所有操作并确认所有信息无误后,您可以点击添加以供审查按钮,等待苹果的反馈。
通常,首次开发者收到反馈需要 3-4 周,尽管这可能会更长或更短,具体取决于季节性需求。随着您发布越来越多的标题,每次所需的时间会越来越少。如果获得批准,您将收到一封电子邮件,告知您应用已上传,或者他们可能会提供需要修改以获得商店上架批准的详细信息。
摘要
在本章中,您学习了如何在 Google Play 和 Apple iOS 应用商店发布您的游戏。您学习了如何通过设置 Google Play 控制台将游戏上传到 Google Play,最后如何将应用发布到商店。然后您学习了如何将游戏的 iOS 版本上传到应用商店,以及所有相关的设置。
希望您喜欢对这个功能探索的旅程,并且继续探索这个领域的可能性。在下一章中,我们将发现移动游戏开发中最新的添加之一——增强现实。
第十五章:增强现实
随着《精灵宝可梦 GO》和 Snapchat 滤镜的流行,增强现实(AR)是将数字元素与真实世界融合的一种方式。具体来说,它是一种将计算机生成的图像叠加到用户对真实世界的视图上的技术,因此提供了一种组合视图,这意味着真实世界和叠加在其上的数字元素都显示给玩家。
在本章中,我们将探讨如何设置我们的项目以利用 AR 为 Android 和 iOS 设备服务,以及我们如何自定义它们。这个项目将是一个简单的 AR 项目,玩家可以在游戏环境中查看各种表面并在其上生成对象。本章的目标将是探索 AR 的基本概念,并了解它们如何在项目中使用。
本章将分为几个主题。它将包含从开始到结束的简单、逐步过程。以下是我们的任务大纲:
-
为 AR 设置项目
-
检测表面
-
与 AR 环境交互
-
在 AR 中生成对象
到本章结束时,你将很好地理解 AR 技术以及如何创建一个基本的 AR 项目。无论你是初学者还是有经验的开发者,本章都将为你提供一个坚实的基础,以便进一步探索和实验 AR,这不仅对移动开发有用,对于那些希望获得与 Meta 的耳机以及苹果的 Vision Pro 中使用的相同技术经验的人来说也很有用。
技术要求
本书使用 Unity 2022.1.0b16 和 Unity Hub 3.3.1,但步骤应该在未来版本的编辑器中只需进行最小更改即可。如果你想下载本书中使用的确切版本,并且有新版本发布,你可以访问 Unity 的下载存档unity3d.com/get-unity/download/archive。你还可以在Unity 编辑器系统要求部分找到 Unity 的系统要求docs.unity3d.com/2022.1/Documentation/Manual/system-requirements.html。要部署你的项目,你需要一个 Android 或 iOS 设备。
你可以在 GitHub 上找到本章中提供的代码文件,链接为github.com/PacktPublishing/Unity-2022-Mobile-Game-Development-3rd-Edition/tree/main/Chapter15。
为 AR 设置项目
在我们开始向我们的项目添加通知之前,我们需要添加三个 Unity 提供的包,以使 iOS 和 Android 设备都支持 AR。在我们的案例中,我们将利用 ARCore 和 ARKit 来创建我们的项目,并使用 AR Foundation 包作为中介,以便我们可以在使用类似连接的同时使用 ARCore 和 ARKit。由于这是一种全新的创建项目的方式,我们将实际创建一个新的 Unity 项目来演示如何使用它。请按照以下步骤操作:
-
要开始,请在您的计算机上打开 Unity Hub。
-
从启动开始,我们将通过点击新建按钮来创建一个新的项目。
-
接下来,在
Mobile AR下,并在模板下,确保3D被选中:

图 15.1 – 创建 3D 项目
-
之后,点击创建项目并等待 Unity 加载。
-
从 Unity 编辑器,转到窗口 | 包管理器。
-
如果尚未设置,请从包菜单的工具栏中点击项目内下拉菜单,并选择Unity 注册表。
-
从这里,转到右上角的搜索栏,并输入
XR。从那里,您需要选择ARKit XR 插件以支持 iOS 设备,或选择ARCore XR 插件以支持 Android,然后点击安装。之后,向下滚动可用的选项,直到找到AR Foundation并选择它。一旦到达那里,点击安装按钮:

图 15.2 – 安装包
我们现在已经拥有了所有需要的包,因此我们可以退出包管理器。
注意
虽然 AR Foundation 可以完成您想要在 AR 中做的几乎所有事情,但还有一些事情是仅适用于 iOS 或 Android 的。有关 AR Foundation 能做什么以及 ARCore 和 ARKit 各自提供的内容的更多信息,请查看docs.unity3d.com/Packages/com.unity.xr.arfoundation@5.0/manual/index.html#platform-support。
-
接下来,通过访问文件 | 构建设置打开构建设置菜单。从那里,将您的平台更改为iOS或Android,然后点击切换平台按钮。之后,点击玩家设置...选项。
-
接下来,如果您计划使用 iOS,请完成步骤 10;如果您计划使用 Android,请完成步骤 11;如果您计划同时使用 iOS 和 Android,请完成这两个步骤。
-
如果您正在使用 iOS,请确保在 iOS 的平台设置部分中,需要 ARKit 支持选项也被选中。
-
对于使用安卓的用户,转到安卓玩家设置...,在其他设置下,转到渲染并取消勾选自动图形 API选项。然后,在图形 API部分,选择Vulkan选项并按–按钮将其从列表中删除。然后,向下滚动并取消勾选多线程****渲染选项。

图 15.3 – 安卓玩家设置配置
- 我们必须禁用此功能的原因是,截至写作时,它与 ARCore 不兼容。您还希望将最小 API 级别设置为Android 7.0‘Nougat’(API 级别 24)或更高。
当在 Unity 中处理 ARCore 项目时,建议您启用 ARM64 目标架构。如果您的应用程序仅支持 32 位 ARMv7 架构,它可能在 64 位设备上无法正常工作,并且可能无法从 Google Play Store 下载。这是因为一些 64 位设备不支持 32 位 ARCore 库。因此,为了避免任何问题,最好在 Unity 项目中启用 ARM64 目标架构。
在配置下,将脚本后端设置为IL2CPP。接下来,在目标架构下,启用ARM64选项。

图 15.4 – 更改目标架构
- 现在,从玩家设置...菜单中选择XR 插件管理选项,然后,在插件提供者下,勾选ARCore字段,对于 iOS,勾选ARKit选项。

图 15.5 – 启用 ARCore
通过这样,我们已经处理了所有支持我们的项目并正确导出的设置!
现在我们已经包含了 AR Foundation,我们现在可以为 VR 项目创建一个基本场景。
基本设置
由于玩家在游戏开始时可能位于任何位置,我们无法使用传统意义上的摄像机,因此我们将首先删除原始的摄像机。按照以下步骤操作:
- 从层次结构面板中选择主摄像机对象,通过右键点击并选择删除或按Delete键来删除它。
在我们开始实现自己的功能之前,我们需要创建两个关键对象:AR 会话和AR 会话原点。
- 在层次结构面板中右键点击并选择XR | AR 会话:

图 15.6 – 创建 AR 会话
AR 会话是控制任何 AR 体验生命周期的,这允许我们根据我们正在工作的平台启用或禁用 AR 功能。
注意
AR Session还负责告诉你你的设备是否支持 AR。有关处理此问题的信息,请参阅docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.2/manual/index.html#checking-for-device-support。
- 通过右键单击并选择XR | AR Session Origin来创建一个AR Session Origin对象。
AR Session Origin用于在游戏本身播放时缩放和偏移虚拟内容。你可能注意到对象有一个子对象,AR Camera,这是在游戏运行时跟随游戏的相机。
在我们将设备部署以确保一切正常工作之前,让我们在我们的场景中添加一个立方体,这样我们就可以看到它是否正常工作。
- 如果还没有切换到场景视图,请从顶部菜单点击GameObject | 3D Object | Cube:

图 15.7 – 创建一个立方体
- 现在,构建你的项目,并将游戏以与第二章中讨论的相同方式部署到设备上,Android 和 iOS 开发的项目设置。
小贴士
对于 iOS 用户,当你构建项目时,可能会告诉你项目缺少.xml文件。如果出现这种情况,当被提示时,请点击是,修复并构建选项。
对于 Android 用户,如果尚未安装,你可能需要安装 Google Play Services for AR。
- 运行你的项目后,如果需要,请授予相机访问权限并打开游戏。一旦项目显示你的环境,当游戏开始时向后退:

图 15.8 – 我们的第一项 AR 结果
我们必须后退以看到立方体的原因是因为所有对象的位置都是基于游戏开始时手机物理所在的位置。立方体也相当大,因为在默认的 1,1,1 缩放下,这意味着它每边相对于真实尺寸的对象有 1 米(或大约 3.3 英尺)宽。显然,我们不希望用户在游戏开始时后退,因此我们需要跟踪我们环境中可用的表面位置,这正是我们将要做的下一件事。
为了检测我们真实世界环境中的表面,我们需要使用一个新的组件,AR Plane Manager。这个组件允许我们根据真实世界环境中的表面创建、删除或更新场景中的 GameObject。以下步骤将自动创建不可见的带有碰撞器的平面,我们可能可以用作游戏玩法的原因:
-
我们不再需要我们创建的原始立方体,因此我们可以通过右键单击它并选择删除,或者通过选择它并按Delete键来从场景中删除它。
-
从 层次 面板中选择 AR 会话原点 对象。从那里,通过在 检查器 窗口的底部点击 添加组件 按钮并输入组件名称然后按 Enter 键,将其添加到 AR 平面管理器 组件中。
到这一点,我们将在运行时生成场景中的表面,但对于像调试这样的东西,能够直观地看到正在生成的平面会是个好主意。所以,我们将使用以下步骤来做这件事:
- 从顶部菜单,转到 游戏对象 | XR | AR 默认平面。
此对象有几个不同的组件,用于创建视觉平面;值得注意的是,AR 平面 和 AR 平面网格可视化器 组件。AR 平面 代表由 AR 设备检测到的平面,而 AR 平面网格可视化器 负责使用 AR 平面 的数据来修改 MeshFilter 和 MeshCollider 组件,以叠加检测到的墙壁,并使用 线条渲染器 组件显示边界。网格渲染器 组件将绘制这些修改后显示的信息。
-
从
预制件。 -
将
预制件文件夹拖动以将其转换为预制件。如果操作正确,你应该会注意到 层次 窗口中 GameObject 的文本现在变成了蓝色:

图 15.9 – 创建预制件
-
一旦创建,你可以在 层次 窗口中删除 AR 默认平面 对象。
-
选择 AR 会话原点 对象。点击 添加组件 按钮,并添加一个 AR 平面管理器 组件。之后,将 AR 默认平面 预制件拖放到 AR 平面 管理器 组件的 平面预制件 属性中:

图 15.10 – 分配平面预制件
这将告诉 平面管理器,每次它检测到场景中的新平面时,它都应该生成一个平面预制件,并为其绘制细节。
- 保存你的项目并再次构建你的游戏。一旦它在你的设备上运行,你就可以在房间里四处走动,同时移动相机:

图 15.11 – AR 中创建平面
你在一个有移动的区域停留的时间越长,手机就需要更长时间来构建你环境中表面的更逼真的描绘。
随意打开我们创建的预制件,并修改你的平面如何被可视化!
现在我们可以看到游戏环境中的事情发生,这很好,但我们目前还没有实际与世界交互的方法——这就是我们将在下一节中探讨的内容。
与 AR 环境交互
我们可以让玩家与世界交互的一种方法是通过允许他们在场景中生成对象,以帮助玩家看到物品将生成在哪里。我们可以创建一个指示器来显示它们实际生成的地方。让我们看看完成这一点的步骤:
- 使用GameObject | 3D Object | Quad创建一个四边形。
四边形代表一个平面,这是最简单的几何类型。在我们的情况下,我们将使用四边形作为指示器,告诉玩家如果他们在屏幕上点击,他们将生成对象的位置。
- 选择四边形后,前往
(0,0,0),90,和(0.2,0.2,1)。
我们将四边形缩小到 20 厘米长,并旋转它以更好地代表地板。我们不希望这些值改变,但最终我们希望移动和旋转这个对象以跟随玩家移动摄像头。为了保护这些数据,我们可以为它创建一个父对象。这样,每当父对象移动或旋转时,子对象也会以相同的方式移动和旋转。
-
通过选择
放置指示器创建一个空的游戏对象。然后,前往(0,0,0)。 -
从层次结构窗口中,将Quad游戏对象拖放到放置指示器对象上,使其成为子对象:

图 15.12 – 创建放置指示器对象
现在我们有一个可以工作的对象,我们需要一种方法来确定玩家的摄像头朝向,以便我们可以移动该对象。我们可以通过使用一个新的组件,AR 射线投射管理器来实现这一点。
- 从层次结构窗口中选择AR 会话起源对象。从那里,向其添加AR 射线投射管理器组件。
AR 射线投射管理器组件提供了向 AR Foundation 进行射线投射的能力。这将允许我们在我们所在的物理环境中执行射线投射。射线投射,也称为碰撞检测,允许我们创建一条射线,这是一条无形的线,可以用来检查是否从其起点和方向有物体与之碰撞。这在游戏中经常用于检查子弹是否会击中玩家。
现在我们已经完成了这个设置,让我们看看我们如何在代码中与这些组件一起工作,以及我们如何使用以下步骤的信息在现实世界空间中放置 AR 对象:
-
前往
Assets文件夹。从那里,创建一个名为Scripts的新文件夹。 -
进入
Scripts文件夹,创建一个名为PlaceARObject的新 C#脚本。 -
在文件顶部添加以下
using语句:using UnityEngine.XR.ARFoundation; /* ARRaycastManager */ using UnityEngine.XR.ARSubsystems; /* TrackableType */ -
将以下属性添加到类中:
/// <summary> /// A reference to the Raycast Manager for being able /// to perform raycasts /// </summary> ARRaycastManager raycastManager; /// <summary> /// A reference to the AR camera to know where to draw /// raycasts from /// </summary> Camera arCamera; -
然后,我们需要在
Start函数中初始化属性:/// <summary> /// Start is called before the first frame update. /// Initialize our private variables /// </summary> void Start() { raycastManager = GameObject.FindObjectOfType <ARRaycastManager>(); arCamera = GameObject.FindObjectOfType<Camera>(); } -
最后,我们需要替换我们的
Update函数并使用LateUpdate代替:/// <summary> /// LateUpdate is called once per frame after all /// Update functions have been called /// </summary> private void LateUpdate() { /* Figure out where the center of the screen is */ var viewportCenter = new Vector2(0.5f, 0.5f); var screenCenter = arCamera.ViewportToScreenPoint(viewportCenter); /* Check if there is something in front of the center of the screen and update the placement indicator if needed */ UpdateIndicator(screenCenter); } -
在前面的代码片段中,我们使用了一个当前不存在的
UpdateIndicator函数,所以让我们添加它:/// <summary> /// Will update the placement indicator's position and /// rotation to be on the floor of any plane surface /// </summary> /// <param name="screenPosition">A position in screen /// space</param> private void UpdateIndicator(Vector2 screenPosition) { var hits = new List<ARRaycastHit>(); raycastManager.Raycast(screenPosition, hits, TrackableType.Planes); /* If there is at least one hit position */ if (hits.Count > 0) { // Get the pose data var placementPose = hits[0].pose; var camForward = arCamera.transform.forward; /* We want the object to be flat */ camForward.y = 0; /* Scale the vector be have a size of 1 */ camForward = camForward.normalized; /* Rotate to face in front of the camera */ placementPose.rotation = Quaternion.LookRotation(camForward); transform.SetPositionAndRotation (placementPose.position, placementPose.rotation); } } -
保存脚本并返回到 Unity 编辑器。将
PlaceARObject脚本附加到放置指示器GameObject。 -
将游戏导出到您选择的设备上并验证它是否正常工作:

图 15.13 – AR 中的放置指示器
如您所见,现在平面将移动并旋转,始终面向我们!您可能会注意到飞机有一个闪烁的纹理。这是由于我们之前在第四章中讨论的 z-fighting 概念,分辨率无关 UI。基本上,两个对象具有相同的坐标,所以 Unity 需要决定它们的绘制顺序。我们可以通过将四边形稍微放置在平面的位置上方来解决这个问题,我们现在就这样做。
-
更新
UpdateIndicator函数,在末尾使用以下代码:/* Rotate to face in front of the camera */ placementPose.rotation = Quaternion.LookRotation(camForward); /* Move the quad slightly above the floor to avoid z-fighting */ var newPosition = placementPose.position; newPosition.y += 0.001f; transform.SetPositionAndRotation(newPosition, placementPose.rotation); } } -
保存脚本并再次导出游戏。如您所见,现在四边形干净地放置在给定的表面上方:

图 15.14 – 调整后的放置指示器
现在我们有一种指示器,让我们实际在 AR 中生成一个对象。
在 AR 中生成对象
在 AR 中创建对象的简单方法就是让玩家在屏幕上点击时,在放置指示器对象所在的位置生成对象。但在我们这样做之前,我们首先需要创建一个我们希望在场景中创建的对象。
按照这里给出的步骤进行:
-
通过转到GameObject | 3D Object | Sphere创建一个球体。
-
从
(0,0,0)设置到(0.2,0.2,0.2)。 -
通过转到Component | Physics | Rigidbody为球体添加一个Rigidbody组件。
通过添加Rigidbody组件,我们让 Unity 知道我们希望这个对象受到重力、碰撞事件和施加到它上的力的作用。在这个阶段,您可以按需自定义对象,更改网格和碰撞器等。
- 前往
Prefabs文件夹。通过从Hierarchy窗口拖放到Project窗口创建我们的球体的 Prefab:

图 15.15 – 创建用于生成的 3D 对象
-
现在这个对象是一个 Prefab,我们可以从Hierarchy窗口中删除它。
-
打开
PlaceARObject脚本,并向其中添加以下属性:[Tooltip("The object to spawn when the screen is tapped")] public GameObject objectToSpawn; -
然后,更新
LateUpdate函数为以下内容:/// <summary> /// LateUpdate is called once per frame after all /// Update functions have been called /// </summary> private void LateUpdate() { /* Figure out where the center of the screen is */ var viewportCenter = new Vector2(0.5f, 0.5f); var screenCenter = arCamera.ViewportToScreenPoint( viewportCenter); /* Check if there is something in front of the center of the screen and update the placement indicator if needed */ UpdateIndicator(screenCenter); /* If we tap on the screen, spawn an object */ if (Input.GetMouseButtonDown(0)) { /* Spawn the object above the floor to see it fall */ Vector3 objPos = transform.position + Vector3.up; if (objectToSpawn) { Instantiate(objectToSpawn, objPos, transform.rotation); } } } -
保存脚本并返回到 Unity 编辑器。
-
从Hierarchy窗口中选择放置指示器对象。从Inspector窗口中,将要生成的对象属性设置为我们的SpherePrefab:

图 15.16 – 设置要生成的对象
- 保存您的项目并构建到您的设备上,然后点击屏幕以在屏幕上生成球体:

图 15.17 – 在我们的 AR 环境中生成对象
如您所见,我们现在可以将对象生成到场景中,并且可以看到它们正确地相互交互!更进一步,您可以创建任何类型的游戏体验!
摘要
在本章中,您已经学习了如何利用 Unity 的 AR 工具集,通过将人工计算机生成的对象添加到现实世界中来增强现实。这项新兴且不断发展的技术仍在开发中,从这项工作中获得的技能在未来可能会变得更加重要,因为像虚拟现实(VR)、混合现实(MR)和其他形式的扩展现实(XR)变得越来越普遍。
在本章中,您学习了如何为 iOS 安装 ARKit、为 Android 安装 ARCore 以及为多平台 AR 解决方案安装 AR Foundation。安装完成后,您学习了如何设置 iOS 和 Android AR 开发的平台设置。之后,我们进行了基本设置,让 Unity 使用其 AR 工具,使用户能够将简单的网格添加到环境中。然后,我们在此基础上使用 AR Plane Manager 检测现实世界中的表面,并学习了如何使用 AR Default Plane 对象来可视化它。然后,我们学习了如何使用 AR Raycast Manager 与 AR 环境交互,以检测我们何时击中现实世界中的网格,并让计算机生成的世界中的对象对其做出反应。最后,我们看到了如何使用这些信息在 AR 中生成对象。
小贴士
除了 AR Foundation、ARCore 和 ARKit 之外,还有几个其他框架和插件可用于将 AR 添加到 Unity 应用程序中。
如果您的项目需要 Unity 提供之外的具体功能,替代框架可以提供必要的工具或为您的特定用例提供更好的支持和性能。例如,Vuforia(www.ptc.com/en/products/vuforia)以其强大的基于标记的跟踪而闻名,而 Wikitude(www.wikitude.com/download-wikitude-sdk-for-unity/)则专注于基于位置的 AR 体验。
最终,选择 AR 框架取决于平台支持、所需功能、开发者专业知识以及项目特定需求等因素。评估每个框架的优势和局限性,选择与您的目标和需求最匹配的框架是至关重要的。
这应该为您提供所有您需要的信息,以便开始自己进行实验,看看您是否可以创建自己的移动设备游戏和 AR 环境中的游戏。所以,勇敢地前进,利用这本书中的知识来使您的游戏达到最佳状态,我期待着玩它们!


浙公网安备 33010602011771号