原文:
zh.annas-archive.org/md5/2F967148E2CB27E3CC5D9AF5E1B4F678
译者:飞龙
前言
在这本书中,我们探索了不断扩展的移动游戏开发世界。使用 Unity 3D 和 Android SDK,我们学习如何创建移动游戏的各个方面,同时利用 Unity 5.0 和 Android L 的新功能。每一章都探索了开发谜题的一个新部分。通过探索移动平台开发的特殊功能,书中每个游戏的设计都是为了提高您对这些功能的理解。我们将在书中完成总共四个完整的游戏,以及您需要创造更多游戏的全部工具。
我们将要制作的第一款游戏是井字游戏。这个游戏的功能与经典的纸质版本完全一样。两个玩家轮流在网格中填入他们的符号,第一个连成一线的三个相同符号的人获胜。这是探索我们在 Unity 中拥有的图形界面选项的完美游戏。通过学习如何在屏幕上添加按钮、文本和图片,您将拥有添加到任何游戏中可能需要的任何界面的理解和工具。
我们将要制作的下一个游戏是坦克大战。在这个游戏中,玩家控制一辆坦克在小城市中四处行驶,射击目标和敌人。这个游戏横跨三个章节,让我们能够探索为 Android 平台制作游戏的各种关键点。我们从创建一个城市和使玩家的坦克通过我们在制作井字游戏时学到的控制方式移动开始。我们还创建并动画化玩家将射击的目标。在游戏的第二部分,我们增加了一些照明和特殊的摄像机效果。到了章节末尾,环境看起来非常棒。在游戏制作的第三部分,我们创建了一些敌人。利用 Unity 的强大功能,这些敌人在城市中追逐玩家并在靠近时攻击他们。
第三个游戏是流行的移动游戏的简单克隆。利用 Unity 的物理系统,我们能够创建结构和向它们投掷鸟类。推倒结构以获得分数,摧毁目标猪以赢得关卡。我们还探索了一些 2D 游戏和 Unity 的 2D 管线特有的功能,例如视差滚动背景和使用精灵。我们通过创建关卡选择菜单和保存高分来完成章节和游戏。
最后,我们将创建一个类似猴子球风格的游戏。这个游戏涉及使用移动设备的特殊输入来控制球体的移动和玩家与世界的互动。当玩家的设备倾斜时,他们将能够引导猴子在关卡中移动。当他们触摸屏幕时,可以对游戏中的敌人造成伤害,并最终收集分散在各处的香蕉。这个游戏还展示了如何包含每个游戏完成外观所必需的特殊效果。当收集香蕉时,我们会制造爆炸效果,当猴子移动时,会产生尘土尾迹。同时,我们还为触摸和爆炸音效添加了声音效果。
我们通过探讨优化来结束这本书。我们探索了 Unity 的所有优秀特性,甚至创建了一些我们自己的功能,以使我们的游戏尽可能运行得更好。我们还花了一些时间了解如何尽量减小资源文件的大小,同时最大化它们在游戏中的外观和效果。在这一刻,我们的旅程结束了,但我们将拥有四个几乎准备投放市场的优秀游戏。
这本书涵盖的内容
第一章,向 Unity 和 Android 问好,探讨了 Android 平台和 Unity 3D 游戏引擎的功能列表,并解释了它们为何是开发的首选。我们还介绍了开发环境的设置,并为你的设备和模拟器创建了一个简单的 Hello World 应用程序。
第二章,看起来很棒 - 图形界面,详细介绍了图形用户界面。通过创建一个井字游戏,你可以在使界面看起来令人愉悦的同时学习用户界面知识。
第三章,任何游戏的核心 - 网格、材质和动画,探讨了如何在 Unity 中利用网格、材质和动画。通过创建一个坦克大战游戏,我们涵盖了玩家在玩游戏时所看到的核心内容。
第四章,布置舞台 - 摄像头效果和照明,解释了 Unity 中可用的摄像头效果和照明选项。通过添加阴影、光照图、距离雾和天空盒,我们的坦克大战环境变得更加动态。通过利用特殊的摄像头效果,我们为玩家创造了额外的反馈。
第五章,四处移动 - 路径查找和 AI,展示了在我们的坦克大战游戏中创建具有挑战性的敌人。我们探索了路径查找和人工智能,为玩家提供了比静止的假目标更有意义的攻击目标。
第六章, 移动设备的特色 - 触摸和倾斜, 涵盖了使现代移动设备特别的功能。我们创建了一个类似 Monkey Ball 风格的游戏,以理解触摸界面和倾斜控制。
第七章, 利用你的重量 - 物理和 2D 相机, 展示了如何创建一个类似 Angry Birds 游戏,同时短暂地休息一下 Monkey Ball 游戏。这里也探讨了物理和 Unity 的 2D 管线。
第八章, 特效 - 声音和粒子, 将我们带回到 Monkey Ball 游戏,添加特殊效果。声音效果和粒子的加入使我们能够创建更加完整的游戏体验。
第九章, 优化, 涵盖了 Unity 3D 中的优化。我们讨论了使我们的坦克大战和 Monkey Ball 游戏尽可能高效的好处和成本。
阅读本书所需的准备
在整本书中,我们将同时使用 Unity 3D 游戏引擎和 Android 平台。正如你在上一节所看到的,我们将在第一章介绍 Unity 和 Android SDK 的获取和安装。为了最大限度地利用本书,你需要有一个运行 Android 系统的设备;一个能良好工作的手机或平板电脑。本书的部分内容涵盖了只有在 Unity 专业版中才有的功能。为了简化起见,我们将假设你使用的是 Windows 电脑。此外,书中代码是用 C#编写的,尽管每章项目的 JavaScript 版本仅供参考。为了充分利用各章节项目提供的模型,你需要 Blender,这是一个免费的建模程序,可在www.blender.org
获取。你还需要一个图片编辑程序;Photoshop 和 Gimp 都是不错的选择。你需要像 Blender 这样的建模程序和像 Photoshop 或 Gimp 这样的图像编辑程序来创建和处理你自己的内容。我们还建议你获取一个可以创建或获取音频文件的来源。本书提供的所有音频文件都可以在www.freesound.org
找到。
本书的目标读者
对于那些刚接触使用 Unity 5.0 和 Android L 进行游戏开发和移动开发的新手来说,这本书将非常合适。那些通过实际例子而不是枯燥的文档最能学习好的读者会发现每个章节都很有用。即使你几乎没有编程技能,这本书也能让你入门,学习编程和游戏开发的一些概念和标准。
编写约定
在这本书中,你会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理程序如下所示:“CheckVictory
函数遍历游戏中胜利的可能组合。”
一段代码如下设置:
public void NewGame() {
xTurn = true;
board = new SquareState[9];
turnIndicatorLandscape.text = "X's Turn";
}
任何命令行输入或输出都如下编写:
adb kill-server
adb start-server
adb devices
新术语和重要词汇会以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,会在文本中以这样的形式出现:“接着点击下载适用于 Windows 的 SDK 工具按钮。”
注意
警告或重要提示会以这样的方框显示。
小贴士
小技巧会以这样的形式出现。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要给我们发送一般反馈,只需电子邮件<feedback@packtpub.com>
,并在邮件的主题中提及书籍的标题。
如果你在一个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们的作者指南:www.packtpub.com/authors。
客户支持
既然你现在拥有了 Packt 的一本书,我们有一些事情可以帮助你最大限度地利用你的购买。
下载示例代码
你可以从你在www.packtpub.com
的账户下载你所购买的所有 Packt Publishing 书籍的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support
注册,我们会直接将文件通过电子邮件发送给你。
下载本书的颜色图片
我们还为你提供了一个 PDF 文件,其中包含本书中使用的截图/图表的颜色图片。颜色图片将帮助你更好地理解输出的变化。你可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/LearningUnityAndroidGameDevelopment_Graphics.pdf
。
勘误
尽管我们已经竭尽所能确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——我们非常感激你能向我们报告。这样做可以避免其他读者产生困扰,并帮助我们在后续版本中改进这本书。如果你发现任何勘误,请通过访问 www.packtpub.com/submit-errata
,选择你的书籍,点击 勘误提交表单 链接,并输入勘误详情。一旦你的勘误被验证,你的提交将被接受,勘误信息将被上传到我们的网站或添加到该标题勘误部分现有的勘误列表中。
要查看之前提交的勘误信息,请访问 www.packtpub.com/books/content/support
,在搜索字段中输入书名。所需信息将在 勘误 部分显示。
盗版
互联网上版权材料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如发现疑似盗版材料,请通过 <copyright@packtpub.com>
联系我们,并提供相关链接。
我们感谢你帮助我们保护作者权益和我们为你提供有价值内容的能力。
问题
如果你对本书的任何方面有问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决问题。
第一章:向 Unity 和 Android 问好
欢迎来到精彩的移动游戏开发世界。无论你仍在寻找合适的开发工具包,还是已经做出了选择,这一章都至关重要。在本章中,我们将探讨选择Unity作为开发环境和Android作为目标平台所带来的各种特性。通过与主要竞争对手的比较,我们将发现为什么 Unity 和 Android 能够站在最顶层。接下来,我们将研究 Unity 和 Android 如何协同工作。最后,我们将设置开发环境,并创建一个简单的“Hello World”应用程序来测试是否一切设置正确。为了本书的目的,我们假设你是在 Windows 环境下工作。
在本章中,我们将涵盖以下主题:
-
Unity 的主要特性
-
主要的 Android 特性
-
Unity 许可选项
-
安装 JDK
-
安装 Android 软件开发工具包(SDK)
-
安装 Unity 3D
-
安装 Unity Remote
了解 Unity 的优势
Unity 最大的特点可能就是其开放性。目前市场上几乎所有的游戏引擎在可构建的内容上都有限制。这很有道理,但也可能限制了团队的能力。一般的游戏引擎针对创建特定类型的游戏进行了高度优化。如果你计划一次又一次地制作相同类型的游戏,这当然很好。但是,当一个人突然有了下一个大热门游戏的灵感时,却发现游戏引擎无法处理,每个人都需要重新学习新引擎或加倍开发时间来使游戏引擎能够胜任,这会相当令人沮丧。Unity 没有这个问题。Unity 的开发者非常努力地优化了引擎的各个方面,同时不限制可以用它制作的游戏类型。从简单的 2D 平台游戏到大规模在线角色扮演游戏,在 Unity 中都是可能的。刚刚完成一个超现实的第一人称射击游戏开发团队可以立即转身制作 2D 格斗游戏,而无需学习全新的系统。
然而,这种开放性也带来了一些缺点。没有默认的工具可以优化以构建完美的游戏。为了解决这个问题,Unity 允许使用创建游戏的相同脚本创建任何可以想象到的工具。此外,还有一个强大的用户社区,他们提供了大量免费和付费的工具和组件,可以快速插入和使用。这导致了一个大量可用内容的庞大选择,可以让你在通往下一个伟大游戏的道路上迅速起步。
当许多潜在用户看到 Unity 时,他们认为,因为它如此便宜,所以不如昂贵的 AAA 游戏引擎好。这根本不是事实。在游戏引擎上投入更多资金,并不会让游戏变得更好。Unity 支持所有你想要的华丽着色器、法线贴图和粒子效果。最好的部分是,几乎所有你想要的华丽功能都包含在 Unity 的免费版本中,而且 90%的情况下,你甚至不需要使用仅限专业版的功能。
在选择游戏引擎时,尤其是在移动市场,最大的担忧之一是它将给最终构建的大小增加多少体积。大多数游戏引擎都相当庞大。利用 Unity 的代码剥离功能,项目的最终构建大小可以变得相当小。代码剥离是 Unity 从编译库中移除每一块多余代码的过程。一个为 Android 编译的空白项目,如果使用完全的代码剥离,最终的大小大约是 7 兆字节。
Unity 最酷的功能之一可能是它的多平台兼容性。通过单个项目,可以构建适用于多个不同的平台。这包括能够同时针对移动设备、PC 和游戏机进行开发。这使你能够专注于真正的问题,比如处理输入、分辨率和性能。
过去,如果一家公司希望将产品部署在多个平台上,他们几乎需要将开发成本翻倍,以便实质上重新编写游戏。每个平台都有自己独特的逻辑和语言,至今仍是如此。得益于 Unity,游戏开发变得前所未有的简单。我们可以使用简单快捷的脚本开发游戏,让 Unity 处理复杂的平台转换工作。
Unity – 群雄中的最佳选择
当然,还有其他几个游戏引擎可以选择。首先想到的两个主要选择是cocos2d和虚幻引擎。虽然这两个都是极佳的选择,但在某些方面你可能觉得它们略有不足。
《愤怒的小鸟》所使用的引擎 cocos2d,可能是你下一个移动平台热门游戏的绝佳选择。然而,正如其名所示,它基本上仅限于 2D 游戏。游戏在 cocos2d 中可能看起来很棒,但如果你想要加入第三个维度,将三维效果加入 cocos2d 可能会有些棘手;这时,你可能需要选择一个新的游戏引擎。cocos2d 的第二个主要问题是它的基础性。任何用于构建或导入资源的工具都需要从头开始创建,或者需要找到现成的。除非你有足够的时间和经验,否则这可能会严重拖慢开发进度。
接下来是大型游戏开发的主流选择,虚幻引擎。这款游戏引擎已经被开发者们成功使用多年,为世界带来了众多伟大的游戏,其中不乏《虚幻竞技场》和《战争机器》等佳作。然而,这些基本上都是主机和电脑游戏,这正是该引擎的根本问题所在。虚幻引擎非常庞大且功能强大,对于移动平台而言,对其优化程度有限。它一直存在同样的问题;它会让项目及其最终构建变得庞大。虚幻引擎的另一个主要问题是它作为第一人称射击游戏引擎的僵化性。虽然技术上可以用它来创建其他类型的游戏,但这样的任务既漫长又复杂。在完成这样的壮举之前,必须对底层系统有一个深入的了解。
总的来说,Unity 在游戏引擎中绝对占有一席之地。或许,你已经发现了这一点,这也是你阅读这本书的原因。但是,选择 Unity 进行游戏开发仍有众多充分的理由。Unity 项目的最终效果可以与 AAA 级作品相媲美。最终构建的负担和体积都很小,这对于移动平台来说非常重要。系统的开放性足以让你创造任何你想要的游戏类型,而其他引擎往往局限于单一类型的游戏。此外,如果在项目生命周期的任何阶段,你的需求发生了变化,增加、移除或更改目标平台的选择也非常容易。
了解安卓的卓越之处。
用户手中拥有超过 3000 万台设备,为何不选择安卓平台作为你下一个移动热作的目标呢?苹果可能凭借 iPhone 的轰动效应走在了前面,但说到智能手机技术,安卓绝对领先一步。它最出色的特性之一就是其开放性,你可以深入了解手机是如何工作的,无论是从物理层面还是技术层面。如果需要,用户几乎可以在所有安卓设备上更换电池和升级 micro SD 卡。将手机连接到电脑也不必是一件麻烦事;它完全可以作为可移动存储媒体使用。
从开发成本的角度来看,安卓市场同样具有优势。其他移动应用商店需要大约 100 美元的年注册费。有些还对一次可以注册用于开发的设备数量有限制。而谷歌 Play 市场的注册费为一次性 25 美元,而且完全不必担心你用于开发的安卓设备数量或类型。
一些其他移动开发工具的缺点之一是,在获得 SDK 访问权限之前,你需要支付年度注册费用。有些工具在查看其文档之前,需要注册和付费。Android 则更加开放和易于接触。任何人都可以免费下载 Android SDK。文档和论坛完全开放,无需支付任何费用即可查看。这意味着 Android 的开发可以更早开始,从一开始就可以将设备测试作为其中的一部分。
了解 Unity 和 Android 如何协同工作。
由于 Unity 以通用方式处理项目和资源,因此无需为多个目标平台创建多个项目。这意味着你可以轻松使用免费的 Unity 版本开始开发,并针对个人电脑。然后,在稍后的时间,你可以通过点击一个按钮将目标切换到 Android 平台。也许,在你的游戏发布后不久,它就在市场上大受欢迎,有强烈的呼声要将它带到其他移动平台。只需再点击一个按钮,你就可以轻松地将目标定位为 iOS,而无需更改项目中的任何内容。
大多数系统需要经过一系列冗长且复杂的步骤才能在设备上运行你的项目。本书的第一个应用程序,我们将经历这个过程,因为了解它是很重要的。然而,一旦你的设备被设置并得到 Android SDK 的识别,只需点击一个按钮,Unity 就能构建你的应用程序,将其推送到设备上并开始运行。对于一些开发者来说,没有什么比尝试将应用程序安装到设备上更让人头疼的了。Unity 简化了这个过程。
通过添加免费的 Android 应用程序Unity Remote,无需经过整个构建过程,简单轻松地测试移动输入。在开发过程中,最让人烦恼的事情之一就是每次需要测试一个小调整时,都要等待 5 分钟的构建时间,尤其是在控制和界面方面。经过前十几次的小调整后,构建时间开始累积。Unity Remote 使得在不点击构建按钮的情况下,简单轻松地测试所有内容成为可能。
这里有三个主要的原因说明为什么 Unity 与 Android 配合得很好:
-
通用项目
-
一键构建过程
-
Unity 远程
当然,我们还可以提出更多关于 Unity 和 Android 如何协同工作的优点。然而,这三个原因是节省时间和金钱的主要因素。你可能拥有世界上最好的游戏,但如果构建和测试需要 10 倍的时间,那还有什么意义呢?
Unity 专业版与基础版之间的区别
Unity 提供两种授权选项:专业版(Pro)和基础版(Basic),可在store.unity3d.com
找到。为了跟随本书的大部分内容,只需使用 Unity Basic 即可。然而,第四章《设置舞台 - 摄像头效果与光照》中的实时阴影以及第九章《优化》中讨论的一些优化功能将需要 Unity Pro。如果你还没有准备好花费 3000 美元购买带有 Android 扩展的完整 Unity Pro 授权,还有其他选择。Unity Basic 是免费的,并附有 Unity Pro 30 天的免费试用期。这个试用版是完整无缺的,就像你购买了 Unity Pro 一样,唯一的缺点是游戏右下角会有一个水印,标注着仅供演示使用。你也可以在以后升级你的授权。Unity Basic 免费提供移动平台选项,而 Unity Pro 需要为每个移动平台购买 Pro 扩展。
授权比较概览
授权比较可以在unity3d.com/unity/licenses
找到。本节将介绍 Unity Android Pro 和 Unity Android Basic 之间的具体差异。我们将在以下各点探讨这些功能是什么以及它们各自有多有用:
NavMeshes、寻路和人群模拟
这个功能是 Unity 内置的寻路系统。它允许角色在游戏中从一个点找到到另一个点的路径。只需在编辑器中烘焙你的导航数据,并在运行时让 Unity 接管即可。直到最近,这还是只有 Unity Pro 才有的功能。现在在 Unity Basic 中唯一受限的部分是使用非网格链接。唯一需要它们的时候,就是你希望你的 AI 角色能够跳跃穿过,或在缺口周围导航时。
LOD 支持
LOD(即细节层次)允许你根据物体与摄像头的距离来控制网格的复杂度。当摄像头靠近一个物体时,你可以渲染一个充满细节的复杂网格。当摄像头远离该物体时,你可以渲染一个简单的网格,因为所有的细节反正也看不到。Unity Pro 提供了一个内置的系统来管理这一点。然而,这也是可以在 Unity Basic 中创建的另一个系统。无论你是否使用 Pro 版本,这对于游戏效率来说都是一个重要的功能。通过在远处渲染较不复杂的网格,一切都可以渲染得更快,为精彩的游戏玩法留出更多空间。
音频过滤器
音频过滤器允许你在运行时为音频剪辑添加效果。例如,你可能为你的角色创造了沙砾脚步声。角色正在奔跑,我们可以清晰地听到脚步声,突然他们进入了一个隧道,一个太阳耀斑击中,造成了时间扭曲,放慢了一切。音频过滤器能够让我们扭曲沙砾脚步声,使其听起来像是从隧道内部传来的,并且被时间扭曲所放慢。当然,你也可以让音频师创造一组新的时间扭曲中的隧道沙砾脚步声,但这可能会使游戏中的音频数量翻倍,并限制我们在运行时能有多动态。我们要么播放时间扭曲的脚步声,要么不播放。音频过滤器可以让我们控制时间扭曲对我们的声音影响有多大。
视频播放与流媒体
在处理复杂或高清晰度的过场动画时,能够播放视频变得非常重要。特别是在移动设备目标中,将它们包含在构建中可能需要很多空间。这就是这个特性中流媒体部分的作用所在。这个特性不仅让我们播放视频,还允许我们从互联网上流式传输视频。然而,这个特性有一个缺点。在移动平台上,视频必须通过设备的内置视频播放系统。这意味着视频只能以全屏播放,不能用作如电视模型上移动图片等效果的材料。从理论上讲,你可以将视频分解为每一帧的单独图片,并在运行时翻阅它们,但由于构建大小和视频质量的原因,不推荐这样做。
使用资源包实现完整的流媒体功能
资源包是 Unity Pro 提供的一个非常好的特性。它们允许你创建额外的内容,并在不需要游戏更新的情况下将其流式传输给用户。你可以添加新角色、关卡,或者你能想到的几乎任何其他内容。它们唯一的缺点是你不能添加更多代码。功能无法改变,但内容可以。这是 Unity Pro 最好的特性之一。
10 万美元的营业额
这不是一个特性,而更像是一个指导原则。根据 Unity 的最终用户许可协议,任何在前一个财年中收入达到 10 万美元的团体或个人都不能许可 Unity 的基本版本。这意味着,如果你赚了很多钱,你就必须购买 Unity Pro。当然,如果你赚了这么多钱,你可能不会有问题地负担得起。至少这是 Unity 的看法,也是为什么会有一个 10 万美元营业额的原因。
Mecanim – IK 骨骼绑定
Unity 的新动画系统Mecanim支持许多激动人心的新功能,其中之一就是IK(逆运动学的简称)。如果你对这个术语不熟悉,IK 允许你定义动画的目标点,让系统自行解决如何到达该点。想象一下,你有一个放在桌子上的杯子,角色想要拿起它。你可以让角色弯腰去拿;但是,如果角色稍微偏移一点呢?或者玩家可能造成的任何其他微小偏移,完全打乱了你的动画?为每一种可能性都制作动画是不切实际的。使用 IK,角色稍微偏移一点几乎无关紧要。
我们只需为手部定义目标点,而将手臂的动画交给逆运动学(IK)系统处理。它会计算出手臂需要怎样移动才能让手到达杯子。另一个有趣的用途是,当角色在房间内走动时,让他们观察有趣的事物:守卫可以追踪最近的人,玩家的角色可以查看可以互动的物体,或者触手怪物可以在没有复杂动画的情况下向玩家发起攻击。这将是一个令人兴奋的功能去尝试。
Mecanim – 同步图层和附加曲线
在 Mecanim 中的同步图层,能让我们保持多组动画状态彼此同步。假设你有一个士兵,你想根据他的生命值来不同地动画他。当他的生命值满时,他快速地走动。受到一点伤害后,他的行走变得更为沉重。如果他的生命值低于一半,他的行走中会引入跛行,而当他几乎要死亡时,他会沿着地面爬行。通过同步图层,我们可以创建一个动画状态机并复制到多个图层中。通过改变动画并同步图层,我们可以轻松地在不同的动画之间过渡,同时保持状态机。
附加曲线功能,简单来说就是在动画中添加曲线的能力。这意味着我们可以通过动画控制各种数值。例如,在游戏世界中,当角色抬起脚准备跳跃时,重力几乎会立即将他们拉下来。在 Unity 中为那个动画添加额外的曲线,我们可以控制重力对角色的影响程度,使他们跳跃时实际上能在空中。这是一个用于在动画的同时控制这类数值的有用功能,但你也可以轻松创建一个控制曲线的脚本。
自定义启动画面
尽管这个功能相当容易理解,但除非你之前使用过 Unity,否则可能不会立即明白为什么要特别指明这一功能。在任何平台上初始化的 Unity 构建的应用程序都会显示一个启动画面。在 Unity Basic 中,这总是 Unity 的标志。通过购买 Unity Pro,你可以用任何你想要的图像替换 Unity 标志。
实时点光源/软阴影
光照和阴影为场景的氛围增添了很多。这个特性让我们可以超越简单的 blob 阴影,使用看起来更逼真的阴影。如果你有足够的处理空间,这当然很好。然而,大多数移动设备并不具备这样的条件。这个特性也不应该用于静态场景;相反,应该使用静态光照贴图,这才是它们的作用所在。
然而,如果你能在简单需求和高质量之间找到一个好的平衡,这可能是区分一个还不错和一款优秀游戏的特点。如果你确实需要实时阴影,那么方向光支持它们,并且是计算速度最快的灯光。这也是 Unity Basic 中唯一支持实时阴影的灯光类型。
HDR 和色调映射
HDR(即高动态范围)和色调映射使我们能够创造出更逼真的光照效果。标准渲染使用从零到一之间的值来表示像素中每种颜色的显示程度。这并不允许探索完整的光照选项光谱。HDR 让系统能够使用超出这一范围的值,并使用色调映射处理它们,以创造更好的效果,如明亮的早晨房间或汽车窗户反射的阳光造成的泛光。这个特性的缺点在于处理器。设备仍然只能处理零到一之间的值,因此转换它们需要时间。此外,效果越复杂,渲染所需的时间越长。即使是在简单游戏中,看到手持设备很好地使用这项技术都会令人惊讶。也许现代平板电脑能够处理。
光探针
光探针是一个有趣的小功能。当它们被放置在世界上时,光探针会计算出物体应该如何被照亮。然后,当角色四处走动时,它们会告诉角色如何进行阴影处理。角色当然会被场景中的灯光照亮,但一次能对物体产生阴影的灯光数量是有限制的。光探针事先进行所有复杂的计算,允许在运行时进行更好的阴影处理。然而,同样存在关于处理能力的问题。如果处理能力不足,你不会得到好的效果;如果过多,将没有剩余的处理能力来玩游戏。
使用全局光照和区域光照的光照贴图
所有版本的 Unity 都支持光照贴图,允许烘焙复杂的静态阴影和光照效果。加上全局光照和区域光照,你可以为场景添加更多真实感。然而,Unity 的每个版本还允许你导入自己的光照贴图。这意味着你可以使用其他程序来渲染光照贴图,然后单独导入它们。
静态批处理
这个特性加快了渲染过程。它不是在每个帧上花费时间将对象分组以加快渲染速度,而是允许系统保存之前生成的组。减少绘制调用次数是使游戏运行更快的重要步骤。这正是这个特性的作用。
渲染到纹理效果
这是一个有趣的功能,但实用性有限。它允许你将摄像头的输出用作游戏中的纹理。这种纹理在最简单的形式下,可以被放置在网格上,充当监控摄像头。你也可以进行一些自定义的后处理,比如当玩家失去生命值时,从世界中移除颜色。然而,这个选项可能会变得非常消耗处理器资源。
全屏后处理效果
这也是一个非常消耗处理器资源的特性,可能不会用于你的移动游戏。然而,你可以为你的场景添加一些非常酷的效果,比如当玩家移动速度非常快时添加动态模糊效果,或者在飞船穿过扭曲的空间区域时添加漩涡效果。最佳效果之一是使用泛光效果,让事物呈现出类似霓虹灯的发光效果。
遮挡剔除
这又是另一个优秀的优化功能。标准的摄像头系统会渲染摄像头视野锥体内的所有内容,即视图空间。遮挡剔除允许我们在摄像头可以进入的空间内设置体积。这些体积用于计算摄像头从这些位置实际能看到的内容。如果有一堵墙挡在前面,那么渲染它背后的所有东西又有什么意义呢?遮挡剔除计算这一点,并阻止摄像头渲染墙后的任何内容。
延迟渲染
如果你希望制作出外观最佳的游戏,具有高度详细的光照和阴影,那么这个特性对你来说将非常有趣。延迟渲染是一个多通道过程,用于计算你的游戏中的光照和阴影细节。然而,这是一个代价高昂的过程,需要一张相当不错的图形卡来充分利用它。不幸的是,这使得它对于移动游戏来说有些难以承受。
模板缓冲区访问
自定义着色器可以使用模板缓冲区通过选择性地覆盖特定像素来创建特殊效果。这类似于使用 alpha 通道选择性地渲染纹理的部分区域。
GPU 蒙皮
这是一种处理和渲染方法,通过它,使用骨架绑定的人物或对象的外观计算被交给图形卡处理,而不是由中央处理器完成。这种方式渲染对象要快得多。然而,这仅支持 DirectX 11 和 OpenGL ES 3.0,这使得它对我们的移动游戏来说有些难以触及。
导航网格 - 动态障碍物和优先级
这个功能与寻路系统结合使用。在脚本中,我们可以动态设置障碍物,角色将找到绕过它们的方法。能够设置优先级意味着不同类型的角色在寻找路径时可以考虑不同类型的对象。例如,士兵必须绕过路障才能达到目标。然而,坦克可以撞过去,如果玩家希望这样做的话。
本地代码插件支持
如果你有一套以动态链接库(DLL)形式的自定义代码,这就是你需要访问的 Unity Pro 功能。否则,Unity 无法访问本地插件,以便与你的游戏一起使用。
性能分析器和 GPU 性能分析
这是一个非常实用的功能。性能分析器提供了大量关于你的游戏对处理器产生多大负载的信息。有了这些信息,我们可以深入到细节中,准确地确定一个脚本处理需要多长时间。然而,在本书的后面,我们还将创建一个工具,以确定你代码中特定部分的处理需要多长时间。
脚本访问资源管道
这是一个还不错的功能。有了对管道的完全访问权限,可以对资源和构建进行大量自定义处理。完整可能性范围超出了本书的讨论范围。但是,你可以将它视为能够将所有导入的纹理稍微调整为蓝色的功能。
深色皮肤
这完全是一个外观功能。它的意义和目的值得商榷。然而,如果你想要一个光滑的深色皮肤外观,这就是你想要的功能。编辑器中有一个选项可以将其更改为 Unity Basic 中使用的颜色方案。对于这个功能,你喜欢什么就是什么。
设置开发环境
在我们能为安卓创建下一个伟大的游戏之前,我们需要安装一些程序。为了使 Android SDK 工作,我们首先安装Java 开发工具(JDK)。然后安装 Android SDK。之后,我们将安装 Unity。接下来,我们还要安装一个可选的代码编辑器。为了确保一切设置正确,我们将连接到我们的设备,并查看如果设备比较棘手的一些特殊策略。最后,我们将安装 Unity Remote,这个程序在移动开发中将变得非常宝贵。
安装 JDK
安卓的开发首选语言是 Java;因此,为了开发它,我们需要在电脑上安装一份Java SE 开发工具包。安装 JDK 的过程在以下步骤中给出:
-
可以从
www.oracle.com/technetwork/java/javase/downloads/index.html
下载最新版本的 JDK。因此,在网页浏览器中打开该网站,你将能够看到以下截图显示的屏幕: -
从可用版本中选择Java 平台(JDK),您将被引导至一个包含许可协议的页面,并允许您选择希望下载的文件类型。
-
接受许可协议,并从页面底部的列表中选择适合您的 Windows 版本。如果您不确定选择哪个版本,通常选择Windows x86是安全的。
-
下载完成后,运行新的安装程序。
-
系统扫描后,点击下一步两次,JDK 将初始化,然后再次点击下一步按钮以将 JDK 安装到默认位置。它在那里和其他任何地方一样好,因此安装完成后,点击关闭按钮。
我们刚刚完成了JDK的安装。我们需要它是因为我们的 Android 开发工具包才能工作。幸运的是,这个关键基石的安装过程既简短又顺利。
安装 Android SDK
为了实际开发和连接我们的设备,我们需要安装 Android SDK。安装 SDK 满足了两个主要需求。首先,它确保我们有最新的驱动程序以识别设备。其次,我们可以使用Android 调试桥(ADB)。ADB 是用于实际连接和与设备交互的系统。以下是安装 Android SDK 的步骤:
-
最新版本的 Android SDK 可在
developer.android.com/sdk/index.html
找到,因此请打开网页浏览器并访问给定的网站。 -
到达页面后,滚动至底部,找到仅 SDK 工具部分。这是我们仅获取 SDK 的地方,我们需要它来使用 Unity 开发 Android 游戏,而不必处理 Android Studio 的花哨内容。
-
我们需要选择带有**(推荐)**标记的
.exe
安装包(如下面的截图所示): -
您随后将被引导至一个条款与条件页面。如果您愿意,可以阅读它,但需要同意才能继续。然后点击下载按钮开始下载安装程序。
-
下载完成后,启动它。
-
点击第一个下一步按钮,安装程序将尝试找到合适的 JDK 版本。如果您没有安装 JDK,您将看到一个通知您找不到 JDK 的页面。
-
如果您跳过了步骤并且没有安装 JDK,请点击页面中间的访问 java.oracle.com按钮,并返回到前面的部分获取安装指导。如果您已经安装了它,请继续进行下一步。
-
再次点击下一步,会出现一个页面询问您安装 SDK 的对象。
-
选择为这台电脑上的任何人安装,因为默认安装位置便于以后访问。
-
点击下一步两次,然后点击安装,将 SDK 安装到默认位置。
-
完成后,点击下一步和完成,以完成 Android SDK 管理器的安装。
-
如果 Android SDK 管理器没有立即启动,请启动它。无论如何,给它一点时间进行初始化。SDK 管理器确保我们有最新的驱动程序、系统和工具,以便与 Android 平台进行开发。但是,我们首先必须安装它们(这可以在以下屏幕完成):
-
默认情况下,SDK 管理器应该选择一些要安装的选项。如果没有,选择最新的 Android API(在撰写本书时为 Android L (API 20)),Android 支持库和Extras中的Google USB 驱动程序。请务必确保选中了Android SDK Platform-tools。这将在后面非常重要。它实际上包含了我们需要连接设备所需的工具。
-
一旦选择好所有内容,点击右下角的安装包。
-
下一个屏幕是另一组许可协议。每次通过 SDK 管理器安装或更新组件时,您都必须同意许可条款才能进行安装。接受所有许可协议,然后点击安装开始流程。
-
您现在可以坐下来放松一下。组件的下载和安装需要一些时间。一旦完成这些步骤,您可以关闭它。我们已经完成了这个过程,但您应该偶尔回来检查一下。定期检查 SDK 管理器是否有更新,以确保您正在使用最新的工具和 API。
Android SDK的安装现在已经完成。没有它,我们将完全无法在 Android 平台上进行任何操作。除了下载和安装组件的长时间等待,这是一个相当简单的安装过程。
安装 Unity 3D
这本书最重要的部分,没有它,其余的内容都没有意义,就是安装 Unity。执行以下步骤来安装 Unity:
-
最新版本的 Unity 可以在
www.unity3d.com/unity/download
找到。在撰写本书时,当前版本是 5.0。 -
下载完成后,启动安装程序,并点击下一步,直到您到达选择组件页面,如下截图所示:
-
在这里,我们可以选择 Unity 安装的功能。实际上,对于跟随本书其余内容来说,这些选项都不是必需的,但它们值得一看,因为每次更新或重新安装 Unity 时,Unity 都会询问您希望安装哪些组件:
-
示例项目:这是 Unity 为了展示其最新功能而构建的当前项目。如果您想提前看看一个完整的 Unity 游戏是什么样的,请保持选中此项。
-
Unity 开发网络播放器:如果你计划使用 Unity 开发浏览器应用程序,则需要此播放器。由于本书重点介绍 Android 开发,因此这是可选的。不过,勾选它是个不错的选择。你永远不知道何时可能需要一个网络演示,而且使用 Unity 开发网络应用程序完全免费,所以勾选它没有坏处。
-
MonoDevelop:选择不勾选这个选项是明智的。下一节会有更多详细信息,但现在只需知道它只是增加了一个用于脚本编辑的程序,而这个程序并没有它应有的那么有用。
-
-
选择或取消选择你所需的选项后,点击下一步。如果你希望严格按照书本操作,请注意我们将取消勾选MonoDevelop,其余的保持勾选。
-
下一步是安装位置。默认位置很好,所以点击安装并等待。这将需要几分钟,所以请坐下来,放松一下,享受你最喜欢的饮料。
-
安装完成后,将显示运行 Unity 的选项。保持勾选并点击完成。如果你以前从未安装过 Unity,将会出现一个许可证激活页面(如下图所示):
-
虽然 Unity 确实提供了一个功能丰富的免费版本,但要完全遵循本书的内容,需要使用一些 Unity Pro 功能。在
store.unity3d.com
上,你可以购买各种许可证。要跟随整本书,你至少需要购买 Unity Pro 和 Android Pro 许可证。购买后,你将收到一封包含新许可证密钥的电子邮件。将密钥输入到提供的文本字段中。 -
如果你还没准备好购买,你还有两个选择。我们将在本章后面的构建一个简单的应用程序部分介绍如何重置你的许可证。以下是你可以选择的两种方式:
-
第一个选择是勾选激活 Unity 的免费版本复选框。这将允许你使用 Unity 的免费版本。如前所述,有很多理由选择这个选项。目前最值得注意的是成本。
-
你也可以选择激活 Unity Pro 的免费 30 天试用选项。Unity 提供一次性的完整功能安装,以及 Unity Pro 的免费 30 天试用。此试用版还包括 Android Pro 附加组件。在这 30 天内制作的所有内容完全属于你,就像你购买了完整的 Unity Pro 许可一样。他们希望你能体验到它有多棒,以便你回来进行购买。不过缺点是,游戏角落会不断显示试用版水印。30 天后,Unity 将恢复到免费版本。如果你打算在购买前先等待,这是一个很好的选择。
-
-
无论你的选择是什么,一旦做出决定,点击确定。
-
下一个页面只是要求你使用 Unity 账户登录。这将是你用来进行购买的账户。只需填写字段并点击确定。
-
如果你还没有进行购买,可以点击创建账户,这样当你购买时就可以准备好了。
-
下一个页面是对你开发兴趣的简短调查。填写完毕后点击确定,或者直接滚动到底部点击现在不要。
-
最后会有一个感谢页面。点击开始使用 Unity。
-
短暂初始化之后,项目向导会打开,我们可以开始创建下一个伟大的游戏。然而,为了连接开发设备,还有很多工作要做。所以现在,点击右上角的X按钮关闭项目向导。我们将在后面的构建一个简单的应用程序部分介绍如何创建新项目。
我们刚刚完成了 Unity 3D 的安装。整本书都依赖于这一步骤。我们还必须做出关于许可证的选择。如果你选择购买专业版,你将能够毫无问题地跟随本书中的所有内容。然而,其他选择会有一些不足之处。你将无法完全访问所有功能,或者在试用期限内向游戏添加水印的同时受限。
可选的代码编辑器
现在需要做一个关于代码编辑器的选择。Unity 自带一个名为MonoDevelop的系统。它在许多方面与Visual Studio相似。而且与 Visual Studio 一样,它会为项目添加许多额外文件和大量体积,这些都是其运行所需的。所有这些额外的体积使得启动时间变得令人讨厌,因为在真正接触到代码之前需要等待。
从技术上讲,你可以使用纯文本编辑器,因为 Unity 并不真正关心。本书推荐使用 Notepad++,可以在notepad-plus-plus.org/download
找到。它是免费使用的,本质上是有代码高亮的 Notepad。Notepad++ 有许多花哨的小工具和插件,可以增加更多功能,但它们并不是跟随本书所必需的。如果你选择这个替代方案,将 Notepad++ 安装到默认位置就可以了。
连接到设备
在使用 Android 设备时,可能最麻烦的步骤就是将设备连接到电脑。由于有如此多不同类型的设备,有时仅仅让电脑识别设备都会有点棘手。
简单的设备连接
简单的设备连接方法涉及更改一些设置以及在命令提示符中做一些工作。这可能看起来有点可怕,但如果一切顺利,你很快就会连接到你的设备:
-
你需要做的第一件事是打开手机的开发者选项。在最新版本的 Android 中,这些选项已被隐藏。进入手机的设置页面,找到关于手机页面。
-
接下来,你需要找到构建号信息栏,并多次点击它。起初,它看起来似乎没有任何反应,但很快就会显示你需要再按几次按钮来激活开发者选项。Android 团队之所以这样做,是为了防止普通用户不小心进行更改。
-
现在回到你的设置页面,应该有一个新的开发者选项页面;现在选择它。这个页面控制了你在开发应用程序时可能需要更改的所有设置。
-
我们现在真正需要勾选的复选框是USB 调试。这允许我们从开发环境中实际检测到我们的设备。
-
如果你使用的是 Kindle,务必进入安全选项,并开启启用 ADB。
提示
开启这些选项时会有几个警告弹窗,它们本质上与电脑上的恶意软件警告相同。意图不良的应用程序可能会干扰你的系统,获取你的私人信息。如果你的设备仅用于开发,所有这些设置都需要打开。然而,正如警告所提示的,如果担心恶意应用程序,不开发时请关闭它们。
-
接下来,在电脑上打开一个命令提示符。最简单的方法是按你的 Windows 键,输入
cmd.exe
,然后按回车。 -
现在,我们需要导航到 ADB 命令。如果你没有将 SDK 安装到默认位置,请将以下命令中的路径替换为你安装 SDK 的路径。
如果你运行的是 32 位 Windows 版本,并且将 SDK 安装到了默认位置,请在命令提示符中输入以下内容:
cd c:\program files\android\android-sdk\platform-tools
如果你运行的是 64 位版本,请在命令提示符中输入以下内容:
cd c:\program files (x86)\android\android-sdk\platform-tools
-
现在,将你的设备连接到电脑上,最好使用随设备附带的 USB 线。
-
等待你的电脑完成识别设备。完成后应该会出现一个设备驱动程序已安装类型的消息弹窗。
-
以下命令让我们看到当前连接并被 ADB 系统识别的设备。模拟设备也会显示出来。在命令提示符中输入以下内容:
adb devices
-
在短暂的处理后,命令提示符将显示已连接设备的列表以及所有连接设备的唯一 ID。如果现在这个列表中包含了你的设备,恭喜你!你有一个对开发者友好的设备。如果它不是完全的开发者友好型,在事情变得复杂之前,你还有一件事可以尝试。
-
去你的设备顶部打开系统通知。应该有一个看起来像 USB 符号的通知。选择它将打开连接设置。这里有几个选项,默认情况下 Android 会选择将 Android 设备作为媒体设备连接。
-
我们需要将设备连接为摄像头。这样做的原因是所使用的连接方式。通常,这将允许你的电脑进行连接。
我们已经完成了首次尝试连接到我们的安卓设备。对大多数人来说,这应该就是连接设备所需的一切。但对一些人来说,这个过程还不够。下一小节将介绍解决连接更难设备问题的方法。
对于更难连接的设备,我们可以尝试一些常规的方法;如果这些步骤无法连接你的设备,你可能需要进行一些特殊的研究。
-
从输入以下命令开始。这将重启连接系统并再次显示设备列表:
adb kill-server adb start-server adb devices
-
如果你仍然没有成功,尝试以下命令。这些命令会强制更新并重启连接系统:
cd ../tools android update adb cd ../platform-tools adb kill-server adb start-server adb devices
-
如果你的设备仍然没有显示出来,那么你有一个最令人讨厌且难以处理的设备。检查制造商的网站,看是否有数据同步和管理程序。如果你的设备已经使用了一段时间,你可能已经被提示不止一次安装这个程序。如果你还没有这样做,即使你从未打算使用它,也请安装最新版本。这样做是为了获取设备的最新驱动,这是最简单的方法。
-
再次使用第一组命令重启连接系统,然后交叉手指等待!
-
如果你仍然无法连接,最好的专业建议就是去谷歌搜索你的问题的解决方案。搜索你的设备品牌加上
adb
作为后缀,应该能在前几个结果中找到针对你的设备的分步教程。另一个了解关于安卓设备所有细节的优秀资源可以在www.xda-developers.com/
找到。
在开发过程中,你会遇到一些不容易连接的设备。我们刚刚介绍了一些快速步骤,并成功连接了这些设备。如果我们可以涵盖每个设备的过程,我们就会这么做。然而,设备种类太多,制造商还在不断推出新产品。
Unity Remote
Unity Remote 是由 Unity 团队创建的一个很棒的应用程序。它允许开发者将他们的安卓设备连接到 Unity 编辑器,并提供移动输入以进行测试。这绝对是任何有志于成为 Unity 和安卓开发者的必备工具。如果你使用的是非亚马逊设备,获取 Unity Remote 非常简单。在撰写这本书的时候,它可以在 Google Play 上找到,地址是play.google.com/store/apps/details?id=com.unity3d.genericremote
。它是免费的,除了将你的安卓设备连接到 Unity 编辑器之外,没有其他功能,所以应用程序权限可以忽略不计。实际上,目前有两个版本的 Unity Remote。要连接到 Unity 4.5 及更高版本,我们必须使用 Unity Remote 4。
然而,如果你喜欢不断增长的亚马逊市场,或者想要针对亚马逊的安卓设备,添加 Unity Remote 会变得有点复杂。首先,你需要从 Unity 资源商店下载一个特殊的 Unity 包。可以在www.assetstore.unity3d.com/en/#!/content/18106
找到它。你需要将这个包导入一个新项目,并从那里构建。通过在 Unity 顶部导航到资产 | 导入包 | 自定义包,然后导航到你保存它的位置来导入包。在下一节中,我们将构建一个简单的应用程序并将其放在我们的设备上。导入包后,从我们打开构建设置窗口的步骤开始,用创建的 APK 替换简单应用程序。
构建一个简单的应用程序
我们现在将创建一个简单的“你好世界”应用程序。这将帮助你熟悉 Unity 界面以及如何将应用程序实际放到你的设备上。
你好世界
为了确保一切设置正确,我们需要一个简单的应用程序来进行测试,还有什么比使用“你好世界”应用程序更好的呢?要构建这个应用程序,请执行以下步骤:
-
第一步非常直接和简单:启动 Unity。
-
如果你到目前为止一直在跟进,完成这些步骤后,你应该会看到一个类似于下一张截图的屏幕。正如标签所示,这是我们打开各种项目的屏幕。但现在,我们感兴趣的是创建一个;因此,从右上角选择新项目,我们将这样做:
-
使用项目名称字段给你的项目命名;
Ch1_HelloWorld
作为一个项目名非常合适。然后使用位置字段右侧的三个点来选择电脑上的一个位置来放置新项目。Unity 将在这个位置创建一个基于项目名称的新文件夹,用于存储你的项目及其所有相关文件: -
目前,我们可以忽略3D和2D按钮。这些按钮让我们确定当创建新场景和导入新资源时 Unity 将使用的默认值。我们还可以忽略资产包按钮。这个按钮让你从 Unity 提供的各种资源和功能中选择。它们可以免费用于你的项目。
-
点击创建项目按钮,Unity 将为我们创建一个全新的项目。
下面的截图展示了 Unity 编辑器的窗口:
-
Unity 的默认布局包含了一系列创建游戏所需的窗口:
-
从左侧开始,层次结构包含了当前场景中所有对象的列表。它们按字母顺序组织,并在父对象下分组。
-
在旁边是场景视图。这个窗口让我们能够在 3D 空间中编辑和排列对象。在左上角,有两组按钮。这些按钮影响你与场景视图的交互方式。
-
最左侧看起来像手的按钮,当你用鼠标点击并拖动时,可以让你平移视角。
-
下一个按钮,交叉的箭头,让你移动对象。如果你使用过任何建模程序,它的行为和提供的工具将会很熟悉。
-
第三个按钮将工具切换到旋转模式。它允许你旋转对象。
-
第四个按钮用于缩放。它也会改变工具。
-
第五个按钮允许你根据对象边界框及其相对于你视角的方向调整其位置和缩放。
-
倒数第二个按钮在轴心和中心之间切换。这将改变最后三个按钮使用的工具位置,要么在选中对象的轴心点,要么在所有选中对象平均位置点。
-
最后一个按钮在局部和全局之间切换。这会改变工具是否与世界原点平行或随选中对象旋转。
-
在场景视图下方是游戏视图。这是场景中任何相机当前渲染的内容。这是玩家在玩游戏时所看到的,用于测试你的游戏。在窗口中上部中央有三个控制游戏视图播放的按钮。
-
第一个是播放按钮。它切换游戏的运行。如果你想测试游戏,按下这个按钮。
-
第二个是暂停按钮。在播放时,按下这个按钮会暂停整个游戏,让你查看游戏的当前状态。
-
第三个是单步按钮。在暂停时,这个按钮可以让你一次一帧地推进游戏。
-
在右侧是检查器窗口。这会显示当前选中任何对象的信息。
-
在左下角是项目窗口。这里显示的是当前项目中存储的所有资源。
-
在其后是控制台。它会显示调试信息、编译错误、警告和运行时错误。
-
-
在顶部,紧挨着帮助的是名为**管理许可…**的选项。选择这个,我们会得到控制许可的选项。按钮描述已经很好地覆盖了它们的功能,所以在这个阶段我们不再详细讲解。
-
接下来我们需要做的是连接可选的代码编辑器。在顶部,转到编辑,然后点击偏好设置…,这将打开以下窗口:
-
通过选择左侧的外部工具,我们可以选择其他软件来管理资源编辑。
-
如果你不想使用 MonoDevelop,请选择外部脚本编辑器旁边的下拉列表,并找到**Notepad++**的可执行文件,或者你选择的任何其他代码编辑器。
-
你的图像应用程序选项也可以在这里改为Adobe Photoshop或你喜欢的任何其他图像编辑程序,与脚本编辑器的方式相同。
-
如果你将 Android SDK 安装到了默认位置,那么不必担心。否则,点击**浏览…**并找到
android-sdk
文件夹。 -
现在,我们要实际创建这个应用程序,请在你的项目窗口内右键点击。
-
在弹出的新窗口中,从菜单中选择创建和C#脚本。
-
为新脚本输入一个名字(
HelloWorld
就很好),然后按Enter键两次:一次确认名字,一次打开它。提示
由于这是第一章,这将是一个简单的 Hello World 应用程序。Unity 支持 C#、JavaScript 和 Boo 作为脚本语言。为了保持一致性,本书将使用 C#。如果你希望使用 JavaScript 编写脚本,可以找到带有本书其他资源的所有项目副本,它们带有
_JS
后缀,表示 JavaScript。 -
每个将要附加到对象的脚本都扩展了
MonoBehaviour
类的功能。JavaScript 会自动这样做,但 C#脚本必须显式定义。但是,正如你在脚本中的默认代码所看到的,我们最初不必担心设置这一点;它会自动完成。扩展MonoBehaviour
类让我们的脚本可以访问游戏对象的各个值,比如位置,并让系统在游戏中的特定事件期间自动调用某些函数,比如更新周期和 GUI 渲染。 -
目前,我们将删除 Unity 在每个新脚本中坚持要包含的
Start
和Update
函数。用一段简单的代码替换它们,在屏幕左上角显示Hello World;现在你可以关闭脚本,回到 Unity 界面:public void OnGUI() { GUILayout.Label("Hello World"); }
-
将
HelloWorld
脚本从项目窗口拖拽到层级窗口中的主相机对象上。恭喜你!你刚刚向 Unity 中的一个对象添加了第一个功能。 -
如果你选择层级中的主相机,那么检查器会显示附加到它的所有组件。列表底部是你的全新
HelloWorld
脚本。 -
在我们测试之前,我们需要保存这个场景。为此,请到顶部选择文件,然后选择保存场景。给它起名为
HelloWorld
并点击保存。你的项目窗口将出现一个新图标,表示你已经保存了场景。 -
现在,你可以自由地按下编辑器中间上方的播放按钮,见证 Hello World 的魔力。
-
我们现在来构建这个应用程序。首先,在顶部选择文件,然后点击构建设置…。
-
默认情况下,目标平台是PC。在平台下,选择Android,并在构建设置窗口左下角点击切换平台。
-
在构建中的场景框下方,有一个标有添加当前的按钮。点击它,将我们当前打开的场景添加到构建中。只有在这个列表中并已选中的场景才会被添加到游戏的最终构建中。旁边带有数字零的场景将是游戏启动时加载的第一个场景。
-
在我们点击构建按钮之前,还有一组东西需要更改。在构建设置窗口底部选择播放器设置…。
-
检查器窗口将打开应用程序的播放器设置(如下截图所示)。从这里,我们可以更改启动画面、图标、屏幕方向以及其他一些技术选项:
-
目前,我们只关心几个选项。最顶部,公司名称是将在应用程序信息下方显示的名字。产品名称是在你的安卓设备上图标下方显示的名字。你可以将这些选项设置为任何你想要的,但它们需要立即设置。
-
重要的设置是捆绑标识符,位于其他设置和识别下方。这是唯一标识你的应用程序与设备上所有其他应用程序不同的标识符。格式是
com.CompanyName.ProductName
,最好在所有产品中使用相同的公司名称。对于这本书,我们将使用com.TomPacktAndBegin.Ch1.HelloWorld
作为捆绑标识符,并选择在组织中使用额外的点(句点)。 -
转到文件,然后再次点击保存。
-
现在,你可以在构建设置窗口中点击构建按钮。
-
选择一个位置保存文件,以及一个文件名(
Ch1_HelloWorld.apk
是个不错的选择)。确保记住它的位置,然后点击保存。 -
如果在构建过程中 Unity 报错关于 Android SDK 的位置,选择它安装位置内的
android-sdk
文件夹。对于 32 位 Windows 系统,默认位置是C:\Program Files\Android\android-sdk
,而对于 64 位 Windows 系统是C:\Program Files (x86)\Android\android-sdk
。 -
当加载完成,应该不会太长时间,你的 APK 就已经制作好了,我们可以继续下一步。
-
本章节我们完成了 Unity 的操作。你可以关闭它并打开一个命令提示符。
-
就像我们连接设备时一样,我们需要导航到
platform-tools
文件夹以连接到我们的设备。如果你将 SDK 安装到默认位置,使用:-
对于 32 位 Windows 系统:
cd c:\program files\android\android-sdk\platform-tools
-
对于 64 位 Windows 系统:
cd c:\program files (x86)\android\android-sdk\platform-tools
-
-
使用以下命令再次检查确保设备已连接并被识别:
adb devices
-
现在我们将安装应用程序。这个命令告诉系统在连接的设备上安装应用程序。
-r
表示如果发现与我们要安装的应用程序具有相同捆绑标识符的应用程序,它应该覆盖。这样,你就可以在开发过程中直接更新游戏,而不是每次需要更新时先卸载再安装新版本。你希望安装的.apk
文件的路径如下所示,用引号括起来:adb install -r "c:\users\tom\desktop\packt\book\ch1_helloworld.apk"
-
用你的 APK 文件路径替换它;字母大小写不重要,但一定要确保所有的空格和标点符号都是正确的。
-
如果一切顺利,控制台将在将应用程序推送到设备后显示上传速度,并在安装完成后显示成功消息。在这个阶段,最常见的错误原因是在发出命令时不在
platform-tools
文件夹中,以及没有正确引用.apk
文件的路径。 -
一旦你收到成功消息,在手机上找到应用程序并启动它。
-
现在,以你用 Unity 的强大功能创建 Android 应用程序的能力为荣吧。
我们已经创建了第一个 Unity 和 Android 应用程序。诚然,这只是个简单的“Hello World”应用程序,但事情总是这样开始的。这对于双重检查设备连接以及在没有游戏干扰的情况下了解构建过程非常有帮助。
如果你想要一个更大的挑战,尝试为应用更改图标。这是一个相当简单的操作,随着游戏的开发,你无疑会想要执行。如何进行这一操作在本节前面已经提到过,但作为提醒,请查看玩家设置。此外,你还需要导入一个图像。查看资产菜单下的内容,了解如何操作。
总结
本章中有许多技术内容。首先,我们讨论了使用 Unity 和 Android 时的好处和可能性。然后是一大堆安装工作:JDK、Android SDK、Unity 3D 和 Unity Remote。之后,我们弄清楚了如何通过命令提示符连接到我们的设备。我们的第一个应用程序制作得既快又简单。我们构建了它,并将其放在设备上。
在下一章中,我们将创建一个更具互动性的游戏——井字棋。我们将探索图形用户界面的奇妙世界。因此,我们不仅会制作游戏,还会让它看起来美观。
第二章:外观美观——图形界面
在上一章中,我们介绍了 Unity 和 Android 的特性,并讨论了将它们一起使用的益处。在我们安装了大量软件并设置好设备之后,我们创建了一个简单的 Hello World 应用程序,以确认一切连接正确。
本章完全关于图形用户界面(GUI)。我们将从使用 Unity 提供的基本 GUI 组件创建一个简单的井字游戏开始。接下来,我们将讨论如何改变我们的 GUI 控件的样式,以改善游戏的外观。我们还将探索一些技巧,以处理 Android 设备的不同屏幕尺寸。最后,我们将学习一种比上一章介绍的方法更快的方式,将我们的游戏放在设备上。说到这里,让我们开始吧。
在本章中,我们将涵盖以下主题:
-
用户偏好设置
-
按钮文字和图片
-
动态 GUI 定位
-
构建和运行
在本章中,我们将在 Unity 中创建一个新项目。这里的第一个部分将指导你完成创建和设置。
创建一个井字游戏
本章的项目是一个简单的类似井字风格的游戏,就像我们可能在纸上玩的那样。与其他任何事情一样,有多种方法可以制作这个游戏。我们将使用 Unity 的 uGUI 系统,以便更好地了解如何为我们的其他游戏创建一个图形用户界面。
游戏板
基本的井字游戏涉及两名玩家和一个 3x3 的网格。玩家轮流用 X 和 O 填充方格。第一个用字母填满一行三个方格的玩家赢得游戏。如果所有方格被填满,但没有玩家达到三个连成一行的方格,则游戏平局。让我们从以下步骤开始创建我们的游戏板:
-
首先,我们需要为本章创建一个项目。因此,启动 Unity,我们将执行这一操作。
如果你一直按照至今的步骤操作,Unity 应该会启动到最后打开的项目。这并不是一个糟糕的特性,但它可能变得非常烦人。想象一下:你一直在一个项目上工作了一段时间,它已经变得很大。现在你需要快速打开别的东西,但 Unity 默认会打开你的大型项目。如果你在它能打开之前等待,那么可能会消耗很多时间。
要更改此功能,请转到 Unity 窗口顶部,点击编辑,然后点击偏好设置。这是我们更改脚本编辑器偏好的同一个地方。不过,这次我们将更改常规标签下的设置。以下屏幕截图显示了常规标签下存在的选项:
-
在这个时候,我们主要关注的是启动时加载上一个项目的选项;不过,我们仍将按顺序介绍所有选项。以下是常规标签下的所有选项的详细解释:
-
自动刷新:这是 Unity 最好的功能之一。因为资产是在 Unity 外部更改的,这个选项允许 Unity 自动检测更改并刷新你项目中的资产。
-
启动时加载上一个项目:这是一个很棒的功能,你应该确保在安装 Unity 时始终不勾选这个选项。勾选后,Unity 将直接打开你最后工作的项目,而不是项目向导。
-
导入时压缩资源:这是用于在资源首次导入 Unity 时自动压缩你的游戏资源的复选框。
-
编辑器分析:这个复选框是用于 Unity 的匿名使用统计。保持勾选状态,Unity 编辑器将偶尔向 Unity 源发送信息。让它开启不会造成任何伤害,并且有助于 Unity 团队改进 Unity 编辑器;然而,这也取决于个人偏好。
-
显示资源商店搜索结果:只有当你计划使用资源商店时,这个设置才是相关的。资源商店可以是任何游戏资产和工具的绝佳来源;然而,由于我们不打算使用它,它与本书的相关性相当有限。它正如其名所示,当你在 Unity 编辑器中搜索资源商店中的内容时,根据这个复选框的设置,会显示搜索结果的数量。
-
验证保存资源:这是一个好选项,可以保持不勾选。如果勾选了这个选项,每次你在 Unity 中点击保存时,都会弹出一个对话框,以便你可以确保保存自上次保存以来所有已更改的资产。这个选项与你模型和纹理无关,而是关注 Unity 的内部文件、材质和预制体。现在最好是关闭它。
-
皮肤(仅限专业版):这个选项仅适用于 Unity Pro 用户。它提供了在 Unity 编辑器的浅色和深色版本之间切换的选项。这纯粹是外观上的,所以你可以根据自己的感觉选择。
-
-
设置好你的偏好设置后,现在转到文件,然后选择新建项目。
-
点击**浏览…**按钮来选择新项目的位置和名称。
-
我们不会使用任何包含的包,所以点击创建,我们可以继续进行。
通过更改一些简单的设置,我们可以避免以后很多麻烦。对于本书中的简单项目来说,这看起来可能不是什么大问题,但对于大型复杂项目,如果你没有选择正确的设置,即使你只是想在项目之间快速切换,也可能会给你带来很多麻烦。
创建棋盘
新项目创建后,我们就有了一个干净的起点来创建我们的游戏。在我们能够创建核心功能之前,我们需要在场景中设置一些结构,以便游戏能够运行,玩家能够与之互动:
-
当 Unity 初始化新项目完成后,我们需要创建一个新的画布。我们可以通过导航到GameObject | UI | Canvas来实现这一点。整个 Unity 的 uGUI 系统需要画布才能在屏幕上绘制任何内容。它有几个关键组件,如接下来的检查器窗口所示,这些组件使得它和界面中的其他所有内容都能正常工作:
-
矩形变换: 这是你几乎在你将要在游戏中使用的每个其他对象上找到的普通变换组件的特殊类型。它跟踪对象在屏幕上的位置、大小、旋转、围绕其旋转的轴心点以及屏幕大小变化时的行为方式。默认情况下,画布的矩形变换被锁定以包括整个屏幕的大小。
-
画布: 这个组件控制它及其所控制的界面元素与相机和场景的交互方式。你可以通过调整渲染模式来改变这一点。默认模式是屏幕空间 - 覆盖,这意味着所有内容都将在屏幕上绘制,并覆盖场景中的其他所有内容。屏幕空间 - 相机模式将在特定距离处从相机绘制所有内容。这使得你的界面受到相机透视性质的影响,但任何可能更靠近相机的模型将出现在它的前面。世界空间模式确保画布及其控制的元素像场景中的任何模型一样在世界中绘制。
-
图形光线投射器: 这是让你实际上可以与各种界面元素交互和点击的组件。
-
-
当你添加画布时,还会创建一个名为EventSystem的额外对象。这就是允许我们的按钮和其他界面元素与脚本交互的东西。如果你不小心删除了它,可以通过转到 Unity 顶部并导航到GameObject | UI | EventSystem来重新创建它。
-
接下来,我们需要调整 Unity 编辑器显示我们游戏的方式,这样我们就可以轻松制作游戏面板。为此,点击场景视图顶部的游戏视图标签,切换到游戏视图。
-
然后,点击写着自由宽高比的按钮,并选择靠近底部的选项:3:2 横向 (3:2)。你游戏将在大多数使用近似此比例屏幕的移动设备上播放。其余的设备在游戏中不会看到任何失真。
-
为了让我们的游戏能够适应各种分辨率,我们需要为画布对象添加一个新组件。在层次结构面板中选择它,然后在检查器面板中点击添加组件,并导航到布局 | 画布缩放器。所选组件允许我们从基本屏幕分辨率开始工作,使其在设备更改时自动缩放我们的 GUI。
-
要选择基本分辨率,请从UI 缩放模式下拉列表中选择随屏幕大小缩放。
-
接下来,我们将X设置为
960
,Y设置为640
。从较大分辨率开始工作比从较小分辨率要好。如果你的分辨率太小,当它们在高分辨率设备上放大时,所有的 GUI 元素都会显得模糊。 -
为了保持组织性,我们需要创建三个空的 GameObject。回到 Unity 顶部,在GameObject下选择创建空对象三次。
-
在层次结构标签中,点击并拖动它们到我们的画布上,使它们成为画布的子对象。
-
为了使它们每个都能用于组织我们的 GUI 元素,我们需要添加 Rect Transform 组件。在检查器中,通过导航到添加组件 | 布局 | Rect Transform来找到它。
-
要重命名它们,请点击检查器顶部它们的名称,并输入新名称。将一个命名为
Board
,另一个Buttons
,最后一个Squares
。 -
接下来,使
Buttons
和Squares
成为Board
的子对象。Buttons
元素将持有我们游戏板上所有可点击的片段,而Squares
将持有已经被选中的格子。 -
为了保持
Board
元素在设备更改时位置不变,我们需要改变它相对于父元素的锚定方式。点击位于Rect Transform右上角的带红色交叉和黄色圆点的方框,展开锚点预设菜单: -
这些选项中的每一个都会影响元素在屏幕尺寸变化时粘附到父元素的哪个角落。我们选择带有四个箭头、每个方向一个的右下角选项。这将使它与父元素一起拉伸。
-
对
Buttons
和Squares
也进行同样的更改。 -
将这些对象的左、上、右和下都设置为
0
。同时确保旋转设置为0
,缩放设置为1
。否则,在我们工作或玩游戏时,界面可能会被奇怪地缩放。 -
接下来,我们需要改变板块的锚点。如果锚点没有展开,点击左侧的小三角形来展开它。无论如何,需要将Max X值设置为
0.667
,这样我们的板块将是一个正方形,覆盖屏幕左边的三分之二。
这个游戏板块是我们项目其余部分的基础。没有它,游戏就无法玩。游戏格子使用它来在屏幕上绘制自己,并锚定到相关位置。稍后,当我们创建菜单时,需要确保玩家只看到我们需要他们与之互动的内容。
游戏格子
既然我们已经有了基础的游戏板块,接下来就需要实际的游戏格子了。没有它们,游戏玩起来就会有些困难。我们需要为玩家创建九个可点击的按钮,九个被选中格子的背景图片,以及九个显示控制格子人员的文本。为了创建并设置它们,请执行以下步骤:
-
就像我们对画布所做的那样,导航到游戏对象 | UI,但这次选择按钮、图像和文本来创建我们需要的一切。
-
每个图像对象都需要一个文本对象作为子对象。然后,所有的图像必须是
Squares
对象的子对象,而按钮必须是Buttons
对象的子对象。 -
所有的按钮和图片都需要在它们的名字中加入数字,以便我们可以将它们组织起来。将按钮命名为
Button0
至Button8
,图片命名为Square0
至Square8
。 -
下一步是布置我们的游戏板,这样我们就可以将事情组织起来并与编程保持同步。我们需要具体设置每个编号的集合。但首先,从锚点预设的右下角选择交叉箭头,确保它们的左、上、右和下的值设置为
0
。 -
为了将我们的按钮和方块放置在正确的位置,只需将数字与以下表格相匹配。这样做的结果就是所有的方块都会有序排列,从左上角开始,到右下角结束:
方块 最小 X 最小 Y 最大 X 最大 Y 0 0 0.67 0.33 1 1 0.33 0.67 0.67 1 2 0.67 0.67 1 1 3 0 0.33 0.33 0.67 4 0.33 0.33 0.67 0.67 5 0.67 0.33 1 0.67 6 0 0 0.33 0.33 7 0.33 0 0.67 0.33 8 0.67 0 1 0.33 -
我们需要添加的最后一样东西是一个指示器,用来显示轮到谁了。像之前一样创建另一个文本对象,并将其重命名为
Turn Indicator
。 -
确保再次将左、上、右和下的值设置为
0
之后,再次将锚点预设设置为蓝色箭头。 -
最后,将锚点下的最小 X 值设置为
0.67
。 -
现在我们拥有玩基本井字游戏所需的一切。要查看它,选择Squares对象并取消右上角的勾选框以关闭它。现在当你点击播放,你应该能够看到整个游戏板并点击按钮。你甚至可以使用 Unity Remote 来测试触摸设置。如果你还没有这样做,保存场景然后继续会是一个好主意。
游戏方块是我们设置初始游戏的最后一步。现在它看起来几乎像一个可玩的游戏了。我们只需要添加一些脚本,就能够玩到我们梦寐以求的所有井字游戏。
控制游戏
拥有一个游戏板是创建任何游戏最重要的部分之一。然而,如果我们无法控制当其各个按钮被按下时发生的情况,那它对我们来说就没有任何好处。现在,让我们创建一些脚本并编写一些代码来解决这个问题:
-
在项目面板中创建两个新的脚本,就像我们在上一章的Hello World项目中做的那样。将新脚本命名为
TicTacToeControl
和SquareState
。打开它们并清除默认函数,就像我们在第一章,Saying Hello to Unity and Android中所做的那样。 -
SquareState
脚本将保存我们游戏板每个方块的可能状态。为此,请清除脚本中的所有内容,包括using UnityEngine
行和public class SquareState
行,这样我们可以用一个简单的枚举来替换它们。枚举只是一个潜在值的列表。这个枚举关注的是控制方块的是哪个玩家。它将允许我们跟踪是 X 控制它,O 控制它,还是它是空的。Clear
语句成为第一个,因此也就是默认状态:public enum SquareState { Clear, Xcontrol, Ocontrol }
-
在我们的另一个脚本
TicTacToeControl
中,首先需要在最开始的using UnityEngine
下面添加一行,这行代码让我们的代码能够与各种 GUI 元素交互,最重要的是,它能与这个游戏交互,允许我们更改控制方块的人和轮到谁的信息:using UnityEngine.UI;
-
接下来,我们需要两个变量,它们将主要控制游戏的流程。它们需要替代两个默认函数。第一个定义了我们的游戏板,这是一个由九个方块组成的数组,用于跟踪谁拥有哪个方块。第二个变量用于记录轮到谁了。当布尔值为
true
时,X 玩家将进行操作。当布尔值为false
时,O 玩家将进行操作:public SquareState[] board = new SquareState[9]; public bool xTurn = true;
-
下一个变量将让我们更改屏幕上显示的轮到谁的信息:
public Text turnIndicatorLandscape;
-
这三个变量将让我们访问到最后一个部分设置的所有 GUI 对象,允许我们根据谁拥有方块来更改图片和文本。我们还可以在点击时打开或关闭按钮和方块。它们都被标记为Landscape,这样当我们在设备Portrait方向上有第二个板块时,我们能够区分它们:
public GameObject[] buttonsLandscape; public Image[] squaresLandscape; public Text[] squareTextsPortrait;
-
最后两个变量,目前将让我们访问到需要更改背景的图片:
public Sprite oImage; public Sprite xImage;
-
我们为此脚本编写的第一个函数将在每次点击按钮时被调用。它接收被点击按钮的数量,并且首先关闭按钮并激活方块:
public void ButtonClick(int squareIndex) { buttonsLandscape[squareIndex].SetActive(false); squaresLandscape[squareIndex].gameObject.SetActive(true);
-
接下来,函数会检查我们之前创建的布尔值,以确定轮到谁了。如果是 X 玩家的回合,方块将设置为使用适当的图片和文本,表明他们的控制权已设定。然后它在脚本内部的游戏板上标记控制方块,最后切换到 O 玩家的回合:
if(xTurn) { squaresLandscape[squareIndex].sprite = xImage; squareTextsLandscape[squareIndex].text = "X"; board[squareIndex] = SquareState.XControl; xTurn = false; turnIndicatorLandscape.text = "O's Turn"; }
-
下一个代码块与上一个相同,不过它标记了 O 玩家的控制权,并将轮次改为 X 玩家:
else { squaresLandscape[squareIndex].sprite = oImage; squareTextsLandscape[squareIndex].text = "O"; board[squareIndex] = SquareState.OControl; xTurn = true; turnIndicatorLandscape.text = "X's Turn"; } }
-
目前代码就这些。接下来,我们需要返回 Unity 编辑器,在场景中设置我们的新脚本。你可以通过创建另一个空的游戏对象并重命名为
GameControl
来实现这一点。 -
通过从项目面板中拖动
TicTacToeControl
脚本,并在选择对象时将其拖放到检查器面板中,将脚本添加到对象上。 -
现在,我们需要附上脚本实际工作所需的所有对象引用。我们不需要在检查器面板中触碰棋盘或XTurn槽,但需要将Turn Indicator对象从层次结构标签拖到检查器面板中的Turn Indicator Landscape槽。
-
接下来,展开Buttons Landscape、Squares Landscape和Square Texts Landscape设置,并将每个大小槽设置为
9
。 -
对于每个新槽,我们需要从层次结构标签中拖动相关的对象。Buttons Landscape下的Element 0对象获得Button0,Element 1获得Button1,依此类推。对所有按钮、图像和文本执行此操作。确保你将它们按正确的顺序排列,否则当玩家进行游戏时,我们的脚本会看起来很混乱,因为它会改变事物。
-
接下来,我们需要一些图像。如果你还没有这样做,通过导航到 Unity 顶部,选择资产 | 导入新资产,并选择要导入的文件,导入本章的起始资产。你需要逐个导航到并选择它们。我们有Onormal和Xnormal用来指示方块的控制器。当按钮只是闲置在那里时,使用ButtonNormal图像,当玩家触摸按钮时,使用ButtonActive。稍后,标题字段将用于我们的主菜单。
-
为了在我们的游戏中使用这些图像,我们需要更改它们的导入设置。逐一选择它们,并在检查器面板中找到纹理类型下拉菜单。我们需要将它们从纹理更改为精灵(2D \ uGUI)。其余设置可以保持默认。如果我们的精灵表包含单个图像中的多个元素,可以使用精灵模式选项。打包标签选项用于在表中分组和查找精灵。像素到单位选项影响精灵在世界空间中渲染时的大小。轴心点选项简单更改图像将旋转的点。
-
对于四个方形图像,我们可以点击精灵编辑器来更改它们渲染时边框的外观。点击后,会打开一个新窗口,显示我们的图像边缘有一些绿线及其在右下角的一些信息。我们可以拖动这些绿线来更改边框属性。绿线外的任何内容都不会随着图像填充比它大的空间而拉伸。每边大约
13
的设置将防止我们的整个边框拉伸。 -
一旦你做了任何更改,请确保点击应用按钮来提交它们。
-
接下来,再次选择游戏控制对象,并将ONormal图像拖到OImage槽,将XNormal图像拖到XImage槽。
-
每个按钮都需要连接到脚本。为此,依次从层次结构中选择它们,并点击它们检查器右下角的加号:
-
然后,我们需要点击无功能左侧的小圆圈,并在新窗口中的列表中选择游戏控制。
-
现在,导航到无功能 | 井字游戏控制 | 按钮点击(int),将我们的代码中的函数连接到按钮。
-
最后,对于每个按钮,将按钮的编号放入函数列表右侧的编号槽中。
-
为了保持组织性,将你的画布对象重命名为
GameBoard_Landscape
。 -
在我们测试之前,请确保通过勾选检查器左上角的框来打开方块对象。同时,取消选中其每个图像子对象的框。
这可能看起来不是世界上最好的游戏,但它是可玩的。我们有一些按钮可以调用脚本中的函数。随着游戏的进行,转向指示会发生变化。此外,每个方块在被选中后会显示谁控制它。再做一些工作,这个游戏就能看起来很棒,也能玩得很好。
调整字体
现在我们已经有了一个基本可玩的游戏,我们需要让它看起来更好一些。我们将添加按钮图片,并选择一些新的字体大小和颜色,使所有内容更具可读性:
-
让我们从按钮开始。选择一个按钮元素,你会在检查器中看到它由一个**图像(脚本)组件和一个按钮(脚本)**组件组成。第一个组件控制当 GUI 元素静止时它的外观。第二个组件控制当玩家与之互动时它的变化以及这会触发哪些功能:
-
源图像:这是当元素静止未被玩家触碰时显示的基础图像。
-
颜色:这控制着正在使用的图像的着色和淡化。
-
材质:这允许你使用可能在 3D 模型上使用的纹理或着色器。
-
图像类型:这决定了图像如何被拉伸以填充可用空间。通常,它会设置为切片,这是用于使用边框的图像,可以选择根据填充中心复选框用颜色填充。否则,它通常会设置为简单,例如,当你使用普通图像时,可以防止保持宽高比的框被奇数大小的矩形变换拉伸。
-
可交互:这简单地切换玩家是否能够点击按钮并触发功能。
-
过渡:这改变了当玩家与按钮交互时按钮的反应方式。颜色色调会使按钮在交互时改变颜色。SpriteSwap会在交互时改变图像。动画将允许你为状态之间的过渡定义更复杂的动画序列。
-
目标图形是用于在屏幕上绘制按钮的基础图像的引用。
-
正常槽、高亮槽、按下槽和禁用槽定义了当按钮未被交互或被鼠标悬停,或者玩家点击按钮并且按钮已被关闭时使用的效果或图像。
-
-
对于我们的每个按钮,我们需要从项目面板将ButtonNormal图像拖到源图像槽中。
-
接下来,点击颜色槽右侧的白框以打开颜色选择器。为了防止我们的按钮变暗,我们需要将A滑块移到最右边或把盒子设置为
255
。 -
我们希望当按钮被按下时改变图像,因此将过渡改为SpriteSwap。
-
移动设备几乎无法悬停在 GUI 元素上,因此我们不需要担心高亮状态。然而,我们确实想要将我们的ButtonActive图像添加到Pressed Sprite槽中,这样当玩家触摸按钮时,它就会切换。
-
按钮方块在有人点击之前应该是空的,因此我们需要删除文本元素。最简单的方法是选择每个按钮下的元素并删除它。
-
接下来,我们需要改变每个图像元素的文本子项。是**文本(脚本)**组件允许我们控制文本如何在屏幕上绘制。
-
文本:这是我们能够更改将在屏幕上绘制的文本的区域。
-
字体:这允许我们选择项目中任何字体文件用于文本。
-
字体样式:这将允许你调整文本的粗体和斜体特性。
-
字体大小:这是文本的大小。这就像在你喜欢的文字处理软件中选择字体大小一样。
-
行间距:这是每行文本之间的距离。
-
富文本:这将允许你使用一些特殊的 HTML 样式标签,仅对部分文本应用颜色、斜体等效果。
-
对齐方式:这会改变文本在框中居中的位置。前三个框调整水平位置。后三个框改变垂直位置。
-
水平溢出/垂直溢出:这些调整文本是否可以绘制在框外,换行还是裁剪。
-
最佳适应:这将自动调整文本的大小,以适应动态大小变化的元素,在最小和最大值之间。
-
颜色/材质:这些会改变文本在绘制时的颜色和纹理。
-
阴影(脚本):这个组件为文本添加了一个阴影效果,就像你在 Photoshop 中添加的那样。
-
-
对于我们的每个文本元素,我们需要使用
120
的Font Size,并且Alignment应该居中。 -
对于Turn Indicator文本元素,我们还需要使用
120
的Font Size,并且需要将其居中。 -
需要做的最后一件事是更改文本元素的颜色为深灰色,这样我们就可以轻松地将其与我们按钮的颜色区分开来:
现在,我们的游戏板运作良好,看起来也很棒。尝试为按钮添加自己的图片。你需要两张图片,一张是按钮静止时的,另一张是按钮被按下时的。此外,默认的 Arial 字体很乏味。为你的游戏找一个新字体;你可以像导入其他游戏资源一样导入它。
旋转设备
如果你到目前为止一直在测试你的游戏,你可能已经注意到,当我们横持设备时,游戏看起来才好看。当设备竖持时,由于正方形和回合指示器试图共享可用的少量水平空间,所有内容都会变得拥挤。由于我们已经为一种布局模式设置好了游戏板,因此为另一种模式复制它就变得相当简单了。然而,这确实需要复制我们的大部分代码,才能使其正常工作:
-
要复制我们的游戏板,右键点击它并从新菜单中选择Duplicate(复制)。将复制的游戏板重命名为
GameBoard_Portrait
。这将是在玩家设备处于竖屏模式时使用的游戏板。为了在制作更改时查看更改,请关闭横屏游戏板,并从Game窗口左上角的下拉列表中选择3:2 Portrait (2:3)。 -
选择GameBoard_Portrait下的Board对象。在其Inspector面板中,我们需要将锚点改为使用屏幕的上三分之二,而不是左三分之二。将Min X设为
0
,Min Y设为0.33
,Max X和Max Y都设为1
即可实现这一点。 -
接下来,需要选择Turn Indicator并将其移到屏幕底部三分之一的位置。将Min X和Min Y设为
0
,Max X设为1
,Max Y设为0.33
,在这里效果会很好。 -
现在我们已经设置好了第二个游戏板,我们需要在代码中为它腾出空间。因此,打开
TicTacToeControl
脚本,并滚动到顶部,这样我们就可以从一些新变量开始。 -
我们将要添加的第一个变量将让我们能够访问屏幕竖屏模式下的回合指示器:
public Text turnIndicatorPortrait;
-
接下来的三个变量将跟踪按钮、正方形图片和所有者文本信息。这些就像我们之前创建的三个列表,用于在横屏模式下跟踪游戏板:
public GameObject[] buttonsPortrait; public Image[] squaresPortrait; public Text[] squareTextsPortrait;
-
在我们脚本顶部要添加的最后两个变量是为了跟踪实际绘制游戏面板的两个画布对象。我们需要这些以便在用户翻转设备时切换它们:
public GameObject gameBoardGroupLandscape; public GameObject gameBoardGroupPortrait;
-
然后,我们需要更新一些函数,使它们对两个面板进行更改,而不仅仅是横屏面板。这两行代码用于在玩家点击时关闭竖屏面板的按钮并激活方块。它们需要放在我们使用
SetActive
对横屏的按钮和方块进行操作的代码后的ButtonClick
函数的开始部分:buttonsPortrait[squareIndex].SetActive(false); squaresPortrait[squareIndex].gameObject.SetActive(true);
-
这两行代码更改了Portrait集中控制方块的图片和文本,以支持 X 玩家。它们放在
ButtonClick
函数的if
语句内,紧接在为横屏集做相同操作的两行代码之后:squaresPortrait[squareIndex].sprite = xImage; squareTextsPortrait[squareIndex].text = "X";
-
这行代码放在同一
if
语句的末尾,更改Portrait集的轮次指示文本:turnIndicatorPortrait.text = "O's Turn";
-
接下来的两行代码更改图片和文本,以支持 O 玩家。它们放在对Landscape集进行相同操作的代码之后,位于
ButtonClick
函数的else
语句内:squaresPortrait[squareIndex].sprite = oImage; squareTextsPortrait[squareIndex].text = "O";
-
这是我们需要添加到
ButtonClick
函数的最后一条代码;它需要放在else
语句的末尾。它只是更改表示轮到谁的文本:turnIndicatorPortrait.text = "X's Turn";
-
接下来,我们需要创建一个新的函数,用于控制在玩家改变设备方向时游戏面板的更改。我们将从定义
Update
函数开始。这是一个由 Unity 每帧调用的特殊函数。它将允许我们检查每一帧的方向是否发生了变化:public void Update() {
-
函数以一个
if
语句开始,该语句使用Input.deviceOrientation
来找出玩家当前的持握方式。它与LandscapeLeft
方向进行比较,以查看设备是否被横向持握,主页按钮在左侧。如果结果为真,则关闭Portrait集的 GUI 元素,同时打开Landscape集:if(Input.deviceOrientation == DeviceOrientation.LandscapeLeft) { gameBoardGroupPortrait.SetActive(false); gameBoardGroupLandscape.SetActive(true); }
-
下一个
else if
语句检查如果主页按钮向下,是否为Portrait
方向。如果为true
,则打开Portrait并关闭Landscape设置:else if(Input.deviceOrientation == DeviceOrientation.Portrait) { gameBoardGroupPortrait.SetActive(true); gameBoardGroupLandscape.SetActive(false); }
-
这个
else if
语句用于检查当主页按钮在右侧时是否为LanscapeRight
方向:else if(Input.deviceOrientation == DeviceOrientation.LandscapeRight) { gameBoardGroupPortrait.SetActive(false); gameBoardGroupLandscape.SetActive(true); }
-
最后,我们检查
PortraitUpsideDown
方向,即主页按钮在设备顶部时。别忘了额外的括号来结束并关闭函数:else if(Input.deviceOrientation == DeviceOrientation.PortraitUpsideDown) { gameBoardGroupPortrait.SetActive(true); gameBoardGroupLandscape.SetActive(false); } }
-
现在我们需要回到 Unity,选择我们的GameControl对象,以便我们可以设置新的Inspector属性。
-
将来自肖像游戏面板的各种部件从层级拖放到检查器中的相关槽位,将转向指示器拖到转向指示器肖像槽位,按钮按顺序拖到按钮肖像列表,方块到方块肖像,以及它们的文本子对象到方块文本肖像。
-
最后,将GameBoard_Portrait对象拖放到游戏面板组肖像槽位中。
现在我们应该能够玩我们的游戏,并在改变设备方向时看到面板切换。由于编辑器和电脑本身没有像移动设备那样的设备方向,你将需要在你的设备上构建项目或使用 Unity 远程连接。确保将你的游戏窗口的显示模式设置为左上角的远程,以便在使用 Unity 远程时与你的设备一起更新。
菜单和胜利
我们的游戏几乎完成了。我们还需要以下内容:
-
一个允许玩家开始新游戏的开始菜单
-
一段用于检查是否有人赢得游戏的代码
-
一个用于显示谁赢得了游戏的游戏结束菜单
设置元素
与游戏面板相比,我们的两个新菜单将相当简单。开始菜单将包括我们游戏的标题图像和一个按钮,而游戏结束菜单将有一个显示胜利消息的文本元素和一个返回主菜单的按钮。下面是设置元素的操作步骤:
-
让我们从开始菜单开始,创建一个新的画布,就像我们之前做的那样,并将其重命名为
OpeningMenu
。这将使我们能够将其与其他创建的屏幕区分开来。 -
接下来,菜单需要一个图像元素和一个按钮元素作为子对象。
-
为了使一切更容易操作,通过它们检查器窗口顶部的复选框关闭游戏面板。
-
对于我们的图像对象,我们可以将标题图像拖到源图像槽位。
-
对于图像的矩形变换,我们需要将Pos X和Pos Y的值设置为
0
。 -
我们还需要调整宽度和高度。我们将匹配原始图像的尺寸,这样它就不会被拉伸。为宽度设置一个值
320
,为高度设置一个值160
。 -
要将图像移动到屏幕上半部分,在Pivot Y槽位中放入一个
0
。这将改变图像的定位基准。 -
对于按钮的矩形变换,我们同样需要在Pos X和Pos Y中输入值
0
。 -
我们需要为宽度再次输入一个值
320
,但这次我们希望高度的值为100
。 -
要将其移动到屏幕下半部分,我们需要在Pivot Y槽位中输入一个值
1
。 -
接下来,我们需要为按钮设置图像,就像之前为游戏板所做的那样。将
ButtonNormal
图像放入源图像槽中。将过渡更改为精灵交换,并将ButtonActive
图像放入按下精灵槽中。别忘了将颜色更改为颜色选择器中的A值为255
,这样我们的按钮就不会部分褪色。 -
最后,为了使此菜单更改按钮文本,请在层次结构中展开按钮并选择文本子对象。
-
在此对象的检查器面板中,文本下方是一个文本字段,我们可以在其中更改按钮上显示的文本。这里的值设置为
新游戏
会很合适。同时,将字体大小更改为45
,这样我们才能实际阅读它。 -
接下来,我们需要创建游戏结束菜单。因此,关闭我们的开场菜单并为游戏结束菜单创建一个新的画布。将其重命名为
GameOverMenu
,以便我们可以继续保持组织性。 -
对于此菜单,我们需要一个文本元素和一个按钮元素作为其子项。
-
我们将几乎与上一个完全相同的方式设置这个。文本和按钮都需要在Pos X和Pos Y槽中具有
0
的值,以及320
的宽度值。 -
文本将使用
160
的高度和0
的Pivot Y。我们还需要将字体大小设置为80
。你可以更改默认文本,但无论如何它都会被我们的代码覆盖。 -
要使菜单中的文本居中,请从对齐属性旁边的两组按钮中选择中间的按钮。
-
按钮将使用
100
的高度和1
的Pivot Y。 -
同时,请确保将源图像、颜色、过渡和按下精灵设置为适当的图像和设置。
-
需要设置的最后一项是按钮的文本子项。将默认文本设置为主菜单,并将字体大小设置为
45
。
这样就完成了我们的菜单设置。我们有所有让玩家与游戏互动所需的屏幕。唯一的问题是,我们没有实现任何功能让它们实际执行操作。
添加代码
为了使我们的游戏板按钮起作用,我们不得不在脚本中创建一个函数,它们可以引用并在被触摸时调用。主菜单的按钮将开始新游戏,而游戏结束菜单的按钮将切换屏幕至主菜单。我们还需要创建一小段代码,以便在开始新游戏时清除并重置游戏板。如果我们不这样做,玩家将无法在需要重新启动整个应用程序之前玩超过一轮的游戏。
-
打开
TicTacToeControl
脚本,这样我们可以对其进行更多修改。 -
我们将在脚本顶部添加三个变量。前两个将跟踪两个新菜单,使我们能够根据需要打开或关闭它们。第三个是用于游戏结束屏幕中的文本对象,它将根据游戏结果给我们提供显示消息的能力。
-
接下来,我们需要创建一个新函数。
NewGame
函数将被主菜单中的按钮调用。其目的是重置棋盘,这样我们就可以继续玩,而无需重置整个应用程序:public void NewGame() {
-
该函数首先将游戏设置为从 X 玩家的回合开始。然后创建一个
SquareStates
的新数组,这实际上会清除旧的棋盘。然后设置横屏和竖屏两组控制的回合指示:xTurn = true; board = new SquareState[9]; turnIndicatorLandscape.text = "X's Turn"; turnIndicatorPortratit.text = "X's Turn";
-
然后,我们遍历竖屏和横屏控制的九个按钮和方块。所有按钮都通过
SetActive
打开,方块关闭,这就像点击检查器面板左上角的小复选框一样:for(int i=0;i<9;i++) { buttonsPortrait[i].SetActive(true); squaresPortrait[i].gameObject.SetActive(false); buttonsLandscape[i].SetActive(true); squaresLandscape[i].gameObject.SetActive(false); }
-
代码的最后三行控制当我们切换到游戏板时哪些屏幕可见。默认情况下,它选择打开横屏板并确保竖屏板关闭。然后关闭主菜单。别忘了最后的括号来结束函数:
gameBoardGroupPortrait.SetActive(false); gameBoardGroupLandscape.SetActive(true); mainMenuGroup.SetActive(false); }
-
接下来,我们需要在
ButtonClick
函数的末尾添加一行代码。这是一个简单的调用,用于检查在处理完按钮和方块后是否有人赢得了游戏:CheckVictory();
-
CheckVictory
函数遍历游戏中可能获胜的组合。如果它找到连续三个匹配的方块,将调用SetWinner
函数,当前游戏将结束:public void CheckVictory() {
-
在这个游戏中,连续三个匹配的方块组成一次胜利。我们从被循环标记的列开始检查。如果第一个方块不是
Clear
,将其与下面的方块进行比较;如果它们匹配,再检查下面的方块。我们的棋盘是作为列表存储但以网格形式绘制,所以我们需要加三来下移一个方块。else if
语句随后对每一行进行检查。通过将循环值乘以三,我们将跳过每一层循环的一行。我们再次将方块与SquareState.Clear
进行比较,然后与它右侧的方块,最后与它右侧的两个方块。如果任一条件正确,我们将集合中的第一个方块发送到另一个函数以更改游戏屏幕:for(int i=0;i<3;i++) { if(board[i] != SquareState.Clear && board[i] == board[i + 3] && board[i] == board[i + 6]) { SetWinner(board[i]); return; } else if(board[i * 3] != SquareState.Clear && board[i * 3] == board[(i * 3) + 1] && board[i * 3] == board[(i * 3) + 2]) { SetWinner(board[i * 3]); return; } }
-
下面的代码片段与刚才看到的
if
语句基本相同。然而,这些代码检查对角线。如果条件为true
,再次发送到另一个函数以更改游戏屏幕。你可能也注意到了函数调用后的返回。如果在任何一点找到胜者,就没有必要检查棋盘的其余部分。因此,我们将提前退出CheckVictory
函数:if(board[0] != SquareState.Clear && board[0] == board[4] && board[0] == board[8]) { SetWinner(board[0]); return; } else if(board[2] != SquareState.Clear && board[2] == board[4] && board[2] == board[6]) { SetWinner(board[2]); return; }
-
这是我们的
CheckVictory
函数的最后一点。如果没有人赢得游戏,由函数的先前部分判断,我们必须检查平局。这是通过检查游戏板的所有格子来完成的。如果其中任何一个格子是Clear
,游戏尚未结束,我们退出函数。但是,如果我们遍历整个循环而没有找到一个Clear
的格子,我们通过宣布平局来设定胜者:for(int i=0;i<board.Length;i++) { if(board[i] == SquareState.Clear) return; } SetWinner(SquareState.Clear); }
-
接下来,我们创建一个
SetWinner
函数,该函数在CheckVictory
函数中被反复调用。这个函数传递了谁赢得了游戏的信息,它最初会开启游戏结束屏幕并关闭游戏板:public void SetWinner(SquareState toWin) { gameOverGroup.SetActive(true); gameBoardGroupPortrait.SetActive(false); gameBoardGroupLandscape.SetActive(false);
-
然后,函数检查谁赢得了比赛,并为
victorText
对象选择一个适当的信息:if(toWin == SquareState.Clear) { victorText.text = "Tie!"; } else if(toWin == SquareState.XControl) { victorText.text = "X Wins!"; } else { victorText.text = "O Wins!"; } }
-
最后,我们有
BackToMainMenu
函数。这个函数简短而精炼;它只是被游戏结束屏幕上的按钮调用,以切换回主菜单:public void BackToMainMenu() { gameOverGroup.SetActive(false); mainMenuGroup.SetActive(true); }
这就是我们在游戏中拥有的所有代码。我们拥有了构成游戏的所有视觉部分,现在我们也拥有了所有功能部分。最后一步是将它们组合起来,完成游戏。
将它们组合起来
我们已经有了代码和菜单。一旦将它们连接起来,我们的游戏就完成了。为了完成这一切,请执行以下步骤:
-
回到 Unity 编辑器,从Hierarchy面板中选择GameControl对象。
-
它的Inspector窗口中的三个新属性需要填写。将OpeningMenu画布拖到Main Menu Group槽中,将GameOverMenu拖到Game Over Group槽中。
-
同时,找到GameOverMenu的文本对象子级,并将其拖到Victor Text槽中。
-
接下来,我们需要为每个菜单连接按钮功能。首先选择OpeningMenu画布的按钮对象子级。
-
点击其**Button (Script)**组件右下角的小加号,以添加新的功能槽。
-
点击新槽中心的圆圈,并从新弹出的窗口中选择GameControl,就像我们对每个游戏板按钮所做的那样。
-
当前显示No Function的下拉列表是我们的下一个目标。点击它,然后导航到TicTacToeControl | NewGame ()。
-
重复这几个步骤,为GameOverMenu的子按钮添加功能。不过,从列表中选择BackToMainMenu()。
-
最后要做的就是使用Inspector左上角的复选框关闭游戏板和游戏结束菜单。只留下开场菜单,这样当我们玩游戏时,游戏将从那里开始。
恭喜!这就是我们的游戏。我们的所有按钮都已设置,我们拥有多个菜单,甚至还创建了一个根据玩家设备方向改变的游戏板。最后要做的就是为我们的设备构建它,并展示出来。
为设备构建的更好方法。
现在,是每个人渴望了解的构建过程部分。有一个更快更简单的方法来构建你的游戏并在你的 Android 设备上玩。长而复杂的方法仍然非常值得一知。如果这个简短的方法失败了,而且在某个时候它会失败,了解长方法有助于你调试任何错误。另外,简短路径只适合为单个设备构建。如果你有多个设备和一个大项目,使用简短的构建过程加载它们将需要更多的时间。按照以下步骤操作:
-
首先,打开构建设置窗口。记住,它可以在 Unity 编辑器顶部的文件下找到。
如果你还没有这样做,保存你的场景。保存场景的选项也可以在 Unity 编辑器顶部的文件下找到。
-
点击添加当前按钮,将我们当前的场景(也是唯一一个场景)添加到构建中的场景列表中。如果这个列表是空的,就没有游戏。
-
如果您还没有这样做,请确保将您的平台更改为Android。毕竟,这是这本书的重点。
-
不要忘记设置玩家设置。点击玩家设置按钮,在检查器窗口中打开它们。你可能还记得我们在第一章中提到过,Saying Hello to Unity and Android。
-
在顶部,设置公司名称和产品名称字段。这些字段分别设置为
TomPacktAndroid
和Ch2 TicTacToe
,将匹配包含的已完成项目。记住,这些字段会被玩你游戏的人看到。 -
在其他设置下的捆绑标识符字段也需要设置。格式仍然是
com.CompanyName.ProductName
,所以com.TomPacktAndroid.Ch2.TicTacToe
会很好用。为了在设备上看到我们酷炫的动态 GUI,还有一个设置应该更改。点击分辨率和展示以展开选项。 -
我们关注的是默认方向。默认是纵向,但这个选项意味着游戏将被固定在纵向显示模式。点击下拉菜单,选择自动旋转。这个选项告诉 Unity 无论游戏是被持在哪个方向,都会自动调整游戏使其直立。
当选择自动旋转时弹出的新选项集允许限制支持的方向。也许你正在制作一个需要更宽并且横屏持握的游戏。通过取消勾选纵向和纵向倒置,Unity 仍然会进行调整(但只针对剩余的方向)。
注意
在你的 Android 设备上,控制按钮位于较短的边之一;这些通常是主页、菜单和返回或最近应用按钮。这一侧通常被认为是设备的底部,这些按钮的位置决定了每个方向。纵向模式是指这些按钮相对于屏幕向下。横向右模式是指它们位于右侧。这种模式开始变得清晰,不是吗?
-
现在,保留所有方向选项的勾选状态,我们将返回到构建设置。
-
下一步(这是非常重要的)是将你的设备连接到电脑上,并给它一点时间以被识别。如果你的设备不是第一个连接到电脑的设备,这条简短的构建路径将会失败。
-
在构建设置窗口的右下角,点击构建并运行按钮。系统会要求你给应用程序文件,即 APK,一个合适的名字,并将其保存到适当的位置。一个像
Ch2_TicTacToe.apk
这样的名字就很好,并且可以将其保存在桌面上。 -
点击保存,然后坐下来欣赏所提供的精彩加载条。如果你注意到了我们在第一章中的Hello World项目中构建的加载条,你会发现这次我们多了一个步骤。应用程序构建完成后,会有一个推送至设备的步骤。这意味着构建成功,Unity 现在正在将应用程序安装到你的设备上。完成这一步后,游戏将在设备上启动,加载完成。
我们刚刚了解了构建并运行按钮,这是由构建设置窗口提供的。这种方法快速、简单,且无需使用命令提示行的痛苦;这样简短的构建路径不是很棒吗?然而,如果构建过程由于任何原因失败,包括无法找到设备,应用程序文件将不会被保存。如果你想再次尝试安装,就必须重新进行整个构建过程。这对于我们简单的井字游戏来说并不算太糟糕,但对于较大的项目可能会消耗很多时间。此外,在构建时你只能将一个 Android 设备连接到电脑上。如果连接更多设备,构建过程肯定会失败。而且 Unity 在完成可能很长的构建过程之后才会检查多个设备。
除了这些注意事项之外,构建并运行选项真的相当不错。让 Unity 处理将游戏传送到设备上的复杂部分。这为我们提供了更多的时间来专注于测试和制作一款伟大的游戏。
如果你想要一个挑战,这是一个艰难的任务:创建单人模式。你将需要从添加一个额外的按钮开始,这个按钮位于开场屏幕上,用于选择第二种游戏模式。任何计算机玩家的逻辑都应该放在Update
函数中。同时,查看Random.Range
以随机选择一个方块进行控制。否则,你可以多做一点工作,让计算机寻找可以获胜或创建两个匹配行的方块。
总结
在这一点上,你应该已经熟悉了 Unity 的新 uGUI 系统,包括如何定位 GUI 元素,根据需要设置它们的样式,以及向它们添加功能。
在本章中,我们通过创建一个井字游戏,学习了关于 GUI 的所有内容。我们首先熟悉了创建按钮和其他要在游戏的 GUI 画布上绘制的对象。在深入改善游戏的外观之后,我们继续通过为游戏板添加动态方向来改进它。我们创建了一个开场和结束屏幕,以完善游戏体验。最后,我们探索了将游戏部署到设备上的另一种构建方法。
在下一章中,我们将开始创建一个全新且更复杂游戏。我们将要制作的坦克大战游戏,将用于了解任何游戏的基本构建块:网格、材质和动画。当一切完成时,我们将能够在多彩的城市中驾驶坦克并射击动画目标。
第三章:任何游戏的核心——网格、材质和动画
在上一章中,我们了解了 GUI。我们从创建一个简单的井字游戏开始,学习游戏的基本组成部分。然后通过改变游戏的外观并使游戏板支持多种屏幕方向来继续。最后,我们完成了一些菜单的制作。
本章将介绍任何游戏的核心:网格、材质和动画。没有这些基础,通常没有东西可以展示给玩家。当然,你也可以只使用 GUI 中的平面图像。但这样有什么乐趣呢?既然你选择了 3D 游戏引擎,不妨充分利用它的功能。
为了理解网格、材质和动画,我们将创建一个坦克大战游戏。这个项目将在其他章节中使用。到本书结束时,这将是我们创建的两个完整游戏之一。在本章中,玩家将驾驶坦克在一个小城市中四处移动,他们能够射击动画目标,我们还将添加一个计数器来跟踪分数。
本章包括以下主题:
-
导入网格
-
创建材质
-
动画
-
创建预制体
-
光线追踪
在本章中,我们将开始一个新项目,请按照第一部分来启动。
设置准备
尽管这个项目最终会比之前的更大,但实际设置与前一个项目类似,并不复杂。这个项目你需要一些起始资源,这些将在设置过程中进行描述。由于这些资源的复杂性和特定性,建议现在使用本书代码包中提供的资源。
与前两章一样,我们需要创建一个新项目,以便开发下一款游戏。显然,首先要做的就是启动一个新的 Unity 项目。为了便于组织,将其命名为Ch3_TankBattle
。以下是启动本项目所需的前提条件:
-
这个项目也会比我们之前的项目变得更大,因此我们应该创建一些文件夹来保持组织性。首先,创建六个文件夹。顶级文件夹将是
Models
、Scripts
和Prefabs
文件夹。在Models
内创建Environment
、Tanks
和Targets
。拥有这些文件夹使得项目管理起来更加容易。任何完整的模型可以包含一个网格文件,一个或多个纹理,每个纹理对应一个材质,以及可能包含数十个动画文件。 -
在继续之前,如果你还没有这样做,最好是将你的目标平台改为 Android。每次更改目标平台,项目中的所有资源都需要重新导入。这是 Unity 自动执行的一步,但随着项目的增长,这将花费越来越多的时间。在项目中有任何内容之前设置目标平台,我们可以节省很多时间。
-
我们还将利用 Unity 一个非常强大的部分:预制体。这些特殊对象使创建游戏的过程大大简化。这个名字意味着预先制造的——事先创建并复制的。对我们来说,这意味着我们可以完全设置一个坦克射击的目标,并将其转换成预制体。然后,我们可以在游戏世界中放置预制体的实例。如果我们需要更改目标,只需修改原始预制体即可。对预制体所做的任何更改也会应用于该预制体的任何实例。别担心,使用时它会更有意义。
-
我们需要为这个项目创建一些网格和纹理。首先,我们需要一辆坦克(如果没有坦克,进行坦克大战是有点困难的)。这个代码包中提供的坦克有一个炮塔和一门大炮,这些都是独立的部件。我们还将使用一个技巧,让坦克的履带看起来像是在移动,所以它们每个都是独立的部件,并使用单独的纹理。
-
最后,我们需要一个动画目标。本书代码包中提供的那个像人的手臂一样装有牛眼的手。它有四个动画。第一个从卷曲的位置开始,移动到伸展的位置。第二个与第一个相反,从伸展的位置回到卷曲的位置。第三个从伸展的位置开始,向后弹起,就像从前面被打到,然后回到卷曲的位置。最后一个与第三个类似,但是它是向前移动,就像是从后面被打到一样。这些动画相当简单,但它们将帮助我们很好地了解 Unity 的动画系统。
这里发生的事情很少;我们只是创建了一个项目并添加了一些文件夹。我们还简要讨论了将为本章项目使用哪些资源。
导入网格
有几种方法可以将资源导入 Unity。我们将介绍最简单(也可能是最好)的方法来导入资源组。让我们开始吧:
-
在 Unity 编辑器中,首先在你的
Tanks
文件夹上右键点击,然后从菜单中选择在资源管理器中显示。 -
这会打开包含所选择资源的文件夹。在本例中,
Models
文件夹在 Windows 文件夹浏览器中打开。我们只需将坦克及其纹理放入Tanks
文件夹中。注意事项
本章提供的文件有
Tank.blend
、Tanks_Type01.png
和TankTread.png
。此外,在 Unity 中使用.blend
文件需要在你的系统中安装 Blender。Blender 是一个免费的建模程序,可在www.blender.org
获取。Unity 利用它将前述文件转换成可以完全利用的文件。 -
当我们回到 Unity,它会检测到我们添加的文件,并自动导入。这是 Unity 最好的特点之一。无需明确告诉 Unity 导入。如果项目资产内部发生变化,它会自动更新资产。
-
你可能还会注意到,当 Unity 导入我们的坦克时,会创建一个额外的文件夹和一些文件。每当导入新网格时,默认情况下 Unity 会尝试将其与材质配对。下一节将详细介绍 Unity 中的材质是什么。现在,它是一个跟踪如何在网格上显示纹理的对象。根据网格中的信息,Unity 在项目中查找具有正确名称的材质。如果找不到,将在网格旁边创建一个
Materials
文件夹,并在其中创建缺失的材质。创建这些材质时,Unity 也会查找正确的纹理。这就是为什么将纹理与网格同时添加到文件夹中很重要,以便它们可以一起导入。如果你没有在导入坦克的同时添加纹理,关于创建材质的部分将介绍如何将纹理添加到材质中。
我们已经将坦克导入 Unity。这真的很简单。对项目中的任何资产或文件夹所做的更改都会被 Unity 自动检测到,并根据需要相应地导入。
坦克导入设置
将任何资源导入 Unity 是通过使用一组默认设置完成的。这些设置都可以从检查器窗口进行更改。选中你的新坦克后,我们将在这里介绍模型导入设置:
如前一张截图所示,在检查器窗口顶部有三个标签页:模型、绑定和动画。模型页面处理网格本身,而绑定和动画用于导入动画。目前我们只关心模型页面,如果尚未选择,请选择它。下面将详细介绍模型页面的每个部分。
网格
前一张截图中的网格部分有以下选项:
-
导入设置窗口中的网格部分以缩放因子属性开始。这是一个告诉 Unity 网格默认大小的值。你的建模程序中的一个通用单位或一米转换为 Unity 中的一个单位。这个坦克是以通用单位制作的,所以坦克的缩放因子是 1。如果你在制作坦克时使用的是厘米,那么缩放因子将是 0.01,因为厘米是米的一百分之一。
-
文件缩放选项是原始创建模型时建模程序中使用的缩放。它主要是信息性的。如果你需要调整导入模型的大小,请调整缩放因子。
-
下一个选项,网格压缩,在我们讨论游戏优化时将在最后一章变得非常重要。压缩设置得越高,游戏中文件的大小就会越小。然而,这也会开始让你的网格出现一些奇怪的现象,因为 Unity 会尝试使其更小。现在,将其设置为关闭。
-
如果你想在游戏运行时对网格进行修改,读/写启用选项将非常有用。这使得你可以实现一些非常酷的功能,比如可破坏的环境,你的脚本可以根据被射击的位置将网格分割成碎片。然而,这也意味着 Unity 需要在内存中保留网格的一个副本,如果它很复杂,这可能会让系统开始变得卡顿。这超出了本书的范围,因此取消选中此选项是个好主意。
-
优化网格选项是一个好的选择,除非你对网格有特定的高级操作。开启这个选项,Unity 会进行一些特殊的“幕后”处理。在计算机图形学中,尤其是在 Unity 中,每个网格最终都是由一系列在屏幕上绘制的三角形组成。此选项允许 Unity 重新排列文件中的三角形,以便更快、更容易地绘制整个网格。
-
导入混合形状选项允许 Unity 理解模型中可能包含的任何混合形状。这些是模型顶点的动画位置。通常,它们用于面部动画。下一个选项,生成碰撞器,在进行物理方面的复杂操作时非常有用。Unity 有一组简单的碰撞器形状,应该尽可能使用,因为它们更容易处理。然而,在某些情况下,它们可能无法完全完成任务;例如,瓦砾或半管,其中碰撞形状太复杂,无法用一系列简单的形状制作。这就是为什么 Unity 有一个网格碰撞器组件。选中此选项后,将为模型中的每个网格添加一个网格碰撞器组件。本章我们将坚持使用简单的碰撞器,所以将生成碰撞器选项关闭。
-
交换 UV和生成光照贴图 UV选项主要用于处理光照,尤其是光照贴图时。Unity 可以处理模型上的两套 UV 坐标。通常,第一套用于纹理,第二套用于光照贴图或阴影纹理。如果它们的顺序错误,交换 UV会将它们更改,使得第二套先出现。如果你需要一个光照贴图的展开,但并未创建一个,生成光照贴图 UV将为你创建一个。在这个项目中我们不使用光照贴图,所以这两个选项可以保持关闭。
法线与切线
早期截图中的法线与切线部分有以下选项:
-
下一个选项部分,法线与切线,从法线选项开始。这定义了 Unity 如何保存你的网格的法线。默认情况下,它们是从文件中导入的;然而,也有一个选项让 Unity 根据网格的定义方式计算它们。否则,如果我们将此选项设置为无,Unity 将不会导入法线。如果我们希望网格受到实时光照的影响或使用法线贴图,就需要法线。在这个项目中我们将使用实时光照,所以将其设置为导入。
-
如果你的网格具有法线贴图,那么切线、平滑角度和分割切线选项将派上用场。切线用于确定光照如何与法线贴图表面交互。默认情况下,Unity 会为你计算这些。导入切线仅限于几种文件类型。基于两个面之间角度的平滑角度,决定了边缘的着色是平滑还是锐利。分割切线选项用于处理一些特定的光照问题。如果光照被接缝破坏,启用此选项将修复它。法线贴图非常适合让低分辨率游戏看起来像高分辨率游戏。然而,由于使用它们需要额外的文件和信息,它们并不适合移动游戏。因此,在本书中我们不使用它们,这些选项都可以关闭以节省内存。
-
保持四边形选项将允许你的模型利用 DirectX 11 的新镶嵌技术,从低细节模型和特殊的位移贴图创建高细节模型。不幸的是,移动设备支持这种细节还需要一段时间,而要成为普遍现象则需要更长时间。
材质
前一个截图中的材质部分有以下选项:
-
最后一个部分,材质,定义了 Unity 应该如何查找材质。第一个选项,导入材质,允许你决定是否导入材质。如果关闭,将应用默认的白色材质。这种材质在项目中任何地方都不会显示;它是一个隐藏的默认值。对于不会有任何纹理的模型,比如碰撞网格,可以关闭这个选项。对于我们坦克模型以及几乎其他所有情况,应该保持开启状态。
-
最后两个选项,材质命名和材质搜索,共同作用于为网格命名和查找材质。在它们下面直接是一个文本框,描述了 Unity 将如何搜索材质。
-
要搜索的材质名称可以是建模程序中使用的纹理名称、建模程序中创建的材质名称,或者是模型和材质的名称。如果找不到纹理名称,将使用材质名称。
-
默认情况下,Unity 会进行递归向上搜索。这意味着我们从
Materials
文件夹开始搜索,然后查找同一文件夹中的任何材质。接着检查父文件夹是否有匹配的材质,然后是上一级文件夹。如此继续,直到找到具有正确名称的材质,或者到达根资产文件夹。 -
另外,我们还可以选择检查整个项目,或者只在我们模型旁边的
Materials
文件夹中查找。这些选项的默认设置已经很好了。通常,它们不需要更改。特别是对于大型项目,可以使用 Unity 编辑器脚本轻松处理,本书将不涉及这部分内容。
-
恢复和应用按钮
接下来,截图中有恢复和应用按钮,下面将对此进行解释:
-
每当对导入设置进行更改时,必须选择两个按钮中的一个,恢复或应用。恢复按钮取消更改,并将导入设置恢复到更改之前的状态。应用按钮确认更改,并使用新设置重新导入模型。如果没有选择这些按钮,Unity 会弹出一个对话框并强制你做出选择,然后才能进行其他操作。
-
最后,我们可以看到如前截图所示有两种预览类型。Imported Object部分是如果我们将对象添加到Scene视图并选择它,在Inspector窗口中对象外观的预览。Preview窗口,我们可以在其中看到坦克模型的区域,是模型在Scene视图中的样子。你可以在该窗口中点击并拖动对象来旋转它,并从不同的角度观察它。此外,在这个窗口中有一个小蓝按钮。点击这个按钮,你将能够给对象添加标签。然后,这些标签也可以在Project窗口中进行搜索。
设置坦克
既然我们已经导入了坦克,我们需要对其进行设置。我们将调整坦克的布局,并创建一些脚本。
坦克
在这一点上,我们创建坦克的主要工作将包括创建和排列坦克的组件。使用以下步骤,我们可以设置我们的坦克:
-
首先,从Project窗口将坦克拖到Hierarchy窗口。你会注意到坦克的名字在Hierarchy窗口中以蓝色显示。这是因为它是一个预制体实例。你的项目中的任何模型在很大程度上都像预制体。然而,我们希望我们的坦克不仅仅是放在那里;所以,作为一个静态网格的预制体是没有帮助的。因此,在Hierarchy窗口中选择你的坦克,我们将开始通过移除Animator组件使其变得有用。为此,在Inspector窗口中选择 Animator 组件右侧的齿轮。从新的下拉列表中选择Remove Component,如下截图所示,它将被移除:
-
如果你正在使用默认提供的坦克,选择它的不同部分,你会发现所有的轴心点都在底部。这对于使我们的炮塔和炮管正确旋转并不有用。解决这个问题的最简单方法就是添加新的空GameObject作为轴心点。
注意事项
场景中的任何物体都是一个
GameObject
。任何空的GameObject
只包含一个Transform组件。 -
在 Unity 编辑器的顶部,Create Empty是GameObject按钮下的第一个选项。它创建了我们所需要的物体。创建两个空的 GameObject,并将一个定位在炮塔底部,另一个定位在炮管底部。此外,分别将它们重命名为
TurretPivot
和CannonPivot
。如果选择了物体,这可以通过Inspector窗口顶部的文本框来完成。 -
在层次结构窗口中,将
TurretPivot
拖到Tank
上。这将改变TurretPivot
的父对象为Tank
。然后,将对象(即炮塔网格)拖到TurretPivot
上。在代码中,我们将旋转枢轴点而不是直接旋转网格。当一个父对象移动或旋转时,所有子对象都会随之移动。当你进行这个更改时,Unity 会抱怨关于对象原始层次结构的更改;它这样做只是为了确保这是一个你想要做的更改,而不是一个意外: -
由于失去与预制件的连接可能会破坏游戏,Unity 想要确保我们确实希望这样做。因此,点击继续按钮,我们就可以在没有 Unity 其他抱怨的情况下完成坦克的工作。我们还需要将
CannonPivot
设置为TurretPivot
的子对象,并将炮管设置为CannonPivot
的子对象。 -
为了完成我们的层次结构更改,我们需要放置摄像机。由于我们希望玩家看起来就像是在坦克里一样,摄像机应该放在坦克后面和上方,稍微向下倾斜,以聚焦在几辆坦克长度前的一个点。一旦定位好,也将其设置为
TurretPivot
的子对象。
我们已经建立了一个基础结构,我们的坦克将会使用这个结构。通过这种方式使用多个对象,我们可以独立地控制它们的移动和动作。在这一点上,我们不再拥有一个只能向前指的僵硬坦克,我们可以独立地倾斜、旋转和瞄准每个部分。
提示
同时,坦克应该位于你希望整个物体围绕其旋转的中心点上方。如果它不是,你可以在层次结构窗口中选择基础坦克对象下的所有内容,并移动它们。
保持计分
本节将重点关注一个简短的脚本,用于跟踪玩家的分数和文本元素的添加。以下是创建我们脚本的步骤:
-
让我们的坦克工作的第一个脚本非常简单。创建一个新的脚本,并将其命名为
ScoreCounter
。顾名思义,它将跟踪分数。在Scripts
文件夹中创建它,并清除到目前为止我们制作的其他脚本中的默认函数。 -
正如上一章所做的那样,由于任何需要访问我们的 GUI 元素的脚本都需要在脚本最顶部添加一行代码,在
using UnityEngine;
这行代码之后添加以下代码行。这允许我们使用并更改需要显示分数的文本元素:using UnityEngine.UI;
-
下一行代码应该从上一章看起来很熟悉。首先,我们定义了一个整数计数器。由于它是静态的,其他脚本(例如我们为靶子创建的脚本)将能够修改这个数字,并给我们得分:
public static int score = 0;
-
然后,我们将添加一个变量来存储界面的文本元素。它将像上一章中的转向指示器一样工作,为我们提供一个位置来更新和显示玩家的分数:
public Text display;
-
这个脚本的最后一段代码是一个
Update
函数。这个函数由 Unity 自动为每一帧调用。这是放置任何需要定期更改而无需玩家直接输入的代码和逻辑的完美位置。对于我们的目的,我们将更新文本元素,并确保它总是显示最新的分数。通过将分数添加到双引号中,我们将数字转换为单词,以便文本元素可以正确使用它:public void Update() { display.text = "" + score; }
这就是这个非常简单的脚本的全部内容。它将跟踪整个游戏过程中的分数。此外,它本身不会执行任何分数增加的操作,而是由其他脚本更新计数器来给玩家加分。
重复按钮
到目前为止我们使用的按钮只在按下并释放时执行操作。我们的玩家需要按住按钮来控制他们的坦克。因此,我们需要创建一个重复按钮;一个只要按住就会执行操作的按钮。按照以下步骤来创建一个重复按钮:
-
创建一个名为
RepeatButton
的新脚本。 -
为了让这个脚本能够访问到它需要工作的 Unity 部分,和之前的脚本一样,我们需要在写着
using UnityEngine;
的那一行下面添加以下两行。第一行将让我们访问到Selectable
类:所有交互式界面元素都从中派生的那个类。第二行将使我们能够处理玩家与我们新按钮交互时发生的事件:using UnityEngine.UI; using UnityEngine.EventSystems;
-
接下来,我们需要更新代码中的
public class
行。任何为游戏中的对象提供功能的普通脚本都是对MonoBehaviour
类的扩展。我们需要将行更改为以下内容,以便我们的脚本可以存在于界面中并扩展其功能:public class RepeatButton : Selectable {
-
我们的脚本总共有四个变量。第一个允许它跟踪是否被按下:
private bool isPressed = false;
-
接下来的三个变量将提供与上一章中按钮相同的功能。对于按钮,我们必须选择一个对象,然后选择特定脚本中的一个函数,最后发送一些值。这里,我们将做同样的事情。这里的第一变量跟踪我们要在场景中与之交互的对象。第二个将是附加到对象上某个脚本中的函数名称。最后一个将是一起发送给函数的数字,它将提供更具体的输入:
public GameObject target; public string function = ""; public float value = 0f;
-
本脚本的第一函数将覆盖
Selectable
类提供的函数。当玩家点击按钮时立即调用它。它接收到一些关于点击方式和位置的信息,这些信息存储在eventData
中。第二行只是调用了父类中同名的函数。该函数最后做的是设置我们的布尔标志,以标记按钮当前正被玩家按下:public override void OnPointerDown(PointerEventData eventData) { base.OnPointerDown(eventData); isPressed = true; }
-
下一个函数与上一个函数完全相同。主要区别在于,当玩家的鼠标或触摸不再位于界面中的按钮上时调用它。第二个区别是它将布尔值设置为
false
,因为当玩家将手指从按钮上移开时,他们不再按下按钮,在这种情况下我们希望停止执行我们的动作:public override void OnPointerExit(PointerEventData eventData) { base.OnPointerExit(eventData); isPressed = false; }
-
以下函数与前两个类似。但是,当按钮释放时调用它:
public override void OnPointerUp(PointerEventData eventData) { base.OnPointerUp(eventData); isPressed = false; }
-
该脚本的最后一个函数是我们的
Update
函数。它首先检查玩家当前是否按下了按钮。然后它在我们目标对象上调用SendMessage
函数,告诉它要执行哪个函数以及使用哪个数字。SendMessage
函数仅对GameObject和MonoBehviour组件可用。它接收一个函数名,并尝试在接收消息的 GameObject 上找到它:public void Update() { if(isPressed) { target.SendMessage(function, value); } }
另一个脚本完成了!这个脚本允许我们按住按钮,而不是被迫反复按下按钮来在游戏中移动。
控制底盘
常规坦克可以进行原地旋转,并且可以轻松地前进和后退。我们将通过创建一个脚本来使我们的坦克实现这一点。按照以下步骤为坦克创建我们的第二个脚本:
-
第二个脚本称为
ChassisControls
。它将使我们的坦克四处移动。我们将在Scripts
文件夹中创建它。 -
脚本的前三行定义了坦克移动所需的变量。我们还可以在检查器窗口中更改它们,以防我们的坦克太快或太慢。第一行定义了一个变量,该变量保存了对
CharacterController
组件的连接。这个组件不仅容易移动坦克,而且还能让它碰到墙壁和其他碰撞体时停止。接下来的两行代码定义了我们移动和旋转的速度:public CharacterController characterControl; public float moveSpeed = 10f; public float rotateSpeed = 45f;
-
我们首先定义
MoveTank
函数,它需要传递一个speed
值来决定坦克应该向哪个方向以及多远前进。正值将使坦克向前移动,负值将使其向后移动:public void MoveTank(float speed) {
-
为了在三维空间中移动,我们需要一个向量——一个既有方向又有大小的值。因此,我们定义了一个移动向量,并将其设置为坦克的前进方向,乘以坦克的速度,再乘以自上一帧以来经过的时间量。
-
如果你记得几何课上的内容,3D 空间有三条轴:x、y 和 z。在 Unity 中,以下约定适用:x 是向右,y 是向上,z 是向前。transform 组件保存了一个对象的位置、旋转和缩放的这些值。我们可以通过调用 Unity 提供的
transform
变量来访问 Unity 中任何对象的 transform 组件。transform
组件还提供了一个forward
变量,它会给出一个指向对象面向方向的向量。 -
此外,我们希望以恒定的速度移动,例如,每秒移动一定的距离;因此,我们使用了
Time.deltaTime
。这是 Unity 提供的一个值,它表示自上次在屏幕上绘制游戏的帧以来已经过去了多少秒。你可以把它想象成翻书。为了使一个人看起来像是在页面上走动,他在每一页上都需要稍微移动一点。在游戏的情况下,页面不是定期翻动的。因此,我们必须根据翻到新页面所花费的时间来调整我们的移动。这有助于我们保持恒定的速度。Vector3 move = characterControl.transform.forward * speed * Time.deltaTime;
-
-
接下来,我们希望角色保持在地面上。通常,在游戏中,任何你想控制的字符不会自动获得像石头那样的所有物理特性,比如重力。例如,当跳跃时,你暂时移除了重力,使角色能够向上移动。这就是为什么下一行代码简单地实现了重力,通过减去正常的重力速度,然后使其与我们的帧率保持同步:
move.y -= 9.8f * Time.deltaTime;
-
最后,对于
MoveTank
函数,我们实际上执行了移动操作。CharacterController
组件有一个特殊的Move
函数,它能够移动角色并受到碰撞的限制。我们只需通过传递move
向量来告诉它本帧我们想要移动多远以及移动的方向。当然,最后的这个花括号结束了这个函数的定义:characterControl.Move(move); }
-
RotateTank
函数也需要一个速度值来指定旋转的速度和方向。我们从定义另一个向量开始;然而,这个向量不是定义移动的方向,而是定义旋转的方向。在这种情况下,我们将围绕向上的方向旋转。然后我们将这个向量乘以我们的speed
和Time.deltaTime
参数,以足够快的速度移动并保持与我们的帧率同步。public void RotateTank(float speed) { Vector3 rotate = Vector3.up * speed * Time.deltaTime;
-
函数的最后部分实际上执行了旋转操作。Transform组件提供了一个
Rotate
函数。在 3D 空间中,尤其是进行旋转操作时,可能会变得复杂和困难。Rotate
函数为我们处理了所有这些操作;我们只需要为它提供要应用的旋转值。此外,别忘了用花括号结束这个函数的定义:characterControl.transform.Rotate(rotate); }
我们创建了一个控制坦克移动的脚本。它将使用CharacterController
组件的专用Move
函数,使我们的坦克能够前进和后退。我们还使用了Transform组件提供的专用Rotate
函数来旋转坦克。
控制炮塔
下一个脚本将允许玩家旋转他们的炮塔并瞄准炮管:
-
我们需要为坦克创建的最后一个脚本为
TurretControls
。这个脚本将允许玩家左右旋转炮塔,以及上下倾斜炮管。与所有其他脚本一样,在Scripts
文件夹中创建它。 -
我们定义的前两个变量将保存指向炮塔和炮管旋转点的指针——我们为坦克创建的空
GameObjects
。第二组是炮塔和炮管的旋转速度。最后,我们设置了一些限制值。如果我们不对炮管的旋转角度进行限制,它只会不停地旋转,穿过坦克。这对于坦克来说并不是最真实的行为,因此我们必须设置一些限制。限制范围是 300 度,因为正前方是 0 度,向下是 90 度。我们希望它是向上的角度,所以范围是 300 度。我们也可以使用 359.9 度,因为 Unity 会将 360 度变为零度,以便它能够继续旋转:public Transform turretPivot; public Transform cannonPivot; public float turretSpeed = 45f; public float cannonSpeed = 20f; public float lowCannonLimit = 315f; public float highCannonLimit = 359.9f;
-
接下来是
RotateTurret
函数。它的工作原理与RotateTank
函数完全相同。但是,我们不是查看CharacterController
组件的transform
变量,而是对turretPivot
变量进行操作:public void RotateTurret(float speed) { Vector3 rotate = Vector3.up * speed * Time.deltaTime; turretPivot.Rotate(rotate); }
-
第二个也是最后一个函数
RotateCannon
,在处理旋转时会更深入一些。这完全是因为我们需要对炮管的旋转设置限制。打开函数后,第一步是确定我们这一帧将旋转多少。我们使用浮点值而不是向量,因为我们必须自己设置旋转:public void RotateCannon(float speed) { float rotate = speed * Time.deltaTime;
-
接下来,我们定义一个变量来保存当前的旋转值。这样做是因为 Unity 不允许我们直接对旋转值进行操作。实际上,Unity 以四元数的形式跟踪旋转,这种方法超出了本书的讨论范围。幸运的是,Unity 为我们提供了名为
EulerAngles
的方法,通过 x、y 和 z 定义旋转。这是围绕三维空间中的三个轴的旋转。Transform组件的localEulerAngles
值是相对于父GameObject的旋转。Vector3 euler = cannonPivot.localEulerAngles;
注意
它被称为
EulerAngles
,因为这是由瑞士数学家莱昂哈德·欧拉提出的一种定义旋转的方法。 -
接下来,我们通过使用
Mathf.Clamp
函数一次性调整旋转并应用限制。Mathf
是一组有用的数学函数。clamp
函数接收一个值,并使其不低于也不高于传递给函数的其他两个值。因此,我们首先发送我们的x轴旋转,这是从euler
的当前 x 旋转中减去 rotate 的结果。由于沿轴正向旋转是顺时针的,因此我们需要减去我们的旋转,以便向上而不是向下使用正值。接下来,我们将下限传递给Clamp
函数,然后是我们的上限:这是我们顶部脚本中定义的lowCannonLimit
和highCannonLimit
变量:euler.x = Mathf.Clamp(euler.x – rotate, lowCannonLimit, highCannonLimit);
-
最后,我们需要将新的旋转实际应用到炮塔的支点。这只需将变换组件的
localEulerAngles
值设置为新的值。同样,请确保使用花括号关闭函数:cannonPivot.localEulerAngles = euler; }
我们现在已经创建了一个可以控制坦克炮塔的脚本。玩家将能够控制炮管的倾斜和炮塔的旋转。这个脚本与我们之前创建的ChassisControls
脚本功能非常相似——区别在于限制炮管可以倾斜的程度。
组装部件
这暂时是最后一个脚本了。我们有我们的坦克和脚本;下一步是将它们组合起来:
-
现在,我们需要向坦克中添加脚本。还记得我们在上一章如何将
井字游戏
脚本添加到摄像机上的吗?首先在层次结构窗口中选择你的坦克。在这些脚本工作之前,我们首先需要在坦克上添加CharacterController
组件。因此,在 Unity 编辑器顶部选择组件,然后选择物理,最后点击角色控制器选项。你会注意到,当你添加新组件后,在场景视图中坦克上会出现一个绿色的胶囊。这个胶囊表示将与其他碰撞体发生碰撞和交互的空间。角色控制器组件上的值允许我们控制它与其他碰撞体的交互方式。在大多数情况下,前四个参数的默认值都是可以的。
角色控制器中的参数如下:
-
斜率限制:这个属性显示控制器可以爬升的斜坡的最大倾斜度。
-
步进偏移:这个属性显示了一个台阶在开始阻挡移动之前可以有多高。
-
皮肤宽度:这定义了另一个碰撞体在完全停止之前可以穿透此控制器碰撞体的距离。这主要用于在物体间挤压。
-
最小移动距离:这个属性用于限制抖动。这是在一帧中实际移动之前必须应用的最小移动量。
-
Center/Radius/Height:这些属性定义了你在 Scene 视图中看到的胶囊的大小。它们用于碰撞。
-
-
目前最后三个值最为重要。我们需要尽可能调整这些值以匹配我们坦克的大小。诚然,胶囊是圆形的,我们的坦克是方形的,但
CharacterController
组件是移动带碰撞的角色最简单的方式,它将最常被使用。将 Radius 属性和 Center 属性的 Y 部分设置为2.3
;其他部分可以保持默认值。 -
现在是向坦克添加脚本的时候了。通过在 Hierarchy 窗口中选择坦克,并将
ChassisControls
、TurretControls
和ScoreCounter
脚本拖到 Inspector 窗口。这与我们在前几章中所做的一样。 -
接下来,我们需要完成在脚本中开始的连接。首先点击
CharacterController
组件的名称,并将其拖到我们新的ChassisControls
脚本组件上的 Character Control 插槽。Unity 允许我们在 Unity 编辑器中连接对象变量,这样它们就不必硬编码。 -
我们还需要连接我们的炮塔和炮管旋转点。因此,从 Hierarchy 窗口点击并拖动点,到
TurretControls
脚本组件上的相应变量。 -
在测试我们的游戏之前,我们需要创建一堆 GUI 按钮来实际控制我们的坦克。首先创建一个画布,就像我们在上一章中所做的那样,并创建一个空的 GameObject。
-
空的 GameObject 需要一个 Rect Transform 组件,并且需要将其设置为
Canvas
的子对象。 -
将其重命名为
LeftControls
并将其锚点设置为左下角。此外,将 Pos X 设置为75
,Pos Y 设置为75
,Pos Z 设置为0
,Width 设置为150
,Height 设置为150
,如下截图所示: -
接下来,我们需要四个按钮作为
LeftControls
的子对象。与上一章一样,通过导航到 GameObject | UI | Button,可以在编辑器顶部找到它们。 -
将四个按钮重命名为
Forward
、Back
、Left
和Right
。同时,你也可以将它们的文本子对象更改为相关的文本,比如F
、B
、L
和R
。 -
按钮仅在玩家点击并释放时激活。仅仅为了使坦克移动而重复点击效果不太好。因此,点击它们每个 Button 组件右侧的齿轮,并选择 Remove Component。
-
现在,将我们的
RepeatButton
脚本添加到每一个按钮上。由于我们扩展了Selectable
类,你可以看到我们对按钮拥有与其他按钮相同的控制权。 -
将四个按钮的 Width 和 Height 值设置为
50
。它们的位置如下所示:按钮 Pos X Pos Y Forward 0 50 左 -50 0 Back 0 -50 Right 50 0 -
现在我们已经有了四个移动按钮,需要将它们连接到我们的坦克上。对于每个按钮,将层次结构面板中的
Tank
拖动到检查器面板中的目标槽里。 -
当我们下次设置函数和值槽时,拼写非常重要。如果有一点偏差,你的函数将找不到,会出现很多错误,坦克也将无法工作。对于
Forward
按钮,将函数槽设置为MoveTank
,值槽设置为1
。Back
按钮在函数槽中也需要MoveTank
的值,但在值槽中需要-1
。Left
按钮在函数槽中需要RotateTank
的值,值槽中需要-1
。Right
按钮在函数槽中需要RotateTank
的值,值槽中需要1
。 -
接下来,我们需要设置我们的炮塔控制。在层次结构窗口中右键点击
LeftControls
,并从新菜单中选择复制。将新副本重命名为RightControls
。 -
这个新的控制集需要一个右下角的锚点,PosX为
-75
,PosY为75
(如下面的截图所示): -
这组下面的按钮需要被重命名为
Up
、Down
、Left
和Right
。它们的文本可以分别更改为U
、D
、L
和R
。 -
Up
按钮的函数槽应该设置为RotateCannon
,其值槽的值为1
。Down
按钮的函数槽值为RotateCannon
,值槽的值为-1
。Left
按钮需要RotateTurret
作为函数槽的值,值槽的值为-1
。最后,Right
按钮需要函数槽的值为RotateTurret
,值槽的值为1
。 -
最后一件事是创建一个新的文本元素,可以通过导航到游戏对象 | UI | 文本来找到,并将其重命名为
Score
。 -
最后,选择你的
Tank
,并将层次结构窗口中的Score
拖动到分数计数器(脚本)组件的显示槽中。 -
将场景保存为
TankBattle
并试玩一下。
我们刚刚完成了坦克的组装。除非在使用移动控制时查看场景视图,否则很难判断坦克是否在移动。炮塔控制可以在游戏视图中看到。除了没有判断坦克是否在移动的参照点外,它运行得相当好。下一步和下一节将为我们添加城市,提供那个参照点。
你可能会注意到,当你第一次尝试倾斜炮管时,会有一个快速的跳跃。这种行为很烦人,会让游戏看起来不完整。尝试调整炮管以修复它。如果你在这方面遇到麻烦,请查看炮管的起始旋转角度。这与我们每次尝试移动它时旋转被限制的方式有关。
创建材质
在 Unity 中,材质是决定模型在屏幕上如何绘制的关键因素。它们可以是简单的全蓝色,也可以是复杂的有波浪反射的水面。在本节中,我们将介绍控制材质的详细内容。我们还将创建我们的城市以及一些简单的材质来为其贴图。
城市
创建一个城市可以为我们的坦克和玩家提供一个良好的游戏场所。按照以下步骤来创建我们的城市:
-
为了本节的目的,本书代码包中提供的城市部分没有被赋予特定的纹理。它只是被展开,并创建了一些可平铺的纹理。因此,我们需要从导入城市和纹理到
Environment
文件夹开始。以我们导入坦克的相同方式来完成这个操作。注意
相关文件包括
TankBattleCity.blend
、brick_001.png
、brick_002.png
、brick_003.png
、dirt_001.png
、dirt_003.png
、pebbles_001.png
、rocks_001.png
、rubble_001.png
以及water_002.png
。 -
当城市被展开时,Unity 仍然会为其创建一个单一材质。然而,在任何建模程序中都没有应用纹理。因此,材质是纯白色的。我们有多个额外的纹理,所以我们需要的不只是一个材质来覆盖整个城市。创建新材质很简单;就像创建新脚本一样。在
Environment
文件夹内的Materials
文件夹上右键点击,选择创建,然后点击菜单中间的材质。 -
这将在文件夹中创建一个新的材质,并立即允许我们为其命名。将材质命名为
Pebbles
。 -
选择新的材质后,查看一下检查器窗口。当我们选择了一个材质,我们就可以获得改变其外观所需的选项:
-
从前面的截图我们可以看到以下内容:
-
在检查器窗口的最顶部,我们有材质的名称,其后是一个着色器下拉列表。着色器本质上是一个简短的脚本,它告诉显卡如何在屏幕上绘制某物。你通常会使用标准着色器;它本质上是一个全能型着色器,因此默认情况下总是被选中。在这里,你可以选择任何特殊效果或自定义着色器。
-
渲染模式下拉菜单让你选择此材料是否使用任何程度的透明度。不透明表示它将是实心的。剪切选项将基于你的纹理透明区域的Alpha 截止值以锐利的边缘渲染。透明选项将基于你的纹理的 alpha 通道提供平滑的边缘。
-
主贴图
主贴图部分包含以下选项:
-
主贴图部分从漫反射开始,这里放置你的主颜色纹理。可以通过纹理槽右侧的颜色选择器进行着色。
-
高光选项定义了材料的光泽度;你可以想象成设备屏幕上的反光。你可以使用图像来控制它,或者使用颜色选择器来确定反射的颜色以及通过平滑度来控制反光的锐利程度。
-
法线贴图选项允许你添加一个控制材料表面阴影的纹理。这些纹理需要特别导入。如果你选择的纹理没有正确设置,会出现一个警告框,你可以选择立即修复来更改它。还会出现一个滑块,让你控制纹理的效果程度。
-
高度贴图选项的工作方式与法线贴图类似。它调整材料的凹凸程度,并提供一个滑块来调整它。
-
遮挡选项允许你向材料添加环境遮挡纹理,根据模型中物体之间的接近程度来控制材料的暗度或亮度。
-
发射选项让你控制材料发出的投影光和颜色。这只会影响光照图和此材料的外观。要实际动态地发出光,必须通过添加实时光源来模拟。
-
细节遮罩选项允许你控制次要贴图中的纹理在材料上的出现位置。
-
平铺和偏移的值控制纹理的大小和位置。平铺的值决定了纹理在规范化的 UV 空间内沿 x 和 y 方向重复的次数。偏移参数是纹理在规范化的 UV 空间中从零点开始的距离。你可以选择数字字段并输入值来修改它们。这样做,并注意底部的预览窗口,你将看到它们如何改变纹理。平铺纹理通常用于大面积表面,这些表面的纹理相似且特定纹理只是重复出现。
次要贴图
次要贴图部分包含以下选项:
-
次要贴图从细节漫反射 x2开始,这是一个额外的漫反射纹理,用于与你的主漫反射纹理混合。它可能用于在巨石表面添加凹凸不平的变化。
-
法线贴图与主要的法线贴图槽类似,控制细节纹理的阴影。
-
第二组贴图平铺和偏移值与第一组类似,只是控制细节纹理。通常这些值设置得比第一组高,以在材质表面添加额外的兴趣点。
-
UV 集只是让您选择细节纹理将要使用的模型展开集,这些纹理将应用于所添加材质的模型。
-
通过从项目窗口拖拽
pebbles_001
纹理,并将其放置在漫反射槽右侧的方框中,来为这个材质添加纹理。 -
为了使纹理的颜色更好,使用漫反射槽右侧的颜色选择器,选择一种浅褐色。
-
主要贴图平铺的X和Y值设为
30
,将使得当贴图平铺应用到我们城市的街道时更容易观察。 -
为了查看我们新材质的效果,首先将你的城市拖到层次结构窗口,使其添加到场景视图中。通过右键拖动,你可以在场景视图中查看四周,使用W、A、S和D可以四处移动。看看城市的街道。
-
现在,将您的新材质从项目窗口拖到场景视图中。拖动材质时,您应该看到网格发生变化,好像它们正在使用该材质。当您拖过街道时,松开左键鼠标。现在材质已经应用到网格上了。
-
然而,我们目前有一个城市的四分之一需要贴图。因此,创建更多材质,并将剩余的纹理应用到城市的其他部分。为每个额外的纹理创建一个新材质,四个额外的
brick_002
纹理,这样我们可以为每栋建筑物的高度设置不同的颜色。 -
根据以下截图或您自己的艺术感,将新的材质应用到城市中:
提示
当你试图到达中心喷泉时,如果坦克挡道了,在层次结构窗口中选择你的坦克,并在场景视图中使用小工具将其拖开。
如果你现在尝试玩游戏,你可能会注意到我们有一些问题。首先,我们只有一个城市的四分之一;如果你制作了自己的城市,可能会有更多。此外,城市上仍然没有碰撞,所以当我们移动时,会直接穿过它。
-
改变坦克的大小非常简单。在层次结构窗口中选择它,并在变换组件中找到缩放标签。更改缩放下的X、Y和Z值将改变坦克的大小。确保均匀地改变这些值,否则当我们开始旋转坦克时,会出现一些奇怪的现象。
0.5
的值使得坦克足够小,可以通过小街道。 -
接下来是城市部分的碰撞处理。在大多数情况下,我们可以使用简单的碰撞形状以加快处理速度。然而,城市的圆形中心需要特别处理。首先在场景视图中双击其中一个方形建筑的墙壁。
提示
在处理预制体时(城市仍然是预制体),点击构成预制体的任何对象都会选择根预制体对象。一旦选择了预制体,点击它的任何部分都会选择那个单独的部分。由于这种行为与非线性预制体对象不同,当你选择场景视图中的对象时,需要注意这一点。
-
选择一组墙壁后,在 Unity 编辑器顶部选择组件,然后选择物理,最后选择盒状碰撞器。
-
由于我们将碰撞器添加到了一个特定的网格上,Unity 会尽可能地自动调整碰撞器以适应形状。对于我们来说,这意味着新的
BoxCollider
组件已经调整到适合建筑的尺寸。继续为其余的方形建筑和外围墙添加BoxCollider
组件。我们的街道本质上只是一个平面,因此BoxCollider
组件对它们来说也足够使用。尽管它是指向顶部的,但喷泉中心的方尖碑本质上只是一个盒子;因此另一个BoxCollider
对它也非常合适。 -
我们还剩一栋建筑和喷泉环需要处理。这些不是盒子、球体或胶囊形状,因此我们简单的碰撞器将无法工作。选择靠近中心喷泉的最后一栋建筑的墙壁。在您选择盒状碰撞器的位置往下几行,有一个网格碰撞器选项。这将给我们的对象添加一个
MeshCollider
组件。这个组件如其名所示,它获取一个网格并将其转变为碰撞器。将MeshCollider
组件添加到特定网格上,它会自动选择该网格作为可碰撞对象。你还需要将MeshCollider
组件添加到中心建筑周围的小型边缘和喷泉周围的环墙上。 -
要解决的最后一个问题是城市区域的复制。首先在层级窗口中选择根城市对象,选择
TankBattleCity
,并从它上面移除Animator
组件。城市不需要动画,因此不需要这个组件。 -
现在,在层级窗口中对城市右键点击,然后点击复制。这将创建一个被选中对象的副本。
-
再复制两次城市区域,我们就会有城市的四个部分了。唯一的问题是,它们将全部处于完全相同的位置。
-
我们需要旋转三个部分以形成一个完整的城市。选择一个部分,并将变换组件中的Y 轴旋转值设置为
90
。这将围绕垂直轴旋转 90 度,给我们半个城市。 -
我们将通过将其中一个剩余的部分设置为
180
度,另一个设置为270
度来完成城市的构建。 -
还剩下最后一件事情要做。我们有四个中心喷泉。在四个城市片段中的三个里,选择构成中心喷泉的三个网格(
Obelisk
、Wall
和Water
),然后在键盘上按下 Delete 键。每次确认你想打破预制件连接,我们的城市就会像下图一样完整:
现在就来尝试这个游戏吧。我们可以驾驶坦克在城市中穿梭并旋转炮塔。这真是太有趣了。我们创建了材质并对城市进行了纹理化处理,在让玩家能够与建筑物和道路发生碰撞后,我们复制了这一部分,以便拥有整个城市。
既然你已经具备了导入网格和创建材质所需的所有技能,挑战就是装饰城市。创建一些瓦砾和坦克陷阱,并练习将它们导入 Unity 并在场景中设置。如果你真的想做得更好,尝试自己创建一个城市;从世界上选择一些东西,或者用你的想象力做一些事情。一旦创建完成,我们就可以在其中释放坦克。
移动的履带
还剩下最后一件事情要做,然后我们就可以完成材质部分并且继续让游戏变得更加有趣。还记得材质的 Offset 值吗?事实证明,我们实际上可以用脚本控制它。执行以下步骤,让履带随着我们的坦克移动:
-
首先,打开
ChassisControls
脚本。 -
首先,我们需要在脚本开始处添加几个变量。前两个将保存对我们坦克履带渲染器的引用,这是网格对象的一部分,负责跟踪应用到网格上的材质并实际进行绘制。这类似于
characterControl
变量保存对我们CharacterController
组件的引用:public Renderer rightTread; public Renderer leftTread;
-
接下来的两个变量将跟踪每个履带应用的偏移量。我们在这里存储它,因为这样比每帧尝试从履带的材质中查找要快。
private float rightOffset = 0; private float leftOffset = 0;
-
为了利用这些新值,需要在
MoveTank
函数的末尾添加以下代码行。这里的第一行根据我们的速度调整右侧履带的偏移量,并与我们的帧率保持同步。第二行利用Renderer
组件的材质值找到坦克履带的材质。材质的mainTextureOffset
值是材质中主纹理的偏移量。在我们的漫反射材质中,这是唯一的纹理。然后,我们必须将偏移量设置为一个包含我们新偏移值的新Vector2
值。Vector2
就像我们用于移动的Vector3
一样,但它工作在 2D 空间而不是 3D 空间。纹理是平面的;因此,它是一个 2D 空间。代码的最后两行与其他两行做同样的事情,但针对的是坦克的左侧履带:rightOffset += speed * Time.deltaTime; rightTread.material.mainTextureOffset = new Vector2(rightOffset, 0); leftOffset += speed * Time.deltaTime; leftTread.material.mainTextureOffset = new Vector2(leftOffset, 0);
-
为了将我们的履带与
Renderer
组件连接起来,我们需要对准点做同样的事情:将履带网格从层次结构窗口拖到检查器窗口中相应的值。完成这一步后,请确保保存并尝试一下。
我们更新了ChassisControls
脚本来使坦克的履带移动。当坦克四处行驶时,纹理会向适当的方向移动。这是用于制作水中波浪和其他移动纹理的相同类型的功能。
材料移动的速度与坦克的速度不完全匹配。找出如何为坦克的履带添加额外的速度值。此外,如果坦克在旋转时履带能向相反方向移动将会很酷。实际上,坦克是通过让一个履带向前另一个向后来实现转向的。
Unity 中的动画
我们将要介绍下一个主题是动画。在探索 Unity 中的动画时,我们将为我们的坦克创建一些射击目标。Unity 的动画系统Mecanim的强大功能很大程度上在于处理人形角色。但是,设置和动画人形角色本身就可以填满一本书,所以这里不会介绍。然而,我们仍然可以学习和使用 Mecanim 做很多事情。
下面的要点将解释导入动画时所有可用的设置:
-
在继续介绍动画导入设置之前,我们需要一个动画模型来操作。我们还有最后一组资源要导入到项目中。将
Target.blend
和Target.png
文件导入到我们项目的Targets
文件夹中。导入后,调整目标模型的导入设置窗口中的模型页面,就像我们对坦克所做的那样。现在,切换到骨骼标签(如下图所示): -
动画类型属性告诉 Unity 在执行动画时当前模型将使用哪种类型的骨骼。不同类型的模型无法共享动画。动画类型下的不同选项如下:
-
人形选项为处理人形角色的页面添加了许多按钮和开关。但同样,这里过于复杂,不进行介绍。
-
一个通用的骨骼仍然使用 Mecanim 及其许多功能。实际上,这仅仅是任何不类似人类结构的动画骨骼。
-
第三个选项,Legacy,使用了 Unity 旧版的动画系统。然而,这个系统将在未来几个 Unity 版本中被淘汰,因此这里也不会进行介绍。
-
最后一个选项,None,表示对象将不会有动画。你可以为坦克和城市选择这个选项,因为它也会阻止 Unity 添加 Animator 组件,并节省最终项目的大小。
-
-
根节点的值是模型文件中每个对象的列表。其目的是选择你的动画绑定的基础对象。对于这个目标,选择位于第二个骨架选项下的Bone_Arm_Upper。
-
当勾选优化游戏对象选项时,它将隐藏你的模型整个骨骼。点击新出现的框上的加号,将允许你选择特定的骨骼,这些骨骼在你通过层级窗口查看模型时仍然需要访问。在处理具有大量骨骼的复杂绑定时,这个选项特别有用。
-
导入设置的最后一个标签页是动画,它包含了我们将文件中的动画导入 Unity 所需的一切。在目标导入设置窗口的顶部,我们有导入动画的复选框。如果一个对象不会进行动画,那么关闭这个选项是个好主意。这样做还可以节省项目空间。
-
下面的选项,烘焙动画,仅当你的动画包含运动学且来自 3ds Max 或 Maya 时使用。这个目标是来自 Blender 的,所以该选项是灰显的。
-
接下来的四个选项,动画压缩,旋转误差,位置误差和缩放误差,主要用于平滑抖动的动画。几乎在所有情况下,默认设置对于使用来说都很好。
-
我们在这里真正关心的是剪辑部分。这将列出当前从模型中导入的每个动画剪辑。在列表的左侧,我们有剪辑的名称。在右侧,我们可以看到剪辑的开始和结束帧。剪辑部分下的各种参数如下:
-
Unity 将为每个新模型添加一个默认动画。这是在保存文件时从你的建模程序的默认预览范围生成的剪辑。在我们的目标案例中,这是默认采集。
-
在 Blender 中,还可以为每个绑定创建一系列动作。默认情况下,Unity 会将它们作为动画剪辑导入。在这种情况下,创建了ArmatureAction剪辑。
-
在剪辑下方和右侧,有一个带有**+和–**按钮的小标签页。这两个按钮分别将剪辑添加到末尾和移除选定的剪辑。
-
-
当选择一个剪辑时,下一个部分会出现。它以一个文本字段开始,用于更改剪辑的名称。
-
在使用 Blender 时,文本字段下方有一个源采集下拉列表。这个列表与默认动画相同。大多数时候,你只需使用默认采集;但是,如果你的动画总是出现错误或缺失,首先尝试更改源采集下拉列表。
-
然后,我们有一个小的时间线,以及动画剪辑的开始和结束帧的输入字段。点击两个蓝色旗帜并在时间线上拖动它们将改变输入字段中的数字。
-
接下来,我们有循环时间、循环姿势和循环偏移。如果我们希望动画重复,请勾选循环时间旁边的框。循环姿势将使得动画的第一帧和最后一帧中的骨骼位置相匹配。当动画循环时,循环偏移将变得可用。这个值让我们调整循环动画开始的帧。
-
接下来的三个小节,根变换旋转、根变换位置(Y)和根变换位置(XZ),允许我们通过动画控制角色的移动。这些部分下的控制如下:
-
这三个部分都有一个烘焙到姿势的选项。如果这些选项未被选中,根节点(我们在绑定页面选择了它)在动画中的移动将被转换为整个对象的移动。这样想:假设你要在动画程序中动画一个向右跑的角色,实际上你会移动他们,而不是像平常一样在原地动画。
-
使用 Unity 的旧动画系统,要让角色的物理部分移动碰撞器,必须用代码移动 GameObject。因此,如果你使用这个动画,角色看起来像是移动了,但实际上没有碰撞。使用这个新系统,当播放动画时整个角色都会移动。然而,这需要不同的更复杂的设置才能完全工作。所以我们没有在坦克上使用这个,尽管我们可以使用。
-
三个部分中的每一个都包含一个基于的下拉选项。这个选项的选择决定了每个部分的物体的中心。如果你在使用人形角色,会有更多的选择,但目前我们只有两个选项。如果选择根节点,意味着根节点对象的轴心点是中心。如果选择原始,则由动画程序定义的原点是物体的中心。
-
前两个部分还有一个偏移选项,用于纠正动作中的错误。当为角色动画行走周期时,如果角色稍微向侧面拉,调整根变换旋转下的偏移选项将纠正它。
-
-
我们的动画剪辑下一个选项是遮罩。通过点击左侧的箭头,你可以展开模型中所有对象的列表。每个对象旁边都有一个复选框。当播放这个剪辑时,未被勾选的对象将不会动画化。这对于挥手动画非常有用。这种动画只需要移动手臂和手,因此我们会取消勾选构成角色身体的所有对象。然后我们可以叠加动画,让角色在站立、行走或奔跑时挥手,而无需创建三个额外的动画。
-
曲线选项将允许你向动画中添加一个浮点值,这个值将在动画过程中改变。当动画播放时,你的代码可以检查这个值。这可以用来调整角色跳跃时受重力的影响,当他们蹲下成球体时改变碰撞器的大小,或者做许多其他的事情。
-
事件的工作原理类似于我们在
RepeatButton
脚本中使用SendMessage
函数的方式。在你的动画中的特定时刻,可以调用一个函数来执行某些操作。 -
运动选项允许你定义动画中的哪个骨骼控制模型的运动。这可以覆盖在绑定标签上选择的骨骼。我们的目标物不会移动,所以这对于我们的情况来说并不是特别相关。
-
最后,我们在底部有恢复按钮、应用按钮和预览窗口。就像我们所有其他导入设置一样,当我们进行更改时,必须点击这些按钮之一。这个预览窗口的特殊之处在于右上角的速度滑块和左上角的大播放按钮。点击这个按钮,我们可以预览选定的动画。这让我们可以检测到我们之前讨论的动作中的错误,并确保动画就是我们想要的效果。
当我们在 Unity 中处理动画时,有许多设置可供我们使用。它们允许我们控制想要导入的原始动画程序中的帧数。此外,它们还可以用来控制动画如何与你的脚本交互。无论你选择什么设置,最重要的是动画剪辑的名称。如果没有设置这个,当你需要处理几个名称相同的动画时,可能会变得极其困难。
目标物的动画
那么,现在我们已经了解了所有的描述,让我们实际用它来制作一些东西。我们将从为目标物设置动画开始。利用我们刚刚获得的知识,我们现在可以如下设置我们的目标物的动画:
-
首先,如果你之前错过了或跳过了,请确保将
Target.blend
和Target.png
文件导入到Targets
文件夹中。此外,在导入设置的Rig页面,确保将Animation Type属性设置为Generic,并将Root Node属性设置为Bone_Arm_Upper。 -
我们总共需要六个动画。在Clips部分点击**+按钮,你可以添加四个更多动画。如果你添加得过多,点击-**按钮来移除多余的剪辑。
-
所有这些剪辑都应该有一个Source Take下拉列表,选择Default Take,所有的Bake into Pose选项都应该勾选,因为目标不会从起始位置移动。
-
首先,让我们创建我们的空闲动画。选择第一个剪辑,并将其重命名为
Idle_Retract
。由于这是一个机械物体,我们可以使用一个非常短的动画;它如此之短,以至于我们只需要使用第一帧。将开始帧设置为0.9
,结束帧设置为1
。 -
我们还需要开启Loop Pose,因为空闲动画当然是循环的。
-
延展空闲动画的创建几乎与上一个完全相同。选择第二个剪辑,并将其重命名为
Idle_Extend
。这里的开始帧是14
,结束帧是14.1
。此外,这个动画需要循环。 -
接下来的两个动画是针对目标展开和缩回的情况。它们将被命名为
Extend
和Retract
,因此请重命名接下来的两个剪辑。Extend
动画将从第1
帧开始,到第13
帧结束。Retract
动画将从第28
帧开始,到第40
帧结束。这两个动画都不会循环。 -
最后两个动画也不会循环。它们是当我们射击目标时使用的。一个是从前面被击中,另一个是从后面被击中。
Hit_Front
动画将从第57
帧到第87
帧。Hit_Back
动画将从第98
帧到第128
帧。 -
一旦完成所有更改,请务必点击Apply,否则更改将不会被保存。
现在我们已经设置好了目标将使用的动画。总共有六个。现在可能看起来不多,但如果没有它们,下一节将无法实现。
使用状态机在 Unity 中控制动画
为了让我们在 Unity 中控制这些新动画,我们需要设置一个状态机。状态机只是一个花哨的对象,用于跟踪一个对象能做什么,以及如何在不同的事物之间进行转换。你可以把它想象成实时策略游戏中的建造者。建造者有一个行走状态,用于移动到下一个建筑工地。当建造者到达那里时,它会切换到建造状态。如果有敌人出现,建造者将进入逃跑状态,直到敌人消失。最后,当建造者什么都不做时,有一个空闲状态。在 Unity 中,当你处理动画和 Mecanim 时,这些被称为 Animator 控制器。
目标状态机
状态机的使用让我们可以更多地关注目标正在做什么,而让 Unity 处理如何做到这一点的部分。执行以下步骤来创建状态机并控制目标:
-
创建一个动画师控制器很简单,这就像我们为脚本和材质所做的那样。该选项位于创建菜单的中间部分。在
Targets
文件夹中创建一个动画控制器,并将其命名为TargetController
。 -
双击
TargetController
打开一个新窗口(如下图所示):动画师窗口是我们编辑状态机的地方。动画师窗口的各个部分如下:
-
在左上角是一个图层按钮。点击它会显示构成你的动画系统的所有可混合图层列表。每个状态机至少会有一个基础图层。添加更多图层可以让我们混合状态机。比如说,如果一个角色在满血时正常行走,当他的血量降到一半以下时,他开始跛行。如果角色只剩下 10%的血量,他开始爬行。这可以通过使用图层来实现,从而避免为每种移动类型创建额外的动画。
-
在它的右边是一个参数按钮,它会显示参数列表。点击**+按钮将在列表中添加一个新参数。这些参数可以是浮点数**,整数,布尔值和触发器。状态之间的转换通常是由这些参数的变化触发的。任何与状态机一起工作的脚本都可以修改这些值。
-
下一个部分像面包屑路径,就像你在网站上可能找到的那样。它让我们一眼就能看到我们在状态机中的位置。
-
右上角的自动实时链接按钮控制我们能够在游戏中实时查看状态机的更新。这对于调试角色转换和控制非常有用。
-
在动画师窗口的中央,有三个框:任何状态,入口和出口。(如果你看不到它们,点击鼠标中键并在网格上拖动以平移视图。)这些框是您的动画状态机的基本控制。任何状态框允许你的对象进入特定的动画,无论它们在状态机的哪个位置,例如,无论玩家正在进行什么操作,都可以移动到死亡动画。入口框在你第一次启动状态机时使用。所有的转换都会被分析,第一个合适以及后续的动画将成为起始位置。出口框主要用于子状态机,并允许你从组中转换出去,而不需要很多额外的复杂连接。
-
-
要创建一个新状态,请在我们的动画师窗口内的网格上点击鼠标右键。将鼠标悬停在创建状态上,然后选择空。这为我们的状态机创建了一个新的空状态。通常,新状态是灰色的,但由于这是我们机器中的第一个状态,所以它是橙色的,这是默认状态的颜色。
-
每个状态机都将从其默认状态开始。点击状态以选择它,我们可以在检查器窗口中查看它(如下截图所示)。
你可以在前面的截图中看到以下字段:
-
在顶部,有一个用于更改状态名称的文本字段。
-
在此之下,你可以添加一个标签以用于组织目的。
-
接下来,有一个速度字段。该字段控制动画的播放速度。
-
动作字段是我们添加到之前创建的动画剪辑连接的地方。
-
足部 IK选项让我们决定是否希望部分动画通过逆运动学(IK)来计算,这是根据末端目标骨骼的位置来计算一系列骨骼如何布局的过程。我们没有为这些动画设置任何 IK,所以不需要担心这个选项。
-
使用写入默认值选项,我们可以控制动画属性在动画结束后是否保持改变。
-
最后一个选项,镜像,用于翻转动画的左右轴(或x轴)。如果你创建了一个右手挥动的动画,这个选项将允许你将其更改为左手挥动的动画。
-
在此之下,是当前状态转变到另一个状态的转换列表。这些都是离开状态而非进入状态的转换。正如你很快将要看到的,此列表中的转换以当前状态的名称开始,向右有一个箭头,后面跟着它所连接的状态名称。
-
右侧的独奏和静音标签下也会出现复选框。这些用于调试状态之间的转换。一次可以静音任意数量的转换,但一次只能独奏一个。当一个转换被静音,意味着状态机在决定要进行的转换时会忽略它。选中独奏框与静音除一个转换之外的所有转换相同;这只是快速使其成为唯一活动转换的方法。
-
-
我们的目标动画将需要各自对应一个状态。因此,再创建五个状态,并将这六个状态重命名为我们之前创建的动画剪辑的名称。默认状态,也就是你创建的第一个状态,在你的屏幕上显示为橙色,应命名为
Idle_Retract
。 -
在项目窗口中,点击目标模型右侧的小三角形(如下截图所示的高亮部分):
这扩展了模型,以便我们可以看到组成该模型的所有对象在 Unity 中的情况。第一组是由实际对象组成的模型。接下来是模型中使用的原始网格。然后是动画剪辑(它们将显示为带有大播放按钮的蓝色框);这些是我们现在感兴趣的内容。最后是一个 Avatar 对象;这是跟踪Rig设置的内容。
-
在你的Animator窗口中选择每个状态,并通过将动画剪辑从Project窗口拖动到Inspector窗口中的Motion字段,将其与正确的剪辑配对。
-
在创建我们的状态转换之前,我们需要几个参数。通过点击左上角的Parameters按钮打开参数列表。然后,点击出现的菜单中的**+按钮,并从菜单中选择Float**。现在应该在列表中显示一个新参数。
-
左侧的新字段是参数名称;你可以通过双击它随时重命名。将这个重命名为
time
。右侧的字段是此参数的当前值。在调试我们的状态机时,我们可以在这里修改这些值以触发状态机的更改。游戏运行时脚本所做的任何更改也会在这里显示。 -
我们还需要两个参数。创建两个Bool参数并将它们重命名为
wasHit
和inTheFront
。这些将触发机器进入被击中的状态,而时间参数将触发机器使用extend
和retract
状态。 -
要创建新的转换,请右键点击一个状态,并从弹出的菜单中选择Make Transition。现在将有一条转换线从状态连接到你的鼠标。要完成转换创建,请点击你希望连接到的状态。线条上会有一个箭头,指示转换的方向。我们需要以下转换:
-
如果你点击其中一个转换线,我们可以查看其设置(如下面的截图所示):
你可以在截图中看到以下内容:
-
在检查器窗口的顶部,我们有与我们在状态中相同的指示器,显示我们正在过渡的状态——过渡开始的状态名称,然后是箭头,最后是过渡结束的状态名称。
-
在熟悉的过渡列表下方,有一个文本字段,我们可以为我们的过渡指定特定的名称。如果我们有几种不同类型的过渡在两个相同的状态之间,这很有用。
-
具有退出时间的复选框决定了过渡是否会在接近动画末尾时等待,然后才切换到下一个动画。这对于像平滑过渡走路和空闲动画这样的情况很有用。
-
在设置下的退出时间中的第一个值设定了过渡开始的时间。这只有在勾选它上面的复选框时才相关。它的值应该从零开始动画,到一结束动画。
-
过渡持续时间设置定义了过渡需要多长时间。它的值也是介于零和一之间。
-
过渡偏移设置定义了过渡将在目标动画的哪个位置开始。
-
中断源和有序中断选项决定了在执行当前过渡的过程中是否可以发生另一个过渡。它们还设置了哪个过渡集具有优先权以及它们将被处理的顺序。
-
接下来是一个时间线块,让我们预览动画之间的过渡。通过拖动小旗子左右移动,我们可以在预览窗口中观看过渡。这个块的顶部显示了表示动画中包含的运动波纹图。下半部分显示了状态作为盒子,在过渡实际发生的地方重叠。这些盒子中的任何一个都可以被拖动以改变过渡的长度。
提示
由于我们两个空闲动画的长度可以忽略不计,这在我们的设置中通常不容易被看到。如果你在
extend
和retract
状态之间创建一个临时过渡,那么它将是可见的。 -
最后,我们有一个条件列表。使用我们设置的参数,我们可以在这里创建任意数量的条件,这些条件必须在过渡发生之前得到满足。
注意
在检查器面板的底部,有另一个预览窗口。它与动画导入设置页面的功能一样,但这个窗口会播放两个相关动画之间的过渡。
-
-
选择
Idle_Retract
状态和Extend
状态之间的过渡。我们希望目标随机弹出。这将由一个脚本来控制,该脚本将改变时间参数。 -
点击条件列表下的**+添加新条件。然后,点击条件中间的箭头来从参数列表中选择时间**。
-
为了将Float值转换为条件语句,我们需要将其与另一个值进行比较。这就是为什么当我们选择参数时,我们会得到一个新的下拉按钮,其中包含比较选项。Float值将大于或小于右侧的值。我们的时间将倒计时,因此从列表中选择Less,并将值保留为零。
-
添加一个条件,以便
Idle_Extend
和Retract
状态之间的转换将是相同的。 -
在
Idle_Extend
状态和Hit_Front
状态之间的转换,我们将使用创建的两个Bool参数。选择转换,并在条件下点击**+**按钮,直到你有两个条件。 -
对于第一个条件,选择wasHit,第二个条件选择inTheFront。Bool参数要么是
true
,要么是false
。在转换的情况下,它需要知道它在等待哪个值。对于这个转换,两者都应该保持为true。 -
接下来,设置
Idle_Extend
和Hit_Back
之间的转换条件,就像你为前一个转换所做的那样。唯一的区别是,在inTheFront条件旁边的下拉列表中选择false
。
这里,我们创建了一个将被我们的目标使用状态机。通过将每个状态链接到一个动画,并将它们全部用转换连接起来,目标将能够切换动画。通过添加条件和参数来控制这种转换。
编写目标脚本
在我们完成目标组合之前,我们只需要一个额外的部分——一个脚本:
-
在我们的
Scripts
文件夹中创建一个新脚本,并将其命名为Target
。 -
首先,为了与我们的状态机交互,我们需要引用
Animator
组件。这是你从坦克和城市中移除的组件。Animator
组件是将所有动画部分联系在一起的部分:public Animator animator;
-
这后面跟着两个浮点值,它们将决定我们的目标在空闲状态下将停留的时间范围,以秒为单位:
public float maxIdleTime = 10f; public float minIdleTime = 3f;
-
接下来,我们有三个值,将保存我们需要更改的参数的 ID 号码。技术上可以使用参数名称来设置它们,但使用 ID 号码要快得多:
private int timeId = -1; private int wasHitId = -1; private int inTheFrontId = -1;
-
最后两个变量将保存两个空闲状态的 ID 号码。我们需要这些来检查我们处于哪个状态。所有 ID 最初都设置为
-1
作为占位值;在下一步中,我们将使用函数将它们设置为实际值:private int idleRetractId = -1; private int idleExtendId = -1;
-
Awake
函数是 Unity 中一个特殊的函数,在游戏开始时对每个脚本进行调用。其目的是在游戏开始之前进行初始化,它非常适合用于最初设置我们的 ID 值。public void Awake() {
-
对于每个 ID,我们调用一次
Animator.StringToHash
函数。这个函数计算我们提供给它的参数或状态的名称的 ID 号码。状态名称还需要加上Base Layer
的前缀。这是因为当可能存在多个不同层,且这些层中有名称相同的状态时,Unity 希望我们能够明确指出。这里的一个非常重要的点是,名称必须与Animator窗口中的名称完全匹配。如果不匹配,ID 将不匹配,会出现错误,并且脚本将无法正确运行。timeId = Animator.StringToHash("time"); wasHitId = Animator.StringToHash("wasHit"); inTheFrontId = Animator.StringToHash("inTheFront"); idleRetractId = Animator.StringToHash("Base Layer.Idle_Retract"); idleExtendId = Animator.StringToHash("Base Layer.Idle_Extend"); }
-
为了利用所有这些 ID,我们求助于我们的好朋友——
Update
函数。在函数的开始,我们使用GetCurrentAnimatorStateInfo
函数来确定当前处于哪个状态。我们向这个函数发送零,因为它想知道我们要查询层的索引,而我们只有一个层。该函数返回一个包含当前状态信息的对象,我们立即获取该状态的nameHash
值(也称为 ID 值),并将我们的变量设置为该值。public void Update() { int currentStateId = animator.GetCurrentAnimatorStateInfo(0).nameHash;
-
下一行代码是将我们的空闲状态 ID 进行比较,以确定我们是否处于这些状态之一。如果是这样,我们调用
SubtractTime
函数(我们稍后会编写)来减少时间参数。if(currentStateId == idleRetractId || currentStateId == idleExtendId) { SubtractTime(); }
-
如果目标当前不在其空闲状态之一,我们首先检查是否被击中。如果是这样,使用
ClearHit
函数清除击中效果,并使用ResetTime
函数重置时间参数。我们稍后会编写这两个函数。最后,我们检查计时器是否已经降到零以下。如果是这样,我们再次重置计时器。else { if(animator.GetBool(wasHitId)) { ClearHit(); ResetTime(); } if(animator.GetFloat(timeId) < 0) { ResetTime(); } } }
-
在
SubtractTime
函数中,我们使用Animator
组件的GetFloat
函数来获取一个浮点参数的值。通过发送我们的timeId
变量,我们可以接收时间参数的当前值。就像我们对坦克所做的那样,然后我们使用Time.deltaTime
来跟上我们的帧率,并从计时器中减去时间。完成此操作后,我们需要将新值传递给状态机,这是通过SetFloat
函数完成的。我们通过给它一个 ID 值来告诉它要更改哪个参数,并通过给我们新的时间值来告诉它要更改什么。public void SubtractTime() { float curTime = animator.GetFloat(timeId); curTime -= Time.deltaTime; animator.SetFloat(timeId, curTime); }
-
接下来要创建的函数是
ClearHit
。这个函数使用Animator
组件的SetBool
来设置布尔参数。它的作用与SetFloat
函数完全一样。我们只需给它一个 ID 和一个值。在这种情况下,我们将两个布尔参数都设置为false
,这样状态机就不再认为它已经被击中。public void ClearHit() { animator.SetBool(wasHitId, false); animator.SetBool(inTheFrontId, false); }
-
脚本要实现的最后一个函数是
ResetTime
。这也是一个简单的函数。首先,我们使用Random.Range
函数来获取一个随机值。通过传递给它一个最小值和最大值,我们新的随机数将位于它们之间。最后,我们使用SetFloat
函数将新值传递给状态机。public void ResetTime() { float newTime = Random.Range(minIdleTime, maxIdleTime); animator.SetFloat(timeId, newTime); }
我们创建了一个脚本来控制我们目标的状体机。为了比较状态和设置参数,我们收集并使用了 ID。现在,不必担心击中状态何时激活。当我们最终让坦克开火时,下一节将对此进行详细说明。
创建预制体
既然我们已经有了模型、动画、状态机和脚本,是时候创建目标并将其转换为预制体了。我们已经拥有所有部件,让我们将它们组合在一起:
-
首先,从Project窗口将Target模型拖动到Hierarchy窗口。这将创建目标对象的新实例。
-
通过选择新的目标对象,我们可以看到它已经附有一个Animator组件;我们只需添加对我们创建的
AnimatorController
的引用。通过将Project窗口中的TargetController
拖动到 Animator 组件的Controller字段中,就像我们迄今为止设置的所有其他对象引用一样。 -
我们还需要将
Target
脚本添加到对象中,并在相关字段中连接到Animator组件的引用。 -
对目标对象的最后一步是添加一个碰撞器,以便实际接收我们的炮弹射击。不幸的是,由于
Target
对象使用骨骼和绑定进行动画,这不像直接在我们射击的网格上添加碰撞器那么简单。相反,我们需要创建一个新的空GameObject
。 -
将其重命名为
TargetCollider
,并将其设置为目标的Bone_Target
骨骼的子对象。 -
在新的 GameObject 上添加一个
MeshCollider
组件。 -
现在,我们需要为此组件提供一些网格数据。在Project窗口中找到Target网格数据,位于Target模型下方。将其拖动到
MeshCollider
组件的Mesh值中。这会在Scene视图中显示一个绿色圆柱体。这是我们的碰撞体,但它尚未与目标对齐。 -
使用Transform组件将GameObject的X值设为
4
,Y和Z值设为0
。旋转需要改为X为0
,Y为-90
,Z为90
。 -
当我们进行更改时,你可能已经注意到所有新内容或更改的内容都变成了粗体。这是为了表示与原始预制体实例相比,这个预制体实例有所不同。请记住,模型本质上是预制体;它们的问题是,我们无法直接进行更改,比如添加脚本。要将此目标变成新的预制体,只需从Hierarchy窗口中将其拖动并放到Project窗口中的
Prefabs
文件夹中。 -
在这个时髦的新预制体创建之后,用它来填充城市。
-
当你放置了所有这些目标时,你可能注意到它们有点大。我们不需要单独编辑每个目标,甚至也不需要将它们作为一个组来编辑,只需对原始预制件进行更改。在项目窗口中选择
Target
预制件。检查器窗口会显示与场景中任何其他对象相同的根预制对象信息。选中我们的预制件后,场景中已经存在的所有实例将自动更新以匹配缩放一半的预制件。我们还可以更改最小和最大空闲时间,并使其影响整个场景。
我们刚刚完成了坦克目标的创建。利用 Unity 的预制系统,我们可以在整个游戏中复制目标,并且轻松地进行影响所有目标的更改。
如果你希望其中一个目标比其他所有目标都大,你可以在场景中更改它。对预制实例所做的任何更改都会被保存,并且优先于对根预制对象所做的更改。此外,当你在检查器窗口中查看实例时,窗口顶部将出现三个新按钮。选择按钮在项目窗口中选择根预制对象。恢复按钮将移除对此实例所做的所有独特更改,而应用按钮则会将此实例中所有更改更新到根对象。
使用你所学的关于动画和状态机的所有知识,这里的挑战是创建第二种类型的目标。尝试不同的移动和行为。你可以创建一个从四处挥动过渡到静止不动的目标。
从光线追踪到射击
现在玩这个游戏,它非常酷。我们有可驾驶的坦克和带有纹理的城市。我们甚至有花哨的动画目标。我们只缺少一样东西:我们如何射击?我们还需要制作一个脚本,这样我们就可以尽情地射击目标了。按照以下步骤创建脚本并设置它:
-
首先,我们需要在坦克中添加一个空的
GameObject
。将其重命名为MuzzlePoint
,并将其设置为炮塔枢轴点对象的子对象。完成此操作后,将其定位在炮管末端,使蓝色箭头指向远离坦克的方向,与炮管同一方向。这将是我们子弹发射的点。 -
我们还需要一些东西来指示我们的射击位置。爆炸效果将在后续章节中介绍,所以从GameObject下的3D Object菜单中选择Sphere,并将其重命名为
TargetPoint
。 -
将球体的每个轴的比例设置为
0.2
,并给它一个红色的材质。这样,它就可以更容易地被看到,而不会完全突兀。它在我们场景中的起始位置并不重要,我们下一个脚本会在我们射击时移动它。 -
从
TargetPoint
中移除SphereCollider
组件。必须移除SphereCollider
,因为我们不希望射击我们自己的目标指示器。 -
现在,创建一个新脚本,并将其命名为
FireControls
。 -
这应该开始让你感到熟悉了。我们从变量开始,这些变量用于保存对我们刚刚创建的枪口和瞄准物体的引用。
public Transform muzzlePoint; public Transform targetPoint;
-
Fire
函数首先定义一个变量,用于保存被射击物体的详细信息:public void Fire() { RaycastHit hit;
-
这后面跟着一个检查
Physics.Raycast
函数的if
语句。Raycast
函数的工作原理就像射击枪一样。我们从一个位置(枪口点的位置)指向一个特定的方向(沿着蓝色轴相对于枪口点向前)并获取击中的物体。如果我们击中某物,if
语句计算结果为true
;否则,它是false
,我们会跳过。if(Physics.Raycast(muzzlePoint.position, muzzlePoint.forward, out hit)) {
-
当我们击中某物时,首先将我们的目标点移动到被击中的点。然后我们使用
SendMessage
函数告诉被击中的物体它已经被击中,这与之前在RepeatButton
脚本中使用的方式相同。我们使用hit.transform.root.gameObject
来获取被击中的 GameObject。同时我们还提供一个值hit.point
,告诉物体被击中的位置。代码行中的SendMessageOptions.DontRequireReceiver
部分使得如果找不到预期的函数,函数不会抛出错误。我们的目标拥有这个函数,但城墙没有,如果不用这个参数,城墙会抛出错误。targetPoint.position = hit.point; hit.transform.root.gameObject.SendMessage("Hit", hit.point, SendMessageOptions.DontRequireReceiver); }
-
如果我们的
Fire
函数没有击中任何物体,最后一部分就会发生。我们将目标点发送回世界原点,这样玩家就知道他们什么都没有击中:else { targetPoint.position = Vector3.zero; } }
-
需要添加的最后一样东西是
Target
脚本末尾的Hit
函数。我们首先获取当前状态 ID,就像之前在脚本中所做的那样。但这次我们只检查是否与扩展的空闲 ID 匹配。如果不匹配,我们使用return
提前退出函数。这样做是因为我们不想让玩家射击那些已经倒下或处于过渡中的目标。如果我们的状态正确,我们继续通过使用SetBool
函数告诉动画我们被击中了。public void Hit(Vector3 point) { int currentStateId = animator.GetCurrentAnimatorStateInfo(0).nameHash; if(currentStateId != idleExtendId) return; animator.SetBool(wasHitId, true);
-
Hit
函数的其余部分要确定目标从哪一侧被击中。为此,我们首先必须将从世界空间接收到的点转换成局部空间。我们的Transform组件的InverseTransformPoint
函数可以很好地完成这个工作。然后我们进行检查,看射击来自哪个方向。由于目标的构建方式,如果x轴上的射击点是正的,那么它来自后面;否则,它来自前面。无论如何,我们都要将状态机中的inTheFront
参数设置为正确的值。然后,通过增加我们在章节开始时在ScoreCounter
脚本中创建的静态变量,给玩家一些分数:Vector3 localPoint = transform.InverseTransformPoint(point); if(localPoint.x > 0) { animator.SetBool(inTheFrontId, false); ScoreCounter.score += 5; } else { animator.SetBool(inTheFrontId, true); ScoreCounter.score += 10; } }
-
接下来,我们需要将新的
FireControls
脚本添加到坦克上。你还需要连接到MuzzlePoint
和TargetPoint
对象的引用。 -
最后,我们需要创建一个新的按钮来控制和触发这个脚本。所以,导航到GameObject | UI | Button,并将按钮重命名为
Fire
。 -
接下来,我们需要点击按钮检查器窗口右下角的小加号,并为对象槽选择
Tank
,就像我们为井字游戏所做的那样。然后,从函数下拉菜单中导航到FireControls | Fire ()。
我们创建了一个脚本,允许我们发射坦克的炮弹。使用射线追踪的方法是最简单且应用最广泛的。通常,子弹飞行速度太快,我们无法看到它们。射线追踪就是这样,即瞬间完成。然而,这种方法没有考虑重力,或者任何可能改变子弹方向的其他因素。
现在所有的按钮和组件都就位了,让它们看起来更好一些。使用你在上一章学到的技能来设计 GUI,让它看起来很棒。也许你甚至可以设法创建一个方向控制板来控制移动。
总结
就这样!这一章节内容很多,我们学到了不少东西。我们导入了网格并设置了一辆坦克。我们创建了材质,为城市添加了颜色。我们还制作了一些目标的动画,并学会了如何将它们击落。内容很多,现在是休息的时候了。玩玩游戏,射击一些目标,收集那些分数。项目已经全部完成,可以在你选择的设备上构建了。构建过程与前两个项目相同,所以尽情享受吧!
下一章将介绍特殊的相机效果和光照。我们将学习关于灯光及其类型。我们的坦克大战游戏将通过添加天空盒和几种灯光来进行扩展。我们还将看看距离雾效。随着阴影和光照图的加入,我们战斗的城市变得真正有趣和生动起来。