Unity-IOS-游戏开发-全-
Unity IOS 游戏开发(全)
原文:
zh.annas-archive.org/md5/fad6b89f1fe6040e64442c7750cf7e1b译者:飞龙
前言
苹果的 iOS 席卷全球,提供了一个游戏开发平台,这是第一次让普通开发者有机会在全球数十亿美元的娱乐软件空间中竞争。虽然为这个平台开发游戏有几种可行的解决方案,但 Unity 已成为 iOS 和其他平台的首选平台。通过 Unity 的工具集和本书,你将迈出第一步,开始为 iOS 平台制作商业质量的游戏。
本书采用学习方法,专注于构建 iOS 游戏所必需的内容。从设计(从移动角度)到脚本和创建以 iOS 为中心的游戏机制,你将学习所有你需要开始的内容。在本书的整个过程中,你将构建课程,以设计和发布一个集成了所有必要组件以制作盈利性标题的游戏。
本书涵盖的内容
第一章,什么是 Unity 以及为什么我应该关心? 讨论了 iOS 开发空间、Unity 以及为什么你想要使用 Unity 作为 iOS 和其他平台的游戏开发平台。
第二章,入门 详细介绍了安装 Unity 以及熟悉用户界面及其语义。
第三章,你好,世界 探索了创建一个示例应用程序,使用苹果的工具配置应用程序,并将该应用程序部署到设备上的过程。
第四章,Unity 概念 讨论了 Unity 平台、它的工作原理以及如何使用该平台来组装游戏。
第五章,脚本:到底是谁的台词? 从 Unity 的角度深入探讨了脚本,包括为什么脚本对于使用 Unity 进行游戏开发至关重要,C#接口以及构建游戏脚本。
第六章,我们的游戏:战斗呐喊! 探讨了 Unity iOS 游戏的一些设计主题,并概述了通过后续章节构建的示例 iOS 游戏的机制。
第七章,输入:让我们动起来 展示了 iOS 平台输入的许多方面,并指导用户如何为基于触摸的游戏构建基本输入系统。
第八章,多媒体 将用户聚焦于将电影、音乐和音频集成到游戏中,以及如何为 Unity iOS 平台特定地制作和集成内容。
第九章,用户界面 从标准 Unity GUI API 和 Prime31 的 UIToolkit 的角度讨论了为 iOS 游戏构建用户界面。
第十章,游戏脚本专注于将我们的游戏需求转换为 Unity 中的 iOS 特定功能,并生成如粒子系统、由动画驱动的行为、碰撞和布娃娃系统等游戏机制。
第十一章,调试和优化概述了调试和性能分析,同时探讨了对象池和 Beast 照明作为优化性能的具体手段。
第十二章,商业化:从你的创作中获得丰厚回报探讨了使用 Unity 商业化 iOS 应用程序的一些方法,包括 iAds、内购和 Unity 资产商店。本章还说明了如何使用 iTunes Connect 跟踪成功。
您需要为本书准备的内容
由于 iOS 开发仅在 OSX 平台上得到官方支持,因此您需要一个运行 OSX、XCode 开发工具和订阅 Apple 开发者计划的机器。您可以在以下位置找到 XCode 和 Apple iOS 开发者计划的详细信息:developer.apple.com。
关于加入 iOS 开发者计划、使用条款和其他未在本书中具体说明的政策的信息,可以在那里找到。
您还需要访问 Unity 开发平台和 iOS 插件,这些可以在以下位置获得:www.unity3d.com。
本书面向的对象
如果您是一位对在 iOS 平台上开发游戏感兴趣的开发者,并希望利用 Unity 平台,本书将为您提供开始所需的核心理解。如果您是一位希望将现有应用程序移植到移动平台的 Unity 开发者,本书将概述使用 Unity iOS 插件发布所涉及的过程。
了解 C#或 JavaScript 将有所帮助,但如果您是这两种语言中的任何一种的资深开发者,您仍然可以通过本书学习如何将您的技能应用于使用 Unity 和 iOS 平台进行移动开发,因为本书的大部分内容都是针对 Unity 和 iOS 平台的概念和实现进行探索。
本书中的示例代码主要使用 C#编写。然而,在某些情况下,JavaScript 被用作教学辅助工具。虽然书中提供了足够的信息来学习 C#的必要组件,但本书的目标并不是教授 C#或其基础知识。
习惯用法
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词如下所示:“一旦下载(您应该有一个.mobileprovision文件),在您的机器上双击该文件。”
代码块设置如下:
import UnityEngine
import System.Collections
class example(MonoBehaviour):
def Start():
curTransform as Transform
curTransform = gameObject.GetComponent[of Transform]()
curTransform = gameObject.transform
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
IEnumerator Start () {
iPhoneUtils.PlayMovie("Snowpocalypse2011.m4v", Color.black, iPhoneMovieControlMode.CancelOnTouch, iPhoneMovieScalingMode.AspectFill );
yield return null;
Application.LoadLevel("MainMenu");
}
新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以这种方式显示:“选择打开其他...按钮,导航到您安装本书资产的位置”。
注意
警告或重要注意事项将以这种方式显示在框中。
注意
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大价值的标题非常重要。
要发送给我们一般性的反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图颜色图像的 PDF 文件。颜色图像将帮助您更好地理解输出的变化。您可以从http://downloads.packtpub.com/sites/default/files/downloads/0409_unityimages.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support,选择您的书,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分中。
侵权
在互联网上侵犯版权材料是一个跨所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢你在保护我们作者和我们提供有价值内容的能力方面的帮助。
问题
如果你在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。
第一章. Unity 是什么,我为什么应该关心?
欢迎来到 Unity 的世界!在这本书中,我们将从头到尾探索如何利用移动设备上最激动人心和易于访问的游戏开发技术之一来开发游戏。
在本章中,你将学习使用 Unity Technologies 的游戏开发产品 Unity 的基本操作。我们将一起探索如何利用这个开发平台在 iOS 设备上发布游戏。
在本章中,我们将:
-
了解 Unity 作为开发平台的价值
-
安装 Unity
-
学习如何配置 Apple 开发者门户以支持开发和发布
-
配置我们的开发环境以发布到 iOS 设备
-
将示例应用程序发布到我们的 iOS 设备
这可能听起来并不多,但在 iOS 开发中,有很多事情你可能会做错,这会导致与 Unity 一起工作时遇到困难。我们不会假设你一切都会做对,我们将一步一步地讲解,以确保你可以把时间花在构建游戏上,而不是试图解读神秘的错误信息。
那么,让我们开始吧...
重要预备知识
本章假设你已经安装了 XCode 和 Apple iOS SDK 4.x 或更高版本。如果你没有安装这些工具,你可以从developer.apple.com获取它们。
此外,假设你已经从www.unity3d.com下载并安装了 Unity。
本章还假设你已经在位于developer.apple.com的 iOS Dev Center 上设置了一个账户。由于 iOS 应用程序在发布到应用商店或分发到设备之前必须进行签名,你必须设置一个账户并在你的机器上安装必要的证书。Dev Center 网站上有很多视频,可以帮助你设置证书。
还请注意,书中展示的截图代表的是 Unity 的 Mac OSX 版本,因为 OSX 平台是 iPhone 应用程序的官方开发环境。
Unity 是什么?
想象一下,你想要为 iPhone 构建一个游戏,并且想要利用所有平台的功能,但你不知道 Objective-C,你也不想构建一个 3D 引擎。市场上有很多用于开发可在 iOS 上运行的应用程序的解决方案——包括创建 Objective-C 项目并使用 OpenGL ES 编写专门针对你内容的游戏引擎的经过验证的方法。
考虑到这些事实,Unity 是什么,你为什么应该关心?
随着数亿台移动设备掌握在消费者手中,并且每天都有更多设备出现,很明显,移动设备已经成为游戏开发者增长最快的领域之一。虽然拥有如此庞大的受众的前景令人兴奋,但存在许多操作系统、视频技术、触摸界面、蜂窝网络技术、3D 加速器等等,这些都使得在没有某种机制来抽象平台差异并允许你专注于重要的事情——提供出色的游戏体验的情况下,真正有利可图地向如此庞大的受众提供吸引人的内容变得困难。
此外,还有大量方法可以将游戏的各种方面传递给最终用户。暂时考虑一下,提供声音、音乐、3D 艺术作品、物理、网络,甚至游戏中的力反馈等可用的技术数量。进一步考虑,要有一个可以快速构建和测试想法的环境需要多大的努力。
要真正在这个新的多屏幕市场中取得成功,你需要一个环境,让你能够集中精力创造出色的体验,而不是游戏将在其上运行的各个硬件平台的繁琐细节,或者游戏如何将体验传递给最终用户的机制。这正是 Unity 为你提供的——这就是你为什么应该关心它的原因!
在设备上运行真实的应用程序
为了说明使用 Unity3d 可以制作的内容类型,我们将从在设备上运行一个真实的应用程序开始。为了正确完成这项工作,你必须执行一系列步骤,特别是如果你是 iOS 平台的新开发者,所以我将花些时间确保你理解正在发生的事情。如果你没有正确地做事,iOS 开发可能会非常苛刻——但一旦你走过几次,它就会变得自然而然。
我们将逐步讲解制作适用于 Unity3 的、可以部署到 iOS 设备上的商业内容的每个必要步骤:
-
加载项目
-
选择 iOS 作为目标平台
-
将应用程序发布到我们的设备上
-
在设备上播放我们的内容
行动时间——加载项目
第一步是点击Unity IDE图标来启动 Unity 开发环境。

如果你熟悉 Unity 2 版本,重要的是要注意,现在不再有单独的 Unity iPhone 应用程序。Unity 3 的新特性之一是,不再为每个部署目标提供不同的环境——你有一个 IDE 可以处理所有事情。这带来了一系列好处,正如我们将在本书的整个过程中看到的那样。
当环境启动时,您首先会看到项目向导。在本章中,我们只是简单地加载和部署现有项目,以便我们可以遍历为发布到 iOS 设备而设置所有内容的流程。
-
选择打开其他...按钮,导航到您安装书籍资产的位置,并选择
Chapter 1文件夹。 -
Unity 将加载此项目,您将看到标准的 Unity 界面:
![行动时间 — 加载项目]()
-
如果您注意到,在上一个截图的中间,您将看到应用程序标题栏中的标准 VCR 控件。
-
如果您点击播放按钮,游戏将在您的机器上启动,您将能够在 Unity IDE 中玩转游戏。花点时间玩一下,因为您想要对应用程序在常规 iOS 设备上安装后的外观和行为有一个大致的了解。
刚才发生了什么?
我们已经在 Unity 环境中加载了示例游戏项目并在我们的开发机上运行了它。在正常的开发生命周期中,您会发现您将在您的机器上执行代码-调试-测试周期,并将其导出到您的设备上以确保性能足够或测试设备特定的功能。
行动时间 — 选择 iPhone 作为目标平台
在我们有机会了解游戏在 iOS 设备上的外观和玩法后,是时候将应用程序部署到我们的目标 iOS 设备上了。在 Unity IDE 中,您通过更改项目的构建设置来完成此操作:

接下来,让我们检查项目的构建设置对话框:

在构建设置对话框中,有一些活动我们要执行:
-
首先,我们想要确保我们的游戏世界中有些内容。由于您正在加载现有项目,您应该已经拥有构建中的场景。如果由于某种原因您没有,您可以按添加当前按钮。这告诉 Unity,您一直在玩的游戏场景是您想要包含在 iOS 游戏中的场景。
-
接下来,我们想要确保我们正在针对正确的平台。在平台列表中,您可以通过查找旁边带有 Unity 标志的平台来确定正在针对哪个平台。在我们的例子中,我们确保它位于“iOS”旁边。
我知道您此时强烈想要点击构建并运行按钮。然而,请记住,iOS 应用程序在部署到设备或销售在苹果应用商店之前必须进行签名。由于我们还没有告诉 Unity 它应该为哪个开发者配置文件和应用程序标识符进行发布,它将无法将应用程序发布到设备。因此,如果您真的这样做,当您尝试为设备构建并运行时,您将看到以下对话框:

刚才发生了什么?
我们刚刚选择了我们的游戏发布目标。由于 Unity 可以发布到多个平台,您将针对您想要的目标平台执行此步骤。因此,如果您针对 Android、网络或甚至游戏机,您只需在对话框中选择该平台,Unity 就会生成在该平台上运行的发行版。
小贴士
在撰写本文时,Unity 提供了对其他平台(如 Android、Xbox360、PS3 和任天堂 Wii)的发布支持,还有许多其他平台正在开发中。这些附加平台将需要购买 Unity 的 Pro 版本,以及发布到特定平台所需的任何费用。
行动时间——将内容发布到我们的设备
要将内容发布到我们的设备,我们必须为 Unity 提供捆绑标识符。要创建一个,我们必须在 iOS 配置文件门户中提供它。该门户位于可访问的 iOS 开发者中心内,网址为developer.apple.com。
在 iOS 开发者程序的首页,您将找到一个链接,点击后会带您进入iOS 配置文件门户。此外,您还会看到指向用于发布您的产品以及获取销售和市场表现信息的iTunes Connect门户的链接:

-
在iOS 配置文件门户中,您将选择App ID设置,以便您可以创建一个新的应用程序 ID(这与 Unity 正在寻找的捆绑标识符相同):
![行动时间——将内容发布到我们的设备]()
- 让我们看看我们如何为我们的应用程序创建App ID:
![行动时间——将内容发布到我们的设备]()
- 对于 iOS 应用程序来说,App ID非常重要,因为它是应用程序通过 Game Center、应用内购买、推送通知以及在 Unity 开发环境中被唯一识别的机制:
![行动时间——将内容发布到我们的设备]()
-
应用标识符有一个描述、一个前缀和一个后缀。前缀是一组随机字符,用于保证唯一性,后缀代表应用的唯一标识符。当 Unity 提到包标识符时,它仅指后缀。
创建您的App ID时,请给出一个清晰的应用描述,以便以后容易找到它。这很重要,因为随着时间的推移,您将拥有大量为所有您将要创建的游戏生成的App ID。对于种子 ID 本身,只需将其设置为生成新 ID。
对于包标识符,使用标准的反向域名表示法来为您的应用生成标识符。一旦创建,我们只需要一种方法将此移动到我们的开发环境中。
注意
对于包标识符,请确保不要在末尾附加通配符字符,因为这将在您寻求添加更高级功能时限制您。
-
我们现在需要将此应用 ID 与配置文件关联起来。如果您已经有了配置文件,您可以修改现有的配置文件并更改它所代表的App ID。如果您没有,或者不想修改现有的配置文件,请进入iOS 配置文件门户的配置部分:
![操作时间 — 将内容发布到我们的设备]()
-
一旦进入,为这个App ID创建一个新的配置文件,并填写所有字段,确保在下拉列表框中选择适当的App ID:
![操作时间 — 将内容发布到我们的设备]()
小贴士
您可能没有注意到,但这个例子说明了当您遵循不给出非常描述性的名称的App ID的坏习惯时会发生什么。虽然在这个例子中我知道我想要第一章,但如果我在开发多本书 — 我将无法识别这个 App ID 代表的是哪一章。
- 注意,我已经选择了所有我希望为此应用配置的设备。如果您在这里没有选择设备,您将无法将该应用部署到该设备。
-
您的配置文件已创建,现在您需要点击下载,以便将其下载到您的开发环境中:
![操作时间 — 将内容发布到我们的设备]()
-
下载后(您应该有一个
.mobileprovision文件),在您的机器上双击该文件。由于这是一个 XCode 的注册文件类型,它应该会为您将其安装到 XCode 中。如果由于某种原因它没有安装,您可以在 XCode 中打开组织者(窗口 | 组织者),在组织者中选择配置文件条目。XCode 然后将安装配置文件,并将其同步到所有可以接受配置文件的设备:
![操作时间 — 将内容发布到我们的设备]()
- XCode 有能力自动配置在 XCode 中配置的 iOS 开发者门户中的设备。这使得确保您的配置文件添加到目标设备变得容易。为此,请确保已勾选自动设备配置复选框:
![行动时间 — 发布到我们的设备]()
- 一旦执行,XCode 将与 iOS 开发者门户通信并下载所有配置的设备,并在组织者中显示它们:
![行动时间 — 发布到我们的设备]()
-
现在我们能够配置 Unity 将内容发布到我们的目标设备上的这个应用 ID。我们通过输入包标识符并设置我们应用的应用 ID后缀来完成这个操作。在我们的例子中,它将是
com.gregorypierce.chapter1:![行动时间 — 发布到我们的设备]()
- 完成这一步后,为 Unity 构建游戏最困难的部分就结束了!
-
通过从文件菜单中选择构建和运行选项(文件 | 构建和运行)来运行此应用程序。这次当您选择“构建和运行”时,Unity 将为您的内容构建一个播放器,并将应用程序部署到连接到机器的 iOS 设备上。
当您看到 XCode 打开并开始构建应用程序时,不要感到惊讶,因为这表明过程正在运行,并且很快您应该会在您的设备上看到示例应用程序开始运行。
刚才发生了什么?
Unity 在幕后所做的是,从 Unity IDE 中获取所有资源和脚本,并组装一个播放器,该播放器能够根据用户的输入回放内容及其所有场景。这是一个非常重要的概念,因为 Unity IDE 中的内容在很大程度上是平台无关的,并且可以在 Unity 环境中简单重新编译后轻松重新部署。这个播放器就是实际部署到 iOS 设备上的应用程序。
我们在 iOS 开发者门户中创建了一些工件,花费了大量步骤。这些工件包括:证书、应用 ID 和配置文件。这些工件在开发者、苹果公司和开发者及消费者手中的 iOS 设备之间形成了一个信任圈。
证书是在苹果环境中创建的凭证,允许内容由开发者专门签名,从而清楚地表明谁创建了内容。如果没有证书,可能有人声称自己是开发者,并以他的名义签名应用程序。
应用 ID 是一个唯一的标识符,允许 iOS 设备和苹果服务明确知道,哪个应用程序正在尝试执行某些操作。最后,配置文件定义了。
配置文件将证书、设备和应用程序 ID 关联起来。如果没有在您的机器上安装配置文件,您将无法对设备或应用商店进行签名或部署应用程序。
一旦我们向 Unity 提供了 App ID,它就能与 XCode 通信,并告诉 XCode 应该使用哪个配置文件和证书来签名我们的应用程序并将其部署到 iOS 设备上。在设备本身上,当 XCode 部署应用程序时,它会将配置文件传输到设备,这样 iOS 设备就能识别出这是一个应该运行的设备,即使苹果应用商店没有提供它。
我们刚刚完成了设置开发环境和发布内容到 Unity 所需的所有步骤。此外,我们使用 Unity Remote 建立了自己的小型测试实验室,这样我们就可以在我们的设备上使用它,同时在开发环境中调试游戏。这是一个关键里程碑,因为我们现在可以完全专注于定制 Unity 和构建游戏。
快速问答 - 基础知识
-
以下哪个平台 Unity 无法发布内容?
-
a. 网络
-
b. 控制台
-
c. iOS 设备
-
d. 安卓设备
-
e. Linux
-
-
你可以去哪里设置你的 iOS 设备的应用 ID?
-
a. 苹果开发者论坛
-
b. XCode 组织者
-
c. iTunes Connect
-
d. iOS Provisioning Portal
-
e. XCode SDK
-
-
关于 Unity 开发的应用程序是否可以在苹果的服务条款内发布到 iOS 设备上,仍然存在不确定性?(是/否)
-
如果您有以下 App ID 255U952NTW.com.gregorypierce.chapter1,您应该向 Unity 提供什么作为您的 Bundle Identifier?
-
a. 第一章
-
b. com.gregorypierce.chapter1
-
c. 255U952NTW
-
d. 255U952NTW.com.gregorypierce.chapter1
-
e. 255U952NTW.com.gregorypierce.chapter1.*
-
-
你可以不创建开发者账户就发布 iOS 应用程序吗?(是/否)
摘要
在本章中,我们学习了如何为发布到 iOS 设备设置所有内容。
具体来说,我们涵盖了:
-
如何加载 Unity 并打开一个新项目
-
如何创建用于签名和发布应用程序的应用程序 ID
-
如何将应用程序部署到 iOS 设备
现在我们已经了解了如何设置我们的开发平台,我们准备好真正深入 Unity 并探索其功能——这是下一章的主题。
第二章 启动和运行
在本章中,我们将详细检查 Unity 界面,探索其所有视图和工具,同时根据我们的特定开发风格进行个性化设置,并使用 Unity Remote 配置我们的环境以进行远程调试。在本章中,我们将完成构建应用程序的基础建设,并探索我们需要的所有 Unity 选项。
在本章中我们应当:
-
探索 Unity 用户界面
-
使用新自定义布局自定义我们的界面
-
配置和部署 Unity Remote 进行调试
-
使用 Unity Remote 和我们的新自定义布局测试我们的应用程序
那我们就开始吧...
欢迎回家
如果您曾经使用过 3D 建模工具或使用任何现代软件开发 IDE 编写过应用程序,您会发现 Unity 3 非常熟悉且相当直观。Unity 的界面由一个工具栏区域组成,该区域包含 5 个基本控制组以及可以包含视图的多个用户可自定义区域。
变换工具
变换工具与场景视图一起使用,允许您操纵场景中的对象。我们将花点时间来介绍这些工具,因为我们的大部分时间都将花费在使用它们上。

从左到右工作,第一个工具是一个多用途工具,用于在场景中操纵相机。您正在移动的相机是您对场景的视图,它与游戏中实际显示的内容没有关系。
在默认模式下,手形工具将简单地沿相机周围平移。按下鼠标左键并拖动将沿相机的 X 轴平移。如果您有鼠标滚轮,滚动该滚轮将沿 Z 轴移动相机。

按住Alt键或右鼠标按钮将使手形光标变为眼睛形。在此模式下,您可以在场景中围绕当前轴点旋转相机。场景视图左上角的场景 Gizmo 反映了这一点。当您旋转相机时,Gizmo 将更新以反映当前相机的轴点。

按住控制键允许您在场景中移动鼠标时缩放相机。如果您需要靠近某个关键动作发生的地方,这特别有用。
变换 Gizmo 开关
有两个 Gizmo 决定了使用变换 Gizmo 更新对象时将如何影响对象。变换 Gizmo 正如其名,它出现在场景视图中,允许我们更改对象的位置或旋转。这些开关决定了 Gizmo 将出现在哪里。

第一个切换按钮是位置切换按钮。如果设置为居中,变换 Gizmo 将出现在你想要变换的对象边界中心。在大多数情况下,如果你在场景中布局对象,这通常是你要做的。然而,如果你想根据对象的轴点改变对象的位置,请选择切换按钮的轴点设置。
第二个切换按钮是旋转切换按钮。在这里,你将确定旋转是否相对于对象的本地坐标系,还是基于全局或世界空间坐标系。
VCR 控制按钮
下一个控制按钮组用于在游戏视图中驱动游戏玩法。这些控制按钮的视觉表示非常常见,几乎不需要解释。

播放控制会使游戏开始播放。如果你想停止并查看某些内容,请按下暂停按钮。当按下暂停按钮时,Unity 将切换到场景视图(除非已经显示),这样你就可以检查场景的细节。再次按下暂停按钮将使游戏从上次停止的地方继续。如果在暂停状态下,你想确定下一个循环将发生什么,可以按下步骤按钮。在游戏播放时按下步骤按钮将使游戏进入暂停状态。
小贴士
如果场景视图在单独的标签页上,你将能够同时看到两个视图。
层下拉菜单
在你开发应用程序时,你将在场景视图中创建层,这些层代表你希望在视图中显示的游戏对象组。这有助于在非常复杂的场景中减少显示的杂乱。

在层下拉菜单中,你可以选择你想看到哪些层以及你想隐藏哪些层。隐藏的对象仍然存在,并在游戏下次运行时在游戏视图中显示。
布局下拉菜单
在创建游戏的过程中,你会发现一些工具在某些场景中很有用,而在其他场景中则没有用。自定义布局允许你定义一组视图,以及它们的定位和配置,同时为布局提供一个独特的名称,这样你可以在以后切换到它。

布局下拉菜单将显示所有可切换的布局,允许你快速在多个 IDE 安排之间切换,以便在你需要时拥有重要的工具。
项目视图

项目视图是管理你项目中所有资产的地方。然而,如果那些文件夹中的文件被更新(即,你在其他工具中更改了对象的网格),这些更新也会在 Unity 中更改。
小贴士
此视图对应于您项目的资产文件夹,但您绝对不应该直接修改项目文件夹,您应该在 Unity 项目视图中进行修改。
您可以通过简单地从桌面或文件系统浏览器拖动它们到项目视图中来向项目中添加新资产,Unity 将导入内容以供使用。在幕后,Unity 还将资产移动到项目的 Assets 文件夹中。
层级视图

层级视图是项目视图的紧密伙伴。项目视图负责管理可用于您沙盒的资产——场景视图,而层级视图用于管理场景中的对象以及这些对象的父子关系。例如,您可能有一个场景中的车辆对象,它有一个灯连接到它。在层级视图中,这些对象将具有父子关系,这样灯就会是车辆对象的子对象。结果是,当父对象通过变换、旋转或其他操作改变时,子对象也会受到影响。
在大型项目中,层级视图中将有大量对象。为了更容易找到特定的对象或对象类型,Unity 在层级视图中提供了搜索框。当您输入对象名称时,Unity 将过滤场景视图,使得您输入的对象在视图中清晰可见,而其他对象则被灰色显示。例如,假设您正在尝试查找由大量游戏对象组成的场景中的方向盘组件。如果您在搜索框中输入方向盘,它将只提供纹理、颜色等,以便于查找。同样,如果您在搜索框中输入对象类型,如灯,场景将只突出显示场景中的灯——即使“灯”这个词不在游戏对象名称中。
场景视图
场景视图是您将花费大部分时间的地方。您将在场景视图中构建游戏、放置相机、更改环境设置、观察遮挡级别等:

游戏视图
游戏视图是动作发生的地方。每次您在 VCR 控件中按下播放时,此视图将使用场景中的活动相机并将该相机看到的渲染到游戏视图中:

控制栏位于游戏视图中,包含用于调整游戏视图以提供有助于渲染更接近实际目标显示的游戏视图的有用控件:

控制栏中的第一个工具是自由纵横比下拉菜单,允许您将游戏视图的纵横比更改为不同的值。这对于 iOS 开发尤为重要,因为您可以选择目标设备的纵横比,并更好地了解内容在适当视角下的外观。
下一个工具是播放时最大化切换按钮,当启用时,将显示游戏视图的全屏。在这种情况下,如果游戏视图的分辨率与屏幕不同,您将注意到游戏视图最大化以覆盖整个显示,但仅以您在自由纵横比下拉菜单中设置的分辨率/纵横比渲染场景。
下一个控件是** Gizmos控件。这将强制 Unity 在游戏视图中渲染场景**视图中存在的所有 Gizmos。
最后一个控件是统计控件。当启用时,它将在游戏视图上显示渲染统计窗口。这是一个非常有用的控件,因为它可以在玩游戏时获得对应用程序在高级别上的性能洞察,而无需深入分析器视图。
检查器
检查器视图包含所选游戏对象的所有属性。检查器显示的属性完全基于所选游戏对象而上下文敏感:

由于 Unity 中的游戏对象由网格、脚本、变换等组件组成,因此构成游戏对象的每个组件都将在其检查器中显示编辑器。例如,在我们的示例检查器中,我们已选择场景中的相机。如您所见,变换、相机等每个都有为该游戏对象显示的编辑器。
控制台视图
控制台视图显示了来自您游戏的所有消息。这些消息可能来自 Unity 引擎,也可能代表您使用脚本命令(如Debug.Log())发送到控制台视图的消息。如果您双击控制台视图中出现的消息,您将被直接带到导致消息的脚本:

分析视图
Unity 分析器是您使用 Unity 构建游戏时的最佳伙伴,尤其是在为 iOS 设备开发时。虽然此工具仅适用于 Unity 的专业版,但它值得特别关注,因为它提供了比游戏视图中的渲染统计窗口多得多的信息:

顶部是性能分析工具,提供有关 CPU 使用率、渲染、内存和音频统计信息。每个性能分析工具旁边都有一个直方图,表示从每个帧的仪器化过程中检索到的值。您可以通过点击并拖动鼠标跨过直方图,查看多个性能分析工具的结果,这有助于将特定的性能问题与其他在应用程序中发生的事件相关联:

底部提供有关应用程序正在执行的功能调用的信息。这是通过代码的仪器化和确定函数被调用的频率来完成的。这有助于确定应用程序中的热点区域,以便您可以集中注意力,并在目标平台上使游戏表现良好:

时间行动 - 创建新布局
尽管 Unity 环境有很多功能和选项,但可能会被数据量完全淹没,或者没有意识到某些事情出了问题。我们将探讨为我们的环境构建一个简单的自定义设置,其中包含我们需要的视图,以及一些默认情况下不会出现在界面中的视图,以便为测试应用程序做好准备。如果您熟悉 Eclipse 开发环境,您可能会认为 Unity 会根据您执行的操作打开视图,但 Unity 并不这样做。然而,我们将通过创建一个适合性能分析应用程序的新布局来模拟一些这种功能。
-
创建新布局的第一步是从一个基本布局开始并对其进行自定义。Unity 有几种默认布局可供选择,但就我们的目的而言,我们将选择宽布局。在窗口菜单中,选择宽布局:
![创建新布局的时间 - 创建新布局]()
-
创建我们布局的下一步是决定哪些视图对于完成任务最重要。由于我们计划进行性能分析,因此将性能分析视图引入我们的环境是有意义的。选择窗口菜单并选择性能分析将显示新视图。然而,您会注意到它是一个独立窗口,而不是连接到环境的其余部分。Unity 不需要所有视图都生活在与其他视图相同的窗口中。实际上,这使用户拥有多个屏幕时更容易,因为您可以在不同的屏幕上拥有不同的视图分组。然而,就我们的目的而言,我们将假设我们只有一个屏幕可以工作。
-
要将分析器定位到我们的界面中,请选择分析器标签并拖动它。这将导致在界面中显示一个灰色的标签版本。当您将这个灰色的版本移动到可以停靠的位置时,它将改变形状以说明如果将其停靠在该位置将看起来如何。现在让我们将其释放到层次视图和项目视图的上方:

由于我们正在分析我们的应用程序,我们可能并不真的需要了解太多关于项目布局的信息,因此我们可以从布局中移除项目视图。为了完成这个任务,我们需要选择视图右上角出现的菜单下拉菜单,并选择关闭标签。一旦完成,项目视图将不再在布局中。不用担心,如果您需要将其恢复,您总是可以通过进入窗口菜单并以我们介绍分析器视图相同的方式将其停靠。
现在我们可能还想要控制台消息,因为它们代表了我们的游戏会话的反馈。我们将通过选择窗口 | 控制台来介绍控制台。当控制台菜单出现时,我们将将其拖到层次标签的旁边,使其与层次标签在同一行显示为一个标签。这代表了 Unity 提供的另一种布局选项,即具有视图标签行的布局:

刚才发生了什么?
我们创建了一个专门用于分析应用程序的新布局并将其保存,这样我们就可以在准备深入调试和应用程序分析时简单地切换到它,而无需在仅仅设计游戏时使我们的环境变得杂乱。这对生产力有很大的影响,因为界面可以根据特定目的进行设置,例如关卡编辑、脚本编写或测试,并且您可以专注于具体需要的环境。虽然现在这可能看起来不是一个很大的细节,但随着您的项目变得越来越大,您会为这种灵活性的存在而感到高兴。
行动时间 — 保存新布局
-
现在我们已经创建了新的布局,我们需要保存它,以便以后可以重用。在窗口菜单中选择布局 | 保存布局:
![行动时间 — 保存新布局]()
-
您将看到一个简单的对话框,它会要求您输入布局的名称。在文本框中输入分析器并按保存:
![行动时间 — 保存新布局]()
-
现在您的布局已经保存,您可以通过选择窗口 | 布局并选择您的布局来随时切换:

刚才发生了什么?
我们刚刚保存了布局,以便我们可以在开发过程中稍后引用它。此外,我们可以通过给他们 Unity 存储的布局文件来与其他开发者共享我们的布局。
布局存储在以下文件夹中,使用.wlt 扩展名:
-
在 Mac OSx 上:
Users/username/Library/Preference/Unity/Editor/Layouts/ -
在 Windows 7 上
Unity 3:
c:\users\username\AppData\Roaming\Unity\Editor-3.x\Preferences\LayoutsUnity 2.x:
C:\users\username\AppData\Roaming\Unity\Editor\Preferences\Layouts -
在 Windows XP 上
Unity 3
C:\Documents and Settings\username\Application Data\Unity\Editor-3.x\Preferences\LayoutsUnity 2.x
C:\Documents and Settings\username\Application Data\Unity\Editor\Preferences\Layouts
最好的方法是部署一个真实的应用程序,从两个不同的布局中查看它,以了解它将如何改变你与环境交互的方式。这也是安装 Unity Remote 的好时机,因为我们希望在快速原型设计时使用它。
行动时间 — 部署 Unity Remote
在为 iOS 设备构建应用程序时,最难的事情之一是能够从设备获得一些实时反馈,同时仍然拥有丰富的开发环境来工作。Unity 通过 Unity Remote 应用程序解决了这个问题,它允许你在 Unity 中播放测试你的应用程序,同时使用 iOS 设备作为控制器。Unity Remote 通过通过 WiFi 将游戏流式传输到 iOS 设备,并从设备收集输入动作并将它们注入到 Unity 环境中来实现这一点。使用 Unity Remote,你可以避免每次更改时都必须构建和部署你的应用程序到你的设备。
当测试我们的应用程序时,Unity Remote 只有一个问题——我们需要为我们的设备专门构建它。
注意
记住,所有 iOS 应用程序在安装到设备之前都必须进行签名。
我们将逐步介绍生成可用于部署到 iOS 设备的 Unity 3 商业内容的必要步骤:
-
第一步是打开 Unity Remote 项目在 XCode 环境中。Unity Remote 源项目不在 Unity 的发行版中,需要从 Unity 网站下载。Unity Remote 是网站上的官方 Unity 扩展,可以在
unity3d.com/support/resources/unity-extensions/unity-remote下载![行动时间 — 部署 Unity Remote]()
-
一旦下载了此项目,请选择UnityRemote.xcodeproj以在 XCode 中打开此项目。正如我们在上一章中所做的那样,我们需要在iOS Provisioning Portal中为 Unity Remote 创建一个App ID:
![行动时间 — 部署 Unity Remote]()
-
在创建了我们的App ID之后,我们需要将此App ID输入到 XCode 项目中,以便应用程序将使用该App ID构建并部署到我们的 iPhone。虽然 Unity 会为我们处理这些步骤,但我们需要自己为 Unity Remote 执行这些步骤,因为这是一个常规的 XCode 应用程序。
注意
别担心,一旦我们安装了 Unity Remote,就不再需要这样做。
-
使用 XCode 中的项目菜单中的编辑项目设置打开项目设置。这将显示 XCode 将用于构建和部署您的应用程序的所有设置:
![行动时间 — 部署 Unity 远程]()
- 对于这个项目,我们关注两组设置:架构和代码签名。
-
在架构部分,我们想确保我们已经将项目的 Base SDK 设置为适合我们设备的适当版本。例如,如果我们的设备正在运行 iOS 4.0,我们想确保基础 SDK 没有设置为为 iOS 4.2 构建。只需选择 Base SDK 行上的下拉列表,XCode 就会告诉您配置的有效选项。如果 XCode 在这里没有显示 SDK,那是因为它没有正确安装,XCode 将无法使用它来构建。
-
在代码签名部分,您将在代码签名标识项下选择任何 iOS条目。当您选择此项目的下拉列表时,它将显示此项目的所有可能的代码签名选项。只需选择与您在 iOS 配置文件中创建的 App ID 相对应的选项即可。
-
更新了 App ID 和设备上的这些设置后,您现在可以从 XCode 的构建菜单中选择构建和运行来构建项目。这将使用 XCode 构建 Unity Remote 应用程序并将其部署到您的设备上。
-
确保在运行此命令时您的目标设备已连接,否则 XCode 会频繁地抱怨:
小贴士
如果您没有运行 Unity Remote 项目期望的标准 iOS 4.0 SDK,您将遇到特定的错误。

这个错误表示 Unity Remote 期望 iphoneos4.0 作为 SDK 已安装。这在工具栏中以及构建配置下拉菜单中的基础 SDK 缺失都有表示。根据您何时开始 iOS 开发,iphoneos4.0 可能已经是一个遥远的记忆。为了解决这个问题,您将不得不通过编辑 Unity Remote 的 Active Target 来调整项目的设置以匹配您已安装的 SDK:

小贴士
在架构部分,您可以更改基础 SDK 为所需的任何 SDK。通常,最佳选项是将此设置为最新版 iOS,除非您有特殊原因需要更改。更改此设置后,您会发现工具栏上的基础 SDK 缺失错误消失了,并且当您构建项目时,它将成功安装到您的设备上。

刚才发生了什么?
我们刚刚构建并部署了 Unity Remote 到我们的设备。这使我们能够将 iOS 设备作为游戏的输入,并从 iOS 设备上测试游戏的行为,而无需在设备上部署应用程序。这很有用,因为它将加快开发过程,并减少我们必须执行的代码-编译-部署循环的数量。
行动时间——使用 Unity Remote 测试我们的应用程序
现在我们已经部署了 Unity Remote,我们就可以开始使用我们的 iOS 设备作为游戏开发环境中的控制器了:
-
在你的设备上运行 Unity Remote,将出现一个列表,列出了准备好为 Unity Remote 提供数据的机器。如果你因为特定的 DNS 或 Bonjour 安全设置而无法出现,你可以通过选择左下角的按钮来输入你想要控制的机器的 IP 地址。如果你不关心在 iOS 设备上模拟游戏的视觉界面,你可以将显示图片单选按钮更改为关闭,这样游戏帧就不会显示在你的设备上,但你仍然可以使用 iOS 设备作为控制器在 Unity 中控制游戏:
![行动时间——使用 Unity Remote 测试我们的应用程序]()
-
在 Unity 工具栏中按播放。这将作为 Unity 和 Unity Remote 开始交换数据的信号。游戏视图中的内容将开始出现在 iOS 设备上,尽管你第一次看到它时可能会认为有问题,因为它看起来像是你的游戏的一个分辨率更低的版本:
![行动时间——使用 Unity Remote 测试我们的应用程序]()
Unity 中游戏的视图表示
- 如果你还记得,我提到 Unity 会将游戏流式传输到 iOS 设备。IDE 实际上正在将游戏中的视频流式传输到你的设备,所以你会看到各种压缩伪影,这取决于你的 Wi-Fi 连接速度和其他因素:
![行动时间——使用 Unity Remote 测试我们的应用程序]()
Unity Remote 上的游戏视图
-
这是 Unity Remote 的正常行为(也是选择“显示图片”单选按钮的原因之一),它不应该以任何方式影响你进行测试的能力。记住,这纯粹是一个测试工具,用于加速开发,因此视觉保真度不是必要的。如果你真的需要知道它将如何看起来,你可以将游戏部署到设备上——但请将这一章节作为书签,因为你会发现,经过一段时间,了解你的内容在设备上的外观所带来的好处将微不足道,与使用 Unity Remote 提高生产力的好处相比。
发生了什么?
Unity Remote 在幕后所做的是获取应用程序的帧缓冲区,将其压缩成视频流,并将其流式传输到 iOS 设备。然后,通过 iOS 设备收集的任何设备输入都会通过 Wi-Fi 传输到 Unity IDE,并用于指导环境中的对象。每当你在编辑器中的播放模式下时,你的设备将成为测试游戏的遥控器。
虽然这种方法对于快速应用开发非常有用,但重要的是要注意,使用这种方法的效果最多只能近似估计,你仍然需要不时在设备上构建和运行应用程序,以确认性能和游戏体验符合预期。同样,重要的是要注意,这种方法非常依赖于你的 Wi-Fi 连接。如果你的设备没有显示完整的 Wi-Fi 信号,你可以预期会有显著的性能影响。
我们刚刚完成了设置开发环境和发布内容到 Unity 所需的所有步骤。此外,我们使用 Unity Remote 建立了一个自己的小型测试实验室,这样我们就可以利用我们的设备,同时在开发环境中调试游戏。这是一个关键的里程碑,因为我们现在可以完全专注于定制 Unity 和构建游戏。
关于 Unity Remote 的最后一点值得注意,虽然我让你自己构建远程应用程序,但实际上你可以在 App Store 中下载它。考虑到这一点,你可能想知道为什么让我构建它?作为一个 iOS 开发者,即使是使用 Unity 的开发者,你会在很多情况下发现自己需要调试 XCode 底层的操作。此外,你可能希望集成 iOS 中 Unity 不支持的一些原生功能。在所有这些场景中,你都会发现自己需要挖掘底层的 XCode 项目,因此现在似乎是熟悉这些事情的最佳时机。
Unity Remote 链接:
-
Unity Remote 3 for iPhone :
-
Unity Remote 3 for iPad:
-
Unity Remote < 3 for iPhone:
快速问答 - 做这件事
-
Unity 视图可以在哪里显示?
-
a. 工具栏
-
b. 在不同屏幕上未分离
-
c. 在标签页中
-
d. 在 iOS 设备上远程操作
-
e. 在其他 Unity 机器上
-
-
你可以去哪里设置 iOS 设备的应用程序 ID?
-
a. 苹果开发者论坛
-
b. XCode 组织者
-
c. iTunes Connect
-
d. iOS 配置文件门户
-
e. XCode SDK
-
-
Unity 界面只能为一位用户和用例进行定制?(是/否)
-
Unity Remote 是否可以通过 3G 连接工作?(是/否)
-
您必须构建 Unity Remote 才能进行远程调试吗?(是/否)
摘要
在本章中,我们探讨了 Unity 环境并学习了如何针对特定目的进行自定义。
具体来说,我们涵盖了以下内容:
-
自定义 Unity 界面
-
自定义 iOS 部署
-
部署 Unity Remote
-
使用自定义布局和 Unity Remote 测试应用程序
虽然我们花了一些时间来回顾 Unity 界面,但我们是在相对较高的层面上进行的。为了更深入地了解 Unity 界面及其选项,建议您阅读 Unity 文档。
在掌握前两章内容后,我们现在可以告别预构建的项目,开始从头开始构建游戏,这正是下一章的主题——Hello World。
第三章。你好,世界
在本章中,我们将从头开始构建一个新的项目,并生成第一个由读者创建的应用程序,我们可以在我们的 iOS 设备上运行。虽然我们之前已经尝试过将应用程序部署到我们的设备,但这些是预构建的应用程序。现在是时候摘掉训练轮,测试这个环境了。
在本章中,我们将:
-
编写我们的第一个场景
-
在编辑器中测试我们的应用程序
-
自定义 iOS 设置
-
将此应用程序部署到设备
这才是真正的乐趣所在...
编写我们的第一个场景
在我们的第一个应用程序中,我们将遵循软件开发传统,创建典型的第一个程序“Hello World”。由于我们使用 Unity 来创建游戏,创建一个 3D 世界并将其部署到我们的设备似乎是有意义的。完成之后,我们应该能在我们的设备上看到地球:

我们“Hello World”游戏的最终输出
从基础知识开始
对于我们的第一个游戏,我们将从基础知识开始。
-
创建场景
-
创建示例对象
-
自定义示例对象
-
控制摄像机
-
部署到 iOS 设备
行动时间 — 创建场景
-
我们的第一步是创建一个新的 Unity 项目,通过选择 文件 | 新项目。在项目目录中指定你希望 Unity 创建此新项目的位置,然后按 创建项目 按钮:
![行动时间 — 创建场景]()
-
Unity 将常见的脚本、资产和其他可重用功能打包成可分发文件,这些文件被称为 Unity 包。这些单个文件归档以
.unityPackage扩展名结尾,并代表 Unity 中共享的主要机制。对于这些第一个应用程序,我们不会将任何包导入 Unity,所以请不要担心选择任何包。大多数游戏都被分成场景或级别,Unity 就是围绕这个概念设计的。在最基本的状态下,场景只是一个容器,指向所有的艺术资产、脚本、行为等等。当然,你可以把所有内容都放在一个大的场景中,但这更适合非常小的游戏或那些从互联网上流式传输所有内容的游戏。考虑到大多数 iOS 设备的约束以及即使是 3G 互联网连接的速度,这样做是不切实际的,所以我们将专注于基于级别的设计。
-
-
当 Unity 创建我们的项目时,它为我们创建了一个默认场景,只需保存即可。Unity 将将所有场景放入项目的
Assets文件夹中。将场景保存为 level1,你会在 项目 视图中看到新的级别表示:

- 一旦你开始创建多个场景,你只需在 项目 视图中双击场景,它就会更改 场景 视图和 游戏 视图以表示新的状态。
刚才发生了什么?
我们刚刚为我们的游戏创建了一个简单的场景。目前它非常空旷,但已经准备好填充演员、剧本和其他功能,成为一个真正的游戏。
行动时间——在场景中创建对象
接下来,我们需要在我们的场景中创建一些对象,否则我们的游戏将会非常无聊。我们需要创建的第一个对象是代表地面的东西。是的,我知道太空中没有地面,但为了说明一些可用的功能,我们需要地面。
-
在主菜单中,通过选择GameObject | 创建其他 | 平面来创建一个新的平面。
-
我们还希望给这个平面一个独特的名称,所以请在检查器视图中通过选择平面文本并将其替换为地面来将对象的名称从平面更改为地面。Unity 现在也将把这个对象称为地面。这将在我们稍后尝试对地面平面进行操作时很有用:
![行动时间——在场景中创建对象]()
-
在检查器中,通过将缩放更改为(100, 1, 100)来增加我们地面平面的尺寸。这应该给我们提供足够的空间来移动:
![行动时间——在场景中创建对象]()
-
Unity 3 的新特性之一是能够从摄像机的视角快速渲染出世界的样子,而无需玩游戏。在层次结构视图中选择主摄像机对象,你将在场景视图中看到一个相机预览窗口,它显示摄像机所看到的内容。这个窗口也是一个实时预览,因此当场景中的对象发生变化时,相机预览将更新以反映这些变化:

刚才发生了什么?
我们刚刚为我们的“Hello World”场景创建了一个简单的地面物体,并探讨了如何使用相机预览来预览世界。
行动时间——光之出现
在 Unity 中,你最重要的资产之一是光照。有了正确的光照效果,你的游戏可以从看起来很普通变成革命性的。Unity 拥有一个非常复杂的照明引擎,可以处理动态光照、使用 Beast 照明系统烘焙光照,以及延迟光照。
-
目前,我们将通过选择创建 | 点光源来创建一个基本的光源,这将向我们的场景添加一个点光源游戏对象:
![行动时间——光之出现]()
-
现在你已经创建了一个点光源,让我们将其放置在场景上方,以便它能够反射到其他对象上。在检查器中,将位置更改为(0,5,0)。这应该将其放置在场景上方:
![行动时间——光之出现]()
-
通过按快捷键 F 将编辑器中心对准这个光源,让我们聚焦于这个对象:
![行动时间——光之出现]()
-
为了确保我们的灯光足够亮,让我们将强度设置为4。你可以使用滑块,或者直接在滑块旁边的文本框中输入4:
![行动时间 — 让有光]()
- 你还可以看到,由于场景中现在有更多的光线,光线的强度已经增加:
![行动时间 — 让有光]()
-
选择主相机,你将看到一个相机预览,显示光线照在地面平面上:

刚才发生了什么?
我们已经为场景添加了一些灯光,这样我们就能看到场景中发生的事情。记住,如果没有灯光,玩家将看不到太多东西。虽然我们在这里创建了一个简单的光照模型,但你将看到,光照可能是你可以添加到你的游戏中的最重要的功能之一,因为它将为你的对象提供大量的视觉保真度。
行动时间 — “Hello World”
如果我们没有在“Hello World”章节中实际放入世界,那么这个章节有什么用呢?
-
在层次视图中,通过创建 | 球体创建一个球体。
-
在检查器中将这个游戏对象重命名为世界。
-
让我们将它稍微移出中心,以便我们可以看到灯光对这个球体的影响,将其位置设置为(-2, 2, 0)。虽然这很有趣,但这并不太像我们所处的世界:
![行动时间 — “Hello World”]()
-
在
第三章文件夹中,你会找到一个名为earth.jpg的文件。选择项目视图,并将这个纹理直接拖动到项目视图中。Unity 环境可能会暂停片刻,因为 Unity 正在导入这个文件。 -
在项目视图中选择地球纹理,你将看到这个纹理的详细信息:
![行动时间 — “Hello World”]()
-
如果我们能够将这个纹理直接应用到我们的球体上,并渲染出我们的世界,那将非常不错。Unity 的开发者也认为这会很好。因此,你可以将地球纹理从项目视图拖动到场景视图中的我们的球体上,它将自动为其添加纹理。
-
选择场景视图并检查我们场景的渲染。我们越来越接近真正的“Hello World”:

刚才发生了什么?
我们已经为我们游戏添加了第一个演员。在这种情况下,我们的演员是一个非常简单的带有纹理的球体模型,但我们刚刚执行了将对象放入 Unity 场景的基本步骤。
行动时间 — 控制相机
这一切都非常有趣,但我们离我们的创作相当远,而且我们没有移动场景的能力。通过传统的 iOS 设备,我们可以通过访问设备内的加速度计来完成这项任务:

Unity 将加速度计的所有功能封装到 Input 类中,我们可以使用其属性和方法来连接到加速度计并确定设备上的情况。
小贴士
来自 Unity iPhone 的用户会注意到,所有 Unity iOS 的输入都封装在 Input 类中。不再需要使用 iPhoneInput 类。
-
在 项目 视图中,通过选择 创建 | JavaScript 创建一个新的 JavaScript 脚本。这将创建一个名为
NewBehaviourScript的新脚本。 -
双击此脚本将在 Unity 的默认编辑器 Unitron 中打开脚本。当我们深入研究脚本时,我们将介绍其他 Unity 提供的用于开发脚本的工具,即 MonoDevelop。
-
在脚本窗口中,我们只需在
Update函数中输入脚本的代码:![行动时间 — 控制相机]()
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件:
www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册以直接将文件通过电子邮件发送给你。- 脚本通常必须附加到游戏对象才能执行任何有用的操作。在我们的例子中,我们想要移动相机,因此需要将此脚本附加到相机上。在 Unity 中有两种方法可以实现这一点。与纹理化一样,你可以直接将脚本拖放到相机上。
-
在 层次结构 视图中选择 主相机 对象。
-
然后,在 项目 视图中,将脚本从 项目 视图中拖动到 检查器 视图中的相机或 层次结构 视图中的对象上。
完成此任务的另一种方法是,在 层次结构 视图中选择 主相机,然后,在 Unity 菜单栏中选择 组件 | 脚本 | 新行为脚本。
在任何情况下,你会发现 Unity 已经将你的脚本添加到了相机中:

刚才发生了什么?
我们刚刚探讨了在 Unity 环境中如何处理脚本。你的游戏功能中的每一部分都将通过脚本实现,而你刚刚也看到了添加这些行为是多么简单。
行动时间 — 部署到 iOS 设备
现在我们已经创建了一个有用的应用程序,是时候将我们的新作品部署到我们的 iOS 设备上,并使用加速度计来探索我们的场景。因为我们之前已经运行过这个过程,所以我们可以从高层次上概述这些细节,但再次走一遍这个过程是很重要的,以确保过程感觉正确。
-
我们首先需要确保我们正在部署正确的应用程序类型。当 Unity 最初创建我们的新项目时,它是为桌面部署创建的。我们需要在构建设置(文件 | 构建设置)中更改这一点:
![执行操作 — 部署到 iOS 设备]()
-
在这里的一个重要步骤是确保我们想要包含在设备构建中的任何场景都包含在内。
-
点击添加当前按钮,我们的 level1 场景以及该场景所需的全部资源都将包含在为设备打包的应用程序中。
-
选择 iOS 平台作为目标平台,然后点击切换平台按钮。Unity 将暂停片刻,并对项目进行必要的更改,以便它可以部署到新的目标平台:
![执行操作 — 部署到 iOS 设备]()
- 完成此操作后,唯一剩下的事情就是更改项目的构建设置,以便 Unity 可以与 XCode 通信并构建用于部署的应用程序。
-
与之前一样,我们需要创建一个App ID并将配置文件部署到我们的开发机器上,以便 Unity 可以完成此操作。进入iOS 配置文件门户并为我们的新应用程序创建一个新的App ID。虽然我们可以使用之前使用的相同App ID,但请记住,这样做会替换我们刚刚构建的应用程序:
![执行操作 — 部署到 iOS 设备]()
-
接下来,我们需要创建另一个配置文件,以便我们可以将应用程序部署到设备:
![执行操作 — 部署到 iOS 设备]()
-
一旦创建了配置文件,您需要下载配置文件,以便 XCode 可以将应用程序部署到您的 iOS 设备上:
![执行操作 — 部署到 iOS 设备]()
-
双击配置文件将打开 XCode 的组织者并将配置文件部署到您的设备:
![执行操作 — 部署到 iOS 设备]()
-
完成此操作后,我们只需通知 Unity 应使用哪个App ID,通过在包标识符中输入该App ID即可:
![执行操作 — 部署到 iOS 设备]()
小贴士
使用我们自己的图标而不是使用库存的 Unity 图标会更好。您使用的图标应该是您 unity 项目中的一个资源,可以是您想要的任何纹理。只需将那个纹理拖放到默认图标框中,Unity 就会完成剩余的工作。
-
完成所有这些步骤后,我们最后需要执行的操作是调用 Unity 的构建过程,以便它可以使用 XCode 编译我们的游戏并将其安装在手机上。我们可以通过文件 | 构建 & 运行来完成此操作。
-
在您完成这些步骤后不久,Unity 将开始在您的 iOS 设备上运行。
记住我们激活了加速度计,所以一旦我们的游戏开始,我们就会开始在场景中移动。
刚才发生了什么?
我们刚刚从头开始构建了我们的第一个游戏应用。目前它做不了太多事情,但我们已经打开了为 iOS 平台构建优秀游戏的大门。这也应该让你对如何通常与 Unity 环境交互有一个感觉。Unity 是一个沙盒,你可以添加各种资产,然后创建脚本,为这些资产赋予行为。
摘要
我们构建了一个实际上值得在本章向人们展示的应用。虽然它不太可能带来数百万美元的销售收入或获得名人地位,但这对于在 Unity 中开发游戏来说是一个关键的里程碑,因为我们刚刚接触到了许多游戏开发工作流程。
现在我们已经构建了第一个场景,我们需要浏览一下 Unity 的核心概念,因为没有对 Unity 工作原理的清晰理解,我们将很难真正利用这个环境。Unity 概念将成为下一章的基础。
第四章:Unity 概念
现在我们已经构建了我们的第一个应用程序,并且对 Unity 的能力有了初步的了解,是时候学习 Unity 的核心概念了,这样我们才能构建更加复杂和引人入胜的游戏。我们已经简要讨论了一些核心概念,但为了构建引人入胜的内容,有必要更深入地理解 Unity 如何使用这些概念。
在本章中,我们将:
-
学习 Unity 的核心概念
-
在工具内创建核心组件
Unity 开发的基本概念
在学习一门外语时,您将花一些时间沉浸在其名词和动词中,以便理解概念并开始构建句子。
资产
资产代表通过它可以构建 Unity 场景的原子 Unity。您将带入环境中的任何东西都被视为资产,包括声音文件、纹理、模型、脚本等等。
您可以通过从桌面拖放它们到项目视图,或者使用导入新资产菜单(资产 | 导入新资产)将资产导入 Unity 项目。一旦在项目视图中,资产就是可以在您的游戏中使用的资源。将项目视图想象成您的调色板,您将从其中绘制您的世界。
Unity 通过文件系统管理资产。如果您进入您的项目文件夹并查看Assets目录,您将找到项目中所有的资产。当 Unity 导入您的资产时,它将在这里存储导入的资产及其所有元数据。
注意
重要提示:您绝对不应该尝试直接管理此文件夹中的资产。
虽然您可以使用各种工具更改纹理、音频、模型等,但您绝对不应该移动它们,因为这会破坏资产与 Unity 元数据之间的链接。实际上,Unity 将表现得好像您刚刚第一次将此资产带入 Unity,任何关系、设置等等都需要重新创建。
行动时间 — 导出资产包
有时候,您可能希望与团队中的其他用户或社区中的其他用户共享一组资产。Unity 通过提供一个简单的机制,称为包,来简化这一过程。包只是将资产捆绑在一起的单个文件。
在这个练习中,我们将探讨如何从现有项目中与其他开发者共享资产:
-
打开本章的项目HelloWorld2。在其中,您将找到一个包含一些额外资产的原始 Hello World 演示版本。
-
选择您想要作为包一部分导出的资产:
- 在上面的图中,项目中的材质元素已被选中。有小狗和草地材质,它们是材质元素的子元素,并将被捆绑在包中。如果您只想共享小狗材质,您只需选择小狗元素,只有那些材质会被打包。
-
接下来,让我们使用导出包菜单(资源 | 导出包)创建一个包。当选择时,这将创建一个从项目视图中选定的项目开始的包,以及任何是该元素子元素的内容。
-
Unity 将显示一个对话框,其中包含所有将要导出的资源。如果您想删除某些资源,取消选中其复选框:
![操作时间 — 导出资源包]()
- 此外,还有一个标记为包含依赖项的复选框。如果您的资源依赖于其他资源,例如脚本,保持此复选框选中状态将告诉 Unity 将这些资源包含在包中。
-
选择导出按钮,然后在随后的文件对话框中为 Unity 包文件提供名称。Unity 将创建并压缩资源到一个扩展名为
.unitypackage的文件中:![操作时间 — 导出资源包]()
刚才发生了什么?
我们创建了一个 Unity 包,其中包含了我们选定的所有资源。这是与您的团队共享资源的理想方式,因为当这些资源被导入时,任何设置、层次结构等都将被保留。
操作时间 — 导入资源包
越来越多的第三方公司正在为 Unity 开发者提供专门为 Unity 打包的资源,包括可以从 Unity 产品内部访问的 Unity 资源商店。所有这些解决方案都通过在“操作时间 — 导出资源包”部分描述的相同.unitypackage格式提供内容。使用 Unity 资源商店的资源,将资源包含到您的项目中的过程是自动的。然而,有时您可能从其他人那里收到了资源,需要自己将它们导入到项目中。
-
在 Unity 中创建一个新的项目,命名为项目导入。
-
使用从“操作时间 — 导出资源”构建的
.unitypackage文件,通过将.unitypackage文件拖放到您项目层次结构中希望它们存储的位置的项目视图中,或者通过使用导入资源菜单(资源 | 导入资源):![操作时间 — 导入资源包]()
-
一旦执行,你会发现 Unity 已经将
.unitypackage文件导入到项目视图中,但它并没有将资产展开成我们之前熟悉的层次结构。Unity 不知道你是否想要包中的所有资产,它给你提供了选择的机会,而不是用一个未知的资产列表污染你的项目。 -
双击
.unitypackage文件,让 Unity 将其展开成其组成部分。你将看到一个对话框,列出了包中的所有内容,以及一条注释,告诉你资产是否为新资产。这在跨项目共享资产时很有用,特别是当资产可能具有相同名称时。
-
选择全部按钮,让 Unity 包含包中的所有资产:
![执行动作 — 导入资产包]()
-
观察项目视图,你会发现你选择的全部资产都已包含在项目中,并且资产的层次结构已被保留:
![执行动作 — 导入资产包]()
注意
尽管我们的包中没有库元素,或者在我们的 Puppy Test 项目中没有,Unity 还是创建了这种层次结构,并相应地导入了资产。这种信息以及与之相关的设置,如果手动管理
Assets文件夹,将会丢失。 -
由于我们已经提取了我们感兴趣的资产,所以从项目层次结构中删除
unityproject文件。
刚才发生了什么?
我们刚刚将资产导入到一个新项目中,保留了从原始项目中资产的原有层次结构。
虽然我们选择了从unityproject导入所有资产,但这并不是必须的。你可以预览你想要导入的资产,并取消选中那些你不想包含在项目中的资产。
游戏对象
通常当你想到游戏对象时,你会想到一个动画士兵、粒子系统或游戏中的精灵。然而,游戏对象更是一个抽象的概念,是一个没有行为或功能的基础对象。游戏对象通过添加行为来获得执行功能的能力。实现这一点的途径是装饰器设计模式,它允许向某个基础类添加任意数量的行为。这些行为,在 Unity 术语中被称为组件,是赋予游戏对象真正意义和定义的东西。正是通过组件,游戏对象才能够在场景中存在,并具有位置、旋转和缩放。
组件
组件是 Game Object 的装饰器,为资产提供额外的行为和配置。渲染、物理、音频、照明等许多核心原则都是通过向对象添加组件来实现的。因此,我们将涵盖的许多 Unity 概念都有计算机图形学定义和组件,当它们附加到 Game Object 上时提供行为。通过一些示例来展示这个概念是最好的方法。
你可能会认为拥有光的最简单方法就是有一个在系统中定义的具体光类型。然而,假设你想要一个普通对象,比如一艘星际飞船的引擎,具有发光的能力?当然,你可以创建一个光对象并将其与引擎模型关联,但假设你还想让它冒烟并受到物理影响,以及播放声音?你立刻就会注意到你的场景会被一些与游戏无关的对象污染——尽管它们会出现在你的层次结构中,你还得管理它们。更优雅的方法是说明你有一个星际飞船引擎,并且它具有某些行为。这些行为是我们将与对象关联的组件。
即使是我们的标准 Game Object,也是通过变换组件获得位置、缩放和旋转的能力。在 Unity 的检查器视图中,你实际上看到的是应用在这个 Game Object 上的组件堆栈。
让我们再举一个例子。
行动时间 — 向游戏对象添加组件
-
在 Unity 中创建一个空的游戏对象:
![行动时间 — 向游戏对象添加组件]()
- 现在我们给这个 Game Object 添加一些组件。
-
在组件菜单中,你可以找到可以添加到这个 Game Object 的所有组件。在渲染子菜单中,选择光选项:

刚才发生了什么?
在 Unity 中,你会注意到你的 Game Object 现在有一个与之关联的光行为:

如果你从层次结构菜单创建一个常规光对象,你会发现这两个对象是相同的。这是因为光Game Object 只是一个带有光组件的空 Game Object——简单地根据你想要创建的光的类型进行了适当的配置。如果你探索一下,你会发现你可以像库存光对象一样配置你的光。
所有这些都应该向你展示出你所拥有的可能性。你并不受限于 Unity 开发者任意构建的对象。实际上,你也可以创建自己的自定义组件,并将这些组件添加到任何你想要的 Game Object 中。
变换
变换是三维空间中某物的基概念。它表示对象的定位、旋转和比例尺,并且可以包含对层次结构中子对象的链接。场景中的每个对象都有一个这样的变换,即使没有任何内容被渲染。变换组件是每个游戏对象都具有的基本组件,也是您不能移除的,因为它是对象存在于三维空间中所需的:

行动时间 — 定位、旋转和缩放游戏对象
-
创建一个立方体游戏对象。
-
通过将变换组件的 X 值更改为10来更改立方体的位置。
您会注意到场景中立方体的位置反映了这个新位置。
-
将立方体沿 X 轴的旋转更改为45。
您现在已将立方体绕 X 轴旋转了 45 度。
-
将立方体的变换组件的 X 轴比例尺更改为10。
您尚未沿 X 轴缩放对象。
发生了什么?
您可以通过更改对象的位置属性来细化场景中对象的位置。这些属性表示该对象在世界空间坐标中的位置 — 场景本身的坐标系。此外,您可以通过调整对象的旋转属性来改变对象的朝向。最后,您可以通过更新缩放属性来为对象定义一个任意比例尺:

相机
相机提供了用户可以通过它查看场景内内容的视口。相机将捕获其视锥体内的所有内容,并将这些内容投影到帧缓冲区,以便在 2D 显示上进行光栅化。视锥体定义了一个几何形状,用于在场景中裁剪游戏对象。如果一个游戏对象不在视锥体内,相机会假设它对用户不可见,并且不会浪费周期去尝试绘制它。这个概念很重要,在我们稍后讨论遮挡剔除时将变得更加重要。现在,只需注意,近裁剪面、远裁剪面和视野定义了这个对象的形状,并且所有这些设置都可以在相机组件中轻松配置:


相机属性
相机组件有几个属性定义了当场景渲染时用户将能看到多少世界。
近裁剪面
近裁剪面定义了一个平面,使得靠近相机的对象不再显示。此设置通常将近裁剪面放置得相当靠近相机,以便对象可以靠近'观众'而不会被裁剪。
远裁剪面
远裁剪平面定义了一个平面,使得距离这个平面以外的物体太远,不应该被绘制。你为这个设置的选择将对游戏性能产生相当大的影响。虽然你可能能够将这个设置延伸到很远的距离,但请意识到那些远处的物体正在被渲染并消耗资源,就像它们更近一样。所以如果你将远裁剪延伸到你的虚拟地平线,那里有一个城市,但城市的大小只有几个像素,你仍然会绘制出这个城市的全部辉煌,并为此付出性能上的代价,尽管它几乎看不见。
视野(FOV)
视野定义了视口沿 Y 轴的宽度。这将影响物体透视的计算,并导致物体根据此设置变得更高/更胖:

相机投影类型
你可以将任何 Unity 相机设置为两种投影类型之一:正交投影和透视投影。这些投影决定了相机所看到的世界的渲染方式。
正交投影
正交投影的相机从相机视图中移除了透视校正,并确保无论物体距离相机有多远,其距离都保持不变。当我们构建传统的二维用户界面时,正交相机将非常重要,对于二维游戏尤其重要。
透视投影
透视相机考虑了游戏对象与观察者之间的距离,并据此应用透视校正,使得远离观察者的物体看起来比实际更小。对于大多数游戏来说,这是绘制内容的主要机制。
需要注意的是,在 Unity 中没有限制要求你在场景中使用一种相机投影类型而不是另一种。实际上,使用多个相机用于不同目的相当常见。例如,你可以有一个负责绘制用户界面元素的正交相机和一个负责常规场景元素的透视相机。
灯光
灯光在概念上很简单。它们照亮场景中的物体,以便它们可以被看到。没有灯光,物体看起来会显得黑暗和没有生机。有了灯光,你可以为场景增添氛围,并通过它们投射的阴影为世界增加深度。Unity 中有三种主要类型的灯光:方向光、点光源和聚光灯。
方向光
方向光是一种与场景距离无限远的灯光。虽然你可以在 Unity 中定位它,但场景中物体的照明仅使用光的方向来确定。方向光是你可以在场景中使用的最便宜的灯光。这是进行全局场景照明的最快方式,因为灯光的位置不会影响场景中某个物体的照明程度,只有光的方向。
点光源
与方向光不同,点光源是一种在场景中具有位置但没有方向的光源——在所有方向上均匀地发射光线。这种光具有平均的计算复杂度,通常用于游戏中大多数效果。
聚光灯
最昂贵的灯光类型,聚光灯模拟了你从手电筒中得到的效果——一个光锥。这个锥形也有衰减,使得锥形中心的灯光比锥形边缘的灯光更亮。虽然可能有一些情况下你可能想使用聚光灯,但在 iOS 设备上这样做的高成本使得这变得非常具有威慑力。
光照贴图
虽然你可以通过接近真实照明的灯光完成许多事情,但要这样做需要非凡的计算能力。在考虑移动设备的计算和图形能力时,实时计算灯光是确保我们的游戏和应用性能不佳的可靠方法。这可以通过使用光照贴图来纠正。
光照贴图最好描述为照亮场景并烘焙这些灯光对场景的影响。由于与材质纹理混合光照贴图在计算上更便宜,我们可以通过光照贴图实现通常需要更昂贵灯光的功能。幸运的是,Unity 通过行业领先野兽光照贴图系统内置了对光照贴图的支持。
使用野兽,你将能够像通常一样在场景中放置灯光,野兽将计算这些灯光对场景中纹理的影响,并为我们创建光照贴图。我们可以快速且低成本地获得卓越的结果。
灯光和野兽光照贴图将在光照章节中更详细地介绍。
声音
声音是游戏中经常被忽视的部分,有助于为玩家设定氛围。观看你最喜欢的电影(不是无声电影),关闭声音。你会发现,当声音打开时,体验并不像声音关闭时那么吸引人,如果你非常了解这部电影——你会发现你的大脑试图通过从记忆中恢复电影中的对话和音乐来填补空白。声音对于参与度至关重要,Unity 拥有一系列音频功能,涵盖了整个频谱。
音频监听器
在现实世界中,声音必须被听到,如果你离声音的发射源太远,你就听不到任何声音。Unity 通过音频监听器处理这种听到声音的概念。这个游戏对象通常附着在玩家的头像、角色或其他场景中的表示上。音频监听器在游戏世界中充当麦克风,它所听到的任何声音——玩家通过扬声器听到。
默认情况下,音频监听器放置在场景中的主摄像机上。如果你想用户从其他对象的视角听到声音,只需将音频监听器从主摄像机移除,并将其附加到你希望代表用户听觉中心的游戏对象上。在 Unity 中,你不能同时激活多个音频监听器。虽然音频监听器是一个组件,但它没有任何可配置的属性。
音频源
音频源代表游戏世界中制造噪音的事物。你可以通过选择游戏对象并选择组件 | 音频 | 音频源来向游戏对象添加音频源。这将向你的游戏对象添加音频源组件:

你还会看到在场景视图中,你的游戏对象上有一个扬声器图标。这表示这个游戏对象上有一个音频源组件,并生成声音:

音频剪辑
虽然我们已经创建了一个音频源,但我们还没有将其与实际的声音关联起来。这个声音就是 Unity 所说的音频剪辑。Unity 支持几乎所有可以编码声音的格式,包括.aif, .wav, .mp3, .ogg,甚至还有.xm, .mod, .it和.s3m这样的跟踪器格式。
添加音频剪辑的时间
-
从
第四章的项目文件夹中选择dog_1.mp3文件,并将其拖动到项目视图中。 -
音频剪辑将被导入 Unity,并出现在带有扬声器图标的项目视图中:
![添加音频剪辑的时间]()
-
在项目视图中选择音频剪辑。检查器将填充所有音频剪辑的设置:
![添加音频剪辑的时间]()
-
创建一个立方体游戏对象。
-
将一个音频源添加到立方体游戏对象上。
-
在音频源游戏对象中,选择循环复选框以指定我们希望这个声音无限期地播放。
-
在音频源属性右侧有一个小圆按钮。按下它以打开资产浏览器。
-
从浏览器中选择dog_1资产:

刚才发生了什么?
你刚刚给一个对象添加了声音。现在当你播放场景时,如果你足够接近那个游戏对象,你将听到声音。如果你遇到麻烦,请确保在音频源中勾选了唤醒时播放。
脚本
当你在 Unity 中创建一个脚本时,你正在创建一个新的行为,该行为将被附加到一个游戏对象上,并且当 Unity 在渲染管线阶段与游戏对象通信时,该行为将被调用。Unity 随带了一些预定义的脚本,用于执行基本操作,如 FPS 控制、iOS 游戏手柄控制等。然而,你会发现,对于你想要做的许多事情,你将需要自己创建脚本。
Unity 支持使用 Javascript、C# 和 Boo 作为主要脚本语言进行脚本编写,但鉴于 Mono 可以与任何 .Net 语言一起工作,你完全可以使用 F# 或甚至 Java 来编写脚本。然而,你可能发现,由于这些路径没有得到官方支持,自支持自定义路径的成本可能不值得付出努力。
在内部,Unity 使用 Mono/.Net 编译路径来编译你项目中的所有脚本。Unity 框架中 C# 和 Javascript 脚本的基础语言实现都是扩展了 Boo 的功能。你会发现创建新的脚本、从 .Net 程序集导入功能,甚至通过直接与构建阶段生成的 抽象语法树 (AST) 接口来扩展语言都非常容易。在构建阶段,Unity 将导致所有脚本被静态编译,嵌入到你的应用程序中,并部署到设备上。
编辑器
对于使用 Unity 进行脚本编写,有两个可用的编辑器:Unitron 和 MonoDevelop。
Unitron 是随 Unity 一起提供的原始脚本编辑器,默认情况下会处于激活状态。它轻量且快速,但缺乏传统 IDE 的所有现代便利功能。随着你的项目变得越来越复杂,你会发现拥有一个强大的 IDE 是必不可少的,并且可能想要迁移到一个更专业的编辑器:

MonoDevelop 在 Unity3 中被引入,作为开发者所需的一种更高生产力的选项,它支持诸如代码补全、更好的键绑定支持、插件、重构、度量等功能。MonoDevelop 是一个与 Mono 一样历史悠久且开源的解决方案,由于它在 Unity 所使用的所有平台上都得到支持,因此它成为开发的一个理想选择:

要更改你使用的编辑器,进入 Unity 首选项(Unity | 首选项),然后在 常规 选项卡中将 外部脚本编辑器更改为你选择的编辑器。如果你想使用 MonoDevelop,你可以在 /Applications/Unity 文件夹中找到它。选择它后,当你双击 Unity 任何语言中的一个脚本时,编辑器将启动并允许你更新脚本。你对脚本所做的更改和保存将立即对 Unity 可用,并且脚本中的任何错误将立即在 Unity 控制台视图中显示:

我们将在下一章中更详细地介绍脚本。
预制体
预制体是一组可重复使用的网格、脚本、动画等,你希望在场景中使用。预制体是任何 Unity 项目的核心组件,但在开始时,可能并不立即清楚为什么你想采用它们。
让我们假装这是我们热门卖家“红色方块必须死”中的敌人,我们需要添加更多的红色方块。我们不想走所有步骤来尝试再次创建这个方块。你的第一个想法可能是复制/粘贴方块,这会起作用。但现在你决定,与其是红色,你想要红色火焰纹理。你会回到场景中的所有方块并更改纹理吗?这可能适用于少量对象,但想象一下在一个复杂的场景中这样做。
更糟糕的是,假设你刚刚导入了大量网格数据,并在你的横版游戏中为僵尸设置了纹理。你将其缩放到适合你的场景,并确保所有动画都能正常工作。然而,这只是你计划放入这个关卡中的许多僵尸之一。当然,你不想为游戏中每个将要出现的僵尸都这样做。
动手实践 — 创建预制体
-
通过选择文件 | 新场景在项目中创建一个新的场景。
-
保存场景。
-
创建一个方块预制体。
-
通过在资产菜单中选择资产 | 创建 | 材质创建一个新的材质。
-
在项目视图中选择材质。
-
在检查器中,选择主颜色选择器,并将材料的颜色设置为红色。
-
通过将材料拖动到层次结构视图中的方块上,将材料分配给方块:
![动手实践 — 创建预制体]()
-
通过选择创建 | 预制体在项目视图中创建一个新的预制体:
![动手实践 — 创建预制体]()
-
让我们将其重命名为EvilBox,这样我们就可以知道当我们使用这个预制体时,我们正在创建必须死的红色方块:
![动手实践 — 创建预制体]()
-
接下来,我们将把之前创建的红色方块拖动到EvilBox预制体上。当你这样做时,你会注意到方块和EvilBox预制体都高亮显示为蓝色。这表示预制体现在具有原始对象的全部设置:
![动手实践 — 创建预制体]()
-
从层次结构视图中删除原始方块。
-
将EvilBox预制体拖入场景几次,你会在层次结构视图中注意到创建了新的对象。这些新的游戏对象是原始预制体模板的实例:

刚才发生了什么?
我们已经为我们的游戏创建了一个可重复使用的预制对象。这个预制对象具有定义良好的外观和行为,可以重复使用。更重要的是,如果您需要更改EvilBox,因为有人告诉您真正邪恶的是蓝色的盒子,您可以选择预制件并更改其设置,所有实例都将相应地更改,因为预制件的每个实例都与其模板属性相关联。
如果出于某种原因,您以某种方式更改了预制实例的一个实例,该实例将从蓝色变为默认的灰色颜色。这表示预制件不再代表实例。但假设您想将实例的设置用作新的默认设置?要完成此操作,将您更改的子实例从层次结构拖动到预制件上,您将看到预制件的设置已更新为实例的设置,并且实例本身现在再次与预制件链接。
场景
场景是所有动作发生的地方。从 Unity 的角度来看,场景和级别这两个词是同义的,有时工具会交替使用这两个术语。您可以从文件菜单创建新的场景:

创建并保存后,场景将在项目视图中显示,并在其旁边有一个 Unity 图标。如果您在寻找场景,可以在项目的Assets文件夹中找到它,文件扩展名为.unity。
摘要
在本章中,我们学会了用 Unity 的语言说话。您现在掌握了所有核心概念及其使用方法,因此我们可以从新手立方体示例毕业,承担更复杂的项目。
现在我们已经了解了核心概念,我们准备构建我们的第一个真正复杂的应用程序,该应用程序将利用我们在脚本中涵盖的所有概念,这是下一章的主题。
第五章。脚本:到底是谁的台词?
我们终于走过了基础阶段。如您所观察到的,我们一直在使用的任务和资产变得越来越复杂。现在我们已经触及脚本,现实将变得具体。脚本就是您赋予创作生命并使其成为实际游戏的方式。在此之前,我们最好的做法是通过脚本相机在无生命的世界上移动——现在我们可以玩游戏了。
在本章中,我们将学习:
-
Unity 如何处理脚本
-
iOS 的特定脚本限制和接口
-
Unity 提供的基本 iOS 脚本
-
将数据集成到网络服务中保存
脚本可能是您需要了解的最重要的东西,以便有效地与 Unity 交互。虽然您可以优化您的资产,高效地构建场景,并利用设备上的每一个功能——但如果您没有正确处理脚本,它将不会做任何有用的事情。
那我们就开始吧...
重要的预备知识
本章假设您有编程背景,并且对 JavaScript、C# 或 Boo 有所了解。由于这些是 Unity 支持的脚本语言,对这些语言的设计有一定的了解是必要的。
Unity 脚本入门
Unity 毫无疑问是任何价格范围内最灵活和最完整的脚本系统之一。它为开发者提供了使用 Boo、JavaScript 和 C# 等语言的能力,Unity 在支持各种开发者方面覆盖了所有基础。虽然系统内没有对 Java 的具体支持,但 C# 是一种成熟的语言,它提供了与该语言近似的支持。此外,通过 .Net/Mono 框架,您可以自行支持 Java、F# 或任何受 .Net 平台支持的其它语言,包括您创建的任何新语言,尽管使用 Boo 构建领域特定语言(DSL)可能更容易。最重要的是,在 Unity 中,每种语言都是一等公民。在少数例外的情况下,您在 Unity 中想要做的任何事情都可以完成,无论您选择哪种语言。
Unity 脚本系统是完成 Unity 内部任务、自定义 Unity 编辑器和与运行时平台交互的核心。通过脚本,您可以访问您想要更改的每一项(我确实是指每一项),包括能够通过抽象语法树(AST)本身扩展 Unity 编译器。
哎呀!你遇到了 Mono!
当微软最初创建 .Net 平台时,它主要是为了 Windows 平台。然而,微软发布了语言规范给 ECMA,不久之后,Mono 开源项目被创建,将 .Net 平台带到了其他平台,包括 OSX 和 Linux。在核心上,Unity 脚本系统是基于 .Net 运行时构建的,并通过 Mono 项目获得了其提供的优势。虽然 Unity 脚本系统完全有可能基于 Java 或其他系统,但 .Net 提供了(开箱即用的)一个丰富的系统,使得跨语言功能实现变得非常简单。
常见语言基础设施
由于 .Net 的 通用语言基础设施 (CLI),Unity 中可以实现多种脚本语言。通用语言基础设施 (CLI) 的目的是提供一个语言中立的平台,用于应用程序开发和执行,包括异常处理、垃圾回收、安全和互操作性等功能。通过在 CLI 范围内实现 .NET 框架的核心方面,这些功能将不会绑定到单一的语言,而是将在框架支持的许多语言中可用。微软对 CLI 的实现被称为 通用语言运行时 (CLR) 或 CLR。
Boo- 比马里奥中的幽灵还要多
Boo 是一种面向对象的、静态类型的编程语言,旨在利用通用语言基础设施对 Unicode、国际化和网络应用程序的支持,同时使用受 Python 启发的语法和特别关注语言和编译器可扩展性。一些值得注意的功能包括类型推断、生成器、多方法、可选的鸭子类型、宏、真正的闭包、柯里化和一等函数。Boo 自 2003 年以来一直在积极开发。
Boo 是一种免费软件,采用 MIT/BSD 风格的许可证发布。它与 Microsoft .NET 和 Mono 框架都兼容。
Boo 脚本看起来是什么样子?
import UnityEngine
import System.Collections
class example(MonoBehaviour):
def Start():
curTransform as Transform
curTransform = gameObject.GetComponent[of Transform]()
curTransform = gameObject.transform
def Update():
other as ScriptName = gameObject.GetComponent[of ScriptName]()
other.DoSomething()
other.someVariable = 5
我应该选择 Boo 吗?
如果你正在寻找一种新的、新颖的编写代码的方式,这种方式不需要太多的仪式感,也不浪费你很多时间在大量的花括号上,Boo 确实是一种你想要深入了解的语言。虽然有些人认为使用空白作为分隔符的想法令人反感,但在 Boo 中编写代码却有一种优雅。如果你使用过传统语言,那么理解它并不困难,而且 Unity(非 iOS 版本)中的一些功能只能使用 Boo 来访问。
虽然 Boo 是一种优秀的语言,但有两个原因你不应该选择它。
-
首先,用 Boo 编写的示例非常少,在社区中关于它的讨论也很少。
-
更重要的是,截至本文写作时,Boo 目前不支持 iOS 平台。
因此,我们在 iOS 语境下对 Boo 的讨论突然结束。
UnityScript/JavaScript — 不仅仅局限于网页
JavaScript 是 ECMAScript 语言标准的实现,通常用于在宿主环境中启用对计算对象的程序性访问。它可以被描述为一种基于原型的面向对象脚本语言,它是动态的、弱类型的,并且具有一等函数。它也被视为一种函数式编程语言,如 Scheme 和 OCaml,因为它具有闭包并支持高阶函数。
虽然 UnityScript 在表面上与 JavaScript 的语法相似,但 Unity 使用的实现并不完全符合 ECMAScript 规范。
一个 JavaScript 脚本看起来是什么样子?
function Start () {
var curTransform : Transform;
curTransform = gameObject.GetComponent(Transform);
// This is equivalent to:
curTransform = gameObject.transform;
}
function Update () {
// To access public variables and functions
// in another script attached to the same game object.
// (ScriptName is the name of the javascript file)
var other : ScriptName = gameObject.GetComponent(ScriptName);
// Call the function DoSomething on the script other.DoSomething (); // set another variable in the other script instance other.someVariable = 5; }
function Update () { // To access public variables and functions // in another script
attached to the same game object. // (ScriptName is the name of the javascript file) var other : ScriptName = gameObject.GetComponent(ScriptName); // Call the function DoSomething on the script other.DoSomething ();
// set another variable in the other script instance
other.someVariable = 5; }
我应该选择 JavaScript 吗?
JavaScript 是 Unity 社区中使用的默认语言,并在整个互联网上广泛使用。许多人将 JavaScript 称为 Unityscript。世界上有很多人每天都在使用它,如果你来自 Flash 背景,这将是你可能获得的 ActionScript 移植便捷性的最接近(直到有人将 ActionScript 移植到 Unity)。JavaScript 的一个强大优势是,完成事情时没有太多的仪式感,在大多数情况下,你会发现你用 JavaScript 构建应用程序时编写的样板代码要少得多。不需要创建类结构,也没有太多直接访问对象属性并使用它们的需要。
JavaScript 可在所有平台上使用,包括 iPhone,但随着你开始使用更大团队构建更复杂的示例,你可能会发现更多的结构和类型将导致缺陷更少。此外,尽管 JavaScript 是目前社区的选择,但大多数作者和开发者正在转向 C#作为他们的主要开发语言。
C# 微软的复仇
C#(发音为“see sharp”)[6]是一种多范式编程语言,包括命令式、声明式、函数式、泛型、面向对象(基于类)和组件式编程学科。它是在.NET 倡议下由微软开发的,后来被 Ecma(ECMA-334)和 ISO(ISO/IEC 23270)批准为标准。C#是专为通用语言基础设施设计的编程语言之一。
C#旨在成为一个简单、现代、通用、面向对象的编程语言。
一个 C#脚本看起来是什么样子?
using UnityEngine; using System.Collections; public class example : MonoBehaviour { void Start() { Transform curTransform; curTransform = gameObject.GetComponent<Transform>(); curTransform = gameObject.transform;
C# scriptstructure}
void Update() { ScriptName other = gameObject.GetComponent<ScriptName>(); other.DoSomething(); other.someVariable = 5;
}
}
我应该选择 C#吗?
作为一种语言,C# 对于在过去 10 年内进行过任何现代面向对象编程的人来说将会很熟悉。其语法对 Java 开发者来说非常熟悉,对于 C++开发者来说也相对容易上手。
互联网上有许多库可以直接添加到你的项目中,用于执行诸如 XML 解析、连接社交网络、加载页面等操作。此外,Unity 社区中 C#开发者数量众多,因此未来对 C#项目和文档的重视程度将进一步提高。
最后,许多已经发布了大量 iOS 项目的开发者表示,C# 是编写应用程序的一个更容易的途径。如果你来自专业的游戏开发背景,或者想要一个干净的面向对象的 Java/Pascal 语法,这是你的最佳选择。
注意
在本书的其余部分,我们将使用 C# 作为脚本的语言。
行动时间 — 创建和组织脚本
在 Unity 中创建一个新的 C# 脚本与使用 JavaScript 或其他任何脚本语言创建脚本的方法类似。
-
选择 资产 | 创建 | C# 脚本。这将在一个项目中创建一个新的 C# 脚本:
注意
虽然 Boo 脚本确实作为一个选项出现,但它不能用于 Unity iOS 项目。
![行动时间 — 创建和组织脚本]()
-
一旦创建了新的脚本,它将以
NewBehaviorScript的名称出现在 项目 视图中,并包含Start()和Update()方法的占位符:![行动时间 — 创建和组织脚本]()
-
创建一个名为
Scripts的文件夹,然后将新创建的脚本拖放到脚本目录中。
刚才发生了什么?
脚本的一个最佳实践是将它们放在项目中的一个文件夹中。你给这个文件夹起什么名字并不重要,因为它更多的是为了你自己的组织目的,而不是为了 Unity。你可以选择将 UI 相关的脚本放在一个目录中,将 AI 脚本放在另一个目录中。无论你如何管理脚本,这取决于你,但在创建普通游戏的过程中,你会创建大量的脚本,所以你想要组织它们,以便它们不会都坐在游戏项目的根目录下。
将脚本附加到游戏对象
如前所述,你可以通过简单地拖动脚本到游戏对象本身来将脚本添加到 GameObject 中。这里重要的是,GameObject 是脚本的所有者,如果该游戏对象不参与场景,脚本中的方法将永远不会被调用。对于本质上是全局的脚本,一个常见的做法是在场景中创建一个空的游戏对象,并将全局脚本附加到它上。虽然人们通常将这些全局脚本与摄像机关联起来,但你应该避免这样做,因为许多复杂的游戏会有很多摄像机——这会复杂化脚本的激活以及确定场景中哪个对象拥有它们。
在 Unity 编辑器中公开变量
很常见的情况是,你会发现自己编写了一个脚本,为脚本中的变量提供了一些值。当你测试你的游戏时,你会发现你希望将这些变量中的某些暴露给 Unity 编辑器,这样你就可以在编辑器中修改它们,而无需编辑和重新编译脚本本身。
这尤其重要,因为很多时候你可能会在脚本中提供一个默认值,你可以根据某些特定的游戏用例来覆盖它。而不是想出一些复杂的机制来做这件事,只需将变量声明为公共变量,它就会在 Unity 编辑器中显示出来。
using UnityEngine;
using System.Collections;
public class SomeScript : MonoBehaviour {
public int testExpose = 5;
public String testStringExpose = "Test";
// Update is called once per frame
void Update () {
}
}
关键脚本方法
Unity 中的脚本编写包括将自定义脚本对象(称为行为)附加到游戏对象上。脚本对象内部的不同函数会在特定事件上被调用。最常用的有以下几种:
| 方法 | 目的 |
|---|---|
Awake |
当脚本对象被加载时调用,但在所有游戏对象初始化之后 |
Start |
当脚本附加的对象在场景中首次使用时只调用一次 |
Update |
这个函数在渲染帧之前被调用。这是大多数游戏行为代码的地方,除了物理代码。 |
FixedUpdate |
这个函数在每个物理时间步中都会被调用一次。这是进行基于物理的游戏行为的地方。与 Update 不同,这个方法不是每次都会被调用,而是在特定的间隔内调用。用于那些应该与帧率无关的事情。 |
LateUpdate |
在调用 Update 之后调用这个函数。如果你想在场景中的所有其他对象更新之后执行某些功能,这很有用。 |
"其他方法" |
当对象被加载时,会运行函数外的代码。这可以用来初始化脚本的状态。 |
iPhoneSettings
虽然 iOS SDK 承诺为所有 iOS 设备提供一个单一的平台,但有时你可能会想要执行特定于设备的功能。例如,如果你在使用 iPhone4,你可能想要加载 Retina Display 兼容的资产;或者如果你发现自己在一个 1G、2G 或基础 3G 设备上,你可能想要提供除了 Game Center 之外的其他网络接口。此外,你可能还想要确定这个设备在世界上的位置。所有这些都可以通过访问 iPhoneSettings 来实现。
屏幕朝向
iPhoneSettings 提供了一个接口,用于从设备获取当前屏幕朝向,并允许你指定你想要内容显示的朝向。
如果你只想在一种模式下操作,你可以将脚本附加到相机上,并在其Start()方法中定义你想要的朝向。
// Start in landscape mode function Start () { iPhoneSettings.screenOrientation = iPhoneScreenOrientation.Landscape;
}
在本章的后面部分,我们将构建一个示例,说明如何根据用户改变设备朝向来在设备能够的多重模式之间移动。
睡眠模式
默认情况下,任何 iOS 便携式设备都会遵循电源管理设置,并在一段时间后变暗屏幕,以防止用户未使用时耗尽电池。每当设备上发生触摸事件时,iOS 电源管理系统都会重置其内部计时器。然而,对于游戏来说,这可能会成为一个问题,因为您可能不需要用户在游戏过程中保持与设备的接触或通过蓝牙键盘。驾驶游戏和飞行模拟器是可能受到影响的游戏类型。
在这些情况下,将设备置于不允许其进入睡眠状态的状态是有意义的。这可以通过将iPhoneSettings的screenCanDarken属性设置为false来实现。重要的是再次审查该声明,设备将永远不会被允许进入睡眠状态。只要您的应用程序正在运行,它就会阻止设备进入睡眠状态。因此,您有责任确保如果您将游戏状态留置在用户未触摸设备的情况下,您将screenCanDarken属性切换回true,以便 iOS 可以变暗屏幕。
设备信息
Unity 为开发者提供了几个有用的字段,这些字段提供了关于设备和其唯一标识的信息。
| 字段 | 描述 |
|---|---|
| name | 这是用户为设备指定的名称。如果您想允许用户在 wan 或本地找到彼此,这很有用 |
| model | 提供一个简单的文本型号名称 |
| systemName | 这只是设备上运行的操作系统名称 |
| generation | 这将告诉您该设备属于哪个世代。值在 iPhoneGeneration 中枚举,并提供了一种可靠的方式来确定用户具体拥有哪种类型的设备 |
| uniqueIdentifier | 这是全球唯一的标识符,用于特定地识别此设备 |
行动时间 — 识别 iOS 类型
假设我们已经设计好我们的应用程序,使其针对 iPhone 4 和 iPad 的高清(HD)进行了优化。
小贴士
如果玩家运行我们应用程序的这个版本,我们希望告诉他们,如果他们使用标准定义(SD)版本的游戏,应用程序将表现得更好。
我们将通过检测设备,立即启动应用程序或显示通知屏幕,然后启动应用程序来实现这一点。
让我们回顾一下实现此应用程序功能所需的步骤。
-
识别我们正在使用的设备类型。
-
根据条件逻辑显示启动屏幕。
-
关闭启动屏幕。
识别我们的设备信息
我们需要做的是:
-
在我们附加到相机的 Start()脚本中添加一些设备检测。
-
我们要寻找的是比 iPhone 4 和 iPad 更旧的 iOS 世代。我们可以通过查看
iPhoneSettings.generation的值来检查这一点。 -
我们选择
iPhoneSettings.generation而不是iPhoneSettings.model,因为generation会返回一个枚举类型,并且由于所有型号都被表示,这将比尝试比较字符串更不容易出错。void Start () { if ( ! ( ( iPhoneSettings.generation == iPhoneGeneration.iPhone4 ) || ( iPhoneSettings.generation == iPhoneGeneration.iPad1Gen ) ) ) { // this device isn't an iPad or an iPhone4 and therefore // doesn't support HD. } }
条件显示启动画面
现在我们知道了我们是否有“合格”的 HD 设备,我们就有了条件显示屏幕的能力。在这个例子中,我们将 SD 显示作为一个单独的级别来构建。这样做是为了能够将这部分工作单独隔离开来,而不会使游戏的其他部分变得杂乱。
-
要加载这个 SD 购买建议级别,我们调用
Application.LoadLevel()方法,并给它提供我们想要加载的级别的名称,在这种情况下,它将是我们的"SDAdvisory"级别。Application.LoadLevel("SDAdvisory");
Unity 现在将加载这个级别并在屏幕上显示它。由于我们将其背景设置为仅用于显示信息的图像,我们需要提供一个机制来关闭这个提示。
关闭启动画面
在我们的新场景中,我们希望能够确认我们已经看到了应该使用游戏 SD 版本的提示,但仍然继续以高清模式玩游戏。如果你没有为你的游戏构建通用二进制(你应该这样做!),你应该检查用户尝试运行的版本的内容是否合适。
要关闭这个级别并加载我们的常规游戏,我们需要在屏幕上显示一个用户可以点击的按钮,并且我们需要监听这个按钮的活动。UnityGUI 提供了一个快速完成此操作的干净机制,尽管性能不是最佳。本书后面的性能和优化部分将涵盖提高性能的方法。
using System.Collections;
using UnityEngine;
public class SplashGUI : MonoBehaviour{
void OnGUI(){
if ( GUI.Button(new Rect(10,10,100,25), "I Understand") ) {
Application.LoadLevel("MainGame");
}
}
}
现在当用户点击带有I Understand标签的按钮时,应用程序将加载MainGame级别,我们的游戏将继续进行,而不会受到任何进一步的干扰。
刚才发生了什么?
当 Unity 在我们的 iOS 设备上启动玩家时,它将iPhoneSettings类填充了我们的设备的设置。由于我们的脚本附加到了场景中的对象,我们的脚本能够从设备上的 Unity 玩家中读取这些设置。基于此,我们使用了一个简单的 C#条件 if 语句来确定我们是否可以开始或需要显示一个标准定义的启动画面。然后我们提供了一个简单的UnityGUI脚本,等待用户按下按钮以启动主游戏。
定位服务
定位服务提供了一个接口,用于访问设备的基站三角测量和 GPS 功能。你可以通过iPhoneSettings获取服务状态,位置数据将通过iPhoneInput.lastLocation可用。
要启动服务,访问StartlocationServiceUpdates()方法,并给它所需的精度,以及更新距离,两者都以米为单位。更新距离是设备必须移动的距离,在它更新iPhoneInput.lastLocation之前。如果你熟悉使用 CoreLocation,你就熟悉了 iOS 的位置服务,因为 Unity 在 CoreLocation API 周围提供了一个包装器,并通过iPhoneInput.lastLocation简单地暴露数据更新。
然而,需要注意的是,GPS 卫星获取不是瞬时的,Unity 将通过LocationServiceStatus属性暴露 CoreLocation 响应的状态。这将返回几个LocationServiceStatus状态之一,你可以轮询这些状态,直到你达到LocationServiceStatus。在野外运行时,此时你将在iPhoneInput.lastLocation中获得实际的 GPS 数据。
行动时间——根据玩家位置更改状态
基于位置的电子游戏是游戏开发社区中一个很大程度上未被充分利用的创新领域。假设我们想要启动一系列利用实际玩家位置信息来改变游戏状态的游戏。为了我们的目的,我们将利用玩家的位置来确定他们所在地区的天气,以及一天中的时间,以便我们的玩家游戏世界可以与外界世界相匹配。
让我们回顾一下实现此应用程序功能所需的步骤:
-
在我们的设备上启动位置服务,
-
轮询设备位置信息,
-
获取此位置的温度,
在我们的设备上启动位置服务
由于这只是在我们的应用程序首次启动时需要做的事情,我们可以在脚本的Start()方法中执行所有操作。我们只需要做的是:
-
调用
iPhoneSettings.StartLocationServiceUpdates()方法,Unity 将开始与平台的核心位置服务进行交互。//Use this for initialization void Start () { iPhoneSettings.StartLocationServiceUpdates(); } -
现在位置服务已经启动,我们需要等待它们可用。位置服务不会立即激活,如果正在使用 GPS,内部系统将不得不获取卫星以三角测量设备的位置。如果我们尝试在完成之前从设备获取信息,结果的行为是未定义的。
-
为了等待位置服务启动,我们需要进入一个循环,在该循环中我们将检查位置服务的状态,然后在再次检查之前进入一个短暂的等待状态。我们可以通过在 Unity 编辑器中配置要等待的时间量来实现这一点。
将变量暴露给 Unity 编辑器
为了将变量暴露给 Unity 编辑器,我们需要以某种方式定义它,使其在我们的脚本中是公共的。这将允许 Unity 将此变量暴露给 IDE 的其余部分,我们可以在不再次编辑脚本的情况下更改此变量的值。
-
在我们的例子中,我们想要暴露我们将在假设无法获得位置服务之前等待的最大时间。
using UnityEngine; using System.Collections; public class LocationBasedGaming : MonoBehaviour { public int retries = 20; // Use this for initialization void Start () { iPhoneSettings.StartLocationServiceUpdates(); } // Update is called once per frame void Update () { } }当你检查这个脚本附加到的游戏对象时,这个信息将在 Unity 编辑器的检查器中显示。在我们的例子中,如果你查看我们附加了这个脚本的相机对象,你会看到变量及其当前值。如果你更改这个值,你将覆盖脚本提供的默认值:
![将变量暴露给 Unity 编辑器]()
- Unity 提供了一个重载的
WaitForSeconds方法,它接受等待的时间量。为了调用这个方法,我们必须:
- Unity 提供了一个重载的
-
执行一个 yield 操作来告诉 Mono 我们的代码需要在函数运行时暂停。通常人们会简单地将这作为一行代码添加,然而,C#要求执行这个 yield 的方法必须有一个不同于
void的返回类型。因此,我们无法简单地在Start()函数中暴露这个。但是,通过提供一个可以处理这个的辅助函数,这个问题很容易解决。IEnumerator EnterWaitState( float time ) { yield return new WaitForSeconds( time ); } -
现在我们可以从
Start()函数内部调用这个方法,等待一定的时间,然后检查位置服务是否可用。// Use this for initialization void Start () { iPhoneSettings.StartLocationServiceUpdates(); while( iPhoneSettings.locationServiceStatus == LocationServiceStatus.Initializing && retries > 0 ) { EnterWaitState( 2.0f ); retries --; } }这段代码将检查设备的状态,等待 2 秒钟然后再次检查。每次它都会减少剩余的重试次数。当这个次数达到零时,我们可以安全地假设用户选择不允许位置服务,或者有某些问题阻止位置服务正常工作(如飞行模式)。
-
另一种可能实现的方法是将
Start()方法的签名从返回void改为返回IEnumerator。IEnumerator Start () { iPhoneSettings.StartLocationServiceUpdates(); while( iPhoneSettings.locationServiceStatus == LocationServiceStatus.Initializing && retries > 0 ) { yield return new WaitForSeconds ( 2.0f ); retries --; } }两种机制都是可接受的,取决于你的意图和习惯的开发风格。这里都包括在内以保持完整性。
轮询设备位置信息
现在我们知道设备正在给我们提供位置数据,我们只需查看 iPhoneInput.lastLocation 变量并提取经纬度。然后我们可以与一个网络服务集成,它可以告诉我们设备所在位置的天气情况。
if (iPhoneSettings.locationServiceStatus == LocationServiceStatus.Failed)
{
Debug.Log("Unable to activate the location services. Device reports failure");
}
else
{
// success path for location services - get information // from Google
float latitude = iPhoneInput.lastLocation.latitude;
float longitude = iPhoneInput.lastLocation.longitude;
}
获取此位置的天气
现在我们知道了设备的经纬度,我们可以获取该位置的天气信息,以便我们在游戏中使用。为了实现这一点,我们将使用谷歌的天气 API 并解析我们收到的 XML。
public class GoogleWeather : MonoBehaviour {
String city;
String conditions;
String tempC;
String tempF;
String humidity;
String wind;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
void forLocation( float lat, float lon )
{
String postLat = "" + lat * 1000000;
String postLon = "" + lon * 1000000;
XmlDocument responseXML = new XmlDocument();
responseXML.Load("http://www.google.com/ig/api?weather=,,," + postLat + "," + postLon );
city = responseXML.SelectSingleNode("/xml_api_reply/weather/ forecast_information/city").Attributes["data"].InnerText;
conditions = responseXML.SelectSingleNode("/xml_api_reply/weather/ current_conditions/condition").Attributes["data"].InnerText;
tempC = responseXML.SelectSingleNode("/xml_api_reply/weather/ current_conditions/temp_c").Attributes["data"].InnerText;
tempF = responseXML.SelectSingleNode("/xml_api_reply/weather/ current_conditions/temp_f").Attributes["data"].InnerText;
humidity = responseXML.SelectSingleNode("/xml_api_reply/weather/ current_conditions/humidity").Attributes["data"].InnerText;
wind = responseXML.SelectSingleNode("/xml_api_reply/weather/ current_conditions/wind_condition").Attributes["data"].InnerText;
}
}
代码首先定义了将存储我们天气查询结果的变量:城市、条件、温度 C、温度 F、湿度和风速。
forLocation() 方法接受经纬度并将它们转换为谷歌期望的格式(API 不支持十进制的经纬度版本——只支持整数表示)。
接下来,API 通过组合传递给 Google 的 URL 并告诉 C#加载这些数据,从 Google 服务器请求 XmlDocument。一旦返回,我们只需选择我们位置的current_conditions,我们现在就有了玩家当前玩游戏位置的天气情况。
刚才发生了什么?
我们在设备上激活了位置服务,并轮询设备以确定设备的位置。我们进入等待循环,以便位置服务有机会启动并提供用户与操作系统交互的机会,以便我们的应用程序获得使用收集到的数据的权限。但是,由于我们无法远程启动此应用程序,我们只是执行了与FindMyiPhone相同的任务。接下来,我们与一个网络服务接口,该服务提供了这个经纬度所代表的地理位置。有了这些信息,我们访问了另一个网络服务,该服务告诉我们这个位置的天气情况。通过将此数据暴露给游戏中的环境脚本,我们创建了一个使用真实位置数据来运行的游戏。
屏幕操作
现代智能手机的其中一个优点是它们允许设备以水平或垂直方向操作。由于这个原因,游戏能够根据设备所处的方向工作已经成为一种预期行为。幸运的是,有方法可以检测屏幕方向,并让 Unity 根据设备方向的改变更新我们的内容。
行动时间——旋转屏幕
一项功能,我觉得 iOS Unity 产品中遗漏了,就是“开箱即用”的旋转功能,这样当用户翻转屏幕时,游戏也会翻转方向。幸运的是,我们可以通过一个简单的旋转脚本来纠正这个问题。
让我们来看看实现此应用程序功能所需的步骤:
-
识别屏幕方向已改变。
-
更新玩家方向。
-
让我们看看它是如何实现的。
识别屏幕方向已改变
处理方向改变的第一步是:
-
实际上实现方向改变。
我们有两种方法可以做到这一点:
-
我们可以只在游戏首次启动时检查,在这种情况下,我们需要将我们的方向检测放入
Start()方法中,因为它只被调用一次。 -
如果我们想在用户玩游戏时检查方向改变,那么我们需要逐帧检查方向的状态。我们通过将方向代码放入
Update()方法中来实现这一点。
方法 描述 Input.deviceOrientationXE"Input.deviceOrientation 方法"返回设备的物理方向 -
-
我们将使用
Input类的deviceOrientation属性来确定设备的方向。这些信息直接来自操作系统,实时更新,因此当方向改变时,我们会收到通知,并可以响应变化,而无需中断游戏。using UnityEngine; using System.Collections; public class OrientationChange : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void FixedUpdate () { if ( Input.deviceOrientation == DeviceOrientation.Portrait ) { } else if ( Input.deviceOrientation == DeviceOrientation.LandscapeLeft ) { } else if ( Input.deviceOrientation == DeviceOrientation.LandscapeRight ) { } else if ( Input.deviceOrientation == DeviceOrientation.PortraitUpsideDown ) { } } }
更新播放器方向
现在我们知道方向已经改变,我们需要更新 Unity iOS 播放器,以便它以正确的方向显示我们的内容。
| 方法 | 描述 |
|---|---|
iPhoneSettings.screenOrientation |
设置屏幕的逻辑方向。 |
void Update () {
if ( Input.deviceOrientation == DeviceOrientation.Portrait )
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.Portrait;
}
else if ( Input.deviceOrientation == DeviceOrientation.LandscapeLeft )
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.LandscapeLeft;
}
else if ( Input.deviceOrientation == DeviceOrientation.LandscapeRight )
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.LandscapeRight;
}
else if ( Input.deviceOrientation == DeviceOrientation.PortraitUpsideDown )
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.PortraitUpsideDown;
}
}
使用 iPhoneSettings.screenOrientation 我们现在可以告诉 Unity 播放器改变其方向。您可以将方向设置为 iPhoneScreenOrientations 中可用的任何一个方向。建议您不要做任何与 iOS 设备预期操作不符的事情,因为苹果可能会因为这种行为拒绝您的应用程序。
刚才发生了什么?
通过向我们的相机添加脚本,我们可以在每一帧的基础上获得 Update() 通知。然后我们可以查看设备的方向,并相应地调整我们的方向。通过更新 iPhoneSettings 属性,我们可以快速翻转场景以匹配我们发现自己所处的任何方向。
iPhoneUtils
iPhoneUtils 是一组简单的实用工具,用于在 iOS 设备上执行一些基本功能。iPhoneUtils 中没有太多内容,但它确实提供了确定您的应用程序是否在设备上被篡改的功能,触发手机的震动,以及播放电影。
播放电影
在 iPhoneUtils 中播放电影有两种机制:PlayMovie 和 PlayMovieURL。PlayMovie 将播放手机上的 StreamingAssets 文件夹中的内容。鉴于您在空中(OTA)分发时可以移动的内容有限,PlayMovie 可能对您没有帮助。PlayMovie 最适合游戏过场动画和您的初始游戏介绍电影,因为这些通常不会很长。
PlayMovieURL 对于长时间运行的影片、希望向用户提供的动态内容(连续剧)或内容太大而无法合理地分布在您的应用程序中的内容来说更为实用。记住,这些仍然是移动设备,它们有带宽限制。如果您使您的应用程序足够大,以至于无法通过空中下载,那么您正在限制您可能的受众,并损害客户的体验。
我的程序是真实的吗?
虽然 isApplicationGenuine 并不能完全确定某人是否盗用了您的应用程序,但它提供了一个机制,通过这个机制您可以检测应用程序是否被篡改。它不会抓住所有的海盗,但这是一个有用的功能。不要仅依赖这个功能来对抗盗版!
行动时间——啊哈!有海盗!
现实情况是,人们会盗取你的游戏。无论他们出于什么原因这样做,你可能都想做出一些回应。假设我们想检测某人是否盗版了我们的应用程序,如果他们确实这样做了,在他们继续到应用程序的演示版本之前,从网络上向他们流式传输一个广告电影。
让我们来看看实现此应用程序功能所需的步骤:
-
这个过程的第一个步骤是创建一个演示场景。
-
在这个场景中放置你想要包含在这个场景中的所有资产。在我的情况下,我会创建一个简单的场景,显示一只小狗坐在一些岩石旁边。你可以控制小狗,并在场景中移动它。
-
现在,让我们创建一个场景的演示版本。出于演示目的,我会使用同一只小狗,但我会在这个场景中只设置它进入睡眠动画循环。每当用户进入演示世界时,他们找到的所有小狗都会处于睡眠状态。
-
根据条件逻辑,显示演示场景。
void Start () { if ( iPhoneUtils.isApplicationGenuine ) { Application.LoadLevel("RegularGame"); } else { Application.LoadLevel("DemoLevel"); } }
我们可以通过检查iPhoneUtils.isApplicationGenuine属性来确定我们的应用程序是否未被苹果签名。需要注意的是,当你在本地机器上构建应用程序时,它将返回false。只有在它被苹果签名并即将上传到 AppStore 时,它才会返回true。因此,你不应该简单地检查应用程序是否为正版,如果不是就拒绝加载,因为审阅者可能会认为你的应用程序无法工作,然后相应地拒绝它。
刚才发生了什么?
当 Unity 播放器启动时,我们检查这是否是我们应用程序的合法版本。如果 Unity 检测到应用程序已被篡改,它将把用户发送到一个广告和一个游戏的演示版本。
但让我们暂停一下。盗版者仍然可以绕过这个限制,并且他们可以为你的应用程序编写特定的补丁。到目前为止,还没有一种万无一失的机制可以检测或防止任何 iOS 应用程序的盗版。只要 iOS 设备可以被越狱,人们就可以绕过你为防止盗版而设置的任何机制。我把这个例子放在这里,是为了让你对如何创造性地应对这种情况的现实有所了解,但不要期望这本身就能防止你的应用程序被盗版。
访问相机
在撰写本文时,Unity iOS 没有提供访问相机进行拍照或流式传输视频的功能。虽然确实存在这个差距,但这种功能通常在游戏中并不常用。然而,如果你确实想访问相机,你有两个选择;为相机编写原生代码插件或获取 Prime31 Etcetera 插件(www.prime31.com)的许可。
摘要
在本章中,我们以高级别了解了不同的 Unity 脚本语言,并对 C#作为脚本语言的一些见解,这些内容我们将贯穿整本书的剩余部分。此外,我们还花了一些时间研究 iPhone 特定的脚本接口,并构建了一些实际应用的示例,这些示例利用了 Unity iOS 播放器中可用的功能。
在掌握了这种特定的 iOS 脚本并更新了我们的演示项目之后,我们可以继续前进,并开始与 iOS 输入系统集成,包括键盘、触摸屏和加速度计。
第六章。我们的游戏:战斗呐喊!
我们花了一些时间讨论了了解 Unity iOS 的基础知识,并查看了一些如何使用其基本功能的示例,但我们需要将所有这些整合起来,为 iOS 市场构建一款游戏。虽然我们可以构想出任何我们想要的任何游戏概念,但我们需要了解我们的平台和用户对该平台的期望。仅仅因为某款游戏在任天堂 DS 或带有键盘和鼠标的 PC 上表现良好,并不意味着这款游戏适合主要界面是触摸屏幕和移动设备的玩家。
在本章中,我们将:
-
漫步于构建 iOS 游戏的决策设计
-
组织我们的项目
-
确定项目资产预算
-
为我们的项目导入和优化资产
游戏概念
由于这是我们打算为应用商店构建的第一款游戏,我们将从一个既有趣又相对基本的概念和实现开始——一个第三人称视角的地牢探险游戏。游戏的设计灵感将来自街机经典游戏《地牢守护者》。我们将通过插件和第三方服务利用 Unity 中可用的 iOS 功能,利用移动游戏中的常用服务。
故事
每款游戏都需要一个故事,这款游戏也不例外。一个故事不一定要是一部宏大的史诗,但它应该向玩家解释他们为什么要做他们正在做的事情,并给他们玩游戏——及其续集的动力。对于《战斗呐喊》,这是一款相当简单的第三人称射击游戏,我们将采用一个相当简单的故事。
伏什星是你们人民的祖先家园。这个星球有一种特殊的资源——重水,伏什星的人民并不知道,这种重水在宇宙中其他智能物种中非常受欢迎,因为它被用于他们的聚变反应堆。伏什星的人民一直在将重水过滤成一种特殊的饮料,供星球上的皇室享用。
有一天,一艘外星飞船出现在伏什上空的轨道上,载着一些外星人,他们已经确定需要重水,并开始入侵这个星球。你是一名驻扎在一家过滤工厂的士兵,负责保卫设施。你的指挥官已经请求支援,因为你的武器装备有限。你所需要做的就是坚守阵地,保持设施的良好状态。
用这个简单的故事,我们可以引导玩家通过我们的场景,并轻松地融入多人游戏元素。
界面
这款游戏的界面可以相对简单,这很重要,因为我们的游戏玩法考虑相对直接。我们需要一种方法让我们的角色在世界中移动,一种让他们攻击敌人的方法,一个显示生命值的显示,以及一种记分的方法:

这个原型代表了我们为游戏构建所需界面的样子。游戏设计需要与设备进行触摸,到目前为止,我们没有理由处理语音、摇晃设备或其他任何事情。如果没有理由添加这些功能,就没有理由添加它们。好的游戏设计是遵循 KISS 原则(保持简单,傻瓜)的问题。
控制
控制是在触摸屏设备上正确实现的一件比较困难的事情,这通常是因为你正在处理一个设计用来手持的设备。考虑到这一点,你需要为设备的两种一般方向做好准备。
第一个用例是用户手持设备。在这种情况下,主要用户交互将是他们的拇指——一个相对低精度的触摸工具,并且它不适合快速移动。
第二个用例是用户将设备放在桌子上,低头看着它。在这种情况下,用户更有可能使用指向性手指,这是一个更高精度的工具,能够进行更快的移动。
你可能会问,这为什么很重要?如果用户正在使用一个精度较低的控制器和移动较慢的手指,为了减少各种游戏类型的挫败感,你需要使你的控制区域更大,并且需要使你的游戏对它的移动更加敏感,反之亦然。考虑到这一点,我们需要为用户提供两组控制,以反映不同的用户手大小,并且我们需要允许这些控制具有可变灵敏度——最好是用户可以在游戏设置中配置的。
音频
在游戏设计中,音频通常是一个事后考虑的问题,而当它真正应该在设计初期就详细考虑,以便它感觉像是游戏的一部分。如果你觉得你游戏的音频几乎和图形元素一样重要,我给你两个挑战:关掉声音听你最喜欢的电影,在网上搜索游戏音乐。做得好的声音和音乐会在用户中引发情感反应,并允许对游戏的其他部分有更深的情感依恋。
我们希望为各种用户交互有声音——敌人的呻吟声、城市冠军的战斗呼喊声、飞行箭矢的声音、被杀者的呼喊声等等。此外,我们希望有几套音乐,以反映游戏的不同情绪。最重要的是,我们希望所有这些声音都能实际适应设备。我们可以做出设计决定,允许用户访问他们的 iTunes 音乐库来获取音乐,但在这个上下文中这不合适。
行动时间——项目设置
现在是我们为游戏创建项目的时候了,这个项目将在接下来的文本中使用。虽然我们之前已经从高层次上走过一遍,但有一些特定的细节我们希望包含在这个新项目中,需要审查。
-
创建一个新的 Unity 项目。
我们的项目将使用 Unity 的一些内置 unitypackages 来提供一些核心功能,而无需我们编写大量代码。
-
为项目输入一个目录名称。
-
在 项目向导 中选择 Character Controller, Skyboxes, Standard Assets (Mobile) 和 Terrain Assets 软件包:
![Time for action — Project setup]()
-
Character Controller 将允许我们在不处理大量脚本的情况下在关卡中移动我们的角色。
-
Skyboxes 对于绘制我们世界的天空非常有用。我们将能够放置一个简单的纹理来表示我们的地下城的天空或天花板。
-
Standard Assets (Mobile) 将包含大量针对 iPhone 优化的界面和预制件。
-
Terrain Assets 将为我们提供在不需要搜索其他资源的情况下在地面绘制的基础。
-
-
正如我们之前所做的那样,我们需要在 Apple 开发者中心的配置文件门户上创建一个新的 App ID 和配置文件。
-
下载并将配置文件导入到 XCode 中。
-
在 玩家设置 中,将 产品名称 更改为 Battlecry。这是当应用程序安装到设备上时应用程序将显示的名称。
-
将我们的 包标识符 设置为从配置文件门户获取的标识符。在这里,我已插入
com.sojournermobile.battlecry以模拟配置文件中存储的内容。 -
由于我们针对的是 iPhone 和 iPad,我们必须将 目标设备 设置为 iPhone + iPad,以便最终得到一个通用二进制文件:
![Time for action — Project setup]()
-
为游戏提供一个图像,这样它在启动时将显示为真实的图标。在资源中有一个 "icon_57_57.png" 文件。导入该文件并将其拖动到默认图标字段中:
![Time for action — Project setup]()
-
为了确保我们链接到最新的 SDK,并能够访问所有可用的 iOS 功能,选择 iOS 最新版作为 SDK 版本:
![Time for action — Project setup]()
-
在项目中保存场景。在可以将场景包含在构建中之前,你需要这样做:
![Time for action — Project setup]()
-
打开 构建设置 并检查我们的场景:
注意
如果场景没有显示出来,请确保你已经保存了它。
![Time for action — Project setup]()
-
从 文件 菜单中选择 构建 & 运行 功能。如果一切正常,你应该能看到你的 iOS 设备启动并显示 Unity 标志。
刚才发生了什么?
我们已经设置了项目结构并生成了游戏的第一版构建。完成这些后,我们可以开始将资产导入到项目中,以开始构建真实场景。
行动时间——构建游戏世界
我们可以访问许多网站来获取游戏资产,但截至 Unity 3.1,已经集成了商店,您可以通过该商店获取项目所需的资产。这个界面非常易于使用,值得检查以构建我们的关卡。虽然我们在这里可能找不到所有东西,但这是一个良好的开始。
一条建议是,有时资产商店中的资产可以通过其他方式以更低的价格购买,所以请务必货比三家。
Unity Asset Store
Unity Asset Store 是直接构建在 Unity 环境中的工具,您可以通过它购买预制件并将其直接集成到项目中,而无需离开工具。资产商店中的预制件包括模型、脚本,甚至可以插入 Unity IDE 本身的功能。
-
通过在窗口菜单中选择资产商店来打开商店:
![Unity Asset Store]()
-
如果您还没有创建账户,请通过创建账户来资产商店进行认证:
![Unity Asset Store]()
-
接下来,您将看到商店的主菜单,您可以在其中购买各种类型的 Unity 兼容资产、声音、预制件、脚本等:
![Unity Asset Store]()
-
使用游戏对象 | 创建其他 | 平面创建场景的地面平面。
-
使用游戏对象 | 创建其他 | 点光源将光源插入场景中。
-
在 Unity资产商店窗口中输入Allegorithmic并选择18 FREE Substances包:
![Unity Asset Store]()
-
下载该包并将资产导入到项目中:
![Unity Asset Store]()
-
在你的项目中展开
Substances_Free目录。 -
展开
Desert_Sand_01包。 -
将Desert_Sand_01材质拖放到平面上:
![Unity Asset Store]()
-
在搜索区域中输入Soldiers Pack:
![Unity Asset Store]()
-
将TheAvatarStudioi的Soldiers Pack导入到项目中:
![Unity Asset Store]()
-
在项目视图中展开
SoldiersPack文件夹。 -
展开位于
SoldiersPack下方的ArmyOne文件夹。 -
将Army 01预制件对象拖放到场景中:
![Unity Asset Store]()
-
在搜索框中输入Stone:
![Unity Asset Store]()
-
将 Universal Image 中的 Stone 资产导入到项目中。
-
在项目中展开
Stone文件夹。 -
将石预制件拖放到场景中。
-
沿着每个轴将 Stone 预制件缩放到10个单位:
![Unity Asset Store]()
-
将放大的石头放置在场景中,以创建一些基本景观:
![Unity Asset Store]()
-
保存场景。
刚才发生了什么?
我们通过从Asset Store导入资源,已经布置好了游戏中的所有动作将发生的舞台。虽然我们为场景中的演员准备好了基本的动画,但我们还没有一个可以被玩家控制的玩家角色。我们所缺少的是参与游戏的演员。我们需要为我们的玩家角色构建一个控制器,使其与我们的游戏设计相匹配。
摘要
在本章中,我们讨论了我们的游戏设计并设置了我们的项目。在这个过程中,我们设置了版本控制,从包括 Unity 资产商店在内的各种来源导入资源,并为我们的设备制作了第一个版本。我们现在准备好迎接第一个挑战,那就是让我们的玩家角色在场景中移动。
第七章。输入:让我们开始移动吧!
到目前为止,我们已经进行了一些基本的场景移动,但那还远远不够,如果我们打算为 iOS 设备构建商业游戏的话。在为 iOS 编写游戏时,经常被忽视的一点是,尽管 iPad、iPhone 等设备可以执行与桌面版本相同的许多功能——但在从键盘鼠标世界转移到触摸屏和加速度计的世界时,你需要了解一些非常具体的技巧。在本章中,我们将花时间深入了解这些细节。
在本章中,我们将:
-
了解 iOS 触摸屏界面
-
了解加速度计及其工作原理
-
在触摸屏上创建一个用于在环境中移动的界面
-
学习如何处理手势
这可能听起来并不多,但在 iOS 开发中,有很多事情你可能会做错,这会导致在使用 Unity 时遇到困难。我们不会假设你一切都会做对,而是会一步一步地讲解,以确保你可以把时间花在构建游戏上,而不是试图解读神秘的错误信息。
那么,让我们继续吧…
输入能力
iPhone 是一个集合了广泛技术的设备,可以用来检测用户的输入。从游戏开发者的角度来看,最重要的两种技术是触摸屏和加速度计。凭借这两种输入机制,迄今为止几乎所有可用的游戏都已被构建,因此我们将深入分析它们的工作原理以及我们如何利用它们的能力来确定用户在游戏中的意图。
触摸技术
触摸屏是一种可以检测显示区域内一个或多个触摸存在和位置的显示设备。虽然早期的触摸设备依赖于被动工具,如笔来检测与触摸表面的交互,但现代触摸设备可以检测与设备的物理接触。
虽然看起来可能不是这样,但用于驱动设备触摸交互的技术有很多种。选择哪种技术的决定取决于众多因素,如成本、耐用性、可扩展性和多功能性。很容易有人建议某一种触摸技术比其他技术优越,但一种适用于特定应用的技术可能完全不适用于另一种应用。例如,iPhone 中使用的这项技术要求用户必须与表面进行物理接触才能注册触摸。然而,如果你正在构建一个展台,你可能希望用户能够戴着手套与设备交互。这个看似无害的选择对选择的技术以及设备本身的设计都有深远的影响。
目前设备中常见的几种触摸表面类型包括:电阻式、电容式和红外式。虽然它们的实现机制各不相同,但它们都遵循相同的的基本步骤——当你将手指或笔尖放在屏幕上时,表面状态会发生某种变化,然后被发送到处理器,处理器确定触摸发生的位置。正是这种状态变化是如何被测量的,将技术区分开来。
尽管今天所有的 iOS 设备都使用特定的表面类型——电容式,但可以预见,随着苹果将平台扩展到覆盖新类型的设备,他们可能在未来的某个时刻改变技术。此外,了解你可能在将内容移植到其他平台时遇到的其他类型的表面也很重要。
电阻式技术
电阻式屏幕由导电材料和电阻材料的多层组成。当施加压力到屏幕上时,手指或笔尖的压力会使导电材料和电阻材料接触——导致电场发生变化。此时,测量连接到导电材料的电路上的电阻将表示触摸的位置。
由于任何压力都可能导致接触发生,电阻式屏幕在你想使用被动工具,如笔作为可能的触摸工具时工作得很好。此外,使用这项技术,你可以戴上手套,因为戴着手套的手和裸手一样可以工作。由于电阻式技术存在的时间较长,它往往生产成本较低,是成本谱系中常见于低端的技术。
电容式技术
电容式屏幕使用一层能够保持电荷的电容材料。当触摸时,这种材料会在接触点表面的特定位置记录电荷量的变化。然后,这些信息被传递到处理器,处理器可以精确地确定触摸发生的位置。iOS 设备通过将电容器排列成网格来简化这一过程,使得屏幕上的每一个点在触摸时都会产生自己的信号。这带来了额外的优势,即产生非常高的分辨率触摸数据,可以被处理器处理。
由于电容式方法依赖于电容材料才能工作,它要求某种能够导电的物质进行触摸。由于人体导电,这可以很好地工作,但它排除了笔尖方法,或者更具体地说,它要求使用特殊的电容笔。
红外技术
红外屏幕使用一排红外或 LED 光束,将其投射在保护玻璃或更常见的亚克力表面下方。然后,摄像头会向上看这个光束网格,寻找信号中断,类似于 iOS 设备使用的网格方法——只是使用红外摄像头和光束。这种方法经过改进并在 Microsoft Surface 上部署,并带来一些意想不到的好处。由于摄像头用于确定触摸位置,因此该摄像头还可以查看该位置的对象。如果该对象是一个标记,它还可以从该标记中提取信息。这种方法在 Microsoft Surface 上得到了很好的应用。
红外方法的一个明显缺点是它需要相当大的空间来发挥作用。由于光学性质,你离表面越远,在该表面上获得的分辨率就越高。这使得这项技术在典型的 iPhone 应用中不切实际。
加速度计
加速度计是一种测量结构上运动加速度的设备。在 iOS 设备中,加速度计是一个三轴系统,可以确定设备各个轴(x,y,z)上的加速度。当设备静止时,加速度计会测量重力(1g)的力。当设备移动时,设备将能够根据这些轴上的加速度测量设备的运动,并确定新设备的方向。不涉及相关的数学,你真正需要知道的是,无论你将设备放在什么方向,设备都能感知到这个方向:
惯性仪
惯性仪是一种测量设备方向的设备。与加速度计不同,设备的方向可以在设备实际移动之前推导出来。目前,惯性仪仅适用于 iOS 设备的一部分,它使得设备中的运动检测更加精细。iOS 设备中的三轴惯性仪与内置的加速度计协同工作,产生完整的 6 轴运动手势灵敏度。在撰写本文时,Unity 中没有对惯性仪的支持,因此我们不会关注其在游戏中的应用。
触摸屏
我们的游戏设计需要在屏幕底部设置一套操纵杆,我们可以使用它们在世界上移动并操纵摄像头。控制方案与玩家熟悉的 Xbox 风格控制器相呼应。

我们还需要使用右键执行操作。类似于 Xbox 控制器,我们希望能够在右操纵杆上轻触以调用动作。
我们想要计划的下一个特性是能够在表面上执行手势,这样我们就可以避免在我们的界面中填充多余的按钮。我们希望在游戏中支持几种手势。
| 手势 | 含义 |
|---|---|
| 向上滑动 | 投掷手榴弹 |
| 向左/右滑动 | 向左/右躲避 |
| 向下滑动 | 守护/躲避 |
加速度计/陀螺仪
我们的游戏设计不需要使用加速度计,但为了说明,我们将使用加速度计作为操纵相机的附加机制,并提供一个摇动命令,如果角色被击倒并需要治疗时我们将使用它。
| 动作 | 含义 |
|---|---|
| 摇动 | 治疗 |
| 向左/右转 | 旋转摄像头 |
实现摇杆
我们的游戏设计草图要求在横屏方向玩游戏,因此我们需要从横屏开始应用程序。
行动时间 — 获取方向
从我们之前的应用程序中我们知道,我们可以通过在我们的应用程序的Awake()方法中执行快速方向更改来实现这一点:
function Awake()
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.Landscape;
}
在之前的例子中,我们把我们的函数放在Start()方法中,但我们将这个调用放在Awake()方法中。这样做的原因是我们希望这个脚本和方向设置在场景加载后立即处理,但在摇杆脚本尝试确定摇杆位置之前。如果我们不这样做,摇杆的位置会太靠近,因为它们的位置是从纵向方向推导出来的。
如果你现在运行你的应用程序,你会发现应用程序将保持单一屏幕方向,然而,当你旋转时,你会得到一个随着屏幕旋转的黑色轮廓。这个黑色轮廓代表 iOS 键盘界面随着设备旋转。为了防止这种情况发生,你需要锁定键盘,使其与应用程序保持相同的方向。
| 键类/方法 | 描述 |
|---|---|
iPhoneSettings.screenOrientation |
获取/设置设备的方向 |
iPhoneScreenOrientation |
可能设备方向的枚举类型 |
iPhoneKeyboard.autorotateXXXX |
设置 iPhoneKeyboard 是否会在设备方向改变时旋转到特定方向 |
void Awake()
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.Landscape;
iPhoneKeyboard.autorotateToPortrait = false;
iPhoneKeyboard.autorotateToPortraitUpsideDown = false;
}
刚才发生了什么?
我们已经配置了游戏,使其在启动时默认为横屏方向。此外,游戏将执行用户期望的操作,你不会遇到 iOS 键盘尝试调整设备方向的图形故障。
接下来,我们的设计要求有两个摇杆区域的触摸界面,左边的摇杆作为移动摇杆,右边的摇杆作为旋转摇杆。我们将使用这些来捕捉用户交互并驱动角色在游戏世界中移动。
行动时间 — 实现摇杆
-
在我们的解决方案的一部分中展开最初添加到项目中的标准资产(移动)unitypackage。在
Prefabs文件夹中有一个Dual Joysticks预制件:![执行动作 — 实现摇杆]()
-
由于这将随着我们相机的用户界面平面移动,我们可以简单地将其设置为相机的子项:
![执行动作 — 实现摇杆]()
-
将游戏视图设置为通过选择游戏视图右上角的下拉菜单以 iPhone 宽显示。这将更准确地展示游戏启动时的外观。现在,当我们查看游戏视图时,我们可以确切地看到我们的相机在游戏开始时将看到什么:

这张图展示了我们期望看到的情况——用户界面中的两个摇杆凸起,准备与用户交互。如果你在你的 iOS 设备上启动应用程序,你会看到当你将手指放在摇杆凸起上时,它们会随着你的手指在触摸过程中移动。当你从凸起上移开手指时,它将迅速回到中心位置。这个信息被预制件捕获并传递给内置的摇杆脚本。
要将输入传递到其他脚本,只需更改预制件指向的脚本。请注意,每个摇杆都可以有自己的脚本,因此你可以为每个摇杆实现两种完全不同的行为。
你还可以通过更新代表摇杆的GUITexture上的纹理字段来更改摇杆使用的纹理:

刚才发生了什么?
我们刚刚实现了游戏的主要输入方式——双摇杆凸起。当我们移动这些摇杆时,它们会将数据传递给摇杆脚本。虽然这很有趣,但它仍然不能让我们的角色在场景中移动。如果我们只需要一些基本的摇杆处理,我们就完成了,但我们需要控制一个角色并在场景中移动我们的相机。幸运的是,有一个预制件可以做到这一点。
移动
现在我们已经有一个可以接受输入的界面,我们需要处理来自该界面的触摸操作,并让我们的角色在场景中移动。我们简单的摇杆是计划的一部分,但现在我们需要处理其余的部分。
执行动作 — 实现相机控制
-
我们需要做的第一件事是从上一个场景中删除我们的主相机和双摇杆。不要担心我们已经删除了主相机,因为我们将在场景中添加一个新的相机。
-
在项目窗口中搜索相机相对控制:
![执行动作 — 实现相机控制]()
-
将相机相对控制预制体拖动到层次结构视图中。当您将此预制体拖动到您的层次结构视图中时,您会发现它由我们之前使用的双摇杆以及称为相机相对控制的东西组成。在相机相对控制下方,您将找到相机枢轴和玩家对象。
相机枢轴正如其名,是相机将围绕其旋转的空间中的点。当您移动右摇杆在 3D 空间中旋转相机时,其动作相对于这个点。
另一个对象是玩家,它承载我们的角色控制器对象,并有一个恰如其分的子节点名称“ReplaceWithRealCharacter”。当我们有真实角色时,我们将将其插入此处:
![执行动作 — 实现相机控制]()
-
切换到游戏视图,我们将看到在运行时这个场景代表什么:
![执行动作 — 实现相机控制]()
- 如您所见,摇杆已经就位,白色胶囊代表角色控制器和玩家游戏对象。如果您现在将此项目部署到您的 iOS 设备上,您将看到您将能够使用摇杆在场景中移动,左摇杆让您在场景中移动,右摇杆旋转相机。
-
接下来,让我们导入我们的玩家角色。打开资产商店,搜索士兵角色包:
![执行动作 — 实现相机控制]()
- 此包包含一个完全装备的角色,我们可以将其用于我们的游戏。
-
在导入角色包后,将士兵预制体拖动到相机相对控制,玩家节点作为子节点。这将断开预制体链接,但这不是问题,因为我们需要断开这个链接来为游戏添加自己的几何形状。
-
现在,删除玩家节点的
ReplaceWithReachCharacter子节点,您将有一个可供使用的士兵。这个士兵不是我们应用中效率最高的模型,但它可以在资产商店中免费获取,并且对我们的目的来说效果很好:![执行动作 — 实现相机控制]()
-
将此应用程序部署到您的 iOS 设备上,并使用摇杆,您会发现,对于您有限的努力,您在世界上有一个角色,它将使用左摇杆导航世界,并且您能够使用右摇杆操纵相机。我们开始得到一些更像游戏的东西,但是玩家没有动画。如果我们想要一个真正的游戏,我们需要一种方法来给这个角色添加一些动画,让人物四处走动,进行攻击动画,受到伤害,并在必要时死亡。
刚才发生了什么?
我们已经扩展了现有应用程序的功能,包括移动我们世界的控制以及玩家角色的相机控制。现在我们已经为角色控制器导入了一个角色,我们需要根据用户输入来对这个角色进行动画处理。
行动时间 — 动画玩家角色
我们导入的角色被称为绑定角色。绑定角色是指具有所有动画骨骼的角色。这些骨骼在动画期间驱动网格本身的运动。让我们禁用网格的渲染,这样我们就可以看到骨骼本身。
-
在层次结构视图中选择士兵网格。
-
在检查器视图中,向下滚动到网格渲染组件并点击其复选框。这将禁用在 Unity 和游戏中的网格渲染:
![行动时间 — 动画玩家角色]()
-
打开项目的场景视图并选择层次结构中的网格。你应该只能看到我们导入的士兵角色的骨骼:

如前所述,Unity 的动画系统相当强大,并且内置了一个易于脚本化的动画混合系统。因此,我们可以处理简单的动画,如行走、奔跑、使用武器等,并且 Unity 可以在我们使用武器的同时混合这些动画。
导入动画
下一步是将动画与这个绑定关联起来,这样我们就可以在场景中“驾驶”我们的角色。你可以通过多种方式来动画化一个角色,例如动作捕捉或手动动画。Unity 支持两种方法将此内容导入到我们的游戏中:动画分割和多个动画文件。
动画分割
有时候,你会收到一个已经将多个动画烘焙到模型上的资产。在许多内容购买场景中,这是一种相当常见的做法。在这些情况下,当你导入模型时,你必须告诉 Unity 如何将单个大型动画分割成多个独立的动画。你将在将模型带入 Unity IDE 时出现的FBXImporter中这样做:

通过选择分割动画选项,Unity 将启用一个表格,允许你指定每个存在的动画。例如,在这个场景中,有一个名为idle的动画,从帧1运行到帧25,一个从帧26到帧50播放的walk,等等。一旦你导入了动画,你可以通过你在表格中给出的名称来引用它们,用于所有动画动作。我们将在稍后详细讨论这个问题。
多个文件
导入动画的首选方式是使用多个动画文件。为此,您需要为动画数据创建一个单独的模型文件,使用命名约定 'model'@'animation name'.fbx,然后将这个.fbx文件简单地拖入 Unity 中,就像拖入任何其他资产一样。
通过这种方式,您可以分别为每个角色明确导入动画,这使得在您的流程中修改这些动画变得更加容易,无需担心哪些帧已更改或动画数据中出现了额外的帧,以便进行潜在的变化。重要的是要注意,这些文件仅包含动画数据——不是实际模型几何形状。此外,重要的是要注意,Unity 不会尝试强制将模型名称映射到您的模型名称:

例如,这里我们导入了一些 FBX 文件中的动画。在我们的场景中没有默认模型,但由于这些动画的骨骼层级与游戏中已有的角色骨骼层级相匹配,我们仍然可以使用这些动画。
要在我们的游戏中使用此动画,我们需要选择我们的角色,并在检查器中展开动画设置:

在这里,我们看到我们的角色指定了 0 个动画。此外,在动画元素中,我们看到没有计划播放的动画。我们可以通过将动画数量增加到3来轻松解决这个问题,以匹配我们导入的三个动画:

这些槽位都可以存放我们导入的动画。动画本身由一个带有时钟的文档图标表示,巧合的是,它的名称与动画本身相同。只需将动画拖到这些槽位上,您就为角色设置了动画:

如果您想为角色设置默认动画,可以将动画拖到动画槽中。在这种情况下,我选择了选择空闲动画:

现在您的角色已经设置完毕,准备进行动画。如果您在编辑器中运行游戏,您将看到您的角色从默认的 T 位置移动到空闲动画,并播放它:

刚才发生了什么?
我们刚刚从资产商店中获取了我们的绑定角色,并使用动画分割和独特的 FBX 动画导入方法向其添加了动画。现在我们的角色可以通过动画数据表现出情感。
导入动画
有时候,你可能没有现成的动画数据,或者你只是想通过集成现有动画来减少开发时间。一个与 Unity 集成良好的服务是 Mixamo 的动画服务。Mixamo 不仅为其角色提供此功能,还为我们能找到的任何绑定角色提供此功能。
动作时间 — 从 Mixamo 导入
我们可以通过访问 www.mixamo.com/ 网站开始(Unity 最新版本中也集成了插件)并浏览他们的动画库来开始:

由于我们希望确保动画能够与我们的角色正确工作,我们可以将我们的角色上传到 Mixamo 并查看动画在我们角色上的播放效果:

我们有机会调整角色中的任何骨骼以适应 Mixamo 在其默认骨骼中定义的骨骼。这将有助于确保我们的动画能够正确播放。在大多数情况下,Mixamo 会自动映射到正确的骨骼,但如果你因为某些特殊的映射需要帮助——所有工具都在那里:

一旦映射成功,我们就可以在浏览 Mixamo 库中动画目录时引用这个已上传的角色,当我们使用该服务创建自己的自定义动画时。现在我们的模型已经准备好了,我们可以查看动画的确切表现,因为 Mixamo 网站上集成了 Unity 播放器,我们可以预览并按需自定义动画。
我们可以调整滑块以实时自定义角色动作,并精确地得到我们想要的效果。一旦我们完成,我们就可以下载我们的动画,以适当的 Unity 动画格式。
如果我们已经有了一个想要导入到 Unity 中的动画,我们也可以使用相同的方法来导入这些动画。Unity 支持两种将此内容导入游戏的方法:动画拆分和多个动画文件。
刚才发生了什么?
我们刚刚从资源商店中获取了我们的绑定角色,并使用 Mixamo 动画服务为其添加了动画。使用这些动画,我们能够让角色在场景中与动画同步移动。现在我们可以移动了,让我们来处理我们输入需求的其他部分——能够根据命令攻击敌人或执行其他动作。
控制角色
我们需要对我们角色做的最后一件事是在场景中驾驶他。这是游戏成败的关键区域,因为它需要与用户交互流畅。这很重要,因为主要角色可能是游戏中使用最频繁的东西之一,所以它应该做得很好。
当角色移动时,我们的角色需要通过动画在场景中改变位置。此外,我们还需要在角色执行的不同动画之间无缝地混合。我们不能让玩家停止行走,然后挥动,然后再开始行走。
行动时间 — 驾驶我们的角色
如果你在上一步之后部署了应用程序,你会观察到我们当前的控件已经可以移动角色在场景中。我们可以使用左侧操纵杆,玩家将在通过空闲动画时在地面上滑行:

我们希望当玩家移动操纵杆时,我们的角色能够行走,因此我们需要对CameraRelativeControl脚本进行一个非常简单的修改来实现这一点。你可以通过查看层次视图中的Player对象来定位这个脚本:

现在看看检查员,你会看到驱动这个对象的全部脚本。请记住这一点,因为我们稍后会更改其中的一些内容:

首先,让我们在脚本中添加一个变量,以便我们可以访问我们的士兵 GameObject。你会注意到在这里,我正在定义脚本中的类型,而不是仅仅声明它为 var。在 Unity iOS 中,你必须定义所有对象类型,出于性能原因不允许动态类型:
private var thisTransform : Transform;
private var character : CharacterController;
private var velocity : Vector3; //Used for continuing momentum while in air
private var canJump = true;
private var soldier : GameObject;
function Start()
{
// Cache component lookup at startup instead of doing this every frame
thisTransform = GetComponent( Transform );
character = GetComponent( CharacterController );
现在,我们只需要在我们的角色中寻找一些速度的变化,这由角色控制器方便地管理,并相应地更改动画。
if ( soldier )
{
if ( ( movement.x != 0 ) || ( movement.z != 0 ) )
{
soldier.animation.Play("walk");
}
else
{
soldier.animation.Play("idle");
}
}
现在,如果你回到应用程序并运行它,你会发现当我们将操纵杆移动到某个速度时,玩家会进入行走动画:

刚才发生了什么?
我们刚刚添加了使用操纵杆数字和 Unity 使用的标准移动系统来驾驶玩家在世界上移动的功能。然而,当我们想要使用多个动画时,我们的方法存在一些限制。
我们当然可以添加一些代码来查找玩家超过一定速度时,通过简单的条件逻辑更改让他们进入跑步动画,但如果你在应用程序中尝试,你会发现这个画面有问题。玩家从本地原点开始动画,然后在动画结束时突然回到原点。此外,当物理应用于此角色时,动画和角色层次之间将出现断开。
当然,您可以通过让动画师在现场执行动画来修复这些问题,但这样您就会失去真正看到动画外观的能力。只有在角色实际运动时,才能真正看到角色的步态、步幅和摆动。
然而,为了修复所有这些问题,我们需要另一个解决方案。我们真正需要做的是在动画过程中跟踪角色的位置,这样当动画结束时,我们就可以从该位置和方向开始重新播放动画。当我们尝试让玩家在台阶上行走或与物体碰撞时,这一点将变得越来越重要。换句话说,我们需要用动画本身来驱动角色的运动,而不仅仅是通过改变角色的位置并告诉他们播放一个动画来实现这一点。
行动时间——使用根运动控制器获得驾照
幸运的是,有一个预构建的解决方案,不出所料,来自 Mixamo,它将为我们解决这个问题,对我们的应用程序代码的影响非常小——那就是根运动控制器:

-
首先,回到资产商店并搜索一个名为根运动控制器的包。这个由 Mixamo 提供并由 Adam Mechtley 开发的包包含了我们使用动画数据驱动角色所需的所有功能——我们只需要进行配置:
![行动时间——使用根运动控制器获得驾照]()
-
接下来,我们需要从我们的层次结构中选择玩家模型,因为这个是我们想要用我们的新控制器控制的层次结构组件。当将此添加到类似的项目中时,请确保将其添加到包含角色控制器的相同节点,因为您希望从根运动计算机产生的运动影响层次结构此级别的所有内容:
注意
例如,如果您将根运动计算机附加到士兵节点上,相机相对控制将不会意识到玩家实际上已经移动。
![行动时间——使用根运动控制器获得驾照]()
-
现在我们已经选择了层次结构中的正确节点,我们可以通过选择Mixamo菜单项来添加根运动计算机组件:
![行动时间——使用根运动控制器获得驾照]()
-
在检查器视图中,您将看到添加到我们的游戏对象中的根运动计算机组件。有了这个,我们就完成了大部分需要完成的工作。那么,让我们来测试一下:
![行动时间——使用根运动控制器获得驾照]()
-
更改角色的动画设置,将默认动画设置为walk:
![行动时间——使用 Root Motion Controller 获得驾照]()
-
接下来,将动画的动画循环模式设置为Loop,以便它将重复播放。现在当你运行应用程序时,玩家将反复向前走,直到他们走出世界。
发生了什么?
我们刚刚完成了一个重要的步骤!我们不仅导入了动画,而且还在使用动画来驱动角色在场景中移动。由于我们的所有动画都与场景中玩家的位置和方向同步,并且由操纵杆控制,我们可以将注意力转向游戏元素。
通过加速度计旋转
下一步我们需要处理的是根据用户倾斜设备来旋转相机。在我们的设计中,我们说这将代表相机的旋转,因此我们需要检测这些动作并根据用户的意图调整我们的相机。
行动时间——根据设备倾斜更新
如前所述,iOS 设备有一个定义的访问权限,允许我们确定设备方向的变化。我们可以在 Input.acceleration 中的 x、y 或 z 值变化中检测到这一点:

由于我们的游戏设计需要我们根据倾斜来操纵相机,我们唯一需要做的就是检查方向变化的方向,然后相应地旋转相机。为了实现这一点,我们可以在一个 GameObject 上附加一个脚本,在该 GameObject 的Update()方法中检查Input.acceleration属性,并确定设备是如何变化的。记住,我们也指定了我们的应用程序设计为在横屏模式下运行,因此我们正在寻找沿设备 Z 轴的旋转。
| 关键方法 | 描述 |
|---|---|
Input.acceleration |
返回设备的加速度计读数 |
using UnityEngine;
using System.Collections;
public class CameraRotate : MonoBehaviour {
public float speed = 10.0f;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
Vector3 direction = Vector3.zero;
direction.x = - Input.acceleration.y;
direction.z = Input.acceleration.x;
if ( direction.sqrMagnitude > 1 )
{
direction.Normalize();
}
direction *= Time.deltaTime;
transform.Translate( direction * speed );
}
}
使用iPhoneSettings.screenOrientation,我们现在可以告诉 Unity 玩家更改其方向。你可以将方向设置为iPhoneScreenOrientations中可用的任何一个。建议你不要做任何不符合 iOS 设备预期操作的事情,因为苹果可能会因为这种行为拒绝你的应用程序。
发生了什么?
通过向我们的相机添加脚本,我们可以在每一帧获得Update()通知。然后我们可以查看设备的方向,并相应地调整我们的方向。通过更新iPhoneSettings属性,我们可以快速翻转场景以匹配我们发现自己所处的任何方向。
摇动设备以执行恢复动作
我们最后需要做的是检测用户是否选择了摇动设备,因为我们的设计规定我们将使用这个动作作为用户将执行恢复动作的指示。
行动时间 — 检测震动
- 处理方向变化的第一步是实际上意识到方向已经改变。我们可以有两种方法来做这件事——我们可以在游戏第一次启动时检查,在这种情况下,我们需要将我们的方向检测放在
Start()方法中,因为它只被调用一次。如果我们想在用户玩游戏时检查方向变化,那么我们需要逐帧检查方向的状态。我们通过将我们的方向代码放在Update()方法中来实现这一点。
| 关键方法 | 描述 |
|---|---|
Input.acceleration |
返回设备的加速度计读数 |
我们将使用deviceOrientation属性来确定设备的方向。这个信息直接来自操作系统,实时更新,所以当方向改变时,我们会收到通知并可以响应变化,而无需打断游戏。
using UnityEngine;
using System.Collections;
public class DetectShake : MonoBehaviour {
public float shakeThreshold = 2.0f;
// Update is called once per frame
void Update () {
Vector3 accel = Input.acceleration;
float x = accel.x;
float y = accel.y;
float z = accel.z;
float shakeStrength = Mathf.Sqrt( x * x + y * y + z * z );
if ( shakeStrength >= shakeThreshold )
{
// do the shake action
}
}
}
医生,治愈你自己
现在我们知道设备已经发生了震动,我们可以执行与震动相关联的特定操作。
在我们的player类中,我们在Player类内部用一个整数简单地表示玩家的健康状态:
public class Player {
public static int MAX_HEALTH = 100;
private int health = 0;
public Player()
{
}
public int getHealth()
{
return health;
}
public void heal()
{
health = health + 10;
if ( health > MAX_HEALTH )
{
health = MAX_HEALTH;
}
}
}
在我们的Player类中有一个简单的治疗方法,每当检测到设备震动时,我们就调用它。
刚才发生了什么?
我们通过检测设备的震动实现了游戏的最后输入特征。基于这个震动,我们改变了用户的状态并采取了一个行动。虽然震动在今天的游戏中并不常见,我鼓励你们谨慎使用,但确实有某些时候它代表了最好的可用输入选项。
摘要
在本章中,我们讨论了 iOS 输入的主要传感器,即触摸屏、陀螺仪和加速度计。完成这个任务后,我们可以提供一个触摸屏界面,玩家可以与之交互,并从 iOS 设备的移动和方向中收集信息。
具体来说,我们涵盖了:
-
不同的触摸技术类型,它们的优缺点
-
如何为设备上的用户构建可触摸的用户界面并从中收集输入
-
如何导入现有的动画以及如何导入 Mixamo 动画
-
如何检测手势并确定用户的意图
-
如何检测设备的移动和方向变化
-
如何使用这些信息在 3D 世界中移动屏幕上的角色
-
如何从动画数据中驱动角色,而不是通过编程
现在我们有一个可以通过我们的 3D 世界控制的角色,我们可以根据用户的输入来动画化这个角色。现在我们有了实际游戏的开始,但世界非常安静,游戏缺乏魅力。在下一章,多媒体,我们将探讨如何向我们的游戏添加声音、音乐和视频,以增加它的吸引力。
第八章. 多媒体
现在我们已经花了一些时间探索游戏的游戏玩法组件并构建控制系统,我们需要花一些时间检查游戏中的视频和音频组件。例如,背景音乐、敌人的声音以及播放电影都是我们游戏需要的重要部分。
在本章中,我们将:
-
学习播放背景音乐
-
学习如何添加环境声音
-
学习如何在应用程序中播放嵌入的电影
-
学习如何从远程位置流式传输电影
那么,让我们继续吧...
重要预备事项
本章假设您对压缩音频格式(如 MP3 和 OGG)以及视频格式(如 MP4)有所了解。此外,它还假设您知道如何使用您喜欢的工具创建这些内容。
音频功能
我们的游戏现在非常安静。尽管我们可以看到玩家的动作,但我们无法听到玩家在做什么——也无法感受到环境本身。我们希望添加一些存在于环境中的声音,并在游戏发生事件时提供一些反馈。
播放声音
如在 Unity 基础知识中讨论的那样,所有播放的音频都是从 AudioListener 的角度进行的。因此,如果我们想让游戏中的某个东西发出声音,我们只需向该 GameObject 添加一个 AudioSource,我们就会得到声音。在我们的设计中,我们要求环境中有环境声音。
行动时间——添加环境声音
当我们在我们的城镇时,我们希望它听起来像森林中的城镇。为了实现这一点,我们应该有一些低音量的环境声音,它只是与环境融合。Creative Commons 许可的音频的一个好来源是 Freesound 项目,可以在www.freesound.org找到。网站上有很多好的候选者,我已经从用户 reinsamba 那里挑选了一个用于这个测试。您可以在项目的资产文件夹中找到一个名为evening in the forest.wav的副本。
-
为了保持您的资产组织有序,在您的项目中创建一个
环境音频文件夹。 -
简单地将资产拖动到项目中,在本例中是
evening in the forest.wav文件,Unity 将执行必要的转换:![添加环境声音的行动时间]()
- 现在您有一个可用的 AudioSource,可以添加到场景中的其他 GameObject 中。但请记住,Unity 不会扭曲资产,除非我们通过更改原始音频文件的一些属性来告诉它。我们引入了一个相当大的 AudioSource 作为
.wav文件。
- 现在您有一个可用的 AudioSource,可以添加到场景中的其他 GameObject 中。但请记住,Unity 不会扭曲资产,除非我们通过更改原始音频文件的一些属性来告诉它。我们引入了一个相当大的 AudioSource 作为
-
告诉 Unity 压缩这个资产,从音频格式选项中选择压缩,这样文件就不会占用太多空间。此外,勾选硬件解码复选框,以便 Unity 将使用 iOS 设备内置的 iOS 硬件:
![添加环境声音的行动时间]()
-
如果没有可用硬件,Unity 将自动回退到软件解码。在我们的例子中,我们是从 26.2MB 的 Wav 文件转换到 0.6MB 的压缩 MP3 文件。如果您想要更高品质的样本,可以将压缩滑块调整到更高的值,但在这个例子中,这只是一个简单的环境噪音,所以我们不需要过度的保真度。
现在我们已经创建了 AudioSource,需要将其附加到一个 GameObject 上,以便它能够播放。由于这是环境声音,我们可以将其附加到玩家将行走的地面。
-
-
简单地将 Audio source 拖动到场景视图中的地面,或者拖动到层次结构视图中的地面。两种方式都会在地面添加一个 AudioSource 组件。
-
通过在检查器的Audio Source 属性中选择循环选项,确保音频持续播放:

我们已经告诉这个 AudioSource 循环其音频,所以我们应该在整个游戏过程中,在这个地面 GameObject 上听到这个音频剪辑。关于 iOS 中压缩音频的一个特殊说明是,压缩过程可能会调整您的音频数据的开始或结束,因此您在压缩后应该听一下,以确保它仍然可以正确循环。
我们并不一定需要将声音附加到地面上,我们也可以将其附加到不同的对象上。例如,如果我们想给市场添加市场嘈杂的声音,我们可以直接将其附加到代表市场的 GameObject 上,当玩家离开它时,声音会减弱。
刚才发生了什么?
我们刚刚为我们的世界创建了一个环境声音轨道。通过导入.wav 文件并压缩它,我们整合了一个适合在移动平台上使用的资产,并且可以使用 iOS 设备硬件来解压缩音频流。由于我们已经将这个 AudioSource 附加到我们世界的地面上,我们可以确信,当我们穿过环境时,它将是可听见的。通过这种方式,我们为我们的世界创造了一些氛围,并给这个世界的这个区域赋予了个性。
行动时间 — 为动作添加声音
虽然我们已经创建了一些背景环境声音,但我们还没有为游戏中发生的行为添加任何声音。例如,如果我们使用我们的远程攻击滑动手势并投掷某物,我们的玩家在投掷动画过程中应该发出一些声音,而投掷的物体如果击中地面——或者其他物体,也应该发出声音。
让我们导入一个简单的声音,当玩家要进行远程攻击时,我们会播放这个声音。我再次使用了来自www.freesound.org的 Creative Commons 许可的声音。这个声音的作者是 Sruddi1,我已经采样了这个声音,包含一个单一的咆哮声,这将是我们的角色在执行远程攻击时发出的声音。我已经将这个声音压缩得类似于导入环境声音时的步骤:

现在我们需要做的就是将这个咆哮声与一个脚本动作关联起来,这个动作是用户扔东西。完成这个任务是对我们在书中早期学到的某些技能的简单扩展。记住,从脚本部分我们知道我们可以将变量传递给脚本,并且这些对象可以是 Unity 中的事物引用。我们需要创建一个简单的脚本来表示这个动作,并公开一个将包含声音的变量。
using UnityEngine;
using System.Collections;
public class ThrowSomething : MonoBehavior {
public AudioClip audioClip;
// Update is called once per frame
void Update () {
// perform the animation for throwing
animation.Play("throw", AnimationPlayMode.Mix);
// play the audio clip one time
//
audio.PlayOneShot( audioClip );
}
}
这个脚本现在会在被调用时播放所需的AudioClip。你所要做的就是将一个AudioClip拖放到你附加此脚本的组件上,这就是将要播放的内容:

重要的一点是,你不必一定将AudioClip附加到游戏中的对象上才能播放声音。当你想要播放AudioClip时,可以即时创建一个GameObject。实际上,如果你想要在检测到某些事件时即时创建声音,这非常有用。
using UnityEngine;
using System.Collections;
public class AudioEngine
{
public AudioEngine ()
{
}
public AudioSource Play( AudioClip clip, Transform position )
{
return Play( clip, position, 1f, 1f );
}
public AudioSource Play(AudioClip clip, Vector3 point, float volume, float pitch)
{
//Create an empty game object
GameObject go = new GameObject("Audio: " + clip.name);
go.transform.position = point;
soundadding, to actions//Create the source
//
AudioSource source = go.AddComponent<AudioSource>();
source.clip = clip;
source.volume = volume;
source.pitch = pitch;
source.Play();
Destroy(go, clip.length);
return source;
}
}
因此,如果我们已经将这个AudioEngine安装到场景中的GameObject上,我们就可以简单地引用它,并告诉它播放我们想要播放的特定AudioClip,并告诉它声音的位置。
刚才发生了什么?
我们创建了一个声音,当用户执行远程攻击手势时就会播放。通过更新我们的脚本,该脚本在检测到远程攻击手势时对玩家进行动画处理,我们已经在脚本中指定了一个新的AudioClip,我们可以通过简单的脚本调用来播放它。现在,当玩家挥剑、击中敌人等时,我们也可以执行类似操作。我们只需要在这些脚本中公开一个AudioClip变量,并传递我们想要播放的声音。
播放音乐
在 Unity 中播放音乐有一些特定的细微差别,以确保其正常工作。虽然音乐就像任何其他声音一样,只是它与玩家在场景中的位置有关。
行动时间 — 音乐的声音
在我们的应用程序中,我们希望音乐持续播放。我们可以通过将GameObject附加到玩家或相机对象,并从那里播放音乐来实现这一点。如果您尝试这样做,您的声音系统将很好地播放音乐,直到场景改变。因此,播放声音和构建音乐系统的主要区别在于确保音乐继续播放。
-
创建一个名为
Music的文件夹。 -
将您喜欢的
.mp3文件导入游戏项目。在项目的示例文件中,您可以找到 basematic 创作的 Orc March 曲目。您可以在www.ccmixter.org/:找到其他创意共享音乐![行动时间 — 音乐的声音]()
-
创建一个名为MusicPlayer的空游戏对象,并将其设置为主相机的子对象:
![行动时间 — 音乐的声音]()
-
将音频源组件添加到MusicPlayer游戏对象。
-
接下来,我们需要确保我们的MusicPlayer对象在场景间移动时不会被销毁,通过在 Unity 中创建一个附加到下一个场景的脚本来实现。为此,在游戏中创建一个名为
scene_2的第二个场景。 -
创建一个名为
MusicPreserver的脚本,并填充以下代码:function Awake() { // find the music player object var musicObject : GameObject = GameObject.Find("MusicPlayer"); // make sure we survive going to different scenes DontDestroyOnLoad(musicObject); } -
将 MusicPreserver 脚本附加到新场景。
刚才发生了什么?
现在,我们可以在游戏中播放音乐,并在场景间移动时保持音乐播放。这将使我们能够为游戏中的区域设置情绪,同时保持更改场景数据时的灵活性。
默认情况下,当 Unity 加载新级别或场景时,前一个场景中的所有对象都会被销毁(这通常会停止我们的音乐)。保留音乐的关键是DontDestroyOnLoad方法,它将告诉 Unity 在处理原始场景的对象时不要动这个游戏对象。现在我们可以拥有跨越整个游戏的真实音乐音轨。
视频功能
iOS 设备对 H.264 编码的视频具有标准的硬件加速播放。Apple 通过 SDK 中的MPMoviePlayerController类公开接口来播放此内容。这个类允许用户播放位于设备上的电影,或者播放位于互联网上某个任意 URL 的电影。Unity 通过iPhoneUtils中的PlayMovie或PlayMovieURL方法公开这两种方法。
无论您是在 WebKit 网络浏览器中使用 UIWebView 流式传输视频,还是在 Unity 中这样做,iOS 设备的视频编码都非常具体。视频必须使用以下压缩标准进行压缩:
-
H.264 基线配置文件级别 3.0 视频
-
支持高达 640x480 分辨率,30fps 的分辨率
-
基线配置文件不支持 B 帧
-
MPEG-4 Part 2 视频(简单配置文件)
如果您需要一些工具来帮助您创建与 iOS 设备兼容的视频,您可以检查 Quicktime Player 本身及其视频导出选项,以及 Handbrake。Handbrake(www.handbrake.fr)为 iOS 平台设备提供了特定的编码视频配置文件。
行动时间 — 播放嵌入视频
对于我们的应用程序设计,我们应在应用程序首次启动时播放一个介绍电影。这个视频通常是某个动画标志或类似内容,在我们的案例中将是 Sojourner Mobile 介绍电影(有时被称为插页)。
我将假设您不是视频工程师,对 MPEG-4、H.264 或 B 帧不太了解,但您有一些想要添加到 iOS 项目中的内容。这给许多新开发者带来了很多麻烦,并导致视频在设备上无法显示。
我们可以通过使用专为为 iOS 设备制作内容而设计的工具来避免所有这些麻烦。您可以用于此目的的一个这样的工具是 iMovie。它具有导出 iOS 设备兼容视频的功能,您可以使用它来处理您的项目。
-
将您的内容导入 iMovie,并使用共享菜单导出:
![行动时间 — 播放嵌入视频]()
-
当提示选择要导出的视频类型时,请选择移动尺寸并按导出按钮:
![行动时间 — 播放嵌入视频]()
- 注意,您也可以选择中等尺寸,但由于这是将被嵌入到我们游戏中的媒体,并且将计入我们的最大应用大小限制,因此最好将此内容保持尽可能小,以便您可以为其他事物(如游戏资源)保留空间。
-
接下来,我们需要将视频移动到名为
StreamingAssets的特殊 Unity 资源文件夹中。Unity 将复制此目录中的文件到我们的应用程序包中,并将它们放在设备上的适当位置,以便我们可以在运行时播放它们:![行动时间 — 播放嵌入视频]()
- 现在我们已经在游戏中有了资产,我们需要播放它。最好的方法是有一个场景,除了显示我们的电影外,不做任何其他事情,然后加载 IntroCity 场景。
-
让我们创建一个名为GameIntro的新场景,它将为此目的服务:
![行动时间 — 播放嵌入视频]()
-
现在我们有了GameIntro场景,我们可以创建一个空的GameObject,并将其脚本附加到它上面,该脚本将播放我们的电影,并在电影结束后加载下一级。
-
现在,我们只需要一个简单的脚本,我们可以将其附加到这个GameObject上,并在脚本的
Start()方法中启动。我们需要使用 C#的Start()方法的协程版本,而不是我们通常的版本。using UnityEngine; using System.Collections; public class PlayIntroMovie : MonoBehaviour { // Use this for initialization IEnumerator Start () { iPhoneUtils.PlayMovie("Snowpocalypse2011.m4v", Color.black, iPhoneMovieControlMode. CancelOnTouch, iPhoneMovieScalingMode.AspectFill ); yield return null; Application.LoadLevel("IntroCity"); } } -
通过将此脚本附加到我们的空GameObject,我们现在拥有了一个完整的介绍电影系统,当我们开始游戏时,我们将看到我们公司的插页广告。
刚才发生了什么?
我们通过播放电影来展示我们的工作室介绍电影,然后加载我们一直在工作的关卡,即开始城镇关卡,从而完成了我们的介绍场景。现在我们的内容开始感觉像我们在 App Store 中找到的内容。
行动时间 — 流式传输视频
我们需要做的第一件事是确保互联网连接可用。一旦我们知道互联网可用,我们就可以开始流式传输我们的视频。我们需要检查网络连接,因为我们想尊重用户可能为了节省电池寿命而关闭网络连接以进入飞行模式,或者因为我们的设备可能不在网络连接区域。
我们可以通过使用 iPhoneSettings 的internetReachability类变量来确定 iOS 设备是否可以连接到网络。这将随着网络状态的变化而更新,并也会告诉您您可用的互联网连接类型。对于我们的目的,我们只需要检查是否有任何类型的连接。我们可以通过简单的条件检查来解决这个问题:
if ( iPhoneSettings.internetReachability != iPhoneNetworkReachability.NotReachable )
{
}
执行此互联网连接检查非常重要。如果您的应用程序尝试连接到互联网但失败,并且您没有优雅地处理这种情况,无论是显示用户可见的错误还是不执行需要网络访问的功能,苹果公司将拒绝该应用程序。您应该准备好在仅限飞行模式的情况下测试您的游戏,以了解在没有网络连接的情况下它是否会表现正常。
现在我们知道我们可以连接到互联网,我们希望将一些视频流式传输到设备上。我已经在网上存储了一个 iOS 设备可以从我们的服务器流式传输的视频:
if ( iPhoneSettings.internetReachability != iPhoneNetworkReachability.NotReachable )
{
iPhoneUtils.PlayMovieURL(http://www.sojournermobile.com/assets/unitybook/commercial.m4v, Color.black, iPhoneMovieControlMode.Hidden )
}
如您所见,这与播放设备上存储的内容并没有太大不同。因为我们希望它像商业广告一样表现,我们希望移除播放器取消此视频或跳过它的能力,所以我们使用iPhoneMovieControlMode.Hidden枚举来确保播放器必须观看这部电影。
刚才发生了什么?
我们已经执行了网络检测来确定互联网连接是否可用。在确认网络连接可用后,我们连接到互联网上的一个流并开始播放我们的商业广告。
摘要
在本章中,我们执行了处理项目中多媒体所需的所有步骤。我们通过提供人们已经期待在游戏中看到的视频和音频环境提示,给我们的游戏增添了更多的灵魂,使我们的游戏看起来像 App Store 中已有的专业游戏。
具体来说,我们涵盖了:
-
如何向环境中添加环境声音
-
如何为游戏添加背景音乐
-
如何根据脚本中的动作播放声音
-
如何播放 Unity 项目中嵌入的视频
-
如何检测我们的网络连接是否活跃
-
如何播放托管在外部网站上的视频
现在我们已经了解了这些并形成了游戏的核心,是时候退后一步,开始考虑如何检查性能问题并调试我们的应用程序了。
第九章。用户界面
虽然可能看起来我们已经创建了游戏的大部分内容,但还有一个非常重要的东西仍然缺失——用户界面(UI)。创建 UI 非常重要,因为这将是您的游戏对用户呈现的真实外观和感觉。如果您有一个通用的界面,无论您的模型看起来如何,游戏都会显得很普通。您可以在 Unity 中设计 UI 的许多方法,但其中大多数是为桌面应用程序设计的,在移动设备上表现不佳。我们将检查标准的 Unity 库设施来构建我们的 UI,并使用 Prime31 UIToolkit 构建一个 UI。
在本章中,我们将:
-
使用原生的 Unity 界面系统
-
探索流行的第三方 Prime31 UIToolkit 方法来生成界面
在我们完成这一章之后,我们将拥有创建几乎所有我们想要创建的游戏所需的所有工具。
那么让我们开始吧...
重要预备要点
本章假设您已经理解了诸如 GIMP 和 Photoshop 等工具的基本概念,包括图层、蒙版和填充。虽然本章中的屏幕将主要关注 Photoshop,但这里使用的所有概念都很容易转移到其他软件。
翻译设计
战吼用户界面设计的草图应该能够显示一些简单的资产,用于显示玩家的当前健康、游戏的分数、摇杆和动作按钮。

我们在“输入”章节中已经涵盖了摇杆和动作按钮,因此我们可以专注于健康显示和显示分数。我们需要一个漂亮的字体来显示文本,以及一些图像来显示健康。此外,我们还想为游戏构建一个主菜单,它将包括一个开始游戏的按钮和一个显示信用额的按钮。
即时模式游戏用户界面
Unity 游戏引擎为开发者提供了一个完整的集成系统,用于游戏用户界面(GUI)开发。这些 GUI 是通过即时模式方法来定义 GUI 并响应其事件构建的。对于大多数游戏来说,这不会成为问题,并且将是您将 HUD 和简单控制放置在屏幕上最容易的方式之一。然而,由于它是一个即时模式 API,开发者非常接近 UI 操作的机制。
例如,我们可以看看如何构建一个简单的 GUI,在屏幕上显示一个按钮。正如你可能猜到的,由于没有工具,并且我们处于即时模式,我们必须手动指定屏幕上组件的布局。
function OnGUI () {
if (GUI.Button (Rect (10,10,150,100), "I am a button")) {
print ("You clicked the button!");
}
}
尽管这可能与跳入 Flash、Photoshop、Objective-C 或你最喜欢的 GUI 框架的感觉不同,但即时模式 GUI 并不是什么新鲜事物。在传统的 GUI 框架中,你会在各种类中设置你的 GUI 组件,然后设置回调和消息系统,以便事件循环可以将数据从组件传递到处理事件的实际应用程序逻辑。那么,这有什么问题吗?你可能想知道。最终你会发现,你的应用程序到处都是代码——特别是如果你真的试图做到面向对象,并且为重用设计你的应用程序的话。
虽然这对于可能需要根据用户类型、屏幕分辨率或使用的数据动态更改布局的复杂商业应用程序来说是有意义的,但这并不是我们试图通过我们的游戏解决的问题。你会发现即时模式 GUI 非常紧凑,所有使你的游戏工作的逻辑通常都在少数几个地方。任何必要的状态数据都会提供给所有界面组件,无需查询或传递数据。这使得应用程序更容易跟踪,并且对性能非常友好。
行动时间 — 创建菜单背景
我们需要做的第一件事是为我们游戏的主菜单创建一个背景。这将给我们一个机会看到如何设置 Unity GUI 组件。为了使事情简单,最初我们将从主菜单开始。
-
创建一个名为MainMenu的新场景。
-
由于这是一个军事风格的游戏,我们可以为我们的主菜单背景使用简单的迷彩纹理。
![行动时间 — 创建菜单背景]()
在此之上,我们将显示我们游戏的按钮。虽然我们可以修改现有的GameIntro场景以处理主菜单,但从设计角度来看,最好创建一个独立的主菜单场景。这将有助于开发工作流程,因为内容创作者可以独立地工作在这个场景上。
![行动时间 — 创建菜单背景]()
-
在添加我们的MainMenu场景后,我们需要对
PlayIntroMovie脚本进行修改,使其加载MainMenu场景而不是直接进入游戏。using UnityEngine; using System.Collections; public class PlayIntroMovie : MonoBehaviour { // Use this for initialization IEnumerator Start () { iPhoneUtils.PlayMovie("Snowpocalypse2011.m4v", Color.black, iPhoneMovieControlMode.CancelOnTouch, iPhoneMovieScalingMode.AspectFill ); yield return null; Application.LoadLevel("MainMenu"); } } -
下一步是将我们要用于背景的纹理导入。在本章的项目文件夹中,你可以找到资产
army_camo.jpg。如果你将其拖入 Unity 中,它将被导入并准备好使用。然而,对于我们的 iOS 发布版本,我们不想仅仅作为背景使用如此大的纹理。如果你显示该资产的检查器设置,你会发现它是以 1024x1024 的分辨率导入的。然而,对于我们的 iOS 游戏,我们不想使用如此大的纹理作为背景。 -
通过选择 iPhone 图标并勾选覆盖 iPhone 设置的复选框,为我们的 iOS 设备覆盖此纹理的设置。将此纹理的最大尺寸设置为512,然后点击应用按钮。
![执行动作 — 创建菜单背景]()
现在我们为 iOS 的纹理将只有 512x512,而其他平台可以是 1024x1024。从工作流程的角度来看,这一点很重要,因为你将想要覆盖你部署到平台的资源,而不为每个平台创建全新的资源。
-
我们接下来的步骤是将此图像设置为场景的背景。在项目视图中选择我们刚刚导入的纹理,并创建一个GUI Texture。
![执行动作 — 创建菜单背景]()
-
将我们的背景纹理的缩放比例更改为(1,1,1),以便它填充整个屏幕。由于GUITexture是 Unity UI 构建的专门用于 UI 元素的构造,因此其位置和缩放都是在屏幕空间中完成的,而不是在世界空间中。通过将所有维度设置为1,我们告诉 Unity 用我们的GUITexture填充屏幕。在这个情况下,Z 轴与之前章节相同。如果你需要在背景之前绘制其他项目,它们只需要具有比背景更低的 Z 值。
注意
我们不需要将GUITexture作为相机的子对象来实现这一点。
![执行动作 — 创建菜单背景]()
-
运行项目,你将看到一个填充整个屏幕的迷彩纹理。

刚才发生了什么?
我们刚刚为主菜单场景创建了一个背景。正如你所注意到的,此项目没有成为场景任何部分的子对象,并且将不考虑场景中的其他项目而显示。现在我们可以继续向菜单中添加按钮。
将菜单显示在屏幕上
要使即时模式 GUI 与 Unity 协同工作,我们只需在脚本中提供一个函数,即OnGUI。就像任何其他脚本一样,我们将把我们的 UI 脚本附加到场景中位于某个位置的GameObject上,Unity 会每帧调用它以确保处理该帧期间发生的任何 GUI 事件。一个明显的警告是,这意味着你的 GUI 循环应该是紧凑的循环并且高度优化;你不应该在 GUI 代码中执行复杂且耗时的计算。
如果我们回顾一下我们的主菜单示例,我们知道我们有两个简单的按钮,当点击时需要执行非常特定的操作。因此,从伪代码的角度来看,我们知道我们的应用程序只需要一个基本的 if/else 条件来检查我们正在使用的两个控件。
// Use this for initialization
if ( MainMenuButton( control_x, control_y, width, height) )
{
loadMainGameScene();
}
else if ( CreditsMenuButton( control_x, control_y, width, height) )
{
loadCreditsScreen();
}
在这个例子中,MainMenuButton和CreditsMenuButton代表我们将要在脚本中定义的两个 GUI 控件,分别用于打开主场景或信用场景。
添加按钮到 GUI 的行动时间
-
从资源文件夹中导入两个纹理
BTN_StartGame和BTN_Credits。这些纹理将用于构建我们的主菜单。你还会注意到这些纹理比包含的按钮要大一些。这是因为在默认情况下,Unity 会期望你的纹理是 2 的幂。我们将在稍后解决这个问题。
![添加按钮到 GUI 的行动时间]()
-
创建一个名为Menu Object的空游戏对象。
回到我们最初的脚本探索,我们知道我们需要在场景中有一个游戏对象,以便我们的脚本能够运行。由于这个对象不需要显示,或具有任何特定的游戏功能,我们只需将一个空游戏对象插入场景,并将我们的脚本附加到该对象。
![添加按钮到 GUI 的行动时间]()
-
由于我们将构建即时模式的 GUI,因此我们可以推断出我们将需要一些脚本来驱动 GUI 的显示。创建一个名为
MainMenu的脚本并将其附加到MenuObject游戏对象。 -
在
MainMenu脚本中定义两个公共变量,这将允许我们指定主菜单命令的按钮纹理。当我们最初导入按钮纹理时,Unity 将它们导入为 Texture2D。如果我们配置我们的脚本以接受 Texture2D,我们可以使用 Unity GUI 配置我们想要显示的纹理,而不是在代码中硬编码此值。我们可以通过在定义脚本的类中定义公共属性来实现这一点。
public Texture2D mainMenuButton; public Texture2D creditsMenuButton; -
接下来,我们需要更新我们的
OnGUI方法以显示主菜单的按钮。我们希望之前导入的 Texture2D 作为按钮显示在我们的界面中,因此我们需要使用 Unity GUI 系统将它们渲染为按钮。Unity 有一个易于使用的
GUI.Button类,它执行此功能。GUI.Button允许我们在界面中创建一个按钮,其坐标位于屏幕空间中。我们通过定义一个矩形(Rect)来实现,其中按钮将被绘制在屏幕上的位置作为前两个参数,宽度和高度作为后两个组件。Rect( x, y, width, height )当与正常的
GUI.Button()方法调用结合时,我们得到以下结果:void OnGUI() { GUI.Button( new Rect ( 0, 0, 256, 256 ), mainMenuButton ); }在这里,我们正在创建一个位于屏幕左上角、大小为 256x256 的新按钮,并将
mainMenuButton定义为应该绘制在这个按钮中的 Texture2D。![添加按钮到 GUI 的行动时间]()
-
接下来,我们需要告诉 Unity GUI 在有人按下该按钮时执行操作。正如伪代码所示,这涉及到将按钮包裹在一个 if 语句中——该语句将在
OnGUI中评估。 -
添加
creditsMenuButton并将按钮居中,通过更新原始GUI.Buttons的 Rects 来实现。using UnityEngine; using System.Collections; public class MainMenu : MonoBehaviour { public Texture2D mainMenuButton; public Texture2D creditsMenuButton; // Process the GUI void OnGUI () { int componentWidth = 256; int componentHeight = 256; int interfaceOrigin = 0; if (GUI.Button( new Rect ( Screen.width / 2 - componentWidth/2, interfaceOrigin, componentWidth, componentHeight ), mainMenuButton) ) { Debug.Log("You clicked the start button "); } else if (GUI.Button( new Rect ( Screen.width / 2 - componentWidth/2, interfaceOrigin + componentHeight, componentWidth, componentHeight ), creditsMenuButton) ) { Debug.Log("You clicked the credits button "); } } }虽然在 Unity 中使用 GUI 组还有其他方法来做这件事,但这种方法更适合作为教学工具。如果我们现在渲染场景,我们会发现两个包含我们的按钮纹理的按钮:
![行动时间 — 向 GUI 添加按钮]()
按压这些按钮将在控制台显示一些调试文本:
![行动时间 — 向 GUI 添加按钮]()
虽然这允许我们的按钮被渲染,但渲染的 GUI 按钮比我们想要的要多。Unity GUI 在其
GUIStyle中定义了自己的样式引擎,它决定了按钮应该如何绘制。由于我们已经将按钮的外观和感觉烘焙到纹理本身中,我们不需要 Unity 显示任何这种样式。 -
更新
GUI.Button的GUIStyle,使其没有关联的样式。if (GUI.Button( new Rect ( Screen.width / 2 - componentWidth/2, interfaceOrigin, componentWidth, componentHeight ), startButton, GUIStyle.none) ) { Debug.Log("You clicked the start button "); }![行动时间 — 向 GUI 添加按钮]()
通过进行这个简单的更改,我们将能够以我们期望的方式渲染 GUI。
-
更新脚本以在按钮按下时加载 Unity 级别。
if (GUI.Button( new Rect ( Screen.width / 2 - componentWidth/2, interfaceOrigin, componentWidth, componentHeight ), startButton, GUIStyle.none) )) { Debug.Log("You clicked the start button "); Application.LoadLevel("IntroCity"); }
通过像之前那样调用Application.LoadLevel,我们现在有一个机制,即通过界面按下按钮,Unity 加载特定的场景。
刚才发生了什么?
我们刚刚构建了一个简单的即时模式 GUI,用于显示我们游戏的主菜单。我们通过使用 Unity GUI 系统和构建一个渲染我们的 GUI 的脚本来实现这一点。在幕后,Unity 正在根据脚本每帧绘制我们的场景。重要的是要注意,如果你更改脚本,使其程序性地根据某些设置或每三帧显示某些内容,Unity 会这样做,因为它会每帧处理 UI 脚本。因此,在OnGUI方法中执行任何非常复杂的操作都可能导致性能显著下降。
更好的方法 — UIToolkit
Unity GUI 库当前实现的一个问题是,它是为 PC 用户设计的,没有考虑到移动设备的任何限制。虽然在某些情况下性能足够好,但在大多数情况下,它生成的绘制调用数量足以破坏你游戏的性能。
在我们之前的例子中,由于我们没有进行任何足够强烈的渲染来影响帧率,因此无法真正看到性能下降的影响。由于我们的场景相当静态,因此无法直观地测量结果。然而,如果我们查看我们的统计数据,我们可以看到,即使是这个非常简单的 GUI,我们也正在使用三个绘制调用——每个 GUI 组件一个。

现在可能看起来并不多,但随着你在优化章节中学到更多,这是 iOS 设备正在做的大量工作。那么如果我们能在单个 draw call 中完成同样多的工作会怎样?如果我们能不管我们绘制多少 GUI 元素都只有一个 draw call 会怎样?Prime31 的 UIToolkit 就出现了。
执行时间 — Prime31 UIToolkit
-
通过访问网站:
github.com/prime31/UIToolkit下载code'n'web TexturePackerPro基本功能。 -
从下载列表中选择 TexturePacker 的最新可用版本并安装 TexturePacker:
![执行时间 — Prime31 UIToolkit]()
-
接下来,将信用菜单和开始游戏菜单的 UI 按钮拖入界面。你可以通过将图像拖入界面右侧的 Sprites 菜单来完成此操作:
![执行时间 — Prime31 UIToolkit]()
-
我们希望使用尽可能少的纹理内存,所以将最大宽度和最大高度设置为可能的最小 2 的幂次。你的纹理仍然需要是正方形,所以你在寻找可以同时设置盒子的宽度和高度的 smallest number。在我们的例子中是 512。如果TexturePacker找不到将你的纹理放入该大小的方法,它将把文本变成红色。
![执行时间 — Prime31 UIToolkit]()
在这个阶段你可能还没有注意到的一个好处是,在我们之前创建纹理时,我们是以 2 的幂次大的纹理来创建的,这样 Unity 才能接受它们。在这里,TexturePacker 正在识别纹理中的空白区域,并优化每个纹理所占用的空间。在这种情况下,我们实际上有额外的空间,如果我们需要的话,可以打包更多的纹理。
-
在输出设置(滚动左侧面板到底部)中将数据格式设置为JSON(JavaScript 对象表示法)并将目录设置为项目的
Assets文件夹。将文件名设置为gui并将.json文件的扩展名更改为.txt,因为 Unity 期望文本资源以.txt扩展名结尾。![执行时间 — Prime31 UIToolkit]()
-
在工具栏中按下发布按钮,将在你的
Assets文件夹中创建适当的文件。![执行时间 — Prime31 UIToolkit]()
现在我们已经完成了从 TexturePacker 需要的所有事情。
-
通过访问
github.com/prime31/UIToolkit网站下载 UIToolkit 库。 -
选择下载并选择.unitypackage 选项:
![执行时间 — Prime31 UIToolkit]()
-
下载完成后,安装 Unity 包,导入其中的所有项目:
![执行时间 — Prime31 UIToolkit]()
-
为了分别检查渲染 GUI 的两种不同方法,为这种方法创建一个新的场景,并将其命名为 UIToolkit。
-
为了确保我们的 UI 组件与场景中的其他部分分离,我们希望在 Unity 中将其放置在特定的层上。层是一组组件的集合,可以作为一个集合进行访问或操作。如果你熟悉 Photoshop,这个概念是相似的。
-
在 Unity 中,通过访问编辑 | 项目设置 | 标签来创建一个新的层,以便我们的 GUI 可以绘制:
![执行时间 — Prime31 UIToolkit]()
-
选择用户层 8,并将层名称设置为UI 层:
![执行时间 — Prime31 UIToolkit]()
-
创建一个空的游戏对象,命名为GUIObject,并将其位置设置为 0,0,0:
![执行时间 — Prime31 UIToolkit]()
-
在项目视图中展开
插件文件夹,你将找到UIToolkit 库。展开 UIToolkit,并将 UI 脚本拖放到GUIObject:![执行时间 — Prime31 UIToolkit]()
-
在层次结构视图中选择MainCamera对象,并从裁剪遮罩中移除UILayer,以确保 UI 不会绘制两次:
![执行时间 — Prime31 UIToolkit]()
-
一旦选择UILayer,裁剪遮罩应显示为混合,如下截图所示:
![执行时间 — Prime31 UIToolkit]()
-
创建一个新的空游戏对象,命名为UIToolkit,并将其设置为之前创建的包含 UI 脚本的GUIObject的子对象:
![执行时间 — Prime31 UIToolkit]()
-
将
UIToolkit plugins文件夹中的UIToolkit脚本拖放到层次结构视图中的 UIToolkit 游戏对象上:![执行时间 — Prime31 UIToolkit]()
-
在层次结构视图中选择GUIObject节点,以显示其检查器。告诉它我们希望 UI 在UILayer上绘制,通过选择UILayer条目旁边的下拉菜单并选择 UI 要绘制的层——我们也将它命名为UILayer:
![执行时间 — Prime31 UIToolkit]()
-
为了完成我们的配置,我们需要告诉UIToolkit我们正在使用哪个 TexturePacker 配置。在层次结构视图中选择UIToolkit节点以显示其检查器。在Texture Packer Config Name中输入我们保存的配置名称,在本例中为gui:
![执行时间 — Prime31 UIToolkit]()
现在我们准备使用在 Texture Packer 中使用的纹理来创建我们的 GUI。
-
创建一个名为
UIToolkitGUI的脚本,并使用这种新方法在屏幕上添加一个按钮:using UnityEngine; using System.Collections; public class UIToolkitGUI : MonoBehaviour { // Use this for initialization void Start () { var mainMenuButton = UIButton.create("BTN_StartGame.png", "BTN_StartGame.png", 0 , 0 ); } }注意,UIToolkit 按钮类接受一个正常按钮状态和一个按下按钮状态。由于我们没有两者,所以我们只是使用了相同的纹理。最后两个参数是我们希望在屏幕上显示此按钮的位置。
-
按下播放,我们的按钮将显示在屏幕上。
刚才发生了什么?
我们刚刚使用 Prime31 的开源免费库 UIToolkit 创建了我们的 GUI。我们实现了与原始菜单相同的功能,但只需要一个绘制调用。虽然我们也可以使用 UnityGUI 实现类似的功能,但随着用户界面的越来越复杂,性能会逐渐下降,因为每个控件的增加和每个 GUIStyle 的渲染都会导致另一个绘制调用。
此外,我们还提出了一种更优化的方法来将纹理放入 UI。默认情况下,Unity 不允许您使用非 2 的幂纹理进行渲染,这将导致每个纹理都有大量的浪费纹理内存。使用这种方法,我们可以将大量纹理打包到一个更大的纹理中,这本身就更高效,并且可以渲染形状迥异的纹理,而不会在纹理内存效率上牺牲太多。
摘要
在本章中,我们学习了如何为我们的游戏构建 UI。我们探讨了使用 Unity GUI API 设置 UI,以及使用领先的第三方插件 Prime31 UIToolkit 设置 UI。
具体来说,我们涵盖了:
-
如何使用标准 GUI 库构建即时模式 GUI
-
如何使用 Prime31 UIToolkit 构建 GUI
-
关于 GUI 性能的一些担忧
现在我们已经完成了 UI 的组装,我们需要为我们的游戏处理更复杂的游戏脚本——这是我们下一章的重点。
第十章:游戏脚本
在过去的几章中,我们已经走了很长的路,并创建了我们游戏的大部分内容,但我们仍然需要做一些工作来实现一些游戏玩法概念。在本章中,我们将探讨实现我们游戏玩法机制所需的一些游戏脚本。
在本章中,我们将学习:
-
如何将粒子系统添加到导入的模型中,以模拟武器
-
如何使用动画来驱动游戏中的事件
-
如何处理角色和敌人上的 ragdoll 物理
-
如何根据游戏玩法来记分和触发事件
在这些基础知识的基础上,我们将完成一个实际游戏玩法机制的核心,我们只需要将其添加到角色所在的世界中。
枪战作为游戏玩法
由于我们的游戏是射击游戏,我们需要提供的主要游戏玩法机制之一是枪械可以发射弹丸并击中世界中的其他物体。虽然游戏中实现枪声的方法有很多,例如模拟枪声或创建代表弹丸的游戏对象,但表示枪声的更有效机制是粒子系统。通过将粒子系统附加到我们的枪械对象上,我们可以轻松地绘制出在游戏世界中向物体发射的弹丸的表示。
Unity 中的粒子系统支持物理、碰撞和内存管理,这在移动设备上尤其高效。唯一需要注意的问题是,移动设备的填充率(屏幕被填充或绘制的速率)并不像它们的桌面版本那么高。同样,如果你执行的动作使得引擎必须重复绘制屏幕的某些区域,你将看到相同的填充率问题。所以,如果你有很多占据屏幕大块区域的粒子,你可能会遇到性能问题。我们将在优化章节中详细讨论这个问题。
行动时间 — 准备武器
对于这个例子,我们将创建一个名为枪场的项目,这样我们就可以轻松测试我们的游戏玩法,而无需构建和部署整个项目。每次你对游戏进行大的玩法机制更改时,通常都有必要单独进行,这样你就可以在开发中更加敏捷。大型项目可能会很快变得杂乱无章,因此最好在简单的项目中测试:
-
创建一个名为GunRange的新项目。将场景保存在项目中,命名为level_0。
-
导入一个静态枪械模型并将其放入名为武器的层级组中。
-
接下来,我们需要确保武器指向 Z 轴。我们通过旋转模型来实现这一点,使得枪口(即,我们的弹丸将从中出来的那一端)指向正 Z 轴。

由于我们将把这个整个武器及其枪火粒子系统作为一个预制件附加到角色上,我们想要确保在这个点上定义一切相对于预制件的坐标系。这样做是为了当它附加到我们的角色或其他对象上时,它能正常工作。
刚才发生了什么?
我们刚刚将一个枪模型导入到场景中,并将其与 Z 轴对齐。我们这样做是相对于预制件坐标系,而不是具体到游戏坐标系。我们这样做是因为我们希望发射的子弹沿着预制件的 Z 轴发射,而不管世界坐标系中发生什么。
你将武器对齐到哪个轴实际上并不重要(只要调整你的模型和脚本),但将武器对齐到指向 Z 轴是有逻辑意义的,这样当我们发射子弹时,它们就会沿着正 Z 轴向下。为什么?在传统的 3D 图形方向中,沿着正 Z 轴的运动表示物体远离相机——所以在这里使用它是合理的。
发射子弹
我们可以使用两种方法将粒子系统添加到我们的武器上。我们可以直接将粒子系统添加到枪的 Game Object 中,或者我们可以添加一个子 Game Object 到枪上,该子 Game Object 执行实际的发射。你选择的方法实际上取决于你需要多少灵活性,以及你希望层次结构中有更多对象的需求。
如果你直接将粒子系统添加到枪的 Game Object 中,粒子将从 Game Object 的中心发射,这在视觉上是不正确的。更通用的做法是创建一个将作为发射点的 Game Object,并将其附加到武器上的一个点上。这是我们项目将采用的方法。
动作时间 — 添加粒子系统
-
从GameObject菜单创建一个空的游戏对象,并选择Create Empty。
![动作时间 — 添加粒子系统]()
-
将这个游戏对象命名为MuzzlePoint。
![动作时间 — 添加粒子系统]()
-
在Hierarchy视图中选择MuzzlePoint游戏对象,以显示与其关联的组件。
![动作时间 — 添加粒子系统]()
-
在Component菜单中选择Ellipsoid Particle Emitter,以便它关联一个粒子系统。
![动作时间 — 添加粒子系统]()
-
在 Unity 中将MuzzlePoint游戏对象放置在枪模型上,以指定子弹应该发射的位置。
![动作时间 — 添加粒子系统]()
-
接下来我们需要做的是让我们的粒子发射器实际发射粒子。由于我们设置了武器使其指向 Z 轴,因此配置我们的粒子系统使其将粒子发送到正 Z 轴是非常容易的。
记住,由于我们已经将这个粒子发射器设置为枪的子节点,我们想要确保为它定义的坐标系相对于枪模型的原点。您可能需要从枪模型的原点进行调整以正确定位它,但请意识到MuzzlePoint的坐标系是相对于枪的坐标系。
![添加粒子系统的时机]()
-
由于我们已经将其设置为枪模型的子节点,我们希望将其配置为与枪模型的原点对齐。你可能需要根据你的特定模型进行调整,但你应该确保从枪模型的原点开始。
由于我们正在创建一个代表子弹的粒子系统,我们希望所有的弹丸都沿着相同的轴线移动。为了使用椭球体粒子发射器实现这一点,我们将发射器的椭球体改为只沿一个轴线发射粒子,在这种情况下是 Z 轴线。如果我们不这样做,粒子就会沿着随机的轴线喷洒。
![添加粒子系统的时机]()
-
-
最后,我们需要向我们的MuzzlePoint游戏对象添加一个简单的脚本,命名为“枪声”,它与枪的射击按钮相关联,这样我们就完成了。
using UnityEngine; using System.Collections; public class Gunfire : MonoBehaviour { public GameObject muzzlePoint; // Use this for initialization void Start () { muzzlePoint = GameObject.Find("MuzzlePoint"); } void fireWeapon() { muzzlePoint.particleEmitter.Emit(1); } } -
如前所述,将士兵模型导入我们的场景中。
![添加粒子系统的时机]()
-
在士兵的层级结构中调整枪的位置,使其成为手的子节点,这样士兵就能携带物品,并且会随着玩家的移动而同步移动。
![添加粒子系统的时机]()
-
更新枪声脚本,添加一个
GUI.Button来模拟真实游戏中的射击按钮。using UnityEngine; using System.Collections; public class Gunfire : MonoBehaviour { public GameObject muzzlePoint; // Use this for initialization void Start () { muzzlePoint = GameObject.Find("MuzzlePoint"); } void fireWeapon() { muzzlePoint.particleEmitter.Emit(1); } // Update is called once per frame void OnGUI () { if ( GUI.Button( new Rect(0,0,50,50), "Fire" ) ) { //Debug.Log("Firing the weapon"); fireWeapon(); } } } -
运行游戏并按下射击按钮。
刚才发生了什么?
我们刚刚赋予了我们的武器发射代表子弹的粒子的能力。我们还将其附加到我们的普通玩家上,使其成为玩家不可分割的一部分,并且会随着玩家在场景中的动画移动。我们现在拥有了涉及射击作为主要游戏机制的游戏的基本构建块之一。
目前我们的粒子具有默认的外观,但如果我们想改变外观,我们只需简单地将一个粒子渲染器组件添加到这个MuzzlePoint游戏对象中。然后我们可以通过提供代表子弹外观的纹理来改变粒子的外观。
让动画驱动
经常会有很多上下文信息包含在我们动画师给出的动画数据中。在我们的例子中,我们有一个动画,我们的角色将执行一个抛掷动画来表示抛掷手榴弹,但目前我们还没有将游戏对象放入世界中的方法来响应这个动作。当然,没有人希望硬编码表示抛掷的动画帧,因为这很可能会在整个游戏过程中发生变化。幸运的是,我们可以将动画的任意部分与 Unity 中的回调关联起来。
动画事件
在 Unity 的动画系统中,我们有能力将事件与动画的任意部分关联起来。Unity 将这种能力称为动画事件。通过动画事件,我们可以让动画系统执行脚本,从而驱动游戏的一些逻辑。动画事件可以添加到从你的建模环境导入的任何动画剪辑,或者使用动画视图创建的任何临时动画。
执行动作 — 添加动画事件
-
要将事件添加到我们的士兵动画中,选择角色并检查动画视图。
![执行动作 — 添加动画事件]()
- 在动画数据上方有两个轨道——最上面的轨道显示为这个动画添加的所有事件。下面的轨道显示为我们的动画数据定义的所有关键帧。
![执行动作 — 添加动画事件]()
-
创建一个名为
GrenadeToss的 C#脚本,并将其作为我们的士兵对象的子对象。 -
在类中删除
Start()和Update()方法,并创建一个名为TossIt的方法,并在其中添加一个日志语句。对于我们的目的来说,Start()和Update()方法是不必要的。using UnityEngine; using System.Collections; public class GrenadeToss : MonoBehaviour { // Use this for initialization void TossIt () { Debug.Log("We have arrived at the toss point of the script"); } } -
接下来,在动画视图中滚动动画,直到你到达动画中你想抛掷发生的点。
![执行动作 — 添加动画事件]()
-
通过点击添加动画事件按钮创建一个新的动画事件。你还可以通过点击动画数据上方的事件轨道来完成此任务。一个编辑动画事件对话框将出现,允许你配置当达到该事件时将被调用的脚本函数。
![执行动作 — 添加动画事件]()
-
打开列表框,你会看到所有可以调用的函数列表。
-
选择
TossIt()函数。![执行动作 — 添加动画事件]()
-
按下动画的播放按钮。如果一切顺利,当达到该帧时,你会看到打印出的日志消息。
-
在项目的
Weapons文件夹中创建一个名为Grenade的文件夹。 -
创建一个新的 Prefab 对象,命名为 Grenade,作为一个一维拉伸的简单球体。
![动作时间 — 添加动画事件]()
- 这是我们将在角色达到
TossIt()动画位置时创建的对象。
- 这是我们将在角色达到
-
更新我们的
GrenadeToss脚本,当玩家达到脚本中的TossIt()函数时实例化一个游戏对象。由于我们希望这个对象由物理系统模拟,我们希望将其创建为 Rigidbody。通过从层次结构中选择 Grenade 对象并为其添加一个 Rigidbody 来实现这一点。![动作时间 — 添加动画事件]()
- 现在每次实例化的 Grenade 对象都将由物理系统进行模拟。
-
我们还希望在这个动画达到这一点时在手的当前位置实例化这个手榴弹。由于我们的士兵模型的所有部分都被命名了,我们可以在从层次结构中查找它之后使用该对象的 Transform。
using UnityEngine; using System.Collections; public class GrenadeToss : MonoBehaviour { public Rigidbody grenade; // Use this for initialization void TossIt(){ Debug.Log("We have arrived at the toss point of the script"); Transform handLocation = GameObject.Find("LeftHandIndex1").transform; Instantiate( grenade, handLocation.position, handLocation.rotation ); animation eventsadding} }
刚才发生了什么?
我们刚刚为我们的游戏增加了一些现实感,并为我们的艺术家提供了一些灵活性。通过导入我们的动画数据并将其与事件关联,我们创建了一个完全基于脚本的驱动游戏行为的路径。我们可以通过允许艺术家在动画中建模武器行为来进一步扩展这一点,当建模的子弹从枪中射出时,我们可以更新我们的弹药计数器,使其与动画完美同步。
此外,我们还提供了功能,允许玩家根据需要创建一个新的游戏对象并将其放入游戏世界,由物理引擎驱动。
你已经死了
现在我们有一个可以开枪的玩家,我们需要确保从那把枪发射的投射物能够与敌人碰撞,并且当敌人被击中时受到伤害并被摧毁。
为了实现这一点,我们将向我们的枪系统添加一个粒子碰撞器,并给我们的敌人添加一个伤害脚本,以便它们能够对被投射物击中做出反应。
世界粒子碰撞器
世界粒子碰撞器用于检测场景中粒子与其他碰撞器之间的碰撞。场景中的其他碰撞器可以是 Unity 可以分配给游戏对象的任何正常球体、盒子、胶囊、轮子或网格碰撞器。
动作时间 — 检测碰撞
-
从项目资源文件夹中导入 Target Dummy 模型。
-
为了确保你的游戏对象具有碰撞数据,请确保检查你的网格的导入设置中的 Generate Colliders 设置。
注意
然而,在许多情况下,使用简单的球体或盒子碰撞器来模拟敌人更节省成本且相当准确。如果你想在特定位置造成伤害,这是在移动设备上实现该效果的一种更经济的方式。
![动作时间 — 检测碰撞]()
-
现在我们已经有了目标游戏对象的碰撞数据,我们需要将粒子系统中的粒子碰撞器添加到我们之前创建的粒子系统中。我们通过选择组件 | 粒子 | 世界粒子碰撞器菜单中的组件来完成此操作。
![行动时间 — 检测碰撞]()
- 仅凭这一点,会导致粒子与其他对象发生碰撞,但我们的真正目的是在碰撞发生时通知游戏对象,以便我们的目标能够对被击中做出反应。
-
要做到这一点,我们在世界粒子碰撞器中选择发送碰撞消息,以便每个粒子游戏对象和参与碰撞的游戏对象都能接收到碰撞消息。
![行动时间 — 检测碰撞]()
- 我们可以在我们想要从粒子系统接收碰撞事件的对象附加的脚本中查找此事件。
-
创建一个名为
Damage的脚本,并将碰撞脚本附加到将处理伤害的 Test Dummy 对象上。using UnityEngine; using System.Collections; public class Damage : MonoBehaviour { void OnParticleCollision() { Debug.Log("Hit!"); } } -
更新
OnParticleCollision()方法以更新被粒子击中的对象的健康状态。当该物品的健康值达到零时,从游戏中移除测试假人。void OnParticleCollision() { HealthScript healthScript = GetComponent<HealthScript>(); healthScript.takeHit(1); Debug.Log("Hit!"); } -
在这里,我们将让我们的 Damage 脚本与
HealthScript通信,该脚本将定义我们的目标假人对象的健康状态,并在它们耗尽健康时移除它们。 -
将
HealthScript附加到目标假人上。using UnityEngine; using System.Collections; public class HealthScript : MonoBehaviour { public int initialHealth; private int currentHealth; // Use this for initialization void Start () { currentHealth = initialHealth; } public void TakeHit( int damage ) { currentHealth = currentHealth - damage; if ( currentHealth <= 0 ) { Destroy( this.gameObject ); } } }
在这里,我们将简单地通过告诉附加了此脚本的游戏对象销毁自己来移除游戏对象,以便在它受到足够伤害后。如果我们想让这个对象爆炸或采取其他行为,我们可以在销毁原始对象时在游戏对象的位置实例化一个爆炸预制体。
刚才发生了什么?
我们现在已经处理了游戏玩法中的另一部分。玩家可以开火,弹体将通过粒子系统发射,当粒子接触到另一个对象时,可以计算伤害并将此对象从游戏中移除。
玩(布)娃娃
到目前为止,我们只是在我们的武器造成伤害时从游戏中移除敌人。然而,我们希望敌人能够根据伤害的物理属性对武器造成的伤害做出逼真的反应。特别是,当手榴弹击中敌人时,爆炸的力量——目前根本未进行模拟,应该将它们抛来抛去。
为了实现这一点,我们将使用 Unity 的 Rag Doll 系统并将其分配给我们的目标假人,这样当它们受到伤害时,物理将驱动它们的运动。
行动时间 — 添加布娃娃
-
在项目中创建一个新的场景。
-
创建一个地面平面并为其添加纹理。
-
将目标假人模型导入场景。
![行动时间 — 添加布娃娃]()
-
通过选择GameObject | 创建其他 | Ragdoll来创建一个 Ragdoll
![操作时间 — 添加 rag doll]()
-
接下来,我们需要将我们的目标假人模型的骨骼映射到 rag doll 的骨骼层级。Unity 通过提供一个 rag doll 向导来简化这个过程,我们可以使用它来在两个骨骼层级之间进行映射。
-
选择 Ragdoll 项目右侧的圆圈,以显示可以连接到它的变换列表。
-
将soldier (Transform)拖放到列表中要映射的 Ragdoll 节点项的根上。
![操作时间 — 添加 rag doll]()
-
在层级视图中选择 Soldier 模型的根,这样你就可以看到 Ragdoll 映射到士兵的几何形状上了。
![操作时间 — 添加 rag doll]()
-
通过按播放按钮来检查场景。
刚才发生了什么?
你会注意到物理系统已经模拟了 rag doll 上的各种力,它已经坍塌了,因为重力是唯一作用在假人上的力,没有任何东西阻止它落到地面平面上。
这引出了我们必须解决的第一个问题——我们需要我们的模型在想要它受到物理世界影响之前保持刚性。我们可以通过关闭物理直到角色处于不应该控制其动作的状态,例如在爆炸的情况下,或者我们需要将角色连接到某个其他系统,以防止它倒下。
我们已经将 rag doll 和刚体物理系统附加到我们的敌人身上,这样它们在受到爆炸和其他世界内物理模拟的影响时可以正确地动画化。当我们打开 rag doll 时,Unity 使用物理而不是我们的动画数据来确定角色的状态。这导致了对世界的更真实反映。
摘要
在本章中,我们探讨了游戏中游戏玩法的基本要求。
具体来说,我们涵盖了:
-
如何为角色配备武器
-
如何让角色在开火动画中移动
-
如何使用粒子系统显示投射物
-
如何让武器对敌人造成影响
-
如何创建具有刚体物理的武器以及如何将 Rag Doll 分配给对象。
现在我们对游戏玩法脚本知识有了更好的理解,是时候看看如何优化我们的游戏,以便在目标设备上获得最佳性能了。虽然提供的脚本可以在任何平台上运行,正如我们将在下一章中看到的,有一些特定的技巧可以使它们在 iOS 平台上表现最佳。
第十一章。调试和优化
现在我们几乎完成了项目,是时候开始努力让系统尽可能高效地运行了。虽然我们通常会在整个开发过程中关注性能,但我认为在项目进行到更深入的阶段时再考虑这一点很重要,这样我们就可以看到如何根据我们在项目早期可能做出的某些有疑问的决定来重新设计产品。通过这种方式,我们将能够在通常构建应用程序的上下文中看到这些决定。
在本章中,我们将:
-
了解 Unity 中的调试选项
-
学习如何分析移动应用程序
-
了解对象池及其在移动设备上的重要性
-
学习如何使用 Beast Lighting 优化光照
本章将突出 Unity 游戏和移动设备上 Unity 游戏制作的一些关键区别。这一章将使我们的应用程序成为热门产品,而不仅仅是人们启动后几小时就会卸载的漂亮展示品。
调试
Unity 拥有一个与其他 Unity 平台同等重要的调试器。调试器将允许你在 Unity 脚本中直接设置断点,当游戏在目标设备上运行时,允许你暂停游戏的执行,以便观察或调整变量。这是一种非常强大的方法,在为设备开发时往往缺失。在过去,最好的办法是在满足某些条件时发出声音或改变屏幕颜色。今天,你可以观察变量值的变化,修改在设备上运行的脚本,并动态地改变游戏的执行流程,而对游戏的帧率影响不大。所以,让我们看看如何实现这一魔法。
在真实硬件上调试运行中的应用程序是你需要非常熟悉的事情。虽然 Unity 可以很好地帮助你模拟内容在你的 Mac 上,但没有任何东西能比知道你的应用程序在野外如何表现更胜一筹。
断点是在游戏中你希望为了调试目的暂停应用程序执行的地方。由于 Unity 脚本环境 MonoDevelop 与 Unity IDE 以及运行时集成,我们可以在脚本中插入断点,这将触发执行暂停。
行动时间 — 使用断点
-
将光标定位在你想暂停执行的那一行,并使用快捷键组合**Apple-**来创建断点。对于许多以键盘为中心的开发者来说,这是做事的首选方式。
![行动时间 — 使用断点]()
- 另一种方法是点击代码和行号左侧的脊柱区域。如果你是一个更倾向于使用鼠标的程序员,你会发现这是一种更自然地设置断点的方式。
![行动时间 — 使用断点]()
-
注意,你创建断点的行在代码旁边的脊柱上用红色圆圈突出显示。这代表了一个活动断点。
要禁用断点,只需点击红色圆圈或再次按**Apple-**,断点将被移除。
应该注意的是,如果你在不含实际代码的行上创建断点,断点实际上会在下一行可执行代码上执行。
-
运行应用程序。

我们现在可以观察到应用程序停止在断点所在的行。这给了我们观察断点达到时应用程序状态的机会。很多时候,在某种控制结构(如if语句)内部放置断点是有用的,以查看是否满足触发它的条件。
发生了什么?
你已经在你的应用程序中创建了一个断点,并且你的应用程序停止在指定的行上。当应用程序处于这种状态时,它会暂停,这样你就可以检查和/或更改应用程序中任何变量的状态。
行动时间 — 调试应用程序
将调试器附加到正在运行的应用程序的过程,以便与运行的应用程序通信并提供数据给用户,被称为将调试器附加到进程。
-
在你的 iOS 设备上部署并运行你的游戏。确保设备与将要进行调试的机器处于同一 WiFi 网络上,然后选择你想要连接的 iOS 设备。
-
在工具栏中选择调试按钮:
![行动时间 — 调试应用程序]()
- MonoDevelop 将启动 Unity 编辑器,经过一段时间后开始执行你的应用程序。你的应用程序将在玩家中开始运行,并在遇到代码中的第一个断点时停止:
![行动时间 — 调试应用程序]()
-
选择你想让 Unity 调试器附加到的游戏。在这张图片中,我们看到 Unity 编辑器作为一个可以附加到的进程,另一个被称为“null (mono)”。这是在你的 iOS 设备上运行的游戏。选择这个游戏实例。
发生了什么?
我们已经将调试器附加到了在我们 iOS 设备上运行的游戏实例。这非常有用,因为我们可以从实际设备获取真实的调试信息。在测试应用程序时,这样做很重要,因为有时应用程序在 PC 上的 Unity 中运行良好,但在实际设备上却失败。
行动时间 — 游戏单步执行
一旦你达到了断点并检查了该点的变量值,接下来你想做的事情就是向前执行应用程序,以便你可以观察应用程序随时间的变化。根据你的意图,有几种选项可供你在应用程序中向前执行。下面的截图显示了这些选项:Step Over, Step Into, Step Out, Pause和Detach Debugger:

-
按下Step Into图标,使应用程序开始运行。
-
如下图所示,应用程序在断点处暂停。当前执行的行以黄色突出显示:
![行动时间 — 游戏步骤]()
- 如前所述,当调试器附加时,你可以获取游戏中任何变量的值。然而,如果需要不断查找每个单独的变量,尤其是当我们想要观察随时间变化的变量时,这会变得很繁琐。你可以通过为感兴趣的变量设置监视来观察其值的变化。
-
在 MonoDevelop 中选择监视选项卡创建一个新的监视。在监视选项卡中输入单词Screen,MonoDevelop 将打开一个自动完成对话框,显示 Screen 对象的所有属性:
![行动时间 — 游戏步骤]()
- 我们可以看到,Screen 有一个附加的 currentResolution 属性。
-
选择currentResolution以创建此变量的监视。如果你选择的变量是一个对象,你将在其左侧看到一个三角形。点击此三角形将显示对象的所有属性,如下面的截图所示:

在这里,我们可以看到 Screen.currentResolution 对象的所有属性。
刚才发生了什么?
我们刚刚使用附加的调试器遍历了我们的应用程序,并添加了一个监视,以便我们可以观察变量随时间的变化。添加监视是检查游戏执行过程中变量变化的一种常见方式。当你开始编写复杂的脚本时,创建监视将帮助你确定在运行时是否发生了正确的行为。
性能分析
我们一直在构建我们的游戏并运行了一段时间,看起来表现良好,但为了真正确定我们的游戏表现如何,我们必须要进行性能分析。性能分析是收集有关游戏每个部分如何表现的信息的行为,以便我们可以优化导致性能瓶颈的代码或资源。
我们可以对游戏的运行方式做出一些假设,并盲目地优化代码,因为按照某种方式做应该会更快,但现实中,最大的性能提升直到您看到分析器告诉您的游戏信息时才会变得明显。要成功分析一个应用程序,必须真正客观地查看数据,因为性能不佳可能是由纹理大小、着色器、脚本、物理、绘制调用,甚至是添加雾气这样简单的事情引起的。
让我们遍历我们的当前应用程序,并了解分析它的过程。
操作时间——微调应用程序(专业版本)
现在,我们将对我们的应用程序进行调整,以确保它在任何环境中都能良好运行,并保持一致的行为。
-
通过选择窗口菜单中的分析器来启动分析器:
![操作时间——微调应用程序(专业版本)]()
- 分析器将启动并显示 Unity 的分析接口。如果您熟悉 XCode 工具的 Instruments,您将在 Unity 分析器中找到许多相同的设计和界面概念:
![操作时间——微调应用程序(专业版本)]()
- 分析器将显示一系列不同设置下的指标:CPU 使用率、渲染、内存、音频和物理。您可以通过点击分析器左上角的 X 来移除分析器。
-
通过点击分析器左上角的 X 来移除音频分析器:
![操作时间——微调应用程序(专业版本)]()
-
通过点击添加分析器下拉菜单并选择音频分析器来将音频分析器添加回来:
![操作时间——微调应用程序(专业版本)]()
- 已经添加的分析器将会变灰。在这个例子中,音频和物理都可以添加到分析器中。
-
通过按下播放按钮开始游戏。立即您会看到分析器填充了来自分析会话的数据:

刚才发生了什么?
我们已经使用内置的 Unity 分析器来检查我们应用程序的性能,并探索它返回的不同指标,以便我们可以对应用程序中的瓶颈有所了解。现在我们已经做了这件事,我们可以识别一些热点,并查看一些修复我们发现的问题的方法。
对象池化——进入池中
从我们的分析会话中我们了解到,在移动平台上最大的性能损失之一是 GameObject 的持续创建和销毁。正如您可能之前在测试我们发射武器时注意到的,游戏中的暂停现象时有发生。让我们来看看为什么会这样发生。
当在移动设备上的 Unity 中创建GameObject类的实例时,设备必须为这个新的GameObject实例分配内存,并可能清除由某些可能不可见对象使用的内存。这个过程,称为垃圾回收,所需的时间足以导致你在游戏中体验到的暂停。这种情况发生的原因是由于目标设备上可用的内存有限以及收集旧对象所需的时间。Java 开发者,尤其是那些勇敢地编写平台游戏的开发者,非常熟悉与垃圾回收相关的问题。
通常,人们会通过使用对象池来解决与垃圾回收相关的一些问题。对象池允许你预先分配一定数量的对象,并通过它们循环使用,这样就不会创建新的对象。在我们的武器示例中,我们会在游戏对象池中分配一定数量的对象,这些对象代表我们希望一次显示的最大子弹数量。当子弹达到玩家一定距离时,我们可以移除它们,并在对象池中为新对象腾出空间,以便显示下一个可见对象。
这种对象池的实现是由 Unity3D 论坛成员创建的,并且还扩展了粒子系统和音频。
using UnityEngine;
using System;
using System.Collections;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ObjectPool : MonoBehaviour
{
public static ObjectPool instance;
/// <summary>
/// The object prefabs which the pool can handle.
/// </summary>
public GameObject[] objectPrefabs;
/// <summary>
/// The pooled objects currently available.
/// </summary>
public List<GameObject>[] pooledObjects;
/// <summary>
/// The amount of objects of each type to buffer.
/// </summary>
public int[] amountToBuffer;
public int defaultBufferAmount = 3;
/// <summary>
/// The container object that we will keep unused pooled objects /// so we dont clog up the editor with objects.
/// </summary>
protected GameObject containerObject;
void Awake ()
profilingobject pooling{
instance = this;
}
// Use this for initialization
void Start ()
{
containerObject = new GameObject("ObjectPool");
//Loop through the object prefabs and make a new list for //each one.
//We do this because the pool can only support prefabs set to it //in the editor,
//so we can assume the lists of pooled objects are in the same //order as object prefabs in the array
pooledObjects = new List<GameObject>[objectPrefabs.Length];
int i = 0;
foreach ( GameObject objectPrefab in objectPrefabs )
{
pooledObjects[i] = new List<GameObject>();
int bufferAmount;
if(i < amountToBuffer.Length) bufferAmount = amountToBuffer[i];
else
bufferAmount = defaultBufferAmount;
for ( int n=0; n<bufferAmount; n++)
{
GameObject newObj = Instantiate(objectPrefab) as GameObject;
newObj.name = objectPrefab.name;
PoolObject(newObj);
}
i++;
}
}
/// <summary>
profilingobject pooling/// Gets a new object for the name type provided. If no object type /// exists or if onlypooled is true and there is no objects of that /// type in the pool
/// then null will be returned.
/// </summary>
/// <returns>
/// The object for type.
/// </returns>
/// <param name='objectType'>
/// Object type.
/// </param>
/// <param name='onlyPooled'>
/// If true, it will only return an object if there is one currently /// pooled.
/// </param>
public GameObject GetObjectForType ( string objectType , bool onlyPooled )
{
for(int i=0; i<objectPrefabs.Length; i++)
{
GameObject prefab = objectPrefabs[i];
if(prefab.name == objectType)
{
if(pooledObjects[i].Count > 0)
{
GameObject pooledObject = pooledObjects[i][0];
pooledObjects[i].RemoveAt(0);
pooledObject.transform.parent = null;
pooledObject.SetActiveRecursively(true);
return pooledObject;
}
else if(!onlyPooled) {
return Instantiate(objectPrefabs[i]) as GameObject;
}
break;
}
}
//If we have gotten here either there was no object of the specified //type or none were left in the pool with onlyPooled set to true
return null;
profilingobject pooling}
/// <summary>
/// Pools the object specified. Will not be pooled if there are no /// prefab of that type.
/// </summary>
/// <param name='obj'>
/// Object to be pooled.
/// </param>
public void PoolObject ( GameObject obj )
{
for ( int i=0; i<objectPrefabs.Length; i++)
{
if(objectPrefabs[i].name == obj.name)
{
obj.SetActiveRecursively(false);
obj.transform.parent = containerObject.transform;
pooledObjects[i].Add(obj);
return;
}
}
}
}
ObjectPool代码非常直观。它公开了一些变量,以便你可以在 Unity IDE 中直接配置它。这里实现的ObjectPool旨在同时池化多种类的对象。你不需要为每个想要池化的GameObject类型实现一个实现。ObjectPool将你想要池化的每种对象类型存储在ObjectPrefabs数组中。

默认情况下,池将创建每种类型最多三个对象。如果你想配置单个对象可以拥有的对象数量,展开Amount To Buffer元素并设置该大小。
行动时间 — 使用对象池进行优化
现在我们有了游戏对象池和性能分析的能力,我们可以看看这种优化是如何提高我们的应用程序性能的。我们将通过替换玩家开火时创建子弹所采用的GameObject实例化方法来实现这一点。
-
按照之前的行动时间 —连接游戏到性能分析器,并运行它,发射大量子弹:
![行动时间 — 使用对象池进行优化]()
- 从这个对象配置文件中我们可以看到,当武器发射时,性能会显著下降。原因是我们在实时创建 GameObject。我们可以看到,当这些对象从视图中移除并被收集时,应用程序的性能变得更加一致。这类问题是明显的性能瓶颈,我们可以通过优化来提高游戏性能。
-
创建一个名为PoolManager的GameObject:
![执行动作 — 使用对象池进行优化]()
-
将ObjectPool脚本添加到PoolManager:
![执行动作 — 使用对象池进行优化]()
-
将你想要池化的预制体添加到Object Prefabs数组中。你可以通过将预制体从Hierarchy视图拖动到Object Prefabs数组中完成此操作:
![执行动作 — 使用对象池进行优化]()
-
用使用Object Pool的优化版
fireWeapon脚本替换现有的fireWeapon脚本:void fireWeapon() { GameObject bullet = ObjectPool.instance.GetObjectForType( "Bullet" , true); bullet.transform.position = spawnPoint.transform.position; bullet.transform.rotation = spawnPoint.transform.rotation; }我们在这里所做的是替换了原本用于从武器中创建新子弹的
GameObject.Instantiate()方法。 -
再次启动游戏并查看Profiler配置文件。发射大量投射物并检查性能。
刚才发生了什么?
我们刚刚通过对象池提高了应用程序的性能。正如你所见,应用程序的性能现在是一致的。这正是我们追求的稳定帧率。
我们不是为创建和删除大量GameObject实例而支付惩罚,而是创建一个这些对象的池,并简单地改变它们的位置、旋转、状态和可见性。这是一个在游戏开发中经常使用的老技巧,但随着现代计算机和图形能力的兴起而失传,但正是我们在移动设备上确保最佳性能所需要的。
放出野兽
当你试图提高应用程序的性能时,你经常需要在应用程序的视觉表现和性能之间做出权衡。这次,我们将通过使用 Illuminate Labs 的集成 Beast 光照贴图系统来执行优化,这将提高我们应用程序的性能和视觉质量。
我们游戏中照明的目标是提供我们世界的真实描绘。通常,世界中的物体会有一些基础材质或纹理来描述它们的自然表面。引擎使用的照明算法随后会计算当物体渲染时,光线对其表面像素或 texels 的影响。
如其名所示,光照贴图是利用光照数据(发射属性、真实光源等)并从这些光源发射光线的过程,就像它们在场景中活跃一样。然后,引擎将存储这些光源的实际效果,并将其存储在将在运行时映射到纹理上的纹理图中(Unity 将此过程称为烘焙光照)。因此,你可以基于大量计算密集型光源创建一个非常复杂的光照环境。你还可以将所有这些信息烘焙到场景的纹理图中,从而在不增加场景中光源成本的情况下提高视觉效果。
由于光照是你在移动设备上可以执行的成本较高的操作之一,因此使用光照贴图代表了一次重大的性能提升。虽然光照贴图最终将被着色器中的逐像素光照所取代,但今天的移动硬件还不足以完成这项任务。
因此,让我们看看我们如何使我们的应用程序实现这一点。
动手实践 — 生成 Beast 光照贴图
为了说明光照贴图如何改进我们的应用程序,我们将使用我们的城市级别场景并为其添加 Beast 光照贴图。
-
准备场景进行光照贴图的第一步是确保我们想要为光照贴图生成的任何对象都被声明为静态。Unity 使用这个声明来做出假设,即对象在场景中不会移动、缩放或以任何方式改变。记住,我们只想在静态几何体上烘焙光照贴图,因为我们正在根据场景中给定时间的光源位置生成光照。如果我们尝试在移动的对象上使用这种方法,例如,你会发现即使对象相对于光源改变了方向,光照和阴影的计算看起来就像对象在另一个地方一样:
![动手实践 — 生成 Beast 光照贴图]()
- 在这里,我们指定了我们的白桦树预制件之一是静态的。同样,我们也可以将这些相同的设置添加到地形中。
-
使用游戏对象 | 创建其他 | 点光源在场景中添加一个光源。将此光源放置在我们的场景中。这是我们想要将光照效果烘焙到场景中的光源。现在,由于我们的对象被定义为静态的,让我们添加一个我们想要烘焙到对象中的光源:
![动手实践 — 生成 Beast 光照贴图]()
-
观察场景,看看在光照正常渲染时一切看起来是什么样子。这将给我们一个概念,如果我们正确执行操作,光照贴图场景应该是什么样子:
![动手实践 — 生成 Beast 光照贴图]()
- 现在我们已经为光照贴图提供了一些光照数据,我们可以开始烘焙过程。
-
在窗口菜单中选择光照贴图选项,以打开 Beast 光照贴图界面:
![动手实践 — 生成 Beast 光照贴图]()
- 这将打开 Unity 光照贴图界面,允许我们配置 Beast 光照贴图会话:
![动手实践——生成 Beast 光照贴图]()
-
当界面打开时,选择场景中添加的点光源,然后点击烘焙标签:
![动手实践——生成 Beast 光照贴图]()
-
我们可以将我们的简单灯光烘焙到场景的光照贴图中。按下烘焙按钮,并观察调试输出窗口:
![动手实践——生成 Beast 光照贴图]()
- 输出窗口告诉我们,它无法烘焙灯光,因为场景中的几何形状没有 UV 数据。记住,我们的光照贴图是作为纹理贴图添加的,因此,如果几何形状没有正确生成 UV,则无法生成光照贴图。
-
选择 Unity 报告没有 UV 数据的几何形状,并选择其FBX 导入器。点击生成光照贴图 UV按钮,以确保 Unity 为这块几何形状生成 UV 信息:
![动手实践——生成 Beast 光照贴图]()
- 现在当你烘焙场景时,Unity 将能够生成一个正确光照贴图化的场景。
-
在层次结构视图中选择点光源,并将其从场景中删除。当你运行游戏时,你会注意到游戏看起来与之前完全一样,但现在它正在运行,没有场景中灯光的性能开销。
刚才发生了什么?
Unity 在幕后所做的是,从 Unity IDE 中提取所有资源和脚本,并组合成一个玩家,该玩家能够根据用户的输入回放内容及其所有场景。这是一个非常重要的概念,因为 Unity IDE 中的内容在很大程度上是平台无关的,并且可以在 Unity 环境中简单重新编译后轻松重新部署。这个玩家是实际部署到 iOS 设备上的应用程序。
摘要
在本章中,我们简要地学习了调试、性能分析和优化 Unity 项目的主题。然而,关于游戏优化,它本身就可以写成一本书,而且其中很多内容与特定应用程序的设计有关。
具体来说,我们涵盖了:
-
如何将 Unity 调试器附加到运行中的应用程序
-
如何分析 Unity 应用程序
-
使用对象池提高移动应用程序性能
-
使用 Beast 光照贴图提高性能和视觉效果
现在我们已经分析了我们的应用程序,并专门工作以提高其性能,现在是时候调查我们游戏的货币化和在 AppStore 上发布它了。
第十二章:商业化:从您的创作中获得丰厚回报
根据美国市场营销协会的定义,市场营销是规划并执行想法、商品和服务的构思、定价、促销和分销的过程,以创造满足个人和组织目标的交换。虽然大多数人将市场营销等同于广告,但市场营销包括市场分析、价值主张、产品差异化、市场策略等等。虽然我们不需要所有这些来构建我们的游戏,但我们需要检查市场营销的某些部分,以确定将产品推向市场并从我们的辛勤工作中获得收益的最佳方式。我们应该收费 99 美分,还是免费并提供广告支持,或者尝试将游戏许可给第三方?这些都是本章将帮助我们解答的问题。
在本章中,我们将讨论:
-
如何将 iAds 添加到产品中
-
如何添加应用内购买
-
如何将内容发布到 Unity Asset Store
-
如何发布我们产品的最终版本
-
如何使用 iTunes Connect 跟踪成功
这是我们将高质量的应用程序推向市场,产生收入并使客户满意的关键一步——我们的首要任务。
让我们继续吧...
商业模式生成
在撰写本文时,有超过 2 亿台 iOS 设备掌握在消费者手中,App Store 的应用程序下载量近 150 亿次,App Store 销售中向开发者支付了超过 25 亿美元。虽然这对普通开发者来说已经足够令人垂涎,但还必须考虑到 App Store 中超过 30 万个应用程序,每月增长率为 11,000 至 15,000 个。因此,不能仅仅依靠发布一个标题并期待它赚钱。您需要一种方法来定位客户,将您的应用程序安装到他们的设备上,并从他们那里获取金钱——这种方法通常被称为商业模式。
虽然通常人们会在计划建立盈利性企业时将此作为第一个话题进行讨论,但大多数人会在第一章中感到眼花缭乱,然后他们会把书放回书架上。
随着苹果 App Store 生态系统的不断发展,四种成功的商业模式已经出现,成为利用您的创作获得收益的有效途径:纯应用销售、广告、应用内购买和 marketplace 组件销售。
纯应用销售
这是发布应用程序到 App Store 的传统方法。您制作一个应用程序,根据某种定价策略设定价格,然后将其发布到 App Store 并等待客户购买您的应用程序。
广告
广告模式通常用于免费分发的应用程序,尽管一些应用程序收费并包含广告。然而,当游戏开始或进行时,应用程序会向广告网络发出请求,在游戏中包含广告内容。当人们观看这些广告或与之互动时,广告供应商会支付应用程序创建者。
应用内购买
应用内购买模式正在迅速成为移动应用程序的首选商业模式。通常有两种与应用内购买一起使用的模式。在第一种模式中,开发者开发了一种货币形式,玩家使用这种货币在游戏世界中购买物品。在第二种模式中,开发者销售额外的武器、轨道、车辆等,然后将这些添加到现有的游戏中以增强玩家的体验。
市场组件
虽然并不是真正用于销售游戏的模式,但如果你在为游戏构建组件和预制件,并且决定它们对你不再有用,或者你想要在不发布整个游戏的情况下赚一些钱,那么在市场上销售组件是可以考虑的。此外,如果你为常见的开发问题开发了一个新颖的解决方案,你应该在 www.gameprefabs.com 或 Unity 资产商店本身销售它。
没有任何东西阻止你选择这些商业模式之一,或者在一个单一的产品中实现所有这些。你需要做的就是平衡你赚钱的愿望与最终用户的体验。只要你不非常侵扰用户玩游戏,这种模式就不会让用户感到烦恼,但如果他们开始感觉你好像在欺骗他们,你将会杀死那只下金蛋的鹅。
准备销售应用程序的时间 - 准备你的应用程序上市
我们终于到达了我们一直等待的点。我们将为顾客准备我们的游戏进行分发,设定价格,然后发布应用程序以供销售。与到目前为止我们一直在使用的开发者门户不同,为了发布我们的内容供消费,我们需要使用位于:itunesconnect.apple.com 的 iTunes Connect 门户。
-
使用你通常用于连接到开发者门户的凭据登录 iTunes Connect 门户:
![准备销售应用程序的时间 - 准备你的应用程序上市]()
-
选择 管理你的应用程序 链接以开始添加应用程序的过程:
![准备销售应用程序的时间 - 准备你的应用程序上市]()
-
选择你国家的 主要语言 并输入你的公司名称或你将合法经营的名字:
![准备销售应用程序的时间 - 准备你的应用程序上市]()
-
接下来,您需要告诉 iTunes Connect 您打算创建哪种类型的应用。如果您能够创建 iOS 应用和 OSX 应用,请简单地选择 iOS 图标:
![操作时间 — 准备应用销售]()
-
现在 iTunes Connect 已知我们试图创建的应用类型,请输入代表您应用的 应用名称、SKU 编号 和 包 ID:
![操作时间 — 准备应用销售]()
-
输入这些信息后,就是设置价格的时候了。输入产品可用的日期。将价格设置为 免费。点击 查看定价矩阵 选项,查看您还有哪些定价选项。
注意
如果您想通过批量购买为教育机构提供折扣,请勾选此选项的复选框。
- 第二个选项 自定义 B2B 应用 是针对向企业客户提供批量购买的应用程序(
www.apple.com/business/vpp/)。如果您勾选此复选框,您的游戏将不会在常规应用商店中提供。
![操作时间 — 准备应用销售]()
- 第二个选项 自定义 B2B 应用 是针对向企业客户提供批量购买的应用程序(
-
接下来,输入游戏的元数据。这是用于填充 iTunes 应用商店页面的信息:
![操作时间 — 准备应用销售]()
-
为了确保应用被正确评级并针对适当的受众,请选择适当的选项。最重要的是,确保任何成熟度和暴力的评级都得到适当体现,否则您的应用将被拒绝。如果您的应用包含长期图形、虐待、色情或裸露内容,则不允许在应用商店销售此类内容:
![操作时间 — 准备应用销售]()
-
最后一步是为 iTunes Connect 提供艺术作品,这些作品将在 iTunes 商店中使用。根据您的目标平台,您可能需要提供最多三件艺术作品。
您应用图标的大版本是将在 App Store 中使用的版本。它必须至少为 72 DPI,且最小尺寸为 512x512 像素(不能放大)。它必须是平面艺术作品,没有圆角。
iPhone 和 iPod touch 截图 必须是
.jpeg,.jpg,.tif,.tiff或.png文件,分辨率为 960x640, 960x600, 640x960, 640x920, 480x320, 480x300, 320x480 或 320x460 像素,至少 72 DPI,并且使用 RGB 颜色空间。iPad 截图必须是
.jpeg,.jpg,.tif,.tiff, 或.png文件,分辨率为 1024x768, 1024x748, 768x1024, 或 768x1004 像素,至少 72 DPI,并且使用 RGB 颜色空间:![操作时间 — 准备应用销售]()
-
现在所有应用设置都已配置完毕,应用已准备好分发。在应用的版本下,您会注意到状态是 准备上传,这意味着苹果正在等待我们上传游戏以供审查。
选择应用程序图标,这样我们就可以上传游戏的最终二进制版本以供审查:

现在我们已经准备好以我们设定的价格发布我们的应用程序,如果它通过了苹果的审查,它将按照我们上面指定的可用日期上市。
刚才发生了什么?
我们刚刚为 iTunes 创建了所有销售我们应用程序的元数据。在最终发布之前,剩余的步骤是将最终二进制文件提交给苹果。我们将在添加了商业模式的其它组件到应用程序后讨论这一点。
行动时间 — 添加 iAds
从你的应用程序中产生收入的一个低风险策略是引入广告模式。你可以在应用程序的主菜单或加载屏幕中轻松显示广告,而不会对你的客户的使用体验产生负面影响,同时还能产生合理的收入。
由于 Unity 本身不提供向应用程序添加 iAds 的功能,我们将通过 Prime31 插件展示如何获取这一功能。虽然当然也可以实现自定义插件,但这超出了本书的范围。更重要的是,将时间用于构建应用程序是更好的选择:
-
访问网站
www.prime31.com并下载 iAds 插件:![行动时间 — 添加 iAds]()
- 虽然插件本身不是免费的,但你应该通过广告获得的收入超过插件的成本,以证明其购买是合理的。
-
通过资产 | 导入包 | 自定义包菜单命令导入购买的 unitypackage:
![行动时间 — 添加 iAds]()
-
在你的机器上选择 unitypackage,以便 Unity 开始导入其资源:
![行动时间 — 添加 iAds]()
-
Unity 现在将开始导入插件,并显示一个对话框确认你希望将包中的所有资源导入到项目中:
![行动时间 — 添加 iAds]()
- 确保你已经选择了所有项目,然后按下导入按钮。当导入过程完成后,你应该在层次结构视图中看到两个新的节点:
![行动时间 — 添加 iAds]()
-
iAds 系统附带一个用于显示广告的现成 Prefab,所以让我们将其拖入我们的场景中。
![行动时间 — 添加 iAds]()
- 现在添加了这一功能后,我们的应用程序已经准备好显示广告。
-
在 iTunes Connect 中打开应用程序并选择设置 iAd 网络:
![行动时间 — 添加 iAds]()
-
通过选择启用 iAds按钮来为你的应用程序启用 iAd 广告网络:

注意
确保适当地设置您的目标受众。这将确保您为您的目标受众适当地投放广告。
刚才发生了什么?
我们刚刚将 iAd 网络添加到我们的应用程序中。当运行我们的游戏时,现在我们将在应用程序的主场景中看到由苹果的受众群体和广告引擎纯粹驱动的 iAd 广告。当人们与游戏中的广告互动时,你将收到苹果的支票。
应用内购买
游戏中的应用内购买为用户提供通过与应用商店交易购买升级、访问级别、武器或其他内容的能力。然后应用商店将与游戏通信,通知它购买已被授权,并且内容应在游戏中解锁。所有这些都在 iOS SDK 和应用商店商业系统中进行,因此您无需处理信用卡交易或跨线的商业交易安全。苹果将处理整个流程,您将以与常规应用程序购买相同的方式收到钱。
在内部,应用内购买使用 iOS SDK Store Kit API 完成。此 API 负责与 App Store 通信并从用户那里收集付款:

到目前为止,Unity 尚未在其 iOS 版本的 Unity 产品中为应用程序提供添加应用内购买的功能的渠道,因此,就像 iAds 一样,我们将不得不依赖第三方插件来实现此功能。就像之前一样,我们将使用 Prime31 的 StoreKit 插件。
订阅类型
有四种不同类型的应用内购买:非消耗品、消耗品、订阅和自动续订订阅:
-
非消耗品 应用内购买代表一类物品,您只需购买一次即可持续访问。这些购买可以在同一 iTunes 账户的设备之间转移。非消耗品物品的例子包括游戏关卡、武器、车辆和其他永久可用物品。
-
消耗品 应用内购买代表一类物品,正如其名所示,玩家购买后使用,每次玩家希望使用该物品时都需要额外购买。消耗品物品的例子包括经验值、健康包、加速器、弹药包等等。
-
订阅 与杂志和报纸订阅类似,持续有限的时间,然后在该时间段结束后需要用户续订。虽然这在游戏圈中并不典型,但订阅的一个例子可能是足球游戏中的防守教练,这允许玩家不必调用防守战术。教练将在订阅期间可用,并在订阅到期后,游戏将要求玩家再次调用防守战术。
-
可自动续订的订阅是指用户可以购买一定时间段的内购内容。在期限结束时,订阅将自动续订,除非用户选择退出。可自动续订的订阅的例子与普通订阅相同,只是玩家在取消订阅之前将继续拥有防守教练。
交付模型
内购可以使用两种交付模型之一与 iTunes 配合使用,即内置模型和服务器模型。
在内置产品模型中,为您的应用向客户交付内购内容所需的一切都内置到游戏中。这意味着所有可消耗和非可消耗的内购内容都已经内置到游戏中。因此,这意味着所有购买访问将通过应用标识符定义。由于单个应用将负责维护这些首选项,因此当用户在不同设备之间移动时,它们可能会丢失,所以您必须确保任何会保留该状态的偏好都存储在应用首选项中,因为它们由 iTunes 备份。另一个解决方案是使用 iCloud 或类似的服务来托管这些应用首选项:

在服务器产品模型中,一个单独的服务器维护内购内容和与 App Store 的交易。然后 App Store、应用和服务器协同确认购买并向 iOS 设备交付内容。在这个模型中,游戏将直接向应用交付新关卡或其他内容。虽然这个模型很有吸引力,但目前它不适合 Unity iOS 应用,因为苹果要求所有内容都必须在 App Store 的应用中分发。这里列出是为了完整性:

注意
由于内购过程不提供修补应用的能力,并且由于 Unity 不会直接加载不在应用包中的内容,因此预计您计划通过内购交付的所有内容都将通过 App Store 中的应用版本交付。
执行时间 — 添加内购
-
定义内购的第一步是在iTunes Connect中向应用添加购买类型。在应用配置文件页面上,选择门户中的管理内购链接:
![执行时间 — 添加内购]()
-
在内购屏幕上选择创建新以开始过程:
![执行时间 — 添加内购]()
-
接下来,我们想要设置一个代表用户新枪型的内购,因此选择非消耗品:
![执行时间 — 添加内购]()
-
在详细信息区域提供产品的参考名称和产品 ID。产品 ID是您稍后在 Store Kit API 中引用产品时将使用的。您可以随意命名产品 ID,它仅用于您自己的内部目的。您应该遵循与产品相同的命名约定,以确保您不会在产品、版本等之间发生名称冲突:
![执行时间 — 添加内购]()
-
由于 iOS 应用程序和 iTunes 支持本地化,您需要设置产品在商店中显示的语言。选择添加语言并输入产品的商店详细信息:
![执行时间 — 添加内购]()
-
接下来设置产品的定价。内购的层级与常规 iTunes App Store 购买的层级相似:
![执行时间 — 添加内购]()
-
接下来我们需要为 App Store 审查团队添加一个截图。目前尚不清楚这个截图是如何使用的,但您无法在没有这个 320x240 图像的情况下提交您的内购:
![执行时间 — 添加内购]()
-
完成此更新后,请检查内购列表,以确保输入的详细信息与您提交的游戏内容相匹配:
注意
购买目前正在进行审查。
![执行时间 — 添加内购]()
- 完成此操作后,我们可以编写必要的插件代码,以便在 Unity 游戏中访问此内购。
-
将 StoreKitManager 预制体从 Prime31 插件拖入场景。StoreKitManager 负责与 iOS 设备上的原生 Objective-C 库通信,以处理与 Apple StoreKit API 的集成。
-
将 StoreKitEventListener 预制体拖入场景。随着 StoreKit 事件在 iOS 设备上发生,StoreKitEventListener 将从操作系统接收有关它们的通知。
-
为了保持简单,我们将创建一个简单的 GUI 按钮,当点击时,将使用户购买 BFG 武器。
-
创建一个名为 InAppPurchases 的新脚本:
using UnityEngine; using System.Collections.Generic; public class InAppPurchases : MonoBehaviour { #if UNITY_IPHONE void OnGUI() { if( GUI.Button( new Rect( 0, 0, 50, 40 ), "Purchase BFG" ) ) { bool canMakePayments = StoreKitBinding.canMakePayments(); if ( canMakePayments ) { StoreKitBinding.purchaseProduct( "bfg_001", 1 ); } } #endif }
刚才发生了什么?
我们刚刚创建了第二个收入渠道,内购。我们刚刚创建了一个机制,通过该机制,用户可以通过在游戏界面上的按钮按下购买 BFG 武器来使用它。
执行时间 — 向 Unity 资产商店添加内容
在您开发游戏的过程中,您可能已经开发了一些资产,如音乐、艺术作品或插件,您希望与其他 Unity 社区的用户分享。Unity Asset Store 通过 Unity IDE 本身提供了一个渠道来分发这些作品,并与您分享 70% 的收入——即使您选择不将其提交到 Apple App Store,这也是在您的游戏中赚钱的一种方式。事实上,Unity Asset Store 上有完整的游戏项目可供开发者参考。我们将提交我们游戏项目的一部分到 Asset Store,以便人们可以在他们的开发项目中下载并从中学习:
-
将内容发布到 Unity Asset Store 的第一步是创建一个账户。如果您一直在跟随,您已经创建了一个客户账户并可以跳过此步骤。否则,打开 Unity 编辑器并选择 窗口 | Asset Store。
-
选择 创建账户 并完成账户创建过程:
![添加内容到 Unity Asset Store 的时间]()
-
从(
download.unity3d.com/assetstore/asset-store-provider-distribution-agreement.pdf)下载并审查 Asset Store 提供者协议。阅读此文档以了解 Unity Technologies 与 Asset Store 提供者之间的关系。 -
从(
download.unity3d.com/assetstore/asset-store-submission-guidelines.pdf)下载并审查 Asset Store 提交指南。这些指南概述了提交内容到 Asset Store 时所需的各项约定。 -
在 Asset Store 窗口中搜索 Asset Store Tools 并下载它们。此图像中用 Unity 图标表示:
![添加内容到 Unity Asset Store 的时间]()
-
下载后,就像导入任何其他 Unity 包一样导入 Asset Store Tools 包。这些工具具有准备内容提交到 Asset Store 所需的框架和脚本:
![添加内容到 Unity Asset Store 的时间]()
-
在下载的插件 AssetStoreTools 层级下,您将找到一个
Templates目录。此目录包含您希望修改以提交到 Asset Store 的所有图像模板。这些关键图像是 Asset Store 将用于创建其自身商店页面的:图像类型 大小 描述 大 1010x389 用于在屏幕上以包为主要项目时推广包。您应该放置图片的实时区域是 550x330 中 460x250 当需要较小的视图大小时,用于替换大图像。您应该放置图片的实时区域是 285x200。 小型 200x258 用于在较小的框视图中推广图像。你应该放置图像的实时区域是 175x100。 图标 128x128 用于下载和列表视图 -
更新你的图片以符合 Unity Technologies 的布局建议,以确保文本正确显示,并且你的图片在商店显示时不会被裁剪:
![行动时间 — 向 Unity Asset Store 添加内容]()
-
在游戏项目中创建一个
AssetStore目录。你想要分发的所有资产都放在项目的根目录下。在我们的例子中,我们正在分发整个游戏项目,所以我们不需要做任何特殊的事情,因为 Asset Store 打包器在向最终用户分发内容时将直接复制我们的文件夹结构和资产。 -
在
AssetStore目录中,为你的关键图像创建一个名为Screenshots的文件夹。这些截图应至少为 640x480。这些是客户在评估 Asset Store 中的资产时可以查看的图像:![行动时间 — 向 Unity Asset Store 添加内容]()
-
启动 Unity Asset Store 包管理器,它将生成提交给 Asset Store 所需的包:

刚才发生了什么?
我们刚刚将我们的游戏项目发布到 Unity Asset Store,供其他开发者购买。这是我们作为内容开发者可用的最终收入来源之一,使我们能够出售像音乐、艺术品、脚本或整个游戏这样的简单产品,供其他开发者购买。
使用 iTunes Connect 衡量成功
一旦你的游戏产品在市场上发布,你将想要衡量成功,以便你可以确定最佳方式来增长市场机会并最大化收入。收集这些信息的渠道是 iTunes Connect。
行动时间 — 我们的游戏表现如何?
为了说明使用 Unity3 可能的内容类型,我们将从在设备上运行一个真实的应用程序开始。你必须执行一系列步骤才能做到这一点,特别是如果你是 iOS 平台的新开发者,所以我将花些时间确保你理解正在发生的事情。如果你没有正确地做事,iOS 开发可能会非常苛刻,但一旦你走过几次,它就会变得自然而然。
我们将逐步介绍制作可用于 Unity3 的商业内容并部署到 iOS 设备的必要步骤:
-
在
itunesconnect.apple.com上打开iTunes Connect门户或从正常的 iTunes 开发者门户启动它:![行动时间 — 我们的游戏表现如何?]()
-
一旦进入门户,请选择门户链接中的销售和趋势链接。这个区域包含了我们发布到 App Store 的所有内容的所有人口统计数据报告:

发生了什么?
我们刚刚探讨了如何查看 iTunes Connect 门户并确定如何衡量你发布的游戏的成功。在 iTunes Connect 中,你可以查看游戏购买者的各种不同维度的统计数据,例如时间、地理、设备等。这将帮助你衡量促销活动、广告活动或新设备发布等事物的影响。
摘要
你面前还有一个最后的挑战,这个挑战比我们迄今为止所做的一切都更重要,那就是真正创建并发布你自己的游戏。人们之所以止步不前,有很多原因,但请意识到,你现在已经拥有了创建游戏、发布游戏并从中赚钱所需的所有信息。你可能无法辞去日常工作,但如果你坚持下去,你很可能有一天会创造出下一个伟大的游戏品牌或游戏公司。
不要担心创造下一个最佳游戏,不要担心拥有最新和最好的图形,不要怀疑那个游戏想法是否会卖出百万份(我无法想象创造《愤怒的小鸟》的人知道它会以这种方式走红)——专注于创造一些东西,然后创造另一些东西,在你意识到之前,你将会创造出一些伟大的东西。但如果你试图将下一个《上古卷轴》、《魔兽世界》、《使命召唤》或《孤岛惊魂》作为你的第一个项目,你是无法做到的。
因此,与其说再见,我宁愿说:“我们在 App Store 上再见。”祝你好运,最重要的是……玩得开心!
第十三章:快速问答 - 答案
第一章
| 1 | e |
|---|---|
| 2 | d |
| 3 | false |
| 4 | b |
| 5 | false |
第二章
| 1 | b,c |
|---|---|
| 2 | d |
| 3 | false |
| 4 | false |
| 5 | false |




































































































































































































浙公网安备 33010602011771号